The webpack compiler can understand modules written as ES2015 modules, CommonJS, or AMD. However, some third-party libraries expect global dependencies (for example, $ for jQuery). These libraries may also create globals that need to be exported. Such "broken modules" are one case where shimming comes into play.
[!WARNING] We don't recommend using globals! The whole concept behind webpack is to enable more modular front-end development. That means writing isolated modules that are well contained and do not rely on hidden dependencies (such as globals). Please use these features only when necessary.
Another case where shimming is useful is when you want to polyfill browser functionality to support more users. Here, you may want to deliver those polyfills only to the browsers that need patching (that is, load them on demand).
The following article walks through both of these use cases.
[!TIP] For simplicity, this guide builds on the examples from Getting Started. Make sure you're familiar with that setup before moving on.
Let's start with the first use case: shimming global variables. Before doing anything, let's take another look at our project:
webpack-demo
├── package.json
├── package-lock.json
├── webpack.config.js
├── /dist
│ └── index.html
├── /src
│ └── index.js
└── /node_modulesRemember the lodash package we were using? For demonstration purposes, let's say we want to provide it as a global throughout our application instead. To do this, we can use ProvidePlugin.
The ProvidePlugin makes a package available as a variable in every module compiled through webpack. If webpack sees that variable used, it includes the given package in the final bundle. Let's remove the import statement for lodash and provide it through the plugin instead:
-import _ from 'lodash';
-
function component() {
const element = document.createElement('div');
- // Lodash, now imported by this script
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());What we've essentially told webpack is this:
If you encounter at least one instance of the variable
_, include thelodashpackage and provide it to the modules that need it.
If we run a build, we should still see the same output:
$ npm run build
..
[webpack-cli] Compilation finished
asset main.js 69.1 KiB [emitted] [minimized] (name: main) 1 related asset
runtime modules 344 bytes 2 modules
cacheable modules 530 KiB
./src/index.js 191 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.x.x compiled successfully in 2910 msWe can also use ProvidePlugin to expose a single export of a module by configuring it with an "array path" (for example, [module, child, ...children?]). Let's imagine we only want to provide the join method from lodash wherever it's invoked:
function component() {
const element = document.createElement('div');
- element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ element.innerHTML = join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());This pairs nicely with Tree Shaking, as the rest of the lodash library should get dropped.
Some legacy modules rely on this being the window object. Let's update our index.js so that's the case:
function component() {
const element = document.createElement('div');
element.innerHTML = join(['Hello', 'webpack'], ' ');
+ // Assume we are in the context of `window`
+ this.alert("Hmmm, this probably isn't a great idea...");
+
return element;
}
document.body.appendChild(component());This becomes a problem when the module runs in a CommonJS context, where this equals module.exports. In that case, you can override this using the imports-loader:
import path from "node:path";
import webpack from "webpack";
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
+ module: {
+ rules: [
+ {
+ test: import.meta.resolve('./src/index.js'),
+ use: 'imports-loader?wrapper=window',
+ },
+ ],
+ },
plugins: [
new webpack.ProvidePlugin({
join: ['lodash', 'join'],
}),
],
};Let's say a library creates a global variable that it expects its consumers to use. We can add a small module to our setup to demonstrate this:
webpack-demo
├── package.json
├── package-lock.json
├── webpack.config.js
├── /dist
├── /src
│ ├── index.js
+ │ ├── globals.js
└── /node_modulesYou'd likely never write code like this in your own source, but you may come across a dated library that contains something similar. In that case, we can use exports-loader to export that global variable as a normal module export. For instance, to export file as file and helpers.parse as parse:
import path from "node:path";
import webpack from "webpack";
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: import.meta.resolve('./src/index.js'),
use: 'imports-loader?wrapper=window',
},
+ {
+ test: import.meta.resolve('./src/globals.js'),
+ use:
+ 'exports-loader?type=commonjs&exports=file,multiple|helpers.parse|parse',
+ },
],
},
plugins: [
new webpack.ProvidePlugin({
join: ['lodash', 'join'],
}),
],
};Now, from within our entry script (src/index.js), we can use const { file, parse } = require('./globals.js'); and everything should work smoothly.
Almost everything we've discussed so far has dealt with handling legacy packages. Let's move on to our second topic: polyfills.
There are many ways to load polyfills. For example, to include babel-polyfill we might:
npm install --save babel-polyfilland import it so it's included in our main bundle:
+import 'babel-polyfill';
+
function component() {
const element = document.createElement('div');
element.innerHTML = join(['Hello', 'webpack'], ' ');
// Assume we are in the context of `window`
this.alert("Hmmm, this probably isn't a great idea...");
return element;
}
document.body.appendChild(component());[!TIP] Note that we don't bind the
importto a variable. This is because polyfills run on their own, before the rest of the code base, allowing us to assume certain native functionality exists.
Note that this approach prioritizes correctness over bundle size. To be safe and robust, polyfills and shims must run before all other code, so they either need to load synchronously, or all app code needs to load after all polyfills and shims have loaded.
There are many misconceptions in the community: that modern browsers "don't need" polyfills, or that polyfills and shims merely add missing features. In fact, they often repair broken implementations, even in the most modern browsers. The best practice therefore remains to load all polyfills and shims unconditionally and synchronously, despite the bundle-size cost this incurs.
If you've mitigated these concerns and are willing to accept the risk of breakage, here's one way you might do it. Let's move our import to a new file and add the whatwg-fetch polyfill:
npm install --save whatwg-fetchWith that in place, we can add logic to conditionally load our new polyfills.bundle.js file. How you make that decision depends on the technologies and browsers you need to support. We'll do some testing to determine whether our polyfills are needed:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Getting Started</title>
+ <script>
+ const modernBrowser = 'fetch' in window && 'assign' in Object;
+
+ if (!modernBrowser) {
+ const scriptElement = document.createElement('script');
+
+ scriptElement.async = false;
+ scriptElement.src = '/polyfills.bundle.js';
+ document.head.appendChild(scriptElement);
+ }
+ </script>
</head>
<body>
- <script src="main.js"></script>
+ <script src="index.bundle.js"></script>
</body>
</html>Now we can fetch some data within our entry script:
function component() {
const element = document.createElement('div');
element.innerHTML = join(['Hello', 'webpack'], ' ');
// Assume we are in the context of `window`
this.alert("Hmmm, this probably isn't a great idea...");
return element;
}
document.body.appendChild(component());
+
+fetch('https://jsonplaceholder.typicode.com/users')
+ .then((response) => response.json())
+ .then((json) => {
+ console.log(
+ "We retrieved some data! AND we're confident it will work on a variety of browser distributions."
+ );
+ console.log(json);
+ })
+ .catch((error) =>
+ console.error('Something went wrong when fetching this data: ', error)
+ );If we run our build, another polyfills.bundle.js file will be emitted and everything should still run smoothly in the browser. This setup could likely be improved further, but it should give you a good idea of how to provide polyfills only to the users who actually need them.
The babel-preset-env package uses browserslist to transpile only what your browser matrix doesn't support. This preset comes with the useBuiltIns option (false by default), which converts your global babel-polyfill import into a more granular, feature-by-feature import pattern:
import 'core-js/modules/es7.string.pad-start';
import 'core-js/modules/es7.string.pad-end';
import 'core-js/modules/web.timers';
import 'core-js/modules/web.immediate';
import 'core-js/modules/web.dom.iterable';See the babel-preset-env documentation for more information.
Node built-ins, such as process, can be polyfilled directly from your configuration file without any special loaders or plugins. See the node configuration page for more information and examples.
A few other tools can help when dealing with legacy modules.
When a module has no AMD or CommonJS version and you want to include its dist, you can flag the module in noParse. This causes webpack to include the module without parsing it or resolving its import and require() statements. This practice can also improve build performance.
[!WARNING] Any feature requiring the AST, such as
ProvidePlugin, will not work.
Finally, some modules support multiple module styles (for example, a combination of AMD, CommonJS, and legacy). In most of these cases, they first check for define and then use some quirky code to export properties. In these cases, it can help to force the CommonJS path by setting additionalCode=var%20define%20=%20false; via the imports-loader.