Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"version": "0.2",
"language": "en,en-gb",
"words": [
"eslintcache",
"commitlint",
"nestedfile",
"directoryfile",
Expand Down Expand Up @@ -34,7 +35,6 @@
"dottedfile",
"tempfile"
],

"ignorePaths": [
"CHANGELOG.md",
"package.json",
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ npm-debug.log*
/reports
/node_modules
/test/fixtures/\[special\$directory\]
/test/fixtures/watch/**/*.txt
/test/outputs
/test/bundled

Expand Down
110 changes: 84 additions & 26 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const getTinyGlobby = memoize(() => require("tinyglobby"));
/** @typedef {import("webpack").Compilation} Compilation */
/** @typedef {import("webpack").Asset} Asset */
/** @typedef {import("webpack").AssetInfo} AssetInfo */
/** @typedef {import("webpack").InputFileSystem} InputFileSystem */
/** @typedef {import("tinyglobby").GlobOptions} GlobbyOptions */
/** @typedef {ReturnType<Compilation["getLogger"]>} WebpackLogger */
/** @typedef {ReturnType<Compilation["getCache"]>} CacheFacade */
Expand Down Expand Up @@ -243,6 +244,63 @@ class CopyPlugin {
return fullContentHash.toString().slice(0, hashDigestLength);
}

/**
* @private
* @param {Compilation} compilation the compilation
* @param {"file" | "dir" | "glob"} typeOfFrom the type of from
* @param {string} absoluteFrom the source content to hash
* @param {InputFileSystem | null} inputFileSystem input file system
* @param {WebpackLogger} logger the logger to use for logging
* @returns {Promise<void>}
*/
static async addCompilationDependency(
compilation,
typeOfFrom,
absoluteFrom,
inputFileSystem,
logger,
) {
switch (typeOfFrom) {
case "dir":
compilation.contextDependencies.add(absoluteFrom);
logger.debug(`added '${absoluteFrom}' as a context dependency`);
break;
case "file":
compilation.fileDependencies.add(absoluteFrom);
logger.debug(`added '${absoluteFrom}' as a file dependency`);
break;
case "glob":
default: {
const contextDependency = getTinyGlobby().isDynamicPattern(absoluteFrom)
? path.normalize(getGlobParent()(absoluteFrom))
: path.normalize(absoluteFrom);

let stats;

// If we have `inputFileSystem` we should check the glob is existing or not
if (inputFileSystem) {
try {
stats = await stat(inputFileSystem, contextDependency);
} catch {
// Nothing
}
}

// To prevent double compilation during aggregation (initial run) - https://github.com/webpack-contrib/copy-webpack-plugin/issues/806.
// On first run we don't know if the glob exists or not, adding the dependency to the context dependencies triggers the `removed` event during aggregation.
// To prevent this behavior we should add the glob to the missing dependencies if the glob doesn't exist,
// otherwise we should add the dependency to the context dependencies.
if (inputFileSystem && !stats) {
compilation.missingDependencies.add(contextDependency);
logger.debug(`added '${contextDependency}' as a missing dependency`);
} else {
compilation.contextDependencies.add(contextDependency);
logger.debug(`added '${contextDependency}' as a context dependency`);
}
}
}
}

/**
* @private
* @param {typeof import("tinyglobby").glob} globby the globby function to use for globbing
Expand Down Expand Up @@ -277,12 +335,13 @@ class CopyPlugin {

logger.debug(`getting stats for '${absoluteFrom}'...`);

const { inputFileSystem } = compiler;
const { inputFileSystem } =
/** @type {Compiler & { inputFileSystem: InputFileSystem }} */
(compiler);

let stats;

try {
// @ts-expect-error - webpack types are incomplete
stats = await stat(inputFileSystem, absoluteFrom);
} catch {
// Nothing
Expand All @@ -291,22 +350,22 @@ class CopyPlugin {
/**
* @type {"file" | "dir" | "glob"}
*/
let fromType;
let typeOfFrom;

if (stats) {
if (stats.isDirectory()) {
fromType = "dir";
typeOfFrom = "dir";
logger.debug(`determined '${absoluteFrom}' is a directory`);
} else if (stats.isFile()) {
fromType = "file";
typeOfFrom = "file";
logger.debug(`determined '${absoluteFrom}' is a file`);
} else {
// Fallback
fromType = "glob";
typeOfFrom = "glob";
logger.debug(`determined '${absoluteFrom}' is unknown`);
}
} else {
fromType = "glob";
typeOfFrom = "glob";
logger.debug(`determined '${absoluteFrom}' is a glob`);
}

Expand All @@ -325,12 +384,8 @@ class CopyPlugin {

let glob;

switch (fromType) {
switch (typeOfFrom) {
case "dir":
compilation.contextDependencies.add(absoluteFrom);

logger.debug(`added '${absoluteFrom}' as a context dependency`);

pattern.context = absoluteFrom;
glob = path.posix.join(
getTinyGlobby().escapePath(getNormalizePath()(absoluteFrom)),
Expand All @@ -342,10 +397,6 @@ class CopyPlugin {
}
break;
case "file":
compilation.fileDependencies.add(absoluteFrom);

logger.debug(`added '${absoluteFrom}' as a file dependency`);

pattern.context = path.dirname(absoluteFrom);
glob = getTinyGlobby().escapePath(getNormalizePath()(absoluteFrom));

Expand All @@ -355,14 +406,6 @@ class CopyPlugin {
break;
case "glob":
default: {
const contextDependencies = path.normalize(
getGlobParent()(absoluteFrom),
);

compilation.contextDependencies.add(contextDependencies);

logger.debug(`added '${contextDependencies}' as a context dependency`);

glob = path.isAbsolute(pattern.from)
? pattern.from
: path.posix.join(
Expand All @@ -388,6 +431,14 @@ class CopyPlugin {
}

if (globEntries.length === 0) {
await CopyPlugin.addCompilationDependency(
compilation,
typeOfFrom,
absoluteFrom,
inputFileSystem,
logger,
);

if (pattern.noErrorOnMissing) {
logger.log(
`finished to process a pattern from '${pattern.from}' using '${pattern.context}' context to '${pattern.to}'`,
Expand All @@ -401,6 +452,14 @@ class CopyPlugin {
return;
}

await CopyPlugin.addCompilationDependency(
compilation,
typeOfFrom,
absoluteFrom,
null,
logger,
);

/**
* @type {Array<CopiedResult | undefined>}
*/
Expand Down Expand Up @@ -475,7 +534,7 @@ class CopyPlugin {
);

// If this came from a glob or dir, add it to the file dependencies
if (fromType === "dir" || fromType === "glob") {
if (typeOfFrom === "dir" || typeOfFrom === "glob") {
compilation.fileDependencies.add(absoluteFilename);

logger.debug(`added '${absoluteFilename}' as a file dependency`);
Expand Down Expand Up @@ -540,7 +599,6 @@ class CopyPlugin {
let data;

try {
// @ts-expect-error - webpack types are incomplete
data = await readFile(inputFileSystem, absoluteFilename);
} catch (error) {
compilation.errors.push(/** @type {Error} */ (error));
Expand Down
65 changes: 64 additions & 1 deletion test/CopyPlugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ describe("CopyPlugin", () => {
});

describe("watch mode", () => {
it('should add the file to the watch list when "from" is a file', (done) => {
it('should add a file to the watch list when "from" is a file', (done) => {
const expectedAssetKeys = ["file.txt"];

run({
Expand All @@ -461,6 +461,27 @@ describe("CopyPlugin", () => {
.catch(done);
});

it('should add a file to the watch list when "from" is a file that does not exist.', (done) => {
run({
patterns: [
{
from: "directory-does-not-exist/file.txt",
noErrorOnMissing: true,
},
],
})
.then(({ stats }) => {
const { missingDependencies } = stats.compilation;
const isIncludeDependency = missingDependencies.has(
path.join(FIXTURES_DIR, "directory-does-not-exist/file.txt"),
);

expect(isIncludeDependency).toBe(true);
})
.then(done)
.catch(done);
});

it('should add a directory to the watch list when "from" is a directory', (done) => {
run({
patterns: [
Expand All @@ -481,6 +502,27 @@ describe("CopyPlugin", () => {
.catch(done);
});

it('should add a directory to the watch list when "from" is a directory and that does not exist.', (done) => {
run({
patterns: [
{
from: "directory-does-not-exist/",
noErrorOnMissing: true,
},
],
})
.then(({ stats }) => {
const { missingDependencies } = stats.compilation;
const isIncludeDependency = missingDependencies.has(
path.join(FIXTURES_DIR, "directory-does-not-exist"),
);

expect(isIncludeDependency).toBe(true);
})
.then(done)
.catch(done);
});

it('should add a directory to the watch list when "from" is a glob', (done) => {
run({
patterns: [
Expand All @@ -501,6 +543,27 @@ describe("CopyPlugin", () => {
.catch(done);
});

it('should add a directory to the watch list when "from" is a glob and that does not exist.', (done) => {
run({
patterns: [
{
from: "directory-does-not-exist/**/*",
noErrorOnMissing: true,
},
],
})
.then(({ stats }) => {
const { missingDependencies } = stats.compilation;
const isIncludeDependency = missingDependencies.has(
path.join(FIXTURES_DIR, "directory-does-not-exist"),
);

expect(isIncludeDependency).toBe(true);
})
.then(done)
.catch(done);
});

it("should not add the directory to the watch list when glob is a file", (done) => {
const expectedAssetKeys = ["directoryfile.txt"];

Expand Down
12 changes: 12 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ declare class CopyPlugin {
* @returns {string} returns the content hash of the source
*/
private static getContentHash;
/**
* @private
* @param {Compilation} compilation the compilation
* @param {"file" | "dir" | "glob"} typeOfFrom the type of from
* @param {string} absoluteFrom the source content to hash
* @param {InputFileSystem | null} inputFileSystem input file system
* @param {WebpackLogger} logger the logger to use for logging
* @returns {Promise<void>}
*/
private static addCompilationDependency;
/**
* @private
* @param {typeof import("tinyglobby").glob} globby the globby function to use for globbing
Expand Down Expand Up @@ -62,6 +72,7 @@ declare namespace CopyPlugin {
Compilation,
Asset,
AssetInfo,
InputFileSystem,
GlobbyOptions,
WebpackLogger,
CacheFacade,
Expand Down Expand Up @@ -94,6 +105,7 @@ type Compiler = import("webpack").Compiler;
type Compilation = import("webpack").Compilation;
type Asset = import("webpack").Asset;
type AssetInfo = import("webpack").AssetInfo;
type InputFileSystem = import("webpack").InputFileSystem;
type GlobbyOptions = import("tinyglobby").GlobOptions;
type WebpackLogger = ReturnType<Compilation["getLogger"]>;
type CacheFacade = ReturnType<Compilation["getCache"]>;
Expand Down
Loading