How webpack’s ContextReplacementPlugin works

It took me quite a long time of using ContextReplacementPlugin to finally realize what it really does. I hope this post will save you from that.

Once in a while, you need to write a dynamic import. A dynamic import is when an imported file is only known at runtime:

require('./inputs/' + inputType + '/index.js');

When you’re bundling this with webpack, webpack can’t know what exact file you’ll need. To make the application work, webpack will find and import all index.js files in all subdirectories of the inputs directory, recursively. This can significantly increase your bundle size.

ContextReplacementPlugin lets you change how webpack deals with dynamic imports. In this case, you can use it to narrow the search scope and thus cut the bundle size.

Here’s an example:

ContextReplacementPlugin is often used with moment.js, a library for working with dates. The thing with moment.js is that it has a bunch of locales, and it imports them dynamically on runtime:

// moment.js
require('./locale/' + name + '.js');

To make this work, webpack imports all the locales it can find – which adds 330 kB of non-minified code.

In most cases, you only need to support a few locales and don’t need all the other ones. ContextReplacementPlugin lets you specify the specific locales that webpack should import:

// webpack.config.js
const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.ContextReplacementPlugin(
      /moment[/\]locale/,
      // Regular expression to match the files
      // that should be imported
      /(en-gb|ru).js/
    )
  ]
};

That works, that’s copy-pasteable, but that’s cryptic. Let’s see what all these parameters really mean.

Context and how to replace it#

Each time you compile a dynamic import like this:

require('./locale/' + name + '.js');

webpack collects three pieces of information:

  • in what directory the files are located (in this case, it’s ./locale),
  • what regular expression should a file match to be imported (in this case, it’s /^.*.js$/),
  • and if webpack should look for the files in the subdirectories (recursive flag; always true by default).

These three pieces of information are called context.

Then, webpack searches the appropriate files using the context. That means it looks into the directory specified in the context, looks into its subdirectories if the flag from the context is true, and uses the regular expression from the context to match and import the files.

Now, the key point. ContextReplacementPlugin lets you replace the context that webpack uses for search – e.g. override the directory in which webpack should look for the files or the regular expression that webpack should use to match the files.

For example, with the moment.js example from above, ContextReplacementPlugin replaces the original regular expression /^.*.js$/ with another expression we pass – /(en-gb|ru).js/. This way, we import only the necessary locales.

To replace the context, you pass the parts you want to replace as the last parameters of ContextReplacementPlugin:

new webpack.ContextReplacementPlugin(
  // The criterion to search; we’ll get to it in a moment
  searchCritetion: RegExp,
  // The new directory to look for the files
  [newDirectory]: String,
  // The new recursive flag. True by default.
  // Pass false to disable recursive lookup
  [newRecursiveFlag]: Boolean,
  // The new regular expression to match
  // and import the files
  [newFilesRegExp]: RegExp
)

Examples#

If you don’t fully understand how webpack generates context, here’re examples:

Finding the original context to replace#

So, you’ve got a case when you have a dynamic import, and you need to replace its context with another one. How do you find this import and pass it to the plugin? You search by the context that the import generates.

ContextReplacementPlugin allows you to search by the context’s directory. This means that if you want to find an import, you should calculate what context it creates, and then pass a regular expression matching that directory to the context. That sounds complex, so let’s just see how it works:

You have an import like this:

require('./locale/' + name + '.js')

and you want to apply ContextReplacementPlugin to it to limit the number of the included files.

Step 1. Calculate the context that will be created by the import
With this import, the context has three parts:

  • directory, which is ./locale,
  • the regular expression to match the files, which is /^.*.js$/.
  • and the recursive flag (always true).

Step 2. Extract the directory from the context
Here, the directory from the original context is //./locale//.

Step 3. Find the full absolute path of the directory
On your machine, the full path of the directory could be something like '/usr/…/my-project/node_modules/moment/locale'.

Step 4. Create a regular expression that matches the path
This could be as simple as /locale/. Or this could be something more specific, like /moment[/\]locale/ (cross-platform version of /moment/locale/).

I recommend to be as specific as possible: a simple regular expression, like /locale/, can unexpectedly match other imports, like require('./images/flags/locale/' + localeName).

Step 5. Pass the regular expression as the first parameter of ContextReplacementPlugin
Like this:

new webpack.ContextReplacementPlugin(
  /moment[/\]locale/,
  …
)

NormalModuleReplacementPlugin#

Note: ContextReplacementPlugin works only with dynamic imports. If you ever want to configure a redirect for a normal, non-dynamic import, use NormalModuleReplacementPlugin. It works with static imports (only with them), and it’s way simpler to understand.

Bonus point: webpack 2#

The traditional usage of ContextReplacementPlugin is to replace one context with another context. However, webpack 2 brought a new API that you can use to granularly redirect imports:

new ContextReplacementPlugin(
  /moment[/\]locale/,
  path.resolve(__dirname, 'src'),
  {
    './en.js': './dir1/en.js',
    './ru.js': './dir2/ru.js',
  }
)

The code above:

  • will add two files, ./src/dir1/en.js and ./src/dir2/ru.js, to the bundle,
  • will redirect all runtime requests from node_modules/moment/locale/en.js to dir1/en.js,
  • will redirect all runtime requests from node_modules/moment/locale/ru.js to dir2/ru.js.

This is something that can’t be achieved with traditional ContextReplacementPlugin. Unfortunately, this API is only briefly mentioned in a GitHub issue.

When is this helpful? I can only think of some cases when a large existing project is being migrated to webpack – but I haven’t seen any practical example. If you know any, share them in the comments!

Here’s how you write your own redirection:

new ContextReplacementPlugin(
  // Specify the criterion for search
  /moment[/\]locale/,
  
  // Specify any directory that’s common
  // for all the redirection targets.
  // It can’t be __dirname: for some reason,
  // that doesn’t work
  path.resolve(__dirname, 'src'),
  
  // Specify the mapping in form of
  // { runtime request : compile-time request }
  // IMPORTANT: runtime request should exactly match
  // the text that is passed into `require()`
  // IMPORTANT: compile-time request should be relative
  // to the directory from the previous parameter
  {
    './en.js': './dir1/en.js',
    './ru.js': './dir2/ru.js',
  }
)

Σ#

The key points:

  • Each time you do a dynamic import, webpack creates a context. Context is an object containing the directory to look into, the recursive flag and the regular expression for matching files.

  • Use ContextReplacementPlugin to change how webpack handles dynamic imports. This can be helpful when you need to decrease the bundle size or migrate some complex code to webpack.

  • You can granularly redirect compile-time requests to the different files with webpack 2.

Further reading#

Here’re some related links:


This is an article in my series of articles about webpack. The next one, “Speeding up build and improving the development workflow”, is coming in July. Leave your email to know when it’s out:
(you’ll receive an email about this post + a couple of more webpack-related posts if I write them; no spam)

3 thoughts on “How webpack’s ContextReplacementPlugin works”

Leave a Reply

Your email address will not be published.