Skip to content

Commit bb50c7e

Browse files
refactor: export a default expression's binding on demand (#22227)
* refactor: export a default expression's binding on demand * fix(library): keep the export helpers where a module entry goes to the registry The module library entry's export definitions are left out of its source and handed over on demand, which only works where the renderer inlines the entry — rendered into the module registry instead, the stashed source is put back and reads the helpers again. The runtime requirements were dropped in both cases, so a chunk whose entry is executed as a factory called `__webpack_require__.d` and `.r` that the runtime no longer defined. Ask what the renderer asks before dropping them: the startup inlines the entry only when no other chunk of the entrypoint loads with it and no module reads it, and a chunk whose runtime lives elsewhere lifts its entry to the top level under the conditions `_getEsmEntryModuleToInline` already states — now a static so the decision can be made before rendering. * test(library): assert the entry exports both bindings from the registry Rendered into the module registry, the entry has its export definitions put back into the factory, and nothing held it to still exposing what they define. --------- Co-authored-by: alexander-akait <[email protected]>
1 parent 9277ea0 commit bb50c7e

14 files changed

Lines changed: 240 additions & 52 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"webpack": patch
3+
---
4+
5+
Export a module library entry's default expression as its own binding, and keep
6+
the export helpers where the entry is not inlined.

‎lib/dependencies/HarmonyExportExpressionDependency.js‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,13 @@ HarmonyExportExpressionDependency.Template = class HarmonyExportDependencyTempla
191191
const dep = /** @type {HarmonyExportExpressionDependency} */ (dependency);
192192
const { declarationId } = dep;
193193
const exportsName = module.exportsArgument;
194+
const meta = moduleGraph.getMetaIfExisting(module);
195+
196+
const onDemand =
197+
!concatenationScope &&
198+
meta !== undefined &&
199+
meta.onDemandExports === true;
200+
194201
if (declarationId) {
195202
/** @type {string} */
196203
let name;
@@ -218,9 +225,12 @@ HarmonyExportExpressionDependency.Template = class HarmonyExportDependencyTempla
218225
const map = new Map();
219226
map.set(used, {
220227
value: `/* export default binding */ ${name}`,
221-
type: ExportBindingType.Getter
228+
type: ExportBindingType.Getter,
229+
finalName: name
222230
});
223-
initFragments.push(new HarmonyExportInitFragment(exportsName, map));
231+
initFragments.push(
232+
new HarmonyExportInitFragment(exportsName, map, undefined, onDemand)
233+
);
224234
}
225235

226236
source.replace(
@@ -275,9 +285,17 @@ HarmonyExportExpressionDependency.Template = class HarmonyExportDependencyTempla
275285
value: name,
276286
type: valueFormat
277287
? ExportBindingType.Value
278-
: ExportBindingType.Getter
288+
: ExportBindingType.Getter,
289+
finalName: name
279290
});
280-
initFragments.push(new HarmonyExportInitFragment(exportsName, map));
291+
initFragments.push(
292+
new HarmonyExportInitFragment(
293+
exportsName,
294+
map,
295+
undefined,
296+
onDemand
297+
)
298+
);
281299
} else {
282300
content = `/* unused harmony default export */ var ${name} = `;
283301
}

