diff --git a/src/content/guides/shimming.md b/src/content/guides/shimming.md index 2976c0dcc40c..e3c42c159986 100644 --- a/src/content/guides/shimming.md +++ b/src/content/guides/shimming.md @@ -13,233 +13,434 @@ related: url: https://github.com/babel/babel-preset-env#usebuiltins --- -`webpack` as a module bundler can understand modules written as ES2015 modules, CommonJS or AMD. But many times, while using third party libraries, we see that they expect dependencies which are global, AKA `$` for `jquery`. They might also be creating global variables which need to be exported. Here we will see different ways to help webpack understand these __broken modules__. +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. +W> __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. -## Prefer unminified CommonJS/AMD files over bundled `dist` versions. +Another instance where _shimming_ can be useful is when you want to [polyfill](https://en.wikipedia.org/wiki/Polyfill) browser functionality to support more users. In this case, you may only want to deliver those polyfills to the browsers that need patch (i.e. load them on demand). -Most modules link the `dist` version in the `main` field of their `package.json`. While this is useful for most developers, for webpack it is better to alias the src version because this way webpack is able to optimize dependencies better. However in most cases `dist` works fine as well. +The following article will walk through both of these use cases. -``` javascript -// webpack.config.js +T> For simplicity, this guide stems from the examples in [Getting Started](/guides/getting-started). Please make sure you are familiar with the setup there before moving on. -module.exports = { - ... - resolve: { - alias: { - jquery: "jquery/src/jquery" - } - } -}; + +## Shimming Globals + +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__ + +``` diff +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`. -## `ProvidePlugin` +The [`ProvidePlugin`](/plugins/provide-plugin) 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: -The [`ProvidePlugin`](/plugins/provide-plugin) makes a module available as a variable in every other module required by `webpack`. The module is required only if you use the variable. -Most legacy modules rely on the presence of specific globals, like jQuery plugins do on `$` or `jQuery`. In this scenario, you can configure webpack to prepend `var $ = require(“jquery”)` every time it encounters the global `$` identifier. +__src/index.js__ -```javascript -module.exports = { - plugins: [ - new webpack.ProvidePlugin({ - $: 'jquery', - jQuery: 'jquery' - }) - ] -}; +``` diff +- 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()); ``` -This plugin is also capable of providing only a certain export of a module by configuring it with an array path using this format: `[module, child, ...children?]`. +__webpack.config.js__ -The following configuration will correctly import function `__assign` from TypeScript's `tslib` package, and provide it wherever it's invoked. +``` diff + 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' ++ }) ++ ] + }; +``` -```javascript -module.exports = { - plugins: [ - new webpack.ProvidePlugin({ - __assign: ['tslib', '__assign'], - __extends: ['tslib', '__extends'], - }) - ] -}; +What we've essentially done here is tell webpack... + +> If you encounter at least one instance of the variable `lodash`, include the `lodash` package and provide it to the modules that need it. + +If we run a build, we should still see the same output: + +``` bash +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: -## `imports-loader` +__src/index.js__ -[`imports-loader`](/loaders/imports-loader/) inserts necessary globals into the required legacy module. -For example, Some legacy modules rely on `this` being the `window` object. This becomes a problem when the module is executed in a CommonJS context where `this` equals `module.exports`. In this case you can override `this` using the `imports-loader`. +``` diff + function component() { + var element = document.createElement('div'); -__webpack.config.js__ +- element.innerHTML = _.join(['Hello', 'webpack'], ' '); ++ element.innerHTML = join(['Hello', 'webpack'], ' '); -```javascript -module.exports = { - module: { - rules: [{ - test: require.resolve("some-module"), - use: 'imports-loader?this=>window' - }] + return element; } -}; -``` -There are modules that support different [module styles](/concepts/modules), like AMD, CommonJS and legacy. However, most of the time 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`: + document.body.appendChild(component()); +``` __webpack.config.js__ -```javascript -module.exports = { - module: { - rules: [{ - test: require.resolve("some-module"), - use: 'imports-loader?define=>false' - }] - } -}; +``` diff + 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](/guides/tree-shaking) as the rest of the `lodash` library should get dropped. -## `exports-loader` -Let's say a library creates a global variable that it expects its consumers to use; In this case, we can use [`exports-loader`](/loaders/exports-loader/), to export that global variable in CommonJS format. For instance, in order to export `file` as `file` and `helpers.parse` as `parse`: +## Granular Shimming -__webpack.config.js__ +Some legacy modules rely on `this` being the `window` object. Let's update our `index.js` so this is the case: -```javascript -module.exports = { - module: { - rules: [{ - test: require.resolve("some-module"), - use: 'exports-loader?file,parse=helpers.parse' - // adds below code the file's source: - // exports["file"] = file; - // exports["parse"] = helpers.parse; - }] +``` diff + 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`](/loaders/imports-loader/): -## `script-loader` +__webpack.config.js__ -The [`script-loader`](/loaders/script-loader/) evaluates code in the global context, just like you would add the code into a `script` tag. In this mode, every normal library should work. `require`, `module`, etc. are undefined. +``` diff + 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'] + }) + ] + }; +``` -W> The file is added as string to the bundle. It is not minimized by `webpack`, so use a minimized version. There is also no dev tool support for libraries added by this loader. -Assuming you have a `legacy.js` file containing … +## Global Exports -```javascript -GLOBAL_CONFIG = {}; +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__ + +``` diff + webpack-demo + |- package.json + |- webpack.config.js + |- /dist + |- /src + |- index.js ++ |- globals.js + |- /node_modules ``` -… using the `script-loader` … +__src/globals.js__ -```javascript -require('script-loader!legacy.js'); +``` js +var file = 'blah.txt'; +var helpers = { + test: function() { console.log('test something'); }, + parse: function() { console.log('parse something'); } +} ``` -… basically yields: +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`](/loaders/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`: -```javascript -eval("GLOBAL_CONFIG = {};"); +__webpack.config.js__ + +``` diff + 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. -## `noParse` option -When there is no AMD/CommonJS version of the module and you want to include the `dist`, you can flag this module as [`noParse`](/configuration/module/#module-noparse). Then `webpack` will just include the module without parsing it, which can be used to improve the build time. +## Loading Polyfills -W> Any feature requiring the AST, like the `ProvidePlugin`, will not work. +Almost everything we've discussed thus far has been in relation to handling legacy packages. Let's move on to our second topic: __polyfills__. -```javascript -module.exports = { - module: { - noParse: /jquery|backbone/ +There's a lot of ways to load polyfills. For example, to include the [`babel-polyfill`](https://babeljs.io/docs/usage/polyfill/) we might simply: + +``` bash +npm i --save babel-polyfill +``` + +and `import` it so as to include it in our main bundle: + +__src/index.js__ + +``` diff ++ import 'babel-polyfill'; ++ + function component() { + var element = document.createElement('div'); + + element.innerHTML = join(['Hello', 'webpack'], ' '); + + return element; } -}; + + document.body.appendChild(component()); ``` +T> Note that we aren't binding the `import` 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. -## Node Built-Ins +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. -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](/configuration/node) for more information and examples. +Let's move our `import` to a new file and add the [`whatwg-fetch`](https://github.com/github/fetch) polyfill: +``` bash +npm i --save whatwg-fetch +``` -## Loading polyfills on demand +__src/index.js__ -It's common in web projects to include polyfills in the main bundle. This is not recommended because we are penalizing modern browsers users by making them download a bigger file with unneeded scripts. +``` diff +- import 'babel-polyfill'; +- + function component() { + var element = document.createElement('div'); -The simplest way to mitigate this is by adding a separate entry point in your webpack config file including the polyfills your project needs. + element.innerHTML = join(['Hello', 'webpack'], ' '); -```javascript -// webpack.config.js -module.exports = { - entry: { - polyfills: [ - 'babel-polyfill', - 'whatwg-fetch' - ], - main: './src/index.js' + return element; } - // ... rest of your webpack config -}; + + document.body.appendChild(component()); ``` -An alternative is to create a new entry file and manually import these packages. +__project__ + +``` diff + webpack-demo + |- package.json + |- webpack.config.js + |- /dist + |- /src + |- index.js + |- globals.js ++ |- polyfills.js + |- /node_modules +``` + +__src/polyfills.js__ ```javascript -// src/polyfills.js import 'babel-polyfill'; import 'whatwg-fetch'; ``` -```javascript -// webpack.config.js -module.exports = { - entry: { - polyfills: './src/polyfills.js', - main: './src/index.js' - } - // rest of your webpack config -}; +__webpack.config.js__ + +``` diff + 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'] + }) + ] + }; ``` -In your html file you need to conditionally load the `polyfills.js` file before your bundle. How you make this decision depends on the technologies and browsers you need to support. +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__ + +``` diff + + + Getting Started ++ + + + + + +``` + +Now we can `fetch` some data within our entry script: + +__src/index.js__ -```html - + 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)) ``` -T> Any script added dynamically like in the example above will run as soon as it's parsed, but we need our polyfill to run before our bundle. This is why we are setting `async` to `false` for each script. +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. -### Smaller babel polyfill +## Further Optimizations -`babel-preset-env` uses [browserslist](https://github.com/ai/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 like: +The `babel-preset-env` package uses [browserslist](https://github.com/ai/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: -```javascript -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"; +``` js +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](https://github.com/babel/babel-preset-env) for more information. + + +## Node Built-Ins + +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](/configuration/node) for more information and examples. + + +## Other Utilities + +There are a few other tools that can help when dealing with legacy modules. + +The [`script-loader`](/loaders/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. + +W> When using the `script-loader`, the module is added as a string to the bundle. It is not minimized by `webpack`, so use a minimized version. There is also no `devtool` 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`](/configuration/module/#module-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. + +W> Any feature requiring the AST, like the `ProvidePlugin`, will not work. + +Lastly, there are some modules that support different [module styles](/concepts/modules) 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`](/loaders/imports-loader/).