Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Webpack plugin: how can I modify and re-parse a module after compilation?

Tags:

webpack

I'm working on a webpack plugin and can't figure out how to modify a module during the build. What I'm trying to do:

  • Collect data via a custom loader (fine)
  • After all modules have been loaded, collect data gathered by my loader (fine)
  • Insert code I generate into an existing module in the build (doing this as described below, not sure if it's the best way)
  • 'update' that module so that the code I added gets parsed and has its 'require's turned into webpack require calls (can't figure out how to do this correctly)

Currently I'm hooking into 'this-compilation' on the compiler, then 'additional-chunk-assets' on the compilation. Grabbing the first chunk (the only one, currently, as I'm still in development), iterating through the modules in that chunk to find the one I want to modify. Then:

  • Appending my generated source to the module's _cachedSource.source._source._value (I also tried appending to the module's ._source._value)
  • setting the ._cachedSource.hash to an empty string (as this seems to be necessary for the next step to work)
  • I pass the module to .rebuildModule()

It looks like rebuildModule should re-parse the source, re-establish dependencies, etc. etc., but it's not parsing my require statements and changing them to webpack requires. The built file includes my modified source but the require('...') statements are unmodified.

How can I make the module I modified 'update' so that webpack will treat my added source the same as the originally parsed source? Is there something I need to do in addition to rebuildModule()? Am I doing this work too late in the build process? Or am I going about it the wrong way?

like image 593
Brendan Gannon Avatar asked Jan 29 '16 19:01

Brendan Gannon


2 Answers

I figured out how to do this in a pretty painless fashion.

Things I had wrong:

  • probably hooking in too late? the earliest plugin where you can accomplish this is the compilation's 'seal' plugin. Despite the name, this plugin hook executes as the very first line in the seal function, so no sealing has yet occurred. At this point all the modules have been loaded.
  • rebuildModule() isn't a good idea, because this re-loads the module from scratch: the file's source is loaded and passed through any applicable loaders, and the _source property of the module object is eventually reassigned when that process is finished.
    • Using rebuildModule() at this point would actually be great if there were a way to modify the module source as it was being loaded in this call (i.e. dynamically assign a loader function that's only used on this rebuild). We'd then be able to take advantage of the sourceMap behavior that happens when a module's source is loaded (see below)

How I got it working:

  • hook into compilation's 'seal' plugin, iterate through the compilation's modules and find the one you want
  • modify the module's source, e.g. module._source._value += extraCode;
  • reparse the module:

    module.parse.parse(module._source.source(), {   current: module,    module.module,   compilation: compilation,   options: compilation.options }); 

The parsing is taken from NormalModule's build method, which is called more or or less immediately after the source has been loaded during the normal module build process.

This implementation gets the modified and parsed source into my final output. Since there's some sourceMap stuff in NormalModuleMixin's doBuild method, and since I'm adding to the source after those functions have been called, I assume the sourceMap will be messed up now. So, next step is getting the sourceMap to reflect the code addition. Not sure whether to try and manually update the sourceMap or look into the idea above, trying to dynamically apply a loader and call rebuildModule() instead of parsing.

If you know a better way of doing any of the above, please let me know!

like image 93
Brendan Gannon Avatar answered Sep 18 '22 03:09

Brendan Gannon


Based on looking at how Webpack's official plugins (such as DefinePlugin) modify module code, I believe the best way to do this is:

  1. Create a custom "dependency" class and a corresponding "template" class.
  2. Attach an instance of the dependency class to each module, e.g. in response to buildModule, with module.addDependency().
  3. Register the dependency template with compilation.dependencyTemplates.set().
  4. In the template's apply method, use source.replace() or source.insert() to make your modifications (where source is the second argument)—see the ReplaceSource docs.

In terms of compilation hooks, the templates are invoked immediately after beforeChunkAssets. Modifying the source in this way preserves SourceMaps.

Example for Webpack 4

const Dependency = require('webpack/lib/Dependency');  class MyDependency extends Dependency {   // Use the constructor to save any information you need for later   constructor(module) {     super();     this.module = module;   } }  MyDependency.Template = class MyDependencyTemplate {   apply(dep, source) {     // dep is the MyDependency instance, so the module is dep.module     source.insert(0, 'console.log("Hello, plugin world!");');   } };  module.exports = class MyPlugin {   apply(compiler) {     compiler.hooks.compilation.tap('MyPluginName', compilation => {       compilation.dependencyTemplates.set(         MyDependency,         new MyDependency.Template()       );       compilation.hooks.buildModule.tap('MyPluginName', module => {         module.addDependency(new MyDependency(module));       });     });   } }; 
like image 44
Trevor Burnham Avatar answered Sep 18 '22 03:09

Trevor Burnham