fix incorrect disposing of modules during HMR

This commit is contained in:
Tobias Koppers 2020-09-04 00:19:39 +02:00
parent 15638abfac
commit 12997f0a6c
10 changed files with 140 additions and 116 deletions

View File

@ -27,6 +27,7 @@ const {
const { find } = require("./util/SetHelpers");
const TupleSet = require("./util/TupleSet");
const { compareModulesById } = require("./util/comparators");
const { getRuntimeKey, keyToRuntime } = require("./util/runtime");
/** @typedef {import("./Chunk")} Chunk */
/** @typedef {import("./Compilation").AssetInfo} AssetInfo */
@ -298,8 +299,10 @@ class HotModuleReplacementPlugin {
records.fullHashChunkModuleHashes = fullHashChunkModuleHashes;
records.chunkModuleHashes = chunkModuleHashes;
records.chunkHashs = {};
records.chunkRuntime = {};
for (const chunk of compilation.chunks) {
records.chunkHashs[chunk.id] = chunk.hash;
records.chunkRuntime[chunk.id] = getRuntimeKey(chunk.runtime);
}
records.chunkModuleIds = {};
for (const chunk of compilation.chunks) {
@ -403,104 +406,126 @@ class HotModuleReplacementPlugin {
r: [],
m: undefined
};
// Create a list of all active modules to verify which modules are removed completely
/** @type {Map<number|string, Module>} */
const allModules = new Map();
for (const module of compilation.modules) {
allModules.set(chunkGraph.getModuleId(module), module);
}
// List of completely removed modules
const allRemovedModules = new Set();
for (const key of Object.keys(records.chunkHashs)) {
// Check which modules are completely removed
for (const id of records.chunkModuleIds[key]) {
if (!allModules.has(id)) {
allRemovedModules.add(id);
}
}
let chunkId;
let newModules;
let newRuntimeModules;
let newFullHashModules;
let newRuntime;
const currentChunk = find(
compilation.chunks,
chunk => `${chunk.id}` === key
);
if (currentChunk) {
const chunkId = currentChunk.id;
const newModules = chunkGraph
chunkId = currentChunk.id;
newRuntime = currentChunk.runtime;
newModules = chunkGraph
.getChunkModules(currentChunk)
.filter(module => updatedModules.has(module, currentChunk));
const newRuntimeModules = Array.from(
newRuntimeModules = Array.from(
chunkGraph.getChunkRuntimeModulesIterable(currentChunk)
).filter(module => updatedModules.has(module, currentChunk));
const fullHashModules = chunkGraph.getChunkFullHashModulesIterable(
currentChunk
);
const newFullHashModules =
newFullHashModules =
fullHashModules &&
Array.from(fullHashModules).filter(module =>
updatedModules.has(module, currentChunk)
);
/** @type {Set<number|string>} */
const allModules = new Set();
for (const module of chunkGraph.getChunkModulesIterable(
currentChunk
)) {
allModules.add(chunkGraph.getModuleId(module));
}
const removedModules = records.chunkModuleIds[chunkId].filter(
id => !allModules.has(id)
);
if (
newModules.length > 0 ||
newRuntimeModules.length > 0 ||
removedModules.length > 0
) {
const hotUpdateChunk = new HotUpdateChunk();
ChunkGraph.setChunkGraphForChunk(hotUpdateChunk, chunkGraph);
hotUpdateChunk.id = chunkId;
hotUpdateChunk.runtime = currentChunk.runtime;
chunkGraph.attachModules(hotUpdateChunk, newModules);
chunkGraph.attachRuntimeModules(
hotUpdateChunk,
newRuntimeModules
);
if (newFullHashModules) {
chunkGraph.attachFullHashModules(
hotUpdateChunk,
newFullHashModules
);
} else {
chunkId = `${+key}` === key ? +key : key;
hotUpdateMainContent.r.push(chunkId);
const runtime = keyToRuntime(records.chunkRuntime[key]);
for (const id of records.chunkModuleIds[key]) {
const module = allModules.get(id);
if (!module) continue;
const hash = chunkGraph.getModuleHash(module, runtime);
const moduleKey = `${key}|${module.identifier()}`;
if (hash !== records.chunkModuleHashes[moduleKey]) {
newModules = newModules || [];
newModules.push(module);
}
hotUpdateChunk.removedModules = removedModules;
const renderManifest = compilation.getRenderManifest({
chunk: hotUpdateChunk,
hash: records.hash,
fullHash: records.hash,
outputOptions: compilation.outputOptions,
moduleTemplates: compilation.moduleTemplates,
dependencyTemplates: compilation.dependencyTemplates,
codeGenerationResults: compilation.codeGenerationResults,
runtimeTemplate: compilation.runtimeTemplate,
moduleGraph: compilation.moduleGraph,
chunkGraph
}
}
if (
(newModules && newModules.length > 0) ||
(newRuntimeModules && newRuntimeModules.length > 0)
) {
const hotUpdateChunk = new HotUpdateChunk();
ChunkGraph.setChunkGraphForChunk(hotUpdateChunk, chunkGraph);
hotUpdateChunk.id = chunkId;
hotUpdateChunk.runtime = newRuntime;
chunkGraph.attachModules(hotUpdateChunk, newModules || []);
chunkGraph.attachRuntimeModules(
hotUpdateChunk,
newRuntimeModules || []
);
if (newFullHashModules) {
chunkGraph.attachFullHashModules(
hotUpdateChunk,
newFullHashModules
);
}
const renderManifest = compilation.getRenderManifest({
chunk: hotUpdateChunk,
hash: records.hash,
fullHash: records.hash,
outputOptions: compilation.outputOptions,
moduleTemplates: compilation.moduleTemplates,
dependencyTemplates: compilation.dependencyTemplates,
codeGenerationResults: compilation.codeGenerationResults,
runtimeTemplate: compilation.runtimeTemplate,
moduleGraph: compilation.moduleGraph,
chunkGraph
});
for (const entry of renderManifest) {
/** @type {string} */
let filename;
/** @type {AssetInfo} */
let assetInfo;
if ("filename" in entry) {
filename = entry.filename;
assetInfo = entry.info;
} else {
({
path: filename,
info: assetInfo
} = compilation.getPathWithInfo(
entry.filenameTemplate,
entry.pathOptions
));
}
const source = entry.render();
compilation.additionalChunkAssets.push(filename);
compilation.emitAsset(filename, source, {
hotModuleReplacement: true,
...assetInfo
});
for (const entry of renderManifest) {
/** @type {string} */
let filename;
/** @type {AssetInfo} */
let assetInfo;
if ("filename" in entry) {
filename = entry.filename;
assetInfo = entry.info;
} else {
({
path: filename,
info: assetInfo
} = compilation.getPathWithInfo(
entry.filenameTemplate,
entry.pathOptions
));
}
const source = entry.render();
compilation.additionalChunkAssets.push(filename);
compilation.emitAsset(filename, source, {
hotModuleReplacement: true,
...assetInfo
});
if (currentChunk) {
currentChunk.files.add(filename);
compilation.hooks.chunkAsset.call(currentChunk, filename);
}
hotUpdateMainContent.c.push(chunkId);
}
} else {
const chunkId = `${+key}` === key ? +key : key;
hotUpdateMainContent.r.push(chunkId);
for (const id of records.chunkModuleIds[chunkId])
allRemovedModules.add(id);
hotUpdateMainContent.c.push(chunkId);
}
}
hotUpdateMainContent.m = Array.from(allRemovedModules);

View File

@ -13,18 +13,6 @@ const Chunk = require("./Chunk");
class HotUpdateChunk extends Chunk {
constructor() {
super();
/** @type {(string|number)[]} */
this.removedModules = undefined;
}
/**
* @param {Hash} hash hash (will be modified)
* @param {ChunkGraph} chunkGraph the chunk graph
* @returns {void}
*/
updateHash(hash, chunkGraph) {
super.updateHash(hash, chunkGraph);
hash.update(JSON.stringify(this.removedModules));
}
}

View File

@ -6,8 +6,6 @@
"use strict";
const { ConcatSource, PrefixSource } = require("webpack-sources");
const HotUpdateChunk = require("./HotUpdateChunk");
const { compareIds } = require("./util/comparators");
/** @typedef {import("webpack-sources").ConcatSource} ConcatSource */
/** @typedef {import("webpack-sources").Source} Source */
@ -292,16 +290,9 @@ class Template {
* @returns {Source} rendered chunk modules in a Source object
*/
static renderChunkModules(renderContext, modules, renderModule, prefix = "") {
const { chunk, chunkGraph } = renderContext;
const { chunkGraph } = renderContext;
var source = new ConcatSource();
let removedModules;
if (chunk instanceof HotUpdateChunk) {
removedModules = chunk.removedModules;
}
if (
modules.length === 0 &&
(!removedModules || removedModules.length === 0)
) {
if (modules.length === 0) {
return null;
}
/** @type {{id: string|number, source: Source|string}[]} */
@ -311,15 +302,6 @@ class Template {
source: renderModule(module) || "false"
};
});
if (removedModules && removedModules.length > 0) {
removedModules.sort(compareIds);
for (const id of removedModules) {
allModules.push({
id,
source: "false"
});
}
}
const bounds = Template.getModulesArrayBounds(allModules);
if (bounds) {
// Render a spare array

View File

@ -320,7 +320,6 @@ class JavascriptModulesPlugin {
hashFunction
}
} = compilation;
const hotUpdateChunk = chunk instanceof HotUpdateChunk ? chunk : null;
const hash = createHash(hashFunction);
if (hashSalt) hash.update(hashSalt);
hash.update(`${chunk.id} `);
@ -352,9 +351,6 @@ class JavascriptModulesPlugin {
}
xor.updateHash(hash);
}
if (hotUpdateChunk) {
hash.update(JSON.stringify(hotUpdateChunk.removedModules));
}
const digest = /** @type {string} */ (hash.digest(hashDigest));
chunk.contentHash.javascript = digest.substr(0, hashDigestLength);
});

View File

@ -83,6 +83,18 @@ const getRuntimeKey = runtime => {
};
exports.getRuntimeKey = getRuntimeKey;
/**
* @param {string} key key of runtimes
* @returns {RuntimeSpec} runtime(s)
*/
const keyToRuntime = key => {
if (key === "*") return undefined;
const items = key.split("\n");
if (items.length === 1) return items[0];
return new SortableSet(items);
};
exports.keyToRuntime = keyToRuntime;
const getRuntimesString = set => {
set.sort();
return Array.from(set).join("+");
@ -314,12 +326,7 @@ class RuntimeSpecMap {
}
keys() {
return Array.from(this._map.keys(), key => {
if (key === "*") return undefined;
const items = key.split("\n");
if (items.length === 1) return items[0];
return new SortableSet(items);
});
return Array.from(this._map.keys(), keyToRuntime);
}
values() {

View File

@ -0,0 +1,2 @@
export * from "./shared";
import.meta.webpackHot.accept("./shared");

View File

@ -0,0 +1,2 @@
export * from "./shared";
import.meta.webpackHot.accept("./shared");

View File

@ -0,0 +1,14 @@
import module from "./module";
it("should not disposed shared modules when a chunk is removed", done => {
import("./chunk1").then(chunk1 => {
import.meta.webpackHot.accept("./module", async () => {
expect(module).toBe(42);
expect(chunk1).toMatchObject({
active: true
});
done();
});
NEXT(require("../../update")(done));
}, done);
});

View File

@ -0,0 +1,3 @@
export default import("./chunk2");
---
export default 42;

View File

@ -0,0 +1,5 @@
export let active = true;
import.meta.webpackHot.dispose(() => {
active = false;
});