Webpack plugin that generates JSON manifest files mapping original filenames to their hashed versions with extensive customization options
—
This document covers the Tapable-based hook system for customizing manifest generation at various stages of the webpack compilation process.
interface WebpackAssetsManifest {
hooks: {
apply: SyncHook<[manifest: WebpackAssetsManifest]>;
customize: SyncWaterfallHook<[
entry: KeyValuePair | false | undefined | void,
original: KeyValuePair,
manifest: WebpackAssetsManifest,
asset: Asset | undefined
]>;
transform: SyncWaterfallHook<[asset: AssetsStorage, manifest: WebpackAssetsManifest]>;
done: AsyncSeriesHook<[manifest: WebpackAssetsManifest, stats: Stats]>;
options: SyncWaterfallHook<[options: Options]>;
afterOptions: SyncHook<[options: Options, manifest: WebpackAssetsManifest]>;
};
}Runs after the plugin setup is complete, before compilation starts:
apply: SyncHook<[manifest: WebpackAssetsManifest]>;manifest.hooks.apply.tap("MyPlugin", (manifest) => {
console.log("Plugin setup complete");
manifest.set("build-time", Date.now().toString());
});
// Or via constructor options
const manifest = new WebpackAssetsManifest({
apply(manifest) {
manifest.set("version", "1.0.0");
},
});Customizes individual asset entries as they are added to the manifest:
customize: SyncWaterfallHook<[
entry: KeyValuePair | false | undefined | void,
original: KeyValuePair,
manifest: WebpackAssetsManifest,
asset: Asset | undefined
]>;manifest.hooks.customize.tap("MyCustomizer", (entry, original, manifest, asset) => {
// Skip certain files
if (original.key.includes(".map")) {
return false; // Skip this entry
}
// Modify entry
if (entry && entry.key.startsWith("img/")) {
return {
key: entry.key.replace("img/", "images/"),
value: entry.value,
};
}
// Add integrity information
if (entry && manifest.options.integrity) {
const integrity = asset?.info[manifest.options.integrityPropertyName];
if (integrity && typeof entry.value === "string") {
return {
key: entry.key,
value: {
src: entry.value,
integrity,
},
};
}
}
return entry;
});Transforms the entire manifest before serialization:
transform: SyncWaterfallHook<[asset: AssetsStorage, manifest: WebpackAssetsManifest]>;manifest.hooks.transform.tap("MyTransformer", (assets, manifest) => {
// Add metadata
const transformed = {
...assets,
_meta: {
generatedAt: new Date().toISOString(),
version: manifest.options.extra.version || "unknown",
nodeEnv: process.env.NODE_ENV,
},
};
return transformed;
});Runs after compilation is complete and the manifest has been written:
done: AsyncSeriesHook<[manifest: WebpackAssetsManifest, stats: Stats]>;manifest.hooks.done.tapPromise("MyDoneHandler", async (manifest, stats) => {
console.log(`Manifest written to ${manifest.getOutputPath()}`);
// Upload to CDN
await uploadToCDN(manifest.toString());
// Generate additional files
await generateServiceWorkerManifest(manifest.assets);
});
// Synchronous version
manifest.hooks.done.tap("MySyncHandler", (manifest, stats) => {
console.log(`Assets: ${Object.keys(manifest.assets).length}`);
});Modifies plugin options before they are processed:
options: SyncWaterfallHook<[options: Options]>;manifest.hooks.options.tap("OptionsModifier", (options) => {
// Environment-specific options
if (process.env.NODE_ENV === "development") {
options.writeToDisk = true;
options.space = 2;
} else {
options.space = 0; // Minify in production
}
return options;
});Runs after options have been processed and validated:
afterOptions: SyncHook<[options: Options, manifest: WebpackAssetsManifest]>;manifest.hooks.afterOptions.tap("PostOptionsSetup", (options, manifest) => {
console.log(`Plugin configured with output: ${options.output}`);
// Set up additional hooks based on final options
if (options.integrity) {
console.log("Subresource integrity enabled");
}
});const manifest = new WebpackAssetsManifest({
output: "assets-manifest.json",
integrity: true,
});
// Setup hook - initialize shared data
manifest.hooks.apply.tap("Setup", (manifest) => {
manifest.options.extra.startTime = Date.now();
});
// Customize individual entries
manifest.hooks.customize.tap("AssetCustomizer", (entry, original, manifest, asset) => {
if (!entry) return entry;
// Group assets by type
const ext = manifest.getExtension(original.key);
const group = ext === ".js" ? "scripts" : ext === ".css" ? "styles" : "assets";
return {
key: `${group}/${entry.key}`,
value: entry.value,
};
});
// Transform entire manifest
manifest.hooks.transform.tap("ManifestTransformer", (assets, manifest) => {
const buildTime = Date.now() - (manifest.options.extra.startTime as number);
return {
...assets,
_build: {
time: buildTime,
timestamp: new Date().toISOString(),
env: process.env.NODE_ENV,
},
};
});
// Post-processing
manifest.hooks.done.tapPromise("PostProcessor", async (manifest, stats) => {
// Write additional manifests
const serviceWorkerManifest = Object.entries(manifest.assets)
.filter(([key]) => !key.startsWith("_"))
.map(([key, value]) => ({
url: typeof value === "string" ? value : value.src,
revision: stats.hash,
}));
await manifest.writeTo("./dist/sw-manifest.json");
});class MyWebpackPlugin {
apply(compiler) {
compiler.hooks.compilation.tap("MyPlugin", (compilation) => {
// Find WebpackAssetsManifest instances
const manifests = compiler.options.plugins?.filter(
plugin => plugin instanceof WebpackAssetsManifest
) as WebpackAssetsManifest[];
manifests.forEach(manifest => {
// Hook into the manifest
manifest.hooks.customize.tap("MyPlugin", (entry, original, manifest, asset) => {
// Custom logic for this plugin
if (entry && asset?.info.myPluginMetadata) {
return {
...entry,
value: {
...entry.value,
metadata: asset.info.myPluginMetadata,
},
};
}
return entry;
});
});
});
}
}The manifest provides utility functions through the utils property:
interface WebpackAssetsManifest {
utils: {
isKeyValuePair: typeof isKeyValuePair;
isObject: typeof isObject;
getSRIHash: typeof getSRIHash;
};
}manifest.hooks.customize.tap("UtilityExample", (entry, original, manifest, asset) => {
// Use utility functions
if (manifest.utils.isKeyValuePair(entry) && manifest.utils.isObject(entry.value)) {
// Generate custom SRI hash
const content = asset?.source.source();
if (content) {
entry.value.customHash = manifest.utils.getSRIHash("md5", content);
}
}
return entry;
});Understanding this order is crucial for proper hook coordination and data flow.
Install with Tessl CLI
npx tessl i tessl/npm-webpack-assets-manifest