UNPKG

webpack-assets-manifest

Version:

This Webpack plugin will generate a JSON file that matches the original filename with the hashed version.

555 lines (554 loc) 24.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.WebpackAssetsManifest = void 0; const promises_1 = require("node:fs/promises"); const node_path_1 = require("node:path"); const proper_lockfile_1 = require("proper-lockfile"); const schema_utils_1 = require("schema-utils"); const tapable_1 = require("tapable"); const helpers_js_1 = require("./helpers.js"); const options_schema_js_1 = require("./options-schema.js"); const type_predicate_js_1 = require("./type-predicate.js"); const PLUGIN_NAME = 'WebpackAssetsManifest'; class WebpackAssetsManifest { options; assets; assetNames = new Map(); compiler; currentAsset; #isMerging = false; hooks = Object.freeze({ apply: new tapable_1.SyncHook(['manifest']), customize: new tapable_1.SyncWaterfallHook(['entry', 'original', 'manifest', 'asset']), transform: new tapable_1.SyncWaterfallHook(['assets', 'manifest']), done: new tapable_1.AsyncSeriesHook(['manifest', 'stats']), options: new tapable_1.SyncWaterfallHook(['options']), afterOptions: new tapable_1.SyncHook(['options', 'manifest']), }); constructor(options = {}) { this.hooks.transform.tap(PLUGIN_NAME, (assets) => { const { sortManifest } = this.options; return sortManifest ? (0, helpers_js_1.getSortedObject)(assets, typeof sortManifest === 'function' ? sortManifest.bind(this) : undefined) : assets; }); this.hooks.afterOptions.tap(PLUGIN_NAME, (options, manifest) => { manifest.options = Object.assign(manifest.defaultOptions, options); (0, schema_utils_1.validate)(options_schema_js_1.optionsSchema, manifest.options, { name: PLUGIN_NAME }); manifest.options.output = (0, node_path_1.normalize)(manifest.options.output); manifest.assets = Object.assign(manifest.options.assets, manifest.assets, manifest.options.assets); manifest.options.apply && manifest.hooks.apply.tap(PLUGIN_NAME, manifest.options.apply); manifest.options.customize && manifest.hooks.customize.tap(PLUGIN_NAME, manifest.options.customize); manifest.options.transform && manifest.hooks.transform.tap(PLUGIN_NAME, manifest.options.transform); manifest.options.done && manifest.hooks.done.tapPromise(PLUGIN_NAME, manifest.options.done); }); this.options = Object.assign(this.defaultOptions, options); this.assets = this.options.assets; } apply(compiler) { this.compiler = compiler; this.options = this.hooks.options.call(this.options); this.hooks.afterOptions.call(this.options, this); if (!this.options.enabled) { return; } compiler.hooks.watchRun.tap(PLUGIN_NAME, this.handleWatchRun.bind(this)); compiler.hooks.compilation.tap(PLUGIN_NAME, this.handleCompilation.bind(this)); compiler.hooks.thisCompilation.tap(PLUGIN_NAME, this.handleThisCompilation.bind(this)); compiler.hooks.afterEmit.tapPromise(PLUGIN_NAME, this.handleAfterEmit.bind(this)); compiler.hooks.done.tapPromise(PLUGIN_NAME, async (stats) => await this.hooks.done.promise(this, stats)); this.hooks.apply.call(this); } get utils() { return { isKeyValuePair: type_predicate_js_1.isKeyValuePair, isObject: type_predicate_js_1.isObject, getSRIHash: helpers_js_1.getSRIHash, }; } get defaultOptions() { return { enabled: true, assets: Object.create(null), output: 'assets-manifest.json', replacer: null, space: 2, writeToDisk: 'auto', fileExtRegex: /\.\w{2,4}\.(?:map|gz|br)$|\.\w+$/i, sortManifest: true, merge: false, publicPath: undefined, contextRelativeKeys: false, apply: undefined, customize: undefined, transform: undefined, done: undefined, entrypoints: false, entrypointsKey: 'entrypoints', entrypointsUseAssets: false, integrity: false, integrityHashes: ['sha256', 'sha384', 'sha512'], integrityPropertyName: 'integrity', extra: Object.create(null), }; } get isMerging() { return this.#isMerging; } getExtension(filename) { if (!filename || typeof filename !== 'string') { return ''; } filename = filename.split(/[?#]/)[0]; if (this.options.fileExtRegex instanceof RegExp) { const ext = filename.match(this.options.fileExtRegex); return ext && ext.length ? ext[0] : ''; } return (0, node_path_1.extname)(filename); } fixKey(key) { return typeof key === 'string' ? key.replace(/\\/g, '/') : key; } setRaw(key, value) { this.assets[key] = value; return this; } set(key, value) { if (this.isMerging && this.options.merge !== 'customize') { return this.setRaw(key, value); } const fixedKey = this.fixKey(key); const publicPath = typeof value === 'string' ? this.getPublicPath(value) : value; const entry = this.hooks.customize.call({ key: fixedKey, value: publicPath, }, { key, value, }, this, this.currentAsset); if (entry === false) { return this; } if ((0, type_predicate_js_1.isKeyValuePair)(entry)) { let { key = fixedKey, value = publicPath } = entry; if (value === publicPath && this.options.integrity) { value = { src: value, integrity: this.currentAsset?.info[this.options.integrityPropertyName] ?? '', }; } return this.setRaw(key, value); } return this.setRaw(fixedKey, publicPath); } has(key) { return Object.hasOwn(this.assets, key) || Object.hasOwn(this.assets, this.fixKey(key)); } get(key, defaultValue) { return this.assets[key] || this.assets[this.fixKey(key)] || defaultValue; } delete(key) { if (Object.hasOwn(this.assets, key)) { return delete this.assets[key]; } key = this.fixKey(key); if (Object.hasOwn(this.assets, key)) { return delete this.assets[key]; } return false; } processAssetsByChunkName(assets, hmrFiles) { if (assets) { Object.keys(assets).forEach((chunkName) => { (0, helpers_js_1.asArray)(assets[chunkName]) .filter((filename) => typeof filename === 'string' && !hmrFiles.has(filename)) .forEach((filename) => { this.assetNames.set(chunkName + this.getExtension(filename), filename); }); }); } } toJSON() { return this.hooks.transform.call(this.assets, this); } toString() { return ((typeof this.options.replacer === 'function' ? JSON.stringify(this, this.options.replacer, this.options.space) : JSON.stringify(this, this.options.replacer, this.options.space)) || '{}'); } async maybeMerge() { if (this.options.merge) { try { const deepmerge = (await Promise.resolve().then(() => __importStar(require('deepmerge')))).default; this.#isMerging = true; const content = await (0, promises_1.readFile)(this.getOutputPath(), { encoding: 'utf8' }); const data = JSON.parse(content); const arrayMerge = (_destArray, srcArray) => srcArray; for (const [key, oldValue] of Object.entries(data)) { if (this.has(key)) { const currentValue = this.get(key); if ((0, type_predicate_js_1.isObject)(oldValue) && (0, type_predicate_js_1.isObject)(currentValue)) { const newValue = deepmerge(oldValue, currentValue, { arrayMerge }); this.set(key, newValue); } } else { this.set(key, oldValue); } } } catch (error) { if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { return; } throw error; } finally { this.#isMerging = false; } } } async emitAssetsManifest(compilation) { const outputPath = this.getOutputPath(); const output = this.getManifestPath(compilation, this.inDevServer() ? (0, node_path_1.basename)(this.options.output) : (0, node_path_1.relative)(compilation.compiler.outputPath, outputPath)); let release; try { if (this.options.merge) { const outputDir = (0, node_path_1.dirname)(outputPath); await (0, promises_1.mkdir)(outputDir, { recursive: true }); release = await (0, proper_lockfile_1.lock)(outputDir, { lockfilePath: (0, node_path_1.join)(outputDir, `${PLUGIN_NAME}.lock`) }); } await this.maybeMerge(); compilation.emitAsset(output, new compilation.compiler.webpack.sources.RawSource(this.toString(), false), { assetsManifest: true, generated: true, generatedBy: [PLUGIN_NAME], }); } finally { await release?.(); } } handleProcessAssetsAnalyse(compilation) { const { contextRelativeKeys } = this.options; const { assetsInfo, chunkGraph, chunks, compiler, codeGenerationResults } = compilation; for (const chunk of chunks) { const modules = chunkGraph.getChunkModulesIterableBySourceType(chunk, 'asset'); if (modules) { const { NormalModule } = compilation.compiler.webpack; const infraLogger = compilation.compiler.getInfrastructureLogger(PLUGIN_NAME); for (const module of modules) { if (module instanceof NormalModule) { const codeGenData = codeGenerationResults?.get(module, chunk.runtime).data; const filename = module.buildInfo?.['filename'] ?? codeGenData?.get('filename'); if (!filename) { infraLogger.warn(`Unable to get filename from module: "${module.rawRequest}"`); continue; } const assetInfo = module.buildInfo?.['assetInfo'] ?? codeGenData?.get('assetInfo'); const info = { rawRequest: module.rawRequest, sourceFilename: (0, node_path_1.relative)(compiler.context, module.userRequest), ...assetInfo, }; assetsInfo.set(filename, info); this.assetNames.set(contextRelativeKeys ? info.sourceFilename : (0, node_path_1.join)((0, node_path_1.dirname)(filename), (0, node_path_1.basename)(module.userRequest)), filename); } else { infraLogger.warn(`Unhandled module: ${module.constructor.name}`); } } } } } processStatsAssets(assets) { const { contextRelativeKeys } = this.options; assets?.forEach((asset) => { if (asset.name && asset.info.sourceFilename) { this.assetNames.set(contextRelativeKeys ? asset.info.sourceFilename : (0, node_path_1.join)((0, node_path_1.dirname)(asset.name), (0, node_path_1.basename)(asset.info.sourceFilename)), asset.name); } }); } getCompilationAssets(compilation) { const hmrFiles = new Set(); const assets = compilation.getAssets().filter((asset) => { if (asset.info.hotModuleReplacement) { hmrFiles.add(asset.name); return false; } return !asset.info['assetsManifest']; }); return { assets, hmrFiles, }; } async handleProcessAssetsReport(compilation) { const stats = compilation.getStats().toJson({ all: false, assets: true, cachedAssets: true, cachedModules: true, chunkGroups: this.options.entrypoints, chunkGroupChildren: this.options.entrypoints, }); const { assets, hmrFiles } = this.getCompilationAssets(compilation); this.processStatsAssets(stats.assets); this.processAssetsByChunkName(stats.assetsByChunkName, hmrFiles); const findAssetKeys = (0, helpers_js_1.findMapKeysByValue)(this.assetNames); const { contextRelativeKeys } = this.options; for (const asset of assets) { const sourceFilenames = findAssetKeys(asset.name); if (!sourceFilenames.length) { const { sourceFilename } = asset.info; const name = sourceFilename ? (contextRelativeKeys ? sourceFilename : (0, node_path_1.basename)(sourceFilename)) : asset.name; sourceFilenames.push(name); } sourceFilenames.forEach((key) => { this.currentAsset = asset; this.set(key, asset.name); this.currentAsset = undefined; }); } if (this.options.entrypoints) { const removeHMR = (file) => !hmrFiles.has(file); const getExtensionGroup = (file) => this.getExtension(file).substring(1).toLowerCase(); const getAssetOrFilename = (file) => { let asset; if (this.options.entrypointsUseAssets) { const firstAssetKey = findAssetKeys(file).pop(); asset = firstAssetKey ? this.assets[firstAssetKey] || this.assets[file] : this.assets[file]; } return asset ? asset : this.getPublicPath(file); }; const entrypoints = Object.fromEntries(Array.from(compilation.entrypoints, ([name, entrypoint]) => { const value = { assets: (0, helpers_js_1.group)(entrypoint.getFiles().filter(removeHMR), getExtensionGroup, getAssetOrFilename), }; const childAssets = stats.namedChunkGroups?.[name]?.childAssets; if (childAssets) { for (const [property, assets] of Object.entries(childAssets)) { value[property] = (0, helpers_js_1.group)(assets.filter(removeHMR), getExtensionGroup, getAssetOrFilename); } } return [name, value]; })); if (this.options.entrypointsKey === false) { for (const key in entrypoints) { this.setRaw(key, entrypoints[key]); } } else { this.setRaw(this.options.entrypointsKey, { ...this.get(this.options.entrypointsKey), ...entrypoints, }); } } await this.emitAssetsManifest(compilation); } getManifestPath(compilation, filename) { return compilation.getPath(filename, { chunk: { name: 'assets-manifest', id: '', hash: '', }, filename: 'assets-manifest.json', }); } async writeTo(destination) { const destinationDir = (0, node_path_1.dirname)(destination); let release; try { await (0, promises_1.mkdir)(destinationDir, { recursive: true }); release = await (0, proper_lockfile_1.lock)(destinationDir, { lockfilePath: (0, node_path_1.join)(destinationDir, `${PLUGIN_NAME}.lock`) }); await (0, promises_1.writeFile)(destination, this.toString()); } finally { await release?.(); } } clear() { Object.keys(this.assets).forEach((key) => { delete this.assets[key]; }); } handleWatchRun() { this.clear(); } shouldWriteToDisk(compilation) { if (this.options.writeToDisk === 'auto') { if (this.inDevServer()) { const wdsWriteToDisk = compilation.options.devServer ? (compilation.options.devServer['devMiddleware']?.writeToDisk ?? compilation.options.devServer['writeToDisk']) : undefined; if (wdsWriteToDisk === true) { return false; } const manifestPath = this.getManifestPath(compilation, this.getOutputPath()); if (typeof wdsWriteToDisk === 'function' && wdsWriteToDisk(manifestPath) === true) { return false; } if (this.compiler?.outputPath) { return (0, node_path_1.relative)(this.compiler.outputPath, manifestPath).startsWith('..'); } } return false; } return this.options.writeToDisk; } async handleAfterEmit(compilation) { if (this.shouldWriteToDisk(compilation)) { await this.writeTo(this.getManifestPath(compilation, this.getOutputPath())); } } handleNormalModuleLoader(compilation, loaderContext, module) { const emitFile = loaderContext.emitFile.bind(module); const { contextRelativeKeys } = this.options; loaderContext.emitFile = (name, content, sourceMap, assetInfo) => { const info = Object.assign({ rawRequest: module.rawRequest, sourceFilename: (0, node_path_1.relative)(compilation.compiler.context, module.userRequest), }, assetInfo); this.assetNames.set(contextRelativeKeys ? info.sourceFilename : (0, node_path_1.join)((0, node_path_1.dirname)(name), (0, node_path_1.basename)(module.userRequest)), name); emitFile(name, content, sourceMap, info); }; } recordSubresourceIntegrity(compilation) { const { integrityHashes, integrityPropertyName } = this.options; for (const asset of compilation.getAssets()) { if (!asset.info[integrityPropertyName]) { const sriHashes = new Map(integrityHashes.map((algorithm) => [algorithm, undefined])); if (asset.info.contenthash) { (0, helpers_js_1.asArray)(asset.info.contenthash) .flatMap((contentHash) => contentHash.split(' ')) .filter((contentHash) => integrityHashes.some((algorithm) => contentHash.startsWith(`${algorithm}-`))) .forEach((sriHash) => sriHashes.set(sriHash.substring(0, sriHash.indexOf('-')), sriHash)); } const assetContent = asset.source.source(); sriHashes.forEach((value, key, map) => { if (typeof value === 'undefined') { map.set(key, (0, helpers_js_1.getSRIHash)(key, assetContent)); } }); asset.info[integrityPropertyName] = Array.from(sriHashes.values()).join(' '); compilation.assetsInfo.set(asset.name, asset.info); } } } handleCompilation(compilation) { compilation.compiler.webpack.NormalModule.getCompilationHooks(compilation).loader.tap(PLUGIN_NAME, this.handleNormalModuleLoader.bind(this, compilation)); compilation.hooks.processAssets.tap({ name: PLUGIN_NAME, stage: compilation.compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ANALYSE, }, this.handleProcessAssetsAnalyse.bind(this, compilation)); } handleThisCompilation(compilation) { if (this.options.integrity) { compilation.hooks.processAssets.tap({ name: PLUGIN_NAME, stage: compilation.compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ANALYSE, }, this.recordSubresourceIntegrity.bind(this, compilation)); } compilation.hooks.processAssets.tapPromise({ name: PLUGIN_NAME, stage: compilation.compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT, }, this.handleProcessAssetsReport.bind(this, compilation)); } inDevServer() { const [, webpackPath, serve] = process.argv; if ((serve === 'serve' && webpackPath && (0, node_path_1.basename)(webpackPath) === 'webpack') || process.argv.some((arg) => arg.includes('webpack-dev-server'))) { return true; } return ((0, type_predicate_js_1.isObject)(this.compiler?.outputFileSystem) && ('__vol' in this.compiler.outputFileSystem || !Object.is(this.compiler.outputFileSystem, this.compiler.intermediateFileSystem))); } getOutputPath() { return (0, node_path_1.isAbsolute)(this.options.output) ? this.options.output : this.compiler ? (0, node_path_1.resolve)(this.compiler.outputPath, this.options.output) : ''; } getPublicPath(filename) { const { publicPath } = this.options; if (typeof publicPath === 'function') { return publicPath(filename, this); } if (publicPath) { const resolvePath = (filename, base) => { try { return new URL(filename, base).toString(); } catch { return base + filename; } }; if (typeof publicPath === 'string') { return resolvePath(filename, publicPath); } const compilerPublicPath = this.compiler?.options.output.publicPath; if (typeof compilerPublicPath === 'string' && compilerPublicPath !== 'auto') { return resolvePath(filename, compilerPublicPath); } } return filename; } getProxy(raw = false) { const setMethod = raw ? 'setRaw' : 'set'; return new Proxy(this, { has(target, property) { return target.has(property); }, get(target, property) { return target.get(property); }, set(target, property, value) { return target[setMethod](property, value).has(property); }, deleteProperty(target, property) { return target.delete(property); }, }); } } exports.WebpackAssetsManifest = WebpackAssetsManifest;