@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
JavaScript
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 };