Skip to content

Commit 3bc5646

Browse files
committed
fix: remove unnecessary __webpack_require__ in ESM library output
When a chunk contains non-JS modules (CSS, assets) alongside a concatenated ESM entry, the runtime cleanup hook in ModuleLibraryPlugin bailed out because getNumberOfChunkModules > 1, leaving unused __webpack_require__.d and __webpack_require__.o helpers in the output. - HarmonyExportInitFragment: only add definePropertyGetters requirement when exportMap is non-empty - ModuleLibraryPlugin: replace coarse getNumberOfChunkModules check with per-module requirement inspection Closes #21029
1 parent 12cb825 commit 3bc5646

10 files changed

Lines changed: 100 additions & 128 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack": patch
3+
---
4+
5+
Remove unnecessary `__webpack_require__` runtime helpers in ESM library output with multi-module chunks.

lib/dependencies/HarmonyExportInitFragment.js

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,6 @@ class HarmonyExportInitFragment extends InitFragment {
161161
* @returns {string | Source | undefined} the source code that will be included as initialization code
162162
*/
163163
getContent({ runtimeTemplate, runtimeRequirements }) {
164-
runtimeRequirements.add(RuntimeGlobals.exports);
165-
runtimeRequirements.add(RuntimeGlobals.definePropertyGetters);
166-
167164
const unusedPart =
168165
this.unusedExports.size > 1
169166
? `/* unused harmony exports ${joinIterableWithComma(
@@ -184,12 +181,14 @@ class HarmonyExportInitFragment extends InitFragment {
184181
)}: ${runtimeTemplate.returningFunction(value)}`
185182
);
186183
}
187-
const definePart =
188-
this.exportMap.size > 0
189-
? `/* harmony export */ ${RuntimeGlobals.definePropertyGetters}(${
190-
this.exportsArgument
191-
}, {${definitions.join(",")}\n/* harmony export */ });\n`
192-
: "";
184+
let definePart = "";
185+
if (this.exportMap.size > 0) {
186+
runtimeRequirements.add(RuntimeGlobals.exports);
187+
runtimeRequirements.add(RuntimeGlobals.definePropertyGetters);
188+
definePart = `/* harmony export */ ${RuntimeGlobals.definePropertyGetters}(${
189+
this.exportsArgument
190+
}, {${definitions.join(",")}\n/* harmony export */ });\n`;
191+
}
193192
return `${definePart}${unusedPart}`;
194193
}
195194
}

