31

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?

Brendan Gannon
  • 2,632
  • 15
  • 22

3 Answers3

22

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!

Brendan Gannon
  • 2,632
  • 15
  • 22
  • 1
    So the approach outlined above has major limitations. It works ok if you need to inject arbitrary code into a module's source. However, the parsing does not seem to play nice with the already-compiled modules, e.g. it will not recognize a `require` for a module already in the bundled dependencies. So this isn't a good solution for implementing changes into a build unless (maybe) the new code doesn't use webpack's features. – Brendan Gannon Mar 14 '16 at 15:47
  • Interesting approach. But why not use Webpack's BannerPlugin? – Evi Song Feb 08 '17 at 03:07
  • @EviSong Haven't looked into BannerPlugin much, but the use case was deriving some new source during the build process and then inserting it into the build after all modules have been parsed etc. So the code to insert wouldn't be known until build time, after the compilation. – Brendan Gannon Feb 08 '17 at 21:14
  • I see. BannerPlugin only handles static content. Your scenario is kind of like the extract-text-webpack-plugin but more complex. – Evi Song Feb 09 '17 at 07:37
  • Did you find a good solution? I'm looking into creating a plugin that might add dependencies to a module that was already processed... (I can't control the order). – jods Mar 08 '17 at 19:16
  • @jods I did not find a good solution, no. Not sure it's possible; if it is, it takes a better understanding of webpack internals than I arrived at. I ended up creating a separate asset and treating that as an 'external' dependency via webpack. In my case that might be preferable anyway, as the asset I was generating was fairly large, and it would probably be preferable to download it in parallel rather than add it to the webpack build file. But going that route meant I had to add an additional script tag to the HTML etc., which is awkward. – Brendan Gannon Mar 12 '17 at 18:59
  • @BrendanGannon ok, not sure how I'll proceed but thanks anyway! – jods Mar 12 '17 at 19:43
  • 1
    any news to share ? – Franck Freiburger Sep 26 '17 at 15:24
8

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));
      });
    });
  }
};
Trevor Burnham
  • 76,828
  • 33
  • 160
  • 196
2

Another idea comes to my mind. What if do some preprocessing before webpack compilation?

  1. Leverage babel + a custom plugin, or even a custom code parser based on babylon, to collect data you need, and then save to temp file.
  2. Run webpack build together with this temp file.

This temp file could be a virtual file. May refer to https://github.com/rmarscher/virtual-module-webpack-plugin/ .

... be used if you generated file contents at build time that needs to be consumed as a module by your source code, but you don't want to write this file to disk.

Evi Song
  • 862
  • 11
  • 14
  • 1
    That's not a bad idea; it would probably work for some situations. In our case I don't think we can afford running an additional build, even if we could strip out some of the loaders, plugins, etc. for that pre-build -- our app is a big one and (even with some optimizations in place) it takes a few minutes to complete. – Brendan Gannon Feb 10 '17 at 15:05