Skip to content

Commit 2b0a4ac

Browse files
authored
fix: correct url resolution and preserve source maps for non-link CSS export types (#20717)
1 parent 118e0c5 commit 2b0a4ac

12 files changed

Lines changed: 290 additions & 71 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+
Correct url() path resolution and preserve source maps for non-link CSS export types (style, text, css-style-sheet)

lib/css/CssGenerator.js

Lines changed: 123 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55

66
"use strict";
77

8-
const { ConcatSource, RawSource, ReplaceSource } = require("webpack-sources");
8+
const {
9+
ConcatSource,
10+
RawSource,
11+
ReplaceSource,
12+
SourceMapSource
13+
} = require("webpack-sources");
914
const { UsageState } = require("../ExportsInfo");
1015
const Generator = require("../Generator");
1116
const InitFragment = require("../InitFragment");
@@ -20,7 +25,7 @@ const RuntimeGlobals = require("../RuntimeGlobals");
2025
const Template = require("../Template");
2126
const CssImportDependency = require("../dependencies/CssImportDependency");
2227
const HarmonyImportSideEffectDependency = require("../dependencies/HarmonyImportSideEffectDependency");
23-
const { getUndoPath } = require("../util/identifier");
28+
2429
const memoize = require("../util/memoize");
2530

2631
/** @typedef {import("webpack-sources").Source} Source */
@@ -127,12 +132,12 @@ class CssGenerator extends Generator {
127132
}
128133

129134
/**
130-
* Generate CSS code for the current module
131-
* @param {NormalModule} module the module to generate CSS code for
135+
* Generate CSS source for the current module
136+
* @param {NormalModule} module the module to generate CSS source for
132137
* @param {GenerateContext} generateContext the generate context
133-
* @returns {string} the CSS code as string
138+
* @returns {Source | null} the CSS source
134139
*/
135-
_generateContentCode(module, generateContext) {
140+
_generateContentSource(module, generateContext) {
136141
const moduleSourceContent = /** @type {Source} */ (
137142
this.generate(module, {
138143
...generateContext,
@@ -141,26 +146,19 @@ class CssGenerator extends Generator {
141146
);
142147

143148
if (!moduleSourceContent) {
144-
return "";
149+
return null;
145150
}
146151

147152
const compilation = generateContext.runtimeTemplate.compilation;
148-
const { path: filename } = compilation.getPathWithInfo(
149-
compilation.outputOptions.cssChunkFilename,
150-
{
151-
runtime: generateContext.runtime,
152-
contentHashType: "css"
153-
}
154-
);
155-
const undoPath = getUndoPath(
156-
filename,
157-
compilation.outputOptions.path,
158-
false
159-
);
153+
// For non-link exportTypes (style, text, css-style-sheet), url() in the CSS
154+
// is resolved relative to the document URL (for <style> tags and CSSStyleSheet),
155+
// not relative to any output file. Use empty undoPath so urls are relative to
156+
// the output root.
157+
const undoPath = "";
160158

161159
const CssModulesPlugin = getCssModulesPlugin();
162160
const hooks = CssModulesPlugin.getCompilationHooks(compilation);
163-
const renderedSource = CssModulesPlugin.renderModule(
161+
return CssModulesPlugin.renderModule(
164162
/** @type {CssModule} */ (module),
165163
{
166164
undoPath,
@@ -170,13 +168,23 @@ class CssGenerator extends Generator {
170168
},
171169
hooks
172170
);
171+
}
173172

174-
if (!renderedSource) {
175-
return "";
173+
/**
174+
* Convert a CSS Source to a JS string literal Source, preserving source map.
175+
* Wraps the CSS content with JSON.stringify so it can be embedded in JS code.
176+
* @param {Source} cssSource the CSS source
177+
* @param {NormalModule} module the module
178+
* @returns {Source} a Source representing a JS string literal
179+
*/
180+
_cssSourceToJsStringLiteral(cssSource, module) {
181+
const { source, map } = cssSource.sourceAndMap();
182+
const content = /** @type {string} */ (source);
183+
const escaped = JSON.stringify(content);
184+
if (map) {
185+
return new SourceMapSource(escaped, module.identifier(), map, content);
176186
}
177-
178-
const content = renderedSource.source();
179-
return typeof content === "string" ? content : content.toString("utf8");
187+
return new RawSource(escaped);
180188
}
181189

182190
/**
@@ -296,17 +304,22 @@ class CssGenerator extends Generator {
296304
const generateContentCode = () => {
297305
switch (exportType) {
298306
case "style": {
299-
const moduleCode = this._generateContentCode(
307+
const cssSource = this._generateContentSource(
300308
module,
301309
generateContext
302310
);
311+
if (!cssSource) return "";
303312
const moduleId = generateContext.chunkGraph.getModuleId(module);
304313

305314
generateContext.runtimeRequirements.add(
306315
RuntimeGlobals.cssInjectStyle
307316
);
308317

309-
return `${RuntimeGlobals.cssInjectStyle}(${JSON.stringify(moduleId || "")}, ${JSON.stringify(moduleCode)});`;
318+
return new ConcatSource(
319+
`${RuntimeGlobals.cssInjectStyle}(${JSON.stringify(moduleId || "")}, `,
320+
this._cssSourceToJsStringLiteral(cssSource, module),
321+
");"
322+
);
310323
}
311324

312325
default:
@@ -325,15 +338,19 @@ class CssGenerator extends Generator {
325338
}
326339
};
327340
const generateExportCode = () => {
341+
/** @returns {Source} generated CSS text as JS expression */
328342
const generateCssText = () => {
329343
const importCode = this._generateImportCode(
330344
module,
331345
generateContext
332346
);
333-
const moduleCode = this._generateContentCode(
347+
const cssSource = this._generateContentSource(
334348
module,
335349
generateContext
336350
);
351+
const jsLiteral = cssSource
352+
? this._cssSourceToJsStringLiteral(cssSource, module)
353+
: new RawSource('""');
337354

338355
if (importCode.length > 0) {
339356
if (
@@ -344,17 +361,24 @@ class CssGenerator extends Generator {
344361
RuntimeGlobals.cssMergeStyleSheets
345362
);
346363

347-
return `${RuntimeGlobals.cssMergeStyleSheets}([${[...importCode.map((part) => part.expr), JSON.stringify(moduleCode)].join(", ")}])`;
364+
const args = importCode.map((part) => part.expr);
365+
return new ConcatSource(
366+
`${RuntimeGlobals.cssMergeStyleSheets}([${args.join(", ")}, `,
367+
jsLiteral,
368+
"])"
369+
);
348370
}
349-
return generateContext.runtimeTemplate.concatenation(
350-
...importCode,
351-
moduleCode
371+
return new ConcatSource(
372+
`${generateContext.runtimeTemplate.concatenation(
373+
...importCode
374+
)} + `,
375+
jsLiteral
352376
);
353377
}
354-
return JSON.stringify(moduleCode);
378+
return jsLiteral;
355379
};
356380
/**
357-
* @returns {string | null} the default export
381+
* @returns {Source | null} the default export
358382
*/
359383
const generateJSDefaultExport = () => {
360384
switch (exportType) {
@@ -364,12 +388,20 @@ class CssGenerator extends Generator {
364388
case "css-style-sheet": {
365389
const constOrVar =
366390
generateContext.runtimeTemplate.renderConst();
367-
return `(${generateContext.runtimeTemplate.basicFunction("", [
368-
`${constOrVar} cssText = ${generateCssText()};`,
369-
`${constOrVar} sheet = new CSSStyleSheet();`,
370-
"sheet.replaceSync(cssText);",
371-
"return sheet;"
372-
])})()`;
391+
const cssText = generateCssText();
392+
const fnPrefix =
393+
generateContext.runtimeTemplate.supportsArrowFunction()
394+
? "() => {\n"
395+
: "function() {\n";
396+
const body =
397+
`${constOrVar} sheet = new CSSStyleSheet();\n` +
398+
"sheet.replaceSync(cssText);\n" +
399+
"return sheet;\n";
400+
return new ConcatSource(
401+
`(${fnPrefix}${constOrVar} cssText = `,
402+
cssText,
403+
`;\n${body}})()`
404+
);
373405
}
374406
default:
375407
return null;
@@ -378,18 +410,8 @@ class CssGenerator extends Generator {
378410

379411
const isCSSModule = /** @type {BuildMeta} */ (module.buildMeta)
380412
.isCSSModule;
413+
/** @type {Source | null} */
381414
const defaultExport = generateJSDefaultExport();
382-
/**
383-
* @param {string} name the export name
384-
* @param {string} value the export value
385-
* @returns {string} the value to be used in the export
386-
*/
387-
const stringifyExportValue = (name, value) => {
388-
if (defaultExport) {
389-
return name === "default" ? value : JSON.stringify(value);
390-
}
391-
return JSON.stringify(value);
392-
};
393415

394416
/** @type {BuildInfo} */
395417
(module.buildInfo).cssData = cssData;
@@ -399,14 +421,7 @@ class CssGenerator extends Generator {
399421
generateContext.runtimeRequirements.add(RuntimeGlobals.module);
400422
}
401423

402-
if (defaultExport) {
403-
cssData.exports.set(
404-
"default",
405-
/** @type {string} */ (defaultExport)
406-
);
407-
}
408-
409-
if (cssData.exports.size === 0 && !isCSSModule) {
424+
if (!defaultExport && cssData.exports.size === 0 && !isCSSModule) {
410425
return new RawSource("");
411426
}
412427

@@ -416,6 +431,28 @@ class CssGenerator extends Generator {
416431
const usedIdentifiers = new Set();
417432
const { RESERVED_IDENTIFIER } = getPropertyName();
418433

434+
if (defaultExport) {
435+
const usedName = generateContext.moduleGraph
436+
.getExportInfo(module, "default")
437+
.getUsedName("default", generateContext.runtime);
438+
if (usedName) {
439+
let identifier = Template.toIdentifier(usedName);
440+
if (RESERVED_IDENTIFIER.has(identifier)) {
441+
identifier = `_${identifier}`;
442+
}
443+
usedIdentifiers.add(identifier);
444+
generateContext.concatenationScope.registerExport(
445+
"default",
446+
identifier
447+
);
448+
source.add(
449+
`${generateContext.runtimeTemplate.renderConst()} ${identifier} = `
450+
);
451+
source.add(defaultExport);
452+
source.add(";\n");
453+
}
454+
}
455+
419456
for (const [name, v] of cssData.exports) {
420457
const usedName = generateContext.moduleGraph
421458
.getExportInfo(module, name)
@@ -439,7 +476,7 @@ class CssGenerator extends Generator {
439476
identifier
440477
);
441478
source.add(
442-
`${generateContext.runtimeTemplate.renderConst()} ${identifier} = ${stringifyExportValue(name, v)};\n`
479+
`${generateContext.runtimeTemplate.renderConst()} ${identifier} = ${JSON.stringify(v)};\n`
443480
);
444481
}
445482
return source;
@@ -462,25 +499,40 @@ class CssGenerator extends Generator {
462499
generateContext.runtimeRequirements.add(RuntimeGlobals.module);
463500

464501
if (!isCSSModule && !needNsObj) {
465-
return new RawSource(
466-
`${module.moduleArgument}.exports = ${defaultExport}`
502+
return new ConcatSource(
503+
`${module.moduleArgument}.exports = `,
504+
/** @type {Source} */ (defaultExport)
467505
);
468506
}
469507

470-
/** @type {string[]} */
471-
const exports = [];
508+
const result = new ConcatSource();
509+
result.add(
510+
`${needNsObj ? `${RuntimeGlobals.makeNamespaceObject}(` : ""}${
511+
module.moduleArgument
512+
}.exports = {\n`
513+
);
514+
515+
if (defaultExport) {
516+
result.add('\t"default": ');
517+
result.add(defaultExport);
518+
if (cssData.exports.size > 0) {
519+
result.add(",\n");
520+
}
521+
}
472522

523+
/** @type {string[]} */
524+
const exportEntries = [];
473525
for (const [name, v] of cssData.exports) {
474-
exports.push(
475-
`\t${JSON.stringify(name)}: ${stringifyExportValue(name, v)}`
526+
exportEntries.push(
527+
`\t${JSON.stringify(name)}: ${JSON.stringify(v)}`
476528
);
477529
}
530+
if (exportEntries.length > 0) {
531+
result.add(exportEntries.join(",\n"));
532+
}
478533

479-
return new RawSource(
480-
`${needNsObj ? `${RuntimeGlobals.makeNamespaceObject}(` : ""}${
481-
module.moduleArgument
482-
}.exports = {\n${exports.join(",\n")}\n}${needNsObj ? ")" : ""};`
483-
);
534+
result.add(`\n}${needNsObj ? ")" : ""};`);
535+
return result;
484536
};
485537

486538
const codeParts = this._exportsOnly
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import textCss from "./style.css";
2+
3+
it("should include CSS source in the JS source map for text exportType", () => {
4+
// With the Source chain approach, CSS source map info flows into the JS source map.
5+
// The CSS content should be present in the exported text.
6+
expect(textCss).toContain(".text-class");
7+
expect(textCss).toContain("color: red");
8+
// The JS source map (bundle0.js.map) should contain the CSS source.
9+
// We verify this by reading the source map file.
10+
const fs = require("fs");
11+
const path = require("path");
12+
const mapFile = path.resolve(__dirname, "bundle0.js.map");
13+
expect(fs.existsSync(mapFile)).toBe(true);
14+
const map = JSON.parse(fs.readFileSync(mapFile, "utf-8"));
15+
const cssSourceIndex = map.sources.findIndex(s => s.includes("style.css"));
16+
expect(cssSourceIndex).toBeGreaterThanOrEqual(0);
17+
expect(map.sourcesContent[cssSourceIndex]).toContain(".text-class");
18+
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.text-class {
2+
color: red;
3+
font-size: 16px;
4+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"use strict";
2+
3+
/** @type {import("../../../../").Configuration} */
4+
module.exports = {
5+
devtool: "source-map",
6+
target: "node",
7+
mode: "development",
8+
module: {
9+
rules: [
10+
{
11+
test: /style\.css$/,
12+
type: "css/auto",
13+
parser: { exportType: "text" }
14+
}
15+
]
16+
},
17+
experiments: {
18+
css: true
19+
}
20+
};
76.3 KB
Loading

0 commit comments

Comments
 (0)