Getting started with webpack - Part 8: Writing your own plugins
You will need Node 6.11.5+ installed on your machine.
Introduction
In the previous post we learned how to optimize our assets before bundling them using webpack. These optimizations are very useful when deploying webpack bundled applications in production as they make our app load faster.
In this part, we will consider how to create your own webpack plugins from scratch. Chances are, you may never have to do this as there are already thousands of awesome webpack plugins you can choose from. All you have to do is search for them on Google. However, for the sake of knowledge, let’s consider how to make a webpack plugin anyway.
Let’s get started.
Source code of the application is available on GitHub.
Prerequisites
To follow along in this series, you need the following requirements:
- Completed all previous parts of the series.
- Basic knowledge of JavaScript.
- Basic knowledge of the CLI.
- A text editor. VS Code is recommended.
- Node.js (>= 6.11.5) and npm installed locally.
Let’s continue with the series.
Setting up our project
Before we start creating our own plugin, we will be using our project code as a base. If you haven’t already, you can download the project code from GitHub. We will be using the code there as a base for the modifications we are going to make going forward. When you have downloaded the project, open Part-7
in your code editor and follow along.
The folder named
Part-8
contains the already finished code for this part. If you want to follow along use part 7 as the base. If you want a source of reference, check part 8.
Before we get started, run the following command in the root of the project to install the npm dependencies:
$ npm install
This will install all the dependencies required for our application to run. To be sure the application works as intended, run the following command below:
$ npm run serve
This command will build the assets and run the Express server. When the build is complete, visit http://localhost:3000 and you will see the application:
Now that we have the application running, we can kill the server using ctrl+c
or by closing the terminal window.
Anatomy of a webpack plugin
Plugins are the backbone of webpack. Some of the core features of webpack are actually written as plugins underneath the hood. This means plugins are exposed to the powerful API that comes with webpack.
When developing plugins for webpack, you need to know the following objects: compiler
and compilation
. Understanding what they do is the first step to knowing how to write plugins for webpack.
According to the documentation, the compiler
object
…represents the fully configured Webpack environment. This object is built once upon starting Webpack, and is configured with all operational settings including options, loaders, and plugins. When applying a plugin to the Webpack environment, the plugin will receive a reference to this compiler. Use the compiler to access the main Webpack environment.
According to the documentation, the compilation
object
…represents a single build of versioned assets. While running Webpack development middleware, a new compilation will be created each time a file change is detected, thus generating a new set of compiled assets. A compilation surfaces information about the present state of module resources, compiled assets, changed files, and watched dependencies. The compilation also provides many callback points at which a plugin may choose to perform custom actions.
If you like to read some code, here’s the source code to the compilation and compiler resources.
Let’s create the most basic plugin so you can see how it would look in the code.
Creating the most basic plugin
For our most basic plugin, we will create a plugin that spits out the obligatory Hello World text in the console when it’s called.
To get started, create a new directory in the src/js
directory called plugins
and in the directory, create a new HelloWorld.js
file and paste the following code into it:
// File: ./src/js/plugins/HelloWorld.js
class HelloWorld {
apply(compiler) {
compiler.hooks.done.tap({ name: 'HelloWorld' }, () => {
console.log('Hello world!');
});
}
};
module.exports = HelloWorld;
Next, go to the webpack configuration file and import the plugin as seen below:
// File: webpack.config.js
const HelloWorld = require('./src/js/plugins/HelloWorld');
module.exports = {
// [...]
plugins: [
// [...]
new HelloWorld(),
// [...]
]
};
Above, we imported the HelloWorld
plugin and then registered it as a webpack plugin by adding it to the plugins
array.
Now, run the following command to build our assets:
$ npm run dev
If all goes well, you should see the text Hello World! in the build output text.
Let’s add some options to the plugin where you can pass the message you want to be displayed instead of hard-coding it.
Open the HelloWorld.js
file and replace the content with the following code:
// File: ./src/js/plugins/HelloWorld.js
class HelloWorld {
constructor(options) {
this.options = options;
}
apply(compiler) {
compiler.hooks.done.tap('HelloWorld', () => {
console.log(this.options.message || 'Hello World!');
});
}
}
module.exports = HelloWorld;
In the code above, we have added a few things. First, we added an options
property to the class. This property will be passed through the plugin constructor. We then use the message
property of the options
property as the message we want to log to the console.
Now open the webpack configuration file and update as seen below:
// File: webpack.config.js
const HelloWorld = require('./src/js/plugins/HelloWorld');
module.exports = {
// [...]
plugins: [
// [...]
new HelloWorld({
message: 'Badge "webpack bundler bender" unlocked!',
}),
// [...]
]
};
Now, let’s build again using the command below:
$ npm run dev
Now you should see the custom message in the console:
Great! New badge ‘webpack bundler bender’ unlocked! Let’s create an actual webpack plugin and make use of some of the other webpack plugin APIs.
Creating our custom webpack plugin
For this example, we will be building a webpack plugin that lists all the bundled files and their size in a file. We can then look at the file and determine what the sizes of our bundled assets are. Let’s assume we need this particular plugin.
Let’s get started. Create a new file WebpackCustomManifestPlugin.js
in the src/plugins
directory. Now paste the following code into the file:
// File: ./src/plugins/WebpackCustomManifestPlugin.js
class WebpackCustomManifestPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
//
}
}
module.exports = WebpackCustomManifestPlugin;
Before adding the actual functionality, let’s go to our webpack configuration file and register the plugin as we want it to be used.
Open the webpack config file and import the plugin as seen below:
// File: webpack.config.js
const CustomManifestPlugin = require('./src/js/plugins/WebpackCustomManifestPlugin');
module.exports = {
// [...]
plugins: [
// [...]
new CustomManifestPlugin({
outputPath: path.resolve(__dirname + '/')
}),
// [...]
]
};
As seen above, we have registered our custom plugin and passed an options object to it. The only configuration we passed to the plugin is the outputPath
. This path is where we will place the generated manifest file.
Now switch to the plugin file and add the following to the class:
// File: ./src/js/plugins/WebpackCustomManifestPlugin.js
const fs = require('fs');
const chalk = require('chalk');
class WebpackCustomManifestPlugin {
constructor(options) {
this.validateOptions(options);
this.options = options;
}
validateOptions(options) {
if (!options || !options.outputPath) {
const msg = `Please specify an outputPath.`;
throw new Error(console.log(chalk.bold.bgRed('Error:'), chalk.bold.red(msg)));
}
}
apply(compiler) {
//
}
}
module.exports = WebpackCustomManifestPlugin;
Above, we have imported chalk and fs. We will need chalk
to write colorful messages to the console and fs
to write to the file system. Next, we added a new method called validateOptions
, which we use to validate the options passed to the plugin. Next, we call this method inside the constructor
method.
Note: You might have to install the
chalk
package using npm by running the command:
npm i chalk -D
Now, in the same file, let’s add the meat of the plugin in the apply
method. Replace the apply
method with the following code:
// File: ./src/js/plugins/WebpackCustomManifestPlugin.js
// [...]
class WebpackCustomManifestPlugin {
// [...]
apply(compiler) {
const { outputPath, fileName = 'manifesto.json' } = this.options;
compiler.hooks.done.tap('Custom Manifest', stats => {
const assetsManifest = [];
const { assets } = stats.compilation;
Object.keys(assets).map(name => {
let size = assets\[name\]['_cachedSize'] / 1000;
if (!isNaN(size)) {
size = Math.round(size) + ' KB';
assetsManifest.push({ name, size });
}
});
try {
let filePath = outputPath + '/' + fileName;
fs.writeFileSync(filePath, JSON.stringify(assetsManifest, null, 4));
console.log(chalk.green.bold('Manifest generated'));
} catch (error) {
console.log(chalk.bold.bgRed('Exception:'), chalk.bold.red(error.message));
}
});
}
}
// [...]
Above, we have the meat of the entire plugin. First, we tap into the done hook because we need to run the plugin when the compilation is complete. Next, we loop through the available assets and for each of them, we get the size. We then write all the assets with sizes into our manifest file and use the chalk
package to write to the console on failure or success.
Now, save the file and run the following command to build the app:
$ npm run dev
If all goes well, you will see a manifesto.json
file in the root of the application with the assets and the size of the assets. It will look something like this:
[
{
"name": "assets/css/app-c7359d5814d5f7029e5c.css",
"size": "177 KB"
},
{
"name": "assets/js/app-8c115777c5531186c849.js",
"size": "11 KB"
}
]
That’s it, you have created your first webpack plugin.
Conclusion
In this part of the series, we learned how we can create our very own webpack plugin. In the next part we will look into how to use webpack with some popular frameworks and libraries.
The source code for this application is available on GitHub.
25 February 2019
by Neo Ighodaro