If you’ve ever worked with webpack, you’ve probably heard of webpack plugins. Plugins are a great option for extending webpack’s implementation and architecture. If you look at webpack’s source code, around 80% of their code is implemented using plugins. It helps in separating the core part of webpack, which leads to better code maintenance.
webpack also supports the concepts of loaders
which help in extending webpack too and work along with resolvers
. They are mainly used to transform the source code. It’s a different topic to cover and I’ll probably write an article about how to create a custom loader pretty soon too.
Before going into details about creating custom webpack plugins, we need to know some basic workings of a module bundler and how webpack works under the hood. The goal of a basic module bundler is to read the source code, find the dependencies - this is called dependency resolution.
During the dependency resolution, the bundler does module mapping (module map), bundling them into one file, packaging it into one module. webpack does these parts in an advanced way and adds some other steps too in order to make it efficient. We can break the architecture of webpack using the following steps:
webpack provides hooks for the compiler, parser, and compilations. It uses a library called tapable, which is maintained by the webpack team and helps in creating strong and powerful hooks where we can tap into methods.
Hooks are similar to events and tapping into them is like listeners listening for an event to fire and run the appropriate method. For example when we place DOM-related event listeners like this:
window.addEventListener('load', (event) => {
loadEventListerner(event)
});
In this, load
is an event or hook in which loadEventListener
is tapping into.
Let’s take a real-world example to explain how webpack uses tapable. Let’s say you are ordering pizza from a pizza delivery app. Now there are a series of steps involved with the process it like checking the menu, customizing your order and then finally placing the order by paying for it. Now from here onwards and until delivering your pizza to you, the app sends you notifications about the order progress.
In this example, we can now replace the pizza delivery app with webpack, yourself with a webpack plugin and notifications with hooks created by tapable.
webpack creates hooks for the compiler, compilations and parser stages using tapable and then the plugin taps into them or listens for them and acts accordingly.
Enough of these theories and concepts, show me the code !!
For this post, we’ll create a simple webpack plugin that checks the size of the bundled file created and logs errors or warnings based on a size limit. Those size limits can be passed in the plugin options as well and we’ll keep the default size limit to 3KB. So whenever the output file plugin is exceeding the size limit, we’ll log an error message and if it’s below it, we will log a safe message and if it’s equal to the size limit, we will simply warn the user.
You can find the code for the plugin here.
In your project directory, install webpack using npm or Yarn:
$ npm init -y
$ npm install webpack webpack-cli --save-dev
After this, create an src
directory with a index.js
file in it, where your input or entry path will point to and create a webpack.config.js
file in your project root directory.
Now you can create a directory for your plugin and name it something like bundlesize-webpack-plugin
and create a index.js
inside that directory.
Your project structure should look something like this:
webpack-Plugin-demo-directory
|- webpack.config.js
|- package.json
|- /src
|- index.js
|- /bundlesize-webpack-plugin
|- index.js
Add the following build script to the scripts
in your package.json file:
"build": "webpack"
And in your bundlesize-webpack-plugin/index.js
write the following code:
module.exports = class BundlesizeWebpackPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
console.log("FROM BUNDLESIZE PLUGIN");
}
};
We will discuss this soon.
Now in your webpack.config.js
, write the following code:
const { resolve } = require("path");
const bundlesizeplugin = require("./bundlesize-webpack-plugin");
module.exports = {
entry: resolve(__dirname, "src/index.js"),
output: {
path: resolve(__dirname, "bin"),
filename: "bundle.js"
},
plugins: [new bundlesizeplugin()]
};
Now run npm run build
.
You should see the “FROM BUNDLESIZE PLUGIN” message appear in your terminal.
Great, you’ve just made a webpack plugin!
Every webpack plugin must have an apply
method in them which is called by webpack and webpack gives the compiler instance as an argument to that method.
A plugin can be class-based or can be function-based. If the plugin is function-based, the function argument is again compiler as well. We’ll go with class-based for this article as that is the recommended way.
You can check the webpack’s source code and how it’s implemented here
In the class’ constructor, you can see there is an options
argument. This is used when your plugin accepts some options. We’ll pass the sizeLimit
as an option and if it’s not passed the default will be 3KB.
So we can now change the constructor method to this:
constructor(options) {
this.options = options || {
sizeLimit: 3
};
}
You can pass the sizeLimit
as plugin options
as well, like this:
plugins: [
new bundlesizeplugin({
sizeLimit: 4
})
]
In webpack.config.js
, we are simply mentioning the entry point and telling webpack to output the bundle file in a folder named bin
in a bundle.js
file, and telling webpack to use our plugin from the bundlesize-webpack-plugin
folder.
Now that we have the project ready, let’s check for asset size and compare with the sizeLimit
. We’re going to use the compiler.hooks.done
hook which is emitted when the compilation work is done and the bundled file is generated. We can get the details about the bundled file that way.
Note that there are some hooks which are asynchronous and we can use an asynchronous tapping method for them. You can learn about these here
apply(compiler) {
compiler.hooks.done.tap("BundleSizePlugin", (stats) => {
const {
path,
filename
} = stats.compilation.options.output;
})
}
In this, we are tapping into the done hook or event of the compiler, the first argument in the method is the plugin name which is used by webpack for referencing and the second method is the callback which takes stats
as an argument. You can check the content of the stats using console.log(stats)
, it will show a large object with every possible detail about the compilation and the file available for that hook. We are extracting the path and the filename from the output property. From now on, it’s pretty much just about getting details for the file using Node.js’ core library path
and fs
modules:
apply(compiler) {
compiler.hooks.done.tap("BundleSizePlugin", stats => {
const { path, filename } = stats.compilation.options.output;
const bundlePath = resolve(path, filename);
const { size } = fs.statSync(bundlePath);
console.log(size); // size in bytes
});
}
Simple right?
Now we can convert the size from bytes to kb using using a function like the one from this StackOverflow answer.
Now simply compare it with the sizeLimit
and console.log
the appropriate message:
apply(compiler) {
compiler.hooks.done.tap("BundleSizePlugin", stats => {
const { path, filename } = stats.compilation.options.output;
const bundlePath = resolve(path, filename);
const { size } = fs.statSync(bundlePath);
const { bundleSize, fullSizeInfo } = formatBytes(size);
const { sizeLimit } = this.options;
if (bundleSize < sizeLimit) {
console.log(
"Safe:Bundle-Size",
fullSizeInfo,
"\n SIZE LIMIT:",
sizeLimit
);
} else {
if (bundleSize === sizeLimit) {
console.warn(
"Warn:Bundle-Size",
fullSizeInfo,
"\n SIZE LIMIT:",
sizeLimit
);
} else {
console.error(
"Unsafe:Bundle-Size",
fullSizeInfo,
"\n SIZE LIMIT:",
sizeLimit
);
}
}
});
}
That’s it! You now have your own webpack plugin which checks for the bundle size and reports based on the size limit.
You can now publish this on the npm registry.
There are few standards that webpack finds effective to have in plugins. You can use webpack-default for a good starting point.
Note that the bundlesize-webpack-plugin, which I’ve published already, is also extending hooks of its own and they are created using tapable. You can find the implementation in the master branch.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Sign up for Infrastructure as a Newsletter.
Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.