A Hands-On Guide to Learn Webpack
Table of Contents
Webpack Zero Configuration
Create package.json
file:
$ npm init -y
Install webpack locally:
$ yarn add webpack -D
Create a sample js file:
// src/app.js
console.log('Hello World!');
Compile it down with webpack even without a configuration:
./node_modules/.bin/webpack src/app.js dist/bundle.js
Watch for file changes and recompile automatically:
./node_modules/.bin/webpack src/app.js dist/bundle.js --watch
We can store those commands within NPM scripts:
"scripts": {
"build": "webpack src/app.js dist/bundle.js",
"watch": "webpack src/app.js dist/bundle.js --watch"
},
Or even shorter:
"scripts": {
"build": "webpack src/app.js dist/bundle.js",
"watch": "npm run build -- --watch"
},
Note that we don’t need to use a full path when calling webpack
within the npm scripts, it will assume a local installation. Also note that we use an extra —
to pass a parameter. Now we can simply run:
npm run build #or
npm run watch
Dedicated Webpack Config File
You can also provide webpack with configuration file like so:
./node_modules/.bin/webpack --config=myconfig.js
And by default webpack will look for a configuration file named webpack.config.js
on the current directory. Create one like this:
const path = require('path');
module.exports = {
entry: './src/app.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'bundle.js'
}
};
Note that webpack needs an absolute path to the output’s directory (the path
parameter).
With this configuration file, our NPM scripts now will be simpler:
"scripts": {
"build": "webpack",
"watch": "webpack --watch"
}
Modules are Simply Files
ES2015 Module
Suppose we have an ES2015 module for notification like this:
// Notification.js
export default function(message) {
console.log(message);
}
We can than use it in our main file like so:
// App.js
import notify from './Notification';
notify('Hello!!');
Common JS Module
Webpack also support the common js format:
// Notification.js
module.exports = function(message) {
console.log(message);
}
// App.js
const notify = require('./Notification');
notify('Hello!!');
Non Default Export
If we export
a module but do not use the default
keyword, we have to explicitly specify the class/function/variable name:
// Notification.js
export function notify(message) {
alert(message);
}
// App.js
import { notify } from './Notification';
notify('Hello!');
Multiple Export In 1 File
ES2015 allows us to export multiple classes/functions/variables:
// Notification.js
export default function(message) {
alert(message);
}
export function log(message) {
console.log(message);
}
export function fireEmojis() {
console.log('🔥🔥🔥🔥');
}
Then we can use it like this:
// App.js
import notify, { log, fireEmojis } from './Notification';
notify('Hello!');
log('Hello!');
fireEmojis();
Note that only 1 default
is allowed within 1 module file. And to import a non-default class/function/variable, you have to use curly braces: { log, fireEmojis }
.
Although we are allowed to use multiple export, it’s always a good idea to keep our module simple and only exposed one default class/function/variable.
Loaders are Transformers
Webpack’s loaders allow us to transform and preprocess any number of file types.
CSS Loader
Suppose we have a CSS file like this:
/* src/style.css */
body {
background: red;
}
Then we’d like to require this CSS file within our app.js
file:
// src/app.js
require('./style.css');
If you compile down with our current webpack configuration, it will throw you an error like this:
ERROR in ./src/style.css
Module parse failed: /Users/risan/repo/webpack.dev/src/style.css Unexpected token (1:5)
You may need an appropriate loader to handle this file type.
We’re going to need the css-loader
to process this CSS file:
$ yarn add css-loader -D
Then we need to configure our webpack configuration:
const path = require('path');
module.exports = {
entry: './src/app.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: 'css-loader'
}
]
}
};
The test
parameter contains a regular expression for targeting the CSS files, while the use
parameter contains the loader’s name to apply to. With this configuration you can now successfully compile down our app.js
file.
Style Loader
We can now compile down our app.js
successfully, but the CSS rule on style.css
is not applied to our page. We need style-loader
to inject the CSS to the page.
$ yarn add style-loader -D
Then add it to the use
parameter:
const path = require('path');
module.exports = {
entry: './src/app.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
};
Note that the loaders registered on use
parameter will be applied from right to left. So in our case the css-loader
will be applied first then the style-loader
. Now you’ll see that our page will have a red-background now.
ES2015 Compilation with Babel
Suppose we use ES2015 standard in our code:
// src/Person.js
export default class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hello my name is ${this.name}.`);
}
}
And our main app.js
file looks like this:
import Person from './Person';
let risan = new Person('Risan');
risan.greet();
With Webpack’s loader we can also compile our ES2015 code. First install Babel and it’s webpack loader:
$ yarn add babel-loader babel-core -D
Install the ES2015 preset:
$ yarn add babel-preset-es2015 -D
Create the .babelrc
file:
{
"presets": ["es2015"]
}
Then finally register our Babel loader into our webpack configuration:
const path = require('path');
module.exports = {
entry: './src/app.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
}
]
}
};
Minification & Environment
To minify the compiled file with Webpack, you can just simply register the uglify
plugin like so:
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: './src/app.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
}
]
},
plugins: [
new webpack.optimize.UglifyJsPlugin()
]
};
If we want to minify the file on production, we can check for the NODE_ENV
environment variable like so:
const path = require('path');
const webpack = require('webpack');
const isInProduction = (process.env.NODE_ENV === 'production');
module.exports = {
entry: './src/app.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
}
]
},
plugins: []
};
if (isInProduction) {
module.exports.plugins.push(
new webpack.optimize.UglifyJsPlugin()
);
}
If the NODE_ENV
is not setup yet, you can also pass it into the command line:
$ NODE_ENV=production ./node_modules/.bin/webpack
Or we can also update our NPM scripts:
{
"name": "webpack.dev",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "webpack",
"production": "NODE_ENV=production webpack",
"watch": "npm run build -- --watch"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"babel-core": "^6.25.0",
"babel-loader": "^7.1.1",
"babel-preset-es2015": "^6.24.1",
"webpack": "^3.4.1"
}
}
SASS Compilation
Suppose you have an scss
file like this:
// src/style.scss
$primary: green;
body {
background: $primary;
}
And on your main file, you imported that scss file like so:
require('./style.scss');
How do you compile sass file using Webpack?
Worry not, there’s a loader for that! Make sure you have libsass
install on your computer:
$ brew install lib sass
Now add the node-sass
and the webpack sass-loader
to your project’s dependencies:
$ yarn add sass-loader node-sass -D
Now update your webpack’s configuration file like so:
const path = require('path');
module.exports = {
entry: './src/app.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.s[ac]ss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
}
]
}
};
Remember that the loaders registered on use
will be applied from right to left. Also note that the test: /\.s[ac]ss$/
will match both .sass
and .scss
file extensions.
Extract CSS to a Dedicated File
From the previous section we saw that our compiled CSS from the SCSS file is injected directly to the webpage by the style-loader
. What if we want to extract the compiled CSS to a dedicated file?
We can use the extract-text-webpack-plugin
. Install the extract extract text plugin:
$ yarn add extract-text-webpack-plugin -D
Suppose we have our scss
file like this:
$primary: green;
body {
background: $primary;
}
And we import it within our app.js
:
require('./style.scss');
To extract the compiled CSS file, we have to update our webpack configuration like so:
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
entry: './src/app.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.s[ac]ss$/,
use: ExtractTextPlugin.extract({
use: ['css-loader', 'sass-loader'],
fallback: 'style-loader'
})
}
]
},
plugins: [
new ExtractTextPlugin('style.css')
]
};
The use
parameter passes to the ExtractTextPlugin.extract()
is the loader to preprocess the SCSS file. While the fallback
parameter contains a loader name that will be used if the CSS file cannot be extracted, on our case it’s the style-loader
which will inject the CSS into the page directly. Don’t forget to register the plugin and pass the output CSS filename:
plugins: [
new ExtractTextPlugin('style.css')
]
Naming Our Entry Files
We can give a name to our entry files:
module.exports = {
entry: {
// Named it as main.
main: './src/app.js'
},
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].js' // Will become main.js
},
...
plugins: [
new ExtractTextPlugin('[name].css') // Will become main.css
]
};
The [name].js
and the [name].css
respectfully will be translated to main.js
and main.css
, because we named our entry file as main
.
We can also pass multiple files into our entry:
module.exports = {
entry: {
main: [
'./src/app.js',
'./src/style.scss'
]
}
};
With this approach we no longer need to import the style.scss
within our app.js
in order to make it included in compilation.
Minify the Extracted CSS
On previous section we learned how to minify the bundled file on production using Webpack UglifyJsPlugin
, let’s give it a try:
const path = require('path');
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const isInProduction = (process.env.NODE_ENV === 'production');
module.exports = {
entry: {
main: [
'./src/app.js',
'./src/style.scss'
]
},
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].js'
},
module: {
rules: [
{
test: /\.s[ac]ss$/,
use: ExtractTextPlugin.extract({
use: ['css-loader', 'sass-loader'],
fallback: 'style-loader'
})
}
]
},
plugins: [
new ExtractTextPlugin('[name].css')
]
};
if (isInProduction) {
module.exports.plugins.push(
new webpack.optimize.UglifyJsPlugin()
);
}
Then we run:
$ NODE_ENV=production ./node_modules/.bin/webpack
You may note that the generated main.js
is minified but the extracted main.css
file is not. To solve this issue we can install a plugin: optimize-css-assets-webpack-plugin
$ yarn add optimize-css-assets-webpack-plugin -D
Simply register this plugin:
const path = require('path');
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const isInProduction = (process.env.NODE_ENV === 'production');
module.exports = {
entry: {
main: [
'./src/app.js',
'./src/style.scss'
]
},
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].js'
},
module: {
rules: [
{
test: /\.s[ac]ss$/,
use: ExtractTextPlugin.extract({
use: ['css-loader', 'sass-loader'],
fallback: 'style-loader'
})
}
]
},
plugins: [
new ExtractTextPlugin('[name].css')
]
};
if (isInProduction) {
module.exports.plugins.push(
new webpack.optimize.UglifyJsPlugin(),
new OptimizeCssAssetsPlugin()
);
}
By default, it will look for .css
file and optimize it using cssnano
, but of course you can tweak this.
The Relative URL Conundrum
Suppose we have an old project and our images are stored within the public dist/images
directory. On our src/style.scss
file we’d like to refer to the dist/images/test.jpg
file:
// src/style.scss
body {
background: url('./images/test.jpg');
}
And here’s our webpack’s configuration file:
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
entry: {
main: [
'./src/app.js',
'./src/style.scss'
]
},
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].js'
},
module: {
rules: [
{
test: /\.s[ac]ss$/,
use: ExtractTextPlugin.extract({
use: ['css-loader', 'sass-loader'],
fallback: 'style-loader'
})
}
]
},
plugins: [
new ExtractTextPlugin('[name].css')
]
};
And if we compile our files, it will throw us an error:
Module not found: Error: Can't resolve './images/test.jpg'
This is because Webpack cannot find the test.jpg
file within the src
directory. How are we going to solve this?
Ignore the URL in CSS
The easiest fix is to ignore the URL on our CSS, we can do this by passing options to our css-loader
:
use: ExtractTextPlugin.extract({
use: [
{
loader: 'css-loader',
options: {
url: false
}
},
'sass-loader'
],
fallback: 'style-loader'
})
Use raw-loader
The second approach is to use raw-loader
instead of the css-loader
, the raw-loader
will simply export the file without processing it thus won’t bother with the URL on CSS. Install it like this:
$ yarn add raw-loader -D
Replace our css-loader
like so:
{
test: /\.s[ac]ss$/,
use: ExtractTextPlugin.extract({
use: ['raw-loader', 'sass-loader'],
fallback: 'style-loader'
})
}
The Webpack Way
The last one is the webpack way, first we have to move our image’s directory to src/images
. Next we need to install the file-loader
:
$ yarn add file-loader
This loader will help webpack to handle file and return it as an url. Next we register this loader to handle all the image files:
module: {
rules: [
{
test: /\.s[ac]ss$/,
use: ExtractTextPlugin.extract({
use: ['css-loader', 'sass-loader'],
fallback: 'style-loader'
})
},
{
test: /\.(png|jpe?g|gif|svg)$/,
use: 'file-loader'
}
]
}
That’s it, if we compile our file the generated main.css
will look similar to this:
body {
background: url(e4421469c6f25ae1ac7c267492fee673.jpg); }
As you see the src/images/test.jpg
file is copied to dist/e4421469c6f25ae1ac7c267492fee673.jpg
and Webpack is smart enough to update the image’s url within the CSS.
Even if we imported a CSS from node_modules
that contains an image. For example, we have a css from some-package
like below where the image is located at node_modules/some-package/img/foobar.jpg
:
/* node_modules/some-package/style.css */
div {
background: url('./img/foobar.jpg');
}
On our style.scss
, we import that CSS file:
@import '~some-package/style.css';
body {
background: url('./images/test.jpg');
}
And if we compile our file again, our generated main.css
file will look similar to this:
div {
background: url(1256a8c3b27439f7a501566d571c92cf.jpg);
}
body {
background: url(e4421469c6f25ae1ac7c267492fee673.jpg); }
You’ll see that the node_modules/some-package/img/foobar.jpg
will also be copied to dist/1256a8c3b27439f7a501566d571c92cf.jpg
.
Keeping the Files Name
As we see previously, our files get copied to dist
directory and the name is being replaced with a random characters. What if we want to keep the original filename? Worry not, we can pass the name
option to file-loader
:
{
test: /\.(png|jpe?g|gif|svg)$/,
loader: 'file-loader',
options: {
name: '[name].[ext]'
}
}
We can even organize it to a directory like img
or whatever you prefer:
{
test: /\.(png|jpe?g|gif|svg)$/,
loader: 'file-loader',
options: {
name: 'img/[name].[ext]'
}
}
We can also add the hash to the filename:
{
test: /\.(png|jpe?g|gif|svg)$/,
loader: 'file-loader',
options: {
name: 'img/[name]_[hash].[ext]'
}
}
And the nice thing is Webpack will automatically update the URL path on our CSS.
How to Strip Unused CSS
We often put many rules on our CSS file and most likely some of them are unused within our page. Worry not, there’s a node module for that: purifycss
And the great thing is that purifycss
also able to read our JS file and locate our dynamic CSS selector. First let’s install the module and its webpack plugin:
$ yarn add purifycss-webpack purify-css -D
Suppose we have an HTML file like this:
<html>
<head>
<title>Webpack</title>
<link rel="stylesheet" href="dist/main.css">
</head>
<body class="one">
<h1>Webpack</h1>
<script src="dist/main.js"></script>
</body>
</html>
And our SCSS file like below, note that the .two
class is unused:
// src/style.scss
.one {
background: red;
}
.two {
background: green;
}
Now let’s configure our webpack configuration to strip-down unused CSS rules using purifycss
:
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const PurifyCSSPlugin = require('purifycss-webpack');
module.exports = {
entry: {
main: [
'./src/app.js',
'./src/style.scss'
]
},
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].js'
},
module: {
rules: [
{
test: /\.s[ac]ss$/,
use: ExtractTextPlugin.extract({
use: ['css-loader', 'sass-loader'],
fallback: 'style-loader'
})
}
]
},
plugins: [
new ExtractTextPlugin('[name].css'),
new PurifyCSSPlugin({
paths: [path.join(__dirname, 'index.html')]
})
]
};
Note that the given paths
parameter is an array, you can also use glob
to find a matching pattern:
const glob = require('glob');
// Omitted...
new PurifyCSSPlugin({
paths: glob.sync(path.join(__dirname, 'resources/views/*.html'))
})
If we compile our file, the generated main.css
will not contain the unused .two
rule:
/* dist/main.css */
.one {
background: red;
}
Dynamic CSS Class
Suppose we have our SCSS file like this:
// src/style.scss
.one {
background: red;
}
.two {
background: green;
}
.dynamic {
background: yellow;
}
What if we apply the dynamic
CSS class dynamically within our JS file like this:
// src/app.js
document.querySelector('body').addEventListener('click', () => {
document.querySelector('body').className = 'dynamic';
});
To make the purifycss
plugin aware about this dynamic class, we also need to pass our compiled JS file to paths
parameter so the plugin can traverse it. In order to use glob
but with multiple path, we can install glob-all
module first:
$ yarn add glob-all -D
Then configure our PurifyCSSPlugin
to also include all JS file within the dist
directory:
const path = require('path');
const glob = require('glob-all');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const PurifyCSSPlugin = require('purifycss-webpack');
module.exports = {
entry: {
main: [
'./src/app.js',
'./src/style.scss'
]
},
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
},
{
test: /\.s[ac]ss$/,
use: ExtractTextPlugin.extract({
use: ['css-loader', 'sass-loader'],
fallback: 'style-loader'
})
}
]
},
plugins: [
new ExtractTextPlugin('[name].css'),
new PurifyCSSPlugin({
paths: glob.sync([
path.join(__dirname, '*.html'),
path.join(__dirname, 'dist/*.js')
])
})
]
};
Long Term Caching
Suppose we have two entries on our webpack configuration: app
for our main application logic and vendor
for our third-party dependencies:
const path = require('path');
module.exports = {
entry: {
app: './src/app.js',
vendor: ['jquery']
},
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].[hash].js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
}
]
}
};
Before compiling, don’t forget to add jQuery
to our dependency list:
$ yarn add jQuery
With the output filename of [name].[hash].js
, we’ll get two files on our dist
directory similar to this:
app.9479ddf09100be22e374.js
vendor.9479ddf09100be22e374.js
Note that the hash for both files are identical. If we compile it again, the generated hash for both files won’t change. And if we update the src/app.js
first, the hash for both files will be changed. Well, we don’t want the hash for vendor to be changed, since we don’t alter anything within the vendor’s entry. We only want the hash to be changed if one of the file within its entry is changed.
Chunkhash
To work around this, we can use chunkhash
instead of hash
:
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].[chunkhash].js'
}
This way we’ll always get two different hash for both entries like this:
app.fafac58446f689400548.js
vendor.95c6eae01b5a86d8acb7.js
And if we update the src/app.js
and compile it again, only the hash of the app
entry that will be changed.
Cleanup Build Directory
As you may have noticed, when the new hash is generated the old file with an old hash is still there cluttering our dist
directory. What if we want to clean up the old build?
Let’s install the clean-webpack-plugin
! This will clean up our build directory before compiling:
$ yarn add clean-webpack-plugin -D
Then we add this toplugins
field within our webpack’s configuration file:
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
app: './src/app.js',
vendor: ['jquery']
},
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].[chunkhash].js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
}
]
},
plugins: [
new CleanWebpackPlugin(['dist'], {
root: __dirname,
verbose: true,
dry: false,
watch: false,
exclude: []
})
]
};
Webpack Manifest
On the previous section we successfully add a versioning to our compiled files. But how do we refer to these files on our HTML if the prepended hash is often changes? To solve this we can create a custom plugin that will write a JSON manifest file that contains the filename both from app
and vendor
entries:
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
app: './src/app.js',
vendor: ['jquery']
},
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].[chunkhash].js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
}
]
},
plugins: [
new CleanWebpackPlugin(['dist'], {
root: __dirname,
verbose: true,
dry: false,
watch: false,
exclude: []
}),
// Our custom plugin.
function() {
this.plugin('done', stats => {
require('fs').writeFileSync(
path.join(__dirname, 'dist/manifest.json'),
JSON.stringify(stats.toJson().assetsByChunkName)
);
});
}
]
};
When the webpack is done
compiling our files, this custom plugin will write a file to distilled/mainifest.json
that will contain the filename to our entries. Here’s the example result:
{
"vendor": "vendor.95c6eae01b5a86d8acb7.js",
"app": "app.1669cd9f67324dfe138b.js"
}
We can than read this manifest.json
and load the compiled files.
Automatic Image Optimization
Suppose on our stylesheet we refer to a big image images/test.jpg
like this:
// src/style.scss
.body {
background: url('./images/test.jpg');
}
With webpack we can automatically optimize this image. Let’s install the image-webpack-loader
to do this job:
$ yarn add image-webpack-loader -D
Next we just need to apply this loader when processing images:
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
entry: {
app: [
'./src/app.js',
'./src/style.scss'
]
},
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].js'
},
module: {
rules: [
{
test: /\.s[ac]ss$/,
use: ExtractTextPlugin.extract({
use: ['css-loader', 'sass-loader'],
fallback: 'style-loader'
})
},
{
test: /\.(png|jpe?g|gif|svg)$/,
use: [
{
loader: 'file-loader',
options: {
name: 'img/[name].[ext]'
}
},
{
loader: 'image-webpack-loader',
options: {
mozjpeg: {
progressive: true,
quality: 60
}
}
}
]
}
]
},
plugins: [
new ExtractTextPlugin('[name].css')
]
};
Developing Webpack Plugin
On our previous section we learn to build a custom webpack plugin to write a JSON manifest of the generated entry files:
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
app: './src/app.js'
},
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].[chunkhash].js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
}
]
},
plugins: [
new CleanWebpackPlugin(['dist'], {
root: __dirname
}),
// Our custom plugin.
function() {
this.plugin('done', stats => {
require('fs').writeFileSync(
path.join(__dirname, 'dist/manifest.json'),
JSON.stringify(stats.toJson().assetsByChunkName)
);
});
}
]
};
We can move the entire custom plugin to its own module, so it will be a lot cleaner. Later we can use the plugin like this:
const BuildManifestPlugin = require('./build/plugins/BuildManifestPlugin');
// Omitted...
plugins: [
new BuildManifestPlugin(path.join(__dirname, 'dist/manifest.json'))
]
Now let’s create a plugin for BuildManifestPlugin
. here’s the basic building of Webpack’s plugin:
// build/plugins/BuildManifestPlugin.js
function BuildManifestPlugin() {
//
}
BuildManifestPlugin.prototype.apply = function(compiler) {
//
}
module.exports = BuildManifestPlugin;
The BuildManifestPlugin
accept one parameter, and it’s a location of the manifest file. And we move our code to the apply
prototype like so:
// build/plugins/BuildManifestPlugin.js
function BuildManifestPlugin(output) {
this.output = output;
}
BuildManifestPlugin.prototype.apply = function(compiler) {
compiler.plugin('done', stats => {
require('fs').writeFileSync(
this.output,
JSON.stringify(stats.toJson().assetsByChunkName)
);
});
}
module.exports = BuildManifestPlugin;
Now our Webpack configuration is a lot cleaner this way:
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const BuildManifestPlugin = require('./build/plugins/BuildManifestPlugin');
module.exports = {
entry: {
app: './src/app.js'
},
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].[chunkhash].js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
}
]
},
plugins: [
new CleanWebpackPlugin(['dist'], {
root: __dirname
}),
new BuildManifestPlugin(path.join(__dirname, 'dist/manifest.json'))
]
};
With our current approach, we don’t see the generated manifest file listed on the console. To solve this we need to update our plugin and hook it up to emit
event rather than done
like so:
// build/plugins/BuildManifestPlugin.js
const path = require('path');
function BuildManifestPlugin(output) {
this.output = output;
}
BuildManifestPlugin.prototype.apply = function(compiler) {
compiler.plugin('emit', (compiler, callback) => {
let manifest = JSON.stringify(compiler.getStats().toJson().assetsByChunkName);
compiler.assets[path.basename(this.output)] = {
source: function() {
return manifest;
},
size: function() {
return manifest.length;
}
}
callback();
});
}
module.exports = BuildManifestPlugin;
The emit
event will pass two parameters: compiler
and callback
, we need to call callback
at the end of our code so webpack now when we’re done. And since the stats
data is not being passed just like on done
event, we should get it from the compiler
object:
compiler.getStats();
In order to display our generated manifest on the console, we need to add it to the composer.assets
array:
compiler.assets[path.basename(this.output)] = {
source: function() {
return manifest;
},
size: function() {
return manifest.length;
}
}
The source
is the content of our manifest file and size
is the file size. If we run this again, we’ll get out manifest file listed on the console like this:
Asset Size Chunks Chunk Names
app.780342ad3ba9525cc839.js 2.54 kB 0 [emitted] app
manifest.json 37 bytes [emitted]