UNPKG

@serwist/webpack-plugin

Version:

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

345 lines (344 loc) 14.2 kB
import { n as performChildCompilation, t as relativeToOutputPath } from "./chunks/relative-to-output-path-Gvhd54pA.js"; import path from "node:path"; import { escapeRegExp, getSourceMapURL, replaceAndUpdateSourceMap, transformManifest } from "@serwist/build"; import { toUnix } from "@serwist/utils"; import prettyBytes from "pretty-bytes"; import crypto from "node:crypto"; import { SerwistConfigError, validationErrorMap } from "@serwist/build/schema"; //#region src/lib/get-asset-hash.ts /** * @param asset * @returns The MD5 hash of the asset's source. * * @private */ const getAssetHash = (asset) => { if (asset.info?.immutable) return null; return crypto.createHash("md5").update(asset.source.source()).digest("hex"); }; //#endregion //#region src/lib/resolve-webpack-url.ts /** * Resolves a url in the way that webpack would (with string concatenation) * * Use publicPath + filePath instead of url.resolve(publicPath, filePath) see: * https://webpack.js.org/configuration/output/#output-publicpath * * @param publicPath The publicPath value from webpack's compilation. * @param paths File paths to join * @returns Joined file path * @private */ const resolveWebpackURL = (publicPath, ...paths) => { if (publicPath === "auto") return paths.join(""); return [publicPath, ...paths].join(""); }; //#endregion //#region src/lib/get-manifest-entries-from-compilation.ts /** * For a given asset, checks whether at least one of the conditions matches. * * @param asset The webpack asset in question. This will be passed * to any functions that are listed as conditions. * @param compilation The webpack compilation. This will be passed * to any functions that are listed as conditions. * @param conditions * @returns Whether or not at least one condition matches. * @private */ 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; }; /** * Returns the names of all the assets in all the chunks in a chunk group, * if provided a chunk group name. * Otherwise, if provided a chunk name, return all the assets in that chunk. * Otherwise, if there isn't a chunk group or chunk with that name, return null. * * @param compilation * @param chunkOrGroup * @returns * @private */ 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; }; /** * Returns the names of all the assets in a chunk. * * @param chunk * @returns * @private */ const getNamesOfAssetsInChunk = (chunk) => { const assetNames = []; assetNames.push(...chunk.files); if (chunk.auxiliaryFiles) assetNames.push(...chunk.auxiliaryFiles); return assetNames; }; /** * Filters the set of assets out, based on the configuration options provided: * - chunks and excludeChunks, for chunkName-based criteria. * - include and exclude, for more general criteria. * * @param compilation The webpack compilation. * @param config The validated configuration, obtained from the plugin. * @returns The assets that should be included in the manifest, * based on the criteria provided. * @private */ const filterAssets = (compilation, config) => { const filteredAssets = /* @__PURE__ */ new Set(); const assets = compilation.getAssets(); const allowedAssetNames = /* @__PURE__ */ 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(/* @__PURE__ */ new Error(`The chunk '${name}' was provided in your Serwist chunks config, but was not found in the compilation.`)); } const deniedAssetNames = /* @__PURE__ */ 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; if (checkConditions(asset, compilation, config.exclude)) continue; if (!(!Array.isArray(config.include) || checkConditions(asset, compilation, config.include))) continue; filteredAssets.add(asset); } return filteredAssets; }; const getManifestEntriesFromCompilation = async (compilation, config) => { const filteredAssets = filterAssets(compilation, config); const { publicPath } = compilation.options.output; const { manifestEntries, size, warnings } = await transformManifest({ fileDetails: Array.from(filteredAssets).map((asset) => { return { file: resolveWebpackURL(publicPath, asset.name), hash: getAssetHash(asset), size: asset.source.size() || 0 }; }), 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)); return { size, sortedEntries: manifestEntries?.sort((a, b) => a.url === b.url ? 0 : a.url > b.url ? 1 : -1) }; }; //#endregion //#region src/lib/get-sourcemap-asset-name.ts /** * If our bundled swDest file contains a sourcemap, we would invalidate that * mapping if we just replaced injectionPoint with the stringified manifest. * Instead, we need to update the swDest contents as well as the sourcemap * at the same time. * * See https://github.com/GoogleChrome/workbox/issues/2235 * * @param compilation The current webpack compilation. * @param swContents The contents of the swSrc file, which may or * may not include a valid sourcemap comment. * @param swDest The configured swDest value. * @returns If the swContents contains a valid sourcemap * comment pointing to an asset present in the compilation, this will return the * name of that asset. Otherwise, it will return undefined. * @private */ 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; } }; //#endregion //#region src/lib/validator.ts const validateInjectManifestOptions = async (input) => { const result = await (await import("./chunks/schema-CpgCa1bB.js").then((n) => n.r)).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; }; //#endregion //#region src/inject-manifest.ts const _generatedAssetNames = /* @__PURE__ */ new Set(); /** * This class supports compiling a service worker file provided via `swSrc`, * and injecting into that service worker a list of URLs and revision * information for precaching based on the webpack asset pipeline. * * Use an instance of `InjectManifest` in the * [`plugins` array](https://webpack.js.org/concepts/plugins/#usage) of a * webpack config. * * In addition to injecting the manifest, this plugin will perform a compilation * of the `swSrc` file, using the options from the main webpack configuration. * * ``` * // The following lists some common options; see the rest of the documentation * // for the full set of options and defaults. * new InjectManifest({ * exclude: [/.../, '...'], * maximumFileSizeToCacheInBytes: ..., * swSrc: '...', * }); * ``` */ var InjectManifest = class { config; alreadyCalled; webpack; /** * Creates an instance of InjectManifest. */ constructor(config) { this.config = config; this.alreadyCalled = false; this.webpack = null; } /** * @param compiler default compiler object passed from webpack * * @private */ propagateWebpackConfig(compiler) { this.webpack = compiler.webpack; const parsedSwSrc = path.parse(this.config.swSrc); this.config = { swDest: `${parsedSwSrc.name}.js`, ...this.config }; } /** * `getManifestEntriesFromCompilation` with a few additional checks. * * @private */ async getManifestEntries(compilation, config) { if (config.disablePrecacheManifest) return { size: 0, sortedEntries: void 0, 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 }; } /** * @param compiler default compiler object passed from webpack * * @private */ 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); })); }); } /** * @param compiler The webpack parent compiler. * @param compilation The webpack compilation. * * @private */ addSrcToAssets(compiler, compilation) { const source = compiler.inputFileSystem.readFileSync(this.config.swSrc); compilation.emitAsset(this.config.swDest, new this.webpack.sources.RawSource(source)); } /** * @param compiler The webpack parent compiler. * @param compilation The webpack compilation. * * @private */ 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(/* @__PURE__ */ new Error("'compileSrc' is 'false', so the 'webpackCompilationPlugins' option will be ignored.")); } } /** * @param compilation The webpack compilation. * * @private */ 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 swAssetString = compilation.getAsset(config.swDest).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) compilation.getLogger(this.constructor.name).info(`The service worker at ${config.swDest ?? ""} will precache ${sortedEntries?.length ?? 0} URLs, totaling ${prettyBytes(size)}.`); } }; //#endregion export { InjectManifest, validateInjectManifestOptions }; //# sourceMappingURL=index.mjs.map