From 1266d7695663ce523a4134061d746f1175e2e23c Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Mon, 25 Oct 2021 09:02:12 +0200 Subject: [PATCH] allow to merge multiple hmr updates which calling hot.check while previous update is ready --- lib/Compilation.js | 22 ++++++- lib/HotModuleReplacementPlugin.js | 14 +++++ lib/RuntimeGlobals.js | 5 ++ lib/RuntimePlugin.js | 54 ++++++++++------- lib/Template.js | 1 + lib/TemplatedPathPlugin.js | 4 ++ lib/config/defaults.js | 12 +++- lib/hmr/HotModuleReplacement.runtime.js | 59 ++++++++++++++++--- lib/hmr/HotModuleReplacementRuntimeModule.js | 1 + .../JavascriptHotModuleReplacement.runtime.js | 21 ++++--- lib/javascript/JavascriptModulesPlugin.js | 2 + lib/runtime/CompilationIndexRuntimeModule.js | 27 +++++++++ lib/runtime/GetChunkFilenameRuntimeModule.js | 2 + lib/runtime/GetMainFilenameRuntimeModule.js | 1 + .../AsyncWasmLoadingRuntimeModule.js | 1 + .../WasmChunkLoadingRuntimeModule.js | 1 + lib/web/JsonpChunkLoadingRuntimeModule.js | 11 ++-- test/hotCases/merge-updates/simple/errors1.js | 1 + test/hotCases/merge-updates/simple/index.js | 34 +++++++++++ test/hotCases/merge-updates/simple/module.js | 6 ++ test/hotCases/merge-updates/simple/module2.js | 6 ++ types.d.ts | 12 ++-- 22 files changed, 248 insertions(+), 49 deletions(-) create mode 100644 lib/runtime/CompilationIndexRuntimeModule.js create mode 100644 test/hotCases/merge-updates/simple/errors1.js create mode 100644 test/hotCases/merge-updates/simple/index.js create mode 100644 test/hotCases/merge-updates/simple/module.js create mode 100644 test/hotCases/merge-updates/simple/module2.js diff --git a/lib/Compilation.js b/lib/Compilation.js index 99807e58d..be2319d9d 100644 --- a/lib/Compilation.js +++ b/lib/Compilation.js @@ -276,6 +276,7 @@ const { isSourceEqual } = require("./util/source"); * @typedef {Object} PathData * @property {ChunkGraph=} chunkGraph * @property {string=} hash + * @property {string=} index * @property {function(number): string=} hashWithLength * @property {(Chunk|ChunkPathData)=} chunk * @property {(Module|ModulePathData)=} module @@ -831,7 +832,7 @@ BREAKING CHANGE: Asset processing hooks in Compilation has been merged into a si /** @type {SyncHook<[Chunk, string]>} */ chunkAsset: new SyncHook(["chunk", "filename"]), - /** @type {SyncWaterfallHook<[string, object, AssetInfo]>} */ + /** @type {SyncWaterfallHook<[string, PathData, AssetInfo]>} */ assetPath: new SyncWaterfallHook(["path", "options", "assetInfo"]), /** @type {SyncBailHook<[], boolean>} */ @@ -996,6 +997,12 @@ BREAKING CHANGE: Asset processing hooks in Compilation has been merged into a si /** @private @type {Map} */ this._modules = new Map(); this.records = null; + /** @type {string | undefined} */ + this.hash = undefined; + /** @type {string | undefined} */ + this.fullHash = undefined; + /** @type {number | undefined} */ + this.index = undefined; /** @type {string[]} */ this.additionalChunkAssets = []; /** @type {CompilationAssets} */ @@ -2781,6 +2788,11 @@ BREAKING CHANGE: Asset processing hooks in Compilation has been merged into a si ChunkGraph.setChunkGraphForModule(module, chunkGraph); } + this.index = + this.records && this.records.index !== undefined + ? this.records.index + 1 + : 0; + this.hooks.seal.call(); this.logger.time("optimize dependencies"); @@ -3056,6 +3068,7 @@ Or do you want to use the entrypoints '${name}' and '${runtime}' independently o this.summarizeDependencies(); if (shouldRecord) { + this.records.index = this.index; this.hooks.record.call(this, this.records); } @@ -4442,6 +4455,7 @@ This prevents using hashes of each other and should be avoided.`); try { manifest = this.getRenderManifest({ chunk, + index: `${this.index}`, hash: this.hash, fullHash: this.fullHash, outputOptions, @@ -4586,9 +4600,10 @@ This prevents using hashes of each other and should be avoided.`); * @returns {string} interpolated path */ getPath(filename, data = {}) { - if (!data.hash) { + if (!data.hash || !data.index) { data = { hash: this.hash, + index: `${this.index}`, ...data }; } @@ -4601,9 +4616,10 @@ This prevents using hashes of each other and should be avoided.`); * @returns {{ path: string, info: AssetInfo }} interpolated path and asset info */ getPathWithInfo(filename, data = {}) { - if (!data.hash) { + if (!data.hash || !data.index) { data = { hash: this.hash, + index: `${this.index}`, ...data }; } diff --git a/lib/HotModuleReplacementPlugin.js b/lib/HotModuleReplacementPlugin.js index 2e512fdbf..9fa91dc9c 100644 --- a/lib/HotModuleReplacementPlugin.js +++ b/lib/HotModuleReplacementPlugin.js @@ -23,6 +23,8 @@ const JavascriptParser = require("./javascript/JavascriptParser"); const { evaluateToIdentifier } = require("./javascript/JavascriptParserHelpers"); +const CompilationIndexRuntimeModule = require("./runtime/CompilationIndexRuntimeModule"); +const GetFullHashRuntimeModule = require("./runtime/GetFullHashRuntimeModule"); const { find, isSubset } = require("./util/SetHelpers"); const TupleSet = require("./util/TupleSet"); const { compareModulesById } = require("./util/comparators"); @@ -48,6 +50,10 @@ const { * @property {SyncBailHook<[TODO, string[]], void>} hotAcceptWithoutCallback */ +const isOutOfBandModule = module => + module instanceof CompilationIndexRuntimeModule || + module instanceof GetFullHashRuntimeModule; + /** @type {WeakMap} */ const parserHooksMap = new WeakMap(); @@ -342,6 +348,7 @@ class HotModuleReplacementPlugin { chunkGraph.getChunkFullHashModulesSet(chunk); if (fullHashModulesInThisChunk !== undefined) { for (const module of fullHashModulesInThisChunk) { + if (isOutOfBandModule(module)) continue; fullHashModules.add(module, chunk); } } @@ -350,6 +357,7 @@ class HotModuleReplacementPlugin { if (records.chunkModuleHashes) { if (fullHashModulesInThisChunk !== undefined) { for (const module of modules) { + if (isOutOfBandModule(module)) continue; const key = `${chunk.id}|${module.identifier()}`; const hash = getModuleHash(module); if ( @@ -370,6 +378,7 @@ class HotModuleReplacementPlugin { } } else { for (const module of modules) { + if (isOutOfBandModule(module)) continue; const key = `${chunk.id}|${module.identifier()}`; const hash = getModuleHash(module); if (records.chunkModuleHashes[key] !== hash) { @@ -381,6 +390,7 @@ class HotModuleReplacementPlugin { } else { if (fullHashModulesInThisChunk !== undefined) { for (const module of modules) { + if (isOutOfBandModule(module)) continue; const key = `${chunk.id}|${module.identifier()}`; const hash = getModuleHash(module); if ( @@ -452,6 +462,7 @@ class HotModuleReplacementPlugin { compilation.outputOptions.hotUpdateMainFilename, { hash: records.hash, + index: `${records.index}`, runtime } ); @@ -625,6 +636,7 @@ class HotModuleReplacementPlugin { chunk: hotUpdateChunk, hash: records.hash, fullHash: records.hash, + index: `${records.index}`, outputOptions: compilation.outputOptions, moduleTemplates: compilation.moduleTemplates, dependencyTemplates: compilation.dependencyTemplates, @@ -710,6 +722,8 @@ To fix this, make sure to include [runtime] in the output.hotUpdateMainFilename { removedChunkIds, removedModules, updatedChunkIds, assetInfo } ] of hotUpdateMainContentByFilename) { const hotUpdateMainJson = { + n: compilation.index, + h: compilation.hash, c: Array.from(updatedChunkIds), r: Array.from(removedChunkIds), m: diff --git a/lib/RuntimeGlobals.js b/lib/RuntimeGlobals.js index 79d5ad5c6..db110bd98 100644 --- a/lib/RuntimeGlobals.js +++ b/lib/RuntimeGlobals.js @@ -140,6 +140,11 @@ exports.nodeModuleDecorator = "__webpack_require__.nmd"; */ exports.getFullHash = "__webpack_require__.h"; +/** + * the compilation index + */ +exports.compilationIndex = "__webpack_require__.N"; + /** * an object containing all installed WebAssembly.Instance export objects keyed by module id */ diff --git a/lib/RuntimePlugin.js b/lib/RuntimePlugin.js index 5ab0b7d78..baffbefe3 100644 --- a/lib/RuntimePlugin.js +++ b/lib/RuntimePlugin.js @@ -12,6 +12,7 @@ const AsyncModuleRuntimeModule = require("./runtime/AsyncModuleRuntimeModule"); const AutoPublicPathRuntimeModule = require("./runtime/AutoPublicPathRuntimeModule"); const CompatGetDefaultExportRuntimeModule = require("./runtime/CompatGetDefaultExportRuntimeModule"); const CompatRuntimeModule = require("./runtime/CompatRuntimeModule"); +const CompilationIndexRuntimeModule = require("./runtime/CompilationIndexRuntimeModule"); const CreateFakeNamespaceObjectRuntimeModule = require("./runtime/CreateFakeNamespaceObjectRuntimeModule"); const CreateScriptUrlRuntimeModule = require("./runtime/CreateScriptUrlRuntimeModule"); const DefinePropertyGettersRuntimeModule = require("./runtime/DefinePropertyGettersRuntimeModule"); @@ -44,6 +45,7 @@ const GLOBALS_ON_REQUIRE = [ RuntimeGlobals.ensureChunk, RuntimeGlobals.entryModuleId, RuntimeGlobals.getFullHash, + RuntimeGlobals.compilationIndex, RuntimeGlobals.global, RuntimeGlobals.makeNamespaceObject, RuntimeGlobals.moduleCache, @@ -84,6 +86,16 @@ const TREE_DEPENDENCIES = { [RuntimeGlobals.shareScopeMap]: [RuntimeGlobals.hasOwnProperty] }; +const handleRuntimeModuleAssetPath = (assetPath, runtimeRequirements) => { + if (typeof assetPath !== "string") return; + if (/\[(full)?hash(:\d+)?\]/.test(assetPath)) { + runtimeRequirements.add(RuntimeGlobals.getFullHash); + } + if (/\[index\]/.test(assetPath)) { + runtimeRequirements.add(RuntimeGlobals.compilationIndex); + } +}; + class RuntimePlugin { /** * @param {Compiler} compiler the Compiler @@ -203,6 +215,15 @@ class RuntimePlugin { } return true; }); + compilation.hooks.runtimeRequirementInTree + .for(RuntimeGlobals.compilationIndex) + .tap("RuntimePlugin", chunk => { + compilation.addRuntimeModule( + chunk, + new CompilationIndexRuntimeModule(compilation.index) + ); + return true; + }); compilation.hooks.runtimeRequirementInTree .for(RuntimeGlobals.global) .tap("RuntimePlugin", chunk => { @@ -229,14 +250,10 @@ class RuntimePlugin { compilation.hooks.runtimeRequirementInTree .for(RuntimeGlobals.getChunkScriptFilename) .tap("RuntimePlugin", (chunk, set) => { - if ( - typeof compilation.outputOptions.chunkFilename === "string" && - /\[(full)?hash(:\d+)?\]/.test( - compilation.outputOptions.chunkFilename - ) - ) { - set.add(RuntimeGlobals.getFullHash); - } + handleRuntimeModuleAssetPath( + compilation.outputOptions.chunkFilename, + set + ); compilation.addRuntimeModule( chunk, new GetChunkFilenameRuntimeModule( @@ -256,12 +273,10 @@ class RuntimePlugin { compilation.hooks.runtimeRequirementInTree .for(RuntimeGlobals.getChunkUpdateScriptFilename) .tap("RuntimePlugin", (chunk, set) => { - if ( - /\[(full)?hash(:\d+)?\]/.test( - compilation.outputOptions.hotUpdateChunkFilename - ) - ) - set.add(RuntimeGlobals.getFullHash); + handleRuntimeModuleAssetPath( + compilation.outputOptions.hotUpdateChunkFilename, + set + ); compilation.addRuntimeModule( chunk, new GetChunkFilenameRuntimeModule( @@ -277,13 +292,10 @@ class RuntimePlugin { compilation.hooks.runtimeRequirementInTree .for(RuntimeGlobals.getUpdateManifestFilename) .tap("RuntimePlugin", (chunk, set) => { - if ( - /\[(full)?hash(:\d+)?\]/.test( - compilation.outputOptions.hotUpdateMainFilename - ) - ) { - set.add(RuntimeGlobals.getFullHash); - } + handleRuntimeModuleAssetPath( + compilation.outputOptions.hotUpdateMainFilename, + set + ); compilation.addRuntimeModule( chunk, new GetMainFilenameRuntimeModule( diff --git a/lib/Template.js b/lib/Template.js index 837aa6f97..98d22bbc9 100644 --- a/lib/Template.js +++ b/lib/Template.js @@ -41,6 +41,7 @@ const MATCH_PADDED_HYPHENS_REPLACE_REGEX = /^-|-$/g; /** * @typedef {Object} RenderManifestOptions * @property {Chunk} chunk the chunk used to render + * @property {string} index * @property {string} hash * @property {string} fullHash * @property {OutputOptions} outputOptions diff --git a/lib/TemplatedPathPlugin.js b/lib/TemplatedPathPlugin.js index 5cfa7e23c..ce679012c 100644 --- a/lib/TemplatedPathPlugin.js +++ b/lib/TemplatedPathPlugin.js @@ -147,6 +147,7 @@ const replacePathVariables = (path, data, assetInfo) => { // Placeholders // // [fullhash] - data.hash (3a4b5c6e7f) + // [index] - data.index (14) // // Legacy Placeholders // @@ -171,6 +172,9 @@ const replacePathVariables = (path, data, assetInfo) => { ) ); } + if (data.index) { + replacements.set("index", replacer(data.index)); + } // Chunk Context // diff --git a/lib/config/defaults.js b/lib/config/defaults.js index 8b77d2daa..57d54f0e7 100644 --- a/lib/config/defaults.js +++ b/lib/config/defaults.js @@ -808,9 +808,17 @@ const applyOutputDefaults = ( D( output, "hotUpdateChunkFilename", - `[id].[fullhash].hot-update.${output.module ? "mjs" : "js"}` + futureDefaults + ? `[id].[index].hot-update.${output.module ? "mjs" : "js"}` + : `[id].[fullhash].hot-update.${output.module ? "mjs" : "js"}` + ); + D( + output, + "hotUpdateMainFilename", + futureDefaults + ? "[runtime].[index].hot-update.json" + : "[runtime].[fullhash].hot-update.json" ); - D(output, "hotUpdateMainFilename", "[runtime].[fullhash].hot-update.json"); D(output, "crossOriginLoading", false); F(output, "scriptType", () => (output.module ? "module" : false)); D( diff --git a/lib/hmr/HotModuleReplacement.runtime.js b/lib/hmr/HotModuleReplacement.runtime.js index 2b79e4122..0126b515a 100644 --- a/lib/hmr/HotModuleReplacement.runtime.js +++ b/lib/hmr/HotModuleReplacement.runtime.js @@ -8,6 +8,10 @@ var $interceptModuleExecution$ = undefined; var $moduleCache$ = undefined; // eslint-disable-next-line no-unused-vars +var $getFullHash$ = undefined; +// eslint-disable-next-line no-unused-vars +var $compilationIndex$ = undefined; +// eslint-disable-next-line no-unused-vars var $hmrModuleData$ = undefined; /** @type {() => Promise} */ var $hmrDownloadManifest$ = undefined; @@ -26,12 +30,15 @@ module.exports = function () { // status var registeredStatusHandlers = []; var currentStatus = "idle"; + var additionalUpdate = false; // while downloading var blockingPromises; // The update info var currentUpdateApplyHandlers; + var currentUpdateCompilationIndex; + var currentUpdateCompilationHash; var queuedInvalidatedModules; // eslint-disable-next-line no-unused-vars @@ -200,6 +207,12 @@ module.exports = function () { var idx = registeredStatusHandlers.indexOf(l); if (idx >= 0) registeredStatusHandlers.splice(idx, 1); }, + getUpdateIndex: function () { + return currentUpdateCompilationIndex; + }, + getUpdateHash: function () { + return currentUpdateCompilationHash; + }, //inherit from previous dispose call data: currentModuleData[moduleId] @@ -230,6 +243,9 @@ module.exports = function () { case "prepare": blockingPromises.push(promise); return promise; + case "check": + if (additionalUpdate) blockingPromises.push(promise); + return promise; default: return promise; } @@ -244,25 +260,51 @@ module.exports = function () { }); } - function hotCheck(applyOnUpdate) { - if (currentStatus !== "idle") { - throw new Error("check() is only allowed in idle status"); + function applyOutOfBandInfo() { + if (currentUpdateCompilationIndex !== undefined) { + $compilationIndex$ = currentUpdateCompilationIndex; + currentUpdateCompilationIndex = undefined; } + if (currentUpdateCompilationHash !== undefined) { + var value = currentUpdateCompilationHash; + $getFullHash$ = function () { + return value; + }; + currentUpdateCompilationHash = undefined; + } + } + + function hotCheck(applyOnUpdate) { + if (currentStatus !== "idle" && currentStatus !== "ready") { + throw new Error("check() is only allowed in idle or ready status"); + } + additionalUpdate = currentStatus === "ready"; + // apply outstanding compilation index update to get the new hot update files + applyOutOfBandInfo(); return setStatus("check") .then($hmrDownloadManifest$) .then(function (update) { if (!update) { - return setStatus(applyInvalidatedModules() ? "ready" : "idle").then( - function () { - return null; + return waitForBlockingPromises(function () { + additionalUpdate = applyInvalidatedModules() || additionalUpdate; + if (additionalUpdate && applyOnUpdate) { + return internalApply(applyOnUpdate); + } else { + return setStatus(additionalUpdate ? "ready" : "idle").then( + function () { + return null; + } + ); } - ); + }); } return setStatus("prepare").then(function () { var updatedModules = []; blockingPromises = []; - currentUpdateApplyHandlers = []; + if (!currentUpdateApplyHandlers) currentUpdateApplyHandlers = []; + currentUpdateCompilationIndex = update.n; + currentUpdateCompilationHash = update.h; return Promise.all( Object.keys($hmrDownloadUpdateHandlers$).reduce(function ( @@ -308,6 +350,7 @@ module.exports = function () { options = options || {}; applyInvalidatedModules(); + applyOutOfBandInfo(); var results = currentUpdateApplyHandlers.map(function (handler) { return handler(options); diff --git a/lib/hmr/HotModuleReplacementRuntimeModule.js b/lib/hmr/HotModuleReplacementRuntimeModule.js index a92a97e9e..7e659742f 100644 --- a/lib/hmr/HotModuleReplacementRuntimeModule.js +++ b/lib/hmr/HotModuleReplacementRuntimeModule.js @@ -21,6 +21,7 @@ class HotModuleReplacementRuntimeModule extends RuntimeModule { require("./HotModuleReplacement.runtime.js") ) .replace(/\$getFullHash\$/g, RuntimeGlobals.getFullHash) + .replace(/\$compilationIndex\$/g, RuntimeGlobals.compilationIndex) .replace( /\$interceptModuleExecution\$/g, RuntimeGlobals.interceptModuleExecution diff --git a/lib/hmr/JavascriptHotModuleReplacement.runtime.js b/lib/hmr/JavascriptHotModuleReplacement.runtime.js index d03e93948..5cf857345 100644 --- a/lib/hmr/JavascriptHotModuleReplacement.runtime.js +++ b/lib/hmr/JavascriptHotModuleReplacement.runtime.js @@ -295,6 +295,7 @@ module.exports = function () { for (var i = 0; i < currentUpdateRuntime.length; i++) { currentUpdateRuntime[i](__webpack_require__); } + currentUpdateRuntime = undefined; // call accept handlers for (var outdatedModuleId in outdatedDependencies) { @@ -428,14 +429,20 @@ module.exports = function () { applyHandlers, updatedModulesList ) { - applyHandlers.push(applyHandler); + if (!currentUpdate) { + currentUpdate = {}; + currentUpdateRuntime = []; + currentUpdateRemovedChunks = []; + applyHandlers.push(applyHandler); + } currentUpdateChunks = {}; - currentUpdateRemovedChunks = removedChunks; - currentUpdate = removedModules.reduce(function (obj, key) { - obj[key] = false; - return obj; - }, {}); - currentUpdateRuntime = []; + currentUpdateRemovedChunks.push.apply( + currentUpdateRemovedChunks, + removedChunks + ); + removedModules.forEach(function (key) { + currentUpdate[key] = false; + }); chunkIds.forEach(function (chunkId) { if ( $hasOwnProperty$($installedChunks$, chunkId) && diff --git a/lib/javascript/JavascriptModulesPlugin.js b/lib/javascript/JavascriptModulesPlugin.js index b9f1c1dfc..37c958c51 100644 --- a/lib/javascript/JavascriptModulesPlugin.js +++ b/lib/javascript/JavascriptModulesPlugin.js @@ -231,6 +231,7 @@ class JavascriptModulesPlugin { "JavascriptModulesPlugin", (result, options) => { const { + index, hash, chunk, chunkGraph, @@ -305,6 +306,7 @@ class JavascriptModulesPlugin { filenameTemplate, pathOptions: { hash, + index, runtime: chunk.runtime, chunk, contentHashType: "javascript" diff --git a/lib/runtime/CompilationIndexRuntimeModule.js b/lib/runtime/CompilationIndexRuntimeModule.js new file mode 100644 index 000000000..669797089 --- /dev/null +++ b/lib/runtime/CompilationIndexRuntimeModule.js @@ -0,0 +1,27 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php +*/ + +"use strict"; + +const RuntimeGlobals = require("../RuntimeGlobals"); +const RuntimeModule = require("../RuntimeModule"); + +class CompilationIndexRuntimeModule extends RuntimeModule { + /** + * @param {number} compilationIndex the compilation index + */ + constructor(compilationIndex) { + super("compilation index"); + this.compilationIndex = compilationIndex; + } + + /** + * @returns {string} runtime code + */ + generate() { + return `${RuntimeGlobals.compilationIndex} = ${this.compilationIndex};`; + } +} + +module.exports = CompilationIndexRuntimeModule; diff --git a/lib/runtime/GetChunkFilenameRuntimeModule.js b/lib/runtime/GetChunkFilenameRuntimeModule.js index d077cb57b..bd635e6a6 100644 --- a/lib/runtime/GetChunkFilenameRuntimeModule.js +++ b/lib/runtime/GetChunkFilenameRuntimeModule.js @@ -144,6 +144,7 @@ class GetChunkFilenameRuntimeModule extends RuntimeModule { ) : JSON.stringify(chunkFilename); const staticChunkFilename = compilation.getPath(chunkFilenameValue, { + index: `" + ${RuntimeGlobals.compilationIndex} + "`, hash: `" + ${RuntimeGlobals.getFullHash}() + "`, hashWithLength: length => `" + ${RuntimeGlobals.getFullHash}().slice(0, ${length}) + "`, @@ -229,6 +230,7 @@ class GetChunkFilenameRuntimeModule extends RuntimeModule { const url = dynamicFilename && compilation.getPath(JSON.stringify(dynamicFilename), { + index: `" + ${RuntimeGlobals.compilationIndex} + "`, hash: `" + ${RuntimeGlobals.getFullHash}() + "`, hashWithLength: length => `" + ${RuntimeGlobals.getFullHash}().slice(0, ${length}) + "`, diff --git a/lib/runtime/GetMainFilenameRuntimeModule.js b/lib/runtime/GetMainFilenameRuntimeModule.js index cd9a6937b..2d374c3e4 100644 --- a/lib/runtime/GetMainFilenameRuntimeModule.js +++ b/lib/runtime/GetMainFilenameRuntimeModule.js @@ -29,6 +29,7 @@ class GetMainFilenameRuntimeModule extends RuntimeModule { const { global, filename, compilation, chunk } = this; const { runtimeTemplate } = compilation; const url = compilation.getPath(JSON.stringify(filename), { + index: `" + ${RuntimeGlobals.compilationIndex} + "`, hash: `" + ${RuntimeGlobals.getFullHash}() + "`, hashWithLength: length => `" + ${RuntimeGlobals.getFullHash}().slice(0, ${length}) + "`, diff --git a/lib/wasm-async/AsyncWasmLoadingRuntimeModule.js b/lib/wasm-async/AsyncWasmLoadingRuntimeModule.js index 3e275fa19..b235f197b 100644 --- a/lib/wasm-async/AsyncWasmLoadingRuntimeModule.js +++ b/lib/wasm-async/AsyncWasmLoadingRuntimeModule.js @@ -26,6 +26,7 @@ class AsyncWasmLoadingRuntimeModule extends RuntimeModule { const wasmModuleSrcPath = compilation.getPath( JSON.stringify(outputOptions.webassemblyModuleFilename), { + index: `" + ${RuntimeGlobals.compilationIndex} + "`, hash: `" + ${RuntimeGlobals.getFullHash}() + "`, hashWithLength: length => `" + ${RuntimeGlobals.getFullHash}}().slice(0, ${length}) + "`, diff --git a/lib/wasm-sync/WasmChunkLoadingRuntimeModule.js b/lib/wasm-sync/WasmChunkLoadingRuntimeModule.js index e956b750a..0a345c5f0 100644 --- a/lib/wasm-sync/WasmChunkLoadingRuntimeModule.js +++ b/lib/wasm-sync/WasmChunkLoadingRuntimeModule.js @@ -233,6 +233,7 @@ class WasmChunkLoadingRuntimeModule extends RuntimeModule { const wasmModuleSrcPath = compilation.getPath( JSON.stringify(outputOptions.webassemblyModuleFilename), { + index: `" + ${RuntimeGlobals.compilationIndex} + "`, hash: `" + ${RuntimeGlobals.getFullHash}() + "`, hashWithLength: length => `" + ${RuntimeGlobals.getFullHash}}().slice(0, ${length}) + "`, diff --git a/lib/web/JsonpChunkLoadingRuntimeModule.js b/lib/web/JsonpChunkLoadingRuntimeModule.js index 39d3f252a..a8450903f 100644 --- a/lib/web/JsonpChunkLoadingRuntimeModule.js +++ b/lib/web/JsonpChunkLoadingRuntimeModule.js @@ -274,9 +274,8 @@ class JsonpChunkLoadingRuntimeModule extends RuntimeModule { "", withHmr ? Template.asString([ - "var currentUpdatedModulesList;", "var waitingUpdateResolves = {};", - "function loadUpdateChunk(chunkId) {", + "function loadUpdateChunk(chunkId, updatedModulesList) {", Template.indent([ `return new Promise(${runtimeTemplate.basicFunction( "resolve, reject", @@ -302,6 +301,9 @@ class JsonpChunkLoadingRuntimeModule extends RuntimeModule { ])};`, `${RuntimeGlobals.loadScript}(url, loadingEnded);` ] + )}).then(${runtimeTemplate.basicFunction( + "updatedModules", + "if(updatedModulesList) updatedModulesList.push.apply(updatedModulesList, updatedModules);" )});` ]), "}", @@ -311,12 +313,13 @@ class JsonpChunkLoadingRuntimeModule extends RuntimeModule { )}] = ${runtimeTemplate.basicFunction( "chunkId, moreModules, runtime", [ + "var updatedModules = [];", "for(var moduleId in moreModules) {", Template.indent([ `if(${RuntimeGlobals.hasOwnProperty}(moreModules, moduleId)) {`, Template.indent([ "currentUpdate[moduleId] = moreModules[moduleId];", - "if(currentUpdatedModulesList) currentUpdatedModulesList.push(moduleId);" + "updatedModules.push(moduleId);" ]), "}" ]), @@ -324,7 +327,7 @@ class JsonpChunkLoadingRuntimeModule extends RuntimeModule { "if(runtime) currentUpdateRuntime.push(runtime);", "if(waitingUpdateResolves[chunkId]) {", Template.indent([ - "waitingUpdateResolves[chunkId]();", + "waitingUpdateResolves[chunkId](updatedModules);", "waitingUpdateResolves[chunkId] = undefined;" ]), "}" diff --git a/test/hotCases/merge-updates/simple/errors1.js b/test/hotCases/merge-updates/simple/errors1.js new file mode 100644 index 000000000..534e9facc --- /dev/null +++ b/test/hotCases/merge-updates/simple/errors1.js @@ -0,0 +1 @@ +module.exports = [[/Module parse failed/]]; diff --git a/test/hotCases/merge-updates/simple/index.js b/test/hotCases/merge-updates/simple/index.js new file mode 100644 index 000000000..0feb78d67 --- /dev/null +++ b/test/hotCases/merge-updates/simple/index.js @@ -0,0 +1,34 @@ +import value from "./module"; +import value2 from "./module2"; + +import.meta.webpackHot.accept("./module"); +import.meta.webpackHot.accept("./module2"); + +it("should merge multiple updates when not using apply", done => { + expect(value).toBe(42); + expect(value2).toBe(42); + NEXT(err => { + if (err) return done(err); + NEXT(err => { + if (err) return done(err); + module.hot + .check() + .then(updatedModules => { + expect(updatedModules).toEqual(["./module.js", "./module2.js"]); + return module.hot.check(true).then(updatedModules => { + expect(updatedModules).toEqual(["./module.js", "./module2.js"]); + try { + expect(value).toBe(43); + expect(value2).toBe(43); + } catch (e) { + return done(e); + } + done(); + }); + }) + .catch(err => { + done(err); + }); + }); + }); +}); diff --git a/test/hotCases/merge-updates/simple/module.js b/test/hotCases/merge-updates/simple/module.js new file mode 100644 index 000000000..6d3dc2874 --- /dev/null +++ b/test/hotCases/merge-updates/simple/module.js @@ -0,0 +1,6 @@ +export default 42; +--- +throw new Error("Invalid") +export default 0; +--- +export default 43 diff --git a/test/hotCases/merge-updates/simple/module2.js b/test/hotCases/merge-updates/simple/module2.js new file mode 100644 index 000000000..75016a225 --- /dev/null +++ b/test/hotCases/merge-updates/simple/module2.js @@ -0,0 +1,6 @@ +export default 42; +--- +}}} +export default 0; +--- +export default 43 diff --git a/types.d.ts b/types.d.ts index 94156d9cf..17e697ead 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1424,7 +1424,7 @@ declare class Compilation { chunkHash: SyncHook<[Chunk, Hash, ChunkHashContext]>; moduleAsset: SyncHook<[Module, string]>; chunkAsset: SyncHook<[Chunk, string]>; - assetPath: SyncWaterfallHook<[string, object, AssetInfo]>; + assetPath: SyncWaterfallHook<[string, PathData, AssetInfo]>; needAdditionalPass: SyncBailHook<[], boolean>; childCompiler: SyncHook<[Compiler, string, number]>; log: SyncBailHook<[string, LogEntry], true>; @@ -1491,6 +1491,9 @@ declare class Compilation { namedChunks: Map; modules: Set; records: any; + hash?: string; + fullHash?: string; + index?: number; additionalChunkAssets: string[]; assets: CompilationAssets; assetsInfo: Map; @@ -1667,8 +1670,6 @@ declare class Compilation { runtime: RuntimeSpec; runtimes: RuntimeSpec[]; }[]; - fullHash?: string; - hash?: string; emitAsset(file: string, source: Source, assetInfo?: AssetInfo): void; updateAsset( file: string, @@ -8611,6 +8612,7 @@ declare interface ParserStateBase { declare interface PathData { chunkGraph?: ChunkGraph; hash?: string; + index?: string; hashWithLength?: (arg0: number) => string; chunk?: Chunk | ChunkPathData; module?: Module | ModulePathData; @@ -9056,6 +9058,7 @@ declare interface RenderManifestOptions { * the chunk used to render */ chunk: Chunk; + index: string; hash: string; fullHash: string; outputOptions: Output; @@ -11419,7 +11422,7 @@ declare interface UpdateHashContextGenerator { chunkGraph: ChunkGraph; runtime: RuntimeSpec; } -type UsageStateType = 0 | 1 | 2 | 3 | 4; +type UsageStateType = 0 | 2 | 3 | 1 | 4; declare interface UserResolveOptions { /** * A list of module alias configurations or an object which maps key to value @@ -12179,6 +12182,7 @@ declare namespace exports { export let harmonyModuleDecorator: string; export let nodeModuleDecorator: string; export let getFullHash: string; + export let compilationIndex: string; export let wasmInstances: string; export let instantiateWasm: string; export let uncaughtErrorHandler: string;