Skip to content

Commit fbfa382

Browse files
fix: prevent empty JS file generation for CSS-only entry points
When CSS files are used as entry points (e.g., `entry: { styles: './styles.css' }`), webpack generates an unnecessary empty .js file alongside the CSS output. Root cause: `chunkHasJs()` returns true for any chunk with entry modules without checking their source types, and `chunkHasRuntimeOrJs()` returns true whenever runtime modules exist, even when there are no JavaScript entry modules that need bootstrapping. Fix: - `chunkHasJs()`: Check entry module source types instead of blindly returning true for any entry module. Only return true when at least one entry module has JavaScript source type. - `chunkHasRuntimeOrJs()`: When runtime modules exist but there are entry modules in the chunk, verify that at least one entry module has JavaScript source type before emitting JS. For standalone runtime chunks (optimization.runtimeChunk: true), keep JS emission since the runtime bootstrap is needed for other entry chunks. Fixes #11671 Co-authored-by: Cursor <[email protected]>
1 parent a089954 commit fbfa382

File tree

2 files changed

+32
-9
lines changed

2 files changed

+32
-9
lines changed

lib/javascript/JavascriptModulesPlugin.js

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,13 @@ const JavascriptParser = require("./JavascriptParser");
8181
* @returns {boolean} true, when a JS file is needed for this chunk
8282
*/
8383
const chunkHasJs = (chunk, chunkGraph) => {
84-
if (chunkGraph.getNumberOfEntryModules(chunk) > 0) return true;
84+
if (chunkGraph.getNumberOfEntryModules(chunk) > 0) {
85+
for (const module of chunkGraph.getChunkEntryModulesIterable(chunk)) {
86+
if (chunkGraph.getModuleSourceTypes(module).has(JAVASCRIPT_TYPE)) {
87+
return true;
88+
}
89+
}
90+
}
8591

8692
return Boolean(
8793
chunkGraph.getChunkModulesIterableBySourceType(chunk, JAVASCRIPT_TYPE)
@@ -94,18 +100,32 @@ const chunkHasJs = (chunk, chunkGraph) => {
94100
* @returns {boolean} true, when a JS file is needed for this chunk
95101
*/
96102
const chunkHasRuntimeOrJs = (chunk, chunkGraph) => {
103+
if (chunkGraph.getChunkModulesIterableBySourceType(chunk, JAVASCRIPT_TYPE)) {
104+
return true;
105+
}
106+
97107
if (
98108
chunkGraph.getChunkModulesIterableBySourceType(
99109
chunk,
100110
WEBPACK_MODULE_TYPE_RUNTIME
101111
)
102112
) {
103-
return true;
113+
const numEntryModules = chunkGraph.getNumberOfEntryModules(chunk);
114+
if (numEntryModules === 0) {
115+
// Runtime-only chunk (e.g. optimization.runtimeChunk: true) —
116+
// keep JS for bootstrap code that loads other entry chunks
117+
return true;
118+
}
119+
// Entry modules exist in this chunk — only emit JS if any need JavaScript
120+
for (const module of chunkGraph.getChunkEntryModulesIterable(chunk)) {
121+
if (chunkGraph.getModuleSourceTypes(module).has(JAVASCRIPT_TYPE)) {
122+
return true;
123+
}
124+
}
125+
return false;
104126
}
105127

106-
return Boolean(
107-
chunkGraph.getChunkModulesIterableBySourceType(chunk, JAVASCRIPT_TYPE)
108-
);
128+
return false;
109129
};
110130

111131
/**

test/configCases/asset-modules/only-entry/test.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,13 @@ it("should work", () => {
2626
break;
2727
}
2828
case 2: {
29-
expect(stats.assets.length).toBe(4);
29+
// CSS-only entry should NOT generate an empty .js file (#11671)
30+
expect(stats.assets.length).toBe(3);
3031

3132
const cssEntryInJs = stats.assets.find(
3233
a => a.name.endsWith("css-entry.js")
3334
);
34-
expect(Boolean(cssEntryInJs)).toBe(true);
35+
expect(Boolean(cssEntryInJs)).toBe(false);
3536

3637
const cssEntry = stats.assets.find(
3738
a => a.name.endsWith("css-entry.css")
@@ -40,7 +41,8 @@ it("should work", () => {
4041
break;
4142
}
4243
case 3: {
43-
expect(stats.assets.length).toBe(5);
44+
// JS entry keeps .js, CSS-only entry should NOT generate .js (#11671)
45+
expect(stats.assets.length).toBe(4);
4446

4547
const jsEntry = stats.assets.find(
4648
a => a.name.endsWith("js-entry.js")
@@ -50,7 +52,7 @@ it("should work", () => {
5052
const cssEntryInJs = stats.assets.find(
5153
a => a.name.endsWith("css-entry.js")
5254
);
53-
expect(Boolean(cssEntryInJs)).toBe(true);
55+
expect(Boolean(cssEntryInJs)).toBe(false);
5456

5557
const cssEntry = stats.assets.find(
5658
a => a.name.endsWith("css-entry.css")
@@ -59,6 +61,7 @@ it("should work", () => {
5961
break;
6062
}
6163
case 4: {
64+
// Node target: CSS entry generates JS (exports class names, no CSS output)
6265
expect(stats.assets.length).toBe(4);
6366

6467
const jsEntry = stats.assets.find(

0 commit comments

Comments
 (0)