diff --git a/lint-staged.config.js b/lint-staged.config.js index 5bf2aa6..ea206c9 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -1,4 +1,4 @@ module.exports = { "*": ["prettier --write --ignore-unknown"], - "*.{js,ts}": ["eslint --cache --fix"], + "*.{js}": ["eslint --cache --fix"], }; diff --git a/package-lock.json b/package-lock.json index 1a1ea5b..f67ab29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,9 @@ "@babel/preset-env": "^7.15.0", "@commitlint/cli": "^15.0.0", "@commitlint/config-conventional": "^15.0.0", + "@types/glob-parent": "^5.1.1", + "@types/normalize-path": "^3.0.0", + "@types/serialize-javascript": "^5.0.1", "@webpack-contrib/eslint-config-webpack": "^3.0.0", "babel-jest": "^27.0.6", "cross-env": "^7.0.3", @@ -41,10 +44,11 @@ "npm-run-all": "^4.1.5", "prettier": "^2.3.2", "standard-version": "^9.3.1", + "typescript": "^4.5.2", "webpack": "^5.64.1" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 12.20.0" }, "funding": { "type": "opencollective", @@ -3151,6 +3155,12 @@ "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", "dev": true }, + "node_modules/@types/glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-r2rwLjU4mYJmX3S81d8KxCboZOGPVEN5hvtYKzQ0aFNRhYir3DIVf8Hlznr65Wk748swi6hhccsDo3MyydHb2A==", + "dev": true + }, "node_modules/@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -3213,6 +3223,12 @@ "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", "dev": true }, + "node_modules/@types/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-Nd8y/5t/7CRakPYiyPzr/IAfYusy1FkcZYFEAcoMZkwpJv2n4Wm+olW+e7xBdHEXhOnWdG9ddbar0gqZWS4x5Q==", + "dev": true + }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -3225,6 +3241,12 @@ "integrity": "sha512-ekoj4qOQYp7CvjX8ZDBgN86w3MqQhLE1hczEJbEIjgFEumDy+na/4AJAbLXfgEWFNB2pKadM5rPFtuSGMWK7xA==", "dev": true }, + "node_modules/@types/serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-QqgTcm7IgIt/oWNFQMlpVv5Z3saYtxWK9yFrAUkk3jxvjbqIG835xNNoOYq12mXKQMuWGc+PgOXwXy92eax5BA==", + "dev": true + }, "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -12349,9 +12371,9 @@ } }, "node_modules/typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", + "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -15206,6 +15228,12 @@ "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", "dev": true }, + "@types/glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-r2rwLjU4mYJmX3S81d8KxCboZOGPVEN5hvtYKzQ0aFNRhYir3DIVf8Hlznr65Wk748swi6hhccsDo3MyydHb2A==", + "dev": true + }, "@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -15268,6 +15296,12 @@ "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", "dev": true }, + "@types/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-Nd8y/5t/7CRakPYiyPzr/IAfYusy1FkcZYFEAcoMZkwpJv2n4Wm+olW+e7xBdHEXhOnWdG9ddbar0gqZWS4x5Q==", + "dev": true + }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -15280,6 +15314,12 @@ "integrity": "sha512-ekoj4qOQYp7CvjX8ZDBgN86w3MqQhLE1hczEJbEIjgFEumDy+na/4AJAbLXfgEWFNB2pKadM5rPFtuSGMWK7xA==", "dev": true }, + "@types/serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-QqgTcm7IgIt/oWNFQMlpVv5Z3saYtxWK9yFrAUkk3jxvjbqIG835xNNoOYq12mXKQMuWGc+PgOXwXy92eax5BA==", + "dev": true + }, "@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -22115,9 +22155,9 @@ } }, "typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", + "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==", "dev": true }, "uglify-js": { diff --git a/package.json b/package.json index 140902b..a633a74 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "url": "https://opencollective.com/webpack" }, "main": "dist/cjs.js", + "types": "types/cjs.d.ts", "engines": { "node": ">= 12.20.0" }, @@ -19,11 +20,14 @@ "start": "npm run build -- -w", "clean": "del-cli dist", "prebuild": "npm run clean", - "build": "cross-env NODE_ENV=production babel src -d dist --copy-files", + "build:types": "tsc --declaration --emitDeclarationOnly --outDir types && prettier \"types/**/*.ts\" --write && prettier types --write", + "build:code": "cross-env NODE_ENV=production babel src -d dist --copy-files", + "build": "npm-run-all -p \"build:**\"", "commitlint": "commitlint --from=master", "security": "npm audit --production", "lint:prettier": "prettier --list-different .", "lint:js": "eslint --cache .", + "lint:types": "tsc --pretty --noEmit", "lint": "npm-run-all -l -p \"lint:**\"", "test:only": "cross-env NODE_ENV=test jest", "test:watch": "npm run test:only -- --watch", @@ -54,6 +58,9 @@ "@babel/preset-env": "^7.15.0", "@commitlint/cli": "^15.0.0", "@commitlint/config-conventional": "^15.0.0", + "@types/glob-parent": "^5.1.1", + "@types/normalize-path": "^3.0.0", + "@types/serialize-javascript": "^5.0.1", "@webpack-contrib/eslint-config-webpack": "^3.0.0", "babel-jest": "^27.0.6", "cross-env": "^7.0.3", @@ -72,6 +79,7 @@ "npm-run-all": "^4.1.5", "prettier": "^2.3.2", "standard-version": "^9.3.1", + "typescript": "^4.5.2", "webpack": "^5.64.1" }, "keywords": [ diff --git a/src/index.js b/src/index.js index 3647cc1..9a7f9d7 100644 --- a/src/index.js +++ b/src/index.js @@ -13,23 +13,164 @@ import { readFile, stat, throttleAll } from "./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} WebpackLogger */ +/** @typedef {ReturnType} CacheFacade */ +/** @typedef {ReturnType["getLazyHashedEtag"]>} Etag */ +/** @typedef {ReturnType} 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 {{ [key: string]: string }} info + */ + +/** + * @typedef {string} StringPattern + */ + +/** + * @typedef {boolean} NoErrorOnMissing + */ + +/** + * @typedef {string} Context + */ + +/** + * @typedef {string} From + */ + +/** + * @callback ToFunction + * @param {{ context: string, absoluteFilename?: string }} pathData + * @return {string} + */ + +/** + * @typedef {string | ToFunction} To + */ + +/** + * @typedef {"dir" | "file" | "template"} ToType + */ + +/** + * @callback TransformerFunction + * @param {Buffer} input + * @param {string} absoluteFilename + */ + +/** + * @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 + */ + +/** + * @callback TransformAllFunction + * @param {{ data: Buffer, sourceFilename: string, absoluteFilename: string }[]} data + */ + +/** + * @typedef { { [key: string]: string } | ((item: { absoluteFilename: string, sourceFilename: string, filename: string, toType: ToType }) => { [key: string]: string }) } 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 { - constructor(options = {}) { - validate(schema, options, { + /** + * @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} + */ 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 @@ -48,6 +189,12 @@ class CopyPlugin { }); } + /** + * @private + * @param {Compilation} compilation + * @param {Snapshot} snapshot + * @returns {Promise} + */ static async checkSnapshotValid(compilation, snapshot) { // eslint-disable-next-line consistent-return return new Promise((resolve, reject) => { @@ -66,11 +213,20 @@ class CopyPlugin { }); } + /** + * @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(hashFunction); + const hash = compiler.webpack.util.createHash( + /** @type {string} */ (hashFunction) + ); if (hashSalt) { hash.update(hashSalt); @@ -80,9 +236,20 @@ class CopyPlugin { const fullContentHash = hash.digest(hashDigest); - return fullContentHash.slice(0, hashDigestLength); + 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 | undefined>} + */ static async runPattern( globby, compiler, @@ -93,518 +260,530 @@ class CopyPlugin { index ) { const { RawSource } = compiler.webpack.sources; - const pattern = - typeof inputPattern === "string" - ? { from: inputPattern } - : { ...inputPattern }; - - pattern.fromOrigin = pattern.from; - pattern.from = path.normalize(pattern.from); - pattern.context = - typeof pattern.context === "undefined" - ? compiler.context - : path.isAbsolute(pattern.context) - ? pattern.context - : path.join(compiler.context, pattern.context); + const pattern = { ...inputPattern }; + const originalFrom = pattern.from; + const normalizedOriginalFrom = path.normalize(originalFrom); logger.log( - `starting to process a pattern from '${pattern.from}' using '${pattern.context}' context` + `starting to process a pattern from '${normalizedOriginalFrom}' using '${pattern.context}' context` ); - if (path.isAbsolute(pattern.from)) { - pattern.absoluteFrom = pattern.from; + let absoluteFrom; + + if (path.isAbsolute(normalizedOriginalFrom)) { + absoluteFrom = normalizedOriginalFrom; } else { - pattern.absoluteFrom = path.resolve(pattern.context, pattern.from); + absoluteFrom = path.resolve(pattern.context, normalizedOriginalFrom); } - logger.debug(`getting stats for '${pattern.absoluteFrom}'...`); + logger.debug(`getting stats for '${absoluteFrom}'...`); const { inputFileSystem } = compiler; let stats; try { - stats = await stat(inputFileSystem, pattern.absoluteFrom); + stats = await stat(inputFileSystem, absoluteFrom); } catch (error) { // Nothing } + /** + * @type {"file" | "dir" | "glob"} + */ + let fromType; + if (stats) { if (stats.isDirectory()) { - pattern.fromType = "dir"; - logger.debug(`determined '${pattern.absoluteFrom}' is a directory`); + fromType = "dir"; + logger.debug(`determined '${absoluteFrom}' is a directory`); } else if (stats.isFile()) { - pattern.fromType = "file"; - logger.debug(`determined '${pattern.absoluteFrom}' is a file`); + fromType = "file"; + logger.debug(`determined '${absoluteFrom}' is a file`); } else { - logger.debug(`determined '${pattern.absoluteFrom}' is a glob`); + // Fallback + fromType = "glob"; + logger.debug(`determined '${absoluteFrom}' is unknown`); } + } else { + fromType = "glob"; + logger.debug(`determined '${absoluteFrom}' is a glob`); } - // eslint-disable-next-line no-param-reassign - pattern.globOptions = { + /** @type {GlobbyOptions & { objectMode: true }} */ + const globOptions = { ...{ followSymbolicLinks: true }, ...(pattern.globOptions || {}), ...{ cwd: pattern.context, objectMode: true }, }; - pattern.globOptions.fs = inputFileSystem; + // @ts-ignore + globOptions.fs = inputFileSystem; + + let glob; - switch (pattern.fromType) { + switch (fromType) { case "dir": - compilation.contextDependencies.add(pattern.absoluteFrom); + compilation.contextDependencies.add(absoluteFrom); - logger.debug(`added '${pattern.absoluteFrom}' as a context dependency`); + logger.debug(`added '${absoluteFrom}' as a context dependency`); - /* eslint-disable no-param-reassign */ - pattern.context = pattern.absoluteFrom; - pattern.glob = path.posix.join( - fastGlob.escapePath( - normalizePath(path.resolve(pattern.absoluteFrom)) - ), + pattern.context = absoluteFrom; + glob = path.posix.join( + fastGlob.escapePath(normalizePath(path.resolve(absoluteFrom))), "**/*" ); - pattern.absoluteFrom = path.join(pattern.absoluteFrom, "**/*"); + absoluteFrom = path.join(absoluteFrom, "**/*"); - if (typeof pattern.globOptions.dot === "undefined") { - pattern.globOptions.dot = true; + if (typeof globOptions.dot === "undefined") { + globOptions.dot = true; } - /* eslint-enable no-param-reassign */ break; case "file": - compilation.fileDependencies.add(pattern.absoluteFrom); + compilation.fileDependencies.add(absoluteFrom); - logger.debug(`added '${pattern.absoluteFrom}' as a file dependency`); + logger.debug(`added '${absoluteFrom}' as a file dependency`); - /* eslint-disable no-param-reassign */ - pattern.context = path.dirname(pattern.absoluteFrom); - pattern.glob = fastGlob.escapePath( - normalizePath(path.resolve(pattern.absoluteFrom)) - ); + pattern.context = path.dirname(absoluteFrom); + glob = fastGlob.escapePath(normalizePath(path.resolve(absoluteFrom))); - if (typeof pattern.globOptions.dot === "undefined") { - pattern.globOptions.dot = true; + if (typeof globOptions.dot === "undefined") { + globOptions.dot = true; } - /* eslint-enable no-param-reassign */ break; + case "glob": default: { - const contextDependencies = path.normalize( - globParent(pattern.absoluteFrom) - ); + const contextDependencies = path.normalize(globParent(absoluteFrom)); compilation.contextDependencies.add(contextDependencies); logger.debug(`added '${contextDependencies}' as a context dependency`); - /* eslint-disable no-param-reassign */ - pattern.fromType = "glob"; - pattern.glob = path.isAbsolute(pattern.fromOrigin) - ? pattern.fromOrigin + glob = path.isAbsolute(originalFrom) + ? originalFrom : path.posix.join( fastGlob.escapePath(normalizePath(path.resolve(pattern.context))), - pattern.fromOrigin + originalFrom ); - /* eslint-enable no-param-reassign */ } } - logger.log(`begin globbing '${pattern.glob}'...`); + logger.log(`begin globbing '${glob}'...`); - let paths; + /** + * @type {GlobEntry[]} + */ + let globEntries; try { - paths = await globby(pattern.glob, pattern.globOptions); + globEntries = await globby(glob, globOptions); } catch (error) { - compilation.errors.push(error); + compilation.errors.push(/** @type {WebpackError} */ (error)); return; } - if (paths.length === 0) { + if (globEntries.length === 0) { if (pattern.noErrorOnMissing) { logger.log( - `finished to process a pattern from '${pattern.from}' using '${pattern.context}' context to '${pattern.to}'` + `finished to process a pattern from '${normalizedOriginalFrom}' using '${pattern.context}' context to '${pattern.to}'` ); return; } - const missingError = new Error(`unable to locate '${pattern.glob}' glob`); + const missingError = new Error(`unable to locate '${glob}' glob`); - compilation.errors.push(missingError); + compilation.errors.push(/** @type {WebpackError} */ (missingError)); return; } - const filteredPaths = ( - await Promise.all( - paths.map(async (item) => { - // Exclude directories - if (!item.dirent.isFile()) { - return false; - } - - if (pattern.filter) { - let isFiltered; + /** + * @type {Array} + */ + let copiedResult; - try { - isFiltered = await pattern.filter(item.path); - } catch (error) { - compilation.errors.push(error); - - return false; - } - - if (!isFiltered) { - logger.log(`skip '${item.path}', because it was filtered`); + try { + copiedResult = await Promise.all( + globEntries.map( + /** + * @param {GlobEntry} globEntry + * @returns {Promise} + */ + async (globEntry) => { + // Exclude directories + if (!globEntry.dirent.isFile()) { + return; } - return isFiltered ? item : false; - } - - return item; - }) - ) - ).filter((item) => item); - - if (filteredPaths.length === 0) { - if (pattern.noErrorOnMissing) { - logger.log( - `finished to process a pattern from '${pattern.from}' using '${pattern.context}' context to '${pattern.to}'` - ); + if (pattern.filter) { + let isFiltered; - return; - } + try { + isFiltered = await pattern.filter(globEntry.path); + } catch (error) { + compilation.errors.push(/** @type {WebpackError} */ (error)); - const missingError = new Error( - `unable to locate '${pattern.glob}' glob after filtering paths` - ); + return; + } - compilation.errors.push(missingError); + if (!isFiltered) { + logger.log(`skip '${globEntry.path}', because it was filtered`); - return; - } + return; + } + } - const files = await Promise.all( - filteredPaths.map(async (item) => { - const from = item.path; + const from = globEntry.path; - logger.debug(`found '${from}'`); + 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); + // `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; - pattern.to = - typeof pattern.to === "function" - ? await pattern.to({ context: pattern.context, absoluteFilename }) - : path.normalize( - typeof pattern.to !== "undefined" ? pattern.to : "" + if (path.isAbsolute(filename)) { + filename = path.relative( + /** @type {string} */ (compiler.options.output.path), + filename ); + } - const isToDirectory = - path.extname(pattern.to) === "" || pattern.to.slice(-1) === path.sep; - - const toType = pattern.toType - ? pattern.toType - : template.test(pattern.to) - ? "template" - : isToDirectory - ? "dir" - : "file"; - - logger.log(`'to' option '${pattern.to}' determinated as '${toType}'`); - - const relativeFrom = path.relative(pattern.context, absoluteFilename); - let filename = - toType === "dir" ? path.join(pattern.to, relativeFrom) : pattern.to; - - if (path.isAbsolute(filename)) { - filename = path.relative(compiler.options.output.path, filename); - } - - logger.log(`determined that '${from}' should write to '${filename}'`); - - const sourceFilename = normalizePath( - path.relative(compiler.context, absoluteFilename) - ); - - return { - absoluteFilename, - sourceFilename, - filename, - toType, - }; - }) - ); - - let assets; - - try { - assets = await Promise.all( - files.map(async (file) => { - const { absoluteFilename, sourceFilename, filename, toType } = file; - const info = - typeof pattern.info === "function" - ? pattern.info(file) || {} - : pattern.info || {}; - const result = { - absoluteFilename, - sourceFilename, - filename, - force: pattern.force, - info, - }; - - // If this came from a glob or dir, add it to the file dependencies - if (pattern.fromType === "dir" || pattern.fromType === "glob") { - compilation.fileDependencies.add(absoluteFilename); - - logger.debug(`added '${absoluteFilename}' as a file dependency`); - } - - let cacheEntry; - - logger.debug(`getting cache for '${absoluteFilename}'...`); + logger.log( + `determined that '${from}' should write to '${filename}'` + ); - try { - cacheEntry = await cache.getPromise( - `${sourceFilename}|${index}`, - null + const sourceFilename = normalizePath( + path.relative(compiler.context, absoluteFilename) ); - } catch (error) { - compilation.errors.push(error); - return; - } + // If this came from a glob or dir, add it to the file dependencies + if (fromType === "dir" || fromType === "glob") { + compilation.fileDependencies.add(absoluteFilename); - if (cacheEntry) { - logger.debug(`found cache for '${absoluteFilename}'...`); + logger.debug(`added '${absoluteFilename}' as a file dependency`); + } - let isValidSnapshot; + let cacheEntry; - logger.debug( - `checking snapshot on valid for '${absoluteFilename}'...` - ); + logger.debug(`getting cache for '${absoluteFilename}'...`); try { - isValidSnapshot = await CopyPlugin.checkSnapshotValid( - compilation, - cacheEntry.snapshot + cacheEntry = await cache.getPromise( + `${sourceFilename}|${index}`, + null ); } catch (error) { - compilation.errors.push(error); + compilation.errors.push(/** @type {WebpackError} */ (error)); return; } - if (isValidSnapshot) { - logger.debug(`snapshot for '${absoluteFilename}' is valid`); + /** + * @type {Asset["source"] | undefined} + */ + let source; - result.source = cacheEntry.source; - } else { - logger.debug(`snapshot for '${absoluteFilename}' is invalid`); - } - } else { - logger.debug(`missed cache for '${absoluteFilename}'`); - } + if (cacheEntry) { + logger.debug(`found cache for '${absoluteFilename}'...`); - if (!result.source) { - const startTime = Date.now(); + let isValidSnapshot; - logger.debug(`reading '${absoluteFilename}'...`); + logger.debug( + `checking snapshot on valid for '${absoluteFilename}'...` + ); - let data; + try { + isValidSnapshot = await CopyPlugin.checkSnapshotValid( + compilation, + cacheEntry.snapshot + ); + } catch (error) { + compilation.errors.push(/** @type {WebpackError} */ (error)); - try { - data = await readFile(inputFileSystem, absoluteFilename); - } catch (error) { - compilation.errors.push(error); + return; + } - 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}'`); } - logger.debug(`read '${absoluteFilename}'`); + if (!source) { + const startTime = Date.now(); - result.source = new RawSource(data); + logger.debug(`reading '${absoluteFilename}'...`); - let snapshot; + let data; - logger.debug(`creating snapshot for '${absoluteFilename}'...`); + try { + data = await readFile(inputFileSystem, absoluteFilename); + } catch (error) { + compilation.errors.push(/** @type {WebpackError} */ (error)); - try { - snapshot = await CopyPlugin.createSnapshot( - compilation, - startTime, - absoluteFilename - ); - } catch (error) { - compilation.errors.push(error); + return; + } - return; - } + logger.debug(`read '${absoluteFilename}'`); + + source = new RawSource(data); - if (snapshot) { - logger.debug(`created snapshot for '${absoluteFilename}'`); - logger.debug(`storing cache for '${absoluteFilename}'...`); + let snapshot; + + logger.debug(`creating snapshot for '${absoluteFilename}'...`); try { - await cache.storePromise(`${sourceFilename}|${index}`, null, { - source: result.source, - snapshot, - }); + snapshot = await CopyPlugin.createSnapshot( + compilation, + startTime, + absoluteFilename + ); } catch (error) { - compilation.errors.push(error); + compilation.errors.push(/** @type {WebpackError} */ (error)); return; } - logger.debug(`stored cache for '${absoluteFilename}'`); - } - } + if (snapshot) { + logger.debug(`created snapshot for '${absoluteFilename}'`); + logger.debug(`storing cache for '${absoluteFilename}'...`); - if (pattern.transform) { - const transform = - typeof pattern.transform === "function" - ? { transformer: pattern.transform } - : pattern.transform; - - if (transform.transformer) { - logger.log(`transforming content for '${absoluteFilename}'...`); - - const buffer = result.source.buffer(); - - if (transform.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: transform.transformer, - contentHash: hasher.update(buffer).digest("hex"), - index, - }; - const cacheKeys = `transform|${serialize( - typeof transform.cache.keys === "function" - ? await transform.cache.keys( - defaultCacheKeys, - absoluteFilename - ) - : { ...defaultCacheKeys, ...pattern.transform.cache.keys } - )}`; + try { + await cache.storePromise(`${sourceFilename}|${index}`, null, { + source, + snapshot, + }); + } catch (error) { + compilation.errors.push(/** @type {WebpackError} */ (error)); - logger.debug( - `getting transformation cache for '${absoluteFilename}'...` - ); + return; + } - const cacheItem = cache.getItemCache( - cacheKeys, - cache.getLazyHashedEtag(result.source) - ); + logger.debug(`stored cache for '${absoluteFilename}'`); + } + } - result.source = await cacheItem.getPromise(); + 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( - result.source - ? `found transformation cache for '${absoluteFilename}'` - : `no transformation cache for '${absoluteFilename}'` - ); + logger.debug( + `getting transformation cache for '${absoluteFilename}'...` + ); - if (!result.source) { - const transformed = await transform.transformer( - buffer, - absoluteFilename + const cacheItem = cache.getItemCache( + cacheKeys, + cache.getLazyHashedEtag(source) ); - result.source = new RawSource(transformed); + source = await cacheItem.getPromise(); logger.debug( - `caching transformation for '${absoluteFilename}'...` + source + ? `found transformation cache for '${absoluteFilename}'` + : `no transformation cache for '${absoluteFilename}'` ); - await cacheItem.storePromise(result.source); + if (!source) { + const transformed = await transformObj.transformer( + buffer, + absoluteFilename + ); - logger.debug( - `cached transformation for '${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) ); } - } else { - result.source = new RawSource( - await transform.transformer(buffer, absoluteFilename) - ); } } - } - if (toType === "template") { - logger.log( - `interpolating template '${filename}' for '${sourceFilename}'...` - ); + 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, - result.source.buffer() - ); - const ext = path.extname(result.sourceFilename); - const base = path.basename(result.sourceFilename); - const name = base.slice(0, base.length - ext.length); - const data = { - filename: normalizePath( - path.relative(pattern.context, absoluteFilename) - ), - contentHash, - chunk: { - name, - id: result.sourceFilename, - hash: contentHash, + 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, - }, - }; - const { path: interpolatedFilename, info: assetInfo } = - compilation.getPathWithInfo(normalizePath(result.filename), data); + chunk: { + name, + id: /** @type {string} */ (sourceFilename), + hash: contentHash, + }, + }; + const { path: interpolatedFilename, info: assetInfo } = + compilation.getPathWithInfo(normalizePath(filename), data); + + info = { ...info, ...assetInfo }; + filename = interpolatedFilename; - result.info = { ...result.info, ...assetInfo }; - result.filename = interpolatedFilename; + logger.log( + `interpolated template '${filename}' for '${sourceFilename}'` + ); + } else { + filename = normalizePath(filename); + } - logger.log( - `interpolated template '${filename}' for '${sourceFilename}'` - ); - } else { - // eslint-disable-next-line no-param-reassign - result.filename = normalizePath(result.filename); + // eslint-disable-next-line consistent-return + return { + sourceFilename, + absoluteFilename, + filename, + source, + info, + force: pattern.force, + }; } - - // eslint-disable-next-line consistent-return - return result; - }) + ) ); } catch (error) { - compilation.errors.push(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 '${pattern.from}' using '${pattern.context}' context to '${pattern.to}'` + `finished to process a pattern from '${normalizedOriginalFrom}' using '${pattern.context}' context` ); // eslint-disable-next-line consistent-return - return assets; + 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( @@ -615,9 +794,10 @@ class CopyPlugin { async (unusedAssets, callback) => { if (typeof globby === "undefined") { try { + // @ts-ignore ({ globby } = await import("globby")); } catch (error) { - callback(error); + callback(/** @type {Error} */ (error)); return; } @@ -625,42 +805,87 @@ class CopyPlugin { logger.log("starting to add additional assets..."); - const assetMap = new Map(); + const copiedResultMap = new Map(); + /** + * @type {(() => Promise)[]} + */ const scheduledTasks = []; - this.patterns.map((item, index) => - scheduledTasks.push(async () => { - let assets; + 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 | 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)); - try { - assets = await CopyPlugin.runPattern( - globby, - compiler, - compilation, - logger, - cache, - item, - index - ); - } catch (error) { - compilation.errors.push(error); + return; + } - return; - } + if (!copiedResult) { + return; + } - if (assets && assets.length > 0) { - if (item.transformAll) { - if (typeof item.to === "undefined") { + /** + * @type {Array} + */ + 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( - new Error( - `Invalid "pattern.to" for the "pattern.from": "${item.from}" and "pattern.transformAll" function. The "to" option must be specified.` + /** @type {WebpackError} */ + ( + new Error( + `Invalid "pattern.to" for the "pattern.from": "${normalizedPattern.from}" and "pattern.transformAll" function. The "to" option must be specified.` + ) ) ); return; } - assets.sort((a, b) => + filteredCopiedResult.sort((a, b) => a.absoluteFilename > b.absoluteFilename ? 1 : a.absoluteFilename < b.absoluteFilename @@ -669,52 +894,71 @@ class CopyPlugin { ); const mergedEtag = - assets.length === 1 - ? cache.getLazyHashedEtag(assets[0].source.buffer()) - : assets.reduce((accumulator, asset, i) => { - // eslint-disable-next-line no-param-reassign - accumulator = cache.mergeEtags( - i === 1 - ? cache.getLazyHashedEtag( - accumulator.source.buffer() - ) - : accumulator, - cache.getLazyHashedEtag(asset.source.buffer()) - ); - - return accumulator; - }); - - const cacheKeys = `transformAll|${serialize({ - version, - from: item.from, - to: item.to, - transformAll: item.transformAll, - })}`; - const eTag = cache.getLazyHashedEtag(mergedEtag); - const cacheItem = cache.getItemCache(cacheKeys, eTag); + 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: item.to }; + transformedAsset = { filename: normalizedPattern.to }; try { - transformedAsset.data = await item.transformAll( - assets.map((asset) => { - return { - data: asset.source.buffer(), - sourceFilename: asset.sourceFilename, - absoluteFilename: asset.absoluteFilename, - }; - }) - ); + transformedAsset.data = + await normalizedPattern.transformAll( + filteredCopiedResult.map((asset) => { + return { + data: asset.source.buffer(), + sourceFilename: asset.sourceFilename, + absoluteFilename: asset.absoluteFilename, + }; + }) + ); } catch (error) { - compilation.errors.push(error); + compilation.errors.push( + /** @type {WebpackError} */ (error) + ); return; } - if (template.test(item.to)) { + const filename = + typeof normalizedPattern.to === "function" + ? normalizedPattern.to({ context }) + : normalizedPattern.to; + + if (template.test(filename)) { const contentHash = CopyPlugin.getContentHash( compiler, compilation, @@ -722,11 +966,11 @@ class CopyPlugin { ); const { path: interpolatedFilename, info: assetInfo } = - compilation.getPathWithInfo(normalizePath(item.to), { + compilation.getPathWithInfo(normalizePath(filename), { contentHash, chunk: { + id: "unknown-copied-asset", hash: contentHash, - contentHash, }, }); @@ -739,87 +983,94 @@ class CopyPlugin { transformedAsset.source = new RawSource( transformedAsset.data ); - transformedAsset.force = item.force; + transformedAsset.force = normalizedPattern.force; await cacheItem.storePromise(transformedAsset); } - assets = [transformedAsset]; + filteredCopiedResult = [transformedAsset]; } - const priority = item.priority || 0; + const priority = normalizedPattern.priority || 0; - if (!assetMap.has(priority)) { - assetMap.set(priority, []); + if (!copiedResultMap.has(priority)) { + copiedResultMap.set(priority, []); } - assetMap.get(priority).push(...assets); - } - }) + copiedResultMap.get(priority).push(...filteredCopiedResult); + }) ); await throttleAll(this.options.concurrency || 100, scheduledTasks); - const assets = [...assetMap.entries()].sort((a, b) => a[0] - b[0]); + 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' - assets + copiedResult .reduce((acc, val) => acc.concat(val[1]), []) .filter(Boolean) - .forEach((asset) => { - const { - absoluteFilename, - sourceFilename, - filename, - source, - force, - } = asset; + .forEach( + /** + * @param {CopiedResult} result + * @returns {void} + */ + (result) => { + const { + absoluteFilename, + sourceFilename, + filename, + source, + force, + } = result; - const existingAsset = compilation.getAsset(filename); + const existingAsset = compilation.getAsset(filename); - if (existingAsset) { - if (force) { - const info = { copied: true, sourceFilename }; + if (existingAsset) { + if (force) { + const info = { copied: true, sourceFilename }; - logger.log( - `force updating '${filename}' from '${absoluteFilename}' to compilation assets, because it already exists...` - ); + logger.log( + `force updating '${filename}' from '${absoluteFilename}' to compilation assets, because it already exists...` + ); - compilation.updateAsset(filename, source, { - ...info, - ...asset.info, - }); + compilation.updateAsset(filename, source, { + ...info, + ...result.info, + }); + + logger.log( + `force updated '${filename}' from '${absoluteFilename}' to compilation assets, because it already exists` + ); + + return; + } logger.log( - `force updated '${filename}' from '${absoluteFilename}' to compilation assets, because it already exists` + `skip adding '${filename}' from '${absoluteFilename}' to compilation assets, because it already exists` ); return; } + const info = { copied: true, sourceFilename }; + logger.log( - `skip adding '${filename}' from '${absoluteFilename}' to compilation assets, because it already exists` + `writing '${filename}' from '${absoluteFilename}' to compilation assets...` ); - return; - } - - const info = { copied: true, sourceFilename }; - - logger.log( - `writing '${filename}' from '${absoluteFilename}' to compilation assets...` - ); - - compilation.emitAsset(filename, source, { - ...info, - ...asset.info, - }); + compilation.emitAsset(filename, source, { + ...info, + ...result.info, + }); - logger.log( - `written '${filename}' from '${absoluteFilename}' to compilation assets` - ); - }); + logger.log( + `written '${filename}' from '${absoluteFilename}' to compilation assets` + ); + } + ); logger.log("finished to adding additional assets"); @@ -832,8 +1083,11 @@ class CopyPlugin { stats.hooks.print .for("asset.info.copied") .tap("copy-webpack-plugin", (copied, { green, formatFlag }) => - // eslint-disable-next-line no-undefined - copied ? green(formatFlag("copied")) : undefined + copied + ? /** @type {Function} */ (green)( + /** @type {Function} */ (formatFlag)("copied") + ) + : "" ); }); } diff --git a/src/options.json b/src/options.json index ae9f96d..d8bd40b 100644 --- a/src/options.json +++ b/src/options.json @@ -115,9 +115,6 @@ } ] }, - "transformPath": { - "instanceof": "Function" - }, "noErrorOnMissing": { "type": "boolean", "description": "Doesn't generate an error on missing file(s).", diff --git a/src/utils.js b/src/utils.js index 90deea6..0ec4355 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,27 +1,66 @@ +/** @typedef {import("webpack").Compilation["inputFileSystem"] } InputFileSystem */ +/** @typedef {import("fs").Stats } Stats */ + +/** + * @param {InputFileSystem} inputFileSystem + * @param {string} path + * @return {Promise} + */ function stat(inputFileSystem, path) { return new Promise((resolve, reject) => { - inputFileSystem.stat(path, (err, stats) => { - if (err) { - reject(err); + inputFileSystem.stat( + path, + /** + * @param {null | undefined | NodeJS.ErrnoException} err + * @param {undefined | Stats} stats + */ + // @ts-ignore + (err, stats) => { + if (err) { + reject(err); + + return; + } + + resolve(stats); } - resolve(stats); - }); + ); }); } +/** + * @param {InputFileSystem} inputFileSystem + * @param {string} path + * @return {Promise} + */ function readFile(inputFileSystem, path) { return new Promise((resolve, reject) => { - inputFileSystem.readFile(path, (err, stats) => { - if (err) { - reject(err); + inputFileSystem.readFile( + path, + /** + * @param {null | undefined | NodeJS.ErrnoException} err + * @param {undefined | string | Buffer} data + */ + (err, data) => { + if (err) { + reject(err); + + return; + } + + resolve(/** @type {string | Buffer} */ (data)); } - resolve(stats); - }); + ); }); } const notSettled = Symbol(`not-settled`); +/** + * @template T + * @typedef {() => Promise} Task + */ + /** * Run tasks with limited concurency. * @template T diff --git a/test/__snapshots__/CopyPlugin.test.js.snap b/test/__snapshots__/CopyPlugin.test.js.snap index 5fe1132..509579f 100644 --- a/test/__snapshots__/CopyPlugin.test.js.snap +++ b/test/__snapshots__/CopyPlugin.test.js.snap @@ -201,7 +201,7 @@ Object { "determined that './fixtures/directory/nested/deep-nested/deepnested.txt' should write to 'nested/deep-nested/deepnested.txt'", "determined that './fixtures/directory/nested/nestedfile.txt' should write to 'nested/nestedfile.txt'", "finished to adding additional assets", - "finished to process a pattern from 'directory' using './fixtures/directory' context to '.'", + "finished to process a pattern from 'directory' using './fixtures/directory' context", "found './fixtures/directory/.dottedfile'", "found './fixtures/directory/directoryfile.txt'", "found './fixtures/directory/nested/deep-nested/deepnested.txt'", @@ -256,7 +256,7 @@ Object { "determined './fixtures/file.txt' is a file", "determined that './fixtures/file.txt' should write to 'file.txt'", "finished to adding additional assets", - "finished to process a pattern from 'file.txt' using './fixtures' context to '.'", + "finished to process a pattern from 'file.txt' using './fixtures' context", "found './fixtures/file.txt'", "getting cache for './fixtures/file.txt'...", "getting stats for './fixtures/file.txt'...", @@ -290,11 +290,12 @@ Object { "creating snapshot for './fixtures/directory/directoryfile.txt'...", "creating snapshot for './fixtures/directory/nested/deep-nested/deepnested.txt'...", "creating snapshot for './fixtures/directory/nested/nestedfile.txt'...", + "determined './fixtures/directory/**' is a glob", "determined that './fixtures/directory/directoryfile.txt' should write to 'directory/directoryfile.txt'", "determined that './fixtures/directory/nested/deep-nested/deepnested.txt' should write to 'directory/nested/deep-nested/deepnested.txt'", "determined that './fixtures/directory/nested/nestedfile.txt' should write to 'directory/nested/nestedfile.txt'", "finished to adding additional assets", - "finished to process a pattern from 'directory/**' using './fixtures' context to '.'", + "finished to process a pattern from 'directory/**' using './fixtures' context", "found './fixtures/directory/directoryfile.txt'", "found './fixtures/directory/nested/deep-nested/deepnested.txt'", "found './fixtures/directory/nested/nestedfile.txt'", @@ -340,7 +341,7 @@ Object { "determined './fixtures/file.txt' is a file", "determined that './fixtures/file.txt' should write to 'newFile.txt'", "finished to adding additional assets", - "finished to process a pattern from 'file.txt' using './fixtures' context to 'newFile.txt'", + "finished to process a pattern from 'file.txt' using './fixtures' context", "found './fixtures/file.txt'", "getting cache for './fixtures/file.txt'...", "getting stats for './fixtures/file.txt'...", diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap index 274df21..f4ff97b 100644 --- a/test/__snapshots__/validate-options.test.js.snap +++ b/test/__snapshots__/validate-options.test.js.snap @@ -16,7 +16,7 @@ exports[`validate options should throw an error on the "options" option with "{" exports[`validate options should throw an error on the "patterns" option with "" value 1`] = ` "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. - options.patterns should be an array: - [non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" `; exports[`validate options should throw an error on the "patterns" option with "[""]" value 1`] = ` @@ -46,7 +46,7 @@ exports[`validate options should throw an error on the "patterns" option with "[ exports[`validate options should throw an error on the "patterns" option with "[{"from":"dir","info":"string"}]" value 1`] = ` "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. - options.patterns[0] should be one of these: - non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, transformPath?, noErrorOnMissing? } + non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, noErrorOnMissing? } Details: * options.patterns[0].info should be one of these: object { … } | function @@ -61,7 +61,7 @@ exports[`validate options should throw an error on the "patterns" option with "[ exports[`validate options should throw an error on the "patterns" option with "[{"from":"dir","info":true}]" value 1`] = ` "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. - options.patterns[0] should be one of these: - non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, transformPath?, noErrorOnMissing? } + non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, noErrorOnMissing? } Details: * options.patterns[0].info should be one of these: object { … } | function @@ -104,7 +104,7 @@ exports[`validate options should throw an error on the "patterns" option with "[ exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","context":"context","transform":true}]" value 1`] = ` "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. - options.patterns[0] should be one of these: - non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, transformPath?, noErrorOnMissing? } + non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, noErrorOnMissing? } Details: * options.patterns[0].transform should be one of these: function | object { transformer?, cache? } @@ -154,7 +154,7 @@ exports[`validate options should throw an error on the "patterns" option with "[ exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":true,"context":"context"}]" value 1`] = ` "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. - options.patterns[0] should be one of these: - non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, transformPath?, noErrorOnMissing? } + non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, noErrorOnMissing? } Details: * options.patterns[0].to should be one of these: string | function @@ -190,25 +190,25 @@ exports[`validate options should throw an error on the "patterns" option with "[ exports[`validate options should throw an error on the "patterns" option with "{}" value 1`] = ` "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. - options.patterns should be an array: - [non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" `; exports[`validate options should throw an error on the "patterns" option with "true" value 1`] = ` "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. - options.patterns should be an array: - [non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" `; exports[`validate options should throw an error on the "patterns" option with "true" value 2`] = ` "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. - options.patterns should be an array: - [non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" `; exports[`validate options should throw an error on the "patterns" option with "undefined" value 1`] = ` "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. - options misses the property 'patterns'. Should be: - [non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" `; exports[`validate options should throw an error on the "unknown" option with "/test/" value 1`] = ` diff --git a/test/transformAll-option.test.js b/test/transformAll-option.test.js index 93d72f4..2d0cf82 100644 --- a/test/transformAll-option.test.js +++ b/test/transformAll-option.test.js @@ -161,13 +161,37 @@ describe("transformAll option", () => { from: "**/*.txt", to: "[contenthash]-[fullhash]-file.txt", transformAll(assets) { - const result = assets.reduce((accumulator, asset) => { + return assets.reduce((accumulator, asset) => { // eslint-disable-next-line no-param-reassign accumulator = `${accumulator}${asset.data}::`; return accumulator; }, ""); + }, + }, + ], + }) + .then(done) + .catch(done); + }); - return result; + it("should interpolate [fullhash] and [contenthash] #2", (done) => { + runEmit({ + expectedAssetKeys: ["4333a40fa67dfaaaefc9-47e8bdc316eff74b2d6e-file.txt"], + expectedAssetContent: { + "4333a40fa67dfaaaefc9-47e8bdc316eff74b2d6e-file.txt": + "::special::new::::::::::new::::::new::", + }, + patterns: [ + { + from: "**/*.txt", + to: () => "[contenthash]-[fullhash]-file.txt", + transformAll(assets) { + return assets.reduce((accumulator, asset) => { + // eslint-disable-next-line no-param-reassign + accumulator = `${accumulator}${asset.data}::`; + + return accumulator; + }, ""); }, }, ], diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..fb41806 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "esnext", + "moduleResolution": "node", + "allowJs": true, + "checkJs": true, + "strict": true, + "types": ["node"], + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true + }, + "include": ["./src/**/*"] +} diff --git a/types/src/cjs.d.ts b/types/src/cjs.d.ts new file mode 100644 index 0000000..2cb80fb --- /dev/null +++ b/types/src/cjs.d.ts @@ -0,0 +1,3 @@ +declare const _exports: typeof plugin.default; +export = _exports; +import plugin = require("./index"); diff --git a/types/src/index.d.ts b/types/src/index.d.ts new file mode 100644 index 0000000..4920c2f --- /dev/null +++ b/types/src/index.d.ts @@ -0,0 +1,259 @@ +export default CopyPlugin; +export type Schema = import("schema-utils/declarations/validate").Schema; +export type Compiler = import("webpack").Compiler; +export type Compilation = import("webpack").Compilation; +export type WebpackError = import("webpack").WebpackError; +export type Asset = import("webpack").Asset; +export type GlobbyOptions = import("globby").Options; +export type GlobEntry = import("globby").GlobEntry; +export type WebpackLogger = ReturnType; +export type CacheFacade = ReturnType; +export type Etag = ReturnType< + ReturnType["getLazyHashedEtag"] +>; +export type Snapshot = ReturnType< + Compilation["fileSystemInfo"]["mergeSnapshots"] +>; +export type Force = boolean; +export type CopiedResult = { + sourceFilename: string; + absoluteFilename: string; + filename: string; + source: Asset["source"]; + force: Force | undefined; + info: { + [key: string]: string; + }; +}; +export type StringPattern = string; +export type NoErrorOnMissing = boolean; +export type Context = string; +export type From = string; +export type ToFunction = (pathData: { + context: string; + absoluteFilename?: string; +}) => string; +export type To = string | ToFunction; +export type ToType = "dir" | "file" | "template"; +export type TransformerFunction = ( + input: Buffer, + absoluteFilename: string +) => any; +export type TransformerCacheObject = + | { + keys: { + [key: string]: any; + }; + } + | { + keys: ( + defaultCacheKeys: { + [key: string]: any; + }, + absoluteFilename: string + ) => Promise<{ + [key: string]: any; + }>; + }; +export type TransformerObject = { + transformer: TransformerFunction; + cache?: boolean | TransformerCacheObject | undefined; +}; +export type Transform = TransformerFunction | TransformerObject; +export type Filter = (filepath: string) => any; +export type TransformAllFunction = ( + data: { + data: Buffer; + sourceFilename: string; + absoluteFilename: string; + }[] +) => any; +export type Info = + | { + [key: string]: string; + } + | ((item: { + absoluteFilename: string; + sourceFilename: string; + filename: string; + toType: ToType; + }) => { + [key: string]: string; + }); +export type ObjectPattern = { + from: From; + globOptions?: import("globby").Options | undefined; + context?: string | undefined; + to?: To | undefined; + toType?: ToType | undefined; + info?: Info | undefined; + filter?: Filter | undefined; + transform?: Transform | undefined; + transformAll?: TransformAllFunction | undefined; + force?: boolean | undefined; + priority?: number | undefined; + noErrorOnMissing?: boolean | undefined; +}; +export type Pattern = StringPattern | ObjectPattern; +export type AdditionalOptions = { + concurrency?: number | undefined; +}; +export type PluginOptions = { + patterns: Pattern[]; + options?: AdditionalOptions | undefined; +}; +/** @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} WebpackLogger */ +/** @typedef {ReturnType} CacheFacade */ +/** @typedef {ReturnType["getLazyHashedEtag"]>} Etag */ +/** @typedef {ReturnType} 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 {{ [key: string]: string }} info + */ +/** + * @typedef {string} StringPattern + */ +/** + * @typedef {boolean} NoErrorOnMissing + */ +/** + * @typedef {string} Context + */ +/** + * @typedef {string} From + */ +/** + * @callback ToFunction + * @param {{ context: string, absoluteFilename?: string }} pathData + * @return {string} + */ +/** + * @typedef {string | ToFunction} To + */ +/** + * @typedef {"dir" | "file" | "template"} ToType + */ +/** + * @callback TransformerFunction + * @param {Buffer} input + * @param {string} absoluteFilename + */ +/** + * @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 + */ +/** + * @callback TransformAllFunction + * @param {{ data: Buffer, sourceFilename: string, absoluteFilename: string }[]} data + */ +/** + * @typedef { { [key: string]: string } | ((item: { absoluteFilename: string, sourceFilename: string, filename: string, toType: ToType }) => { [key: string]: string }) } 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] + */ +declare class CopyPlugin { + /** + * @private + * @param {Compilation} compilation + * @param {number} startTime + * @param {string} dependency + * @returns {Promise} + */ + private static createSnapshot; + /** + * @private + * @param {Compilation} compilation + * @param {Snapshot} snapshot + * @returns {Promise} + */ + private static checkSnapshotValid; + /** + * @private + * @param {Compiler} compiler + * @param {Compilation} compilation + * @param {Buffer} source + * @returns {string} + */ + private static getContentHash; + /** + * @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 | undefined>} + */ + private static runPattern; + /** + * @param {PluginOptions} [options] + */ + constructor(options?: PluginOptions | undefined); + /** + * @private + * @type {Pattern[]} + */ + private patterns; + /** + * @private + * @type {AdditionalOptions} + */ + private options; + /** + * @param {Compiler} compiler + */ + apply(compiler: Compiler): void; +} diff --git a/types/src/utils.d.ts b/types/src/utils.d.ts new file mode 100644 index 0000000..ac61248 --- /dev/null +++ b/types/src/utils.d.ts @@ -0,0 +1,35 @@ +export type InputFileSystem = import("webpack").Compilation["inputFileSystem"]; +export type Stats = import("fs").Stats; +export type Task = () => Promise; +/** @typedef {import("webpack").Compilation["inputFileSystem"] } InputFileSystem */ +/** @typedef {import("fs").Stats } Stats */ +/** + * @param {InputFileSystem} inputFileSystem + * @param {string} path + * @return {Promise} + */ +export function stat( + inputFileSystem: InputFileSystem, + path: string +): Promise; +/** + * @param {InputFileSystem} inputFileSystem + * @param {string} path + * @return {Promise} + */ +export function readFile( + inputFileSystem: InputFileSystem, + path: string +): Promise; +/** + * @template T + * @typedef {() => Promise} Task + */ +/** + * Run tasks with limited concurency. + * @template T + * @param {number} limit - Limit of tasks that run at once. + * @param {Task[]} tasks - List of tasks to run. + * @returns {Promise} A promise that fulfills to an array of the results + */ +export function throttleAll(limit: number, tasks: Task[]): Promise;