931 lines
28 KiB
JavaScript
931 lines
28 KiB
JavaScript
"use strict";
|
|
|
|
const path = require("path");
|
|
|
|
const {
|
|
validate
|
|
} = require("schema-utils");
|
|
|
|
const serialize = require("serialize-javascript");
|
|
|
|
const normalizePath = require("normalize-path");
|
|
|
|
const globParent = require("glob-parent");
|
|
|
|
const fastGlob = require("fast-glob"); // @ts-ignore
|
|
|
|
|
|
const {
|
|
version
|
|
} = require("../package.json");
|
|
|
|
const schema = require("./options.json");
|
|
|
|
const {
|
|
readFile,
|
|
stat,
|
|
throttleAll
|
|
} = require("./utils");
|
|
|
|
const template = /\[\\*([\w:]+)\\*\]/i;
|
|
/** @typedef {import("schema-utils/declarations/validate").Schema} Schema */
|
|
|
|
/** @typedef {import("webpack").Compiler} Compiler */
|
|
|
|
/** @typedef {import("webpack").Compilation} Compilation */
|
|
|
|
/** @typedef {import("webpack").WebpackError} WebpackError */
|
|
|
|
/** @typedef {import("webpack").Asset} Asset */
|
|
|
|
/** @typedef {import("globby").Options} GlobbyOptions */
|
|
|
|
/** @typedef {import("globby").GlobEntry} GlobEntry */
|
|
|
|
/** @typedef {ReturnType<Compilation["getLogger"]>} WebpackLogger */
|
|
|
|
/** @typedef {ReturnType<Compilation["getCache"]>} CacheFacade */
|
|
|
|
/** @typedef {ReturnType<ReturnType<Compilation["getCache"]>["getLazyHashedEtag"]>} Etag */
|
|
|
|
/** @typedef {ReturnType<Compilation["fileSystemInfo"]["mergeSnapshots"]>} Snapshot */
|
|
|
|
/**
|
|
* @typedef {boolean} Force
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} CopiedResult
|
|
* @property {string} sourceFilename
|
|
* @property {string} absoluteFilename
|
|
* @property {string} filename
|
|
* @property {Asset["source"]} source
|
|
* @property {Force | undefined} force
|
|
* @property {Record<string, any>} info
|
|
*/
|
|
|
|
/**
|
|
* @typedef {string} StringPattern
|
|
*/
|
|
|
|
/**
|
|
* @typedef {boolean} NoErrorOnMissing
|
|
*/
|
|
|
|
/**
|
|
* @typedef {string} Context
|
|
*/
|
|
|
|
/**
|
|
* @typedef {string} From
|
|
*/
|
|
|
|
/**
|
|
* @callback ToFunction
|
|
* @param {{ context: string, absoluteFilename?: string }} pathData
|
|
* @return {string | Promise<string>}
|
|
*/
|
|
|
|
/**
|
|
* @typedef {string | ToFunction} To
|
|
*/
|
|
|
|
/**
|
|
* @typedef {"dir" | "file" | "template"} ToType
|
|
*/
|
|
|
|
/**
|
|
* @callback TransformerFunction
|
|
* @param {Buffer} input
|
|
* @param {string} absoluteFilename
|
|
* @returns {string | Buffer | Promise<string> | Promise<Buffer>}
|
|
*/
|
|
|
|
/**
|
|
* @typedef {{ keys: { [key: string]: any } } | { keys: ((defaultCacheKeys: { [key: string]: any }, absoluteFilename: string) => Promise<{ [key: string]: any }>) }} TransformerCacheObject
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} TransformerObject
|
|
* @property {TransformerFunction} transformer
|
|
* @property {boolean | TransformerCacheObject} [cache]
|
|
*/
|
|
|
|
/**
|
|
* @typedef {TransformerFunction | TransformerObject} Transform
|
|
*/
|
|
|
|
/**
|
|
* @callback Filter
|
|
* @param {string} filepath
|
|
* @returns {boolean | Promise<boolean>}
|
|
*/
|
|
|
|
/**
|
|
* @callback TransformAllFunction
|
|
* @param {{ data: Buffer, sourceFilename: string, absoluteFilename: string }[]} data
|
|
* @returns {string | Buffer | Promise<string> | Promise<Buffer>}
|
|
*/
|
|
|
|
/**
|
|
* @typedef { Record<string, any> | ((item: { absoluteFilename: string, sourceFilename: string, filename: string, toType: ToType }) => Record<string, any>) } Info
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} ObjectPattern
|
|
* @property {From} from
|
|
* @property {GlobbyOptions} [globOptions]
|
|
* @property {Context} [context]
|
|
* @property {To} [to]
|
|
* @property {ToType} [toType]
|
|
* @property {Info} [info]
|
|
* @property {Filter} [filter]
|
|
* @property {Transform} [transform]
|
|
* @property {TransformAllFunction} [transformAll]
|
|
* @property {Force} [force]
|
|
* @property {number} [priority]
|
|
* @property {NoErrorOnMissing} [noErrorOnMissing]
|
|
*/
|
|
|
|
/**
|
|
* @typedef {StringPattern | ObjectPattern} Pattern
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} AdditionalOptions
|
|
* @property {number} [concurrency]
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} PluginOptions
|
|
* @property {Pattern[]} patterns
|
|
* @property {AdditionalOptions} [options]
|
|
*/
|
|
|
|
class CopyPlugin {
|
|
/**
|
|
* @param {PluginOptions} [options]
|
|
*/
|
|
constructor(options = {
|
|
patterns: []
|
|
}) {
|
|
validate(
|
|
/** @type {Schema} */
|
|
schema, options, {
|
|
name: "Copy Plugin",
|
|
baseDataPath: "options"
|
|
});
|
|
/**
|
|
* @private
|
|
* @type {Pattern[]}
|
|
*/
|
|
|
|
this.patterns = options.patterns;
|
|
/**
|
|
* @private
|
|
* @type {AdditionalOptions}
|
|
*/
|
|
|
|
this.options = options.options || {};
|
|
}
|
|
/**
|
|
* @private
|
|
* @param {Compilation} compilation
|
|
* @param {number} startTime
|
|
* @param {string} dependency
|
|
* @returns {Promise<Snapshot | undefined>}
|
|
*/
|
|
|
|
|
|
static async createSnapshot(compilation, startTime, dependency) {
|
|
// eslint-disable-next-line consistent-return
|
|
return new Promise((resolve, reject) => {
|
|
compilation.fileSystemInfo.createSnapshot(startTime, [dependency], // @ts-ignore
|
|
// eslint-disable-next-line no-undefined
|
|
undefined, // eslint-disable-next-line no-undefined
|
|
undefined, null, (error, snapshot) => {
|
|
if (error) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
|
|
resolve(
|
|
/** @type {Snapshot} */
|
|
snapshot);
|
|
});
|
|
});
|
|
}
|
|
/**
|
|
* @private
|
|
* @param {Compilation} compilation
|
|
* @param {Snapshot} snapshot
|
|
* @returns {Promise<boolean | undefined>}
|
|
*/
|
|
|
|
|
|
static async checkSnapshotValid(compilation, snapshot) {
|
|
// eslint-disable-next-line consistent-return
|
|
return new Promise((resolve, reject) => {
|
|
compilation.fileSystemInfo.checkSnapshotValid(snapshot, (error, isValid) => {
|
|
if (error) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
|
|
resolve(isValid);
|
|
});
|
|
});
|
|
}
|
|
/**
|
|
* @private
|
|
* @param {Compiler} compiler
|
|
* @param {Compilation} compilation
|
|
* @param {Buffer} source
|
|
* @returns {string}
|
|
*/
|
|
|
|
|
|
static getContentHash(compiler, compilation, source) {
|
|
const {
|
|
outputOptions
|
|
} = compilation;
|
|
const {
|
|
hashDigest,
|
|
hashDigestLength,
|
|
hashFunction,
|
|
hashSalt
|
|
} = outputOptions;
|
|
const hash = compiler.webpack.util.createHash(
|
|
/** @type {string} */
|
|
hashFunction);
|
|
|
|
if (hashSalt) {
|
|
hash.update(hashSalt);
|
|
}
|
|
|
|
hash.update(source);
|
|
const fullContentHash = hash.digest(hashDigest);
|
|
return fullContentHash.toString().slice(0, hashDigestLength);
|
|
}
|
|
/**
|
|
* @private
|
|
* @param {typeof import("globby").globby} globby
|
|
* @param {Compiler} compiler
|
|
* @param {Compilation} compilation
|
|
* @param {WebpackLogger} logger
|
|
* @param {CacheFacade} cache
|
|
* @param {ObjectPattern & { context: string }} inputPattern
|
|
* @param {number} index
|
|
* @returns {Promise<Array<CopiedResult | undefined> | undefined>}
|
|
*/
|
|
|
|
|
|
static async runPattern(globby, compiler, compilation, logger, cache, inputPattern, index) {
|
|
const {
|
|
RawSource
|
|
} = compiler.webpack.sources;
|
|
const pattern = { ...inputPattern
|
|
};
|
|
const originalFrom = pattern.from;
|
|
const normalizedOriginalFrom = path.normalize(originalFrom);
|
|
logger.log(`starting to process a pattern from '${normalizedOriginalFrom}' using '${pattern.context}' context`);
|
|
let absoluteFrom;
|
|
|
|
if (path.isAbsolute(normalizedOriginalFrom)) {
|
|
absoluteFrom = normalizedOriginalFrom;
|
|
} else {
|
|
absoluteFrom = path.resolve(pattern.context, normalizedOriginalFrom);
|
|
}
|
|
|
|
logger.debug(`getting stats for '${absoluteFrom}'...`);
|
|
const {
|
|
inputFileSystem
|
|
} = compiler;
|
|
let stats;
|
|
|
|
try {
|
|
stats = await stat(inputFileSystem, absoluteFrom);
|
|
} catch (error) {// Nothing
|
|
}
|
|
/**
|
|
* @type {"file" | "dir" | "glob"}
|
|
*/
|
|
|
|
|
|
let fromType;
|
|
|
|
if (stats) {
|
|
if (stats.isDirectory()) {
|
|
fromType = "dir";
|
|
logger.debug(`determined '${absoluteFrom}' is a directory`);
|
|
} else if (stats.isFile()) {
|
|
fromType = "file";
|
|
logger.debug(`determined '${absoluteFrom}' is a file`);
|
|
} else {
|
|
// Fallback
|
|
fromType = "glob";
|
|
logger.debug(`determined '${absoluteFrom}' is unknown`);
|
|
}
|
|
} else {
|
|
fromType = "glob";
|
|
logger.debug(`determined '${absoluteFrom}' is a glob`);
|
|
}
|
|
/** @type {GlobbyOptions & { objectMode: true }} */
|
|
|
|
|
|
const globOptions = { ...{
|
|
followSymbolicLinks: true
|
|
},
|
|
...(pattern.globOptions || {}),
|
|
...{
|
|
cwd: pattern.context,
|
|
objectMode: true
|
|
}
|
|
}; // @ts-ignore
|
|
|
|
globOptions.fs = inputFileSystem;
|
|
let glob;
|
|
|
|
switch (fromType) {
|
|
case "dir":
|
|
compilation.contextDependencies.add(absoluteFrom);
|
|
logger.debug(`added '${absoluteFrom}' as a context dependency`);
|
|
pattern.context = absoluteFrom;
|
|
glob = path.posix.join(fastGlob.escapePath(normalizePath(path.resolve(absoluteFrom))), "**/*");
|
|
absoluteFrom = path.join(absoluteFrom, "**/*");
|
|
|
|
if (typeof globOptions.dot === "undefined") {
|
|
globOptions.dot = true;
|
|
}
|
|
|
|
break;
|
|
|
|
case "file":
|
|
compilation.fileDependencies.add(absoluteFrom);
|
|
logger.debug(`added '${absoluteFrom}' as a file dependency`);
|
|
pattern.context = path.dirname(absoluteFrom);
|
|
glob = fastGlob.escapePath(normalizePath(path.resolve(absoluteFrom)));
|
|
|
|
if (typeof globOptions.dot === "undefined") {
|
|
globOptions.dot = true;
|
|
}
|
|
|
|
break;
|
|
|
|
case "glob":
|
|
default:
|
|
{
|
|
const contextDependencies = path.normalize(globParent(absoluteFrom));
|
|
compilation.contextDependencies.add(contextDependencies);
|
|
logger.debug(`added '${contextDependencies}' as a context dependency`);
|
|
glob = path.isAbsolute(originalFrom) ? originalFrom : path.posix.join(fastGlob.escapePath(normalizePath(path.resolve(pattern.context))), originalFrom);
|
|
}
|
|
}
|
|
|
|
logger.log(`begin globbing '${glob}'...`);
|
|
/**
|
|
* @type {GlobEntry[]}
|
|
*/
|
|
|
|
let globEntries;
|
|
|
|
try {
|
|
globEntries = await globby(glob, globOptions);
|
|
} catch (error) {
|
|
compilation.errors.push(
|
|
/** @type {WebpackError} */
|
|
error);
|
|
return;
|
|
}
|
|
|
|
if (globEntries.length === 0) {
|
|
if (pattern.noErrorOnMissing) {
|
|
logger.log(`finished to process a pattern from '${normalizedOriginalFrom}' using '${pattern.context}' context to '${pattern.to}'`);
|
|
return;
|
|
}
|
|
|
|
const missingError = new Error(`unable to locate '${glob}' glob`);
|
|
compilation.errors.push(
|
|
/** @type {WebpackError} */
|
|
missingError);
|
|
return;
|
|
}
|
|
/**
|
|
* @type {Array<CopiedResult | undefined>}
|
|
*/
|
|
|
|
|
|
let copiedResult;
|
|
|
|
try {
|
|
copiedResult = await Promise.all(globEntries.map(
|
|
/**
|
|
* @param {GlobEntry} globEntry
|
|
* @returns {Promise<CopiedResult | undefined>}
|
|
*/
|
|
async globEntry => {
|
|
// Exclude directories
|
|
if (!globEntry.dirent.isFile()) {
|
|
return;
|
|
}
|
|
|
|
if (pattern.filter) {
|
|
let isFiltered;
|
|
|
|
try {
|
|
isFiltered = await pattern.filter(globEntry.path);
|
|
} catch (error) {
|
|
compilation.errors.push(
|
|
/** @type {WebpackError} */
|
|
error);
|
|
return;
|
|
}
|
|
|
|
if (!isFiltered) {
|
|
logger.log(`skip '${globEntry.path}', because it was filtered`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const from = globEntry.path;
|
|
logger.debug(`found '${from}'`); // `globby`/`fast-glob` return the relative path when the path contains special characters on windows
|
|
|
|
const absoluteFilename = path.resolve(pattern.context, from);
|
|
const to = typeof pattern.to === "function" ? await pattern.to({
|
|
context: pattern.context,
|
|
absoluteFilename
|
|
}) : path.normalize(typeof pattern.to !== "undefined" ? pattern.to : "");
|
|
const toType = pattern.toType ? pattern.toType : template.test(to) ? "template" : path.extname(to) === "" || to.slice(-1) === path.sep ? "dir" : "file";
|
|
logger.log(`'to' option '${to}' determinated as '${toType}'`);
|
|
const relativeFrom = path.relative(pattern.context, absoluteFilename);
|
|
let filename = toType === "dir" ? path.join(to, relativeFrom) : to;
|
|
|
|
if (path.isAbsolute(filename)) {
|
|
filename = path.relative(
|
|
/** @type {string} */
|
|
compiler.options.output.path, filename);
|
|
}
|
|
|
|
logger.log(`determined that '${from}' should write to '${filename}'`);
|
|
const sourceFilename = normalizePath(path.relative(compiler.context, absoluteFilename)); // If this came from a glob or dir, add it to the file dependencies
|
|
|
|
if (fromType === "dir" || fromType === "glob") {
|
|
compilation.fileDependencies.add(absoluteFilename);
|
|
logger.debug(`added '${absoluteFilename}' as a file dependency`);
|
|
}
|
|
|
|
let cacheEntry;
|
|
logger.debug(`getting cache for '${absoluteFilename}'...`);
|
|
|
|
try {
|
|
cacheEntry = await cache.getPromise(`${sourceFilename}|${index}`, null);
|
|
} catch (error) {
|
|
compilation.errors.push(
|
|
/** @type {WebpackError} */
|
|
error);
|
|
return;
|
|
}
|
|
/**
|
|
* @type {Asset["source"] | undefined}
|
|
*/
|
|
|
|
|
|
let source;
|
|
|
|
if (cacheEntry) {
|
|
logger.debug(`found cache for '${absoluteFilename}'...`);
|
|
let isValidSnapshot;
|
|
logger.debug(`checking snapshot on valid for '${absoluteFilename}'...`);
|
|
|
|
try {
|
|
isValidSnapshot = await CopyPlugin.checkSnapshotValid(compilation, cacheEntry.snapshot);
|
|
} catch (error) {
|
|
compilation.errors.push(
|
|
/** @type {WebpackError} */
|
|
error);
|
|
return;
|
|
}
|
|
|
|
if (isValidSnapshot) {
|
|
logger.debug(`snapshot for '${absoluteFilename}' is valid`);
|
|
({
|
|
source
|
|
} = cacheEntry);
|
|
} else {
|
|
logger.debug(`snapshot for '${absoluteFilename}' is invalid`);
|
|
}
|
|
} else {
|
|
logger.debug(`missed cache for '${absoluteFilename}'`);
|
|
}
|
|
|
|
if (!source) {
|
|
const startTime = Date.now();
|
|
logger.debug(`reading '${absoluteFilename}'...`);
|
|
let data;
|
|
|
|
try {
|
|
data = await readFile(inputFileSystem, absoluteFilename);
|
|
} catch (error) {
|
|
compilation.errors.push(
|
|
/** @type {WebpackError} */
|
|
error);
|
|
return;
|
|
}
|
|
|
|
logger.debug(`read '${absoluteFilename}'`);
|
|
source = new RawSource(data);
|
|
let snapshot;
|
|
logger.debug(`creating snapshot for '${absoluteFilename}'...`);
|
|
|
|
try {
|
|
snapshot = await CopyPlugin.createSnapshot(compilation, startTime, absoluteFilename);
|
|
} catch (error) {
|
|
compilation.errors.push(
|
|
/** @type {WebpackError} */
|
|
error);
|
|
return;
|
|
}
|
|
|
|
if (snapshot) {
|
|
logger.debug(`created snapshot for '${absoluteFilename}'`);
|
|
logger.debug(`storing cache for '${absoluteFilename}'...`);
|
|
|
|
try {
|
|
await cache.storePromise(`${sourceFilename}|${index}`, null, {
|
|
source,
|
|
snapshot
|
|
});
|
|
} catch (error) {
|
|
compilation.errors.push(
|
|
/** @type {WebpackError} */
|
|
error);
|
|
return;
|
|
}
|
|
|
|
logger.debug(`stored cache for '${absoluteFilename}'`);
|
|
}
|
|
}
|
|
|
|
if (pattern.transform) {
|
|
/**
|
|
* @type {TransformerObject}
|
|
*/
|
|
const transformObj = typeof pattern.transform === "function" ? {
|
|
transformer: pattern.transform
|
|
} : pattern.transform;
|
|
|
|
if (transformObj.transformer) {
|
|
logger.log(`transforming content for '${absoluteFilename}'...`);
|
|
const buffer = source.buffer();
|
|
|
|
if (transformObj.cache) {
|
|
// TODO: remove in the next major release
|
|
const hasher = compiler.webpack && compiler.webpack.util && compiler.webpack.util.createHash ? compiler.webpack.util.createHash("xxhash64") : // eslint-disable-next-line global-require
|
|
require("crypto").createHash("md4");
|
|
const defaultCacheKeys = {
|
|
version,
|
|
sourceFilename,
|
|
transform: transformObj.transformer,
|
|
contentHash: hasher.update(buffer).digest("hex"),
|
|
index
|
|
};
|
|
const cacheKeys = `transform|${serialize(typeof transformObj.cache === "boolean" ? defaultCacheKeys : typeof transformObj.cache.keys === "function" ? await transformObj.cache.keys(defaultCacheKeys, absoluteFilename) : { ...defaultCacheKeys,
|
|
...transformObj.cache.keys
|
|
})}`;
|
|
logger.debug(`getting transformation cache for '${absoluteFilename}'...`);
|
|
const cacheItem = cache.getItemCache(cacheKeys, cache.getLazyHashedEtag(source));
|
|
source = await cacheItem.getPromise();
|
|
logger.debug(source ? `found transformation cache for '${absoluteFilename}'` : `no transformation cache for '${absoluteFilename}'`);
|
|
|
|
if (!source) {
|
|
const transformed = await transformObj.transformer(buffer, absoluteFilename);
|
|
source = new RawSource(transformed);
|
|
logger.debug(`caching transformation for '${absoluteFilename}'...`);
|
|
await cacheItem.storePromise(source);
|
|
logger.debug(`cached transformation for '${absoluteFilename}'`);
|
|
}
|
|
} else {
|
|
source = new RawSource(await transformObj.transformer(buffer, absoluteFilename));
|
|
}
|
|
}
|
|
}
|
|
|
|
let info = typeof pattern.info === "undefined" ? {} : typeof pattern.info === "function" ? pattern.info({
|
|
absoluteFilename,
|
|
sourceFilename,
|
|
filename,
|
|
toType
|
|
}) || {} : pattern.info || {};
|
|
|
|
if (toType === "template") {
|
|
logger.log(`interpolating template '${filename}' for '${sourceFilename}'...`);
|
|
const contentHash = CopyPlugin.getContentHash(compiler, compilation, source.buffer());
|
|
const ext = path.extname(sourceFilename);
|
|
const base = path.basename(sourceFilename);
|
|
const name = base.slice(0, base.length - ext.length);
|
|
const data = {
|
|
filename: normalizePath(path.relative(pattern.context, absoluteFilename)),
|
|
contentHash,
|
|
chunk: {
|
|
name,
|
|
id:
|
|
/** @type {string} */
|
|
sourceFilename,
|
|
hash: contentHash
|
|
}
|
|
};
|
|
const {
|
|
path: interpolatedFilename,
|
|
info: assetInfo
|
|
} = compilation.getPathWithInfo(normalizePath(filename), data);
|
|
info = { ...info,
|
|
...assetInfo
|
|
};
|
|
filename = interpolatedFilename;
|
|
logger.log(`interpolated template '${filename}' for '${sourceFilename}'`);
|
|
} else {
|
|
filename = normalizePath(filename);
|
|
} // eslint-disable-next-line consistent-return
|
|
|
|
|
|
return {
|
|
sourceFilename,
|
|
absoluteFilename,
|
|
filename,
|
|
source,
|
|
info,
|
|
force: pattern.force
|
|
};
|
|
}));
|
|
} catch (error) {
|
|
compilation.errors.push(
|
|
/** @type {WebpackError} */
|
|
error);
|
|
return;
|
|
}
|
|
|
|
if (copiedResult.length === 0) {
|
|
if (pattern.noErrorOnMissing) {
|
|
logger.log(`finished to process a pattern from '${normalizedOriginalFrom}' using '${pattern.context}' context to '${pattern.to}'`);
|
|
return;
|
|
}
|
|
|
|
const missingError = new Error(`unable to locate '${glob}' glob after filtering paths`);
|
|
compilation.errors.push(
|
|
/** @type {WebpackError} */
|
|
missingError);
|
|
return;
|
|
}
|
|
|
|
logger.log(`finished to process a pattern from '${normalizedOriginalFrom}' using '${pattern.context}' context`); // eslint-disable-next-line consistent-return
|
|
|
|
return copiedResult;
|
|
}
|
|
/**
|
|
* @param {Compiler} compiler
|
|
*/
|
|
|
|
|
|
apply(compiler) {
|
|
const pluginName = this.constructor.name;
|
|
compiler.hooks.thisCompilation.tap(pluginName, compilation => {
|
|
const logger = compilation.getLogger("copy-webpack-plugin");
|
|
const cache = compilation.getCache("CopyWebpackPlugin");
|
|
/**
|
|
* @type {typeof import("globby").globby}
|
|
*/
|
|
|
|
let globby;
|
|
compilation.hooks.processAssets.tapAsync({
|
|
name: "copy-webpack-plugin",
|
|
stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL
|
|
}, async (unusedAssets, callback) => {
|
|
if (typeof globby === "undefined") {
|
|
try {
|
|
// @ts-ignore
|
|
({
|
|
globby
|
|
} = await import("globby"));
|
|
} catch (error) {
|
|
callback(
|
|
/** @type {Error} */
|
|
error);
|
|
return;
|
|
}
|
|
}
|
|
|
|
logger.log("starting to add additional assets...");
|
|
const copiedResultMap = new Map();
|
|
/**
|
|
* @type {(() => Promise<void>)[]}
|
|
*/
|
|
|
|
const scheduledTasks = [];
|
|
this.patterns.map(
|
|
/**
|
|
* @param {Pattern} item
|
|
* @param {number} index
|
|
* @return {number}
|
|
*/
|
|
(item, index) => scheduledTasks.push(async () => {
|
|
/**
|
|
* @type {ObjectPattern}
|
|
*/
|
|
const normalizedPattern = typeof item === "string" ? {
|
|
from: item
|
|
} : { ...item
|
|
};
|
|
const context = typeof normalizedPattern.context === "undefined" ? compiler.context : path.isAbsolute(normalizedPattern.context) ? normalizedPattern.context : path.join(compiler.context, normalizedPattern.context);
|
|
normalizedPattern.context = context;
|
|
/**
|
|
* @type {Array<CopiedResult | undefined> | undefined}
|
|
*/
|
|
|
|
let copiedResult;
|
|
|
|
try {
|
|
copiedResult = await CopyPlugin.runPattern(globby, compiler, compilation, logger, cache,
|
|
/** @type {ObjectPattern & { context: string }} */
|
|
normalizedPattern, index);
|
|
} catch (error) {
|
|
compilation.errors.push(
|
|
/** @type {WebpackError} */
|
|
error);
|
|
return;
|
|
}
|
|
|
|
if (!copiedResult) {
|
|
return;
|
|
}
|
|
/**
|
|
* @type {Array<CopiedResult>}
|
|
*/
|
|
|
|
|
|
let filteredCopiedResult = copiedResult.filter(
|
|
/**
|
|
* @param {CopiedResult | undefined} result
|
|
* @returns {result is CopiedResult}
|
|
*/
|
|
result => Boolean(result));
|
|
|
|
if (typeof normalizedPattern.transformAll !== "undefined") {
|
|
if (typeof normalizedPattern.to === "undefined") {
|
|
compilation.errors.push(
|
|
/** @type {WebpackError} */
|
|
new Error(`Invalid "pattern.to" for the "pattern.from": "${normalizedPattern.from}" and "pattern.transformAll" function. The "to" option must be specified.`));
|
|
return;
|
|
}
|
|
|
|
filteredCopiedResult.sort((a, b) => a.absoluteFilename > b.absoluteFilename ? 1 : a.absoluteFilename < b.absoluteFilename ? -1 : 0);
|
|
const mergedEtag = filteredCopiedResult.length === 1 ? cache.getLazyHashedEtag(filteredCopiedResult[0].source) : filteredCopiedResult.reduce(
|
|
/**
|
|
* @param {Etag} accumulator
|
|
* @param {CopiedResult} asset
|
|
* @param {number} i
|
|
* @return {Etag}
|
|
*/
|
|
// @ts-ignore
|
|
(accumulator, asset, i) => {
|
|
// eslint-disable-next-line no-param-reassign
|
|
accumulator = cache.mergeEtags(i === 1 ? cache.getLazyHashedEtag(
|
|
/** @type {CopiedResult}*/
|
|
accumulator.source) : accumulator, cache.getLazyHashedEtag(asset.source));
|
|
return accumulator;
|
|
});
|
|
const cacheItem = cache.getItemCache(`transformAll|${serialize({
|
|
version,
|
|
from: normalizedPattern.from,
|
|
to: normalizedPattern.to,
|
|
transformAll: normalizedPattern.transformAll
|
|
})}`, mergedEtag);
|
|
let transformedAsset = await cacheItem.getPromise();
|
|
|
|
if (!transformedAsset) {
|
|
transformedAsset = {
|
|
filename: normalizedPattern.to
|
|
};
|
|
|
|
try {
|
|
transformedAsset.data = await normalizedPattern.transformAll(filteredCopiedResult.map(asset => {
|
|
return {
|
|
data: asset.source.buffer(),
|
|
sourceFilename: asset.sourceFilename,
|
|
absoluteFilename: asset.absoluteFilename
|
|
};
|
|
}));
|
|
} catch (error) {
|
|
compilation.errors.push(
|
|
/** @type {WebpackError} */
|
|
error);
|
|
return;
|
|
}
|
|
|
|
const filename = typeof normalizedPattern.to === "function" ? await normalizedPattern.to({
|
|
context
|
|
}) : normalizedPattern.to;
|
|
|
|
if (template.test(filename)) {
|
|
const contentHash = CopyPlugin.getContentHash(compiler, compilation, transformedAsset.data);
|
|
const {
|
|
path: interpolatedFilename,
|
|
info: assetInfo
|
|
} = compilation.getPathWithInfo(normalizePath(filename), {
|
|
contentHash,
|
|
chunk: {
|
|
id: "unknown-copied-asset",
|
|
hash: contentHash
|
|
}
|
|
});
|
|
transformedAsset.filename = interpolatedFilename;
|
|
transformedAsset.info = assetInfo;
|
|
}
|
|
|
|
const {
|
|
RawSource
|
|
} = compiler.webpack.sources;
|
|
transformedAsset.source = new RawSource(transformedAsset.data);
|
|
transformedAsset.force = normalizedPattern.force;
|
|
await cacheItem.storePromise(transformedAsset);
|
|
}
|
|
|
|
filteredCopiedResult = [transformedAsset];
|
|
}
|
|
|
|
const priority = normalizedPattern.priority || 0;
|
|
|
|
if (!copiedResultMap.has(priority)) {
|
|
copiedResultMap.set(priority, []);
|
|
}
|
|
|
|
copiedResultMap.get(priority).push(...filteredCopiedResult);
|
|
}));
|
|
await throttleAll(this.options.concurrency || 100, scheduledTasks);
|
|
const copiedResult = [...copiedResultMap.entries()].sort((a, b) => a[0] - b[0]); // Avoid writing assets inside `p-limit`, because it creates concurrency.
|
|
// It could potentially lead to an error - 'Multiple assets emit different content to the same filename'
|
|
|
|
copiedResult.reduce((acc, val) => acc.concat(val[1]), []).filter(Boolean).forEach(
|
|
/**
|
|
* @param {CopiedResult} result
|
|
* @returns {void}
|
|
*/
|
|
result => {
|
|
const {
|
|
absoluteFilename,
|
|
sourceFilename,
|
|
filename,
|
|
source,
|
|
force
|
|
} = result;
|
|
const existingAsset = compilation.getAsset(filename);
|
|
|
|
if (existingAsset) {
|
|
if (force) {
|
|
const info = {
|
|
copied: true,
|
|
sourceFilename
|
|
};
|
|
logger.log(`force updating '${filename}' from '${absoluteFilename}' to compilation assets, because it already exists...`);
|
|
compilation.updateAsset(filename, source, { ...info,
|
|
...result.info
|
|
});
|
|
logger.log(`force updated '${filename}' from '${absoluteFilename}' to compilation assets, because it already exists`);
|
|
return;
|
|
}
|
|
|
|
logger.log(`skip adding '${filename}' from '${absoluteFilename}' to compilation assets, because it already exists`);
|
|
return;
|
|
}
|
|
|
|
const info = {
|
|
copied: true,
|
|
sourceFilename
|
|
};
|
|
logger.log(`writing '${filename}' from '${absoluteFilename}' to compilation assets...`);
|
|
compilation.emitAsset(filename, source, { ...info,
|
|
...result.info
|
|
});
|
|
logger.log(`written '${filename}' from '${absoluteFilename}' to compilation assets`);
|
|
});
|
|
logger.log("finished to adding additional assets");
|
|
callback();
|
|
});
|
|
|
|
if (compilation.hooks.statsPrinter) {
|
|
compilation.hooks.statsPrinter.tap(pluginName, stats => {
|
|
stats.hooks.print.for("asset.info.copied").tap("copy-webpack-plugin", (copied, {
|
|
green,
|
|
formatFlag
|
|
}) => copied ?
|
|
/** @type {Function} */
|
|
green(
|
|
/** @type {Function} */
|
|
formatFlag("copied")) : "");
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
module.exports = CopyPlugin; |