├── test ├── fixtures │ ├── file.txt │ ├── noextension │ ├── watch │ │ ├── .gitkeep │ │ ├── _t1 │ │ │ ├── .gitkeep │ │ │ └── directory │ │ │ │ └── .gitkeep │ │ ├── _t2 │ │ │ ├── .gitkeep │ │ │ └── directory │ │ │ │ └── .gitkeep │ │ ├── _t3 │ │ │ ├── .gitkeep │ │ │ └── directory │ │ │ │ └── .gitkeep │ │ ├── _t4 │ │ │ ├── .gitkeep │ │ │ └── directory │ │ │ │ └── .gitkeep │ │ ├── _t5 │ │ │ ├── .gitkeep │ │ │ └── directory │ │ │ │ └── .gitkeep │ │ └── directory │ │ │ └── .gitkeep │ ├── .file.txt │ ├── binextension.bin │ ├── dir (86) │ │ ├── file.txt │ │ └── nesteddir │ │ │ ├── nestedfile.txt │ │ │ └── deepnesteddir │ │ │ └── deepnesteddir.txt │ ├── symlink │ │ ├── directory │ │ │ ├── file.txt │ │ │ └── nested-directory │ │ │ │ └── file-in-nested-directory.txt │ │ ├── file-ln.txt │ │ ├── file.txt │ │ └── directory-ln │ ├── directory │ │ ├── directoryfile.txt │ │ ├── nested │ │ │ ├── nestedfile.txt │ │ │ └── deep-nested │ │ │ │ └── deepnested.txt │ │ └── .dottedfile │ ├── [(){}[]!+@escaped-test^$] │ │ └── hello.txt │ └── file.txt.gz ├── helpers │ ├── enter.js │ ├── built-in-modules │ │ ├── process.js │ │ ├── fs.js │ │ ├── url.js │ │ ├── path.js │ │ ├── util.js │ │ └── stream.js │ ├── enter-with-asset-modules.js │ ├── removeIllegalCharacterForWindows.js │ ├── index.js │ ├── compile.js │ ├── readAssets.js │ ├── readAsset.js │ ├── PreCopyPlugin.js │ ├── ChildCompiler.js │ ├── getCompiler.js │ ├── BreakContenthashPlugin.js │ └── run.js ├── __snapshots__ │ ├── transformAll-option.test.js.snap │ ├── validate-options.test.js.snap │ └── CopyPlugin.test.js.snap ├── noErrorOnMissing.test.js ├── filter-option.test.js ├── toType-option.test.js ├── priority-option.test.js ├── info-option.test.js ├── context-option.test.js ├── transformAll-option.test.js ├── transform-option.test.js ├── force-option.test.js ├── validate-options.test.js ├── globOptions-option.test.js ├── from-option.test.js └── to-option.test.js ├── .husky ├── pre-commit └── commit-msg ├── .gitattributes ├── .prettierignore ├── jest.config.js ├── lint-staged.config.js ├── commitlint.config.js ├── .editorconfig ├── tsconfig.json ├── .gitignore ├── .github └── workflows │ ├── dependency-review.yml │ └── nodejs.yml ├── babel.config.js ├── eslint.config.mjs ├── globalSetup.js ├── .cspell.json ├── LICENSE ├── types ├── utils.d.ts └── index.d.ts ├── package.json └── src ├── utils.js └── options.json /test/fixtures/file.txt: -------------------------------------------------------------------------------- 1 | new -------------------------------------------------------------------------------- /test/fixtures/noextension: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/watch/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | lint-staged 2 | -------------------------------------------------------------------------------- /test/fixtures/.file.txt: -------------------------------------------------------------------------------- 1 | dot 2 | -------------------------------------------------------------------------------- /test/fixtures/binextension.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/dir (86)/file.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/watch/_t1/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/watch/_t2/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/watch/_t3/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/watch/_t4/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/watch/_t5/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/watch/directory/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | commitlint --edit $1 2 | -------------------------------------------------------------------------------- /test/fixtures/symlink/directory/file.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/symlink/file-ln.txt: -------------------------------------------------------------------------------- 1 | file.txt -------------------------------------------------------------------------------- /test/fixtures/symlink/file.txt: -------------------------------------------------------------------------------- 1 | data 2 | -------------------------------------------------------------------------------- /test/fixtures/watch/_t1/directory/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/watch/_t2/directory/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/watch/_t3/directory/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/watch/_t4/directory/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/watch/_t5/directory/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/dir (86)/nesteddir/nestedfile.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/directory/directoryfile.txt: -------------------------------------------------------------------------------- 1 | new -------------------------------------------------------------------------------- /test/fixtures/directory/nested/nestedfile.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/symlink/directory-ln: -------------------------------------------------------------------------------- 1 | ./directory/ -------------------------------------------------------------------------------- /test/fixtures/[(){}[]!+@escaped-test^$]/hello.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/helpers/enter.js: -------------------------------------------------------------------------------- 1 | // Entry point for tests 2 | -------------------------------------------------------------------------------- /test/fixtures/directory/nested/deep-nested/deepnested.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/directory/.dottedfile: -------------------------------------------------------------------------------- 1 | dottedfile contents 2 | -------------------------------------------------------------------------------- /test/fixtures/dir (86)/nesteddir/deepnesteddir/deepnesteddir.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/helpers/built-in-modules/process.js: -------------------------------------------------------------------------------- 1 | module.exports = process; 2 | -------------------------------------------------------------------------------- /test/fixtures/symlink/directory/nested-directory/file-in-nested-directory.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/helpers/built-in-modules/fs.js: -------------------------------------------------------------------------------- 1 | module.exports = require("node:fs"); 2 | -------------------------------------------------------------------------------- /test/helpers/built-in-modules/url.js: -------------------------------------------------------------------------------- 1 | module.exports = require("node:url"); 2 | -------------------------------------------------------------------------------- /test/helpers/built-in-modules/path.js: -------------------------------------------------------------------------------- 1 | module.exports = require("node:path"); 2 | -------------------------------------------------------------------------------- /test/helpers/built-in-modules/util.js: -------------------------------------------------------------------------------- 1 | module.exports = require("node:util"); 2 | -------------------------------------------------------------------------------- /test/helpers/built-in-modules/stream.js: -------------------------------------------------------------------------------- 1 | module.exports = require("node:stream"); 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | package-lock.json -diff 2 | * text=auto 3 | bin/* eol=lf 4 | yarn.lock -diff 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /dist 3 | /node_modules 4 | /test/fixtures 5 | /test/bundled 6 | CHANGELOG.md -------------------------------------------------------------------------------- /test/fixtures/file.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/copy-webpack-plugin/main/test/fixtures/file.txt.gz -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: "node", 3 | globalSetup: "/globalSetup.js", 4 | }; 5 | -------------------------------------------------------------------------------- /test/helpers/enter-with-asset-modules.js: -------------------------------------------------------------------------------- 1 | export { default } from "../fixtures/directory/nested/deep-nested/deepnested.txt"; 2 | -------------------------------------------------------------------------------- /test/helpers/removeIllegalCharacterForWindows.js: -------------------------------------------------------------------------------- 1 | module.exports = (string) => 2 | process.platform !== "win32" ? string : string.replaceAll(/[*?"<>|]/g, ""); 3 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "*": [ 3 | "prettier --cache --write --ignore-unknown", 4 | "cspell --cache --no-must-find-files", 5 | ], 6 | "*.js": ["eslint --cache --fix"], 7 | }; 8 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | rules: { 4 | "header-max-length": [0], 5 | "body-max-line-length": [0], 6 | "footer-max-line-length": [0], 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /test/helpers/index.js: -------------------------------------------------------------------------------- 1 | export { default as compile } from "./compile"; 2 | export { default as readAsset } from "./readAsset"; 3 | export { default as getCompiler } from "./getCompiler"; 4 | export { default as readAssets } from "./readAssets"; 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /test/helpers/compile.js: -------------------------------------------------------------------------------- 1 | export default (compiler) => 2 | new Promise((resolve, reject) => { 3 | compiler.run((error, stats) => { 4 | if (error) { 5 | return reject(error); 6 | } 7 | 8 | return resolve({ stats, compiler }); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "allowJs": true, 6 | "checkJs": true, 7 | "strict": true, 8 | "types": ["node"], 9 | "resolveJsonModule": true, 10 | "allowSyntheticDefaultImports": true 11 | }, 12 | "include": ["./src/**/*"] 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | .eslintcache 5 | .cspellcache 6 | 7 | /coverage 8 | /dist 9 | /local 10 | /reports 11 | /node_modules 12 | /test/fixtures/\[special\$directory\] 13 | /test/fixtures/watch/**/*.txt 14 | /test/outputs 15 | /test/bundled 16 | 17 | .DS_Store 18 | Thumbs.db 19 | .idea 20 | .vscode 21 | *.sublime-project 22 | *.sublime-workspace 23 | *.iml 24 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | name: "Dependency Review" 2 | on: [pull_request] 3 | 4 | permissions: 5 | contents: read 6 | 7 | jobs: 8 | dependency-review: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: "Checkout Repository" 12 | uses: actions/checkout@v5 13 | - name: "Dependency Review" 14 | uses: actions/dependency-review-action@v4 15 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const MIN_BABEL_VERSION = 7; 2 | 3 | module.exports = (api) => { 4 | api.assertVersion(MIN_BABEL_VERSION); 5 | api.cache(true); 6 | 7 | return { 8 | presets: [ 9 | [ 10 | "@babel/preset-env", 11 | { 12 | exclude: ["proposal-dynamic-import"], 13 | targets: { 14 | node: "18.12.0", 15 | }, 16 | }, 17 | ], 18 | ], 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "eslint/config"; 2 | import configs from "eslint-config-webpack/configs.js"; 3 | import eslintPluginJest from "eslint-plugin-jest"; 4 | 5 | export default defineConfig([ 6 | { 7 | extends: [configs["recommended-dirty"]], 8 | plugins: { 9 | jest: eslintPluginJest, 10 | }, 11 | rules: { 12 | "jest/expect-expect": [ 13 | "error", 14 | { 15 | assertFunctionNames: ["expect", "runEmit", "runForce", "runChange"], 16 | }, 17 | ], 18 | }, 19 | }, 20 | ]); 21 | -------------------------------------------------------------------------------- /test/helpers/readAssets.js: -------------------------------------------------------------------------------- 1 | import readAsset from "./readAsset"; 2 | 3 | /** 4 | * @param {import("webpack").Compiler} compiler The webpack compiler instance 5 | * @param {{ compilation: { assets: { [key: string]: unknown } } }} stats The webpack stats object 6 | * @returns {{ [key: string]: unknown }} An object mapping asset names to their content 7 | */ 8 | export default function readAssets(compiler, stats) { 9 | const assets = {}; 10 | 11 | for (const asset of Object.keys(stats.compilation.assets).filter( 12 | (a) => a !== "main.js", 13 | )) { 14 | assets[asset] = readAsset(asset, compiler, stats); 15 | } 16 | 17 | return assets; 18 | } 19 | -------------------------------------------------------------------------------- /test/helpers/readAsset.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | export default (asset, compiler, stats) => { 4 | const usedFs = compiler.outputFileSystem; 5 | const outputPath = stats.compilation.outputOptions.path; 6 | 7 | let data = ""; 8 | let targetFile = asset; 9 | 10 | const queryStringIdx = targetFile.indexOf("?"); 11 | 12 | if (queryStringIdx >= 0) { 13 | targetFile = targetFile.slice(0, queryStringIdx); 14 | } 15 | 16 | try { 17 | const isArchive = /.gz$/i.test(targetFile); 18 | data = usedFs.readFileSync(path.join(outputPath, targetFile)); 19 | 20 | if (!isArchive) { 21 | data = data.toString(); 22 | } 23 | } catch (error) { 24 | data = error.toString(); 25 | } 26 | 27 | return data; 28 | }; 29 | -------------------------------------------------------------------------------- /test/helpers/PreCopyPlugin.js: -------------------------------------------------------------------------------- 1 | class PreCopyPlugin { 2 | constructor(options = {}) { 3 | this.options = options.options || {}; 4 | } 5 | 6 | apply(compiler) { 7 | const plugin = { name: "PreCopyPlugin" }; 8 | 9 | compiler.hooks.thisCompilation.tap(plugin, (compilation) => { 10 | compilation.hooks.additionalAssets.tapAsync( 11 | "pre-copy-webpack-plugin", 12 | (callback) => { 13 | for (const { name, data, info } of this.options.additionalAssets) { 14 | const { RawSource } = compiler.webpack.sources; 15 | const source = new RawSource(data); 16 | 17 | compilation.emitAsset(name, source, info); 18 | } 19 | 20 | callback(); 21 | }, 22 | ); 23 | }); 24 | } 25 | } 26 | 27 | export default PreCopyPlugin; 28 | -------------------------------------------------------------------------------- /globalSetup.js: -------------------------------------------------------------------------------- 1 | const fs = require("node:fs"); 2 | const path = require("node:path"); 3 | 4 | const removeIllegalCharacterForWindows = require("./test/helpers/removeIllegalCharacterForWindows"); 5 | 6 | const baseDir = path.resolve(__dirname, "test/fixtures"); 7 | 8 | const specialFiles = { 9 | "[special$directory]/nested/nestedfile.txt": "", 10 | "[special$directory]/(special-*file).txt": "special", 11 | "[special$directory]/directoryfile.txt": "new", 12 | }; 13 | 14 | module.exports = () => { 15 | for (const originFile of Object.keys(specialFiles)) { 16 | const file = removeIllegalCharacterForWindows(originFile); 17 | const dir = path.dirname(file); 18 | 19 | fs.mkdirSync(path.join(baseDir, dir), { recursive: true }); 20 | fs.writeFileSync(path.join(baseDir, file), specialFiles[originFile]); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /test/__snapshots__/transformAll-option.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing 2 | 3 | exports[`cache should work with the "memory" cache: assets 1`] = ` 4 | { 5 | "file.txt": "new::directory/nested/deep-nested/deepnested.txt::directory/nested/nestedfile.txt::", 6 | } 7 | `; 8 | 9 | exports[`cache should work with the "memory" cache: assets 2`] = ` 10 | { 11 | "file.txt": "new::directory/nested/deep-nested/deepnested.txt::directory/nested/nestedfile.txt::", 12 | } 13 | `; 14 | 15 | exports[`cache should work with the "memory" cache: errors 1`] = `[]`; 16 | 17 | exports[`cache should work with the "memory" cache: errors 2`] = `[]`; 18 | 19 | exports[`cache should work with the "memory" cache: warnings 1`] = `[]`; 20 | 21 | exports[`cache should work with the "memory" cache: warnings 2`] = `[]`; 22 | -------------------------------------------------------------------------------- /test/helpers/ChildCompiler.js: -------------------------------------------------------------------------------- 1 | export default class ChildCompiler { 2 | apply(compiler) { 3 | compiler.hooks.make.tapAsync("Child Compiler", (compilation, callback) => { 4 | const outputOptions = { 5 | filename: "output.js", 6 | publicPath: compilation.outputOptions.publicPath, 7 | }; 8 | const childCompiler = compilation.createChildCompiler( 9 | "ChildCompiler", 10 | outputOptions, 11 | ); 12 | childCompiler.runAsChild((error, entries, childCompilation) => { 13 | if (error) { 14 | throw error; 15 | } 16 | 17 | const assets = childCompilation.getAssets(); 18 | 19 | if (assets.length > 0) { 20 | callback( 21 | new Error("Copy plugin should not be ran in child compilations"), 22 | ); 23 | 24 | return; 25 | } 26 | 27 | callback(); 28 | }); 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/helpers/getCompiler.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | import { Volume, createFsFromVolume } from "memfs"; 4 | import webpack from "webpack"; 5 | 6 | export default (config = {}) => { 7 | const fullConfig = { 8 | mode: "development", 9 | context: path.resolve(__dirname, "../fixtures"), 10 | entry: path.resolve(__dirname, "../helpers/enter.js"), 11 | output: { 12 | path: path.resolve(__dirname, "../build"), 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.txt/, 18 | type: "asset/resource", 19 | generator: { 20 | filename: "asset-modules/[name][ext]", 21 | }, 22 | }, 23 | ], 24 | }, 25 | ...config, 26 | }; 27 | 28 | const compiler = webpack(fullConfig); 29 | 30 | if (!config.outputFileSystem) { 31 | compiler.outputFileSystem = createFsFromVolume(new Volume()); 32 | } 33 | 34 | return compiler; 35 | }; 36 | -------------------------------------------------------------------------------- /.cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "language": "en,en-gb", 4 | "words": [ 5 | "eslintcache", 6 | "commitlint", 7 | "nestedfile", 8 | "directoryfile", 9 | "globby", 10 | "posix", 11 | "newfile", 12 | "fullhash", 13 | "deepnested", 14 | "subdir", 15 | "newdirectory", 16 | "nesteddir", 17 | "Globby", 18 | "determinated", 19 | "Etags", 20 | "newchanged", 21 | "dottedfile", 22 | "mathes", 23 | "newext", 24 | "noextension", 25 | "binextension", 26 | "tempdir", 27 | "memfs", 28 | "enry", 29 | "globstar", 30 | "deepnesteddir", 31 | "globstar", 32 | "bazz", 33 | "newbinextension", 34 | "behavour", 35 | "dottedfile", 36 | "tempfile" 37 | ], 38 | "ignorePaths": [ 39 | "CHANGELOG.md", 40 | "package.json", 41 | "dist/**", 42 | "**/__snapshots__/**", 43 | "package-lock.json", 44 | "node_modules", 45 | "coverage", 46 | "*.log", 47 | "test/fixtures/**" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /test/helpers/BreakContenthashPlugin.js: -------------------------------------------------------------------------------- 1 | class BreakContenthashPlugin { 2 | constructor(options = {}) { 3 | this.options = options.options || {}; 4 | } 5 | 6 | apply(compiler) { 7 | const plugin = { name: "BrokeContenthashPlugin" }; 8 | 9 | compiler.hooks.thisCompilation.tap(plugin, (compilation) => { 10 | compilation.hooks.processAssets.tapAsync( 11 | { 12 | name: "broken-contenthash-webpack-plugin", 13 | stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE, 14 | }, 15 | (unusedAssets, callback) => { 16 | for (const { name, newName, newHash } of this.options.targetAssets) { 17 | const asset = compilation.getAsset(name); 18 | 19 | compilation.updateAsset(asset.name, asset.source, { 20 | ...asset.info, 21 | contenthash: newHash, 22 | }); 23 | compilation.renameAsset(asset.name, newName); 24 | } 25 | 26 | callback(); 27 | }, 28 | ); 29 | }); 30 | } 31 | } 32 | 33 | export default BreakContenthashPlugin; 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /test/noErrorOnMissing.test.js: -------------------------------------------------------------------------------- 1 | import { runEmit } from "./helpers/run"; 2 | 3 | describe("noErrorOnMissing option", () => { 4 | describe("is a file", () => { 5 | it("should work", (done) => { 6 | runEmit({ 7 | expectedAssetKeys: [], 8 | patterns: [ 9 | { 10 | from: "unknown.unknown", 11 | noErrorOnMissing: true, 12 | }, 13 | ], 14 | }) 15 | .then(done) 16 | .catch(done); 17 | }); 18 | }); 19 | 20 | describe("is a directory", () => { 21 | it("should work", (done) => { 22 | runEmit({ 23 | expectedAssetKeys: [], 24 | patterns: [ 25 | { 26 | from: "unknown", 27 | noErrorOnMissing: true, 28 | }, 29 | ], 30 | }) 31 | .then(done) 32 | .catch(done); 33 | }); 34 | }); 35 | 36 | describe("is a glob", () => { 37 | it("should work", (done) => { 38 | runEmit({ 39 | expectedAssetKeys: [], 40 | patterns: [ 41 | { 42 | from: "*.unknown", 43 | noErrorOnMissing: true, 44 | }, 45 | ], 46 | }) 47 | .then(done) 48 | .catch(done); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/filter-option.test.js: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | 3 | import { runEmit } from "./helpers/run"; 4 | 5 | describe('"filter" option', () => { 6 | it("should work, copy files and filter some of them", (done) => { 7 | runEmit({ 8 | expectedAssetKeys: [ 9 | ".dottedfile", 10 | "nested/deep-nested/deepnested.txt", 11 | "nested/nestedfile.txt", 12 | ], 13 | patterns: [ 14 | { 15 | from: "directory", 16 | filter: (resourcePath) => { 17 | if (/directoryfile\.txt$/.test(resourcePath)) { 18 | return false; 19 | } 20 | 21 | return true; 22 | }, 23 | }, 24 | ], 25 | }) 26 | .then(done) 27 | .catch(done); 28 | }); 29 | 30 | it("should work, copy files and filter some of them using async function", (done) => { 31 | runEmit({ 32 | expectedAssetKeys: [ 33 | ".dottedfile", 34 | "nested/deep-nested/deepnested.txt", 35 | "nested/nestedfile.txt", 36 | ], 37 | patterns: [ 38 | { 39 | from: "directory", 40 | filter: async (resourcePath) => { 41 | const data = await fs.promises.readFile(resourcePath); 42 | const content = data.toString(); 43 | 44 | if (content === "new") { 45 | return false; 46 | } 47 | 48 | return true; 49 | }, 50 | }, 51 | ], 52 | }) 53 | .then(done) 54 | .catch(done); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /types/utils.d.ts: -------------------------------------------------------------------------------- 1 | export type InputFileSystem = import("webpack").Compilation["inputFileSystem"]; 2 | export type Stats = import("fs").Stats; 3 | export type Task = () => Promise; 4 | /** 5 | * @template T 6 | * @param {(() => unknown) | undefined} fn The function to memoize. 7 | * @returns {() => T} A memoized function that returns the result of the original function. 8 | */ 9 | export function memoize(fn: (() => unknown) | undefined): () => T; 10 | /** 11 | * @param {InputFileSystem} inputFileSystem the input file system to use for reading the file. 12 | * @param {string} path the path to the file to read. 13 | * @returns {Promise} a promise that resolves to the content of the file. 14 | */ 15 | export function readFile( 16 | inputFileSystem: InputFileSystem, 17 | path: string, 18 | ): Promise; 19 | /** @typedef {import("webpack").Compilation["inputFileSystem"] } InputFileSystem */ 20 | /** @typedef {import("fs").Stats } Stats */ 21 | /** 22 | * @param {InputFileSystem} inputFileSystem the input file system to use for reading the file stats. 23 | * @param {string} path the path to the file or directory to get stats for. 24 | * @returns {Promise} a promise that resolves to the stats of the file or directory. 25 | */ 26 | export function stat( 27 | inputFileSystem: InputFileSystem, 28 | path: string, 29 | ): Promise; 30 | /** 31 | * @template T 32 | * @typedef {() => Promise} Task 33 | */ 34 | /** 35 | * Run tasks with limited concurrency. 36 | * @template T 37 | * @param {number} limit Limit of tasks that run at once. 38 | * @param {Task[]} tasks List of tasks to run. 39 | * @returns {Promise} A promise that fulfills to an array of the results 40 | */ 41 | export function throttleAll(limit: number, tasks: Task[]): Promise; 42 | -------------------------------------------------------------------------------- /test/toType-option.test.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | import { runEmit } from "./helpers/run"; 4 | 5 | const FIXTURES_DIR_NORMALIZED = path 6 | .join(__dirname, "fixtures") 7 | .replaceAll("\\", "/"); 8 | 9 | describe("toType option", () => { 10 | it("should copy a file to a new file", (done) => { 11 | runEmit({ 12 | expectedAssetKeys: ["new-file.txt"], 13 | patterns: [ 14 | { 15 | from: "file.txt", 16 | to: "new-file.txt", 17 | toType: "file", 18 | }, 19 | ], 20 | }) 21 | .then(done) 22 | .catch(done); 23 | }); 24 | 25 | it("should copy a file to a new directory", (done) => { 26 | runEmit({ 27 | expectedAssetKeys: ["new-file.txt/file.txt"], 28 | patterns: [ 29 | { 30 | from: "file.txt", 31 | to: "new-file.txt", 32 | toType: "dir", 33 | }, 34 | ], 35 | }) 36 | .then(done) 37 | .catch(done); 38 | }); 39 | 40 | it("should copy a file to a new directory (variant 2)", (done) => { 41 | runEmit({ 42 | expectedAssetKeys: [ 43 | "directory/directoryfile.txt-new-directoryfile.txt.5d7817ed5bc246756d73.47e8bdc316eff74b2d6e-47e8bdc316eff74b2d6e.txt", 44 | ], 45 | patterns: [ 46 | { 47 | from: "directory/directoryfile.*", 48 | to: "[path][base]-new-[name][ext].[contenthash].[hash]-[fullhash][ext]", 49 | toType: "template", 50 | }, 51 | ], 52 | }) 53 | .then(done) 54 | .catch(done); 55 | }); 56 | 57 | it("should copy a file to a new file with no extension", (done) => { 58 | runEmit({ 59 | expectedAssetKeys: ["newname"], 60 | patterns: [ 61 | { 62 | from: "file.txt", 63 | to: "newname", 64 | toType: "file", 65 | }, 66 | ], 67 | }) 68 | .then(done) 69 | .catch(done); 70 | }); 71 | 72 | it("should copy a file to a new directory with an extension", (done) => { 73 | runEmit({ 74 | expectedAssetKeys: ["newdirectory.ext/file.txt"], 75 | patterns: [ 76 | { 77 | from: "file.txt", 78 | to: "newdirectory.ext", 79 | toType: "dir", 80 | }, 81 | ], 82 | }) 83 | .then(done) 84 | .catch(done); 85 | }); 86 | 87 | it("should warn when file not found and stats is undefined", (done) => { 88 | runEmit({ 89 | expectedAssetKeys: [], 90 | expectedErrors: [ 91 | new Error( 92 | `unable to locate '${FIXTURES_DIR_NORMALIZED}/nonexistent.txt' glob`, 93 | ), 94 | ], 95 | patterns: [ 96 | { 97 | from: "nonexistent.txt", 98 | to: ".", 99 | toType: "dir", 100 | }, 101 | ], 102 | }) 103 | .then(done) 104 | .catch(done); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /test/priority-option.test.js: -------------------------------------------------------------------------------- 1 | import { run } from "./helpers/run"; 2 | 3 | describe("priority option", () => { 4 | it("should copy without specifying priority option", (done) => { 5 | run({ 6 | expectedAssetKeys: [], 7 | patterns: [ 8 | { 9 | from: "dir (86)/file.txt", 10 | to: "newfile.txt", 11 | }, 12 | { 13 | from: "file.txt", 14 | to: "newfile.txt", 15 | force: true, 16 | }, 17 | ], 18 | }) 19 | .then(({ stats }) => { 20 | const { info } = stats.compilation.getAsset("newfile.txt"); 21 | 22 | expect(info.sourceFilename).toBe("file.txt"); 23 | 24 | done(); 25 | }) 26 | .catch(done); 27 | }); 28 | 29 | it("should copy with specifying priority option", (done) => { 30 | run({ 31 | expectedAssetKeys: [], 32 | patterns: [ 33 | { 34 | from: "dir (86)/file.txt", 35 | to: "newfile.txt", 36 | force: true, 37 | priority: 10, 38 | }, 39 | { 40 | from: "file.txt", 41 | to: "newfile.txt", 42 | priority: 5, 43 | }, 44 | ], 45 | }) 46 | .then(({ stats }) => { 47 | const { info } = stats.compilation.getAsset("newfile.txt"); 48 | 49 | expect(info.sourceFilename).toBe("dir (86)/file.txt"); 50 | 51 | done(); 52 | }) 53 | .catch(done); 54 | }); 55 | 56 | it("should copy with specifying priority option and respect negative priority", (done) => { 57 | run({ 58 | expectedAssetKeys: [], 59 | patterns: [ 60 | { 61 | from: "dir (86)/file.txt", 62 | to: "newfile.txt", 63 | priority: 10, 64 | force: true, 65 | }, 66 | { 67 | from: "file.txt", 68 | to: "other-newfile.txt", 69 | }, 70 | { 71 | from: "file.txt", 72 | to: "newfile.txt", 73 | priority: -5, 74 | }, 75 | ], 76 | }) 77 | .then(({ stats }) => { 78 | const { info } = stats.compilation.getAsset("newfile.txt"); 79 | 80 | expect(info.sourceFilename).toBe("dir (86)/file.txt"); 81 | 82 | done(); 83 | }) 84 | .catch(done); 85 | }); 86 | 87 | it("should copy with specifying priority option and respect order of patterns", (done) => { 88 | run({ 89 | expectedAssetKeys: [], 90 | patterns: [ 91 | { 92 | from: "dir (86)/file.txt", 93 | to: "newfile.txt", 94 | priority: 10, 95 | }, 96 | { 97 | from: "file.txt", 98 | to: "newfile.txt", 99 | priority: 10, 100 | force: true, 101 | }, 102 | ], 103 | }) 104 | .then(({ stats }) => { 105 | const { info } = stats.compilation.getAsset("newfile.txt"); 106 | 107 | expect(info.sourceFilename).toBe("file.txt"); 108 | 109 | done(); 110 | }) 111 | .catch(done); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /test/info-option.test.js: -------------------------------------------------------------------------------- 1 | import { run, runEmit } from "./helpers/run"; 2 | 3 | describe("info option", () => { 4 | it('should work without "info" option', (done) => { 5 | runEmit({ 6 | expectedAssetKeys: ["file.txt"], 7 | patterns: [ 8 | { 9 | from: "file.txt", 10 | }, 11 | ], 12 | }) 13 | .then(done) 14 | .catch(done); 15 | }); 16 | 17 | it('should work when "info" option is a object', (done) => { 18 | run({ 19 | expectedAssetKeys: ["file.txt"], 20 | patterns: [ 21 | { 22 | from: "file.txt", 23 | info: { test: true }, 24 | }, 25 | ], 26 | }) 27 | .then(({ compilation }) => { 28 | expect(compilation.assetsInfo.get("file.txt").test).toBe(true); 29 | }) 30 | .then(done) 31 | .catch(done); 32 | }); 33 | 34 | it('should work when "info" option is a object and "force" option is true', (done) => { 35 | const expectedAssetKeys = ["file.txt"]; 36 | 37 | run({ 38 | preCopy: { 39 | additionalAssets: [ 40 | { name: "file.txt", data: "Content", info: { custom: true } }, 41 | ], 42 | }, 43 | expectedAssetKeys, 44 | patterns: [ 45 | { 46 | from: "file.txt", 47 | force: true, 48 | info: { test: true }, 49 | }, 50 | ], 51 | }) 52 | .then(({ compilation }) => { 53 | expect(compilation.assetsInfo.get("file.txt").test).toBe(true); 54 | }) 55 | .then(done) 56 | .catch(done); 57 | }); 58 | 59 | it('should work when "info" option is a function', (done) => { 60 | run({ 61 | expectedAssetKeys: ["file.txt"], 62 | patterns: [ 63 | { 64 | from: "file.txt", 65 | info: (file) => { 66 | expect.assertions(4); 67 | 68 | const fileKeys = ["absoluteFilename", "sourceFilename", "filename"]; 69 | 70 | for (const key of fileKeys) { 71 | expect(key in file).toBe(true); 72 | } 73 | 74 | return { test: true }; 75 | }, 76 | }, 77 | ], 78 | }) 79 | .then(({ compilation }) => { 80 | expect(compilation.assetsInfo.get("file.txt").test).toBe(true); 81 | }) 82 | .then(done) 83 | .catch(done); 84 | }); 85 | 86 | it('should work when "info" option is a function and "force" option is true', (done) => { 87 | const expectedAssetKeys = ["file.txt"]; 88 | 89 | run({ 90 | preCopy: { 91 | additionalAssets: [ 92 | { name: "file.txt", data: "Content", info: { custom: true } }, 93 | ], 94 | }, 95 | expectedAssetKeys, 96 | patterns: [ 97 | { 98 | from: "file.txt", 99 | force: true, 100 | info: () => ({ test: true }), 101 | }, 102 | ], 103 | }) 104 | .then(({ compilation }) => { 105 | expect(compilation.assetsInfo.get("file.txt").test).toBe(true); 106 | }) 107 | .then(done) 108 | .catch(done); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: copy-webpack-plugin 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | pull_request: 9 | branches: 10 | - main 11 | - next 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | lint: 18 | name: Lint - ${{ matrix.os }} - Node v${{ matrix.node-version }} 19 | 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | strategy: 24 | matrix: 25 | os: [ubuntu-latest] 26 | node-version: [lts/*] 27 | 28 | runs-on: ${{ matrix.os }} 29 | 30 | concurrency: 31 | group: lint-${{ matrix.os }}-v${{ matrix.node-version }}-${{ github.ref }} 32 | cancel-in-progress: true 33 | 34 | steps: 35 | - uses: actions/checkout@v5 36 | with: 37 | fetch-depth: 0 38 | 39 | - name: Use Node.js ${{ matrix.node-version }} 40 | uses: actions/setup-node@v4 41 | with: 42 | node-version: ${{ matrix.node-version }} 43 | cache: "npm" 44 | 45 | - name: Install dependencies 46 | run: npm ci 47 | 48 | - name: Lint 49 | run: npm run lint 50 | 51 | - name: Build types 52 | run: npm run build:types 53 | 54 | - name: Check types 55 | run: if [ -n "$(git status types --porcelain)" ]; then echo "Missing types. Update types by running 'npm run build:types'"; exit 1; else echo "All types are valid"; fi 56 | 57 | - name: Security audit 58 | run: npm run security 59 | 60 | - name: Validate PR commits with commitlint 61 | if: github.event_name == 'pull_request' 62 | run: npx commitlint --from ${{ github.event.pull_request.head.sha }}~${{ github.event.pull_request.commits }} --to ${{ github.event.pull_request.head.sha }} --verbose 63 | 64 | test: 65 | name: Test - ${{ matrix.os }} - Node v${{ matrix.node-version }}, Webpack ${{ matrix.webpack-version }} 66 | 67 | strategy: 68 | matrix: 69 | os: [ubuntu-latest, windows-latest, macos-latest] 70 | node-version: [18.x, 20.x, 22.x, 24.x] 71 | webpack-version: [latest] 72 | 73 | runs-on: ${{ matrix.os }} 74 | 75 | concurrency: 76 | group: test-${{ matrix.os }}-v${{ matrix.node-version }}-${{ matrix.webpack-version }}-${{ github.ref }} 77 | cancel-in-progress: true 78 | 79 | steps: 80 | - name: Setup Git 81 | if: matrix.os == 'windows-latest' 82 | run: git config --global core.autocrlf input 83 | 84 | - uses: actions/checkout@v5 85 | 86 | - name: Use Node.js ${{ matrix.node-version }} 87 | uses: actions/setup-node@v4 88 | with: 89 | node-version: ${{ matrix.node-version }} 90 | cache: "npm" 91 | 92 | - name: Install dependencies 93 | run: npm ci 94 | 95 | - name: Install webpack ${{ matrix.webpack-version }} 96 | if: matrix.webpack-version != 'latest' 97 | run: npm i webpack@${{ matrix.webpack-version }} 98 | 99 | - name: Run tests for webpack version ${{ matrix.webpack-version }} 100 | run: npm run test:coverage -- --ci 101 | 102 | - name: Submit coverage data to codecov 103 | uses: codecov/codecov-action@v5 104 | with: 105 | token: ${{ secrets.CODECOV_TOKEN }} 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "copy-webpack-plugin", 3 | "version": "13.0.1", 4 | "description": "Copy files && directories with webpack", 5 | "keywords": [ 6 | "webpack", 7 | "plugin", 8 | "transfer", 9 | "move", 10 | "copy" 11 | ], 12 | "homepage": "https://github.com/webpack/copy-webpack-plugin", 13 | "bugs": "https://github.com/webpack/copy-webpack-plugin/issues", 14 | "repository": "webpack/copy-webpack-plugin", 15 | "funding": { 16 | "type": "opencollective", 17 | "url": "https://opencollective.com/webpack" 18 | }, 19 | "license": "MIT", 20 | "author": "Len Boyette", 21 | "main": "dist/index.js", 22 | "types": "types/index.d.ts", 23 | "files": [ 24 | "dist", 25 | "types" 26 | ], 27 | "scripts": { 28 | "start": "npm run build -- -w", 29 | "clean": "del-cli dist types", 30 | "prebuild": "npm run clean", 31 | "build:types": "tsc --declaration --emitDeclarationOnly --outDir types --rootDir src && prettier \"types/**/*.ts\" --write", 32 | "build:code": "cross-env NODE_ENV=production babel src -d dist --copy-files", 33 | "build": "npm-run-all -p \"build:**\"", 34 | "commitlint": "commitlint --from=main", 35 | "security": "npm audit --production", 36 | "lint:prettier": "prettier --cache --list-different .", 37 | "lint:js": "eslint --cache .", 38 | "lint:spelling": "cspell --cache --no-must-find-files --quiet \"**/*.*\"", 39 | "lint:types": "tsc --pretty --noEmit", 40 | "lint": "npm-run-all -l -p \"lint:**\"", 41 | "fix:js": "npm run lint:js -- --fix", 42 | "fix:prettier": "npm run lint:prettier -- --write", 43 | "fix": "npm-run-all -l fix:js fix:prettier", 44 | "test:only": "cross-env NODE_ENV=test node --experimental-vm-modules node_modules/jest/bin/jest.js", 45 | "test:watch": "npm run test:only -- --watch", 46 | "test:coverage": "npm run test:only -- --collectCoverageFrom=\"src/**/*.js\" --coverage", 47 | "pretest": "npm run lint", 48 | "test": "npm run test:coverage", 49 | "prepare": "husky && npm run build", 50 | "release": "standard-version" 51 | }, 52 | "dependencies": { 53 | "glob-parent": "^6.0.1", 54 | "normalize-path": "^3.0.0", 55 | "schema-utils": "^4.2.0", 56 | "serialize-javascript": "^6.0.2", 57 | "tinyglobby": "^0.2.12" 58 | }, 59 | "devDependencies": { 60 | "@babel/cli": "^7.24.6", 61 | "@babel/core": "^7.25.2", 62 | "@babel/eslint-parser": "^7.25.1", 63 | "@babel/preset-env": "^7.25.3", 64 | "@commitlint/cli": "^19.3.0", 65 | "@commitlint/config-conventional": "^19.2.2", 66 | "@eslint/js": "^9.32.0", 67 | "@eslint/markdown": "^7.0.0", 68 | "@stylistic/eslint-plugin": "^5.2.2", 69 | "@types/glob-parent": "^5.1.3", 70 | "@types/node": "^22.13.5", 71 | "@types/normalize-path": "^3.0.2", 72 | "@types/serialize-javascript": "^5.0.4", 73 | "babel-jest": "^30.0.0", 74 | "cross-env": "^7.0.3", 75 | "cspell": "^8.15.6", 76 | "del": "^6.1.1", 77 | "del-cli": "^6.0.0", 78 | "eslint": "^9.31.0", 79 | "eslint-config-prettier": "^10.1.8", 80 | "eslint-config-webpack": "^4.4.2", 81 | "eslint-plugin-import": "^2.32.0", 82 | "eslint-plugin-jest": "^29.0.1", 83 | "eslint-plugin-jsdoc": "^51.4.1", 84 | "eslint-plugin-n": "^17.21.0", 85 | "eslint-plugin-prettier": "^5.5.3", 86 | "eslint-plugin-unicorn": "^60.0.0", 87 | "file-loader": "^6.2.0", 88 | "globals": "^16.3.0", 89 | "husky": "^9.1.4", 90 | "is-gzip": "^2.0.0", 91 | "jest": "^30.0.0", 92 | "lint-staged": "^15.2.8", 93 | "memfs": "^4.11.1", 94 | "npm-run-all": "^4.1.5", 95 | "prettier": "^3.2.5", 96 | "standard-version": "^9.3.1", 97 | "typescript": "^5.4.5", 98 | "typescript-eslint": "^8.37.0", 99 | "webpack": "^5.91.0" 100 | }, 101 | "peerDependencies": { 102 | "webpack": "^5.1.0" 103 | }, 104 | "engines": { 105 | "node": ">= 18.12.0" 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("webpack").Compilation["inputFileSystem"] } InputFileSystem */ 2 | /** @typedef {import("fs").Stats } Stats */ 3 | 4 | /** 5 | * @param {InputFileSystem} inputFileSystem the input file system to use for reading the file stats. 6 | * @param {string} path the path to the file or directory to get stats for. 7 | * @returns {Promise} a promise that resolves to the stats of the file or directory. 8 | */ 9 | function stat(inputFileSystem, path) { 10 | return new Promise((resolve, reject) => { 11 | inputFileSystem.stat( 12 | path, 13 | /** 14 | * @param {null | undefined | NodeJS.ErrnoException} err an error that occurred while trying to get the stats. 15 | * @param {undefined | Stats} stats the stats of the file or directory, if available. 16 | */ 17 | (err, stats) => { 18 | if (err) { 19 | reject(err); 20 | 21 | return; 22 | } 23 | 24 | resolve(stats); 25 | }, 26 | ); 27 | }); 28 | } 29 | 30 | /** 31 | * @param {InputFileSystem} inputFileSystem the input file system to use for reading the file. 32 | * @param {string} path the path to the file to read. 33 | * @returns {Promise} a promise that resolves to the content of the file. 34 | */ 35 | function readFile(inputFileSystem, path) { 36 | return new Promise((resolve, reject) => { 37 | inputFileSystem.readFile( 38 | path, 39 | /** 40 | * @param {null | undefined | NodeJS.ErrnoException} err /an error that occurred while trying to read the file. 41 | * @param {undefined | string | Buffer} data the content of the file, if available. 42 | */ 43 | (err, data) => { 44 | if (err) { 45 | reject(err); 46 | 47 | return; 48 | } 49 | 50 | resolve(/** @type {string | Buffer} */ (data)); 51 | }, 52 | ); 53 | }); 54 | } 55 | 56 | const notSettled = Symbol("not-settled"); 57 | 58 | /** 59 | * @template T 60 | * @typedef {() => Promise} Task 61 | */ 62 | 63 | /** 64 | * Run tasks with limited concurrency. 65 | * @template T 66 | * @param {number} limit Limit of tasks that run at once. 67 | * @param {Task[]} tasks List of tasks to run. 68 | * @returns {Promise} A promise that fulfills to an array of the results 69 | */ 70 | function throttleAll(limit, tasks) { 71 | if (!Number.isInteger(limit) || limit < 1) { 72 | throw new TypeError( 73 | `Expected \`limit\` to be a finite number > 0, got \`${limit}\` (${typeof limit})`, 74 | ); 75 | } 76 | 77 | if ( 78 | !Array.isArray(tasks) || 79 | !tasks.every((task) => typeof task === "function") 80 | ) { 81 | throw new TypeError( 82 | "Expected `tasks` to be a list of functions returning a promise", 83 | ); 84 | } 85 | 86 | return new Promise((resolve, reject) => { 87 | const result = Array.from({ length: tasks.length }).fill(notSettled); 88 | 89 | const entries = tasks.entries(); 90 | 91 | const next = () => { 92 | const { done, value } = entries.next(); 93 | 94 | if (done) { 95 | const isLast = !result.includes(notSettled); 96 | 97 | if (isLast) { 98 | resolve(/** @type {T[]} */ (result)); 99 | } 100 | 101 | return; 102 | } 103 | 104 | const [index, task] = value; 105 | 106 | /** 107 | * @param {T} taskResult The result of the task that was fulfilled. 108 | */ 109 | const onFulfilled = (taskResult) => { 110 | result[index] = taskResult; 111 | next(); 112 | }; 113 | 114 | task().then(onFulfilled, reject); 115 | }; 116 | 117 | for (let i = 0; i < limit; i++) { 118 | next(); 119 | } 120 | }); 121 | } 122 | 123 | /** 124 | * @template T 125 | * @param {(() => unknown) | undefined} fn The function to memoize. 126 | * @returns {() => T} A memoized function that returns the result of the original function. 127 | */ 128 | function memoize(fn) { 129 | let cache = false; 130 | /** @type {T} */ 131 | let result; 132 | 133 | return () => { 134 | if (cache) { 135 | return result; 136 | } 137 | 138 | result = /** @type {T} */ (/** @type {() => unknown} */ (fn)()); 139 | cache = true; 140 | // Allow to clean up memory for fn 141 | // and all dependent resources 142 | 143 | fn = undefined; 144 | 145 | return result; 146 | }; 147 | } 148 | 149 | module.exports = { memoize, readFile, stat, throttleAll }; 150 | -------------------------------------------------------------------------------- /src/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "definitions": { 3 | "ObjectPattern": { 4 | "type": "object", 5 | "additionalProperties": false, 6 | "properties": { 7 | "from": { 8 | "type": "string", 9 | "description": "Glob or path from where we copy files.", 10 | "link": "https://github.com/webpack/copy-webpack-plugin#from", 11 | "minLength": 1 12 | }, 13 | "to": { 14 | "anyOf": [ 15 | { 16 | "type": "string" 17 | }, 18 | { 19 | "instanceof": "Function" 20 | } 21 | ], 22 | "description": "Output path.", 23 | "link": "https://github.com/webpack/copy-webpack-plugin#to" 24 | }, 25 | "context": { 26 | "type": "string", 27 | "description": "A path that determines how to interpret the 'from' path.", 28 | "link": "https://github.com/webpack/copy-webpack-plugin#context" 29 | }, 30 | "globOptions": { 31 | "type": "object", 32 | "description": "Allows to configure the glob pattern matching library used by the plugin.", 33 | "link": "https://github.com/webpack/copy-webpack-plugin#globoptions" 34 | }, 35 | "filter": { 36 | "instanceof": "Function", 37 | "description": "Allows to filter copied assets.", 38 | "link": "https://github.com/webpack/copy-webpack-plugin#filter" 39 | }, 40 | "transformAll": { 41 | "instanceof": "Function", 42 | "description": "Allows you to modify the contents of multiple files and save the result to one file.", 43 | "link": "https://github.com/webpack/copy-webpack-plugin#transformall" 44 | }, 45 | "toType": { 46 | "enum": ["dir", "file", "template"], 47 | "description": "Determinate what is to option - directory, file or template.", 48 | "link": "https://github.com/webpack/copy-webpack-plugin#totype" 49 | }, 50 | "force": { 51 | "type": "boolean", 52 | "description": "Overwrites files already in 'compilation.assets' (usually added by other plugins/loaders).", 53 | "link": "https://github.com/webpack/copy-webpack-plugin#force" 54 | }, 55 | "priority": { 56 | "type": "number", 57 | "description": "Allows to specify the priority of copying files with the same destination name.", 58 | "link": "https://github.com/webpack/copy-webpack-plugin#priority" 59 | }, 60 | "info": { 61 | "anyOf": [ 62 | { 63 | "type": "object" 64 | }, 65 | { 66 | "instanceof": "Function" 67 | } 68 | ], 69 | "description": "Allows to add assets info.", 70 | "link": "https://github.com/webpack/copy-webpack-plugin#info" 71 | }, 72 | "transform": { 73 | "description": "Allows to modify the file contents.", 74 | "link": "https://github.com/webpack/copy-webpack-plugin#transform", 75 | "anyOf": [ 76 | { 77 | "instanceof": "Function" 78 | }, 79 | { 80 | "type": "object", 81 | "additionalProperties": false, 82 | "properties": { 83 | "transformer": { 84 | "instanceof": "Function", 85 | "description": "Allows to modify the file contents.", 86 | "link": "https://github.com/webpack/copy-webpack-plugin#transformer" 87 | }, 88 | "cache": { 89 | "description": "Enables/disables and configure caching.", 90 | "link": "https://github.com/webpack/copy-webpack-plugin#cache", 91 | "anyOf": [ 92 | { 93 | "type": "boolean" 94 | }, 95 | { 96 | "type": "object", 97 | "additionalProperties": false, 98 | "properties": { 99 | "keys": { 100 | "anyOf": [ 101 | { 102 | "type": "object", 103 | "additionalProperties": true 104 | }, 105 | { 106 | "instanceof": "Function" 107 | } 108 | ] 109 | } 110 | } 111 | } 112 | ] 113 | } 114 | } 115 | } 116 | ] 117 | }, 118 | "noErrorOnMissing": { 119 | "type": "boolean", 120 | "description": "Doesn't generate an error on missing file(s).", 121 | "link": "https://github.com/webpack/copy-webpack-plugin#noerroronmissing" 122 | } 123 | }, 124 | "required": ["from"] 125 | }, 126 | "StringPattern": { 127 | "type": "string", 128 | "minLength": 1 129 | } 130 | }, 131 | "type": "object", 132 | "additionalProperties": false, 133 | "properties": { 134 | "patterns": { 135 | "type": "array", 136 | "minItems": 1, 137 | "items": { 138 | "anyOf": [ 139 | { 140 | "$ref": "#/definitions/StringPattern" 141 | }, 142 | { 143 | "$ref": "#/definitions/ObjectPattern" 144 | } 145 | ] 146 | } 147 | }, 148 | "options": { 149 | "type": "object", 150 | "additionalProperties": false, 151 | "properties": { 152 | "concurrency": { 153 | "type": "number", 154 | "description": "Limits the number of simultaneous requests to fs.", 155 | "link": "https://github.com/webpack/copy-webpack-plugin#concurrency" 156 | } 157 | } 158 | } 159 | }, 160 | "required": ["patterns"] 161 | } 162 | -------------------------------------------------------------------------------- /test/context-option.test.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | import { runEmit } from "./helpers/run"; 4 | 5 | const FIXTURES_DIR = path.join(__dirname, "fixtures"); 6 | 7 | describe("context option", () => { 8 | it('should work when "from" is a file and "context" is a relative path', (done) => { 9 | runEmit({ 10 | expectedAssetKeys: ["directoryfile.txt"], 11 | patterns: [ 12 | { 13 | from: "directoryfile.txt", 14 | context: "directory", 15 | }, 16 | ], 17 | }) 18 | .then(done) 19 | .catch(done); 20 | }); 21 | 22 | it('should work when "from" is a directory and "context" is a relative path', (done) => { 23 | runEmit({ 24 | expectedAssetKeys: ["deep-nested/deepnested.txt", "nestedfile.txt"], 25 | patterns: [ 26 | { 27 | from: "nested", 28 | context: "directory", 29 | }, 30 | ], 31 | }) 32 | .then(done) 33 | .catch(done); 34 | }); 35 | 36 | it('should work when "from" is a glob and "context" is a relative path', (done) => { 37 | runEmit({ 38 | expectedAssetKeys: [ 39 | "nested/deep-nested/deepnested.txt", 40 | "nested/nestedfile.txt", 41 | ], 42 | patterns: [ 43 | { 44 | from: "nested/**/*", 45 | context: "directory", 46 | }, 47 | ], 48 | }) 49 | .then(done) 50 | .catch(done); 51 | }); 52 | 53 | it('should work when "from" is a file and "context" is an absolute path', (done) => { 54 | runEmit({ 55 | expectedAssetKeys: ["directoryfile.txt"], 56 | patterns: [ 57 | { 58 | from: "directoryfile.txt", 59 | context: path.join(FIXTURES_DIR, "directory"), 60 | }, 61 | ], 62 | }) 63 | .then(done) 64 | .catch(done); 65 | }); 66 | 67 | it('should work when "from" is a directory and "context" is an absolute path', (done) => { 68 | runEmit({ 69 | expectedAssetKeys: ["deep-nested/deepnested.txt", "nestedfile.txt"], 70 | patterns: [ 71 | { 72 | from: "nested", 73 | context: path.join(FIXTURES_DIR, "directory"), 74 | }, 75 | ], 76 | }) 77 | .then(done) 78 | .catch(done); 79 | }); 80 | 81 | it('should work when "from" is a glob and "context" is an absolute path', (done) => { 82 | runEmit({ 83 | expectedAssetKeys: [ 84 | "nested/deep-nested/deepnested.txt", 85 | "nested/nestedfile.txt", 86 | ], 87 | patterns: [ 88 | { 89 | from: "nested/**/*", 90 | context: path.join(FIXTURES_DIR, "directory"), 91 | }, 92 | ], 93 | }) 94 | .then(done) 95 | .catch(done); 96 | }); 97 | 98 | it('should work when "from" is a file and "context" with special characters', (done) => { 99 | runEmit({ 100 | expectedAssetKeys: ["directoryfile.txt"], 101 | patterns: [ 102 | { 103 | from: "directoryfile.txt", 104 | context: "[special$directory]", 105 | }, 106 | ], 107 | }) 108 | .then(done) 109 | .catch(done); 110 | }); 111 | 112 | it('should work when "from" is a directory and "context" with special characters', (done) => { 113 | runEmit({ 114 | expectedAssetKeys: [ 115 | "directoryfile.txt", 116 | "(special-*file).txt", 117 | "nested/nestedfile.txt", 118 | ], 119 | patterns: [ 120 | { 121 | // Todo strange behavour when you use `FIXTURES_DIR`, need investigate for next major release 122 | from: ".", 123 | context: "[special$directory]", 124 | }, 125 | ], 126 | }) 127 | .then(done) 128 | .catch(done); 129 | }); 130 | 131 | it('should work when "from" is a glob and "context" with special characters', (done) => { 132 | runEmit({ 133 | expectedAssetKeys: [ 134 | "directoryfile.txt", 135 | "(special-*file).txt", 136 | "nested/nestedfile.txt", 137 | ], 138 | patterns: [ 139 | { 140 | from: "**/*", 141 | context: "[special$directory]", 142 | }, 143 | ], 144 | }) 145 | .then(done) 146 | .catch(done); 147 | }); 148 | 149 | it('should work when "from" is a glob and "context" with special characters #2', (done) => { 150 | runEmit({ 151 | expectedAssetKeys: ["(special-*file).txt"], 152 | patterns: [ 153 | { 154 | from: "\\(special-*file\\).txt", 155 | context: "[special$directory]", 156 | }, 157 | ], 158 | }) 159 | .then(done) 160 | .catch(done); 161 | }); 162 | 163 | it('should work when "from" is a file and "to" is a directory', (done) => { 164 | runEmit({ 165 | expectedAssetKeys: ["newdirectory/directoryfile.txt"], 166 | patterns: [ 167 | { 168 | context: "directory", 169 | from: "directoryfile.txt", 170 | to: "newdirectory", 171 | }, 172 | ], 173 | }) 174 | .then(done) 175 | .catch(done); 176 | }); 177 | 178 | it('should work when "from" is a directory and "to" is a directory', (done) => { 179 | runEmit({ 180 | expectedAssetKeys: [ 181 | "newdirectory/deep-nested/deepnested.txt", 182 | "newdirectory/nestedfile.txt", 183 | ], 184 | patterns: [ 185 | { 186 | context: "directory", 187 | from: "nested", 188 | to: "newdirectory", 189 | }, 190 | ], 191 | }) 192 | .then(done) 193 | .catch(done); 194 | }); 195 | 196 | it('should work when "from" is a glob and "to" is a directory', (done) => { 197 | runEmit({ 198 | expectedAssetKeys: [ 199 | "nested/directoryfile.txt", 200 | "nested/nested/deep-nested/deepnested.txt", 201 | "nested/nested/nestedfile.txt", 202 | ], 203 | patterns: [ 204 | { 205 | context: "directory", 206 | from: "**/*", 207 | to: "nested", 208 | }, 209 | ], 210 | }) 211 | .then(done) 212 | .catch(done); 213 | }); 214 | }); 215 | -------------------------------------------------------------------------------- /test/transformAll-option.test.js: -------------------------------------------------------------------------------- 1 | import CopyPlugin from "../src"; 2 | 3 | import { compile, getCompiler, readAssets } from "./helpers"; 4 | import { runEmit } from "./helpers/run"; 5 | 6 | describe("transformAll option", () => { 7 | it('should be defined "assets"', (done) => { 8 | runEmit({ 9 | expectedAssetKeys: ["file.txt"], 10 | patterns: [ 11 | { 12 | from: "file.txt", 13 | to: "file.txt", 14 | transformAll(assets) { 15 | expect(assets).toBeDefined(); 16 | 17 | return ""; 18 | }, 19 | }, 20 | ], 21 | }) 22 | .then(() => { 23 | // runEmit performs assertions internally, no need to check result 24 | done(); 25 | }) 26 | .catch(done); 27 | }); 28 | 29 | it("should transform files", (done) => { 30 | runEmit({ 31 | expectedAssetKeys: ["file.txt"], 32 | expectedAssetContent: { 33 | "file.txt": 34 | "new::directory/nested/deep-nested/deepnested.txt::directory/nested/nestedfile.txt::", 35 | }, 36 | patterns: [ 37 | { 38 | from: "directory/**/*.txt", 39 | to: "file.txt", 40 | transformAll(assets) { 41 | const result = assets.reduce((accumulator, asset) => { 42 | const content = asset.data.toString() || asset.sourceFilename; 43 | 44 | accumulator = `${accumulator}${content}::`; 45 | return accumulator; 46 | }, ""); 47 | 48 | return result; 49 | }, 50 | }, 51 | ], 52 | }) 53 | .then(() => { 54 | // runEmit performs assertions internally, no need to check result 55 | done(); 56 | }) 57 | .catch(done); 58 | }); 59 | 60 | it("should transform files when async function used", () => 61 | runEmit({ 62 | expectedAssetKeys: ["file.txt"], 63 | expectedAssetContent: { 64 | "file.txt": 65 | "directory/directoryfile.txt::directory/nested/deep-nested/deepnested.txt::directory/nested/nestedfile.txt::", 66 | }, 67 | patterns: [ 68 | { 69 | from: "directory/**/*.txt", 70 | to: "file.txt", 71 | async transformAll(assets) { 72 | // Use implicit return and remove the block statement 73 | return assets.reduce( 74 | (accumulator, asset) => `${accumulator}${asset.sourceFilename}::`, 75 | "", 76 | ); 77 | }, 78 | }, 79 | ], 80 | })); 81 | 82 | it("should transform files with force option enabled", (done) => { 83 | runEmit({ 84 | expectedAssetKeys: ["file.txt"], 85 | expectedAssetContent: { 86 | "file.txt": 87 | "directory/directoryfile.txt::directory/nested/deep-nested/deepnested.txt::directory/nested/nestedfile.txt::", 88 | }, 89 | patterns: [ 90 | { 91 | from: "file.txt", 92 | }, 93 | { 94 | from: "directory/**/*.txt", 95 | to: "file.txt", 96 | transformAll(assets) { 97 | const result = assets.reduce((accumulator, asset) => { 98 | accumulator = `${accumulator}${asset.sourceFilename}::`; 99 | return accumulator; 100 | }, ""); 101 | 102 | return result; 103 | }, 104 | force: true, 105 | }, 106 | ], 107 | }) 108 | .then(done) 109 | .catch(done); 110 | }); 111 | 112 | it('should warn when "to" option is not defined', (done) => { 113 | runEmit({ 114 | expectedAssetKeys: [], 115 | expectedErrors: [ 116 | new Error( 117 | 'Invalid "pattern.to" for the "pattern.from": "file.txt" and "pattern.transformAll" function. The "to" option must be specified.', 118 | ), 119 | ], 120 | patterns: [ 121 | { 122 | from: "file.txt", 123 | transformAll() { 124 | return ""; 125 | }, 126 | }, 127 | ], 128 | }) 129 | .then(done) 130 | .catch(done); 131 | }); 132 | 133 | it("should warn when function throw error", (done) => { 134 | runEmit({ 135 | expectedAssetKeys: [], 136 | expectedErrors: [new Error("a failure happened")], 137 | patterns: [ 138 | { 139 | from: "directory/**/*.txt", 140 | to: "file.txt", 141 | transformAll() { 142 | throw new Error("a failure happened"); 143 | }, 144 | }, 145 | ], 146 | }) 147 | .then(done) 148 | .catch(done); 149 | }); 150 | 151 | it("should interpolate [fullhash] and [contenthash]", (done) => { 152 | runEmit({ 153 | expectedAssetKeys: ["4333a40fa67dfaaaefc9-47e8bdc316eff74b2d6e-file.txt"], 154 | expectedAssetContent: { 155 | "4333a40fa67dfaaaefc9-47e8bdc316eff74b2d6e-file.txt": 156 | "::special::new::::::::::new::::::new::", 157 | }, 158 | patterns: [ 159 | { 160 | from: "**/*.txt", 161 | to: "[contenthash]-[fullhash]-file.txt", 162 | transformAll(assets) { 163 | return assets.reduce((accumulator, asset) => { 164 | accumulator = `${accumulator}${asset.data}::`; 165 | return accumulator; 166 | }, ""); 167 | }, 168 | }, 169 | ], 170 | }) 171 | .then(done) 172 | .catch(done); 173 | }); 174 | 175 | it("should interpolate [fullhash] and [contenthash] #2", (done) => { 176 | runEmit({ 177 | expectedAssetKeys: ["4333a40fa67dfaaaefc9-47e8bdc316eff74b2d6e-file.txt"], 178 | expectedAssetContent: { 179 | "4333a40fa67dfaaaefc9-47e8bdc316eff74b2d6e-file.txt": 180 | "::special::new::::::::::new::::::new::", 181 | }, 182 | patterns: [ 183 | { 184 | from: "**/*.txt", 185 | to: () => "[contenthash]-[fullhash]-file.txt", 186 | transformAll(assets) { 187 | return assets.reduce((accumulator, asset) => { 188 | accumulator = `${accumulator}${asset.data}::`; 189 | 190 | return accumulator; 191 | }, ""); 192 | }, 193 | }, 194 | ], 195 | }) 196 | .then(done) 197 | .catch(done); 198 | }); 199 | }); 200 | 201 | describe("cache", () => { 202 | it('should work with the "memory" cache', async () => { 203 | const compiler = getCompiler({}); 204 | 205 | new CopyPlugin({ 206 | patterns: [ 207 | { 208 | from: "directory/**/*.txt", 209 | to: "file.txt", 210 | transformAll(assets) { 211 | const result = assets.reduce((accumulator, asset) => { 212 | const content = asset.data.toString() || asset.sourceFilename; 213 | 214 | accumulator = `${accumulator}${content}::`; 215 | return accumulator; 216 | }, ""); 217 | 218 | return result; 219 | }, 220 | }, 221 | ], 222 | }).apply(compiler); 223 | 224 | const { stats } = await compile(compiler); 225 | 226 | expect(stats.compilation.emittedAssets.size).toBe(2); 227 | expect(readAssets(compiler, stats)).toMatchSnapshot("assets"); 228 | expect(stats.compilation.errors).toMatchSnapshot("errors"); 229 | expect(stats.compilation.warnings).toMatchSnapshot("warnings"); 230 | 231 | const { stats: newStats } = await compile(compiler); 232 | 233 | expect(newStats.compilation.emittedAssets.size).toBe(0); 234 | expect(readAssets(compiler, newStats)).toMatchSnapshot("assets"); 235 | expect(newStats.compilation.errors).toMatchSnapshot("errors"); 236 | expect(newStats.compilation.warnings).toMatchSnapshot("warnings"); 237 | }); 238 | }); 239 | -------------------------------------------------------------------------------- /test/helpers/run.js: -------------------------------------------------------------------------------- 1 | // Ideally we pass in patterns and confirm the resulting assets 2 | import fs from "node:fs"; 3 | import path from "node:path"; 4 | 5 | import CopyPlugin from "../../src/index"; 6 | 7 | import BreakContenthashPlugin from "./BreakContenthashPlugin"; 8 | import ChildCompilerPlugin from "./ChildCompiler"; 9 | import PreCopyPlugin from "./PreCopyPlugin"; 10 | 11 | import removeIllegalCharacterForWindows from "./removeIllegalCharacterForWindows"; 12 | 13 | import { compile, getCompiler, readAssets } from "./"; 14 | 15 | // ESLint disable for expect since this file is only used in test contexts 16 | /* eslint-disable no-undef */ 17 | 18 | const isWin = process.platform === "win32"; 19 | 20 | const ignore = [ 21 | "**/symlink/**/*", 22 | "**/file-ln.txt", 23 | "**/directory-ln", 24 | "**/watch/**/*", 25 | ]; 26 | 27 | /** 28 | * @param {{ patterns?: Array, compiler?: unknown, preCopy?: unknown, breakContenthash?: unknown, withChildCompilation?: unknown, expectedErrors?: Array, expectedWarnings?: Array }} opts Options for running the test 29 | * @returns {Promise<{ compilation: unknown, compiler: unknown, stats: unknown }>} Resolves with compilation, compiler, and stats 30 | */ 31 | function run(opts) { 32 | return new Promise((resolve, reject) => { 33 | if (Array.isArray(opts.patterns)) { 34 | for (const pattern of opts.patterns) { 35 | if (pattern.context) { 36 | pattern.context = removeIllegalCharacterForWindows(pattern.context); 37 | } 38 | 39 | if (typeof pattern !== "string" && (!opts.symlink || isWin)) { 40 | pattern.globOptions ||= {}; 41 | pattern.globOptions.ignore = [ 42 | ...ignore, 43 | ...(pattern.globOptions.ignore || []), 44 | ]; 45 | } 46 | } 47 | } 48 | 49 | const compiler = opts.compiler || getCompiler(); 50 | 51 | if (opts.preCopy) { 52 | new PreCopyPlugin({ options: opts.preCopy }).apply(compiler); 53 | } 54 | 55 | if (opts.breakContenthash) { 56 | new BreakContenthashPlugin({ options: opts.breakContenthash }).apply( 57 | compiler, 58 | ); 59 | } 60 | 61 | new CopyPlugin({ patterns: opts.patterns, options: opts.options }).apply( 62 | compiler, 63 | ); 64 | 65 | if (opts.withChildCompilation) { 66 | new ChildCompilerPlugin().apply(compiler); 67 | } 68 | 69 | // Execute the functions in series 70 | compile(compiler) 71 | .then(({ stats }) => { 72 | const { compilation } = stats; 73 | 74 | if (opts.expectedErrors) { 75 | expect(compilation.errors).toEqual(opts.expectedErrors); 76 | } else if (compilation.errors.length > 0) { 77 | throw compilation.errors[0]; 78 | } 79 | 80 | if (opts.expectedWarnings) { 81 | expect(compilation.warnings).toEqual(opts.expectedWarnings); 82 | } else if (compilation.warnings.length > 0) { 83 | throw compilation.warnings[0]; 84 | } 85 | 86 | const enryPoint = path.resolve(__dirname, "enter.js"); 87 | 88 | if (compilation.fileDependencies.has(enryPoint)) { 89 | compilation.fileDependencies.delete(enryPoint); 90 | } 91 | 92 | resolve({ compilation, compiler, stats }); 93 | }) 94 | .catch(reject); 95 | }); 96 | } 97 | 98 | /** 99 | * @param {{ expectedAssetKeys?: Array, expectedAssetContent?: { [key: string]: unknown }, skipAssetsTesting?: boolean }} opts Options for running the test 100 | * @returns {Promise} Resolves when the test is complete 101 | */ 102 | function runEmit(opts) { 103 | return run(opts).then(({ compilation, compiler, stats }) => { 104 | if (opts.skipAssetsTesting) { 105 | return; 106 | } 107 | 108 | if (opts.expectedAssetKeys && opts.expectedAssetKeys.length > 0) { 109 | expect( 110 | Object.keys(compilation.assets) 111 | .filter((a) => a !== "main.js") 112 | .sort(), 113 | ).toEqual( 114 | opts.expectedAssetKeys.sort().map(removeIllegalCharacterForWindows), 115 | ); 116 | } else { 117 | delete compilation.assets["main.js"]; 118 | expect(compilation.assets).toEqual({}); 119 | } 120 | 121 | if (opts.expectedAssetContent) { 122 | for (const assetName in opts.expectedAssetContent) { 123 | expect(compilation.assets[assetName]).toBeDefined(); 124 | 125 | if (compilation.assets[assetName]) { 126 | let expectedContent = opts.expectedAssetContent[assetName]; 127 | let compiledContent = readAssets(compiler, stats)[assetName]; 128 | 129 | if (!Buffer.isBuffer(expectedContent)) { 130 | expectedContent = Buffer.from(expectedContent); 131 | } 132 | 133 | if (!Buffer.isBuffer(compiledContent)) { 134 | compiledContent = Buffer.from(compiledContent); 135 | } 136 | 137 | expect(Buffer.compare(expectedContent, compiledContent)).toBe(0); 138 | } 139 | } 140 | } 141 | }); 142 | } 143 | 144 | /** 145 | * @param {{ compiler?: unknown }} opts Options for running the test 146 | * @returns {Promise} Resolves when the test is complete 147 | */ 148 | function runForce(opts) { 149 | opts.compiler ||= getCompiler(); 150 | 151 | new PreCopyPlugin({ options: opts }).apply(opts.compiler); 152 | 153 | return runEmit(opts).then(() => {}); 154 | } 155 | 156 | /** 157 | * @param {number} ms Milliseconds to delay 158 | * @returns {Promise} Resolves after the delay 159 | */ 160 | const delay = (ms) => 161 | new Promise((resolve) => { 162 | setTimeout(resolve, ms); 163 | }); 164 | 165 | /** 166 | * @param {{ patterns?: Array, options?: unknown, newFileLoc1?: string, newFileLoc2?: string, expectedAssetKeys?: Array }} opts Options for running the test 167 | * @returns {Promise} Resolves when the test is complete 168 | */ 169 | function runChange(opts) { 170 | return new Promise((resolve) => { 171 | const compiler = getCompiler(); 172 | 173 | new CopyPlugin({ patterns: opts.patterns, options: opts.options }).apply( 174 | compiler, 175 | ); 176 | 177 | // Create two test files 178 | fs.writeFileSync(opts.newFileLoc1, "file1contents"); 179 | fs.writeFileSync(opts.newFileLoc2, "file2contents"); 180 | 181 | const arrayOfStats = []; 182 | 183 | const watching = compiler.watch({}, (error, stats) => { 184 | if (error || stats.hasErrors()) { 185 | throw error; 186 | } 187 | 188 | arrayOfStats.push(stats); 189 | }); 190 | 191 | delay(500) 192 | .then(() => { 193 | fs.appendFileSync(opts.newFileLoc1, "extra"); 194 | 195 | return delay(500); 196 | }) 197 | .then(() => { 198 | watching.close(() => { 199 | const assetsBefore = readAssets(compiler, arrayOfStats[0]); 200 | const assetsAfter = readAssets(compiler, arrayOfStats.pop()); 201 | const filesForCompare = Object.keys(assetsBefore); 202 | const changedFiles = []; 203 | 204 | for (const file of filesForCompare) { 205 | if (assetsBefore[file] === assetsAfter[file]) { 206 | changedFiles.push(file); 207 | } 208 | } 209 | 210 | const lastFiles = Object.keys(assetsAfter); 211 | 212 | if ( 213 | opts.expectedAssetKeys && 214 | opts.expectedAssetKeys.length > 0 && 215 | changedFiles.length > 0 216 | ) { 217 | expect(changedFiles.sort()).toEqual( 218 | opts.expectedAssetKeys 219 | .sort() 220 | .map(removeIllegalCharacterForWindows), 221 | ); 222 | } 223 | 224 | if (lastFiles.length > 0) { 225 | expect(lastFiles.sort()).toEqual(Object.keys(assetsAfter).sort()); 226 | } 227 | 228 | resolve(); 229 | }); 230 | }); 231 | }); 232 | } 233 | 234 | export { run, runChange, runEmit, runForce }; 235 | -------------------------------------------------------------------------------- /test/transform-option.test.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import zlib from "node:zlib"; 3 | 4 | import { run, runEmit } from "./helpers/run"; 5 | 6 | const FIXTURES_DIR = path.join(__dirname, "fixtures"); 7 | 8 | describe("transform option", () => { 9 | it('should transform file when "from" is a file', (done) => { 10 | runEmit({ 11 | expectedAssetKeys: ["file.txt"], 12 | expectedAssetContent: { 13 | "file.txt": "newchanged", 14 | }, 15 | patterns: [ 16 | { 17 | from: "file.txt", 18 | transform: { 19 | transformer(content, absoluteFrom) { 20 | expect(absoluteFrom).toContain(FIXTURES_DIR); 21 | 22 | return `${content}changed`; 23 | }, 24 | }, 25 | }, 26 | ], 27 | }) 28 | .then(done) 29 | .catch(done); 30 | }); 31 | 32 | it('should transform target path of every when "from" is a directory', (done) => { 33 | runEmit({ 34 | expectedAssetKeys: [ 35 | ".dottedfile", 36 | "directoryfile.txt", 37 | "nested/deep-nested/deepnested.txt", 38 | "nested/nestedfile.txt", 39 | ], 40 | expectedAssetContent: { 41 | ".dottedfile": "dottedfile contents\nchanged", 42 | "directoryfile.txt": "newchanged", 43 | "nested/deep-nested/deepnested.txt": "changed", 44 | "nested/nestedfile.txt": "changed", 45 | }, 46 | patterns: [ 47 | { 48 | from: "directory", 49 | transform: { 50 | transformer(content, absoluteFrom) { 51 | expect(absoluteFrom).toContain(FIXTURES_DIR); 52 | 53 | return `${content}changed`; 54 | }, 55 | }, 56 | }, 57 | ], 58 | }) 59 | .then(done) 60 | .catch(done); 61 | }); 62 | 63 | it('should transform target path of every file when "from" is a glob', (done) => { 64 | runEmit({ 65 | expectedAssetKeys: [ 66 | "directory/directoryfile.txt", 67 | "directory/nested/deep-nested/deepnested.txt", 68 | "directory/nested/nestedfile.txt", 69 | ], 70 | expectedAssetContent: { 71 | "directory/directoryfile.txt": "newchanged", 72 | "directory/nested/deep-nested/deepnested.txt": "changed", 73 | "directory/nested/nestedfile.txt": "changed", 74 | }, 75 | patterns: [ 76 | { 77 | from: "directory/**/*", 78 | transform: { 79 | transformer(content, absoluteFrom) { 80 | expect(absoluteFrom).toContain(FIXTURES_DIR); 81 | 82 | return `${content}changed`; 83 | }, 84 | }, 85 | }, 86 | ], 87 | }) 88 | .then(done) 89 | .catch(done); 90 | }); 91 | 92 | it("should transform file when transform is function", (done) => { 93 | runEmit({ 94 | expectedAssetKeys: ["file.txt"], 95 | expectedAssetContent: { 96 | "file.txt": "newchanged!", 97 | }, 98 | patterns: [ 99 | { 100 | from: "file.txt", 101 | transform: (content) => `${content}changed!`, 102 | }, 103 | ], 104 | }) 105 | .then(done) 106 | .catch(done); 107 | }); 108 | 109 | it("should transform file when function return Promise", (done) => { 110 | runEmit({ 111 | expectedAssetKeys: ["file.txt"], 112 | expectedAssetContent: { 113 | "file.txt": "newchanged!", 114 | }, 115 | patterns: [ 116 | { 117 | from: "file.txt", 118 | transform(content) { 119 | return new Promise((resolve) => { 120 | resolve(`${content}changed!`); 121 | }); 122 | }, 123 | }, 124 | ], 125 | }) 126 | .then(done) 127 | .catch(done); 128 | }); 129 | 130 | it("should transform file when function `transformer` return Promise", (done) => { 131 | runEmit({ 132 | expectedAssetKeys: ["file.txt"], 133 | expectedAssetContent: { 134 | "file.txt": "newchanged!", 135 | }, 136 | patterns: [ 137 | { 138 | from: "file.txt", 139 | transform: { 140 | transformer(content) { 141 | return new Promise((resolve) => { 142 | resolve(`${content}changed!`); 143 | }); 144 | }, 145 | }, 146 | }, 147 | ], 148 | }) 149 | .then(done) 150 | .catch(done); 151 | }); 152 | 153 | it("should transform target path when async function used", (done) => { 154 | runEmit({ 155 | expectedAssetKeys: ["file.txt"], 156 | expectedAssetContent: { 157 | "file.txt": "newchanged!", 158 | }, 159 | patterns: [ 160 | { 161 | from: "file.txt", 162 | transform: { 163 | async transformer(content) { 164 | const newPath = await new Promise((resolve) => { 165 | resolve(`${content}changed!`); 166 | }); 167 | 168 | return newPath; 169 | }, 170 | }, 171 | }, 172 | ], 173 | }) 174 | .then(done) 175 | .catch(done); 176 | }); 177 | 178 | it("should warn when function throw error", (done) => { 179 | runEmit({ 180 | expectedAssetKeys: [], 181 | expectedErrors: [new Error("a failure happened")], 182 | patterns: [ 183 | { 184 | from: "file.txt", 185 | transform: { 186 | transformer() { 187 | throw new Error("a failure happened"); 188 | }, 189 | }, 190 | }, 191 | ], 192 | }) 193 | .then(done) 194 | .catch(done); 195 | }); 196 | 197 | it("should warn when Promise was rejected", (done) => { 198 | runEmit({ 199 | expectedAssetKeys: [], 200 | expectedErrors: [new Error("a failure happened")], 201 | patterns: [ 202 | { 203 | from: "file.txt", 204 | transform: { 205 | transformer() { 206 | return new Promise((resolve, reject) => { 207 | reject(new Error("a failure happened")); 208 | }); 209 | }, 210 | }, 211 | }, 212 | ], 213 | }) 214 | .then(done) 215 | .catch(done); 216 | }); 217 | 218 | it("should warn when async function throw error", (done) => { 219 | runEmit({ 220 | expectedAssetKeys: [], 221 | expectedErrors: [new Error("a failure happened")], 222 | patterns: [ 223 | { 224 | from: "file.txt", 225 | transform: { 226 | async transformer() { 227 | await new Promise((resolve, reject) => { 228 | reject(new Error("a failure happened")); 229 | }); 230 | }, 231 | }, 232 | }, 233 | ], 234 | }) 235 | .then(done) 236 | .catch(done); 237 | }); 238 | 239 | it("should be a different size for the source file and the converted file", (done) => { 240 | run({ 241 | patterns: [ 242 | { 243 | from: "file.txt", 244 | }, 245 | { 246 | from: "file.txt", 247 | to: "file.txt.gz", 248 | transform: { 249 | transformer: (content) => zlib.gzipSync(content), 250 | }, 251 | }, 252 | ], 253 | }) 254 | .then(({ compilation }) => { 255 | expect(compilation.assets["file.txt"].size()).not.toBe( 256 | compilation.assets["file.txt.gz"].size(), 257 | ); 258 | }) 259 | .then(done) 260 | .catch(done); 261 | }); 262 | 263 | it('should transform file when "from" is a file and it contains specific content', (done) => { 264 | runEmit({ 265 | expectedAssetKeys: ["subdir/test.txt"], 266 | expectedAssetContent: { 267 | "subdir/test.txt": "newchanged", 268 | }, 269 | patterns: [ 270 | { 271 | from: "file.txt", 272 | transform: { 273 | transformer(content, absoluteFrom) { 274 | expect(absoluteFrom).toContain(FIXTURES_DIR); 275 | 276 | return `${content}changed`; 277 | }, 278 | }, 279 | to({ context, absoluteFilename }) { 280 | expect(absoluteFilename).toBe(path.join(FIXTURES_DIR, "file.txt")); 281 | 282 | const targetPath = path.relative(context, absoluteFilename); 283 | 284 | return targetPath.replace("file.txt", "subdir/test.txt"); 285 | }, 286 | }, 287 | ], 288 | }) 289 | .then(done) 290 | .catch(done); 291 | }); 292 | }); 293 | -------------------------------------------------------------------------------- /test/force-option.test.js: -------------------------------------------------------------------------------- 1 | import { runForce } from "./helpers/run"; 2 | 3 | describe("force option", () => { 4 | describe("is not specified", () => { 5 | it('should not overwrite a file already in the compilation by default when "from" is a file', (done) => { 6 | runForce({ 7 | additionalAssets: [{ name: "file.txt", data: "existing" }], 8 | expectedAssetKeys: ["file.txt"], 9 | expectedAssetContent: { 10 | "file.txt": "existing", 11 | }, 12 | patterns: [ 13 | { 14 | from: "file.txt", 15 | }, 16 | ], 17 | }) 18 | .then(done) 19 | .catch(done); 20 | }); 21 | 22 | it('should not overwrite files already in the compilation when "from" is a directory', (done) => { 23 | runForce({ 24 | additionalAssets: [ 25 | { name: ".dottedfile", data: "existing" }, 26 | { name: "directoryfile.txt", data: "existing" }, 27 | { name: "nested/deep-nested/deepnested.txt", data: "existing" }, 28 | { name: "nested/nestedfile.txt", data: "existing" }, 29 | ], 30 | expectedAssetKeys: [ 31 | ".dottedfile", 32 | "directoryfile.txt", 33 | "nested/deep-nested/deepnested.txt", 34 | "nested/nestedfile.txt", 35 | ], 36 | expectedAssetContent: { 37 | ".dottedfile": "existing", 38 | "nested/deep-nested/deepnested.txt": "existing", 39 | "nested/nestedfile.txt": "existing", 40 | "directoryfile.txt": "existing", 41 | }, 42 | patterns: [ 43 | { 44 | from: "directory", 45 | }, 46 | ], 47 | }) 48 | .then(done) 49 | .catch(done); 50 | }); 51 | 52 | it('should not overwrite files already in the compilation when "from" is a glob', (done) => { 53 | runForce({ 54 | additionalAssets: [ 55 | { name: "directory/directoryfile.txt", data: "existing" }, 56 | { 57 | name: "directory/nested/deep-nested/deepnested.txt", 58 | data: "existing", 59 | }, 60 | { name: "directory/nested/nestedfile.txt", data: "existing" }, 61 | ], 62 | expectedAssetKeys: [ 63 | "directory/directoryfile.txt", 64 | "directory/nested/deep-nested/deepnested.txt", 65 | "directory/nested/nestedfile.txt", 66 | ], 67 | expectedAssetContent: { 68 | "directory/nested/deep-nested/deepnested.txt": "existing", 69 | "directory/nested/nestedfile.txt": "existing", 70 | "directory/directoryfile.txt": "existing", 71 | }, 72 | patterns: [ 73 | { 74 | from: "directory/**/*", 75 | }, 76 | ], 77 | }) 78 | .then(done) 79 | .catch(done); 80 | }); 81 | }); 82 | 83 | describe('is "false" (Boolean)', () => { 84 | it('should not overwrite a file already in the compilation by default when "from" is a file', (done) => { 85 | runForce({ 86 | additionalAssets: [{ name: "file.txt", data: "existing" }], 87 | expectedAssetKeys: ["file.txt"], 88 | expectedAssetContent: { 89 | "file.txt": "existing", 90 | }, 91 | patterns: [ 92 | { 93 | force: false, 94 | from: "file.txt", 95 | }, 96 | ], 97 | }) 98 | .then(done) 99 | .catch(done); 100 | }); 101 | 102 | it('should not overwrite files already in the compilation when "from" is a directory', (done) => { 103 | runForce({ 104 | additionalAssets: [ 105 | { name: ".dottedfile", data: "existing" }, 106 | { name: "directoryfile.txt", data: "existing" }, 107 | { name: "nested/deep-nested/deepnested.txt", data: "existing" }, 108 | { name: "nested/nestedfile.txt", data: "existing" }, 109 | ], 110 | expectedAssetKeys: [ 111 | ".dottedfile", 112 | "directoryfile.txt", 113 | "nested/deep-nested/deepnested.txt", 114 | "nested/nestedfile.txt", 115 | ], 116 | expectedAssetContent: { 117 | ".dottedfile": "existing", 118 | "nested/deep-nested/deepnested.txt": "existing", 119 | "nested/nestedfile.txt": "existing", 120 | "directoryfile.txt": "existing", 121 | }, 122 | patterns: [ 123 | { 124 | force: false, 125 | from: "directory", 126 | }, 127 | ], 128 | }) 129 | .then(done) 130 | .catch(done); 131 | }); 132 | 133 | it('should not overwrite files already in the compilation when "from" is a glob', (done) => { 134 | runForce({ 135 | additionalAssets: [ 136 | { name: "directory/directoryfile.txt", data: "existing" }, 137 | { 138 | name: "directory/nested/deep-nested/deepnested.txt", 139 | data: "existing", 140 | }, 141 | { name: "directory/nested/nestedfile.txt", data: "existing" }, 142 | ], 143 | expectedAssetKeys: [ 144 | "directory/directoryfile.txt", 145 | "directory/nested/deep-nested/deepnested.txt", 146 | "directory/nested/nestedfile.txt", 147 | ], 148 | expectedAssetContent: { 149 | "directory/nested/deep-nested/deepnested.txt": "existing", 150 | "directory/nested/nestedfile.txt": "existing", 151 | "directory/directoryfile.txt": "existing", 152 | }, 153 | patterns: [ 154 | { 155 | force: false, 156 | from: "directory/**/*", 157 | }, 158 | ], 159 | }) 160 | .then(done) 161 | .catch(done); 162 | }); 163 | }); 164 | 165 | describe('is "true" (Boolean)', () => { 166 | it('should force overwrite a file already in the compilation when "from" is a file', (done) => { 167 | runForce({ 168 | additionalAssets: [{ name: "file.txt", data: "existing" }], 169 | expectedAssetKeys: ["file.txt"], 170 | expectedAssetContent: { 171 | "file.txt": "new", 172 | }, 173 | patterns: [ 174 | { 175 | force: true, 176 | from: "file.txt", 177 | }, 178 | ], 179 | }) 180 | .then(done) 181 | .catch(done); 182 | }); 183 | 184 | it('should force overwrite files already in the compilation when "from" is a directory', (done) => { 185 | runForce({ 186 | additionalAssets: [ 187 | { name: ".dottedfile", data: "existing" }, 188 | { name: "directoryfile.txt", data: "existing" }, 189 | { name: "nested/deep-nested/deepnested.txt", data: "existing" }, 190 | { name: "nested/nestedfile.txt", data: "existing" }, 191 | ], 192 | expectedAssetKeys: [ 193 | ".dottedfile", 194 | "directoryfile.txt", 195 | "nested/deep-nested/deepnested.txt", 196 | "nested/nestedfile.txt", 197 | ], 198 | expectedAssetContent: { 199 | ".dottedfile": "dottedfile contents\n", 200 | "nested/deep-nested/deepnested.txt": "", 201 | "nested/nestedfile.txt": "", 202 | "directoryfile.txt": "new", 203 | }, 204 | patterns: [ 205 | { 206 | force: true, 207 | from: "directory", 208 | }, 209 | ], 210 | }) 211 | .then(done) 212 | .catch(done); 213 | }); 214 | 215 | it('should force overwrite files already in the compilation when "from" is a glob', (done) => { 216 | runForce({ 217 | additionalAssets: [ 218 | { name: "directory/directoryfile.txt", data: "existing" }, 219 | { 220 | name: "directory/nested/deep-nested/deepnested.txt", 221 | data: "existing", 222 | }, 223 | { name: "directory/nested/nestedfile.txt", data: "existing" }, 224 | ], 225 | expectedAssetKeys: [ 226 | "directory/directoryfile.txt", 227 | "directory/nested/deep-nested/deepnested.txt", 228 | "directory/nested/nestedfile.txt", 229 | ], 230 | expectedAssetContent: { 231 | "directory/nested/deep-nested/deepnested.txt": "", 232 | "directory/nested/nestedfile.txt": "", 233 | "directory/directoryfile.txt": "new", 234 | }, 235 | patterns: [ 236 | { 237 | force: true, 238 | from: "directory/**/*", 239 | }, 240 | ], 241 | }) 242 | .then(done) 243 | .catch(done); 244 | }); 245 | }); 246 | }); 247 | -------------------------------------------------------------------------------- /test/validate-options.test.js: -------------------------------------------------------------------------------- 1 | import CopyPlugin from "../src/index"; 2 | 3 | describe("validate options", () => { 4 | const tests = { 5 | patterns: { 6 | success: [ 7 | ["test.txt"], 8 | ["test.txt", "test-other.txt"], 9 | [ 10 | "test.txt", 11 | { 12 | from: "test.txt", 13 | to: "dir", 14 | context: "context", 15 | }, 16 | ], 17 | [ 18 | { 19 | from: "test.txt", 20 | }, 21 | ], 22 | [ 23 | { 24 | from: "test.txt", 25 | to: "dir", 26 | }, 27 | ], 28 | [ 29 | { 30 | from: "test.txt", 31 | to: () => {}, 32 | }, 33 | ], 34 | [ 35 | { 36 | from: "test.txt", 37 | context: "context", 38 | }, 39 | ], 40 | [ 41 | { 42 | from: "test.txt", 43 | to: "dir", 44 | context: "context", 45 | toType: "file", 46 | force: true, 47 | transform: { 48 | transformer: () => {}, 49 | cache: true, 50 | }, 51 | noErrorOnMissing: true, 52 | }, 53 | ], 54 | [ 55 | { 56 | from: "test.txt", 57 | to: "dir", 58 | context: "context", 59 | transform: () => {}, 60 | }, 61 | ], 62 | [ 63 | { 64 | from: "test.txt", 65 | to: "dir", 66 | context: "context", 67 | globOptions: { 68 | dot: false, 69 | }, 70 | }, 71 | ], 72 | [ 73 | { 74 | from: "test.txt", 75 | to: "dir", 76 | context: "context", 77 | transform: { 78 | cache: { 79 | keys: { 80 | foo: "bar", 81 | }, 82 | }, 83 | }, 84 | }, 85 | ], 86 | [ 87 | { 88 | from: "test.txt", 89 | to: "dir", 90 | context: "context", 91 | transform: { 92 | cache: { 93 | keys: () => ({ 94 | foo: "bar", 95 | }), 96 | }, 97 | }, 98 | }, 99 | ], 100 | [ 101 | { 102 | from: "test.txt", 103 | to: "dir", 104 | context: "context", 105 | transform: { 106 | cache: { 107 | keys: async () => ({ 108 | foo: "bar", 109 | }), 110 | }, 111 | }, 112 | }, 113 | ], 114 | [ 115 | { 116 | from: "test.txt", 117 | filter: () => true, 118 | }, 119 | ], 120 | [ 121 | { 122 | from: "test.txt", 123 | info: { custom: true }, 124 | }, 125 | { 126 | from: "test.txt", 127 | info: () => ({ custom: true }), 128 | }, 129 | ], 130 | [ 131 | { 132 | from: "test.txt", 133 | to: "dir", 134 | priority: 5, 135 | }, 136 | ], 137 | [ 138 | { 139 | from: "test.txt", 140 | to: "dir", 141 | context: "context", 142 | transformAll: ({ existingAsset }) => existingAsset.source.source(), 143 | }, 144 | ], 145 | ], 146 | failure: [ 147 | undefined, 148 | true, 149 | "true", 150 | "", 151 | {}, 152 | [], 153 | [""], 154 | [{}], 155 | [ 156 | { 157 | from: "dir", 158 | info: "string", 159 | }, 160 | ], 161 | [ 162 | { 163 | from: "dir", 164 | info: true, 165 | }, 166 | ], 167 | [ 168 | { 169 | from: "", 170 | to: "dir", 171 | context: "context", 172 | }, 173 | ], 174 | [ 175 | { 176 | from: true, 177 | to: "dir", 178 | context: "context", 179 | }, 180 | ], 181 | [ 182 | { 183 | from: "test.txt", 184 | to: true, 185 | context: "context", 186 | }, 187 | ], 188 | [ 189 | { 190 | from: "test.txt", 191 | to: "dir", 192 | context: true, 193 | }, 194 | ], 195 | [ 196 | { 197 | from: "test.txt", 198 | to: "dir", 199 | context: "context", 200 | toType: "foo", 201 | }, 202 | ], 203 | [ 204 | { 205 | from: "test.txt", 206 | to: "dir", 207 | context: "context", 208 | force: "true", 209 | }, 210 | ], 211 | [ 212 | { 213 | from: "test.txt", 214 | to: "dir", 215 | context: "context", 216 | transform: { 217 | foo: "bar", 218 | }, 219 | }, 220 | ], 221 | [ 222 | { 223 | from: "test.txt", 224 | to: "dir", 225 | context: "context", 226 | transform: true, 227 | }, 228 | ], 229 | [ 230 | { 231 | from: { 232 | glob: "**/*", 233 | dot: false, 234 | }, 235 | to: "dir", 236 | context: "context", 237 | }, 238 | ], 239 | [ 240 | { 241 | from: "", 242 | to: "dir", 243 | context: "context", 244 | noErrorOnMissing: "true", 245 | }, 246 | ], 247 | [ 248 | { 249 | from: "test.txt", 250 | filter: "test", 251 | }, 252 | ], 253 | [ 254 | { 255 | from: "test.txt", 256 | to: "dir", 257 | priority: "5", 258 | }, 259 | ], 260 | [ 261 | { 262 | from: "test.txt", 263 | to: "dir", 264 | priority: () => {}, 265 | }, 266 | ], 267 | [ 268 | { 269 | from: "test.txt", 270 | to: "dir", 271 | priority: true, 272 | }, 273 | ], 274 | [ 275 | { 276 | from: "test.txt", 277 | to: "dir", 278 | context: "context", 279 | transformAll: true, 280 | }, 281 | ], 282 | ], 283 | }, 284 | options: { 285 | success: [{ concurrency: 50 }], 286 | failure: [{ unknown: true }, { concurrency: true }], 287 | }, 288 | unknown: { 289 | success: [], 290 | failure: [1, true, false, "test", /test/, [], {}, { foo: "bar" }], 291 | }, 292 | }; 293 | 294 | function stringifyValue(value) { 295 | if ( 296 | Array.isArray(value) || 297 | (value && typeof value === "object" && value.constructor === Object) 298 | ) { 299 | return JSON.stringify(value); 300 | } 301 | 302 | return value; 303 | } 304 | 305 | async function createTestCase(key, value, type) { 306 | it(`should ${ 307 | type === "success" ? "successfully validate" : "throw an error on" 308 | } the "${key}" option with "${stringifyValue(value)}" value`, async () => { 309 | let error; 310 | 311 | try { 312 | // eslint-disable-next-line no-new 313 | new CopyPlugin( 314 | key === "options" 315 | ? { patterns: [{ from: "file.txt" }], [key]: value } 316 | : { [key]: value }, 317 | ); 318 | } catch (err) { 319 | if (err.name !== "ValidationError") { 320 | throw err; 321 | } 322 | 323 | error = err; 324 | } finally { 325 | if (type === "success") { 326 | expect(error).toBeUndefined(); 327 | } else if (type === "failure") { 328 | expect(() => { 329 | throw error; 330 | }).toThrowErrorMatchingSnapshot(); 331 | } 332 | } 333 | }); 334 | } 335 | 336 | for (const [key, values] of Object.entries(tests)) { 337 | for (const type of Object.keys(values)) { 338 | for (const value of values[type]) { 339 | createTestCase(key, value, type); 340 | } 341 | } 342 | } 343 | }); 344 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export = CopyPlugin; 2 | declare class CopyPlugin { 3 | /** 4 | * @private 5 | * @param {Compilation} compilation the compilation 6 | * @param {number} startTime the start time of the snapshot creation 7 | * @param {string} dependency the dependency for which the snapshot is created 8 | * @returns {Promise} creates a snapshot for the given dependency 9 | */ 10 | private static createSnapshot; 11 | /** 12 | * @private 13 | * @param {Compilation} compilation the compilation 14 | * @param {Snapshot} snapshot /the snapshot to check 15 | * @returns {Promise} checks if the snapshot is valid 16 | */ 17 | private static checkSnapshotValid; 18 | /** 19 | * @private 20 | * @param {Compiler} compiler the compiler 21 | * @param {Compilation} compilation the compilation 22 | * @param {Buffer} source the source content to hash 23 | * @returns {string} returns the content hash of the source 24 | */ 25 | private static getContentHash; 26 | /** 27 | * @private 28 | * @param {Compilation} compilation the compilation 29 | * @param {"file" | "dir" | "glob"} typeOfFrom the type of from 30 | * @param {string} absoluteFrom the source content to hash 31 | * @param {InputFileSystem | null} inputFileSystem input file system 32 | * @param {WebpackLogger} logger the logger to use for logging 33 | * @returns {Promise} 34 | */ 35 | private static addCompilationDependency; 36 | /** 37 | * @private 38 | * @param {typeof import("tinyglobby").glob} globby the globby function to use for globbing 39 | * @param {Compiler} compiler the compiler 40 | * @param {Compilation} compilation the compilation 41 | * @param {WebpackLogger} logger the logger to use for logging 42 | * @param {CacheFacade} cache the cache facade to use for caching 43 | * @param {number} concurrency /maximum number of concurrent operations 44 | * @param {ObjectPattern & { context: string }} pattern the pattern to process 45 | * @param {number} index the index of the pattern in the patterns array 46 | * @returns {Promise | undefined>} processes the pattern and returns an array of copied results 47 | */ 48 | private static glob; 49 | /** 50 | * @param {PluginOptions=} options options for the plugin 51 | */ 52 | constructor(options?: PluginOptions | undefined); 53 | /** 54 | * @private 55 | * @type {Pattern[]} 56 | */ 57 | private patterns; 58 | /** 59 | * @private 60 | * @type {AdditionalOptions} 61 | */ 62 | private options; 63 | /** 64 | * @param {Compiler} compiler the compiler 65 | */ 66 | apply(compiler: Compiler): void; 67 | } 68 | declare namespace CopyPlugin { 69 | export { 70 | Schema, 71 | Compiler, 72 | Compilation, 73 | Asset, 74 | AssetInfo, 75 | InputFileSystem, 76 | GlobbyOptions, 77 | WebpackLogger, 78 | CacheFacade, 79 | Etag, 80 | Snapshot, 81 | Force, 82 | CopiedResult, 83 | StringPattern, 84 | NoErrorOnMissing, 85 | Context, 86 | From, 87 | ToFunction, 88 | To, 89 | ToType, 90 | TransformerFunction, 91 | TransformerCacheObject, 92 | TransformerObject, 93 | Transform, 94 | Filter, 95 | TransformAllFunction, 96 | Info, 97 | ObjectPattern, 98 | Pattern, 99 | AdditionalOptions, 100 | PluginOptions, 101 | }; 102 | } 103 | type Schema = import("schema-utils/declarations/validate").Schema; 104 | type Compiler = import("webpack").Compiler; 105 | type Compilation = import("webpack").Compilation; 106 | type Asset = import("webpack").Asset; 107 | type AssetInfo = import("webpack").AssetInfo; 108 | type InputFileSystem = import("webpack").InputFileSystem; 109 | type GlobbyOptions = import("tinyglobby").GlobOptions; 110 | type WebpackLogger = ReturnType; 111 | type CacheFacade = ReturnType; 112 | type Etag = ReturnType< 113 | ReturnType["getLazyHashedEtag"] 114 | >; 115 | type Snapshot = ReturnType; 116 | type Force = boolean; 117 | type CopiedResult = { 118 | /** 119 | * relative path to the file from the context 120 | */ 121 | sourceFilename: string; 122 | /** 123 | * absolute path to the file 124 | */ 125 | absoluteFilename: string; 126 | /** 127 | * relative path to the file from the output path 128 | */ 129 | filename: string; 130 | /** 131 | * source of the file 132 | */ 133 | source: Asset["source"]; 134 | /** 135 | * whether to force update the asset if it already exists 136 | */ 137 | force: Force | undefined; 138 | /** 139 | * additional information about the asset 140 | */ 141 | info: Record; 142 | }; 143 | type StringPattern = string; 144 | type NoErrorOnMissing = boolean; 145 | type Context = string; 146 | type From = string; 147 | type ToFunction = (pathData: { 148 | context: string; 149 | absoluteFilename?: string; 150 | }) => string | Promise; 151 | type To = string | ToFunction; 152 | type ToType = "dir" | "file" | "template"; 153 | type TransformerFunction = ( 154 | input: Buffer, 155 | absoluteFilename: string, 156 | ) => string | Buffer | Promise | Promise; 157 | type TransformerCacheObject = 158 | | { 159 | keys: { 160 | [key: string]: unknown; 161 | }; 162 | } 163 | | { 164 | keys: ( 165 | defaultCacheKeys: { 166 | [key: string]: unknown; 167 | }, 168 | absoluteFilename: string, 169 | ) => Promise<{ 170 | [key: string]: unknown; 171 | }>; 172 | }; 173 | type TransformerObject = { 174 | /** 175 | * function to transform the file content 176 | */ 177 | transformer: TransformerFunction; 178 | /** 179 | * whether to cache the transformed content or an object with keys for caching 180 | */ 181 | cache?: (boolean | TransformerCacheObject) | undefined; 182 | }; 183 | type Transform = TransformerFunction | TransformerObject; 184 | type Filter = (filepath: string) => boolean | Promise; 185 | type TransformAllFunction = ( 186 | data: { 187 | data: Buffer; 188 | sourceFilename: string; 189 | absoluteFilename: string; 190 | }[], 191 | ) => string | Buffer | Promise | Promise; 192 | type Info = 193 | | Record 194 | | ((item: { 195 | absoluteFilename: string; 196 | sourceFilename: string; 197 | filename: string; 198 | toType: ToType; 199 | }) => Record); 200 | type ObjectPattern = { 201 | /** 202 | * source path or glob pattern to copy files from 203 | */ 204 | from: From; 205 | /** 206 | * options for globbing 207 | */ 208 | globOptions?: GlobbyOptions | undefined; 209 | /** 210 | * context for the source path or glob pattern 211 | */ 212 | context?: Context | undefined; 213 | /** 214 | * destination path or function to determine the destination path 215 | */ 216 | to?: To | undefined; 217 | /** 218 | * type of the destination path, can be "dir", "file" or "template" 219 | */ 220 | toType?: ToType | undefined; 221 | /** 222 | * additional information about the asset 223 | */ 224 | info?: Info | undefined; 225 | /** 226 | * function to filter files, if it returns false, the file will be skipped 227 | */ 228 | filter?: Filter | undefined; 229 | /** 230 | * function to transform the file content, can be a function or an object with a transformer function and cache options 231 | */ 232 | transform?: Transform | undefined; 233 | /** 234 | * function to transform all files, it receives an array of objects with data, sourceFilename and absoluteFilename properties 235 | */ 236 | transformAll?: TransformAllFunction | undefined; 237 | /** 238 | * whether to force update the asset if it already exists 239 | */ 240 | force?: Force | undefined; 241 | /** 242 | * priority of the pattern, patterns with higher priority will be processed first 243 | */ 244 | priority?: number | undefined; 245 | /** 246 | * whether to skip errors when no files are found for the pattern 247 | */ 248 | noErrorOnMissing?: NoErrorOnMissing | undefined; 249 | }; 250 | type Pattern = StringPattern | ObjectPattern; 251 | type AdditionalOptions = { 252 | /** 253 | * maximum number of concurrent operations, default is 100 254 | */ 255 | concurrency?: number | undefined; 256 | }; 257 | type PluginOptions = { 258 | /** 259 | * array of patterns to copy files from 260 | */ 261 | patterns: Pattern[]; 262 | /** 263 | * additional options for the plugin 264 | */ 265 | options?: AdditionalOptions | undefined; 266 | }; 267 | -------------------------------------------------------------------------------- /test/globOptions-option.test.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | import { runEmit } from "./helpers/run"; 4 | 5 | const FIXTURES_DIR_NORMALIZED = path 6 | .join(__dirname, "fixtures") 7 | .replaceAll("\\", "/"); 8 | 9 | describe("globOptions option", () => { 10 | // Expected behavior from `globby`/`fast-glob` 11 | it('should copy files exclude dot files when "from" is a directory', (done) => { 12 | runEmit({ 13 | expectedAssetKeys: [".file.txt"], 14 | patterns: [ 15 | { 16 | from: ".file.txt", 17 | globOptions: { 18 | dot: false, 19 | }, 20 | }, 21 | ], 22 | }) 23 | .then(done) 24 | .catch(done); 25 | }); 26 | 27 | it('should copy files exclude dot files when "from" is a directory (variant 2)', (done) => { 28 | runEmit({ 29 | expectedAssetKeys: [ 30 | "directoryfile.txt", 31 | "nested/deep-nested/deepnested.txt", 32 | "nested/nestedfile.txt", 33 | ], 34 | patterns: [ 35 | { 36 | from: "directory", 37 | globOptions: { 38 | dot: false, 39 | }, 40 | }, 41 | ], 42 | }) 43 | .then(done) 44 | .catch(done); 45 | }); 46 | 47 | it('should copy files exclude dot files when "from" is a glob (variant 2)', (done) => { 48 | runEmit({ 49 | expectedAssetKeys: ["file.txt"], 50 | patterns: [ 51 | { 52 | from: "*.txt", 53 | globOptions: { 54 | dot: false, 55 | }, 56 | }, 57 | ], 58 | }) 59 | .then(done) 60 | .catch(done); 61 | }); 62 | 63 | it("should copy files include dot files (variant 2)", (done) => { 64 | runEmit({ 65 | expectedAssetKeys: [".file.txt", "file.txt"], 66 | patterns: [ 67 | { 68 | from: "*.txt", 69 | globOptions: { 70 | dot: true, 71 | }, 72 | }, 73 | ], 74 | }) 75 | .then(done) 76 | .catch(done); 77 | }); 78 | 79 | it('should ignore files when "from" is a file (variant 2)', (done) => { 80 | runEmit({ 81 | expectedErrors: [ 82 | new Error( 83 | `unable to locate '${FIXTURES_DIR_NORMALIZED}/file.txt' glob`, 84 | ), 85 | ], 86 | patterns: [ 87 | { 88 | from: "file.txt", 89 | globOptions: { 90 | ignore: ["**/file.*"], 91 | }, 92 | }, 93 | ], 94 | }) 95 | .then(done) 96 | .catch(done); 97 | }); 98 | 99 | it('should files when "from" is a directory (variant 2)', (done) => { 100 | runEmit({ 101 | expectedAssetKeys: [ 102 | ".dottedfile", 103 | "directoryfile.txt", 104 | "nested/deep-nested/deepnested.txt", 105 | ], 106 | patterns: [ 107 | { 108 | from: "directory", 109 | globOptions: { 110 | ignore: ["**/nestedfile.*"], 111 | }, 112 | }, 113 | ], 114 | }) 115 | .then(done) 116 | .catch(done); 117 | }); 118 | 119 | it("should work with globOptions.objectMode && globOptions.gitignore", (done) => { 120 | runEmit({ 121 | expectedAssetKeys: [ 122 | ".dottedfile", 123 | "directoryfile.txt", 124 | "nested/deep-nested/deepnested.txt", 125 | ], 126 | patterns: [ 127 | { 128 | from: "directory", 129 | globOptions: { 130 | objectMode: true, 131 | gitignore: true, 132 | ignore: ["**/nestedfile.*"], 133 | }, 134 | }, 135 | ], 136 | }) 137 | .then(done) 138 | .catch(done); 139 | }); 140 | 141 | it('should files in nested directory when "from" is a directory (variant 2)', (done) => { 142 | runEmit({ 143 | expectedAssetKeys: [".dottedfile", "directoryfile.txt"], 144 | patterns: [ 145 | { 146 | from: "directory", 147 | globOptions: { 148 | ignore: ["**/nested/**"], 149 | }, 150 | }, 151 | ], 152 | }) 153 | .then(done) 154 | .catch(done); 155 | }); 156 | 157 | it("should files when from is a glob (variant 2)", (done) => { 158 | runEmit({ 159 | expectedAssetKeys: [ 160 | "directory/directoryfile.txt", 161 | "directory/nested/deep-nested/deepnested.txt", 162 | ], 163 | patterns: [ 164 | { 165 | from: "directory/**/*", 166 | globOptions: { 167 | ignore: ["**/nestedfile.*"], 168 | }, 169 | }, 170 | ], 171 | }) 172 | .then(done) 173 | .catch(done); 174 | }); 175 | 176 | it("should files in nested directory when from is a glob (variant 2)", (done) => { 177 | runEmit({ 178 | expectedAssetKeys: ["directory/directoryfile.txt"], 179 | patterns: [ 180 | { 181 | from: "directory/**/*", 182 | globOptions: { 183 | ignore: ["**/nested/**"], 184 | }, 185 | }, 186 | ], 187 | }) 188 | .then(done) 189 | .catch(done); 190 | }); 191 | 192 | it("should ignore files with a certain extension (variant 2)", (done) => { 193 | runEmit({ 194 | expectedAssetKeys: [".dottedfile"], 195 | patterns: [ 196 | { 197 | from: "directory", 198 | globOptions: { 199 | ignore: ["**/*.txt"], 200 | }, 201 | }, 202 | ], 203 | }) 204 | .then(done) 205 | .catch(done); 206 | }); 207 | 208 | it("should ignore files with multiple ignore patterns (variant 2)", (done) => { 209 | runEmit({ 210 | expectedAssetKeys: ["directory/nested/nestedfile.txt"], 211 | patterns: [ 212 | { 213 | from: "directory/**/*", 214 | globOptions: { 215 | ignore: ["**/directoryfile.*", "**/deep-nested/**"], 216 | }, 217 | }, 218 | ], 219 | }) 220 | .then(done) 221 | .catch(done); 222 | }); 223 | 224 | it("should ignore files with flatten copy", (done) => { 225 | runEmit({ 226 | expectedAssetKeys: ["img/.dottedfile", "img/nestedfile.txt"], 227 | patterns: [ 228 | { 229 | from: "directory/", 230 | toType: "file", 231 | to({ absoluteFilename }) { 232 | return `img/${path.basename(absoluteFilename)}`; 233 | }, 234 | globOptions: { 235 | ignore: ["**/directoryfile.*", "**/deep-nested/**"], 236 | }, 237 | }, 238 | ], 239 | }) 240 | .then(done) 241 | .catch(done); 242 | }); 243 | 244 | it("should ignore files except those with dots", (done) => { 245 | runEmit({ 246 | expectedAssetKeys: [".dottedfile"], 247 | patterns: [ 248 | { 249 | from: "directory", 250 | globOptions: { 251 | ignore: ["!(**/.*)"], 252 | }, 253 | }, 254 | ], 255 | }) 256 | .then(done) 257 | .catch(done); 258 | }); 259 | 260 | it("should ignore files that start with a dot", (done) => { 261 | runEmit({ 262 | expectedAssetKeys: [ 263 | "directoryfile.txt", 264 | "nested/deep-nested/deepnested.txt", 265 | "nested/nestedfile.txt", 266 | ], 267 | patterns: [ 268 | { 269 | from: "directory", 270 | globOptions: { 271 | ignore: ["**/.*"], 272 | }, 273 | }, 274 | ], 275 | }) 276 | .then(done) 277 | .catch(done); 278 | }); 279 | 280 | it("should ignores all files even if they start with a dot", (done) => { 281 | runEmit({ 282 | expectedErrors: [ 283 | new Error( 284 | `unable to locate '${FIXTURES_DIR_NORMALIZED}/directory/**/*' glob`, 285 | ), 286 | ], 287 | patterns: [ 288 | { 289 | from: "directory", 290 | globOptions: { 291 | ignore: ["**/*"], 292 | }, 293 | }, 294 | ], 295 | }) 296 | .then(done) 297 | .catch(done); 298 | }); 299 | 300 | it('should ignore files when "from" is a file (global ignore)', (done) => { 301 | runEmit({ 302 | expectedErrors: [ 303 | new Error( 304 | `unable to locate '${FIXTURES_DIR_NORMALIZED}/file.txt' glob`, 305 | ), 306 | ], 307 | patterns: [ 308 | { 309 | from: "file.txt", 310 | globOptions: { 311 | ignore: ["**/file.*"], 312 | }, 313 | }, 314 | ], 315 | }) 316 | .then(done) 317 | .catch(done); 318 | }); 319 | 320 | it('should ignore the "cwd" option', (done) => { 321 | runEmit({ 322 | expectedAssetKeys: [ 323 | ".dottedfile", 324 | "directoryfile.txt", 325 | "nested/deep-nested/deepnested.txt", 326 | "nested/nestedfile.txt", 327 | ], 328 | patterns: [ 329 | { 330 | from: "directory", 331 | globOptions: { 332 | cwd: path.resolve(__dirname, "fixtures/nested"), 333 | }, 334 | }, 335 | ], 336 | }) 337 | .then(done) 338 | .catch(done); 339 | }); 340 | 341 | it('should work with the "deep" option', (done) => { 342 | runEmit({ 343 | expectedAssetKeys: [ 344 | ".dottedfile", 345 | "directoryfile.txt", 346 | "nested/nestedfile.txt", 347 | ], 348 | patterns: [ 349 | { 350 | from: "directory", 351 | globOptions: { 352 | deep: 2, 353 | }, 354 | }, 355 | ], 356 | }) 357 | .then(done) 358 | .catch(done); 359 | }); 360 | 361 | it('should work with the "markDirectories" option', (done) => { 362 | runEmit({ 363 | expectedAssetKeys: [ 364 | ".dottedfile", 365 | "directoryfile.txt", 366 | "nested/deep-nested/deepnested.txt", 367 | "nested/nestedfile.txt", 368 | ], 369 | patterns: [ 370 | { 371 | from: "directory", 372 | globOptions: { 373 | markDirectories: true, 374 | }, 375 | }, 376 | ], 377 | }) 378 | .then(done) 379 | .catch(done); 380 | }); 381 | 382 | it('should work with the "objectMode" option', (done) => { 383 | runEmit({ 384 | expectedAssetKeys: [ 385 | ".dottedfile", 386 | "directoryfile.txt", 387 | "nested/deep-nested/deepnested.txt", 388 | "nested/nestedfile.txt", 389 | ], 390 | patterns: [ 391 | { 392 | from: "directory", 393 | globOptions: { 394 | objectMode: true, 395 | }, 396 | }, 397 | ], 398 | }) 399 | .then(done) 400 | .catch(done); 401 | }); 402 | 403 | it("should emit error when not found assets for copy", (done) => { 404 | expect.assertions(1); 405 | 406 | runEmit({ 407 | expectedAssetKeys: [], 408 | patterns: [ 409 | { 410 | from: "directory", 411 | globOptions: { 412 | onlyDirectories: true, 413 | }, 414 | }, 415 | ], 416 | }) 417 | .then(done) 418 | .catch((error) => { 419 | expect(error).toBeDefined(); 420 | 421 | done(); 422 | }); 423 | }); 424 | 425 | it('should work with the "onlyFiles" option', (done) => { 426 | runEmit({ 427 | expectedAssetKeys: [ 428 | ".dottedfile", 429 | "directoryfile.txt", 430 | "nested/deep-nested/deepnested.txt", 431 | "nested/nestedfile.txt", 432 | ], 433 | patterns: [ 434 | { 435 | from: "directory", 436 | globOptions: { 437 | onlyFiles: true, 438 | }, 439 | }, 440 | ], 441 | }) 442 | .then(done) 443 | .catch(done); 444 | }); 445 | }); 446 | -------------------------------------------------------------------------------- /test/from-option.test.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | import { getCompiler } from "./helpers"; 4 | import { runEmit } from "./helpers/run"; 5 | 6 | const FIXTURES_DIR_NORMALIZED = path 7 | .join(__dirname, "fixtures") 8 | .replaceAll("\\", "/"); 9 | 10 | describe("from option", () => { 11 | describe("is a file", () => { 12 | it("should copy a file", (done) => { 13 | runEmit({ 14 | expectedAssetKeys: ["file.txt"], 15 | patterns: [ 16 | { 17 | from: "file.txt", 18 | }, 19 | ], 20 | }) 21 | .then(done) 22 | .catch(done); 23 | }); 24 | 25 | it('should copy a file when "from" an absolute path', (done) => { 26 | runEmit({ 27 | expectedAssetKeys: ["file.txt"], 28 | patterns: [ 29 | { 30 | from: path.posix.join(FIXTURES_DIR_NORMALIZED, "file.txt"), 31 | }, 32 | ], 33 | }) 34 | .then(done) 35 | .catch(done); 36 | }); 37 | 38 | it("should copy a file from nesting directory", (done) => { 39 | runEmit({ 40 | expectedAssetKeys: ["directoryfile.txt"], 41 | patterns: [ 42 | { 43 | from: "directory/directoryfile.txt", 44 | }, 45 | ], 46 | }) 47 | .then(done) 48 | .catch(done); 49 | }); 50 | 51 | it('should copy a file from nesting directory when "from" an absolute path', (done) => { 52 | runEmit({ 53 | expectedAssetKeys: ["directoryfile.txt"], 54 | patterns: [ 55 | { 56 | from: path.posix.join( 57 | FIXTURES_DIR_NORMALIZED, 58 | "directory/directoryfile.txt", 59 | ), 60 | }, 61 | ], 62 | }) 63 | .then(done) 64 | .catch(done); 65 | }); 66 | 67 | it("should copy a file (symbolic link)", (done) => { 68 | runEmit({ 69 | symlink: true, 70 | expectedErrors: 71 | process.platform === "win32" 72 | ? [ 73 | new Error( 74 | `unable to locate '${FIXTURES_DIR_NORMALIZED}/symlink/file-ln.txt' glob`, 75 | ), 76 | ] 77 | : [], 78 | expectedAssetKeys: process.platform === "win32" ? [] : ["file-ln.txt"], 79 | patterns: [ 80 | { 81 | from: "symlink/file-ln.txt", 82 | }, 83 | ], 84 | }) 85 | .then(done) 86 | .catch(done); 87 | }); 88 | 89 | it("should throw an error on the missing file", (done) => { 90 | runEmit({ 91 | expectedAssetKeys: [], 92 | expectedErrors: [ 93 | new Error( 94 | `unable to locate '${FIXTURES_DIR_NORMALIZED}/nonexistent.txt' glob`, 95 | ), 96 | ], 97 | patterns: [ 98 | { 99 | from: "nonexistent.txt", 100 | }, 101 | ], 102 | }) 103 | .then(done) 104 | .catch(done); 105 | }); 106 | }); 107 | 108 | describe("is a directory", () => { 109 | it("should copy files", (done) => { 110 | runEmit({ 111 | expectedAssetKeys: [ 112 | ".dottedfile", 113 | "directoryfile.txt", 114 | "nested/deep-nested/deepnested.txt", 115 | "nested/nestedfile.txt", 116 | ], 117 | patterns: [ 118 | { 119 | from: "directory", 120 | }, 121 | ], 122 | }) 123 | .then(done) 124 | .catch(done); 125 | }); 126 | 127 | it('should copy files when "from" is current directory', (done) => { 128 | runEmit({ 129 | expectedAssetKeys: [ 130 | ".file.txt", 131 | "[(){}[]!+@escaped-test^$]/hello.txt", 132 | "[special$directory]/(special-*file).txt", 133 | "[special$directory]/directoryfile.txt", 134 | "[special$directory]/nested/nestedfile.txt", 135 | "binextension.bin", 136 | "dir (86)/file.txt", 137 | "dir (86)/nesteddir/deepnesteddir/deepnesteddir.txt", 138 | "dir (86)/nesteddir/nestedfile.txt", 139 | "directory/.dottedfile", 140 | "directory/directoryfile.txt", 141 | "directory/nested/deep-nested/deepnested.txt", 142 | "directory/nested/nestedfile.txt", 143 | "file.txt", 144 | "file.txt.gz", 145 | "noextension", 146 | ], 147 | patterns: [ 148 | { 149 | from: ".", 150 | }, 151 | ], 152 | }) 153 | .then(done) 154 | .catch(done); 155 | }); 156 | 157 | it('should copy files when "from" is relative path to context', (done) => { 158 | runEmit({ 159 | expectedAssetKeys: [ 160 | ".file.txt", 161 | "[(){}[]!+@escaped-test^$]/hello.txt", 162 | "[special$directory]/(special-*file).txt", 163 | "[special$directory]/directoryfile.txt", 164 | "[special$directory]/nested/nestedfile.txt", 165 | "binextension.bin", 166 | "dir (86)/file.txt", 167 | "dir (86)/nesteddir/deepnesteddir/deepnesteddir.txt", 168 | "dir (86)/nesteddir/nestedfile.txt", 169 | "directory/.dottedfile", 170 | "directory/directoryfile.txt", 171 | "directory/nested/deep-nested/deepnested.txt", 172 | "directory/nested/nestedfile.txt", 173 | "file.txt", 174 | "file.txt.gz", 175 | "noextension", 176 | ], 177 | patterns: [ 178 | { 179 | from: "../fixtures", 180 | }, 181 | ], 182 | }) 183 | .then(done) 184 | .catch(done); 185 | }); 186 | 187 | it("should copy files with a forward slash", (done) => { 188 | runEmit({ 189 | expectedAssetKeys: [ 190 | ".dottedfile", 191 | "directoryfile.txt", 192 | "nested/deep-nested/deepnested.txt", 193 | "nested/nestedfile.txt", 194 | ], 195 | patterns: [ 196 | { 197 | from: "directory/", 198 | }, 199 | ], 200 | }) 201 | .then(done) 202 | .catch(done); 203 | }); 204 | 205 | it("should copy files from symbolic link", (done) => { 206 | runEmit({ 207 | // Windows doesn't support symbolic link 208 | symlink: true, 209 | expectedErrors: 210 | process.platform === "win32" 211 | ? [ 212 | new Error( 213 | `unable to locate '${FIXTURES_DIR_NORMALIZED}/symlink/directory-ln/**/*' glob`, 214 | ), 215 | ] 216 | : [], 217 | expectedAssetKeys: 218 | process.platform === "win32" 219 | ? [] 220 | : ["file.txt", "nested-directory/file-in-nested-directory.txt"], 221 | patterns: [ 222 | { 223 | from: "symlink/directory-ln", 224 | }, 225 | ], 226 | }) 227 | .then(done) 228 | .catch(done); 229 | }); 230 | 231 | it("should copy files when 'from' is a absolute path", (done) => { 232 | runEmit({ 233 | expectedAssetKeys: [ 234 | ".dottedfile", 235 | "directoryfile.txt", 236 | "nested/deep-nested/deepnested.txt", 237 | "nested/nestedfile.txt", 238 | ], 239 | patterns: [ 240 | { 241 | from: path.posix.join(FIXTURES_DIR_NORMALIZED, "directory"), 242 | }, 243 | ], 244 | }) 245 | .then(done) 246 | .catch(done); 247 | }); 248 | 249 | it("should copy files when 'from' with special characters", (done) => { 250 | runEmit({ 251 | expectedAssetKeys: [ 252 | "directoryfile.txt", 253 | "(special-*file).txt", 254 | "nested/nestedfile.txt", 255 | ], 256 | patterns: [ 257 | { 258 | from: 259 | path.sep === "/" ? "[special$directory]" : "[special$directory]", 260 | }, 261 | ], 262 | }) 263 | .then(done) 264 | .catch(done); 265 | }); 266 | 267 | it("should copy files from nested directory", (done) => { 268 | runEmit({ 269 | expectedAssetKeys: ["deep-nested/deepnested.txt", "nestedfile.txt"], 270 | patterns: [ 271 | { 272 | from: "directory/nested", 273 | }, 274 | ], 275 | }) 276 | .then(done) 277 | .catch(done); 278 | }); 279 | 280 | it("should copy files from nested directory with an absolute path", (done) => { 281 | runEmit({ 282 | expectedAssetKeys: ["deep-nested/deepnested.txt", "nestedfile.txt"], 283 | patterns: [ 284 | { 285 | from: path.posix.join(FIXTURES_DIR_NORMALIZED, "directory/nested"), 286 | }, 287 | ], 288 | }) 289 | .then(done) 290 | .catch(done); 291 | }); 292 | 293 | it("should throw an error on the missing directory", (done) => { 294 | runEmit({ 295 | expectedAssetKeys: [], 296 | expectedErrors: [ 297 | new Error( 298 | `unable to locate '${FIXTURES_DIR_NORMALIZED}/nonexistent' glob`, 299 | ), 300 | ], 301 | patterns: [ 302 | { 303 | from: "nonexistent", 304 | }, 305 | ], 306 | }) 307 | .then(done) 308 | .catch(done); 309 | }); 310 | }); 311 | 312 | describe("is a glob", () => { 313 | it("should copy files", (done) => { 314 | runEmit({ 315 | expectedAssetKeys: ["file.txt"], 316 | patterns: [ 317 | { 318 | from: "*.txt", 319 | }, 320 | ], 321 | }) 322 | .then(done) 323 | .catch(done); 324 | }); 325 | 326 | it("should copy files when a glob contains absolute path", (done) => { 327 | runEmit({ 328 | expectedAssetKeys: ["file.txt"], 329 | patterns: [ 330 | { 331 | from: path.posix.join(FIXTURES_DIR_NORMALIZED, "*.txt"), 332 | }, 333 | ], 334 | }) 335 | .then(done) 336 | .catch(done); 337 | }); 338 | 339 | it("should copy files using globstar", (done) => { 340 | runEmit({ 341 | expectedAssetKeys: [ 342 | "[(){}[]!+@escaped-test^$]/hello.txt", 343 | "binextension.bin", 344 | "dir (86)/file.txt", 345 | "dir (86)/nesteddir/deepnesteddir/deepnesteddir.txt", 346 | "dir (86)/nesteddir/nestedfile.txt", 347 | "file.txt", 348 | "file.txt.gz", 349 | "directory/directoryfile.txt", 350 | "directory/nested/deep-nested/deepnested.txt", 351 | "directory/nested/nestedfile.txt", 352 | "[special$directory]/directoryfile.txt", 353 | "[special$directory]/(special-*file).txt", 354 | "[special$directory]/nested/nestedfile.txt", 355 | "noextension", 356 | ], 357 | patterns: [ 358 | { 359 | from: "**/*", 360 | }, 361 | ], 362 | }) 363 | .then(done) 364 | .catch(done); 365 | }); 366 | 367 | it("should copy files using globstar and contains an absolute path", (done) => { 368 | runEmit({ 369 | expectedAssetKeys: [ 370 | "[(){}[]!+@escaped-test^$]/hello.txt", 371 | "file.txt", 372 | "directory/directoryfile.txt", 373 | "directory/nested/deep-nested/deepnested.txt", 374 | "directory/nested/nestedfile.txt", 375 | "[special$directory]/directoryfile.txt", 376 | "[special$directory]/(special-*file).txt", 377 | "[special$directory]/nested/nestedfile.txt", 378 | "dir (86)/file.txt", 379 | "dir (86)/nesteddir/deepnesteddir/deepnesteddir.txt", 380 | "dir (86)/nesteddir/nestedfile.txt", 381 | ], 382 | patterns: [ 383 | { 384 | from: path.posix.join(FIXTURES_DIR_NORMALIZED, "**/*.txt"), 385 | }, 386 | ], 387 | }) 388 | .then(done) 389 | .catch(done); 390 | }); 391 | 392 | it("should copy files in nested directory using globstar", (done) => { 393 | const compiler = getCompiler({ 394 | output: { 395 | hashDigestLength: 6, 396 | }, 397 | }); 398 | 399 | runEmit({ 400 | compiler, 401 | expectedAssetKeys: [ 402 | "nested/[(){}[]!+@escaped-test^$]/hello-31d6cf.txt", 403 | "nested/binextension-31d6cf.bin", 404 | "nested/dir (86)/file-31d6cf.txt", 405 | "nested/dir (86)/nesteddir/deepnesteddir/deepnesteddir-31d6cf.txt", 406 | "nested/dir (86)/nesteddir/nestedfile-31d6cf.txt", 407 | "nested/file-5d7817.txt", 408 | "nested/file.txt-f18c8d.gz", 409 | "nested/directory/directoryfile-5d7817.txt", 410 | "nested/directory/nested/deep-nested/deepnested-31d6cf.txt", 411 | "nested/directory/nested/nestedfile-31d6cf.txt", 412 | "nested/[special$directory]/(special-*file)-517cf2.txt", 413 | "nested/[special$directory]/directoryfile-5d7817.txt", 414 | "nested/[special$directory]/nested/nestedfile-31d6cf.txt", 415 | "nested/noextension-31d6cf", 416 | ], 417 | patterns: [ 418 | { 419 | from: "**/*", 420 | to: "nested/[path][name]-[contenthash][ext]", 421 | }, 422 | ], 423 | }) 424 | .then(done) 425 | .catch(done); 426 | }); 427 | 428 | it("should copy files from nested directory", (done) => { 429 | runEmit({ 430 | expectedAssetKeys: ["directory/directoryfile.txt"], 431 | patterns: [ 432 | { 433 | from: "directory/directory*.txt", 434 | }, 435 | ], 436 | }) 437 | .then(done) 438 | .catch(done); 439 | }); 440 | 441 | it("should copy files from nested directory #2", (done) => { 442 | runEmit({ 443 | expectedAssetKeys: [ 444 | "directory/directoryfile.txt", 445 | "directory/nested/deep-nested/deepnested.txt", 446 | "directory/nested/nestedfile.txt", 447 | ], 448 | patterns: [ 449 | { 450 | from: "directory/**/*.txt", 451 | }, 452 | ], 453 | }) 454 | .then(done) 455 | .catch(done); 456 | }); 457 | 458 | it("should copy files using bracketed glob", (done) => { 459 | runEmit({ 460 | expectedAssetKeys: [ 461 | "directory/directoryfile.txt", 462 | "directory/nested/deep-nested/deepnested.txt", 463 | "directory/nested/nestedfile.txt", 464 | "file.txt", 465 | "noextension", 466 | ], 467 | patterns: [ 468 | { 469 | from: "{file.txt,noextension,directory/**/*}", 470 | }, 471 | ], 472 | }) 473 | .then(done) 474 | .catch(done); 475 | }); 476 | 477 | it("should copy files (symbolic link)", (done) => { 478 | runEmit({ 479 | // Windows doesn't support symbolic link 480 | symlink: true, 481 | expectedErrors: 482 | process.platform === "win32" 483 | ? [ 484 | new Error( 485 | `unable to locate '${FIXTURES_DIR_NORMALIZED}/symlink/**/*.txt' glob`, 486 | ), 487 | ] 488 | : [], 489 | expectedAssetKeys: 490 | process.platform === "win32" 491 | ? [] 492 | : [ 493 | "symlink/directory-ln/file.txt", 494 | "symlink/directory-ln/nested-directory/file-in-nested-directory.txt", 495 | "symlink/directory/file.txt", 496 | "symlink/directory/nested-directory/file-in-nested-directory.txt", 497 | "symlink/file-ln.txt", 498 | "symlink/file.txt", 499 | ], 500 | patterns: [ 501 | { 502 | from: "symlink/**/*.txt", 503 | }, 504 | ], 505 | }) 506 | .then(done) 507 | .catch(done); 508 | }); 509 | 510 | it("should throw an error on the missing glob", (done) => { 511 | runEmit({ 512 | expectedAssetKeys: [], 513 | expectedErrors: [ 514 | new Error( 515 | `unable to locate '${FIXTURES_DIR_NORMALIZED}/nonexistent/**/*' glob`, 516 | ), 517 | ], 518 | patterns: [ 519 | { 520 | from: "nonexistent/**/*", 521 | }, 522 | ], 523 | }) 524 | .then(done) 525 | .catch(done); 526 | }); 527 | }); 528 | }); 529 | -------------------------------------------------------------------------------- /test/__snapshots__/validate-options.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`validate options should throw an error on the "options" option with "{"concurrency":true}" value 1`] = ` 4 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 5 | - options.options.concurrency should be a number. 6 | -> Limits the number of simultaneous requests to fs. 7 | -> Read more at https://github.com/webpack/copy-webpack-plugin#concurrency" 8 | `; 9 | 10 | exports[`validate options should throw an error on the "options" option with "{"unknown":true}" value 1`] = ` 11 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 12 | - options.options has an unknown property 'unknown'. These properties are valid: 13 | object { concurrency? }" 14 | `; 15 | 16 | exports[`validate options should throw an error on the "patterns" option with "" value 1`] = ` 17 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 18 | - options.patterns should be an array: 19 | [non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" 20 | `; 21 | 22 | exports[`validate options should throw an error on the "patterns" option with "[""]" value 1`] = ` 23 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 24 | - options.patterns[0] should be a non-empty string." 25 | `; 26 | 27 | exports[`validate options should throw an error on the "patterns" option with "[]" value 1`] = ` 28 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 29 | - options.patterns should be a non-empty array." 30 | `; 31 | 32 | exports[`validate options should throw an error on the "patterns" option with "[{"from":"","to":"dir","context":"context","noErrorOnMissing":"true"}]" value 1`] = ` 33 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 34 | - options.patterns[0].noErrorOnMissing should be a boolean. 35 | -> Doesn't generate an error on missing file(s). 36 | -> Read more at https://github.com/webpack/copy-webpack-plugin#noerroronmissing" 37 | `; 38 | 39 | exports[`validate options should throw an error on the "patterns" option with "[{"from":"","to":"dir","context":"context"}]" value 1`] = ` 40 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 41 | - options.patterns[0].from should be a non-empty string. 42 | -> Glob or path from where we copy files. 43 | -> Read more at https://github.com/webpack/copy-webpack-plugin#from" 44 | `; 45 | 46 | exports[`validate options should throw an error on the "patterns" option with "[{"from":"dir","info":"string"}]" value 1`] = ` 47 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 48 | - options.patterns[0] should be one of these: 49 | non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, noErrorOnMissing? } 50 | Details: 51 | * options.patterns[0].info should be one of these: 52 | object { … } | function 53 | -> Allows to add assets info. 54 | -> Read more at https://github.com/webpack/copy-webpack-plugin#info 55 | Details: 56 | * options.patterns[0].info should be an object: 57 | object { … } 58 | * options.patterns[0].info should be an instance of function." 59 | `; 60 | 61 | exports[`validate options should throw an error on the "patterns" option with "[{"from":"dir","info":true}]" value 1`] = ` 62 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 63 | - options.patterns[0] should be one of these: 64 | non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, noErrorOnMissing? } 65 | Details: 66 | * options.patterns[0].info should be one of these: 67 | object { … } | function 68 | -> Allows to add assets info. 69 | -> Read more at https://github.com/webpack/copy-webpack-plugin#info 70 | Details: 71 | * options.patterns[0].info should be an object: 72 | object { … } 73 | * options.patterns[0].info should be an instance of function." 74 | `; 75 | 76 | exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","filter":"test"}]" value 1`] = ` 77 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 78 | - options.patterns[0].filter should be an instance of function. 79 | -> Allows to filter copied assets. 80 | -> Read more at https://github.com/webpack/copy-webpack-plugin#filter" 81 | `; 82 | 83 | exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","context":"context","force":"true"}]" value 1`] = ` 84 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 85 | - options.patterns[0].force should be a boolean. 86 | -> Overwrites files already in 'compilation.assets' (usually added by other plugins/loaders). 87 | -> Read more at https://github.com/webpack/copy-webpack-plugin#force" 88 | `; 89 | 90 | exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","context":"context","toType":"foo"}]" value 1`] = ` 91 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 92 | - options.patterns[0].toType should be one of these: 93 | "dir" | "file" | "template" 94 | -> Determinate what is to option - directory, file or template. 95 | -> Read more at https://github.com/webpack/copy-webpack-plugin#totype" 96 | `; 97 | 98 | exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","context":"context","transform":{"foo":"bar"}}]" value 1`] = ` 99 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 100 | - options.patterns[0].transform has an unknown property 'foo'. These properties are valid: 101 | object { transformer?, cache? }" 102 | `; 103 | 104 | exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","context":"context","transform":true}]" value 1`] = ` 105 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 106 | - options.patterns[0] should be one of these: 107 | non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, noErrorOnMissing? } 108 | Details: 109 | * options.patterns[0].transform should be one of these: 110 | function | object { transformer?, cache? } 111 | -> Allows to modify the file contents. 112 | -> Read more at https://github.com/webpack/copy-webpack-plugin#transform 113 | Details: 114 | * options.patterns[0].transform should be an instance of function. 115 | * options.patterns[0].transform should be an object: 116 | object { transformer?, cache? }" 117 | `; 118 | 119 | exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","context":"context","transformAll":true}]" value 1`] = ` 120 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 121 | - options.patterns[0].transformAll should be an instance of function. 122 | -> Allows you to modify the contents of multiple files and save the result to one file. 123 | -> Read more at https://github.com/webpack/copy-webpack-plugin#transformall" 124 | `; 125 | 126 | exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","context":true}]" value 1`] = ` 127 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 128 | - options.patterns[0].context should be a string. 129 | -> A path that determines how to interpret the 'from' path. 130 | -> Read more at https://github.com/webpack/copy-webpack-plugin#context" 131 | `; 132 | 133 | exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","priority":"5"}]" value 1`] = ` 134 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 135 | - options.patterns[0].priority should be a number. 136 | -> Allows to specify the priority of copying files with the same destination name. 137 | -> Read more at https://github.com/webpack/copy-webpack-plugin#priority" 138 | `; 139 | 140 | exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","priority":true}]" value 1`] = ` 141 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 142 | - options.patterns[0].priority should be a number. 143 | -> Allows to specify the priority of copying files with the same destination name. 144 | -> Read more at https://github.com/webpack/copy-webpack-plugin#priority" 145 | `; 146 | 147 | exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir"}]" value 1`] = ` 148 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 149 | - options.patterns[0].priority should be a number. 150 | -> Allows to specify the priority of copying files with the same destination name. 151 | -> Read more at https://github.com/webpack/copy-webpack-plugin#priority" 152 | `; 153 | 154 | exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":true,"context":"context"}]" value 1`] = ` 155 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 156 | - options.patterns[0] should be one of these: 157 | non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, noErrorOnMissing? } 158 | Details: 159 | * options.patterns[0].to should be one of these: 160 | string | function 161 | -> Output path. 162 | -> Read more at https://github.com/webpack/copy-webpack-plugin#to 163 | Details: 164 | * options.patterns[0].to should be a string. 165 | * options.patterns[0].to should be an instance of function." 166 | `; 167 | 168 | exports[`validate options should throw an error on the "patterns" option with "[{"from":{"glob":"**/*","dot":false},"to":"dir","context":"context"}]" value 1`] = ` 169 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 170 | - options.patterns[0].from should be a non-empty string. 171 | -> Glob or path from where we copy files. 172 | -> Read more at https://github.com/webpack/copy-webpack-plugin#from" 173 | `; 174 | 175 | exports[`validate options should throw an error on the "patterns" option with "[{"from":true,"to":"dir","context":"context"}]" value 1`] = ` 176 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 177 | - options.patterns[0].from should be a non-empty string. 178 | -> Glob or path from where we copy files. 179 | -> Read more at https://github.com/webpack/copy-webpack-plugin#from" 180 | `; 181 | 182 | exports[`validate options should throw an error on the "patterns" option with "[{}]" value 1`] = ` 183 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 184 | - options.patterns[0] misses the property 'from'. Should be: 185 | non-empty string 186 | -> Glob or path from where we copy files. 187 | -> Read more at https://github.com/webpack/copy-webpack-plugin#from" 188 | `; 189 | 190 | exports[`validate options should throw an error on the "patterns" option with "{}" value 1`] = ` 191 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 192 | - options.patterns should be an array: 193 | [non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" 194 | `; 195 | 196 | exports[`validate options should throw an error on the "patterns" option with "true" value 1`] = ` 197 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 198 | - options.patterns should be an array: 199 | [non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" 200 | `; 201 | 202 | exports[`validate options should throw an error on the "patterns" option with "true" value 2`] = ` 203 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 204 | - options.patterns should be an array: 205 | [non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" 206 | `; 207 | 208 | exports[`validate options should throw an error on the "patterns" option with "undefined" value 1`] = ` 209 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 210 | - options misses the property 'patterns'. Should be: 211 | [non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" 212 | `; 213 | 214 | exports[`validate options should throw an error on the "unknown" option with "/test/" value 1`] = ` 215 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 216 | - options has an unknown property 'unknown'. These properties are valid: 217 | object { patterns, options? }" 218 | `; 219 | 220 | exports[`validate options should throw an error on the "unknown" option with "[]" value 1`] = ` 221 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 222 | - options has an unknown property 'unknown'. These properties are valid: 223 | object { patterns, options? }" 224 | `; 225 | 226 | exports[`validate options should throw an error on the "unknown" option with "{"foo":"bar"}" value 1`] = ` 227 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 228 | - options has an unknown property 'unknown'. These properties are valid: 229 | object { patterns, options? }" 230 | `; 231 | 232 | exports[`validate options should throw an error on the "unknown" option with "{}" value 1`] = ` 233 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 234 | - options has an unknown property 'unknown'. These properties are valid: 235 | object { patterns, options? }" 236 | `; 237 | 238 | exports[`validate options should throw an error on the "unknown" option with "1" value 1`] = ` 239 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 240 | - options has an unknown property 'unknown'. These properties are valid: 241 | object { patterns, options? }" 242 | `; 243 | 244 | exports[`validate options should throw an error on the "unknown" option with "false" value 1`] = ` 245 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 246 | - options has an unknown property 'unknown'. These properties are valid: 247 | object { patterns, options? }" 248 | `; 249 | 250 | exports[`validate options should throw an error on the "unknown" option with "test" value 1`] = ` 251 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 252 | - options has an unknown property 'unknown'. These properties are valid: 253 | object { patterns, options? }" 254 | `; 255 | 256 | exports[`validate options should throw an error on the "unknown" option with "true" value 1`] = ` 257 | "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. 258 | - options has an unknown property 'unknown'. These properties are valid: 259 | object { patterns, options? }" 260 | `; 261 | -------------------------------------------------------------------------------- /test/__snapshots__/CopyPlugin.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing 2 | 3 | exports[`CopyPlugin cache should work with the "filesystem" cache and multi compiler mode: assets 1`] = ` 4 | { 5 | ".dottedfile": "dottedfile contents 6 | ", 7 | "directoryfile.txt": "new", 8 | "nested/deep-nested/deepnested.txt": "", 9 | "nested/nestedfile.txt": "", 10 | } 11 | `; 12 | 13 | exports[`CopyPlugin cache should work with the "filesystem" cache and multi compiler mode: assets 2`] = ` 14 | { 15 | ".dottedfile": "dottedfile contents 16 | ", 17 | "directoryfile.txt": "new", 18 | "nested/deep-nested/deepnested.txt": "", 19 | "nested/nestedfile.txt": "", 20 | } 21 | `; 22 | 23 | exports[`CopyPlugin cache should work with the "filesystem" cache and multi compiler mode: assets 3`] = ` 24 | { 25 | ".dottedfile": "dottedfile contents 26 | ", 27 | "directoryfile.txt": "new", 28 | "nested/deep-nested/deepnested.txt": "", 29 | "nested/nestedfile.txt": "", 30 | } 31 | `; 32 | 33 | exports[`CopyPlugin cache should work with the "filesystem" cache and multi compiler mode: assets 4`] = ` 34 | { 35 | ".dottedfile": "dottedfile contents 36 | ", 37 | "directoryfile.txt": "new", 38 | "nested/deep-nested/deepnested.txt": "", 39 | "nested/nestedfile.txt": "", 40 | } 41 | `; 42 | 43 | exports[`CopyPlugin cache should work with the "filesystem" cache and multi compiler mode: errors 1`] = `[]`; 44 | 45 | exports[`CopyPlugin cache should work with the "filesystem" cache and multi compiler mode: errors 2`] = `[]`; 46 | 47 | exports[`CopyPlugin cache should work with the "filesystem" cache and multi compiler mode: errors 3`] = `[]`; 48 | 49 | exports[`CopyPlugin cache should work with the "filesystem" cache and multi compiler mode: errors 4`] = `[]`; 50 | 51 | exports[`CopyPlugin cache should work with the "filesystem" cache and multi compiler mode: warnings 1`] = `[]`; 52 | 53 | exports[`CopyPlugin cache should work with the "filesystem" cache and multi compiler mode: warnings 2`] = `[]`; 54 | 55 | exports[`CopyPlugin cache should work with the "filesystem" cache and multi compiler mode: warnings 3`] = `[]`; 56 | 57 | exports[`CopyPlugin cache should work with the "filesystem" cache and multi compiler mode: warnings 4`] = `[]`; 58 | 59 | exports[`CopyPlugin cache should work with the "filesystem" cache: assets 1`] = ` 60 | { 61 | ".dottedfile": "dottedfile contents 62 | ", 63 | "directoryfile.txt": "new", 64 | "nested/deep-nested/deepnested.txt": "", 65 | "nested/nestedfile.txt": "", 66 | } 67 | `; 68 | 69 | exports[`CopyPlugin cache should work with the "filesystem" cache: assets 2`] = ` 70 | { 71 | ".dottedfile": "dottedfile contents 72 | ", 73 | "directoryfile.txt": "new", 74 | "nested/deep-nested/deepnested.txt": "", 75 | "nested/nestedfile.txt": "", 76 | } 77 | `; 78 | 79 | exports[`CopyPlugin cache should work with the "filesystem" cache: errors 1`] = `[]`; 80 | 81 | exports[`CopyPlugin cache should work with the "filesystem" cache: errors 2`] = `[]`; 82 | 83 | exports[`CopyPlugin cache should work with the "filesystem" cache: warnings 1`] = `[]`; 84 | 85 | exports[`CopyPlugin cache should work with the "filesystem" cache: warnings 2`] = `[]`; 86 | 87 | exports[`CopyPlugin cache should work with the "memory" cache: assets 1`] = ` 88 | { 89 | ".dottedfile": "dottedfile contents 90 | ", 91 | "directoryfile.txt": "new", 92 | "nested/deep-nested/deepnested.txt": "", 93 | "nested/nestedfile.txt": "", 94 | } 95 | `; 96 | 97 | exports[`CopyPlugin cache should work with the "memory" cache: assets 2`] = ` 98 | { 99 | ".dottedfile": "dottedfile contents 100 | ", 101 | "directoryfile.txt": "new", 102 | "nested/deep-nested/deepnested.txt": "", 103 | "nested/nestedfile.txt": "", 104 | } 105 | `; 106 | 107 | exports[`CopyPlugin cache should work with the "memory" cache: errors 1`] = `[]`; 108 | 109 | exports[`CopyPlugin cache should work with the "memory" cache: errors 2`] = `[]`; 110 | 111 | exports[`CopyPlugin cache should work with the "memory" cache: warnings 1`] = `[]`; 112 | 113 | exports[`CopyPlugin cache should work with the "memory" cache: warnings 2`] = `[]`; 114 | 115 | exports[`CopyPlugin cache should work with the "transform" option: assets 1`] = ` 116 | { 117 | "new0.txt": "new", 118 | "new1-2.txt": "newadded1", 119 | "new1.txt": "newadded1", 120 | "new2.txt": "newadded2", 121 | "new3.txt": "newadded3", 122 | "new4.txt": "newbaz", 123 | "new5.txt": "newbaz", 124 | "new6.txt": "newbaz", 125 | } 126 | `; 127 | 128 | exports[`CopyPlugin cache should work with the "transform" option: assets 2`] = ` 129 | { 130 | "new0.txt": "new", 131 | "new1-2.txt": "newadded1", 132 | "new1.txt": "newadded1", 133 | "new2.txt": "newadded2", 134 | "new3.txt": "newadded3", 135 | "new4.txt": "newbaz", 136 | "new5.txt": "newbaz", 137 | "new6.txt": "newbaz", 138 | } 139 | `; 140 | 141 | exports[`CopyPlugin cache should work with the "transform" option: errors 1`] = `[]`; 142 | 143 | exports[`CopyPlugin cache should work with the "transform" option: errors 2`] = `[]`; 144 | 145 | exports[`CopyPlugin cache should work with the "transform" option: warnings 1`] = `[]`; 146 | 147 | exports[`CopyPlugin cache should work with the "transform" option: warnings 2`] = `[]`; 148 | 149 | exports[`CopyPlugin logging should logging when "from" is a directory: logs 1`] = ` 150 | { 151 | "logs": [ 152 | "'to' option '.' determinated as 'dir'", 153 | "'to' option '.' determinated as 'dir'", 154 | "'to' option '.' determinated as 'dir'", 155 | "'to' option '.' determinated as 'dir'", 156 | "added './fixtures/directory' as a context dependency", 157 | "added './fixtures/directory/.dottedfile' as a file dependency", 158 | "added './fixtures/directory/directoryfile.txt' as a file dependency", 159 | "added './fixtures/directory/nested/deep-nested/deepnested.txt' as a file dependency", 160 | "added './fixtures/directory/nested/nestedfile.txt' as a file dependency", 161 | "begin globbing './fixtures/directory/**/*'...", 162 | "created snapshot for './fixtures/directory/.dottedfile'", 163 | "created snapshot for './fixtures/directory/directoryfile.txt'", 164 | "created snapshot for './fixtures/directory/nested/deep-nested/deepnested.txt'", 165 | "created snapshot for './fixtures/directory/nested/nestedfile.txt'", 166 | "creating snapshot for './fixtures/directory/.dottedfile'...", 167 | "creating snapshot for './fixtures/directory/directoryfile.txt'...", 168 | "creating snapshot for './fixtures/directory/nested/deep-nested/deepnested.txt'...", 169 | "creating snapshot for './fixtures/directory/nested/nestedfile.txt'...", 170 | "determined './fixtures/directory' is a directory", 171 | "determined that './fixtures/directory/.dottedfile' should write to '.dottedfile'", 172 | "determined that './fixtures/directory/directoryfile.txt' should write to 'directoryfile.txt'", 173 | "determined that './fixtures/directory/nested/deep-nested/deepnested.txt' should write to 'nested/deep-nested/deepnested.txt'", 174 | "determined that './fixtures/directory/nested/nestedfile.txt' should write to 'nested/nestedfile.txt'", 175 | "finished to adding additional assets", 176 | "finished to process a pattern from 'directory' using './fixtures/directory' context", 177 | "found './fixtures/directory/.dottedfile'", 178 | "found './fixtures/directory/directoryfile.txt'", 179 | "found './fixtures/directory/nested/deep-nested/deepnested.txt'", 180 | "found './fixtures/directory/nested/nestedfile.txt'", 181 | "getting cache for './fixtures/directory/.dottedfile'...", 182 | "getting cache for './fixtures/directory/directoryfile.txt'...", 183 | "getting cache for './fixtures/directory/nested/deep-nested/deepnested.txt'...", 184 | "getting cache for './fixtures/directory/nested/nestedfile.txt'...", 185 | "getting stats for './fixtures/directory'...", 186 | "missed cache for './fixtures/directory/.dottedfile'", 187 | "missed cache for './fixtures/directory/directoryfile.txt'", 188 | "missed cache for './fixtures/directory/nested/deep-nested/deepnested.txt'", 189 | "missed cache for './fixtures/directory/nested/nestedfile.txt'", 190 | "read './fixtures/directory/.dottedfile'", 191 | "read './fixtures/directory/directoryfile.txt'", 192 | "read './fixtures/directory/nested/deep-nested/deepnested.txt'", 193 | "read './fixtures/directory/nested/nestedfile.txt'", 194 | "reading './fixtures/directory/.dottedfile'...", 195 | "reading './fixtures/directory/directoryfile.txt'...", 196 | "reading './fixtures/directory/nested/deep-nested/deepnested.txt'...", 197 | "reading './fixtures/directory/nested/nestedfile.txt'...", 198 | "starting to add additional assets...", 199 | "starting to process a pattern from 'directory' using './fixtures' context", 200 | "stored cache for './fixtures/directory/.dottedfile'", 201 | "stored cache for './fixtures/directory/directoryfile.txt'", 202 | "stored cache for './fixtures/directory/nested/deep-nested/deepnested.txt'", 203 | "stored cache for './fixtures/directory/nested/nestedfile.txt'", 204 | "storing cache for './fixtures/directory/.dottedfile'...", 205 | "storing cache for './fixtures/directory/directoryfile.txt'...", 206 | "storing cache for './fixtures/directory/nested/deep-nested/deepnested.txt'...", 207 | "storing cache for './fixtures/directory/nested/nestedfile.txt'...", 208 | "writing '.dottedfile' from './fixtures/directory/.dottedfile' to compilation assets...", 209 | "writing 'directoryfile.txt' from './fixtures/directory/directoryfile.txt' to compilation assets...", 210 | "writing 'nested/deep-nested/deepnested.txt' from './fixtures/directory/nested/deep-nested/deepnested.txt' to compilation assets...", 211 | "writing 'nested/nestedfile.txt' from './fixtures/directory/nested/nestedfile.txt' to compilation assets...", 212 | "written '.dottedfile' from './fixtures/directory/.dottedfile' to compilation assets", 213 | "written 'directoryfile.txt' from './fixtures/directory/directoryfile.txt' to compilation assets", 214 | "written 'nested/deep-nested/deepnested.txt' from './fixtures/directory/nested/deep-nested/deepnested.txt' to compilation assets", 215 | "written 'nested/nestedfile.txt' from './fixtures/directory/nested/nestedfile.txt' to compilation assets", 216 | ], 217 | } 218 | `; 219 | 220 | exports[`CopyPlugin logging should logging when "from" is a file: logs 1`] = ` 221 | { 222 | "logs": [ 223 | "'to' option '.' determinated as 'dir'", 224 | "added './fixtures/file.txt' as a file dependency", 225 | "begin globbing './fixtures/file.txt'...", 226 | "created snapshot for './fixtures/file.txt'", 227 | "creating snapshot for './fixtures/file.txt'...", 228 | "determined './fixtures/file.txt' is a file", 229 | "determined that './fixtures/file.txt' should write to 'file.txt'", 230 | "finished to adding additional assets", 231 | "finished to process a pattern from 'file.txt' using './fixtures' context", 232 | "found './fixtures/file.txt'", 233 | "getting cache for './fixtures/file.txt'...", 234 | "getting stats for './fixtures/file.txt'...", 235 | "missed cache for './fixtures/file.txt'", 236 | "read './fixtures/file.txt'", 237 | "reading './fixtures/file.txt'...", 238 | "starting to add additional assets...", 239 | "starting to process a pattern from 'file.txt' using './fixtures' context", 240 | "stored cache for './fixtures/file.txt'", 241 | "storing cache for './fixtures/file.txt'...", 242 | "writing 'file.txt' from './fixtures/file.txt' to compilation assets...", 243 | "written 'file.txt' from './fixtures/file.txt' to compilation assets", 244 | ], 245 | } 246 | `; 247 | 248 | exports[`CopyPlugin logging should logging when "from" is a glob: logs 1`] = ` 249 | { 250 | "logs": [ 251 | "'to' option '.' determinated as 'dir'", 252 | "'to' option '.' determinated as 'dir'", 253 | "'to' option '.' determinated as 'dir'", 254 | "added './fixtures/directory' as a context dependency", 255 | "added './fixtures/directory/directoryfile.txt' as a file dependency", 256 | "added './fixtures/directory/nested/deep-nested/deepnested.txt' as a file dependency", 257 | "added './fixtures/directory/nested/nestedfile.txt' as a file dependency", 258 | "begin globbing './fixtures/directory/**'...", 259 | "created snapshot for './fixtures/directory/directoryfile.txt'", 260 | "created snapshot for './fixtures/directory/nested/deep-nested/deepnested.txt'", 261 | "created snapshot for './fixtures/directory/nested/nestedfile.txt'", 262 | "creating snapshot for './fixtures/directory/directoryfile.txt'...", 263 | "creating snapshot for './fixtures/directory/nested/deep-nested/deepnested.txt'...", 264 | "creating snapshot for './fixtures/directory/nested/nestedfile.txt'...", 265 | "determined './fixtures/directory/**' is a glob", 266 | "determined that './fixtures/directory/directoryfile.txt' should write to 'directory/directoryfile.txt'", 267 | "determined that './fixtures/directory/nested/deep-nested/deepnested.txt' should write to 'directory/nested/deep-nested/deepnested.txt'", 268 | "determined that './fixtures/directory/nested/nestedfile.txt' should write to 'directory/nested/nestedfile.txt'", 269 | "finished to adding additional assets", 270 | "finished to process a pattern from 'directory/**' using './fixtures' context", 271 | "found './fixtures/directory/directoryfile.txt'", 272 | "found './fixtures/directory/nested/deep-nested/deepnested.txt'", 273 | "found './fixtures/directory/nested/nestedfile.txt'", 274 | "getting cache for './fixtures/directory/directoryfile.txt'...", 275 | "getting cache for './fixtures/directory/nested/deep-nested/deepnested.txt'...", 276 | "getting cache for './fixtures/directory/nested/nestedfile.txt'...", 277 | "getting stats for './fixtures/directory/**'...", 278 | "missed cache for './fixtures/directory/directoryfile.txt'", 279 | "missed cache for './fixtures/directory/nested/deep-nested/deepnested.txt'", 280 | "missed cache for './fixtures/directory/nested/nestedfile.txt'", 281 | "read './fixtures/directory/directoryfile.txt'", 282 | "read './fixtures/directory/nested/deep-nested/deepnested.txt'", 283 | "read './fixtures/directory/nested/nestedfile.txt'", 284 | "reading './fixtures/directory/directoryfile.txt'...", 285 | "reading './fixtures/directory/nested/deep-nested/deepnested.txt'...", 286 | "reading './fixtures/directory/nested/nestedfile.txt'...", 287 | "starting to add additional assets...", 288 | "starting to process a pattern from 'directory/**' using './fixtures' context", 289 | "stored cache for './fixtures/directory/directoryfile.txt'", 290 | "stored cache for './fixtures/directory/nested/deep-nested/deepnested.txt'", 291 | "stored cache for './fixtures/directory/nested/nestedfile.txt'", 292 | "storing cache for './fixtures/directory/directoryfile.txt'...", 293 | "storing cache for './fixtures/directory/nested/deep-nested/deepnested.txt'...", 294 | "storing cache for './fixtures/directory/nested/nestedfile.txt'...", 295 | "writing 'directory/directoryfile.txt' from './fixtures/directory/directoryfile.txt' to compilation assets...", 296 | "writing 'directory/nested/deep-nested/deepnested.txt' from './fixtures/directory/nested/deep-nested/deepnested.txt' to compilation assets...", 297 | "writing 'directory/nested/nestedfile.txt' from './fixtures/directory/nested/nestedfile.txt' to compilation assets...", 298 | "written 'directory/directoryfile.txt' from './fixtures/directory/directoryfile.txt' to compilation assets", 299 | "written 'directory/nested/deep-nested/deepnested.txt' from './fixtures/directory/nested/deep-nested/deepnested.txt' to compilation assets", 300 | "written 'directory/nested/nestedfile.txt' from './fixtures/directory/nested/nestedfile.txt' to compilation assets", 301 | ], 302 | } 303 | `; 304 | 305 | exports[`CopyPlugin logging should logging when 'to' is a function: logs 1`] = ` 306 | { 307 | "logs": [ 308 | "'to' option 'newFile.txt' determinated as 'file'", 309 | "added './fixtures/file.txt' as a file dependency", 310 | "begin globbing './fixtures/file.txt'...", 311 | "created snapshot for './fixtures/file.txt'", 312 | "creating snapshot for './fixtures/file.txt'...", 313 | "determined './fixtures/file.txt' is a file", 314 | "determined that './fixtures/file.txt' should write to 'newFile.txt'", 315 | "finished to adding additional assets", 316 | "finished to process a pattern from 'file.txt' using './fixtures' context", 317 | "found './fixtures/file.txt'", 318 | "getting cache for './fixtures/file.txt'...", 319 | "getting stats for './fixtures/file.txt'...", 320 | "missed cache for './fixtures/file.txt'", 321 | "read './fixtures/file.txt'", 322 | "reading './fixtures/file.txt'...", 323 | "starting to add additional assets...", 324 | "starting to process a pattern from 'file.txt' using './fixtures' context", 325 | "stored cache for './fixtures/file.txt'", 326 | "storing cache for './fixtures/file.txt'...", 327 | "writing 'newFile.txt' from './fixtures/file.txt' to compilation assets...", 328 | "written 'newFile.txt' from './fixtures/file.txt' to compilation assets", 329 | ], 330 | } 331 | `; 332 | 333 | exports[`CopyPlugin should work with multi compiler mode: assets 1`] = ` 334 | { 335 | ".dottedfile": "dottedfile contents 336 | ", 337 | "directoryfile.txt": "new", 338 | "nested/deep-nested/deepnested.txt": "", 339 | "nested/nestedfile.txt": "", 340 | } 341 | `; 342 | 343 | exports[`CopyPlugin should work with multi compiler mode: assets 2`] = ` 344 | { 345 | ".dottedfile": "dottedfile contents 346 | ", 347 | "directoryfile.txt": "new", 348 | "nested/deep-nested/deepnested.txt": "", 349 | "nested/nestedfile.txt": "", 350 | } 351 | `; 352 | 353 | exports[`CopyPlugin should work with multi compiler mode: errors 1`] = `[]`; 354 | 355 | exports[`CopyPlugin should work with multi compiler mode: errors 2`] = `[]`; 356 | 357 | exports[`CopyPlugin should work with multi compiler mode: warnings 1`] = `[]`; 358 | 359 | exports[`CopyPlugin should work with multi compiler mode: warnings 2`] = `[]`; 360 | 361 | exports[`CopyPlugin stats should work have assets info: assets 1`] = ` 362 | { 363 | ".dottedfile": "dottedfile contents 364 | ", 365 | "asset-modules/deepnested.txt": "", 366 | "directoryfile.txt": "new", 367 | "nested/deep-nested/deepnested.txt": "", 368 | "nested/nestedfile.txt": "", 369 | } 370 | `; 371 | 372 | exports[`CopyPlugin stats should work have assets info: assets info 1`] = ` 373 | [ 374 | { 375 | "info": { 376 | "copied": true, 377 | "immutable": undefined, 378 | "sourceFilename": "directory/.dottedfile", 379 | }, 380 | "name": ".dottedfile", 381 | }, 382 | { 383 | "info": { 384 | "copied": undefined, 385 | "immutable": undefined, 386 | "sourceFilename": "directory/nested/deep-nested/deepnested.txt", 387 | }, 388 | "name": "asset-modules/deepnested.txt", 389 | }, 390 | { 391 | "info": { 392 | "copied": true, 393 | "immutable": undefined, 394 | "sourceFilename": "directory/directoryfile.txt", 395 | }, 396 | "name": "directoryfile.txt", 397 | }, 398 | { 399 | "info": { 400 | "copied": undefined, 401 | "immutable": undefined, 402 | "sourceFilename": undefined, 403 | }, 404 | "name": "main.js", 405 | }, 406 | { 407 | "info": { 408 | "copied": true, 409 | "immutable": undefined, 410 | "sourceFilename": "directory/nested/deep-nested/deepnested.txt", 411 | }, 412 | "name": "nested/deep-nested/deepnested.txt", 413 | }, 414 | { 415 | "info": { 416 | "copied": true, 417 | "immutable": undefined, 418 | "sourceFilename": "directory/nested/nestedfile.txt", 419 | }, 420 | "name": "nested/nestedfile.txt", 421 | }, 422 | ] 423 | `; 424 | 425 | exports[`CopyPlugin stats should work have assets info: errors 1`] = `[]`; 426 | 427 | exports[`CopyPlugin stats should work have assets info: warnings 1`] = `[]`; 428 | -------------------------------------------------------------------------------- /test/to-option.test.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | import { getCompiler } from "./helpers"; 4 | import { runEmit } from "./helpers/run"; 5 | 6 | const BUILD_DIR = path.join(__dirname, "build"); 7 | const TEMP_DIR = path.join(__dirname, "tempdir"); 8 | const FIXTURES_DIR = path.join(__dirname, "fixtures"); 9 | 10 | describe("to option", () => { 11 | describe("is a file", () => { 12 | it("should copy a file to a new file", (done) => { 13 | runEmit({ 14 | expectedAssetKeys: ["newfile.txt"], 15 | patterns: [ 16 | { 17 | from: "file.txt", 18 | to: "newfile.txt", 19 | }, 20 | ], 21 | }) 22 | .then(() => { 23 | // runEmit performs assertions internally, no need to check result 24 | done(); 25 | }) 26 | .catch(done); 27 | }); 28 | 29 | it('should copy a file to a new file when "to" is absolute path', (done) => { 30 | runEmit({ 31 | expectedAssetKeys: ["../tempdir/newfile.txt"], 32 | patterns: [ 33 | { 34 | from: "file.txt", 35 | to: path.join(TEMP_DIR, "newfile.txt"), 36 | }, 37 | ], 38 | }) 39 | .then(done) 40 | .catch(done); 41 | }); 42 | 43 | it("should copy a file to a new file inside nested directory", (done) => { 44 | runEmit({ 45 | expectedAssetKeys: ["newdirectory/newfile.txt"], 46 | patterns: [ 47 | { 48 | from: "file.txt", 49 | to: "newdirectory/newfile.txt", 50 | }, 51 | ], 52 | }) 53 | .then(done) 54 | .catch(done); 55 | }); 56 | 57 | it('should copy a file to a new file inside nested directory when "to" an absolute path', (done) => { 58 | runEmit({ 59 | expectedAssetKeys: ["newdirectory/newfile.txt"], 60 | patterns: [ 61 | { 62 | from: "file.txt", 63 | to: path.join(BUILD_DIR, "newdirectory/newfile.txt"), 64 | }, 65 | ], 66 | }) 67 | .then(done) 68 | .catch(done); 69 | }); 70 | 71 | it("should copy a file to a new file inside other directory what out of context", (done) => { 72 | runEmit({ 73 | expectedAssetKeys: ["../tempdir/newdirectory/newfile.txt"], 74 | patterns: [ 75 | { 76 | from: "file.txt", 77 | to: path.join(TEMP_DIR, "newdirectory/newfile.txt"), 78 | }, 79 | ], 80 | }) 81 | .then(done) 82 | .catch(done); 83 | }); 84 | 85 | it("should copy a file using invalid template syntax", (done) => { 86 | runEmit({ 87 | expectedAssetKeys: ["directory/[md5::base64:20].txt"], 88 | patterns: [ 89 | { 90 | from: "directory/directoryfile.txt", 91 | to: "directory/[md5::base64:20].txt", 92 | }, 93 | ], 94 | }) 95 | .then(done) 96 | .catch(done); 97 | }); 98 | }); 99 | 100 | describe("is a directory", () => { 101 | it("should copy a file to a new directory", (done) => { 102 | runEmit({ 103 | expectedAssetKeys: ["newdirectory/file.txt"], 104 | patterns: [ 105 | { 106 | from: "file.txt", 107 | to: "newdirectory", 108 | }, 109 | ], 110 | }) 111 | .then(done) 112 | .catch(done); 113 | }); 114 | 115 | it("should copy a file to a new directory out of context", (done) => { 116 | runEmit({ 117 | expectedAssetKeys: ["../tempdir/file.txt"], 118 | patterns: [ 119 | { 120 | from: "file.txt", 121 | to: TEMP_DIR, 122 | }, 123 | ], 124 | }) 125 | .then(done) 126 | .catch(done); 127 | }); 128 | 129 | it("should copy a file to a new directory with a forward slash", (done) => { 130 | runEmit({ 131 | expectedAssetKeys: ["newdirectory/file.txt"], 132 | patterns: [ 133 | { 134 | from: "file.txt", 135 | to: "newdirectory/", 136 | }, 137 | ], 138 | }) 139 | .then(done) 140 | .catch(done); 141 | }); 142 | 143 | it("should copy a file to a new directory with an extension and path separator at end", (done) => { 144 | runEmit({ 145 | expectedAssetKeys: ["newdirectory.ext/file.txt"], 146 | patterns: [ 147 | { 148 | from: "file.txt", 149 | to: `newdirectory.ext${path.sep}`, 150 | }, 151 | ], 152 | }) 153 | .then(done) 154 | .catch(done); 155 | }); 156 | 157 | it('should copy a file to a new directory when "to" is absolute path', (done) => { 158 | runEmit({ 159 | expectedAssetKeys: ["file.txt"], 160 | patterns: [ 161 | { 162 | from: "file.txt", 163 | to: BUILD_DIR, 164 | }, 165 | ], 166 | }) 167 | .then(done) 168 | .catch(done); 169 | }); 170 | 171 | it('should copy a file to a new directory when "to" is absolute path with a forward slash', (done) => { 172 | runEmit({ 173 | expectedAssetKeys: ["file.txt"], 174 | patterns: [ 175 | { 176 | from: "file.txt", 177 | to: `${BUILD_DIR}/`, 178 | }, 179 | ], 180 | }) 181 | .then(done) 182 | .catch(done); 183 | }); 184 | 185 | it("should copy a file to a new directory from nested directory", (done) => { 186 | runEmit({ 187 | expectedAssetKeys: ["newdirectory/directoryfile.txt"], 188 | patterns: [ 189 | { 190 | from: "directory/directoryfile.txt", 191 | to: "newdirectory", 192 | }, 193 | ], 194 | }) 195 | .then(done) 196 | .catch(done); 197 | }); 198 | 199 | it('should copy a file to a new directory from nested directory when "from" is absolute path', (done) => { 200 | runEmit({ 201 | expectedAssetKeys: ["newdirectory/directoryfile.txt"], 202 | patterns: [ 203 | { 204 | from: path.join(FIXTURES_DIR, "directory", "directoryfile.txt"), 205 | to: "newdirectory", 206 | }, 207 | ], 208 | }) 209 | .then(done) 210 | .catch(done); 211 | }); 212 | 213 | it('should copy a file to a new directory from nested directory when "from" is absolute path with a forward slash', (done) => { 214 | runEmit({ 215 | expectedAssetKeys: ["newdirectory/directoryfile.txt"], 216 | patterns: [ 217 | { 218 | from: path.join(FIXTURES_DIR, "directory", "directoryfile.txt"), 219 | to: "newdirectory/", 220 | }, 221 | ], 222 | }) 223 | .then(done) 224 | .catch(done); 225 | }); 226 | 227 | it("should copy files to a new directory", (done) => { 228 | runEmit({ 229 | expectedAssetKeys: [ 230 | "newdirectory/.dottedfile", 231 | "newdirectory/directoryfile.txt", 232 | "newdirectory/nested/deep-nested/deepnested.txt", 233 | "newdirectory/nested/nestedfile.txt", 234 | ], 235 | patterns: [ 236 | { 237 | from: "directory", 238 | to: "newdirectory", 239 | }, 240 | ], 241 | }) 242 | .then(done) 243 | .catch(done); 244 | }); 245 | 246 | it("should copy files to a new nested directory", (done) => { 247 | runEmit({ 248 | expectedAssetKeys: [ 249 | "newdirectory/deep-nested/deepnested.txt", 250 | "newdirectory/nestedfile.txt", 251 | ], 252 | patterns: [ 253 | { 254 | from: path.join(FIXTURES_DIR, "directory", "nested"), 255 | to: "newdirectory", 256 | }, 257 | ], 258 | }) 259 | .then(done) 260 | .catch(done); 261 | }); 262 | 263 | it("should copy files to a new directory out of context", (done) => { 264 | runEmit({ 265 | expectedAssetKeys: [ 266 | "../tempdir/.dottedfile", 267 | "../tempdir/directoryfile.txt", 268 | "../tempdir/nested/deep-nested/deepnested.txt", 269 | "../tempdir/nested/nestedfile.txt", 270 | ], 271 | patterns: [ 272 | { 273 | from: "directory", 274 | to: TEMP_DIR, 275 | }, 276 | ], 277 | }) 278 | .then(done) 279 | .catch(done); 280 | }); 281 | 282 | it('should copy files to a new directory when "to" is absolute path', (done) => { 283 | runEmit({ 284 | expectedAssetKeys: [ 285 | ".dottedfile", 286 | "directoryfile.txt", 287 | "nested/deep-nested/deepnested.txt", 288 | "nested/nestedfile.txt", 289 | ], 290 | patterns: [ 291 | { 292 | from: "directory", 293 | to: BUILD_DIR, 294 | }, 295 | ], 296 | }) 297 | .then(done) 298 | .catch(done); 299 | }); 300 | 301 | it('should copy files to a new directory when "to" is absolute path with a forward slash', (done) => { 302 | runEmit({ 303 | expectedAssetKeys: [ 304 | ".dottedfile", 305 | "directoryfile.txt", 306 | "nested/deep-nested/deepnested.txt", 307 | "nested/nestedfile.txt", 308 | ], 309 | patterns: [ 310 | { 311 | from: "directory", 312 | to: `${BUILD_DIR}/`, 313 | }, 314 | ], 315 | }) 316 | .then(done) 317 | .catch(done); 318 | }); 319 | 320 | it("should copy files to a new directory from nested directory", (done) => { 321 | runEmit({ 322 | expectedAssetKeys: [ 323 | "newdirectory/deep-nested/deepnested.txt", 324 | "newdirectory/nestedfile.txt", 325 | ], 326 | patterns: [ 327 | { 328 | from: "directory/nested", 329 | to: "newdirectory", 330 | }, 331 | ], 332 | }) 333 | .then(done) 334 | .catch(done); 335 | }); 336 | 337 | it('should copy a file to a new directory when "to" is empty', (done) => { 338 | runEmit({ 339 | expectedAssetKeys: ["file.txt"], 340 | patterns: [ 341 | { 342 | from: "file.txt", 343 | to: "", 344 | }, 345 | ], 346 | }) 347 | .then(done) 348 | .catch(done); 349 | }); 350 | }); 351 | 352 | describe("is a template", () => { 353 | it('should copy a file using "contenthash"', (done) => { 354 | const compiler = getCompiler({ 355 | output: { 356 | hashDigestLength: 6, 357 | }, 358 | }); 359 | 360 | runEmit({ 361 | compiler, 362 | expectedAssetKeys: ["directory/5d7817.txt"], 363 | patterns: [ 364 | { 365 | from: "directory/directoryfile.txt", 366 | to: "directory/[contenthash].txt", 367 | }, 368 | ], 369 | }) 370 | .then(done) 371 | .catch(done); 372 | }); 373 | 374 | it("should copy a file using custom `contenthash` digest", (done) => { 375 | const compiler = getCompiler({ 376 | output: { 377 | hashFunction: "sha1", 378 | hashDigest: "hex", 379 | hashDigestLength: 4, 380 | }, 381 | }); 382 | 383 | runEmit({ 384 | expectedAssetKeys: ["directory/c2a6.txt"], 385 | patterns: [ 386 | { 387 | from: "directory/directoryfile.txt", 388 | to: "directory/[contenthash].txt", 389 | }, 390 | ], 391 | compiler, 392 | }) 393 | .then(done) 394 | .catch(done); 395 | }); 396 | 397 | it("should copy a file using `contenthash` with hashSalt", (done) => { 398 | const compiler = getCompiler({ 399 | output: { 400 | hashSalt: "qwerty", 401 | }, 402 | }); 403 | 404 | runEmit({ 405 | expectedAssetKeys: ["directory/64cc145fc382934bd97a.txt"], 406 | patterns: [ 407 | { 408 | from: "directory/directoryfile.txt", 409 | to: "directory/[contenthash].txt", 410 | }, 411 | ], 412 | compiler, 413 | }) 414 | .then(done) 415 | .catch(done); 416 | }); 417 | 418 | it('should copy a file using "name" and "ext"', (done) => { 419 | runEmit({ 420 | expectedAssetKeys: ["binextension.bin"], 421 | patterns: [ 422 | { 423 | from: "binextension.bin", 424 | to: "[name][ext]", 425 | }, 426 | ], 427 | }) 428 | .then(done) 429 | .catch(done); 430 | }); 431 | 432 | it('should copy a file using "name", "contenthash" and "ext"', (done) => { 433 | runEmit({ 434 | expectedAssetKeys: ["file-5d7817.txt"], 435 | patterns: [ 436 | { 437 | from: "file.txt", 438 | to: "[name]-[contenthash:6][ext]", 439 | }, 440 | ], 441 | }) 442 | .then(done) 443 | .catch(done); 444 | }); 445 | 446 | it("should copy a file from nested directory", (done) => { 447 | runEmit({ 448 | expectedAssetKeys: ["directoryfile-5d7817.txt"], 449 | patterns: [ 450 | { 451 | from: "directory/directoryfile.txt", 452 | to: "[name]-[contenthash:6][ext]", 453 | }, 454 | ], 455 | }) 456 | .then(done) 457 | .catch(done); 458 | }); 459 | 460 | it("should copy a file from nested directory to new directory", (done) => { 461 | runEmit({ 462 | expectedAssetKeys: ["newdirectory/directoryfile-5d7817.txt"], 463 | patterns: [ 464 | { 465 | from: "directory/directoryfile.txt", 466 | to: "newdirectory/[name]-[contenthash:6][ext]", 467 | }, 468 | ], 469 | }) 470 | .then(done) 471 | .catch(done); 472 | }); 473 | 474 | it('should copy a file without an extension using "name", "ext"', (done) => { 475 | runEmit({ 476 | expectedAssetKeys: ["noextension.31d6cf.newext"], 477 | patterns: [ 478 | { 479 | from: "noextension", 480 | to: "[name][ext].[contenthash:6].newext", 481 | }, 482 | ], 483 | }) 484 | .then(done) 485 | .catch(done); 486 | }); 487 | 488 | it('should copy files using "path", "name", "contenthash" and "ext"', (done) => { 489 | runEmit({ 490 | expectedAssetKeys: [ 491 | "newdirectory/.dottedfile-5e294e", 492 | "newdirectory/directoryfile-5d7817.txt", 493 | "newdirectory/nested/deep-nested/deepnested-31d6cf.txt", 494 | "newdirectory/nested/nestedfile-31d6cf.txt", 495 | ], 496 | patterns: [ 497 | { 498 | from: "directory", 499 | to: "newdirectory/[path][name]-[contenthash:6][ext]", 500 | }, 501 | ], 502 | }) 503 | .then(done) 504 | .catch(done); 505 | }); 506 | 507 | it('should copy a file to "compiler.options.output" by default', (done) => { 508 | runEmit({ 509 | compilation: { output: { path: "/path/to" } }, 510 | expectedAssetKeys: ["newfile.txt"], 511 | patterns: [ 512 | { 513 | from: "file.txt", 514 | to: "newfile.txt", 515 | }, 516 | ], 517 | }) 518 | .then(done) 519 | .catch(done); 520 | }); 521 | }); 522 | 523 | describe("to option as function", () => { 524 | it('should transform target path when "from" is a file', (done) => { 525 | runEmit({ 526 | expectedAssetKeys: ["subdir/test.txt"], 527 | patterns: [ 528 | { 529 | from: "file.txt", 530 | to({ context, absoluteFilename }) { 531 | expect(absoluteFilename).toBe( 532 | path.join(FIXTURES_DIR, "file.txt"), 533 | ); 534 | 535 | const targetPath = path.relative(context, absoluteFilename); 536 | 537 | return targetPath.replace("file.txt", "subdir/test.txt"); 538 | }, 539 | }, 540 | ], 541 | }) 542 | .then(done) 543 | .catch(done); 544 | }); 545 | 546 | it('should transform target path of every when "from" is a directory', (done) => { 547 | runEmit({ 548 | expectedAssetKeys: [ 549 | "../.dottedfile", 550 | "../deepnested.txt", 551 | "../directoryfile.txt", 552 | "../nestedfile.txt", 553 | ], 554 | patterns: [ 555 | { 556 | from: "directory", 557 | toType: "file", 558 | to({ context, absoluteFilename }) { 559 | expect(absoluteFilename).toContain( 560 | path.join(FIXTURES_DIR, "directory"), 561 | ); 562 | 563 | const targetPath = path.relative(context, absoluteFilename); 564 | 565 | return path.resolve(__dirname, path.basename(targetPath)); 566 | }, 567 | }, 568 | ], 569 | }) 570 | .then(done) 571 | .catch(done); 572 | }); 573 | 574 | it('should transform target path of every file when "from" is a glob', (done) => { 575 | runEmit({ 576 | expectedAssetKeys: [ 577 | "../deepnested.txt.tst", 578 | "../directoryfile.txt.tst", 579 | "../nestedfile.txt.tst", 580 | ], 581 | patterns: [ 582 | { 583 | from: "directory/**/*", 584 | to({ context, absoluteFilename }) { 585 | expect(absoluteFilename).toContain(FIXTURES_DIR); 586 | 587 | const targetPath = path.relative(context, absoluteFilename); 588 | 589 | return path.resolve( 590 | __dirname, 591 | `${path.basename(targetPath)}.tst`, 592 | ); 593 | }, 594 | }, 595 | ], 596 | }) 597 | .then(done) 598 | .catch(done); 599 | }); 600 | 601 | it("should transform target path when function return Promise", (done) => { 602 | runEmit({ 603 | expectedAssetKeys: ["../file.txt"], 604 | patterns: [ 605 | { 606 | from: "file.txt", 607 | to({ context, absoluteFilename }) { 608 | expect(absoluteFilename).toContain(FIXTURES_DIR); 609 | 610 | const targetPath = path.relative(context, absoluteFilename); 611 | 612 | return new Promise((resolve) => { 613 | resolve(path.resolve(__dirname, path.basename(targetPath))); 614 | }); 615 | }, 616 | }, 617 | ], 618 | }) 619 | .then(done) 620 | .catch(done); 621 | }); 622 | 623 | it("should transform target path when async function used", (done) => { 624 | runEmit({ 625 | expectedAssetKeys: ["../file.txt"], 626 | patterns: [ 627 | { 628 | from: "file.txt", 629 | async to({ context, absoluteFilename }) { 630 | expect(absoluteFilename).toContain(FIXTURES_DIR); 631 | 632 | const targetPath = path.relative(context, absoluteFilename); 633 | 634 | const newPath = await new Promise((resolve) => { 635 | resolve(path.resolve(__dirname, path.basename(targetPath))); 636 | }); 637 | 638 | return newPath; 639 | }, 640 | }, 641 | ], 642 | }) 643 | .then(done) 644 | .catch(done); 645 | }); 646 | 647 | it("should warn when function throw error", (done) => { 648 | runEmit({ 649 | expectedAssetKeys: [], 650 | expectedErrors: [new Error("a failure happened")], 651 | patterns: [ 652 | { 653 | from: "file.txt", 654 | to() { 655 | throw new Error("a failure happened"); 656 | }, 657 | }, 658 | ], 659 | }) 660 | .then(done) 661 | .catch(done); 662 | }); 663 | 664 | it("should warn when Promise was rejected", (done) => { 665 | runEmit({ 666 | expectedAssetKeys: [], 667 | expectedErrors: [new Error("a failure happened")], 668 | patterns: [ 669 | { 670 | from: "file.txt", 671 | to() { 672 | return new Promise((resolve, reject) => { 673 | reject(new Error("a failure happened")); 674 | }); 675 | }, 676 | }, 677 | ], 678 | }) 679 | .then(done) 680 | .catch(done); 681 | }); 682 | 683 | it("should warn when async function throw error", (done) => { 684 | runEmit({ 685 | expectedAssetKeys: [], 686 | expectedErrors: [new Error("a failure happened")], 687 | patterns: [ 688 | { 689 | from: "file.txt", 690 | async to() { 691 | await new Promise((resolve, reject) => { 692 | reject(new Error("a failure happened")); 693 | }); 694 | }, 695 | }, 696 | ], 697 | }) 698 | .then(done) 699 | .catch(done); 700 | }); 701 | 702 | it("should transform target path of every file in glob after applying template", (done) => { 703 | runEmit({ 704 | expectedAssetKeys: [ 705 | "transformed/directory/directoryfile-5d7817.txt", 706 | "transformed/directory/nested/deep-nested/deepnested-31d6cf.txt", 707 | "transformed/directory/nested/nestedfile-31d6cf.txt", 708 | ], 709 | patterns: [ 710 | { 711 | from: "directory/**/*", 712 | to({ absoluteFilename }) { 713 | expect(absoluteFilename).toContain(FIXTURES_DIR); 714 | 715 | return "transformed/[path][name]-[contenthash:6][ext]"; 716 | }, 717 | }, 718 | ], 719 | }) 720 | .then(done) 721 | .catch(done); 722 | }); 723 | 724 | it("should copy files", (done) => { 725 | runEmit({ 726 | expectedAssetKeys: ["txt"], 727 | patterns: [ 728 | { 729 | from: "directory/nested/deep-nested", 730 | toType: "file", 731 | to({ absoluteFilename }) { 732 | const mathes = absoluteFilename.match(/\.([^.]*)$/); 733 | const [, res] = mathes; 734 | const target = res; 735 | 736 | return target; 737 | }, 738 | }, 739 | ], 740 | }) 741 | .then(done) 742 | .catch(done); 743 | }); 744 | 745 | it("should copy files to a non-root directory", (done) => { 746 | runEmit({ 747 | expectedAssetKeys: ["nested/txt"], 748 | patterns: [ 749 | { 750 | from: "directory/nested/deep-nested", 751 | toType: "file", 752 | to({ absoluteFilename }) { 753 | const mathes = absoluteFilename.match(/\.([^.]*)$/); 754 | const [, res] = mathes; 755 | const target = `nested/${res}`; 756 | 757 | return target; 758 | }, 759 | }, 760 | ], 761 | }) 762 | .then(done) 763 | .catch(done); 764 | }); 765 | 766 | it("should copy files (variant 2)", (done) => { 767 | runEmit({ 768 | expectedAssetKeys: [ 769 | "deep-nested-deepnested.txt", 770 | "directoryfile.txt", 771 | "nested-nestedfile.txt", 772 | ], 773 | patterns: [ 774 | { 775 | from: "**/*", 776 | context: "directory", 777 | to({ context, absoluteFilename }) { 778 | const targetPath = path.relative(context, absoluteFilename); 779 | const pathSegments = path.parse(targetPath); 780 | const result = []; 781 | 782 | if (pathSegments.root) { 783 | result.push(pathSegments.root); 784 | } 785 | 786 | if (pathSegments.dir) { 787 | result.push(pathSegments.dir.split(path.sep).pop()); 788 | } 789 | 790 | if (pathSegments.base) { 791 | result.push(pathSegments.base); 792 | } 793 | 794 | return result.join("-"); 795 | }, 796 | }, 797 | ], 798 | }) 799 | .then(done) 800 | .catch(done); 801 | }); 802 | }); 803 | 804 | describe("settings for to option for flatten copy", () => { 805 | it('should flatten a directory\'s files to a root directory when "from" is a file', (done) => { 806 | runEmit({ 807 | expectedAssetKeys: ["directoryfile.txt"], 808 | patterns: [ 809 | { 810 | to: ".", 811 | from: "directory/directoryfile.txt", 812 | }, 813 | ], 814 | }) 815 | .then(done) 816 | .catch(done); 817 | }); 818 | 819 | it('should flatten a directory\'s files to a new directory when "from" is a file', (done) => { 820 | runEmit({ 821 | expectedAssetKeys: ["nested/directoryfile.txt"], 822 | patterns: [ 823 | { 824 | to({ absoluteFilename }) { 825 | return `nested/${path.basename(absoluteFilename)}`; 826 | }, 827 | from: "directory/directoryfile.txt", 828 | }, 829 | ], 830 | }) 831 | .then(done) 832 | .catch(done); 833 | }); 834 | 835 | it('should flatten a directory\'s files to a root directory when "from" is a directory', (done) => { 836 | runEmit({ 837 | expectedAssetKeys: [ 838 | ".dottedfile", 839 | "deepnested.txt", 840 | "directoryfile.txt", 841 | "nestedfile.txt", 842 | ], 843 | patterns: [ 844 | { 845 | to: "[name][ext]", 846 | from: "directory", 847 | }, 848 | ], 849 | }) 850 | .then(done) 851 | .catch(done); 852 | }); 853 | 854 | it('should flatten a directory\'s files to new directory when "from" is a directory', (done) => { 855 | runEmit({ 856 | expectedAssetKeys: [ 857 | "newdirectory/.dottedfile", 858 | "newdirectory/deepnested.txt", 859 | "newdirectory/directoryfile.txt", 860 | "newdirectory/nestedfile.txt", 861 | ], 862 | patterns: [ 863 | { 864 | toType: "file", 865 | to({ absoluteFilename }) { 866 | return `newdirectory/${path.basename(absoluteFilename)}`; 867 | }, 868 | from: "directory", 869 | }, 870 | ], 871 | }) 872 | .then(done) 873 | .catch(done); 874 | }); 875 | 876 | it('should flatten a directory\'s files to a root directory when "from" is a glob', (done) => { 877 | runEmit({ 878 | expectedAssetKeys: [ 879 | "deepnested.txt", 880 | "directoryfile.txt", 881 | "nestedfile.txt", 882 | ], 883 | patterns: [ 884 | { 885 | to({ absoluteFilename }) { 886 | return path.basename(absoluteFilename); 887 | }, 888 | from: "directory/**/*", 889 | }, 890 | ], 891 | }) 892 | .then(done) 893 | .catch(done); 894 | }); 895 | 896 | it('should flatten a directory\'s files to a new directory when "from" is a glob', (done) => { 897 | runEmit({ 898 | expectedAssetKeys: [ 899 | "nested/deepnested.txt", 900 | "nested/directoryfile.txt", 901 | "nested/nestedfile.txt", 902 | ], 903 | patterns: [ 904 | { 905 | to({ absoluteFilename }) { 906 | return `nested/${path.basename(absoluteFilename)}`; 907 | }, 908 | from: "directory/**/*", 909 | }, 910 | ], 911 | }) 912 | .then(done) 913 | .catch(done); 914 | }); 915 | 916 | it('should flatten files in a relative context to a root directory when "from" is a glob', (done) => { 917 | runEmit({ 918 | expectedAssetKeys: [ 919 | "deepnested.txt", 920 | "directoryfile.txt", 921 | "nestedfile.txt", 922 | ], 923 | patterns: [ 924 | { 925 | context: "directory", 926 | from: "**/*", 927 | to({ absoluteFilename }) { 928 | return path.basename(absoluteFilename); 929 | }, 930 | }, 931 | ], 932 | }) 933 | .then(done) 934 | .catch(done); 935 | }); 936 | 937 | it('should flatten files in a relative context to a non-root directory when "from" is a glob', (done) => { 938 | runEmit({ 939 | expectedAssetKeys: [ 940 | "nested/deepnested.txt", 941 | "nested/directoryfile.txt", 942 | "nested/nestedfile.txt", 943 | ], 944 | patterns: [ 945 | { 946 | context: "directory", 947 | from: "**/*", 948 | to({ absoluteFilename }) { 949 | return `nested/${path.basename(absoluteFilename)}`; 950 | }, 951 | }, 952 | ], 953 | }) 954 | .then(done) 955 | .catch(done); 956 | }); 957 | }); 958 | 959 | it("should process template string", (done) => { 960 | runEmit({ 961 | expectedAssetKeys: [ 962 | "directory/directoryfile.txt-new-directoryfile.txt.5d7817ed5bc246756d73.47e8bdc316eff74b2d6e-47e8bdc316eff74b2d6e.txt--[unknown]", 963 | ], 964 | patterns: [ 965 | { 966 | from: "directory/directoryfile.*", 967 | to: "[path][base]-new-[name][ext].[contenthash].[hash]-[fullhash][ext]--[unknown]", 968 | }, 969 | ], 970 | }) 971 | .then(done) 972 | .catch(done); 973 | }); 974 | 975 | it("should rewrite invalid [contenthash] in 'production' mode", (done) => { 976 | const compiler = getCompiler({ 977 | mode: "production", 978 | }); 979 | 980 | runEmit({ 981 | compiler, 982 | breakContenthash: { 983 | targetAssets: [ 984 | { 985 | name: "5d7817ed5bc246756d73-directoryfile.txt", 986 | newName: "33333333333333333333-directoryfile.txt", 987 | newHash: "33333333333333333333", 988 | }, 989 | ], 990 | }, 991 | expectedAssetKeys: ["5d7817ed5bc246756d73-directoryfile.txt"], 992 | patterns: [ 993 | { 994 | from: "directory/directoryfile.*", 995 | to: "[contenthash]-[name][ext]", 996 | toType: "template", 997 | }, 998 | ], 999 | }) 1000 | .then(done) 1001 | .catch(done); 1002 | }); 1003 | }); 1004 | --------------------------------------------------------------------------------