Skip to content

Commit a527c89

Browse files
fix: better handling globs for watching (#808)
1 parent 10d5972 commit a527c89

File tree

5 files changed

+162
-28
lines changed

5 files changed

+162
-28
lines changed

.cspell.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"version": "0.2",
33
"language": "en,en-gb",
44
"words": [
5+
"eslintcache",
56
"commitlint",
67
"nestedfile",
78
"directoryfile",
@@ -34,7 +35,6 @@
3435
"dottedfile",
3536
"tempfile"
3637
],
37-
3838
"ignorePaths": [
3939
"CHANGELOG.md",
4040
"package.json",

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ npm-debug.log*
1010
/reports
1111
/node_modules
1212
/test/fixtures/\[special\$directory\]
13+
/test/fixtures/watch/**/*.txt
1314
/test/outputs
1415
/test/bundled
1516

src/index.js

Lines changed: 84 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const getTinyGlobby = memoize(() => require("tinyglobby"));
2222
/** @typedef {import("webpack").Compilation} Compilation */
2323
/** @typedef {import("webpack").Asset} Asset */
2424
/** @typedef {import("webpack").AssetInfo} AssetInfo */
25+
/** @typedef {import("webpack").InputFileSystem} InputFileSystem */
2526
/** @typedef {import("tinyglobby").GlobOptions} GlobbyOptions */
2627
/** @typedef {ReturnType<Compilation["getLogger"]>} WebpackLogger */
2728
/** @typedef {ReturnType<Compilation["getCache"]>} CacheFacade */
@@ -243,6 +244,63 @@ class CopyPlugin {
243244
return fullContentHash.toString().slice(0, hashDigestLength);
244245
}
245246

247+
/**
248+
* @private
249+
* @param {Compilation} compilation the compilation
250+
* @param {"file" | "dir" | "glob"} typeOfFrom the type of from
251+
* @param {string} absoluteFrom the source content to hash
252+
* @param {InputFileSystem | null} inputFileSystem input file system
253+
* @param {WebpackLogger} logger the logger to use for logging
254+
* @returns {Promise<void>}
255+
*/
256+
static async addCompilationDependency(
257+
compilation,
258+
typeOfFrom,
259+
absoluteFrom,
260+
inputFileSystem,
261+
logger,
262+
) {
263+
switch (typeOfFrom) {
264+
case "dir":
265+
compilation.contextDependencies.add(absoluteFrom);
266+
logger.debug(`added '${absoluteFrom}' as a context dependency`);
267+
break;
268+
case "file":
269+
compilation.fileDependencies.add(absoluteFrom);
270+
logger.debug(`added '${absoluteFrom}' as a file dependency`);
271+
break;
272+
case "glob":
273+
default: {
274+
const contextDependency = getTinyGlobby().isDynamicPattern(absoluteFrom)
275+
? path.normalize(getGlobParent()(absoluteFrom))
276+
: path.normalize(absoluteFrom);
277+
278+
let stats;
279+
280+
// If we have `inputFileSystem` we should check the glob is existing or not
281+
if (inputFileSystem) {
282+
try {
283+
stats = await stat(inputFileSystem, contextDependency);
284+
} catch {
285+
// Nothing
286+
}
287+
}
288+
289+
// To prevent double compilation during aggregation (initial run) - https://github.com/webpack-contrib/copy-webpack-plugin/issues/806.
290+
// 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.
291+
// To prevent this behavior we should add the glob to the missing dependencies if the glob doesn't exist,
292+
// otherwise we should add the dependency to the context dependencies.
293+
if (inputFileSystem && !stats) {
294+
compilation.missingDependencies.add(contextDependency);
295+
logger.debug(`added '${contextDependency}' as a missing dependency`);
296+
} else {
297+
compilation.contextDependencies.add(contextDependency);
298+
logger.debug(`added '${contextDependency}' as a context dependency`);
299+
}
300+
}
301+
}
302+
}
303+
246304
/**
247305
* @private
248306
* @param {typeof import("tinyglobby").glob} globby the globby function to use for globbing
@@ -277,12 +335,13 @@ class CopyPlugin {
277335

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

280-
const { inputFileSystem } = compiler;
338+
const { inputFileSystem } =
339+
/** @type {Compiler & { inputFileSystem: InputFileSystem }} */
340+
(compiler);
281341

282342
let stats;
283343

284344
try {
285-
// @ts-expect-error - webpack types are incomplete
286345
stats = await stat(inputFileSystem, absoluteFrom);
287346
} catch {
288347
// Nothing
@@ -291,22 +350,22 @@ class CopyPlugin {
291350
/**
292351
* @type {"file" | "dir" | "glob"}
293352
*/
294-
let fromType;
353+
let typeOfFrom;
295354

296355
if (stats) {
297356
if (stats.isDirectory()) {
298-
fromType = "dir";
357+
typeOfFrom = "dir";
299358
logger.debug(`determined '${absoluteFrom}' is a directory`);
300359
} else if (stats.isFile()) {
301-
fromType = "file";
360+
typeOfFrom = "file";
302361
logger.debug(`determined '${absoluteFrom}' is a file`);
303362
} else {
304363
// Fallback
305-
fromType = "glob";
364+
typeOfFrom = "glob";
306365
logger.debug(`determined '${absoluteFrom}' is unknown`);
307366
}
308367
} else {
309-
fromType = "glob";
368+
typeOfFrom = "glob";
310369
logger.debug(`determined '${absoluteFrom}' is a glob`);
311370
}
312371

@@ -325,12 +384,8 @@ class CopyPlugin {
325384

326385
let glob;
327386

328-
switch (fromType) {
387+
switch (typeOfFrom) {
329388
case "dir":
330-
compilation.contextDependencies.add(absoluteFrom);
331-
332-
logger.debug(`added '${absoluteFrom}' as a context dependency`);
333-
334389
pattern.context = absoluteFrom;
335390
glob = path.posix.join(
336391
getTinyGlobby().escapePath(getNormalizePath()(absoluteFrom)),
@@ -342,10 +397,6 @@ class CopyPlugin {
342397
}
343398
break;
344399
case "file":
345-
compilation.fileDependencies.add(absoluteFrom);
346-
347-
logger.debug(`added '${absoluteFrom}' as a file dependency`);
348-
349400
pattern.context = path.dirname(absoluteFrom);
350401
glob = getTinyGlobby().escapePath(getNormalizePath()(absoluteFrom));
351402

@@ -355,14 +406,6 @@ class CopyPlugin {
355406
break;
356407
case "glob":
357408
default: {
358-
const contextDependencies = path.normalize(
359-
getGlobParent()(absoluteFrom),
360-
);
361-
362-
compilation.contextDependencies.add(contextDependencies);
363-
364-
logger.debug(`added '${contextDependencies}' as a context dependency`);
365-
366409
glob = path.isAbsolute(pattern.from)
367410
? pattern.from
368411
: path.posix.join(
@@ -388,6 +431,14 @@ class CopyPlugin {
388431
}
389432

390433
if (globEntries.length === 0) {
434+
await CopyPlugin.addCompilationDependency(
435+
compilation,
436+
typeOfFrom,
437+
absoluteFrom,
438+
inputFileSystem,
439+
logger,
440+
);
441+
391442
if (pattern.noErrorOnMissing) {
392443
logger.log(
393444
`finished to process a pattern from '${pattern.from}' using '${pattern.context}' context to '${pattern.to}'`,
@@ -401,6 +452,14 @@ class CopyPlugin {
401452
return;
402453
}
403454

455+
await CopyPlugin.addCompilationDependency(
456+
compilation,
457+
typeOfFrom,
458+
absoluteFrom,
459+
null,
460+
logger,
461+
);
462+
404463
/**
405464
* @type {Array<CopiedResult | undefined>}
406465
*/
@@ -475,7 +534,7 @@ class CopyPlugin {
475534
);
476535

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

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

542601
try {
543-
// @ts-expect-error - webpack types are incomplete
544602
data = await readFile(inputFileSystem, absoluteFilename);
545603
} catch (error) {
546604
compilation.errors.push(/** @type {Error} */ (error));

test/CopyPlugin.test.js

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ describe("CopyPlugin", () => {
442442
});
443443

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

448448
run({
@@ -461,6 +461,27 @@ describe("CopyPlugin", () => {
461461
.catch(done);
462462
});
463463

464+
it('should add a file to the watch list when "from" is a file that does not exist.', (done) => {
465+
run({
466+
patterns: [
467+
{
468+
from: "directory-does-not-exist/file.txt",
469+
noErrorOnMissing: true,
470+
},
471+
],
472+
})
473+
.then(({ stats }) => {
474+
const { missingDependencies } = stats.compilation;
475+
const isIncludeDependency = missingDependencies.has(
476+
path.join(FIXTURES_DIR, "directory-does-not-exist/file.txt"),
477+
);
478+
479+
expect(isIncludeDependency).toBe(true);
480+
})
481+
.then(done)
482+
.catch(done);
483+
});
484+
464485
it('should add a directory to the watch list when "from" is a directory', (done) => {
465486
run({
466487
patterns: [
@@ -481,6 +502,27 @@ describe("CopyPlugin", () => {
481502
.catch(done);
482503
});
483504

505+
it('should add a directory to the watch list when "from" is a directory and that does not exist.', (done) => {
506+
run({
507+
patterns: [
508+
{
509+
from: "directory-does-not-exist/",
510+
noErrorOnMissing: true,
511+
},
512+
],
513+
})
514+
.then(({ stats }) => {
515+
const { missingDependencies } = stats.compilation;
516+
const isIncludeDependency = missingDependencies.has(
517+
path.join(FIXTURES_DIR, "directory-does-not-exist"),
518+
);
519+
520+
expect(isIncludeDependency).toBe(true);
521+
})
522+
.then(done)
523+
.catch(done);
524+
});
525+
484526
it('should add a directory to the watch list when "from" is a glob', (done) => {
485527
run({
486528
patterns: [
@@ -501,6 +543,27 @@ describe("CopyPlugin", () => {
501543
.catch(done);
502544
});
503545

546+
it('should add a directory to the watch list when "from" is a glob and that does not exist.', (done) => {
547+
run({
548+
patterns: [
549+
{
550+
from: "directory-does-not-exist/**/*",
551+
noErrorOnMissing: true,
552+
},
553+
],
554+
})
555+
.then(({ stats }) => {
556+
const { missingDependencies } = stats.compilation;
557+
const isIncludeDependency = missingDependencies.has(
558+
path.join(FIXTURES_DIR, "directory-does-not-exist"),
559+
);
560+
561+
expect(isIncludeDependency).toBe(true);
562+
})
563+
.then(done)
564+
.catch(done);
565+
});
566+
504567
it("should not add the directory to the watch list when glob is a file", (done) => {
505568
const expectedAssetKeys = ["directoryfile.txt"];
506569

types/index.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ declare class CopyPlugin {
2323
* @returns {string} returns the content hash of the source
2424
*/
2525
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<void>}
34+
*/
35+
private static addCompilationDependency;
2636
/**
2737
* @private
2838
* @param {typeof import("tinyglobby").glob} globby the globby function to use for globbing
@@ -62,6 +72,7 @@ declare namespace CopyPlugin {
6272
Compilation,
6373
Asset,
6474
AssetInfo,
75+
InputFileSystem,
6576
GlobbyOptions,
6677
WebpackLogger,
6778
CacheFacade,
@@ -94,6 +105,7 @@ type Compiler = import("webpack").Compiler;
94105
type Compilation = import("webpack").Compilation;
95106
type Asset = import("webpack").Asset;
96107
type AssetInfo = import("webpack").AssetInfo;
108+
type InputFileSystem = import("webpack").InputFileSystem;
97109
type GlobbyOptions = import("tinyglobby").GlobOptions;
98110
type WebpackLogger = ReturnType<Compilation["getLogger"]>;
99111
type CacheFacade = ReturnType<Compilation["getCache"]>;

0 commit comments

Comments
 (0)