‎lib/javascript/JavascriptModulesPlugin.js‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,10 +1044,8 @@ class JavascriptModulesPlugin {
10441044
// ESM entry chunk whose runtime lives in a separate chunk: inline the
10451045
// single entry module at the top level (instead of the module registry)
10461046
// so its own top-level declarations stay live ESM bindings for consumers.
1047-
const inlinedEntryModule = this._getEsmEntryModuleToInline(
1048-
renderContext,
1049-
allModules
1050-
);
1047+
const inlinedEntryModule =
1048+
JavascriptModulesPlugin._getEsmEntryModuleToInline(renderContext);
10511049
let registryModules = allModules;
10521050
if (inlinedEntryModule) {
10531051
const inlinedEntrySource = this.renderModule(
@@ -1113,18 +1111,20 @@ class JavascriptModulesPlugin {
11131111
* its exports as live ESM bindings. Only a self-contained ESM entry whose
11141112
* top-level declarations can be lifted into the chunk scope qualifies;
11151113
* otherwise `undefined` is returned and the registry path is used.
1116-
* @param {RenderContext} renderContext render context
1117-
* @param {Module[]} allModules all javascript modules of the chunk
1114+
*
1115+
* Static so that a plugin can decide before the chunk is rendered what it may
1116+
* leave out of the inlined entry's source and put back in the registry.
1117+
* @param {Pick<RenderContext, "chunk" | "chunkGraph" | "moduleGraph" | "runtimeTemplate" | "codeGenerationResults">} context chunk, graphs and code generation results
11181118
* @returns {Module | undefined} the entry module to inline, if any
11191119
*/
1120-
_getEsmEntryModuleToInline(renderContext, allModules) {
1120+
static _getEsmEntryModuleToInline(context) {
11211121
const {
11221122
chunk,
11231123
chunkGraph,
11241124
moduleGraph,
11251125
runtimeTemplate,
11261126
codeGenerationResults
1127-
} = renderContext;
1127+
} = context;
11281128

11291129
// Only a single ESM entry whose runtime lives in a separate chunk.
11301130
if (

‎lib/library/ModuleLibraryPlugin.js‎

Lines changed: 66 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ const ConcatenatedModule = require("../optimize/ConcatenatedModule");
2020
const { InlinedUsedName } = require("../optimize/InlineExports");
2121
const RuntimeGlobals = require("../runtime/RuntimeGlobals");
2222
const Template = require("../template/Template");
23+
const { someInIterable } = require("../util/IterableHelpers");
2324
const { renameIdentifiers } = require("../util/concatenate");
2425
const {
2526
RESERVED_IDENTIFIER,
2627
SAFE_IDENTIFIER,
2728
propertyAccess
2829
} = require("../util/property");
29-
const { getEntryRuntime } = require("../util/runtime");
30+
const { getEntryRuntime, intersectRuntime } = require("../util/runtime");
3031
const AbstractLibraryPlugin = require("./AbstractLibraryPlugin");
3132

3233
/** @import { Source } from "webpack-sources" */
@@ -37,7 +38,6 @@ const AbstractLibraryPlugin = require("./AbstractLibraryPlugin");
3738
* LibraryExport
3839
* } from "../../declarations/WebpackOptions"
3940
*/
40-
/** @import Chunk from "../graph/Chunk" */
4141
/** @import CodeGenerationResults from "../module/CodeGenerationResults" */
4242
/** @import Compilation from "../Compilation" */
4343
/** @import Compiler from "../Compiler" */
@@ -170,6 +170,7 @@ class ModuleLibraryPlugin extends AbstractLibraryPlugin {
170170
compilation.hooks.additionalChunkRuntimeRequirements.tap(
171171
PLUGIN_NAME,
172172
(chunk, set, { chunkGraph, codeGenerationResults }) => {
173+
const { moduleGraph, runtimeTemplate } = compilation;
173174
if (!set.has(RuntimeGlobals.definePropertyGetters)) return;
174175
if (chunkGraph.getNumberOfEntryModules(chunk) !== 1) return;
175176
if (
@@ -184,6 +185,48 @@ class ModuleLibraryPlugin extends AbstractLibraryPlugin {
184185
if (javascriptHooks.inlineInRuntimeBailout.isUsed()) return;
185186

186187
const [module] = chunkGraph.getChunkEntryModulesIterable(chunk);
188+
if (chunk.hasRuntime()) {
189+
// The startup inlines the entry only where nothing else loads with
190+
// it: another chunk of the entrypoint delays the execution, and a
191+
// module reading the entry needs it in the registry.
192+
for (const [
193+
,
194+
entrypoint
195+
] of chunkGraph.getChunkEntryModulesWithChunkGroupIterable(chunk)) {
196+
if (entrypoint && entrypoint.chunks.some((c) => c !== chunk)) {
197+
return;
198+
}
199+
}
200+
if (
201+
someInIterable(
202+
moduleGraph.getIncomingConnectionsByOriginModule(module),
203+
([originModule, connections]) =>
204+
originModule &&
205+
originModule !== module &&
206+
connections.some((c) => c.isTargetActive(chunk.runtime)) &&
207+
someInIterable(
208+
chunkGraph.getModuleRuntimes(originModule),
209+
(runtime) =>
210+
intersectRuntime(runtime, chunk.runtime) !== undefined
211+
)
212+
)
213+
) {
214+
return;
215+
}
216+
} else if (
217+
// A chunk whose runtime is elsewhere lifts its entry to the top
218+
// level under the same conditions, and renders it into the module
219+
// registry otherwise.
220+
JavascriptModulesPlugin._getEsmEntryModuleToInline({
221+
chunk,
222+
chunkGraph,
223+
moduleGraph,
224+
runtimeTemplate,
225+
codeGenerationResults
226+
}) !== module
227+
) {
228+
return;
229+
}
187230
const codeGenResult = codeGenerationResults.get(
188231
module,
189232
chunk.runtime
@@ -212,45 +255,32 @@ class ModuleLibraryPlugin extends AbstractLibraryPlugin {
212255
}
213256
}
214257

215-
// The helpers land in the runtime, not in this chunk, so what still
216-
// needs them is asked of every chunk sharing the entrypoint — the
217-
// runtime chunk, and whatever `splitChunks` moved out of the entry.
218-
/** @type {Set<Chunk>} */
219-
const siblings = new Set([chunk]);
220-
for (const [
221-
,
222-
entrypoint
223-
] of chunkGraph.getChunkEntryModulesWithChunkGroupIterable(chunk)) {
224-
if (!entrypoint) continue;
225-
for (const sibling of entrypoint.chunks) siblings.add(sibling);
226-
}
227-
258+
// The entrypoint loads as this chunk alone, so what still needs the
259+
// helpers is whatever else it holds beside the entry.
228260
let otherNeedsDefine = false;
229261
let otherNeedsNamespaceObject = false;
230262
let otherNeedsExports = false;
231263
let otherNeedsRequireScope = false;
232-
outer: for (const sibling of siblings) {
233-
for (const m of chunkGraph.getChunkModulesIterable(sibling)) {
234-
if (m === module) continue;
235-
const requirements = chunkGraph.getModuleRuntimeRequirements(
236-
m,
237-
sibling.runtime
238-
);
239-
if (!requirements) continue;
240-
if (requirements.has(RuntimeGlobals.definePropertyGetters)) {
241-
otherNeedsDefine = true;
242-
}
243-
if (requirements.has(RuntimeGlobals.makeNamespaceObject)) {
244-
otherNeedsNamespaceObject = true;
245-
}
246-
if (requirements.has(RuntimeGlobals.exports)) {
247-
otherNeedsExports = true;
248-
}
249-
if (requirements.has(RuntimeGlobals.requireScope)) {
250-
otherNeedsRequireScope = true;
251-
}
252-
if (otherNeedsDefine) break outer;
264+
for (const m of chunkGraph.getChunkModulesIterable(chunk)) {
265+
if (m === module) continue;
266+
const requirements = chunkGraph.getModuleRuntimeRequirements(
267+
m,
268+
chunk.runtime
269+
);
270+
if (!requirements) continue;
271+
if (requirements.has(RuntimeGlobals.definePropertyGetters)) {
272+
otherNeedsDefine = true;
273+
}
274+
if (requirements.has(RuntimeGlobals.makeNamespaceObject)) {
275+
otherNeedsNamespaceObject = true;
276+
}
277+
if (requirements.has(RuntimeGlobals.exports)) {
278+
otherNeedsExports = true;
279+
}
280+
if (requirements.has(RuntimeGlobals.requireScope)) {
281+
otherNeedsRequireScope = true;
253282
}
283+
if (otherNeedsDefine) break;
254284
}
255285

256286
if (otherNeedsDefine) return;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import "./side";
2+
3+
export const keep = "keep";
4+
export default "main";
5+
6+
it("should define the export helpers the entry factory reads", () => {
7+
expect(keep).toBe("keep");
8+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Stays an ES module exporting nothing: an export the entry reads would have it
2+
// ask for the export helpers itself, and a default the entry interops with would
3+
// put `__webpack_require__.n` in the entry's own source.
4+
export {};
5+
6+
const executed = [];
7+
8+
executed.push("side");
9+
10+
it("should execute the module that loads with the entry", () => {
11+
expect(executed).toEqual(["side"]);
12+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"use strict";
2+
3+
module.exports = {
4+
findBundle(_i, options) {
5+
return `./${Object.keys(options.entry)[0]}.mjs`;
6+
}
7+
};
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"use strict";
2+
3+
/** @import { Compiler, Configuration } from "../../../../" */
4+
5+
/**
6+
* Asserts the emitted entry still exposes both of its bindings, which the
7+
* definitions put back into the factory are what define.
8+
* @param {string} assetName emitted entry asset to assert on
9+
* @returns {(this: Compiler) => void} plugin
10+
*/
11+
const assertExportNames = (assetName) =>
12+
function apply() {
13+
this.hooks.compilation.tap("testcase", (compilation) => {
14+
compilation.hooks.afterProcessAssets.tap("testcase", (assets) => {
15+
const source = assets[assetName].source().toString();
16+
expect(source).toContain("as default");
17+
expect(source).toContain("as keep");
18+
});
19+
});
20+
};
21+
22+
/**
23+
* The split chunk loads with the entry, so the entry is executed as a factory
24+
* rather than inlined into the startup, and the export definitions it left out
25+
* on demand are put back there — the helpers they read have to stay.
26+
* @param {string} name config name
27+
* @param {boolean} concatenateModules whether to concatenate modules
28+
* @returns {Configuration} configuration
29+
*/
30+
const createConfig = (name, concatenateModules) => ({
31+
mode: "production",
32+
name,
33+
entry: { [name]: "./index.js" },
34+
output: {
35+
module: true,
36+
filename: "[name].mjs",
37+
chunkFormat: "module",
38+
library: {
39+
type: "module"
40+
}
41+
},
42+
optimization: {
43+
minimize: false,
44+
concatenateModules,
45+
splitChunks: {
46+
cacheGroups: {
47+
side: {
48+
test: /side\.js$/,
49+
chunks: "all",
50+
name: "side",
51+
enforce: true
52+
}
53+
}
54+
}
55+
},
56+
plugins: [assertExportNames(`${name}.mjs`)]
57+
});
58+
59+
/** @type {Configuration[]} */
60+
module.exports = [
61+
createConfig("concat", true),
62+
createConfig("no-concat", false)
63+
];
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { getNamed } from "./helper";
2+
3+
export const keep = "keep";
4+
export default class {
5+
value() {
6+
return getNamed();
7+
}
8+
}
9+
10+
it("should export an anonymous class as default", () => {
11+
expect(getNamed()).toBe("named");
12+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { getNamed } from "./helper";
2+
3+
export const keep = "keep";
4+
export default function () {
5+
return getNamed();
6+
}
7+
8+
it("should export an anonymous function declaration as default", () => {
9+
expect(getNamed()).toBe("named");
10+
});

0 commit comments

Comments
 (0)