Creating a Custom webpack Plugin

October 22, 2019 0 Comments

Creating a Custom webpack Plugin



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:

  • Compiler: it has the top-level API and provides hooks for them for controlling the execution of webpack.
  • Compilation or dependency graph: returned by the compiler and it starts creating the dependency graph.
  • Resolver: creates an absolute path for the entry path provided and return details like results, request, path, context, etc.
  • Parser: it creates the AST (abstract syntax tree) and then looks for interesting thing like requires and imports and then creates the dependency object.
  • Module Factories: These objects are passed to the moduleFactory function and creates the module.
  • Templates: it does the data binding of the module object and create the code in the bundled file.

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.

What are Hooks and Tapping 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.

How webpack Uses tapable & How Plugins Coming Into the Picture?

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.

Let’s Setup the Project First.

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:

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!

Breaking it down

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.

  • We went over how webpack works under the hood and how its architecture is implemented
  • We learned about hooks and what it means to tap into them
  • We saw how plugins come into the system
  • We made a simple plugin to check the size of the bundled file

Tag cloud