Skip to content

Commit 102e1a4

Browse files
authored
feat(css): added css-style-sheet to exportType for CSSStyleSheet return (#20104)
1 parent 1baf9d3 commit 102e1a4

38 files changed

+683
-42
lines changed

declarations/WebpackOptions.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -785,7 +785,7 @@ export type CssGeneratorLocalIdentName = string;
785785
/**
786786
* Configure how CSS content is exported as default.
787787
*/
788-
export type CssParserExportType = "link" | "text";
788+
export type CssParserExportType = "link" | "text" | "css-style-sheet";
789789
/**
790790
* Enable/disable `@import` at-rules handling.
791791
*/

lib/RuntimeGlobals.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ module.exports.createScript = "__webpack_require__.ts";
7878
*/
7979
module.exports.createScriptUrl = "__webpack_require__.tu";
8080

81+
/**
82+
* merge multiple CSS stylesheets (CSSStyleSheet or string) into one CSS text string
83+
* Arguments: (sheets: Array<CSSStyleSheet | string> | CSSStyleSheet | string) => string
84+
*/
85+
module.exports.cssMergeStyleSheets = "__webpack_require__.mcs";
86+
8187
/**
8288
* The current scope when getting a module from a remote
8389
*/

lib/config/defaults.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,6 +1059,23 @@ const applyModuleDefaults = (
10591059
type: CSS_MODULE_TYPE_GLOBAL,
10601060
resolve
10611061
});
1062+
1063+
rules.push(
1064+
{
1065+
with: { type: "css" },
1066+
parser: {
1067+
exportType: "css-style-sheet"
1068+
},
1069+
resolve
1070+
},
1071+
{
1072+
assert: { type: "css" },
1073+
parser: {
1074+
exportType: "css-style-sheet"
1075+
},
1076+
resolve
1077+
}
1078+
);
10621079
}
10631080
rules.push(
10641081
{

lib/css/CssGenerator.js

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const memoize = require("../util/memoize");
4444
/** @typedef {import("../CssModule")} CssModule */
4545
/** @typedef {import("../Compilation")} Compilation */
4646
/** @typedef {import("../Module").RuntimeRequirements} RuntimeRequirements */
47+
/** @typedef {import("../../declarations/WebpackOptions").CssParserExportType} CssParserExportType */
4748

4849
const getPropertyName = memoize(() => require("../util/propertyName"));
4950
const getCssModulesPlugin = memoize(() => require("./CssModulesPlugin"));
@@ -83,17 +84,18 @@ class CssGenerator extends Generator {
8384
* Generate JavaScript code that requires and concatenates all CSS imports
8485
* @param {NormalModule} module the module to generate CSS text for
8586
* @param {GenerateContext} generateContext the generate context
86-
* @returns {{ expr: string }[]} JavaScript code that concatenates all imported CSS
87+
* @returns {{ expr: string, type: CssParserExportType }[]} JavaScript code that concatenates all imported CSS
8788
*/
8889
_generateImportCode(module, generateContext) {
8990
const moduleGraph = generateContext.moduleGraph;
90-
/** @type {{ expr: string }[]} */
91+
/** @type {{ expr: string, type: CssParserExportType }[]} */
9192
const parts = [];
9293

9394
// Iterate through module.dependencies to maintain source order
9495
for (const dep of module.dependencies) {
9596
if (dep instanceof CssImportDependency) {
96-
const depModule = moduleGraph.getModule(dep);
97+
/** @type {CssModule} */
98+
const depModule = /** @type {CssModule} */ (moduleGraph.getModule(dep));
9799
const importVar = generateContext.runtimeTemplate.moduleExports({
98100
module: depModule,
99101
chunkGraph: generateContext.chunkGraph,
@@ -106,7 +108,10 @@ class CssGenerator extends Generator {
106108
RuntimeGlobals.compatGetDefaultExport
107109
);
108110
parts.push({
109-
expr: `(${RuntimeGlobals.compatGetDefaultExport}(${importVar})() || "")`
111+
expr: `(${RuntimeGlobals.compatGetDefaultExport}(${importVar})() || "")`,
112+
type: /** @type {CssParserExportType} */ (
113+
/** @type {BuildMeta} */ (depModule.buildMeta).exportType
114+
)
110115
});
111116
}
112117
}
@@ -308,21 +313,47 @@ class CssGenerator extends Generator {
308313
...generateContext,
309314
cssData
310315
});
316+
317+
const generateCssText = () => {
318+
const importCode = this._generateImportCode(module, generateContext);
319+
const moduleCode = this._generateModuleCode(module, generateContext);
320+
321+
if (importCode.length > 0) {
322+
if (
323+
exportType === "css-style-sheet" ||
324+
importCode.some((part) => part.type !== exportType)
325+
) {
326+
generateContext.runtimeRequirements.add(
327+
RuntimeGlobals.cssMergeStyleSheets
328+
);
329+
330+
return `${RuntimeGlobals.cssMergeStyleSheets}([${[...importCode.map((part) => part.expr), JSON.stringify(moduleCode)].join(", ")}])`;
331+
}
332+
return generateContext.runtimeTemplate.concatenation(
333+
...importCode,
334+
moduleCode
335+
);
336+
}
337+
return JSON.stringify(moduleCode);
338+
};
339+
311340
/**
312341
* @returns {string | null} the default export
313342
*/
314343
const generateJSDefaultExport = () => {
315344
switch (exportType) {
316345
case "text": {
317-
const importCode = this._generateImportCode(module, generateContext);
318-
const moduleCode = this._generateModuleCode(module, generateContext);
319-
320-
return generateContext.runtimeTemplate.concatenation(
321-
...importCode,
322-
moduleCode
323-
);
346+
return generateCssText();
347+
}
348+
case "css-style-sheet": {
349+
const constOrVar = generateContext.runtimeTemplate.renderConst();
350+
return `(${generateContext.runtimeTemplate.basicFunction("", [
351+
`${constOrVar} cssText = ${generateCssText()};`,
352+
`${constOrVar} sheet = new CSSStyleSheet();`,
353+
"sheet.replaceSync(cssText);",
354+
"return sheet;"
355+
])})()`;
324356
}
325-
// case "style-sheet"
326357
default:
327358
return null;
328359
}
@@ -428,7 +459,9 @@ class CssGenerator extends Generator {
428459
);
429460
}
430461
case "css": {
431-
generateContext.runtimeRequirements.add(RuntimeGlobals.hasCssModules);
462+
if (!this._generatesJsOnly(module)) {
463+
generateContext.runtimeRequirements.add(RuntimeGlobals.hasCssModules);
464+
}
432465

433466
return InitFragment.addToSource(source, initFragments, generateContext);
434467
}
@@ -463,10 +496,7 @@ class CssGenerator extends Generator {
463496
* @returns {SourceTypes} available types (do not mutate)
464497
*/
465498
getTypes(module) {
466-
const exportType = /** @type {BuildMeta} */ (module.buildMeta).exportType;
467-
const onlyJsOutput =
468-
this._exportsOnly || (exportType && exportType !== "link");
469-
if (onlyJsOutput) {
499+
if (this._generatesJsOnly(module)) {
470500
return JS_TYPES;
471501
}
472502
const sourceTypes = new Set();
@@ -534,6 +564,18 @@ class CssGenerator extends Generator {
534564
updateHash(hash, { module }) {
535565
hash.update(/** @type {boolean} */ (this._esModule).toString());
536566
}
567+
568+
/**
569+
* @param {NormalModule} module module
570+
* @returns {boolean} true if the module only outputs JavaScript
571+
*/
572+
_generatesJsOnly(module) {
573+
const exportType = /** @type {BuildMeta} */ (module.buildMeta).exportType;
574+
return (
575+
this._exportsOnly ||
576+
/** @type {boolean} */ (exportType && exportType !== "link")
577+
);
578+
}
537579
}
538580

539581
module.exports = CssGenerator;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
MIT License http://www.opensource.org/licenses/mit-license.php
3+
Author Natsu @xiaoxiaojx
4+
*/
5+
6+
"use strict";
7+
8+
const RuntimeGlobals = require("../RuntimeGlobals");
9+
const RuntimeModule = require("../RuntimeModule");
10+
const Template = require("../Template");
11+
12+
/** @typedef {import("../Chunk")} Chunk */
13+
14+
class CssMergeStyleSheetsRuntimeModule extends RuntimeModule {
15+
constructor() {
16+
super("css merge stylesheets");
17+
}
18+
19+
/**
20+
* @returns {string | null} runtime code
21+
*/
22+
generate() {
23+
const { runtimeTemplate } = /** @type {import("../Compilation")} */ (
24+
this.compilation
25+
);
26+
27+
return Template.asString([
28+
`${RuntimeGlobals.cssMergeStyleSheets} = ${runtimeTemplate.basicFunction(
29+
"sheets",
30+
[
31+
"var sheetsArray = Array.isArray(sheets) ? sheets : [sheets];",
32+
"var cssTexts = [];",
33+
"for (var i = 0; i < sheetsArray.length; i++) {",
34+
Template.indent([
35+
"var s = sheetsArray[i];",
36+
"if (!s) continue;",
37+
"if (typeof s === 'string') {",
38+
Template.indent("cssTexts.push(s);"),
39+
"} else if (s.cssRules) {",
40+
Template.indent([
41+
"var rules = s.cssRules;",
42+
"for (var j = 0; j < rules.length; j++) {",
43+
Template.indent("cssTexts.push(rules[j].cssText);"),
44+
"}"
45+
]),
46+
"}"
47+
]),
48+
"}",
49+
"return cssTexts.join('');"
50+
]
51+
)};`
52+
]);
53+
}
54+
}
55+
56+
module.exports = CssMergeStyleSheetsRuntimeModule;

lib/css/CssModulesPlugin.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const memoize = require("../util/memoize");
4848
const nonNumericOnlyHash = require("../util/nonNumericOnlyHash");
4949
const removeBOM = require("../util/removeBOM");
5050
const CssGenerator = require("./CssGenerator");
51+
const CssMergeStyleSheetsRuntimeModule = require("./CssMergeStyleSheetsRuntimeModule");
5152
const CssParser = require("./CssParser");
5253

5354
/** @typedef {import("webpack-sources").Source} Source */
@@ -637,6 +638,15 @@ class CssModulesPlugin {
637638
set.add(RuntimeGlobals.publicPath);
638639
set.add(RuntimeGlobals.getChunkCssFilename);
639640
});
641+
642+
compilation.hooks.runtimeRequirementInTree
643+
.for(RuntimeGlobals.cssMergeStyleSheets)
644+
.tap(PLUGIN_NAME, (chunk) => {
645+
compilation.addRuntimeModule(
646+
chunk,
647+
new CssMergeStyleSheetsRuntimeModule()
648+
);
649+
});
640650
}
641651
);
642652
}

lib/css/CssParser.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
const vm = require("vm");
99
const CommentCompilationWarning = require("../CommentCompilationWarning");
10+
const CssModule = require("../CssModule");
1011
const ModuleDependencyWarning = require("../ModuleDependencyWarning");
1112
const { CSS_MODULE_TYPE_AUTO } = require("../ModuleTypeConstants");
1213
const Parser = require("../Parser");
@@ -345,7 +346,7 @@ class CssParser extends Parser {
345346
importOption = true,
346347
url = true,
347348
namedExports = true,
348-
exportType = "link"
349+
exportType
349350
} = {}) {
350351
super();
351352
this.defaultMode = defaultMode;
@@ -1814,12 +1815,30 @@ class CssParser extends Parser {
18141815

18151816
const buildMeta = /** @type {BuildMeta} */ (state.module.buildMeta);
18161817

1817-
buildMeta.exportType = this.exportType;
18181818
buildMeta.exportsType = this.namedExports ? "namespace" : "default";
18191819
buildMeta.defaultObject = this.namedExports ? false : "redirect-warn";
1820+
buildMeta.exportType = this.exportType;
1821+
1822+
if (!buildMeta.exportType) {
1823+
// Inherit exportType from parent module to ensure consistency.
1824+
// When a CSS file is imported with syntax like `import "./basic.css" with { type: "css" }`,
1825+
// the parent module's exportType is set to "css-style-sheet".
1826+
// Child modules imported via @import should inherit this exportType
1827+
// instead of using the default "link", ensuring that the entire
1828+
// import chain uses the same export format.
1829+
const parent = state.compilation.moduleGraph.getIssuer(module);
1830+
if (parent instanceof CssModule) {
1831+
buildMeta.exportType = /** @type {BuildMeta} */ (
1832+
parent.buildMeta
1833+
).exportType;
1834+
}
1835+
}
18201836

18211837
// TODO this.namedExports?
1822-
if (this.exportType === "text") {
1838+
if (
1839+
buildMeta.exportType === "text" ||
1840+
buildMeta.exportType === "css-style-sheet"
1841+
) {
18231842
module.addDependency(new StaticExportsDependency(["default"], true));
18241843
} else {
18251844
module.addDependency(new StaticExportsDependency([], true));

schemas/WebpackOptions.check.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

schemas/WebpackOptions.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,7 @@
552552
},
553553
"CssParserExportType": {
554554
"description": "Configure how CSS content is exported as default.",
555-
"enum": ["link", "text"]
555+
"enum": ["link", "text", "css-style-sheet"]
556556
},
557557
"CssParserImport": {
558558
"description": "Enable/disable `@import` at-rules handling.",

schemas/plugins/css/CssAutoParserOptions.check.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)