lib/library/ModuleLibraryPlugin.js

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -111,24 +111,15 @@ class ModuleLibraryPlugin extends AbstractLibraryPlugin {
111111

112112
// `ModuleLibraryPlugin` stashes the on-demand exports source via
113113
// `onDemandExportsGeneration` and only re-emits it when the
114-
// module is wrapped in an IIFE/factory. When a single concatenated
115-
// entry is inlined directly, the stashed source — and the
114+
// module is wrapped in an IIFE/factory. When the entry is
115+
// inlined directly, the stashed source — and the
116116
// `definePropertyGetters` / `requireScope` runtime helpers it
117117
// pulled in — never make it into the output. Drop those helpers
118-
// from the chunk's set in that simple shape so the bundle stays
119-
// clean.
118+
// from the chunk's set so the bundle stays clean.
120119
compilation.hooks.additionalChunkRuntimeRequirements.tap(
121120
PLUGIN_NAME,
122121
(chunk, set, { chunkGraph, codeGenerationResults }) => {
123122
if (!set.has(RuntimeGlobals.definePropertyGetters)) return;
124-
125-
// Only handle the simple "single concatenated entry"
126-
// shape. Anything else (additional modules, multiple
127-
// entries, sibling runtime chunks, or chunk-level
128-
// requirements that disable inline startup) forces the
129-
// module through factory/IIFE rendering, which re-emits
130-
// the source.
131-
if (chunkGraph.getNumberOfChunkModules(chunk) !== 1) return;
132123
if (chunkGraph.getNumberOfEntryModules(chunk) !== 1) return;
133124
if (chunkGraph.hasChunkEntryDependentChunks(chunk)) return;
134125
if (
@@ -140,8 +131,6 @@ class ModuleLibraryPlugin extends AbstractLibraryPlugin {
140131
) {
141132
return;
142133
}
143-
// Anyone tapping `inlineInRuntimeBailout` may force factory
144-
// rendering at render time, so conservatively bail out.
145134
if (javascriptHooks.inlineInRuntimeBailout.isUsed()) return;
146135

147136
const [module] = chunkGraph.getChunkEntryModulesIterable(chunk);
@@ -153,14 +142,6 @@ class ModuleLibraryPlugin extends AbstractLibraryPlugin {
153142
) {
154143
return;
155144
}
156-
// If the generated source references any
157-
// `__webpack_require__.<helper>` (the on-demand `.d(...)`
158-
// is stashed, but `.r(__webpack_exports__)` from the ESM
159-
// compat flag, namespace objects, deferred externals, ...
160-
// stay in the result) the helpers and the require scope
161-
// they live in are still needed. The dot in the substring
162-
// avoids matching the bare `"__webpack_require__"` string
163-
// literals that some test fixtures include.
164145
const codeGenResult = codeGenerationResults.get(
165146
module,
166147
chunk.runtime
@@ -174,9 +155,34 @@ class ModuleLibraryPlugin extends AbstractLibraryPlugin {
174155
return;
175156
}
176157

158+
// Check whether any other module in the chunk still needs
159+
// the helpers we want to remove.
160+
let otherNeedsDefine = false;
161+
let otherNeedsExports = false;
162+
let otherNeedsRequireScope = false;
163+
for (const m of chunkGraph.getChunkModulesIterable(chunk)) {
164+
if (m === module) continue;
165+
const requirements = chunkGraph.getModuleRuntimeRequirements(
166+
m,
167+
chunk.runtime
168+
);
169+
if (!requirements) continue;
170+
if (requirements.has(RuntimeGlobals.definePropertyGetters)) {
171+
otherNeedsDefine = true;
172+
}
173+
if (requirements.has(RuntimeGlobals.exports)) {
174+
otherNeedsExports = true;
175+
}
176+
if (requirements.has(RuntimeGlobals.requireScope)) {
177+
otherNeedsRequireScope = true;
178+
}
179+
if (otherNeedsDefine) break;
180+
}
181+
182+
if (otherNeedsDefine) return;
177183
set.delete(RuntimeGlobals.definePropertyGetters);
178-
set.delete(RuntimeGlobals.exports);
179-
set.delete(RuntimeGlobals.requireScope);
184+
if (!otherNeedsExports) set.delete(RuntimeGlobals.exports);
185+
if (!otherNeedsRequireScope) set.delete(RuntimeGlobals.requireScope);
180186
}
181187
);
182188
});

test/configCases/html/script-src-classic/__snapshots__/ConfigCacheTest.snap

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,6 @@
33
exports[`ConfigCacheTestCases html script-src-classic exported tests should emit IIFE-wrapped chunks for <script type=module src> too (still valid as ES modules) 1`] = `
44
"/******/ (() => { // webpackBootstrap
55
/******/ \\"use strict\\";
6-
/******/ // The require scope
7-
/******/ var __webpack_require__ = {};
8-
/******/
9-
/************************************************************************/
10-
/******/ /* webpack/runtime/define property getters */
11-
/******/ (() => {
12-
/******/ // define getter functions for harmony exports
13-
/******/ __webpack_require__.d = (exports, definition) => {
14-
/******/ for(var key in definition) {
15-
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
16-
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
17-
/******/ }
18-
/******/ }
19-
/******/ };
20-
/******/ })();
21-
/******/
22-
/******/ /* webpack/runtime/hasOwnProperty shorthand */
23-
/******/ (() => {
24-
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
25-
/******/ })();
26-
/******/
27-
/************************************************************************/
28-
var __webpack_exports__ = {};
296
/*!*************************!*\\\\
307
!*** ./module-entry.js ***!
318
\\\\*************************/

test/configCases/html/script-src-classic/__snapshots__/ConfigTest.snap

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,6 @@
33
exports[`ConfigTestCases html script-src-classic exported tests should emit IIFE-wrapped chunks for <script type=module src> too (still valid as ES modules) 1`] = `
44
"/******/ (() => { // webpackBootstrap
55
/******/ \\"use strict\\";
6-
/******/ // The require scope
7-
/******/ var __webpack_require__ = {};
8-
/******/
9-
/************************************************************************/
10-
/******/ /* webpack/runtime/define property getters */
11-
/******/ (() => {
12-
/******/ // define getter functions for harmony exports
13-
/******/ __webpack_require__.d = (exports, definition) => {
14-
/******/ for(var key in definition) {
15-
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
16-
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
17-
/******/ }
18-
/******/ }
19-
/******/ };
20-
/******/ })();
21-
/******/
22-
/******/ /* webpack/runtime/hasOwnProperty shorthand */
23-
/******/ (() => {
24-
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
25-
/******/ })();
26-
/******/
27-
/************************************************************************/
28-
var __webpack_exports__ = {};
296
/*!*************************!*\\\\
307
!*** ./module-entry.js ***!
318
\\\\*************************/

test/configCases/html/script-src/__snapshots__/ConfigCacheTest.snap

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,7 @@
11
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
22

