feat: HMR support for ESM
Github Actions / lint (push) Waiting to run Details
Github Actions / validate-legacy-node (push) Waiting to run Details
Github Actions / benchmark (1/4) (push) Waiting to run Details
Github Actions / benchmark (2/4) (push) Waiting to run Details
Github Actions / benchmark (3/4) (push) Waiting to run Details
Github Actions / benchmark (4/4) (push) Waiting to run Details
Github Actions / basic (push) Waiting to run Details
Github Actions / unit (push) Waiting to run Details
Github Actions / integration (10.x, macos-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (10.x, macos-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (10.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (10.x, ubuntu-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (10.x, windows-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (10.x, windows-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (12.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (14.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (16.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (18.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (20.x, macos-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (20.x, macos-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (20.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (20.x, ubuntu-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (20.x, windows-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (20.x, windows-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (22.x, macos-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (22.x, macos-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (22.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (22.x, ubuntu-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (22.x, windows-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (22.x, windows-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (24.x, macos-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (24.x, macos-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (24.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (24.x, ubuntu-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (24.x, windows-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (24.x, windows-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (lts/*, ubuntu-latest, a, 1) (push) Blocked by required conditions Details
Github Actions / integration (lts/*, ubuntu-latest, b, 1) (push) Blocked by required conditions Details

This commit is contained in:
Natsu Xiao 2025-06-23 20:22:23 +08:00 committed by GitHub
parent 605897418a
commit 09fda8730d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 578 additions and 117 deletions

View File

@ -6,8 +6,7 @@
"use strict"; "use strict";
const { ConcatSource } = require("webpack-sources"); const { ConcatSource } = require("webpack-sources");
const { RuntimeGlobals } = require(".."); const { RuntimeGlobals, HotUpdateChunk } = require("..");
const HotUpdateChunk = require("../HotUpdateChunk");
const Template = require("../Template"); const Template = require("../Template");
const { getAllChunks } = require("../javascript/ChunkHelpers"); const { getAllChunks } = require("../javascript/ChunkHelpers");
const { const {
@ -72,30 +71,47 @@ class ModuleChunkFormatPlugin {
hooks.renderChunk.tap(PLUGIN_NAME, (modules, renderContext) => { hooks.renderChunk.tap(PLUGIN_NAME, (modules, renderContext) => {
const { chunk, chunkGraph, runtimeTemplate } = renderContext; const { chunk, chunkGraph, runtimeTemplate } = renderContext;
const hotUpdateChunk = chunk instanceof HotUpdateChunk ? chunk : null; const hotUpdateChunk = chunk instanceof HotUpdateChunk ? chunk : null;
const source = new ConcatSource(); const source = new ConcatSource();
source.add(
`export const __webpack_id__ = ${JSON.stringify(chunk.id)};\n`
);
source.add(
`export const __webpack_ids__ = ${JSON.stringify(chunk.ids)};\n`
);
source.add("export const __webpack_modules__ = ");
source.add(modules);
source.add(";\n");
const runtimeModules = chunkGraph.getChunkRuntimeModulesInOrder(chunk);
if (runtimeModules.length > 0) {
source.add("export const __webpack_runtime__ =\n");
source.add(
Template.renderChunkRuntimeModules(runtimeModules, renderContext)
);
}
if (hotUpdateChunk) { if (hotUpdateChunk) {
throw new Error("HMR is not implemented for module chunk format yet"); return source;
} else { }
source.add( const { entries, runtimeChunk } = getChunkInfo(chunk, chunkGraph);
`export const __webpack_id__ = ${JSON.stringify(chunk.id)};\n` if (runtimeChunk) {
); const currentOutputName = compilation
source.add( .getPath(
`export const __webpack_ids__ = ${JSON.stringify(chunk.ids)};\n` getChunkFilenameTemplate(chunk, compilation.outputOptions),
); {
source.add("export const __webpack_modules__ = "); chunk,
source.add(modules); contentHashType: "javascript"
source.add(";\n"); }
const runtimeModules = )
chunkGraph.getChunkRuntimeModulesInOrder(chunk); .replace(/^\/+/g, "")
if (runtimeModules.length > 0) { .split("/");
source.add("export const __webpack_runtime__ =\n");
source.add( /**
Template.renderChunkRuntimeModules(runtimeModules, renderContext) * @param {Chunk} chunk the chunk
); * @returns {string} the relative path
} */
const { entries, runtimeChunk } = getChunkInfo(chunk, chunkGraph); const getRelativePath = chunk => {
if (runtimeChunk) { const baseOutputName = currentOutputName.slice();
const currentOutputName = compilation const chunkOutputName = compilation
.getPath( .getPath(
getChunkFilenameTemplate(chunk, compilation.outputOptions), getChunkFilenameTemplate(chunk, compilation.outputOptions),
{ {
@ -106,101 +122,83 @@ class ModuleChunkFormatPlugin {
.replace(/^\/+/g, "") .replace(/^\/+/g, "")
.split("/"); .split("/");
/** // remove common parts except filename
* @param {Chunk} chunk the chunk while (
* @returns {string} the relative path baseOutputName.length > 1 &&
*/ chunkOutputName.length > 1 &&
const getRelativePath = chunk => { baseOutputName[0] === chunkOutputName[0]
const baseOutputName = currentOutputName.slice(); ) {
const chunkOutputName = compilation baseOutputName.shift();
.getPath( chunkOutputName.shift();
getChunkFilenameTemplate(chunk, compilation.outputOptions),
{
chunk,
contentHashType: "javascript"
}
)
.replace(/^\/+/g, "")
.split("/");
// remove common parts except filename
while (
baseOutputName.length > 1 &&
chunkOutputName.length > 1 &&
baseOutputName[0] === chunkOutputName[0]
) {
baseOutputName.shift();
chunkOutputName.shift();
}
const last = chunkOutputName.join("/");
// create final path
return getUndoPath(baseOutputName.join("/"), last, true) + last;
};
const entrySource = new ConcatSource();
entrySource.add(source);
entrySource.add(";\n\n// load runtime\n");
entrySource.add(
`import ${RuntimeGlobals.require} from ${JSON.stringify(
getRelativePath(/** @type {Chunk} */ (runtimeChunk))
)};\n`
);
const startupSource = new ConcatSource();
startupSource.add(
`var __webpack_exec__ = ${runtimeTemplate.returningFunction(
`${RuntimeGlobals.require}(${RuntimeGlobals.entryModuleId} = moduleId)`,
"moduleId"
)}\n`
);
const loadedChunks = new Set();
let index = 0;
for (let i = 0; i < entries.length; i++) {
const [module, entrypoint] = entries[i];
if (!chunkGraph.getModuleSourceTypes(module).has("javascript")) {
continue;
}
const final = i + 1 === entries.length;
const moduleId = chunkGraph.getModuleId(module);
const chunks = getAllChunks(
/** @type {Entrypoint} */ (entrypoint),
/** @type {Chunk} */ (runtimeChunk),
undefined
);
for (const chunk of chunks) {
if (loadedChunks.has(chunk) || !chunkHasJs(chunk, chunkGraph))
continue;
loadedChunks.add(chunk);
startupSource.add(
`import * as __webpack_chunk_${index}__ from ${JSON.stringify(
getRelativePath(chunk)
)};\n`
);
startupSource.add(
`${RuntimeGlobals.externalInstallChunk}(__webpack_chunk_${index}__);\n`
);
index++;
}
startupSource.add(
`${
final ? `var ${RuntimeGlobals.exports} = ` : ""
}__webpack_exec__(${JSON.stringify(moduleId)});\n`
);
} }
const last = chunkOutputName.join("/");
// create final path
return getUndoPath(baseOutputName.join("/"), last, true) + last;
};
entrySource.add( const entrySource = new ConcatSource();
hooks.renderStartup.call( entrySource.add(source);
startupSource, entrySource.add(";\n\n// load runtime\n");
entries[entries.length - 1][0], entrySource.add(
{ `import ${RuntimeGlobals.require} from ${JSON.stringify(
...renderContext, getRelativePath(/** @type {Chunk} */ (runtimeChunk))
inlined: false )};\n`
} );
)
const startupSource = new ConcatSource();
startupSource.add(
`var __webpack_exec__ = ${runtimeTemplate.returningFunction(
`${RuntimeGlobals.require}(${RuntimeGlobals.entryModuleId} = moduleId)`,
"moduleId"
)}\n`
);
const loadedChunks = new Set();
let index = 0;
for (let i = 0; i < entries.length; i++) {
const [module, entrypoint] = entries[i];
if (!chunkGraph.getModuleSourceTypes(module).has("javascript")) {
continue;
}
const final = i + 1 === entries.length;
const moduleId = chunkGraph.getModuleId(module);
const chunks = getAllChunks(
/** @type {Entrypoint} */ (entrypoint),
/** @type {Chunk} */ (runtimeChunk),
undefined
);
for (const chunk of chunks) {
if (loadedChunks.has(chunk) || !chunkHasJs(chunk, chunkGraph))
continue;
loadedChunks.add(chunk);
startupSource.add(
`import * as __webpack_chunk_${index}__ from ${JSON.stringify(
getRelativePath(chunk)
)};\n`
);
startupSource.add(
`${RuntimeGlobals.externalInstallChunk}(__webpack_chunk_${index}__);\n`
);
index++;
}
startupSource.add(
`${
final ? `var ${RuntimeGlobals.exports} = ` : ""
}__webpack_exec__(${JSON.stringify(moduleId)});\n`
); );
return entrySource;
} }
entrySource.add(
hooks.renderStartup.call(
startupSource,
entries[entries.length - 1][0],
{
...renderContext,
inlined: false
}
)
);
return entrySource;
} }
return source; return source;
}); });

View File

@ -63,6 +63,12 @@ class ModuleChunkLoadingPlugin {
compilation.hooks.runtimeRequirementInTree compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.onChunksLoaded) .for(RuntimeGlobals.onChunksLoaded)
.tap(PLUGIN_NAME, handler); .tap(PLUGIN_NAME, handler);
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.hmrDownloadUpdateHandlers)
.tap(PLUGIN_NAME, handler);
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.hmrDownloadManifest)
.tap(PLUGIN_NAME, handler);
compilation.hooks.runtimeRequirementInTree compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.externalInstallChunk) .for(RuntimeGlobals.externalInstallChunk)
.tap(PLUGIN_NAME, (chunk, set) => { .tap(PLUGIN_NAME, (chunk, set) => {
@ -99,6 +105,26 @@ class ModuleChunkLoadingPlugin {
set.add(RuntimeGlobals.getChunkScriptFilename); set.add(RuntimeGlobals.getChunkScriptFilename);
}); });
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.hmrDownloadUpdateHandlers)
.tap(PLUGIN_NAME, (chunk, set) => {
if (!isEnabledForChunk(chunk)) return;
set.add(RuntimeGlobals.publicPath);
set.add(RuntimeGlobals.loadScript);
set.add(RuntimeGlobals.getChunkUpdateScriptFilename);
set.add(RuntimeGlobals.moduleCache);
set.add(RuntimeGlobals.hmrModuleData);
set.add(RuntimeGlobals.moduleFactoriesAddOnly);
});
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.hmrDownloadManifest)
.tap(PLUGIN_NAME, (chunk, set) => {
if (!isEnabledForChunk(chunk)) return;
set.add(RuntimeGlobals.publicPath);
set.add(RuntimeGlobals.getUpdateManifestFilename);
});
}); });
} }
} }

View File

@ -109,6 +109,9 @@ class ModuleChunkLoadingRuntimeModule extends RuntimeModule {
const withHmr = this._runtimeRequirements.has( const withHmr = this._runtimeRequirements.has(
RuntimeGlobals.hmrDownloadUpdateHandlers RuntimeGlobals.hmrDownloadUpdateHandlers
); );
const withHmrManifest = this._runtimeRequirements.has(
RuntimeGlobals.hmrDownloadManifest
);
const { linkPreload, linkPrefetch } = const { linkPreload, linkPrefetch } =
ModuleChunkLoadingRuntimeModule.getCompilationHooks(compilation); ModuleChunkLoadingRuntimeModule.getCompilationHooks(compilation);
const isNeutralPlatform = runtimeTemplate.isNeutralPlatform(); const isNeutralPlatform = runtimeTemplate.isNeutralPlatform();
@ -346,7 +349,92 @@ class ModuleChunkLoadingRuntimeModule extends RuntimeModule {
"installedChunks[chunkId] === 0", "installedChunks[chunkId] === 0",
"chunkId" "chunkId"
)};` )};`
: "// no on chunks loaded" : "// no on chunks loaded",
withHmr
? Template.asString([
Template.getFunctionContent(
require("../hmr/JavascriptHotModuleReplacement.runtime.js")
)
.replace(/\$key\$/g, "jsonp")
.replace(/\$installedChunks\$/g, "installedChunks")
.replace(/\$loadUpdateChunk\$/g, "loadUpdateChunk")
.replace(/\$moduleCache\$/g, RuntimeGlobals.moduleCache)
.replace(/\$moduleFactories\$/g, RuntimeGlobals.moduleFactories)
.replace(
/\$ensureChunkHandlers\$/g,
RuntimeGlobals.ensureChunkHandlers
)
.replace(/\$hasOwnProperty\$/g, RuntimeGlobals.hasOwnProperty)
.replace(/\$hmrModuleData\$/g, RuntimeGlobals.hmrModuleData)
.replace(
/\$hmrDownloadUpdateHandlers\$/g,
RuntimeGlobals.hmrDownloadUpdateHandlers
)
.replace(
/\$hmrInvalidateModuleHandlers\$/g,
RuntimeGlobals.hmrInvalidateModuleHandlers
),
"",
"function loadUpdateChunk(chunkId, updatedModulesList) {",
Template.indent([
`return new Promise(${runtimeTemplate.basicFunction(
"resolve, reject",
[
"// start update chunk loading",
`var url = ${RuntimeGlobals.publicPath} + ${RuntimeGlobals.getChunkUpdateScriptFilename}(chunkId);`,
`var onResolve = ${runtimeTemplate.basicFunction("obj", [
"var updatedModules = obj.__webpack_modules__;",
"var updatedRuntime = obj.__webpack_runtime__;",
"if(updatedRuntime) currentUpdateRuntime.push(updatedRuntime);",
"for(var moduleId in updatedModules) {",
Template.indent([
`if(${RuntimeGlobals.hasOwnProperty}(updatedModules, moduleId)) {`,
Template.indent([
"currentUpdate[moduleId] = updatedModules[moduleId];",
"if(updatedModulesList) updatedModulesList.push(moduleId);"
]),
"}"
]),
"}",
"resolve(obj);"
])};`,
`var onReject = ${runtimeTemplate.basicFunction("error", [
"var errorMsg = error.message || 'unknown reason';",
"error.message = 'Loading hot update chunk ' + chunkId + ' failed.\\n(' + errorMsg + ')';",
"error.name = 'ChunkLoadError';",
"reject(error);"
])}`,
`var loadScript = ${runtimeTemplate.basicFunction(
"url, onResolve, onReject",
[
`return ${importFunctionName}(/* webpackIgnore: true */ url).then(onResolve).catch(onReject)`
]
)}
loadScript(url, onResolve, onReject);`
]
)});`
]),
"}",
""
])
: "// no HMR",
"",
withHmrManifest
? Template.asString([
`${
RuntimeGlobals.hmrDownloadManifest
} = ${runtimeTemplate.basicFunction("", [
'if (typeof fetch === "undefined") throw new Error("No browser support: need fetch API");',
`return fetch(${RuntimeGlobals.publicPath} + ${
RuntimeGlobals.getUpdateManifestFilename
}()).then(${runtimeTemplate.basicFunction("response", [
"if(response.status === 404) return; // no update available",
'if(!response.ok) throw new Error("Failed to fetch update manifest " + response.statusText);',
"return response.json();"
])});`
])};`
])
: "// no HMR manifest"
]); ]);
} }
} }

View File

@ -212,7 +212,12 @@ const describeCases = config => {
link.href = file.name; link.href = file.name;
runner._moduleScope.document.head.appendChild(link); runner._moduleScope.document.head.appendChild(link);
} else { } else {
runner.require(outputDirectory, `./${file.name}`); const result = runner.require(
outputDirectory,
`./${file.name}`
);
if (typeof result === "object" && "then" in result)
promise = promise.then(() => result);
} }
} }
} else { } else {

View File

@ -0,0 +1,3 @@
export const message = "Hello from async module!";
---
export const message = "Updated async module!";

View File

@ -0,0 +1,34 @@
import update from "../../update.esm";
import.meta.webpackHot.accept(["./async-module", "./lazy-module"]);
it("should handle HMR with async chunks in ESM format", (done) => {
// Initial load of async chunks
Promise.all([
import("./async-module"),
import("./lazy-module")
]).then(([asyncModule, lazyModule]) => {
expect(asyncModule.message).toBe("Hello from async module!");
expect(lazyModule.data.value).toBe(42);
NEXT(update(done, true, () => {
// Re-import after HMR update
Promise.all([
import("./async-module"),
import("./lazy-module")
]).then(([updatedAsyncModule, updatedLazyModule]) => {
expect(updatedAsyncModule.message).toBe("Updated async module!");
expect(updatedLazyModule.data.value).toBe(100);
done();
}).catch(done);
}));
}).catch(done);
});
it("should support dynamic imports with proper ESM chunk loading", (done) => {
// Test that dynamic imports work correctly with ESM chunk format
import("./async-module").then((module) => {
expect(module.message).toBeDefined();
expect(typeof module.message).toBe("string");
done();
}).catch(done);
});

View File

@ -0,0 +1,9 @@
export const data = {
type: "lazy",
value: 42
};
---
export const data = {
type: "lazy",
value: 100
};

View File

@ -0,0 +1,17 @@
/** @type {import("../../../../types").Configuration} */
module.exports = {
mode: "development",
experiments: {
outputModule: true
},
output: {
module: true,
chunkFormat: "module",
filename: "[name].mjs",
chunkFilename: "[name].chunk.mjs",
enabledLibraryTypes: ["module"]
},
optimization: {
minimize: false
}
};

View File

@ -0,0 +1,29 @@
import * as styles from "./style.module.css";
import update from "../../update.esm";
it("should work", async function (done) {
expect(styles).toMatchObject({ class: "_style_module_css-class" });
const styles2 = await import("./style2.module.css");
expect(styles2).toMatchObject({
foo: "_style2_module_css-foo"
});
import.meta.webpackHot.accept(["./style.module.css", "./style2.module.css"], () => {
expect(styles).toMatchObject({
"class-other": "_style_module_css-class-other"
});
import("./style2.module.css").then(styles2 => {
expect(styles2).toMatchObject({
"bar": "_style2_module_css-bar"
});
done();
});
});
NEXT(update(done));
});
module.hot.accept();

View File

@ -0,0 +1,7 @@
.class {
color: red;
}
---
.class-other {
color: blue;
}

View File

@ -0,0 +1,7 @@
.foo {
color: red;
}
---
.bar {
color: blue;
}

View File

@ -0,0 +1,8 @@
module.exports = {
moduleScope(scope) {
const link = scope.window.document.createElement("link");
link.rel = "stylesheet";
link.href = "https://test.cases/path/bundle.css";
scope.window.document.head.appendChild(link);
}
};

View File

@ -0,0 +1,18 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
mode: "development",
experiments: {
outputModule: true,
css: true
},
output: {
module: true,
chunkFormat: "module",
filename: "[name].mjs",
chunkFilename: "[name].chunk.mjs",
enabledLibraryTypes: ["module"]
},
optimization: {
minimize: false
}
};

View File

@ -0,0 +1,4 @@
export const asyncData = {
loaded: true,
content: "Async shared content"
};

View File

@ -0,0 +1,23 @@
import { sharedData } from "./shared";
import update from "../../update.esm";
it("should handle HMR with runtime chunk in ESM format", (done) => {
expect(sharedData.version).toBe("1.0.0");
import.meta.webpackHot.accept(["./shared"]);
NEXT(update(done, true, () => {
import("./shared").then(updatedModule => {
expect(updatedModule.sharedData.version).toBe("2.0.0");
done();
}).catch(done);
}));
});
it("should load async shared module with runtime chunk", (done) => {
import("./async-shared").then(module => {
expect(module.asyncData.loaded).toBe(true);
expect(module.asyncData.content).toBe("Async shared content");
done();
}).catch(done);
});

View File

@ -0,0 +1,9 @@
export const sharedData = {
version: "1.0.0",
timestamp: Date.now()
};
---
export const sharedData = {
version: "2.0.0",
timestamp: Date.now()
};

View File

@ -0,0 +1,18 @@
/** @type {import("../../../../types").Configuration} */
module.exports = {
mode: "development",
experiments: {
outputModule: true
},
output: {
module: true,
chunkFormat: "module",
filename: "[name].mjs",
chunkFilename: "[name].chunk.mjs",
enabledLibraryTypes: ["module"]
},
optimization: {
minimize: false,
runtimeChunk: "single"
}
};

View File

@ -0,0 +1,22 @@
import { greeting } from "./module.js";
import update from "../../update.esm.js";
import.meta.webpackHot.accept(["./module.js"]);
it("should update a simple ES module with HMR", (done) => {
expect(greeting).toBe("Hello World!");
NEXT(update(done, true, () => {
// After HMR update, we need to re-import the module in ESM
import("./module.js").then(updatedModule => {
expect(updatedModule.greeting).toBe("Hello HMR!");
done();
}).catch(done);
}));
});
it("should have HMR runtime available in ESM output", () => {
expect(typeof import.meta.webpackHot.accept).toBe("function");
expect(typeof import.meta.webpackHot.decline).toBe("function");
expect(typeof import.meta.webpackHot.dispose).toBe("function");
});

View File

@ -0,0 +1,3 @@
export const greeting = "Hello World!";
---
export const greeting = "Hello HMR!";

View File

@ -0,0 +1,17 @@
/** @type {import("../../../../types").Configuration} */
module.exports = {
mode: "development",
experiments: {
outputModule: true
},
output: {
module: true,
chunkFormat: "module",
filename: "[name].mjs",
chunkFilename: "[name].chunk.mjs",
enabledLibraryTypes: ["module"]
},
optimization: {
minimize: false
}
};

View File

@ -0,0 +1,16 @@
export function commonFunction(input) {
return `Common function processed: ${input}`;
}
export const commonData = {
shared: true
};
---
export function commonFunction(input) {
return `Updated common function: ${input}`;
}
export const commonData = {
shared: true,
updated: true
};

View File

@ -0,0 +1,25 @@
import update from "../../update.esm";
import.meta.webpackHot.accept(["./common/shared", "vendor-lib"]);
it("should handle HMR with split chunks in ESM format", (done) => {
Promise.all([
import("./common/shared"),
import("vendor-lib")
]).then(([commonModule, vendorModule]) => {
expect(commonModule.commonFunction("test")).toBe("Common function processed: test");
expect(vendorModule.default.version).toBe("1.0.0");
done();
}).catch(done);
NEXT(update(done, true, () => {
// Re-import after HMR update
Promise.all([
import("./common/shared"),
import("vendor-lib")
]).then(([commonModule, vendorModule]) => {
expect(commonModule.commonFunction("test")).toBe("Updated common function: test");
expect(vendorModule.default.version).toBe("2.0.0");
done();
}).catch(done);
}));
});

View File

@ -0,0 +1,17 @@
const vendorLib = {
version: "1.0.0",
init: function() {
console.log("Vendor lib initialized");
}
};
export default vendorLib;
---
const vendorLib = {
version: "2.0.0",
init: function() {
console.log("Vendor lib initialized v2");
}
};
export default vendorLib;

View File

@ -0,0 +1,34 @@
/** @type {import("../../../../types").Configuration} */
module.exports = {
mode: "development",
experiments: {
outputModule: true
},
output: {
module: true,
chunkFormat: "module",
filename: "[name].mjs",
chunkFilename: "[name].chunk.mjs",
enabledLibraryTypes: ["module"]
},
optimization: {
minimize: false,
splitChunks: {
chunks: "all",
minSize: 0,
cacheGroups: {
common: {
test: /common/,
name: "common",
priority: 10,
enforce: true
},
vendor: {
test: /node_modules/,
name: "vendor",
priority: 20
}
}
}
}
};

View File

@ -0,0 +1,16 @@
export default function update(done, options, callback) {
return function (err, stats) {
if (err) return done(err);
import.meta.webpackHot
.check(options || true)
.then(updatedModules => {
if (!updatedModules) {
return done(new Error("No update available"));
}
if (callback) callback(stats);
})
.catch(err => {
done(err);
});
};
};

View File

@ -222,6 +222,14 @@ class TestRunner {
content: fs.readFileSync(module, "utf-8") content: fs.readFileSync(module, "utf-8")
}; };
} }
if (module.startsWith("https://test.")) {
const realPath = urlToPath(module, currentDirectory);
return {
subPath: "",
modulePath: realPath,
content: fs.readFileSync(realPath, "utf-8")
};
}
} }
/** /**