fix(css): avoid extra `module.export` output for css module (#19265)

This commit is contained in:
hai-x 2025-04-06 20:53:42 +08:00 committed by GitHub
parent e5c3f95b84
commit e0891eeea0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 435 additions and 40 deletions

View File

@ -105,6 +105,7 @@ const makeSerializable = require("./util/makeSerializable");
* @property {boolean=} async
* @property {boolean=} sideEffectFree
* @property {Record<string, string>=} exportsFinalName
* @property {boolean=} isCSSModule
*/
/**

View File

@ -34,6 +34,11 @@ const ASSET_AND_JS_AND_CSS_URL_TYPES = new Set([
"css-url"
]);
/**
* @type {"javascript"}
*/
const JS_TYPE = "javascript";
/**
* @type {ReadonlySet<"javascript">}
*/
@ -54,6 +59,10 @@ const JS_AND_CSS_URL_TYPES = new Set(["javascript", "css-url"]);
*/
const JS_AND_CSS_TYPES = new Set(["javascript", "css"]);
/**
* @type {"css"}
*/
const CSS_TYPE = "css";
/**
* @type {ReadonlySet<"css">}
*/
@ -94,6 +103,7 @@ const CONSUME_SHARED_TYPES = new Set(["consume-shared"]);
const SHARED_INIT_TYPES = new Set(["share-init"]);
module.exports.NO_TYPES = NO_TYPES;
module.exports.JS_TYPE = JS_TYPE;
module.exports.JS_TYPES = JS_TYPES;
module.exports.JS_AND_CSS_TYPES = JS_AND_CSS_TYPES;
module.exports.JS_AND_CSS_URL_TYPES = JS_AND_CSS_URL_TYPES;
@ -102,6 +112,7 @@ module.exports.ASSET_TYPES = ASSET_TYPES;
module.exports.ASSET_AND_JS_TYPES = ASSET_AND_JS_TYPES;
module.exports.ASSET_AND_CSS_URL_TYPES = ASSET_AND_CSS_URL_TYPES;
module.exports.ASSET_AND_JS_AND_CSS_URL_TYPES = ASSET_AND_JS_AND_CSS_URL_TYPES;
module.exports.CSS_TYPE = CSS_TYPE;
module.exports.CSS_TYPES = CSS_TYPES;
module.exports.CSS_URL_TYPES = CSS_URL_TYPES;
module.exports.CSS_IMPORT_TYPES = CSS_IMPORT_TYPES;

View File

@ -11,7 +11,10 @@ const Generator = require("../Generator");
const InitFragment = require("../InitFragment");
const {
JS_AND_CSS_EXPORT_TYPES,
JS_AND_CSS_TYPES
JS_AND_CSS_TYPES,
CSS_TYPES,
JS_TYPE,
CSS_TYPE
} = require("../ModuleSourceTypesConstants");
const RuntimeGlobals = require("../RuntimeGlobals");
const Template = require("../Template");
@ -27,21 +30,25 @@ const Template = require("../Template");
/** @typedef {import("../Generator").GenerateContext} GenerateContext */
/** @typedef {import("../Generator").UpdateHashContext} UpdateHashContext */
/** @typedef {import("../Module").BuildInfo} BuildInfo */
/** @typedef {import("../Module").BuildMeta} BuildMeta */
/** @typedef {import("../Module").ConcatenationBailoutReasonContext} ConcatenationBailoutReasonContext */
/** @typedef {import("../Module").SourceTypes} SourceTypes */
/** @typedef {import("../ModuleGraph")} ModuleGraph */
/** @typedef {import("../NormalModule")} NormalModule */
/** @typedef {import("../util/Hash")} Hash */
class CssGenerator extends Generator {
/**
* @param {CssAutoGeneratorOptions | CssGlobalGeneratorOptions | CssModuleGeneratorOptions} options options
* @param {ModuleGraph} moduleGraph the module graph
*/
constructor(options) {
constructor(options, moduleGraph) {
super();
this.convention = options.exportsConvention;
this.localIdentName = options.localIdentName;
this.exportsOnly = options.exportsOnly;
this.esModule = options.esModule;
this._moduleGraph = moduleGraph;
}
/**
@ -169,6 +176,13 @@ class CssGenerator extends Generator {
return source;
}
if (
cssData.exports.size === 0 &&
!(/** @type {BuildMeta} */ (module.buildMeta).isCSSModule)
) {
return new RawSource("");
}
const needNsObj =
this.esModule &&
generateContext.moduleGraph
@ -237,7 +251,22 @@ class CssGenerator extends Generator {
*/
getTypes(module) {
// TODO, find a better way to prevent the original module from being removed after concatenation, maybe it is a bug
return this.exportsOnly ? JS_AND_CSS_EXPORT_TYPES : JS_AND_CSS_TYPES;
if (this.exportsOnly) {
return JS_AND_CSS_EXPORT_TYPES;
}
const sourceTypes = new Set();
const connections = this._moduleGraph.getIncomingConnections(module);
for (const connection of connections) {
if (!connection.originModule) {
continue;
}
if (connection.originModule.type.split("/")[0] !== CSS_TYPE)
sourceTypes.add(JS_TYPE);
}
if (sourceTypes.has(JS_TYPE)) {
return JS_AND_CSS_TYPES;
}
return CSS_TYPES;
}
/**
@ -248,12 +277,17 @@ class CssGenerator extends Generator {
getSize(module, type) {
switch (type) {
case "javascript": {
const buildInfo = /** @type {BuildInfo} */ (module.buildInfo);
if (!buildInfo.cssData) {
const cssData = /** @type {BuildInfo} */ (module.buildInfo).cssData;
if (!cssData) {
return 42;
}
const exports = buildInfo.cssData.exports;
if (cssData.exports.size === 0) {
if (/** @type {BuildMeta} */ (module.buildMeta).isCSSModule) {
return 42;
}
return 0;
}
const exports = cssData.exports;
const stringifiedExports = JSON.stringify(
Array.from(exports).reduce((obj, [key, value]) => {
obj[key] = value;

View File

@ -301,7 +301,10 @@ class CssModulesPlugin {
.tap(PLUGIN_NAME, generatorOptions => {
validateGeneratorOptions[type](generatorOptions);
return new CssGenerator(generatorOptions);
return new CssGenerator(
generatorOptions,
compilation.moduleGraph
);
});
normalModuleFactory.hooks.createModuleClass
.for(type)

View File

@ -361,6 +361,9 @@ class CssParser extends Parser {
const isModules = mode === "global" || mode === "local";
/** @type {BuildMeta} */
(module.buildMeta).isCSSModule = isModules;
const locConverter = new LocConverter(source);
/** @type {number} */

View File

@ -8,6 +8,7 @@
const asyncLib = require("neo-async");
const ChunkGraph = require("../ChunkGraph");
const ModuleGraph = require("../ModuleGraph");
const { JS_TYPE } = require("../ModuleSourceTypesConstants");
const { STAGE_DEFAULT } = require("../OptimizationStages");
const HarmonyImportDependency = require("../dependencies/HarmonyImportDependency");
const { compareModulesByIdentifier } = require("../util/comparators");
@ -452,7 +453,7 @@ class ModuleConcatenationPlugin {
chunkGraph.disconnectChunkAndModule(chunk, m);
} else {
const newSourceTypes = new Set(sourceTypes);
newSourceTypes.delete("javascript");
newSourceTypes.delete(JS_TYPE);
chunkGraph.setChunkModuleSourceTypes(
chunk,
m,

View File

@ -8,6 +8,7 @@ const vm = require("vm");
const rimraf = require("rimraf");
const checkArrayExpectation = require("./checkArrayExpectation");
const createLazyTestEnv = require("./helpers/createLazyTestEnv");
const FakeDocument = require("./helpers/FakeDocument");
const casesPath = path.join(__dirname, "hotCases");
let categories = fs
@ -108,8 +109,7 @@ const describeCases = config => {
// ignored
}
compiler = webpack(options);
compiler.run((err, stats) => {
const onCompiled = (err, stats) => {
if (err) return done(err);
const jsonStats = stats.toJson({
errorDetails: true
@ -179,9 +179,8 @@ const describeCases = config => {
},
document: {
createElement(type) {
return {
const ele = {
_type: type,
sheet: {},
getAttribute(name) {
return this[name];
},
@ -199,6 +198,11 @@ const describeCases = config => {
}
}
};
ele.sheet =
type === "link"
? new FakeDocument.FakeSheet(ele, outputDirectory)
: {};
return ele;
},
head: {
appendChild(element) {
@ -353,8 +357,15 @@ const describeCases = config => {
let promise = Promise.resolve();
const info = stats.toJson({ all: false, entrypoints: true });
if (config.target === "web") {
for (const file of info.entrypoints.main.assets)
_require(`./${file.name}`);
for (const file of info.entrypoints.main.assets) {
if (file.name.endsWith(".css")) {
const link = window.document.createElement("link");
link.href = path.join(outputDirectory, file.name);
window.document.head.appendChild(link);
} else {
_require(`./${file.name}`);
}
}
} else {
const assets = info.entrypoints.main.assets;
const result = _require(
@ -375,7 +386,9 @@ const describeCases = config => {
done(err);
}
);
});
};
compiler = webpack(options);
compiler.run(onCompiled);
}, 20000);
const {

View File

@ -1,6 +1,6 @@
it("should compile and load style on demand", (done) => {
import("./style.css").then(x => {
expect(x).toEqual(nsObj({}));
expect(x).toEqual({});
const style = getComputedStyle(document.body);
expect(style.getPropertyValue("background")).toBe(" red");
expect(style.getPropertyValue("margin")).toBe(" 10px");

View File

@ -1,9 +1,9 @@
import * as style from "./style.css";
it("should compile and load style on demand", done => {
expect(style).toEqual(nsObj({}));
expect(style).toEqual({});
import("./style2.css").then(x => {
expect(x).toEqual(nsObj({}));
expect(x).toEqual({});
done();
}, done);
});

View File

@ -1,9 +1,9 @@
import * as style from "./style.css";
it("should compile and load style on demand", done => {
expect(style).toEqual(nsObj({}));
expect(style).toEqual({});
import("./style2.css").then(x => {
expect(x).toEqual(nsObj({}));
expect(x).toEqual({});
const style = getComputedStyle(document.body);
expect(style.getPropertyValue("background")).toBe(" red");
expect(style.getPropertyValue("margin")).toBe(" 10px");

View File

@ -1,7 +1,7 @@
import * as style from "./style.css";
it("should compile and load initial style", () => {
expect(style).toEqual(nsObj({}));
expect(style).toEqual({});
const computedStyle = getComputedStyle(document.body);
expect(computedStyle.getPropertyValue("background")).toBe(" red");
expect(computedStyle.getPropertyValue("margin")).toBe(" 10px");

View File

@ -1,9 +1,9 @@
import * as style from "./style.css";
it("should compile and load style on demand", done => {
expect(style).toEqual(nsObj({}));
expect(style).toEqual({});
import("./style2.css").then(x => {
expect(x).toEqual(nsObj({}));
expect(x).toEqual({});
const style = getComputedStyle(document.body);
expect(style.getPropertyValue("background")).toBe(" red");
expect(style.getPropertyValue("margin")).toBe(" 10px");

View File

@ -1,9 +1,9 @@
import * as style from "./style.css";
it("should compile and load style on demand", done => {
expect(style).toEqual(nsObj({}));
expect(style).toEqual({});
import("./style2.css").then(x => {
expect(x).toEqual(nsObj({}));
expect(x).toEqual({});
const style = getComputedStyle(document.body);
expect(style.getPropertyValue("background")).toBe(" red");
expect(style.getPropertyValue("margin")).toBe(" 10px");

View File

@ -8,7 +8,7 @@ it("should work with js", done => {
});
it("should work with css", done => {
expect(style).toEqual(nsObj({}));
expect(style).toEqual({});
const computedStyle = getComputedStyle(document.body);
@ -16,7 +16,7 @@ it("should work with css", done => {
expect(computedStyle.getPropertyValue("color")).toBe(" yellow");
import("./async.css").then(x => {
expect(x).toEqual(nsObj({}));
expect(x).toEqual({});
const style = getComputedStyle(document.body);

View File

@ -1,6 +1,6 @@
it("should import an external css", done => {
import("../external/style.css").then(x => {
expect(x).toEqual(nsObj({}));
expect(x).toEqual({});
done();
}, done);
});

View File

@ -1,6 +1,6 @@
it("should import an external css", done => {
import("./style.css").then(x => {
expect(x).toEqual(nsObj({}));
expect(x).toEqual({});
const style = getComputedStyle(document.body);
expect(style.getPropertyValue("color")).toBe(" green");
expect(style.getPropertyValue("background")).toBe(

View File

@ -1,7 +1,7 @@
import * as style from "./style.css";
it("should compile and load style on demand", () => {
expect(style).toEqual(nsObj({}));
expect(style).toEqual({});
const computedStyle = getComputedStyle(document.body);
expect(computedStyle.getPropertyValue("background")).toBe(" red");
expect(computedStyle.getPropertyValue("margin")).toBe(" 10px");

View File

@ -0,0 +1,3 @@
.bar {
background-color: black;
}

View File

@ -0,0 +1,3 @@
.bar {
background-color: black;
}

View File

@ -0,0 +1,3 @@
.bar {
background-color: black;
}

View File

@ -0,0 +1,3 @@
.bar {
background-color: black;
}

View File

@ -0,0 +1,3 @@
.bar {
background-color: black;
}

View File

@ -0,0 +1,5 @@
@import url("./a1.css");
.foo {
background-color: red;
}

View File

@ -0,0 +1,4 @@
import "./main.css"
require("./a2.css")
import("./a2.css").then(() => {})

View File

@ -0,0 +1,3 @@
import a1 from "./a1.module.css"
const a2 = require("./a2.module.css")
import("./a3.module.css").then(() => {})

View File

@ -0,0 +1,12 @@
module.exports = {
findBundle: function (i) {
switch (i) {
case 0:
return ["test.js"];
case 1:
return ["test.js", `1/main.js`];
case 2:
return ["test.js", `2/main.js`];
}
}
};

View File

@ -0,0 +1,30 @@
it("should work", () => {
const stats = __STATS__.children[__STATS_I__];
expect(stats.assets.findIndex(a => a.name === "test.js") > -1).toBe(true);
expect(
stats.assets.findIndex(a => a.name === `${__STATS_I__}/main.css`) > -1
).toBe(true);
if (__STATS_I__ === 0) {
// ./main.css
// ./a.css
// and it still output two runtime module:
// 'webpack/runtime/make namespace object'
// 'webpack/runtime/css loading'
expect(stats.modules.length).toBe(4);
} else if (__STATS_I__ === 1) {
stats.modules
.filter(module => module.moduleType === "css/auto")
.forEach(module => {
expect(module.sizes["javascript"] === 1).toBe(true);
});
} else if (__STATS_I__ === 2) {
stats.modules
.filter(module => module.moduleType === "css/auto")
.forEach(module => {
expect(module.sizes["javascript"] === 1).toBe(false);
});
}
});

View File

@ -0,0 +1,69 @@
const path = require("path");
const fs = require("fs");
const webpack = require("../../../../");
const entry = i => {
switch (i) {
case 0:
return {
main: ["./main.css"]
};
case 1:
return {
main: ["./main1.js"]
};
case 2:
return {
main: ["./main2.js"]
};
}
};
/**
* @param {number} i param
* @returns {import("../../../../").Configuration} return
*/
const common = i => ({
entry: {
...entry(i)
},
target: "web",
devtool: false,
experiments: {
css: true
},
output: {
filename: `${i}/[name].js`,
chunkFilename: `${i}/[name].js`,
cssFilename: `${i}/[name].css`,
cssChunkFilename: `${i}/[name].css`
},
plugins: [
{
apply(compiler) {
compiler.hooks.compilation.tap("Test", compilation => {
compilation.hooks.processAssets.tap(
{
name: "copy-webpack-plugin",
stage:
compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL
},
() => {
const data = fs.readFileSync(
path.resolve(__dirname, "./test.js")
);
compilation.emitAsset(
"test.js",
new webpack.sources.RawSource(data)
);
}
);
});
}
}
]
});
/** @type {import("../../../../").Configuration[]} */
module.exports = [...[0, 1].map(i => common(i))];

View File

@ -1,9 +1,9 @@
import * as style from "./style.css";
it("should compile and load style on demand", done => {
expect(style).toEqual(nsObj({}));
expect(style).toEqual({});
import("./style2.css").then(x => {
expect(x).toEqual(nsObj({}));
expect(x).toEqual({});
const style = getComputedStyle(document.body);
expect(style.getPropertyValue("background")).toBe(" red");
expect(style.getPropertyValue("margin")).toBe(" 10px");

View File

@ -2,7 +2,7 @@ import * as styles1 from "./style.less";
import * as styles2 from "./style.modules.less";
it("should prefer relative", () => {
expect(styles1).toEqual(nsObj({}));
expect(styles1).toEqual({});
expect(styles2).toEqual(nsObj({
"style-module": "_style_modules_less-style-module",
}));

View File

@ -2,7 +2,7 @@ import * as styles1 from "./style.css";
import * as styles2 from "./style.modules.css";
it("should prefer relative", () => {
expect(styles1).toEqual(nsObj({}));
expect(styles1).toEqual({});
expect(styles2).toEqual(nsObj({
"style-module": "_style_modules_css-style-module",
}));

View File

@ -2,13 +2,13 @@ import * as pureStyle from "./style.css";
import * as styles from "./style.modules.css";
it("should work", done => {
expect(pureStyle).toEqual(nsObj({}));
expect(pureStyle).toEqual({});
const style = getComputedStyle(document.body);
expect(style.getPropertyValue("background")).toBe(" red");
expect(styles.foo).toBe('_style_modules_css-foo');
import(/* webpackPrefetch: true */ "./style2.css").then(x => {
expect(x).toEqual(nsObj({}));
expect(x).toEqual({});
const style = getComputedStyle(document.body);
expect(style.getPropertyValue("color")).toBe(" blue");

View File

@ -7,7 +7,7 @@ it(`should generate correct url public path with css filename`, done => {
document.body.appendChild(h1);
import("./index.css").then(x => {
try {
expect(x).toEqual(nsObj({}));
expect(x).toEqual({});
const style1 = getComputedStyle(h1);
expect(style1).toMatchSnapshot();
const style2 = getComputedStyle(h2);

View File

@ -5,7 +5,7 @@ function getPropertyValue(property) {
return this[property];
}
module.exports = class FakeDocument {
class FakeDocument {
constructor(basePath) {
this.head = this.createElement("head");
this.body = this.createElement("body");
@ -54,7 +54,7 @@ module.exports = class FakeDocument {
}
return style;
}
};
}
class FakeElement {
constructor(document, type, basePath) {
@ -252,3 +252,8 @@ class FakeSheet {
return rules;
}
}
FakeDocument.FakeSheet = FakeSheet;
FakeDocument.FakeElement = FakeDocument;
module.exports = FakeDocument;

View File

@ -0,0 +1,4 @@
.html {
color: green;
}

View File

@ -0,0 +1,9 @@
@import url("./a.css");
---
html {
color: blue;
}
---
html {
color: yellow;
}

View File

@ -0,0 +1,21 @@
import "./index.css"
it("should work", done => {
const links = window.document.getElementsByTagName("link");
expect(links[0].sheet.css).toContain("color: green;");
NEXT(
require("../../update")(done, true, () => {
const links = window.document.getElementsByTagName("link");
expect(links[0].sheet.css).toContain("color: blue;");
NEXT(
require("../../update")(done, true, () => {
const links = window.document.getElementsByTagName("link");
expect(links[0].sheet.css).toContain("color: yellow;");
done();
})
);
})
);
});

View File

@ -0,0 +1,5 @@
module.exports = function (config) {
if (config.target !== "web") {
return false;
}
};

View File

@ -0,0 +1,8 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
mode: "development",
devtool: false,
experiments: {
css: true
}
};

View File

@ -0,0 +1,11 @@
.html {
color: red;
}
---
html {
color: blue;
}
---
html {
color: yellow;
}

View File

@ -0,0 +1,19 @@
it("should work", done => {
const links = window.document.getElementsByTagName("link");
expect(links[0].sheet.css).toContain("color: red;");
NEXT(
require("../../update")(done, true, () => {
const links = window.document.getElementsByTagName("link");
expect(links[0].sheet.css).toContain("color: blue;");
NEXT(
require("../../update")(done, true, () => {
const links = window.document.getElementsByTagName("link");
expect(links[0].sheet.css).toContain("color: yellow;");
done();
})
);
})
);
});

View File

@ -0,0 +1,5 @@
module.exports = function (config) {
if (config.target !== "web") {
return false;
}
};

View File

@ -0,0 +1,29 @@
const webpack = require("../../../../");
/** @type {import("../../../../").Configuration} */
module.exports = {
mode: "development",
devtool: false,
entry: ["./index.js", "./index.css"],
experiments: {
css: true
},
plugins: [
{
apply(compiler) {
compiler.hooks.compilation.tap("Test", compilation => {
compilation.hooks.additionalTreeRuntimeRequirements.tap(
"Test",
(module, set, context) => {
// To prevent the runtime error `ReferenceError: __webpack_exports__ is not defined`,
// which occurs because the default `output.library` setting is `commonjs2`,
// resulting in adding `module.exports = __webpack_exports__;`.
set.add(webpack.RuntimeGlobals.startup);
set.add(webpack.RuntimeGlobals.exports);
}
);
});
}
}
]
};

View File

@ -0,0 +1,39 @@
const getFile = name =>
__non_webpack_require__("fs").readFileSync(
__non_webpack_require__("path").join(__dirname, name),
"utf-8"
);
it("should work", async function (done) {
let promise = import("./style.css");
NEXT(
require("../../update")(done, true, () => {
promise.then(res => {
const links = window.document.getElementsByTagName("link");
let href = links[0].href;
expect(href).toBe("https://test.cases/path/style_css.css");
href = href
.replace(/^https:\/\/test\.cases\/path\//, "")
.replace(/^https:\/\/example\.com\//, "");
let sheet = getFile(href);
expect(sheet).toContain("color: red;");
module.hot.accept("./style.css", () => {
const links = window.document.getElementsByTagName("link");
let href = links[0].href;
expect(href).toContain("https://test.cases/path/style_css.css?hmr");
href = href
.replace(/^https:\/\/test\.cases\/path\//, "")
.replace(/^https:\/\/example\.com\//, "")
.split("?")[0];
let sheet = getFile(href);
expect(sheet).toContain("color: blue;");
done();
});
NEXT(require("../../update")(done));
});
})
);
});

View File

@ -0,0 +1,11 @@
html {
color: red;
}
---
html {
color: red;
}
---
html {
color: blue;
}

View File

@ -0,0 +1,5 @@
module.exports = function (config) {
if (config.target !== "web") {
return false;
}
};

View File

@ -0,0 +1,19 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
mode: "development",
devtool: false,
output: {
cssFilename: "[name].css",
cssChunkFilename: "[name].css"
},
experiments: {
css: true,
lazyCompilation: {
entries: false,
imports: true
}
},
node: {
__dirname: false
}
};

1
types.d.ts vendored
View File

@ -7617,6 +7617,7 @@ declare interface KnownBuildMeta {
async?: boolean;
sideEffectFree?: boolean;
exportsFinalName?: Record<string, string>;
isCSSModule?: boolean;
}
declare interface KnownCreateStatsOptionsContext {
forToString?: boolean;