33
exports[`ConfigCacheTestCases html script-src exported tests should bundle <script type=module src> through ESM resolution alongside classic scripts 1`] = `
4-
"/******/ // The require scope
5-
/******/ var __webpack_require__ = {};
6-
/******/
7-
/************************************************************************/
8-
/******/ /* webpack/runtime/define property getters */
9-
/******/ (() => {
10-
/******/ // define getter functions for harmony exports
11-
/******/ __webpack_require__.d = (exports, definition) => {
12-
/******/ for(var key in definition) {
13-
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
14-
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
15-
/******/ }
16-
/******/ }
17-
/******/ };
18-
/******/ })();
19-
/******/
20-
/******/ /* webpack/runtime/hasOwnProperty shorthand */
21-
/******/ (() => {
22-
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
23-
/******/ })();
24-
/******/
25-
/************************************************************************/
26-
var __webpack_exports__ = {};
27-
/*!*************************!*\\\\
4+
"/*!*************************!*\\\\
285
!*** ./module-entry.js ***!
296
\\\\*************************/
307
/* unused harmony export value */

test/configCases/html/script-src/__snapshots__/ConfigTest.snap

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,7 @@
11
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
22

33
exports[`ConfigTestCases html script-src exported tests should bundle <script type=module src> through ESM resolution alongside classic scripts 1`] = `
4-
"/******/ // The require scope
5-
/******/ var __webpack_require__ = {};
6-
/******/
7-
/************************************************************************/
8-
/******/ /* webpack/runtime/define property getters */
9-
/******/ (() => {
10-
/******/ // define getter functions for harmony exports
11-
/******/ __webpack_require__.d = (exports, definition) => {
12-
/******/ for(var key in definition) {
13-
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
14-
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
15-
/******/ }
16-
/******/ }
17-
/******/ };
18-
/******/ })();
19-
/******/
20-
/******/ /* webpack/runtime/hasOwnProperty shorthand */
21-
/******/ (() => {
22-
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
23-
/******/ })();
24-
/******/
25-
/************************************************************************/
26-
var __webpack_exports__ = {};
27-
/*!*************************!*\\\\
4+
"/*!*************************!*\\\\
285
!*** ./module-entry.js ***!
296
\\\\*************************/
307
/* unused harmony export value */
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import concat from "./concat";
2+
import "./style.css";
3+
4+
const getFile = () => {
5+
const fs = __non_webpack_require__("fs");
6+
return fs.readFileSync(__filename, "utf-8");
7+
};
8+
9+
const stripModuleMarkers = (content) =>
10+
content.split(/^;\/\/ /m)[0];
11+
12+
const RuntimeGlobals_Exports = "__webpack_exports__";
13+
const RuntimeGlobals_Require = "__webpack_require__";
14+
const exportsReg = new RegExp(
15+
"var\\s+" + RuntimeGlobals_Exports + "\\s*="
16+
);
17+
const definePropertyGettersReg = new RegExp(
18+
RuntimeGlobals_Require + "\\.d\\s*="
19+
);
20+
const hasOwnPropertyReg = new RegExp(
21+
RuntimeGlobals_Require + "\\.o\\s*="
22+
);
23+
const requireScopeReg = new RegExp(
24+
"var\\s+" + RuntimeGlobals_Require + "\\s*=\\s*\\{"
25+
);
26+
const isNoConcat = /-no-concat\.mjs$/.test(__filename);
27+
28+
it("should not emit __webpack_require__ helpers with CSS modules", () => {
29+
const content = stripModuleMarkers(getFile());
30+
expect(concat).toBe("concat");
31+
32+
if (isNoConcat) {
33+
expect(content).toMatch(exportsReg);
34+
} else {
35+
expect(content).not.toMatch(exportsReg);
36+
expect(content).not.toMatch(definePropertyGettersReg);
37+
expect(content).not.toMatch(hasOwnPropertyReg);
38+
expect(content).not.toMatch(requireScopeReg);
39+
}
40+
});
41+
42+
export { concat };
43+
export default "foo";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.main { color: red; }

test/configCases/library/module-useless-export-requirement/webpack.config.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,25 @@ const configs = [
3434
optimization: {
3535
avoidEntryIife: false
3636
}
37+
},
38+
{
39+
name: "entry4",
40+
entry: "./entry4.js",
41+
css: true
3742
}
3843
];
3944

4045
module.exports = configs.reduce(
4146
/** @type {(result: EXPECTED_ANY, config: EXPECTED_ANY) => Configuration[]} */ (
4247
result,
43-
{ name, entry, optimization }
48+
{ name, entry, optimization, css }
4449
) => {
50+
const extra = css
51+
? { experiments: { ...common.experiments, css: true } }
52+
: {};
4553
result.push({
4654
...common,
55+
...extra,
4756
optimization: {
4857
...optimization,
4958
concatenateModules: true
@@ -54,6 +63,7 @@ module.exports = configs.reduce(
5463
});
5564
result.push({
5665
...common,
66+
...extra,
5767
optimization: {
5868
...optimization,
5969
concatenateModules: false

0 commit comments

Comments
 (0)