UNPKG

@serwist/webpack-plugin

Version:

A plugin for your webpack build process, helping you generate a manifest of local files that should be precached.

281 lines (273 loc) 11.8 kB
import path from 'node:path'; import { transformManifest, getSourceMapURL, escapeRegExp, replaceAndUpdateSourceMap } from '@serwist/build'; import { r as relativeToOutputPath, p as performChildCompilation, t as toUnix } from './chunks/perform-child-compilation.js'; import prettyBytes from 'pretty-bytes'; import { validationErrorMap, SerwistConfigError } from '@serwist/build/schema'; import crypto from 'node:crypto'; const validateInjectManifestOptions = async (input)=>{ const result = await (await import('./chunks/schema.js')).injectManifestOptions.spa(input, { error: validationErrorMap }); if (!result.success) { throw new SerwistConfigError({ moduleName: "@serwist/webpack-plugin", message: JSON.stringify(result.error.format(), null, 2) }); } return result.data; }; const getAssetHash = (asset)=>{ if (asset.info?.immutable) { return null; } return crypto.createHash("md5").update(asset.source.source()).digest("hex"); }; const resolveWebpackURL = (publicPath, ...paths)=>{ if (publicPath === "auto") { return paths.join(""); } return [ publicPath, ...paths ].join(""); }; const checkConditions = (asset, compilation, conditions = [])=>{ const matchPart = compilation.compiler.webpack.ModuleFilenameHelpers.matchPart; for (const condition of conditions){ if (typeof condition === "function") { if (condition({ asset, compilation })) { return true; } } else if (matchPart(asset.name, condition)) { return true; } } return false; }; const getNamesOfAssetsInChunkOrGroup = (compilation, chunkOrGroup)=>{ const chunkGroup = compilation.namedChunkGroups?.get(chunkOrGroup); if (chunkGroup) { const assetNames = []; for (const chunk of chunkGroup.chunks){ assetNames.push(...getNamesOfAssetsInChunk(chunk)); } return assetNames; } const chunk = compilation.namedChunks?.get(chunkOrGroup); if (chunk) { return getNamesOfAssetsInChunk(chunk); } return null; }; const getNamesOfAssetsInChunk = (chunk)=>{ const assetNames = []; assetNames.push(...chunk.files); if (chunk.auxiliaryFiles) { assetNames.push(...chunk.auxiliaryFiles); } return assetNames; }; const filterAssets = (compilation, config)=>{ const filteredAssets = new Set(); const assets = compilation.getAssets(); const allowedAssetNames = new Set(); if (Array.isArray(config.chunks)) { for (const name of config.chunks){ const assetsInChunkOrGroup = getNamesOfAssetsInChunkOrGroup(compilation, name); if (assetsInChunkOrGroup) { for (const assetName of assetsInChunkOrGroup){ allowedAssetNames.add(assetName); } } else { compilation.warnings.push(new Error(`The chunk '${name}' was provided in your Serwist chunks config, but was not found in the compilation.`)); } } } const deniedAssetNames = new Set(); if (Array.isArray(config.excludeChunks)) { for (const name of config.excludeChunks){ const assetsInChunkOrGroup = getNamesOfAssetsInChunkOrGroup(compilation, name); if (assetsInChunkOrGroup) { for (const assetName of assetsInChunkOrGroup){ deniedAssetNames.add(assetName); } } } } for (const asset of assets){ if (deniedAssetNames.has(asset.name)) { continue; } if (Array.isArray(config.chunks) && !allowedAssetNames.has(asset.name)) { continue; } const isExcluded = checkConditions(asset, compilation, config.exclude); if (isExcluded) { continue; } const isIncluded = !Array.isArray(config.include) || checkConditions(asset, compilation, config.include); if (!isIncluded) { continue; } filteredAssets.add(asset); } return filteredAssets; }; const getManifestEntriesFromCompilation = async (compilation, config)=>{ const filteredAssets = filterAssets(compilation, config); const { publicPath } = compilation.options.output; const fileDetails = Array.from(filteredAssets).map((asset)=>{ return { file: resolveWebpackURL(publicPath, asset.name), hash: getAssetHash(asset), size: asset.source.size() || 0 }; }); const { manifestEntries, size, warnings } = await transformManifest({ fileDetails, additionalPrecacheEntries: config.additionalPrecacheEntries, dontCacheBustURLsMatching: config.dontCacheBustURLsMatching, manifestTransforms: config.manifestTransforms, maximumFileSizeToCacheInBytes: config.maximumFileSizeToCacheInBytes, modifyURLPrefix: config.modifyURLPrefix, transformParam: compilation, disablePrecacheManifest: config.disablePrecacheManifest }); for (const warning of warnings){ compilation.warnings.push(new Error(warning)); } const sortedEntries = manifestEntries?.sort((a, b)=>a.url === b.url ? 0 : a.url > b.url ? 1 : -1); return { size, sortedEntries }; }; const getSourcemapAssetName = (compilation, swContents, swDest)=>{ const url = getSourceMapURL(swContents); if (url) { const swAssetDirname = path.dirname(swDest); const sourcemapURLAssetName = path.normalize(path.join(swAssetDirname, url)); if (compilation.getAsset(sourcemapURLAssetName)) { return sourcemapURLAssetName; } } return undefined; }; const _generatedAssetNames = new Set(); class InjectManifest { config; alreadyCalled; webpack; constructor(config){ this.config = config; this.alreadyCalled = false; this.webpack = null; } propagateWebpackConfig(compiler) { this.webpack = compiler.webpack; const parsedSwSrc = path.parse(this.config.swSrc); this.config = { swDest: `${parsedSwSrc.name}.js`, ...this.config }; } async getManifestEntries(compilation, config) { if (config.disablePrecacheManifest) { return { size: 0, sortedEntries: undefined, manifestString: "undefined" }; } if (this.alreadyCalled) { const warningMessage = `${this.constructor.name} has been called multiple times, perhaps due to running webpack in --watch mode. The precache manifest generated after the first call may be inaccurate! Please see https://github.com/GoogleChrome/workbox/issues/1790 for more information.`; if (!compilation.warnings.some((warning)=>warning instanceof Error && warning.message === warningMessage)) { compilation.warnings.push(new Error(warningMessage)); } } else { this.alreadyCalled = true; } config.exclude.push(({ asset })=>_generatedAssetNames.has(asset.name)); const { size, sortedEntries } = await getManifestEntriesFromCompilation(compilation, config); let manifestString = JSON.stringify(sortedEntries); if (this.config.compileSrc && !(compilation.options?.devtool === "eval-cheap-source-map" && compilation.options.optimization?.minimize)) { manifestString = manifestString.replace(/"/g, `'`); } return { size, sortedEntries, manifestString }; } apply(compiler) { this.propagateWebpackConfig(compiler); compiler.hooks.make.tapPromise(this.constructor.name, (compilation)=>this.handleMake(compiler, compilation).catch((error)=>{ compilation.errors.push(error); })); const { PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER } = this.webpack.Compilation; compiler.hooks.thisCompilation.tap(this.constructor.name, (compilation)=>{ compilation.hooks.processAssets.tapPromise({ name: this.constructor.name, stage: PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER - 10 }, ()=>this.addAssets(compilation).catch((error)=>{ compilation.errors.push(error); })); }); } addSrcToAssets(compiler, compilation) { const source = compiler.inputFileSystem.readFileSync(this.config.swSrc); compilation.emitAsset(this.config.swDest, new this.webpack.sources.RawSource(source)); } async handleMake(compiler, compilation) { this.config = await validateInjectManifestOptions(this.config); this.config.swDest = relativeToOutputPath(compilation, this.config.swDest); _generatedAssetNames.add(this.config.swDest); if (this.config.compileSrc) { await performChildCompilation(compiler, compilation, this.constructor.name, this.config.swSrc, this.config.swDest, this.config.webpackCompilationPlugins); } else { this.addSrcToAssets(compiler, compilation); if (Array.isArray(this.config.webpackCompilationPlugins) && this.config.webpackCompilationPlugins.length > 0) { compilation.warnings.push(new Error("'compileSrc' is 'false', so the 'webpackCompilationPlugins' option will be ignored.")); } } } async addAssets(compilation) { const config = Object.assign({}, this.config); const { size, sortedEntries, manifestString } = await this.getManifestEntries(compilation, config); compilation.fileDependencies.add(path.resolve(config.swSrc)); const swAsset = compilation.getAsset(config.swDest); const swAssetString = swAsset.source.source().toString(); const globalRegexp = new RegExp(escapeRegExp(config.injectionPoint), "g"); const injectionResults = swAssetString.match(globalRegexp); if (!injectionResults) { throw new Error(`Can't find ${config.injectionPoint} in your SW source.`); } if (injectionResults.length !== 1) { throw new Error(`Multiple instances of ${config.injectionPoint} were found in your SW source. Include it only once. For more info, see https://github.com/GoogleChrome/workbox/issues/2681`); } const sourcemapAssetName = getSourcemapAssetName(compilation, swAssetString, config.swDest); if (sourcemapAssetName) { _generatedAssetNames.add(sourcemapAssetName); const sourcemapAsset = compilation.getAsset(sourcemapAssetName); const { source, map } = await replaceAndUpdateSourceMap({ jsFilename: toUnix(config.swDest), originalMap: JSON.parse(sourcemapAsset.source.source().toString()), originalSource: swAssetString, replaceString: manifestString, searchString: config.injectionPoint }); compilation.updateAsset(sourcemapAssetName, new this.webpack.sources.RawSource(map)); compilation.updateAsset(config.swDest, new this.webpack.sources.RawSource(source)); } else { compilation.updateAsset(config.swDest, new this.webpack.sources.RawSource(swAssetString.replace(config.injectionPoint, manifestString))); } if (compilation.getLogger) { const logger = compilation.getLogger(this.constructor.name); logger.info(`The service worker at ${config.swDest ?? ""} will precache ${sortedEntries?.length ?? 0} URLs, totaling ${prettyBytes(size)}.`); } } } export { InjectManifest, validateInjectManifestOptions };