Webpack plugins are how you hook into the Webpack build process, and they’re much more powerful than just extending functionality.
Here’s a simple plugin that logs the total compilation time.
class MyWebpackPlugin {
apply(compiler) {
const startTime = Date.now();
compiler.hooks.done.tap('MyWebpackPlugin', (stats) => {
const endTime = Date.now();
const duration = (endTime - startTime) / 1000;
console.log(`Webpack build took ${duration} seconds.`);
});
}
}
module.exports = MyWebpackPlugin;
And here’s how you’d use it in your webpack.config.js:
const MyWebpackPlugin = require('./my-webpack-plugin');
module.exports = {
// ... other webpack configurations
plugins: [
new MyWebpackPlugin(),
],
};
When you run webpack from your terminal, you’ll see the output:
Webpack build took 2.345 seconds.
This plugin taps into the done hook, which is emitted after the compilation is finished. The tap method registers a listener for this hook. The first argument, 'MyWebpackPlugin', is the name of your plugin, used for debugging. The second argument is the callback function that gets executed when the hook is triggered. Inside the callback, stats is an object containing all the compilation information.
Let’s build a more practical plugin: one that injects a custom banner into your bundled JavaScript file. This is useful for adding version information, copyright notices, or license details.
class BannerPlugin {
constructor(options) {
this.banner = options.banner || 'Default Banner';
}
apply(compiler) {
compiler.hooks.emit.tap('BannerPlugin', (compilation) => {
// Iterate over all emitted assets
for (const assetName in compilation.assets) {
if (assetName.endsWith('.js')) { // Only target JavaScript files
const source = compilation.assets[assetName].source();
const newSource = `/**\n * ${this.banner}\n */\n${source}`;
compilation.assets[assetName] = {
source: () => newSource,
size: () => newSource.length,
};
}
}
});
}
}
module.exports = BannerPlugin;
To use this plugin, you’d configure it like so:
const BannerPlugin = require('./banner-plugin');
module.exports = {
// ...
plugins: [
new BannerPlugin({
banner: 'My Awesome App v1.0.0\nCopyright 2023',
}),
],
};
This plugin uses the emit hook, which is called just before Webpack writes the assets to disk. Inside the apply method, we access compilation.assets. This is an object where keys are asset names (like main.js) and values are objects with source() and size() methods representing the asset’s content and size. We iterate through these assets, check if they are JavaScript files, prepend our banner, and then replace the original asset with our modified one.
The most surprising thing about Webpack plugins is that they operate on a deeply internal representation of the build process, allowing you to not just modify output but also intercept, transform, and even halt the build itself.
Here’s how the compilation.assets object works under the hood. When you access compilation.assets['main.js'], you’re getting an object that Webpack uses to represent the file. This object typically has a source() method that returns the file content as a string or Buffer, and a size() method that returns its size in bytes. By providing your own source() and size() methods, you can effectively replace the content of any asset Webpack is about to emit.
The compilation object is the central hub for all information about the current build. It contains modules, chunks, assets, and various hooks that allow plugins to interact with different stages of the compilation. Understanding its structure is key to writing effective plugins.
The options object passed to your plugin’s constructor is your plugin’s configuration. This allows users to customize your plugin’s behavior, as shown with the banner string in the BannerPlugin.
The Webpack plugin system is built around the Tapable library, which provides the hooks and the mechanism for plugins to tap into them. Each hook represents a specific point in the build lifecycle where plugins can execute their logic. Some common hooks include environment, afterEnvironment, entryOption, beforeRun, run, watchRun, normalModuleFactory, contextModuleFactory, afterCompile, emit, afterEmit, done, and failed.
A common pitfall is misunderstanding when certain hooks fire. For instance, afterCompile runs after all modules have been processed and before assets are generated, while emit runs just before assets are written to disk. The done hook fires after all assets have been emitted.
The stats object passed to the done hook contains a wealth of information about the build, including compilation errors, warnings, the total number of modules, chunks, and assets, as well as the total build time. You can access and process this information to generate custom reports or perform actions based on the build outcome.
The next concept you’ll likely explore is creating plugins that modify the module resolution process or generate entirely new types of assets.