diff --git a/lib/HotModuleReplacementPlugin.js b/lib/HotModuleReplacementPlugin.js index 1ca848df1..307a7736c 100644 --- a/lib/HotModuleReplacementPlugin.js +++ b/lib/HotModuleReplacementPlugin.js @@ -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} */ + 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} */ - 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); diff --git a/lib/HotUpdateChunk.js b/lib/HotUpdateChunk.js index 4452deff9..d93983852 100644 --- a/lib/HotUpdateChunk.js +++ b/lib/HotUpdateChunk.js @@ -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)); } } diff --git a/lib/Template.js b/lib/Template.js index 8a6b25c1d..3b4538270 100644 --- a/lib/Template.js +++ b/lib/Template.js @@ -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 diff --git a/lib/javascript/JavascriptModulesPlugin.js b/lib/javascript/JavascriptModulesPlugin.js index db0c31b76..2da415d41 100644 --- a/lib/javascript/JavascriptModulesPlugin.js +++ b/lib/javascript/JavascriptModulesPlugin.js @@ -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); }); diff --git a/lib/util/runtime.js b/lib/util/runtime.js index 3f1c02621..a3120aee2 100644 --- a/lib/util/runtime.js +++ b/lib/util/runtime.js @@ -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() { diff --git a/test/hotCases/disposing/remove-chunk-with-shared/chunk1.js b/test/hotCases/disposing/remove-chunk-with-shared/chunk1.js new file mode 100644 index 000000000..1b3aa494d --- /dev/null +++ b/test/hotCases/disposing/remove-chunk-with-shared/chunk1.js @@ -0,0 +1,2 @@ +export * from "./shared"; +import.meta.webpackHot.accept("./shared"); diff --git a/test/hotCases/disposing/remove-chunk-with-shared/chunk2.js b/test/hotCases/disposing/remove-chunk-with-shared/chunk2.js new file mode 100644 index 000000000..1b3aa494d --- /dev/null +++ b/test/hotCases/disposing/remove-chunk-with-shared/chunk2.js @@ -0,0 +1,2 @@ +export * from "./shared"; +import.meta.webpackHot.accept("./shared"); diff --git a/test/hotCases/disposing/remove-chunk-with-shared/index.js b/test/hotCases/disposing/remove-chunk-with-shared/index.js new file mode 100644 index 000000000..42a431dcf --- /dev/null +++ b/test/hotCases/disposing/remove-chunk-with-shared/index.js @@ -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); +}); diff --git a/test/hotCases/disposing/remove-chunk-with-shared/module.js b/test/hotCases/disposing/remove-chunk-with-shared/module.js new file mode 100644 index 000000000..391a1a12e --- /dev/null +++ b/test/hotCases/disposing/remove-chunk-with-shared/module.js @@ -0,0 +1,3 @@ +export default import("./chunk2"); +--- +export default 42; diff --git a/test/hotCases/disposing/remove-chunk-with-shared/shared.js b/test/hotCases/disposing/remove-chunk-with-shared/shared.js new file mode 100644 index 000000000..28eccb6c1 --- /dev/null +++ b/test/hotCases/disposing/remove-chunk-with-shared/shared.js @@ -0,0 +1,5 @@ +export let active = true; + +import.meta.webpackHot.dispose(() => { + active = false; +});