This commit is contained in:
Ryuya 2025-10-06 14:10:18 +08:00 committed by GitHub
commit e270331275
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 186 additions and 14 deletions

View File

@ -6,6 +6,7 @@
"use strict";
const RuntimeGlobals = require("../RuntimeGlobals");
const { getLibraryType } = require("../util/LibraryHelpers");
const ExportWebpackRequireRuntimeModule = require("./ExportWebpackRequireRuntimeModule");
const ModuleChunkLoadingRuntimeModule = require("./ModuleChunkLoadingRuntimeModule");
@ -104,7 +105,15 @@ class ModuleChunkLoadingPlugin {
set.add(RuntimeGlobals.publicPath);
}
// Avoid generating dynamic filename helper for ESM libraries with outputModule
const outputModule =
compilation.options &&
compilation.options.experiments &&
compilation.options.experiments.outputModule;
const isESMLibrary = getLibraryType(chunk, compilation) === "module";
if (!(outputModule && isESMLibrary)) {
set.add(RuntimeGlobals.getChunkScriptFilename);
}
});
compilation.hooks.runtimeRequirementInTree

View File

@ -17,6 +17,7 @@ const {
getChunkFilenameTemplate
} = require("../javascript/JavascriptModulesPlugin");
const { getInitialChunkIds } = require("../javascript/StartupHelpers");
const { getLibraryType } = require("../util/LibraryHelpers");
const compileBooleanMatcher = require("../util/compileBooleanMatcher");
const { getUndoPath } = require("../util/identifier");
@ -95,6 +96,13 @@ class ModuleChunkLoadingRuntimeModule extends RuntimeModule {
runtimeTemplate,
outputOptions: { importFunctionName, crossOriginLoading, charset }
} = compilation;
const outputModule =
compilation.options &&
compilation.options.experiments &&
compilation.options.experiments.outputModule;
const libraryType = getLibraryType(chunk, compilation);
const isESMLibrary = libraryType === "module";
const fn = RuntimeGlobals.ensureChunkHandlers;
const withBaseURI = this._runtimeRequirements.has(RuntimeGlobals.baseURI);
const withExternalInstallChunk = this._runtimeRequirements.has(
@ -221,6 +229,70 @@ class ModuleChunkLoadingRuntimeModule extends RuntimeModule {
: `if(${hasJsMatcher("chunkId")}) {`,
Template.indent([
"// setup Promise in chunk cache",
outputModule && isESMLibrary
? // For ESM library output generate statically analyzable imports per chunk
(() => {
// Build a switch over known async JS chunks with literal URLs
const meta =
compilation.outputOptions.importMetaName ||
"import.meta";
const relevantChunks = new Set();
for (const c of chunk.getAllAsyncChunks()) {
relevantChunks.add(c);
}
const includeEntries = chunkGraph
.getTreeRuntimeRequirements(chunk)
.has(
RuntimeGlobals.ensureChunkIncludeEntries
);
if (includeEntries) {
for (const c of chunkGraph.getRuntimeChunkDependentChunksIterable(
chunk
)) {
relevantChunks.add(c);
}
}
for (const ep of chunk.getAllReferencedAsyncEntrypoints()) {
relevantChunks.add(
ep.chunks[ep.chunks.length - 1]
);
}
const cases = [];
for (const c of relevantChunks) {
if (!chunkHasJs(c, chunkGraph)) continue;
const filename = compilation.getPath(
getChunkFilenameTemplate(
c,
compilation.outputOptions
),
{ chunk: c, contentHashType: "javascript" }
);
const spec = JSON.stringify(
rootOutputDir + filename
);
const cid = JSON.stringify(
/** @type {string|number} */ (c.id)
);
cases.push(
`case ${cid}: promise = ${importFunctionName}(new URL(${spec}, ${meta}.url).href); break;`
);
}
return Template.asString([
"var promise;",
"switch(chunkId) {",
Template.indent(cases),
"default: promise = Promise.reject(new Error('Missing chunk mapping for ' + chunkId));",
"}",
`promise = promise.then(installChunk, ${runtimeTemplate.basicFunction(
"e",
[
"if(installedChunks[chunkId] !== 0) installedChunks[chunkId] = undefined;",
"throw e;"
]
)});`
]);
})()
: // Traditional string concatenation for non-ESM output
`var promise = ${importFunctionName}(${
compilation.outputOptions.publicPath === "auto"
? JSON.stringify(rootOutputDir)

View File

@ -0,0 +1,30 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
/** @typedef {import("../../declarations/WebpackOptions").LibraryOptions} LibraryOptions */
/** @typedef {import("../../declarations/WebpackOptions").LibraryType} LibraryType */
/** @typedef {import("../Chunk")} Chunk */
/** @typedef {import("../Compilation")} Compilation */
/**
* Determine library type from chunk entry options or compilation output options
* @param {Chunk} chunk The chunk to get library type for
* @param {Compilation} compilation The compilation
* @returns {LibraryType | undefined} The library type or undefined
*/
module.exports.getLibraryType = (chunk, compilation) => {
const entryOptions = chunk.getEntryOptions();
const libraryType =
entryOptions && entryOptions.library !== undefined
? entryOptions.library.type
: compilation.outputOptions.library &&
typeof compilation.outputOptions.library === "object" &&
!Array.isArray(compilation.outputOptions.library)
? compilation.outputOptions.library.type
: undefined;
return libraryType;
};

View File

@ -0,0 +1,3 @@
export default function () {
return 2;
}

View File

@ -0,0 +1,4 @@
export default async function getNumber() {
const num = (await import("./chunk.js")).default;
return 1 + num();
}

View File

@ -0,0 +1,23 @@
// Test for issue #15947 - ESM library with dynamic imports
it("should generate statically analyzable dynamic imports for ESM library output", () => {
const fs = require("fs");
const path = require("path");
const outputPath = path.join(__dirname, "lib.js");
const content = fs.readFileSync(outputPath, "utf-8");
// Should use new URL with import.meta.url and literal path
expect(content).toMatch(/import\(\s*new\s+URL\(\s*"[^"]+"\s*,\s*import\.meta\.url\s*\)\.href\s*\)/);
// Should not use dynamic __webpack_require__.u() or publicPath string concatenation
expect(content).not.toMatch(/__webpack_require__\.u\(/);
expect(content).not.toMatch(/\+\s*__webpack_require__\.p\s*\+/);
// Verify that the chunk file was created
const chunkFiles = fs
.readdirSync(__dirname)
.filter(f => f.startsWith("chunk.") && f.endsWith(".js"));
expect(chunkFiles.length).toBeGreaterThan(0);
// Verify the ESM export is present
expect(content).toMatch(/export\s*\{/);
});

View File

@ -0,0 +1,7 @@
"use strict";
module.exports = {
findBundle() {
return ["./index.js"];
}
};

View File

@ -0,0 +1,24 @@
"use strict";
/** @type {import("../../../../types").Configuration} */
module.exports = [
{
entry: "./entry.js",
output: {
filename: "lib.js",
chunkFilename: "chunk.[chunkhash:8].js",
library: {
type: "module"
}
},
experiments: {
outputModule: true
}
},
{
entry: "./index.js",
output: {
filename: "index.js"
}
}
];