Skip to content

Commit d2143ec

Browse files
Merge pull request #345 from webpack/fix/report-removal-after-start-time
fix: report a file removed after startTime to a watcher attached later
2 parents 38505ef + cc817ae commit d2143ec

4 files changed

Lines changed: 82 additions & 2 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"watchpack": patch
3+
---
4+
5+
fix: report a file removed after `startTime` to a watcher attached later, as a change after `startTime` already is.
6+
7+
Previously a watcher attached with a `startTime` learned of files changed since then ("outdated on attach") but not of files removed since then, so webpack missed a dependency deleted while a build ran.

‎lib/DirectoryWatcher.js‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,10 @@ class DirectoryWatcher extends EventEmitter {
233233
this.initialScanRemoved = new Set();
234234
/** @type {undefined | number} */
235235
this.initialScanFinished = undefined;
236+
// when a known file was removed, so a watcher attaching later with an
237+
// earlier start time learns of the removal, as it learns of a change
238+
/** @type {Map<string, number>} */
239+
this.removedTimes = new Map();
236240
/** @type {Map<string, Set<Watcher<DirectoryWatcherEvents> | Watcher<FileWatcherEvents>>>} */
237241
this.watchers = new Map();
238242
/** @type {Watcher<FileWatcherEvents> | null} */
@@ -319,6 +323,7 @@ class DirectoryWatcher extends EventEmitter {
319323
const oldFile = this.files.get(itemPath);
320324
if (oldFile) {
321325
this.files.delete(itemPath);
326+
if (!initial) this.removedTimes.set(withoutCase(itemPath), Date.now());
322327
const key = withoutCase(itemPath);
323328
const count = /** @type {number} */ (this.filesWithoutCase.get(key)) - 1;
324329
if (count <= 0) {
@@ -377,6 +382,7 @@ class DirectoryWatcher extends EventEmitter {
377382
});
378383

379384
if (!old) {
385+
this.removedTimes.delete(withoutCase(target));
380386
const key = withoutCase(target);
381387
const count = this.filesWithoutCase.get(key);
382388
this.filesWithoutCase.set(key, (count || 0) + 1);
@@ -566,11 +572,17 @@ class DirectoryWatcher extends EventEmitter {
566572
} else if (
567573
target !== this.path &&
568574
!this.directories.has(target) &&
569-
watcher.checkStartTime(
575+
(watcher.checkStartTime(
570576
/** @type {number} */
571577
(this.initialScanFinished),
572578
false,
573-
)
579+
) ||
580+
(this.removedTimes.has(withoutCase(target)) &&
581+
watcher.checkStartTime(
582+
/** @type {number} */
583+
(this.removedTimes.get(withoutCase(target))),
584+
false,
585+
)))
574586
) {
575587
process.nextTick(() => {
576588
if (this.closed) return;

‎test/Watchpack.test.js‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,65 @@ describe("Watchpack", () => {
10431043
});
10441044
});
10451045

1046+
it("should detect a past removal of a file (watched)", (done) => {
1047+
testHelper.file("a");
1048+
// one options object shares one watcher pool, as webpack does
1049+
const options = {};
1050+
const w2 = new WatchpackTest(options);
1051+
const w = new WatchpackTest(options);
1052+
w.on("aggregated", (changes, removals) => {
1053+
expect([...changes]).toEqual([]);
1054+
expect([...removals]).toEqual([path.join(fixtures, "a")]);
1055+
w.close();
1056+
w2.close();
1057+
done();
1058+
});
1059+
testHelper.tick(400, () => {
1060+
w2.watch([path.join(fixtures, "a")], []);
1061+
testHelper.tick(() => {
1062+
const startTime = Date.now();
1063+
testHelper.tick(() => {
1064+
// attach once the shared pool has seen the removal, not a live one
1065+
w2.once("remove", () => {
1066+
testHelper.tick(() => {
1067+
w.watch([path.join(fixtures, "a")], [], startTime);
1068+
});
1069+
});
1070+
testHelper.remove("a");
1071+
});
1072+
});
1073+
});
1074+
});
1075+
1076+
it("should not detect a removal before the start time (watched)", (done) => {
1077+
testHelper.file("a");
1078+
// one options object shares one watcher pool, as webpack does
1079+
const options = {};
1080+
const w2 = new WatchpackTest(options);
1081+
const w = new WatchpackTest(options);
1082+
let aggregatedEvents = 0;
1083+
w.on("aggregated", () => {
1084+
aggregatedEvents++;
1085+
});
1086+
testHelper.tick(400, () => {
1087+
w2.watch([path.join(fixtures, "a")], []);
1088+
testHelper.tick(() => {
1089+
w2.once("remove", () => {
1090+
testHelper.tick(() => {
1091+
w.watch([path.join(fixtures, "a")], [], Date.now());
1092+
testHelper.tick(1000, () => {
1093+
expect(aggregatedEvents).toBe(0);
1094+
w.close();
1095+
w2.close();
1096+
done();
1097+
});
1098+
});
1099+
});
1100+
testHelper.remove("a");
1101+
});
1102+
});
1103+
});
1104+
10461105
it("should watch a single file removal", (done) => {
10471106
testHelper.file("a");
10481107
const w = new WatchpackTest({

‎types/DirectoryWatcher.d.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ declare class DirectoryWatcher extends EventEmitter<{
6262
initialScanRemoved: null | InitialScanRemoved;
6363
/** @type {undefined | number} */
6464
initialScanFinished: undefined | number;
65+
/** @type {Map<string, number>} */
66+
removedTimes: Map<string, number>;
6567
/** @type {Map<string, Set<Watcher<DirectoryWatcherEvents> | Watcher<FileWatcherEvents>>>} */
6668
watchers: Map<
6769
string,

0 commit comments

Comments
 (0)