Skip to content

Commit 6dec682

Browse files
authored
fix: prevent empty JS file generation for CSS-only entry points (#20454)
1 parent c835794 commit 6dec682

File tree

22 files changed

+523
-29
lines changed

22 files changed

+523
-29
lines changed
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+
Fixed an issue where empty JavaScript files were generated for CSS-only entry points. The code now correctly checks if entry modules have JavaScript source types before determining whether to generate a JS file.

lib/css/CssGenerator.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ const {
1919
const RuntimeGlobals = require("../RuntimeGlobals");
2020
const Template = require("../Template");
2121
const CssImportDependency = require("../dependencies/CssImportDependency");
22-
const EntryDependency = require("../dependencies/EntryDependency");
2322
const { getUndoPath } = require("../util/identifier");
2423
const memoize = require("../util/memoize");
2524

@@ -472,11 +471,7 @@ class CssGenerator extends Generator {
472471
const sourceTypes = new Set();
473472
const connections = this._moduleGraph.getIncomingConnections(module);
474473

475-
let isEntryModule = false;
476474
for (const connection of connections) {
477-
if (connection.dependency instanceof EntryDependency) {
478-
isEntryModule = true;
479-
}
480475
if (
481476
exportType === "link" &&
482477
connection.dependency instanceof CssImportDependency
@@ -503,7 +498,7 @@ class CssGenerator extends Generator {
503498
}
504499
}
505500
if (this._generatesJsOnly(module)) {
506-
if (sourceTypes.has(JAVASCRIPT_TYPE) || isEntryModule) {
501+
if (sourceTypes.has(JAVASCRIPT_TYPE)) {
507502
return JAVASCRIPT_TYPES;
508503
}
509504
return new Set();
@@ -578,3 +573,5 @@ class CssGenerator extends Generator {
578573
}
579574

580575
module.exports = CssGenerator;
576+
577+
module.exports = CssGenerator;

lib/javascript/JavascriptModulesPlugin.js

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,37 +75,75 @@ const JavascriptParser = require("./JavascriptParser");
7575
/** @typedef {import("../util/concatenate").ScopeSet} ScopeSet */
7676
/** @typedef {import("../util/concatenate").UsedNamesInScopeInfo} UsedNamesInScopeInfo */
7777

78+
/** @type {WeakMap<ChunkGraph, WeakMap<Chunk, boolean>>} */
79+
const chunkHasJsCache = new WeakMap();
80+
7881
/**
7982
* @param {Chunk} chunk a chunk
8083
* @param {ChunkGraph} chunkGraph the chunk graph
8184
* @returns {boolean} true, when a JS file is needed for this chunk
8285
*/
83-
const chunkHasJs = (chunk, chunkGraph) => {
84-
if (chunkGraph.getNumberOfEntryModules(chunk) > 0) return true;
86+
const _chunkHasJs = (chunk, chunkGraph) => {
87+
if (chunkGraph.getNumberOfEntryModules(chunk) > 0) {
88+
for (const module of chunkGraph.getChunkEntryModulesIterable(chunk)) {
89+
if (chunkGraph.getModuleSourceTypes(module).has(JAVASCRIPT_TYPE)) {
90+
return true;
91+
}
92+
}
93+
}
8594

8695
return Boolean(
8796
chunkGraph.getChunkModulesIterableBySourceType(chunk, JAVASCRIPT_TYPE)
8897
);
8998
};
9099

100+
/**
101+
* @param {Chunk} chunk a chunk
102+
* @param {ChunkGraph} chunkGraph the chunk graph
103+
* @returns {boolean} true, when a JS file is needed for this chunk
104+
*/
105+
const chunkHasJs = (chunk, chunkGraph) => {
106+
let innerCache = chunkHasJsCache.get(chunkGraph);
107+
if (innerCache === undefined) {
108+
innerCache = new WeakMap();
109+
chunkHasJsCache.set(chunkGraph, innerCache);
110+
}
111+
112+
const cachedResult = innerCache.get(chunk);
113+
if (cachedResult !== undefined) {
114+
return cachedResult;
115+
}
116+
117+
const result = _chunkHasJs(chunk, chunkGraph);
118+
innerCache.set(chunk, result);
119+
return result;
120+
};
121+
91122
/**
92123
* @param {Chunk} chunk a chunk
93124
* @param {ChunkGraph} chunkGraph the chunk graph
94125
* @returns {boolean} true, when a JS file is needed for this chunk
95126
*/
96127
const chunkHasRuntimeOrJs = (chunk, chunkGraph) => {
128+
if (chunkHasJs(chunk, chunkGraph)) {
129+
return true;
130+
}
131+
97132
if (
98133
chunkGraph.getChunkModulesIterableBySourceType(
99134
chunk,
100135
WEBPACK_MODULE_TYPE_RUNTIME
101136
)
102137
) {
103-
return true;
138+
for (const chunkGroup of chunk.groupsIterable) {
139+
for (const c of chunkGroup.chunks) {
140+
if (chunkHasJs(c, chunkGraph)) return true;
141+
}
142+
}
143+
return false;
104144
}
105145

106-
return Boolean(
107-
chunkGraph.getChunkModulesIterableBySourceType(chunk, JAVASCRIPT_TYPE)
108-
);
146+
return false;
109147
};
110148

111149
/**

test/configCases/asset-modules/entry-with-runtimeChunk/test.config.js

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,13 @@ module.exports = {
66

77
switch (i % 4) {
88
case 0:
9-
return ["test.js", `${i}/runtime~app.${ext}`];
9+
return ["test.js"];
1010
case 1:
1111
return ["test.js", `${i}/app.${ext}`, `${i}/runtime~app.${ext}`];
1212
case 2:
13-
return ["test.js", `${i}/app.${ext}`, `${i}/runtime~app.${ext}`];
13+
return ["test.js"];
1414
case 3:
15-
return [
16-
"test.js",
17-
`${i}/entry1.${ext}`,
18-
`${i}/entry2.${ext}`,
19-
`${i}/runtime~entry1.${ext}`,
20-
`${i}/runtime~entry2.${ext}`
21-
];
15+
return ["test.js", `${i}/entry2.${ext}`, `${i}/runtime~entry2.${ext}`];
2216
default:
2317
break;
2418
}

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ it("should work", () => {
2626
break;
2727
}
2828
case 2: {
29-
expect(stats.assets.length).toBe(4);
29+
expect(stats.assets.length).toBe(3);
3030

3131
const cssEntryInJs = stats.assets.find(
3232
a => a.name.endsWith("css-entry.js")
3333
);
34-
expect(Boolean(cssEntryInJs)).toBe(true);
34+
expect(Boolean(cssEntryInJs)).toBe(false);
3535

3636
const cssEntry = stats.assets.find(
3737
a => a.name.endsWith("css-entry.css")
@@ -40,7 +40,7 @@ it("should work", () => {
4040
break;
4141
}
4242
case 3: {
43-
expect(stats.assets.length).toBe(5);
43+
expect(stats.assets.length).toBe(4);
4444

4545
const jsEntry = stats.assets.find(
4646
a => a.name.endsWith("js-entry.js")
@@ -50,7 +50,7 @@ it("should work", () => {
5050
const cssEntryInJs = stats.assets.find(
5151
a => a.name.endsWith("css-entry.js")
5252
);
53-
expect(Boolean(cssEntryInJs)).toBe(true);
53+
expect(Boolean(cssEntryInJs)).toBe(false);
5454

5555
const cssEntry = stats.assets.find(
5656
a => a.name.endsWith("css-entry.css")
@@ -59,7 +59,7 @@ it("should work", () => {
5959
break;
6060
}
6161
case 4: {
62-
expect(stats.assets.length).toBe(4);
62+
expect(stats.assets.length).toBe(3);
6363

6464
const jsEntry = stats.assets.find(
6565
a => a.name.endsWith("js-entry.js")
@@ -69,7 +69,7 @@ it("should work", () => {
6969
const cssEntryInJs = stats.assets.find(
7070
a => a.name.endsWith("css-entry.js")
7171
);
72-
expect(Boolean(cssEntryInJs)).toBe(true);
72+
expect(Boolean(cssEntryInJs)).toBe(false);
7373
break;
7474
}
7575
case 5: {

test/helpers/FakeDocument.js

Lines changed: 141 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,67 @@ class FakeDocument {
4949
return this._elementsByTagName.get(name) || [];
5050
}
5151

52+
querySelectorAll(selector) {
53+
// Simple selector support for common cases
54+
// Tag selector: "link", "script", etc.
55+
if (/^[a-zA-Z][a-zA-Z0-9-]*$/.test(selector)) {
56+
return this.getElementsByTagName(selector);
57+
}
58+
// Class selector: ".class"
59+
if (selector.startsWith(".")) {
60+
const className = selector.slice(1);
61+
const allElements = [];
62+
for (const elements of this._elementsByTagName.values()) {
63+
for (const element of elements) {
64+
if (element.getAttribute("class") === className) {
65+
allElements.push(element);
66+
}
67+
}
68+
}
69+
return allElements;
70+
}
71+
// ID selector: "#id"
72+
if (selector.startsWith("#")) {
73+
const id = selector.slice(1);
74+
for (const elements of this._elementsByTagName.values()) {
75+
for (const element of elements) {
76+
if (element.getAttribute("id") === id) {
77+
return [element];
78+
}
79+
}
80+
}
81+
return [];
82+
}
83+
// Attribute selector: "[attr]", "[attr=value]"
84+
if (selector.startsWith("[") && selector.endsWith("]")) {
85+
const attrSelector = selector.slice(1, -1);
86+
const allElements = [];
87+
if (attrSelector.includes("=")) {
88+
const [attr, value] = attrSelector
89+
.split("=")
90+
.map((s) => s.trim().replace(/^["']|["']$/g, ""));
91+
for (const elements of this._elementsByTagName.values()) {
92+
for (const element of elements) {
93+
if (element.getAttribute(attr) === value) {
94+
allElements.push(element);
95+
}
96+
}
97+
}
98+
} else {
99+
for (const elements of this._elementsByTagName.values()) {
100+
for (const element of elements) {
101+
if (element.getAttribute(attrSelector) !== undefined) {
102+
allElements.push(element);
103+
}
104+
}
105+
}
106+
}
107+
return allElements;
108+
}
109+
// Default: return empty array for unsupported selectors
110+
return [];
111+
}
112+
52113
getComputedStyle(element) {
53114
const style = { getPropertyValue };
54115
const links = this.getElementsByTagName("link");
@@ -83,8 +144,11 @@ class FakeElement {
83144

84145
_load(node) {
85146
if (node._type === "link") {
86-
setTimeout(() => {
87-
if (node.onload) node.onload({ type: "load", target: node });
147+
const timer = setTimeout(() => {
148+
clearTimeout(timer);
149+
const loadEvent = { type: "load", target: node };
150+
if (node.onload) node.onload(loadEvent);
151+
node._dispatchEvent(loadEvent);
88152
}, 100);
89153
} else if (node._type === "script" && this._document.onScript) {
90154
Promise.resolve().then(() => {
@@ -184,6 +248,81 @@ class FakeElement {
184248
set rel(value) {
185249
this._attributes.rel = value;
186250
}
251+
252+
addEventListener(event, handler) {
253+
if (!this._eventListeners) {
254+
this._eventListeners = new Map();
255+
}
256+
if (!this._eventListeners.has(event)) {
257+
this._eventListeners.set(event, []);
258+
}
259+
this._eventListeners.get(event).push(handler);
260+
}
261+
262+
removeEventListener(event, handler) {
263+
if (!this._eventListeners) return;
264+
const handlers = this._eventListeners.get(event);
265+
if (!handlers) return;
266+
const index = handlers.indexOf(handler);
267+
if (index >= 0) {
268+
handlers.splice(index, 1);
269+
}
270+
}
271+
272+
_dispatchEvent(event) {
273+
if (!this._eventListeners) return;
274+
const handlers = this._eventListeners.get(event.type);
275+
if (handlers) {
276+
for (const handler of handlers) {
277+
handler(event);
278+
}
279+
}
280+
}
281+
282+
cloneNode(deep = false) {
283+
const cloned = new FakeElement(
284+
this._document,
285+
this._type,
286+
this._document._basePath
287+
);
288+
289+
// Copy attributes
290+
cloned._attributes = { ...this._attributes };
291+
292+
// Copy src and href
293+
cloned._src = this._src;
294+
cloned._href = this._href;
295+
296+
// For link elements, create a new sheet with the same href
297+
if (this._type === "link" && this.sheet) {
298+
cloned.sheet = new FakeSheet(cloned, this._document._basePath);
299+
if (this._href) {
300+
cloned.href = this._href;
301+
}
302+
}
303+
304+
// Copy event handlers if they exist
305+
if (this.onload) {
306+
cloned.onload = this.onload;
307+
}
308+
// Copy event listeners
309+
if (this._eventListeners) {
310+
cloned._eventListeners = new Map();
311+
for (const [event, handlers] of this._eventListeners.entries()) {
312+
cloned._eventListeners.set(event, [...handlers]);
313+
}
314+
}
315+
316+
// Deep clone children if requested
317+
if (deep) {
318+
for (const child of this._children) {
319+
const clonedChild = child.cloneNode(true);
320+
cloned.appendChild(clonedChild);
321+
}
322+
}
323+
324+
return cloned;
325+
}
187326
}
188327

189328
class FakeSheet {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
body {
2+
color: red;
3+
}
4+
---
5+
body {
6+
color: blue;
7+
}
8+
---
9+
body {
10+
color: green;
11+
}
12+

0 commit comments

Comments
 (0)