@serwist/webpack-plugin
Version:
A plugin for your webpack build process, helping you generate a manifest of local files that should be precached.
213 lines (183 loc) • 7.19 kB
text/typescript
/*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import type { FileDetails, ManifestEntry } from "@serwist/build";
import { transformManifest } from "@serwist/build";
import type { Asset, Chunk, Compilation, WebpackError } from "webpack";
import { getAssetHash } from "./get-asset-hash.js";
import { resolveWebpackURL } from "./resolve-webpack-url.js";
import type { InjectManifestOptions, InjectManifestOptionsComplete } from "./types.js";
/**
* 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: Asset, compilation: Compilation, conditions: Array<string | RegExp | ((arg0: any) => boolean)> = []): boolean => {
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;
}
}
// We'll only get here if none of the conditions applied.
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: Compilation, chunkOrGroup: string): string[] | null => {
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);
}
// If we get here, there's no chunkGroup or chunk with that name.
return null;
};
/**
* Returns the names of all the assets in a chunk.
*
* @param chunk
* @returns
* @private
*/
const getNamesOfAssetsInChunk = (chunk: Chunk): string[] => {
const assetNames: string[] = [];
assetNames.push(...chunk.files);
// This only appears to be set in webpack v5.
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: Compilation, config: InjectManifestOptions): Set<Asset> => {
const filteredAssets = new Set<Asset>();
const assets = compilation.getAssets();
const allowedAssetNames = new Set<string>();
// See https://github.com/GoogleChrome/workbox/issues/1287
if (Array.isArray(config.chunks)) {
for (const name of config.chunks) {
// See https://github.com/GoogleChrome/workbox/issues/2717
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.`) as WebpackError,
);
}
}
}
const deniedAssetNames = new Set();
if (Array.isArray(config.excludeChunks)) {
for (const name of config.excludeChunks) {
// See https://github.com/GoogleChrome/workbox/issues/2717
const assetsInChunkOrGroup = getNamesOfAssetsInChunkOrGroup(compilation, name);
if (assetsInChunkOrGroup) {
for (const assetName of assetsInChunkOrGroup) {
deniedAssetNames.add(assetName);
}
} // Don't warn if the chunk group isn't found.
}
}
for (const asset of assets) {
// chunk based filtering is funky because:
// - Each asset might belong to one or more chunks.
// - If *any* of those chunk names match our config.excludeChunks,
// then we skip that asset.
// - If the config.chunks is defined *and* there's no match
// between at least one of the chunkNames and one entry, then
// we skip that assets as well.
if (deniedAssetNames.has(asset.name)) {
continue;
}
if (Array.isArray(config.chunks) && !allowedAssetNames.has(asset.name)) {
continue;
}
// Next, check asset-level checks via includes/excludes:
const isExcluded = checkConditions(asset, compilation, config.exclude);
if (isExcluded) {
continue;
}
// Treat an empty config.includes as an implicit inclusion.
const isIncluded = !Array.isArray(config.include) || checkConditions(asset, compilation, config.include);
if (!isIncluded) {
continue;
}
// If we've gotten this far, then add the asset.
filteredAssets.add(asset);
}
return filteredAssets;
};
export const getManifestEntriesFromCompilation = async (
compilation: Compilation,
config: InjectManifestOptionsComplete,
): Promise<{ size: number; sortedEntries: ManifestEntry[] | undefined }> => {
const filteredAssets = filterAssets(compilation, config);
const { publicPath } = compilation.options.output;
const fileDetails = Array.from(filteredAssets).map((asset) => {
return {
file: resolveWebpackURL(publicPath as string, asset.name),
hash: getAssetHash(asset),
size: asset.source.size() || 0,
} satisfies FileDetails;
});
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,
});
// See https://github.com/GoogleChrome/workbox/issues/2790
for (const warning of warnings) {
compilation.warnings.push(new Error(warning) as WebpackError);
}
// Ensure that the entries are properly sorted by URL.
const sortedEntries = manifestEntries?.sort((a, b) => (a.url === b.url ? 0 : a.url > b.url ? 1 : -1));
return { size, sortedEntries };
};