Webpack’s shimming feature is fundamentally about injecting code into your bundle to make modern JavaScript features available in older environments, effectively tricking newer code into thinking legacy APIs exist.
Let’s see this in action. Imagine you’re using a library that relies on Promise.prototype.finally, but your target environment (say, an older Node.js version or a very old browser) doesn’t support it. Webpack can help.
Here’s a simplified webpack.config.js:
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
},
],
},
// This is where the magic happens for older environments
resolve: {
fallback: {
"promise": require.resolve("promise/polyfill.js")
}
}
};
And your src/index.js:
// This code assumes Promise.prototype.finally exists
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Success!');
}, 1000);
});
myPromise.finally(() => {
console.log('This will run regardless of resolution or rejection.');
});
console.log('Promise setup complete.');
When you run npx webpack, the bundle.js in the dist directory will contain the necessary code. If you were to run this bundle.js in an environment without Promise.prototype.finally, the require.resolve("promise/polyfill.js") part tells Webpack to find the polyfill.js file within the promise npm package and inject its contents into the bundle. This polyfill provides the missing finally method, allowing myPromise.finally(...) to execute without error.
The core problem Webpack shimming addresses is feature disparity between development environments and deployment environments. When you write code using modern JavaScript syntax or APIs, you expect it to work everywhere. However, if your users are on older browsers or Node.js versions that haven’t kept up with the ECMAScript standard, your code will break. Shimming allows you to bundle the necessary "shims" – small pieces of code that provide the missing functionality – directly into your application’s JavaScript.
Internally, Webpack uses the resolve.fallback (or the older node.fallback in Webpack 5+, and node in Webpack 4 and earlier) configuration to achieve this. When Webpack encounters a require() or import() statement for a module that doesn’t exist in the target environment, but is listed in resolve.fallback, it replaces that dependency with the specified shim. This effectively "polyfills" the module or global API. The require.resolve() function is crucial here; it finds the absolute path to the specified file on your system, which Webpack then includes in the bundle.
You can shim not just global APIs like Promise but also entire Node.js modules that might not be available in a browser environment. For instance, if you’re using a library that requires the crypto module and you’re bundling for the browser, you might configure resolve.fallback: { "crypto": require.resolve("crypto-browserify") }. Webpack will then bundle crypto-browserify and make it available under the name crypto to your code. The key is understanding which APIs your code uses and which environments lack them, then finding appropriate npm packages that provide those missing pieces.
The most counterintuitive aspect of shimming is that you’re often telling Webpack to not use a built-in Node.js module or a browser API, but rather a separate, often much smaller, npm package that mimics its behavior. For example, if you’re targeting a very old browser that lacks Buffer, you might set resolve.fallback: { "buffer": require.resolve("buffer/") }. This replaces the expectation of a native Buffer with the buffer package’s implementation, ensuring your code that relies on Buffer operations doesn’t fail. It’s a form of dependency injection at the module resolution level.
The next hurdle you’ll face is managing the size impact of these shims, as they can significantly increase your bundle size.