Skip to content

Commit 1485eb5

Browse files
committed
keep hmr update with output.clean=true based on timestamp
1 parent 4abf353 commit 1485eb5

2 files changed

Lines changed: 139 additions & 18 deletions

File tree

lib/CleanPlugin.js

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const processAsyncTree = require("./util/processAsyncTree");
1919
/** @typedef {import("./util/fs").StatsCallback} StatsCallback */
2020

2121
/** @typedef {(function(string):boolean)|RegExp} IgnoreItem */
22+
/** @typedef {Map<string, number>} Assets */
2223
/** @typedef {function(IgnoreItem): void} AddToIgnoreCallback */
2324

2425
/**
@@ -40,18 +41,32 @@ const validate = createSchemaValidation(
4041
baseDataPath: "options"
4142
}
4243
);
44+
const _10sec = 10 * 1000;
45+
46+
/**
47+
* marge assets map 2 into map 1
48+
* @param {Assets} as1 assets
49+
* @param {Assets} as2 assets
50+
* @returns {void}
51+
*/
52+
const mergeAssets = (as1, as2) => {
53+
for (const [key, value1] of as2) {
54+
const value2 = as1.get(key);
55+
if (!value2 || value1 > value2) as1.set(key, value1);
56+
}
57+
};
4358

4459
/**
4560
* @param {OutputFileSystem} fs filesystem
4661
* @param {string} outputPath output path
47-
* @param {Set<string>} currentAssets filename of the current assets (must not start with .. or ., must only use / as path separator)
62+
* @param {Map<string, number>} currentAssets filename of the current assets (must not start with .. or ., must only use / as path separator)
4863
* @param {function((Error | null)=, Set<string>=): void} callback returns the filenames of the assets that shouldn't be there
4964
* @returns {void}
5065
*/
5166
const getDiffToFs = (fs, outputPath, currentAssets, callback) => {
5267
const directories = new Set();
5368
// get directories of assets
54-
for (const asset of currentAssets) {
69+
for (const [asset] of currentAssets) {
5570
directories.add(asset.replace(/(^|\/)[^/]*$/, ""));
5671
}
5772
// and all parent directories
@@ -91,13 +106,15 @@ const getDiffToFs = (fs, outputPath, currentAssets, callback) => {
91106
};
92107

93108
/**
94-
* @param {Set<string>} currentAssets assets list
95-
* @param {Set<string>} oldAssets old assets list
109+
* @param {Assets} currentAssets assets list
110+
* @param {Assets} oldAssets old assets list
96111
* @returns {Set<string>} diff
97112
*/
98113
const getDiffToOldAssets = (currentAssets, oldAssets) => {
99114
const diff = new Set();
100-
for (const asset of oldAssets) {
115+
const now = Date.now();
116+
for (const [asset, ts] of oldAssets) {
117+
if (ts >= now) continue;
101118
if (!currentAssets.has(asset)) diff.add(asset);
102119
}
103120
return diff;
@@ -124,7 +141,7 @@ const doStat = (fs, filename, callback) => {
124141
* @param {Logger} logger logger
125142
* @param {Set<string>} diff filenames of the assets that shouldn't be there
126143
* @param {function(string): boolean} isKept check if the entry is ignored
127-
* @param {function(Error=): void} callback callback
144+
* @param {function(Error=, Assets=): void} callback callback
128145
* @returns {void}
129146
*/
130147
const applyDiff = (fs, outputPath, dry, logger, diff, isKept, callback) => {
@@ -137,11 +154,13 @@ const applyDiff = (fs, outputPath, dry, logger, diff, isKept, callback) => {
137154
};
138155
/** @typedef {{ type: "check" | "unlink" | "rmdir", filename: string, parent: { remaining: number, job: Job } | undefined }} Job */
139156
/** @type {Job[]} */
140-
const jobs = Array.from(diff, filename => ({
157+
const jobs = Array.from(diff.keys(), filename => ({
141158
type: "check",
142159
filename,
143160
parent: undefined
144161
}));
162+
/** @type {Assets} */
163+
const keptAssets = new Map();
145164
processAsyncTree(
146165
jobs,
147166
10,
@@ -161,6 +180,7 @@ const applyDiff = (fs, outputPath, dry, logger, diff, isKept, callback) => {
161180
switch (type) {
162181
case "check":
163182
if (isKept(filename)) {
183+
keptAssets.set(filename, Date.now());
164184
// do not decrement parent entry as we don't want to delete the parent
165185
log(`${filename} will be kept`);
166186
return process.nextTick(callback);
@@ -247,7 +267,10 @@ const applyDiff = (fs, outputPath, dry, logger, diff, isKept, callback) => {
247267
break;
248268
}
249269
},
250-
callback
270+
err => {
271+
if (err) return callback(err);
272+
callback(undefined, keptAssets);
273+
}
251274
);
252275
};
253276

@@ -302,6 +325,7 @@ class CleanPlugin {
302325
// We assume that no external modification happens while the compiler is active
303326
// So we can store the old assets and only diff to them to avoid fs access on
304327
// incremental builds
328+
/** @type {undefined|Assets} */
305329
let oldAssets;
306330

307331
compiler.hooks.emit.tapAsync(
@@ -322,7 +346,9 @@ class CleanPlugin {
322346
);
323347
}
324348

325-
const currentAssets = new Set();
349+
/** @type {Assets} */
350+
const currentAssets = new Map();
351+
const now = Date.now();
326352
for (const asset of Object.keys(compilation.assets)) {
327353
if (/^[A-Za-z]:\\|^\/|^\\\\/.test(asset)) continue;
328354
let normalizedAsset;
@@ -335,7 +361,12 @@ class CleanPlugin {
335361
);
336362
} while (newNormalizedAsset !== normalizedAsset);
337363
if (normalizedAsset.startsWith("../")) continue;
338-
currentAssets.add(normalizedAsset);
364+
const assetInfo = compilation.assetsInfo.get(asset);
365+
if (assetInfo && assetInfo.hotModuleReplacement) {
366+
currentAssets.set(normalizedAsset, now + _10sec);
367+
} else {
368+
currentAssets.set(normalizedAsset, now);
369+
}
339370
}
340371

341372
const outputPath = compilation.getPath(compiler.outputPath, {});
@@ -346,19 +377,34 @@ class CleanPlugin {
346377
return keepFn(path);
347378
};
348379

380+
/**
381+
* @param {Error=} err err
382+
* @param {Set<string>=} diff diff
383+
*/
349384
const diffCallback = (err, diff) => {
350385
if (err) {
351386
oldAssets = undefined;
352-
return callback(err);
387+
callback(err);
388+
return;
353389
}
354-
applyDiff(fs, outputPath, dry, logger, diff, isKept, err => {
355-
if (err) {
356-
oldAssets = undefined;
357-
} else {
358-
oldAssets = currentAssets;
390+
applyDiff(
391+
fs,
392+
outputPath,
393+
dry,
394+
logger,
395+
diff,
396+
isKept,
397+
(err, notDeletedAssets) => {
398+
if (err) {
399+
oldAssets = undefined;
400+
} else {
401+
if (oldAssets) mergeAssets(currentAssets, oldAssets);
402+
oldAssets = currentAssets;
403+
if (notDeletedAssets) mergeAssets(oldAssets, notDeletedAssets);
404+
}
405+
callback(err);
359406
}
360-
callback(err);
361-
});
407+
);
362408
};
363409

364410
if (oldAssets) {

test/HotModuleReplacementPlugin.test.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,81 @@ describe("HotModuleReplacementPlugin", () => {
9999
});
100100
}, 120000);
101101

102+
it("output.clean=true should keep 1 last update", done => {
103+
const outputPath = path.join(__dirname, "js", "HotModuleReplacementPlugin");
104+
const entryFile = path.join(outputPath, "entry.js");
105+
const recordsFile = path.join(outputPath, "records.json");
106+
let step = 0;
107+
let firstUpdate;
108+
try {
109+
fs.mkdirSync(outputPath, { recursive: true });
110+
} catch (e) {
111+
// empty
112+
}
113+
fs.writeFileSync(entryFile, `${++step}`, "utf-8");
114+
const updates = new Set();
115+
const hasFile = file => {
116+
try {
117+
fs.statSync(path.join(outputPath, file));
118+
return true;
119+
} catch (err) {
120+
return false;
121+
}
122+
};
123+
const compiler = webpack({
124+
mode: "development",
125+
cache: false,
126+
entry: {
127+
0: entryFile
128+
},
129+
recordsPath: recordsFile,
130+
output: {
131+
path: outputPath,
132+
clean: true
133+
},
134+
plugins: [new webpack.HotModuleReplacementPlugin()]
135+
});
136+
const callback = (err, stats) => {
137+
if (err) return done(err);
138+
const jsonStats = stats.toJson();
139+
const hash = jsonStats.hash;
140+
const hmrUpdateMainFileName = `0.${hash}.hot-update.json`;
141+
142+
switch (step) {
143+
case 1:
144+
expect(updates.size).toBe(0);
145+
firstUpdate = hmrUpdateMainFileName;
146+
break;
147+
case 2:
148+
expect(updates.size).toBe(1);
149+
expect(updates.has(firstUpdate)).toBe(true);
150+
expect(hasFile(firstUpdate)).toBe(true);
151+
break;
152+
case 3:
153+
expect(updates.size).toBe(2);
154+
for (const file of updates) {
155+
expect(hasFile(file)).toBe(true);
156+
}
157+
return setTimeout(() => {
158+
fs.writeFileSync(entryFile, `${++step}`, "utf-8");
159+
compiler.run(err => {
160+
if (err) return done(err);
161+
for (const file of updates) {
162+
expect(hasFile(file)).toBe(false);
163+
}
164+
done();
165+
});
166+
}, 10100);
167+
}
168+
169+
updates.add(hmrUpdateMainFileName);
170+
fs.writeFileSync(entryFile, `${++step}`, "utf-8");
171+
compiler.run(callback);
172+
};
173+
174+
compiler.run(callback);
175+
}, 20000);
176+
102177
it("should correct working when entry is Object and key is a number", done => {
103178
const outputPath = path.join(__dirname, "js", "HotModuleReplacementPlugin");
104179
const entryFile = path.join(outputPath, "entry.js");

0 commit comments

Comments
 (0)