Custom build steps in large codebases are implemented as loaders and plugins.
A loader is a function exported from a module that receives a file's source and returns new source. It runs in a loader context (this):
this.getOptions()reads options (validate them with a schema).this.resourcePathis the file being processed;this.addDependency(file)makes webpack watch extra files.- Return a string, or call
this.callback(err, content, sourceMap)to also pass a source map; for async work useconst done = this.async(). - Loaders should be pure and cacheable (they are by default;
this.cacheable(false)opts out). Apitchmethod can run before the chain in reverse order.
A plugin is a class with apply(compiler). webpack's internals are built on Tapable hooks, and plugins tap them:
- Compiler hooks cover the whole lifecycle:
beforeRun,compile,thisCompilation,emit,done,watchRun. - A compilation represents one build of the graph. In
thisCompilation, tapcompilation.hooks.processAssetswith astage(e.g.PROCESS_ASSETS_STAGE_ADDITIONAL) to add or modify output files viacompilation.emitAssetandwebpack.sources. - Hooks can be sync (
tap), callback-async (tapAsync) or promise-based (tapPromise).
Use compilation.errors.push(new webpack.WebpackError(...)) or this.emitError in loaders to report problems instead of throwing raw exceptions.
Example
// banner-loader.js: prepend a comment to every matched module
module.exports = function bannerLoader(source) {
const { text } = this.getOptions();
return `/* ${text} */\n${source}`;
};
// build-info-plugin.js: emit build-info.json with the asset list
const { Compilation, sources } = require("webpack");
class BuildInfoPlugin {
apply(compiler) {
compiler.hooks.thisCompilation.tap("BuildInfoPlugin", (compilation) => {
compilation.hooks.processAssets.tap(
{ name: "BuildInfoPlugin", stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE },
(assets) => {
const info = { builtAt: new Date().toISOString(), files: Object.keys(assets) };
compilation.emitAsset("build-info.json", new sources.RawSource(JSON.stringify(info)));
},
);
});
}
}
module.exports = BuildInfoPlugin;
// config: module.rules: [{ test: /\.js$/, use: { loader: path.resolve("banner-loader.js"), options: { text: "v1" } } }]
// plugins: [new BuildInfoPlugin()]Remember
- Loaders: functions from source to source, with this.getOptions/callback/async
- Plugins:
apply(compiler)tapping Tapable hooks - Modify output in
processAssetswith the right stage - Report errors through webpack instead of throwing
Watch out for
- Mutating assets in the
emithook (deprecated style) instead ofprocessAssetsstages.