The webpack
compiler can understand modules written as ES2015 modules, CommonJS or AMD. However, some third party libraries may expect global dependencies (e.g. $
for jQuery
). The libraries might also create globals which need to be exported. These "broken modules" are one instance where shimming comes into play.
We don't recommend using globals! The whole concept behind webpack is to allow more modular front-end development. This means writing isolated modules that are well contained and do not rely on hidden dependencies (e.g. globals). Please use these features only when necessary.
Another instance where shimming can be useful is when you want to polyfill browser functionality to support more users. In this case, you may only want to deliver those polyfills to the browsers that need patching (i.e. load them on demand).
The following article will walk through both of these use cases.
For simplicity, this guide stems from the examples in Getting Started. Please make sure you are familiar with the setup there before moving on.
Let's start with the first use case of shimming global variables. Before we do anything let's take another look at our project:
project
webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
|- /node_modules
Remember that lodash
package we were using? For demonstration purposes, let's say we wanted to instead provide this as a global throughout our application. To do this, we can use the ProvidePlugin
.
The ProvidePlugin
makes a package available as a variable in every module compiled through webpack. If webpack sees that variable used, it will include the given package in the final bundle. Let's go ahead by removing the import
statement for lodash
and instead providing it via the plugin:
src/index.js
- import _ from 'lodash';
-
function component() {
var element = document.createElement('div');
- // Lodash, now imported by this script
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
- }
+ },
+ plugins: [
+ new webpack.ProvidePlugin({
+ lodash: 'lodash'
+ })
+ ]
};
What we've essentially done here is tell webpack...
If you encounter at least one instance of the variable
lodash
, include thelodash
package and provide it to the modules that need it.
If we run a build, we should still see the same output:
TODO: Include output
We can also use the ProvidePlugin
to expose a single export of a module by configuring it with an "array path" (e.g. [module, child, ...children?]
). So let's imagine we only wanted to provide the join
method from lodash
wherever it's invoked:
src/index.js
function component() {
var element = document.createElement('div');
- element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ element.innerHTML = join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new webpack.ProvidePlugin({
- lodash: 'lodash'
+ join: ['lodash', 'join']
})
]
};
This would go 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 this is the case:
function component() {
var 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 is executed in a CommonJS context where this
is equal to module.exports
. In this case you can override this
using the imports-loader
:
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: require.resolve('index.js'),
+ use: 'imports-loader?this=>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:
project
webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
+ |- globals.js
|- /node_modules
src/globals.js
var file = 'blah.txt';
var helpers = {
test: function() { console.log('test something'); },
parse: function() { console.log('parse something'); }
}
Now, while you'd likely never do this in your own source code, you may encounter a dated library you'd like to use that contains similar code to what's shown above. In this case, we can use exports-loader
, to export that global variable as a normal module export. For instance, in order to export file
as file
and helpers.parse
as parse
:
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: require.resolve('index.js'),
use: 'imports-loader?this=>window'
- }
+ },
+ {
+ test: require.resolve('globals.js'),
+ use: 'exports-loader?file,parse=helpers.parse'
+ }
]
},
plugins: [
new webpack.ProvidePlugin({
join: ['lodash', 'join']
})
]
};
Now from within our entry script (i.e. src/index.js
), we could import { file, parse } from './globals.js';
and all should work smoothly.
Almost everything we've discussed thus far has been in relation to handling legacy packages. Let's move on to our second topic: polyfills.
There's a lot of ways to load polyfills. For example, to include the babel-polyfill
we might simply:
npm i --save babel-polyfill
and import
it so as to include it in our main bundle:
src/index.js
+ import 'babel-polyfill';
+
function component() {
var element = document.createElement('div');
element.innerHTML = join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
Note that we aren't binding theimport
to a variable. This is because polyfills simply run on their own, prior to the rest of the code base, allowing us to then assume certain native functionality exists.
Now while this is one approach, including polyfills in the main bundle is not recommended because this penalizes modern browsers users by making them download a bigger file with unneeded scripts.
Let's move our import
to a new file and add the whatwg-fetch
polyfill:
npm i --save whatwg-fetch
src/index.js
- import 'babel-polyfill';
-
function component() {
var element = document.createElement('div');
element.innerHTML = join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
project
webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
|- globals.js
+ |- polyfills.js
|- /node_modules
src/polyfills.js
import 'babel-polyfill';
import 'whatwg-fetch';
webpack.config.js
const path = require('path');
module.exports = {
- entry: './src/index.js',
+ entry: {
+ polyfills: './src/polyfills.js',
+ index: './src/index.js'
+ },
output: {
- filename: 'bundle.js',
+ filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: require.resolve('index.js'),
use: 'imports-loader?this=>window'
},
{
test: require.resolve('globals.js'),
use: 'exports-loader?file,parse=helpers.parse'
}
]
},
plugins: [
new webpack.ProvidePlugin({
join: ['lodash', 'join']
})
]
};
With that in place, we can add the logic to conditionally load our new polyfills.bundle.js
file. How you make this decision depends on the technologies and browsers you need to support. We'll just do some simple testing to determine whether our polyfills are needed:
dist/index.html
<html>
<head>
<title>Getting Started</title>
+ <script>
+ var modernBrowser = (
+ 'fetch' in window &&
+ 'assign' in Object
+ );
+
+ if ( !modernBrowser ) {
+ var scriptElement = document.createElement('script');
+
+ scriptElement.async = false;
+ scriptElement.src = '/polyfills.bundle.js';
+ document.head.appendChild(scriptElement);
+ }
+ </script>
</head>
<body>
<script src="index.bundle.js"></script>
</body>
</html>
Now we can fetch
some data within our entry script:
src/index.js
function component() {
var element = document.createElement('div');
element.innerHTML = join(['Hello', 'webpack'], ' ');
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. Note that this set up could likely be improved upon but it should give you a good idea of how you can provide polyfills only to the users that actually need them.
The babel-preset-env
package uses browserslist to transpile only what is not supported in your browsers matrix. This preset comes with the useBuiltIns
option, false
by default, which converts your global babel-polyfill
import to 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 repository for more information.
Node built-ins, like process
, can be polyfilled right directly from your configuration file without the use of any special loaders or plugins. See the node configuration page for more information and examples.
There are a few other tools that can help when dealing with legacy modules.
The script-loader
evaluates code in the global context, similar to inclusion via a script
tag. In this mode, every normal library should work. require
, module
, etc. are undefined.
When using thescript-loader
, the module is added as a string to the bundle. It is not minimized bywebpack
, so use a minimized version. There is also nodevtool
support for libraries added by this loader.
When there is no AMD/CommonJS version of the module and you want to include the dist
, you can flag this module in noParse
. This will cause webpack to include the module without parsing it or resolving require()
and import
statements. This practice is also used to improve the build performance.
Any feature requiring the AST, like theProvidePlugin
, will not work.
Lastly, there are some modules that support different module styles like 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 could help to force the CommonJS path by setting define=>false
via the imports-loader
.