Skip to content

Commit 6ecd97c

Browse files
feat: annotate every module instantiation webpack emits as a statement
Carry the side-effect information into the deferred import, the eager slot of a wrapped concatenation member and the three entry-startup calls as well. Annotating an expression a caller may go on to invoke is not safe: a minifier binds the annotation to the outermost call, so `f(id)(x)` loses `(x)` with it. The entry-startup calls skip it where the module id is assigned inside the argument, and the entry IIFE keeps it for writing into `__webpack_exports__` rather than returning it. Refs #17121
1 parent 6ac75d4 commit 6ecd97c

8 files changed

Lines changed: 143 additions & 6 deletions

File tree

‎lib/javascript/StartupHelpers.js‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,21 @@ module.exports.generateEntryStartup = (
103103
)}`
104104
];
105105

106+
/** @type {Map<ModuleId, string>} entry module id -> its side-effects annotation */
107+
const pureByModuleId = new Map();
108+
// `__webpack_exec__` assigns `entryModuleId` on the way in, which the annotation
109+
// would license a minifier to drop along with the call.
110+
const canAnnotate = !chunkGraph
111+
.getTreeRuntimeRequirements(chunk)
112+
.has(RuntimeGlobals.entryModuleId);
113+
106114
/**
107115
* Returns fn to execute.
108116
* @param {ModuleId} id id
109117
* @returns {string} fn to execute
110118
*/
111-
const runModule = (id) => `__webpack_exec__(${JSON.stringify(id)})`;
119+
const runModule = (id) =>
120+
`${pureByModuleId.get(id) || ""}__webpack_exec__(${JSON.stringify(id)})`;
112121
/**
113122
* Output combination.
114123
* @param {Chunks} chunks chunks
@@ -150,6 +159,15 @@ module.exports.generateEntryStartup = (
150159
/** @type {Entrypoint} */
151160
(entrypoint).getRuntimeChunk();
152161
const moduleId = /** @type {ModuleId} */ (chunkGraph.getModuleId(module));
162+
if (canAnnotate) {
163+
pureByModuleId.set(
164+
moduleId,
165+
runtimeTemplate.moduleSideEffectsAnnotation(
166+
module,
167+
chunkGraph.moduleGraph
168+
)
169+
);
170+
}
153171
const chunks = getAllChunks(
154172
/** @type {Entrypoint} */
155173
(entrypoint),

‎lib/optimize/ConcatenatedModule.js‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2810,7 +2810,7 @@ ${defineGetters}`
28102810
result.add(
28112811
`\n// DEFERRED EXTERNAL MODULE: ${info.module.readableIdentifier(
28122812
requestShortener
2813-
)}\nvar ${info.deferredName} = ${loader};`
2813+
)}\nvar ${info.deferredName} = /*#__PURE__*/${loader};`
28142814
);
28152815
if (info.deferredNamespaceObjectUsed) {
28162816
runtimeRequirements.add(RuntimeGlobals.makeDeferredNamespaceObject);
@@ -2866,7 +2866,10 @@ ${defineGetters}`
28662866
result.add(
28672867
`\n;// ${info.module.readableIdentifier(
28682868
requestShortener
2869-
)}\n${info.namespaceFnName}();\n`
2869+
)}\n${runtimeTemplate.moduleSideEffectsAnnotation(
2870+
info.module,
2871+
moduleGraph
2872+
)}${info.namespaceFnName}();\n`
28702873
);
28712874
} else if (!info.wrapped) {
28722875
result.add(
@@ -2945,7 +2948,10 @@ ${defineGetters}`
29452948
result.add(
29462949
`\n;// EXTERNAL MODULE: ${info.module.readableIdentifier(
29472950
requestShortener
2948-
)}\n${info.namespaceFnName}();\n`
2951+
)}\n${runtimeTemplate.moduleSideEffectsAnnotation(
2952+
info.module,
2953+
moduleGraph
2954+
)}${info.namespaceFnName}();\n`
29492955
);
29502956
}
29512957
// If a module is deferred in other places, but used as non-deferred here,

‎lib/template/RuntimeTemplate.js‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1852,7 +1852,9 @@ class RuntimeTemplate {
18521852

18531853
/**
18541854
* The annotation for a module instantiation, carrying what webpack knows about
1855-
* the module's side effects on to whoever consumes the emitted bundle.
1855+
* the module's side effects on to whoever consumes the emitted bundle. Only ever
1856+
* write it in front of a call the emitted statement itself terminates: a minifier
1857+
* binds it to the outermost call, so `f(id)(x)` would drop `(x)` along with it.
18561858
* @param {Module} module the instantiated module
18571859
* @param {ModuleGraph} moduleGraph the module graph
18581860
* @returns {string} the annotation, or an empty string when it may have side effects
@@ -2391,7 +2393,7 @@ class RuntimeTemplate {
23912393
// would deadlock; forcing the namespace throws instead.
23922394
outgoingAsyncModules.delete(originModule);
23932395

2394-
importContent = `/* deferred harmony import */ ${optDeclaration}${importVar} = ${getOptimizedDeferredModule(
2396+
importContent = `/* deferred harmony import */ ${optDeclaration}${importVar} = /*#__PURE__*/${getOptimizedDeferredModule(
23952397
moduleId,
23962398
exportsType,
23972399
Array.from(outgoingAsyncModules, (mod) => chunkGraph.getModuleId(mod)),
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const fs = require("fs");
2+
const path = require("path");
3+
4+
const read = (name) =>
5+
fs.readFileSync(path.join(path.dirname(__filename), name), "utf-8");
6+
7+
const libraries = ["bundle0.mjs", "bundle1.mjs", "bundle2.js"];
8+
9+
it("should annotate the entry a bootstrapped runtime starts", () => {
10+
expect(read("bundle0.mjs")).toMatch(
11+
/__webpack_exports__ = \/\*#__PURE__\*\/__webpack_require__\(/
12+
);
13+
});
14+
15+
it("should annotate the entry an ESM chunk starts", () => {
16+
expect(read("bundle1.mjs")).toMatch(
17+
/__webpack_exports__ = \/\*#__PURE__\*\/__webpack_exec__\(/
18+
);
19+
});
20+
21+
it("should annotate the entry a CommonJS chunk starts", () => {
22+
expect(read("bundle2.js")).toMatch(
23+
/__webpack_exports__ = \(\/\*#__PURE__\*\/__webpack_exec__\(/
24+
);
25+
});
26+
27+
it("should keep the annotated module in every library", () => {
28+
for (const name of libraries) {
29+
expect(read(name)).toContain("a * b");
30+
}
31+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { mul } from "./pure-cjs";
2+
import { helper } from "./refs-entry.js";
3+
4+
export function times(a, b) {
5+
return mul(a, b);
6+
}
7+
8+
export function viaHelper(a) {
9+
return helper(a);
10+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
exports.mul = function (a, b) {
2+
return a * b;
3+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Importing the entry back keeps webpack from inlining it, so the runtime has to
2+
// start it with a call — the statement these cases are about.
3+
import { times } from "./lib.js";
4+
5+
export function helper(a) {
6+
return times(a, 2);
7+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"use strict";
2+
3+
const common = {
4+
target: "node",
5+
mode: "production",
6+
module: {
7+
rules: [{ test: /[\\/]pure-cjs\.js$/, sideEffects: false }]
8+
},
9+
optimization: {
10+
// Keeps the entry a module of its own, so the runtime has to start it.
11+
concatenateModules: false
12+
}
13+
};
14+
15+
/** @type {import("../../../../types").Configuration[]} */
16+
module.exports = [
17+
{
18+
...common,
19+
name: "bootstrap",
20+
entry: "./lib.js",
21+
output: {
22+
filename: "bundle0.mjs",
23+
library: { type: "module" },
24+
module: true
25+
},
26+
experiments: { outputModule: true }
27+
},
28+
{
29+
...common,
30+
name: "esm-chunk",
31+
entry: { bundle1: "./lib.js" },
32+
output: {
33+
filename: "[name].mjs",
34+
library: { type: "module" },
35+
module: true
36+
},
37+
optimization: {
38+
...common.optimization,
39+
runtimeChunk: { name: "runtime1" }
40+
},
41+
experiments: { outputModule: true }
42+
},
43+
{
44+
...common,
45+
name: "commonjs-chunk",
46+
entry: { bundle2: "./lib.js" },
47+
output: {
48+
filename: "[name].js",
49+
library: { type: "commonjs2" }
50+
},
51+
optimization: { ...common.optimization, runtimeChunk: { name: "runtime2" } }
52+
},
53+
{
54+
name: "assertions",
55+
target: "node",
56+
mode: "production",
57+
dependencies: ["bootstrap", "esm-chunk", "commonjs-chunk"],
58+
output: { filename: "bundle3.js" }
59+
}
60+
];

0 commit comments

Comments
 (0)