Skip to content

Commit fadbef6

Browse files
committed
fix(heft-sass-plugin): set sourceMapUrl in loadAsync to prevent data: URL crash on Linux/macOS
1 parent 4e14a4c commit fadbef6

5 files changed

Lines changed: 109 additions & 1 deletion

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"changes": [
3+
{
4+
"comment": "Fix sourceMap: true crashing on Linux/macOS when compiled .scss files use @use or @import",
5+
"type": "patch",
6+
"packageName": "@rushstack/heft-sass-plugin"
7+
}
8+
],
9+
"packageName": "@rushstack/heft-sass-plugin",
10+
"email": "cmalonzo@microsoft.com"
11+
}

heft-plugins/heft-sass-plugin/src/SassProcessor.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,10 @@ export class SassProcessor {
242242

243243
return {
244244
contents: record.content,
245-
syntax: determineSyntaxFromFilePath(absolutePath)
245+
syntax: determineSyntaxFromFilePath(absolutePath),
246+
// Without sourceMapUrl, sass-embedded falls back to a data: URL for this file in the
247+
// source map. data: URLs crash heftUrlToPath on Linux/macOS (non-empty URL host).
248+
sourceMapUrl: url
246249
};
247250
};
248251

@@ -860,6 +863,7 @@ export class SassProcessor {
860863
// Rewrite heft: URL sources to paths relative to the map file's directory
861864
// so that source-map-loader can resolve them back to the original .scss.
862865
const rewrittenSources: string[] = result.sourceMap.sources.map((source) => {
866+
if (!source.startsWith('heft:')) return source;
863867
const absoluteSourcePath: string = heftUrlToPath(source);
864868
return Path.convertToSlashes(path.relative(mapDir, absoluteSourcePath));
865869
});

heft-plugins/heft-sass-plugin/src/test/SassProcessor.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,40 @@ describe(SassProcessor.name, () => {
759759
expect(parsedMap.sources[0]).toMatch(/classes-and-exports\.module\.scss$/);
760760
});
761761

762+
it('emits a valid .css.map when the entry file @uses a partial (Linux/macOS regression)', async () => {
763+
// use-with-partial.module.scss uses @use 'partial', which goes through loadAsync.
764+
// Without sourceMapUrl on the ImporterResult, sass-embedded falls back to a data: URL
765+
// for the partial in the source map; heftUrlToPath then crashes on Linux/macOS.
766+
const { processor } = createProcessor(terminalProvider, { sourceMap: true });
767+
await compileFixtureAsync(processor, 'use-with-partial.module.scss');
768+
769+
const mapPaths: string[] = getAllWrittenPathsMatching('.css.map');
770+
expect(mapPaths).toHaveLength(1);
771+
772+
const mapJson: string = getWrittenFile('use-with-partial.module.css.map');
773+
const parsedMap: { version: number; mappings: string; sources: string[] } = JSON.parse(mapJson);
774+
expect(parsedMap.version).toBe(3);
775+
expect(parsedMap.mappings).toBeTruthy();
776+
// Both the entry file and the partial must resolve to real paths, not data: URLs
777+
expect(parsedMap.sources.every((s: string) => !s.startsWith('data:'))).toBe(true);
778+
});
779+
780+
it('emits a valid .css.map when the entry file uses a pkg: import (Linux/macOS regression)', async () => {
781+
// pkg: imports go through _canonicalizePackageInnerAsync before reaching loadAsync.
782+
// Without sourceMapUrl, the same data: URL crash applies to this path.
783+
const { processor } = createProcessor(terminalProvider, { sourceMap: true });
784+
await compileFixtureAsync(processor, 'pkg-import-test.module.scss');
785+
786+
const mapPaths: string[] = getAllWrittenPathsMatching('.css.map');
787+
expect(mapPaths).toHaveLength(1);
788+
789+
const mapJson: string = getWrittenFile('pkg-import-test.module.css.map');
790+
const parsedMap: { version: number; mappings: string; sources: string[] } = JSON.parse(mapJson);
791+
expect(parsedMap.version).toBe(3);
792+
expect(parsedMap.mappings).toBeTruthy();
793+
expect(parsedMap.sources.every((s: string) => !s.startsWith('data:'))).toBe(true);
794+
});
795+
762796
it('does not emit .css.map or sourceMappingURL comment by default', async () => {
763797
const { processor } = createProcessor(terminalProvider);
764798
await compileFixtureAsync(processor, 'classes-and-exports.module.scss');

heft-plugins/heft-sass-plugin/src/test/__snapshots__/SassProcessor.test.ts.snap

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1331,6 +1331,59 @@ export default styles;",
13311331
}
13321332
`;
13331333

1334+
exports[`SassProcessor sourceMap option emits a valid .css.map when the entry file @uses a partial (Linux/macOS regression): terminal-output 1`] = `
1335+
Array [
1336+
"[verbose] Checking for changes to 1 files...[n]",
1337+
"[ log] Compiling 1 files...[n]",
1338+
]
1339+
`;
1340+
1341+
exports[`SassProcessor sourceMap option emits a valid .css.map when the entry file @uses a partial (Linux/macOS regression): written-files 1`] = `
1342+
Map {
1343+
"/fake/output/dts/use-with-partial.module.scss.d.ts" => "declare interface IStyles {
1344+
container: string;
1345+
header: string;
1346+
}
1347+
declare const styles: IStyles;
1348+
export default styles;",
1349+
"/fake/output/css/use-with-partial.module.css" => ".container {
1350+
color: #0078d4;
1351+
padding: 8px;
1352+
}
1353+
1354+
.header {
1355+
border-bottom: 1px solid #0078d4;
1356+
}
1357+
/*# sourceMappingURL=use-with-partial.module.css.map */
1358+
",
1359+
"/fake/output/css/use-with-partial.module.css.map" => "{\\"version\\":3,\\"sourceRoot\\":\\"\\",\\"sources\\":[\\"fixtures/use-with-partial.module.scss\\",\\"fixtures/_partial.scss\\"],\\"names\\":[],\\"mappings\\":\\"AAIA;EACE,OCHY;EDIZ;;;AAGF;EACE\\",\\"sourcesContent\\":[\\"// Uses the modern @use syntax to import variables from a local partial.\\\\n// Verifies that SassProcessor resolves _partial.scss when @use 'partial' is written.\\\\n@use 'partial' as tokens;\\\\n\\\\n.container {\\\\n color: tokens.$brand-color;\\\\n padding: tokens.$spacing-unit * 2;\\\\n}\\\\n\\\\n.header {\\\\n border-bottom: 1px solid tokens.$brand-color;\\\\n}\\\\n\\",\\"// Sass partial exposing shared design tokens.\\\\n// Imported via @use 'partial' in use-with-partial.module.scss.\\\\n$brand-color: #0078d4;\\\\n$spacing-unit: 4px;\\\\n\\"],\\"file\\":\\"use-with-partial.module.css\\"}",
1360+
}
1361+
`;
1362+
1363+
exports[`SassProcessor sourceMap option emits a valid .css.map when the entry file uses a pkg: import (Linux/macOS regression): terminal-output 1`] = `
1364+
Array [
1365+
"[verbose] Checking for changes to 1 files...[n]",
1366+
"[ log] Compiling 1 files...[n]",
1367+
]
1368+
`;
1369+
1370+
exports[`SassProcessor sourceMap option emits a valid .css.map when the entry file uses a pkg: import (Linux/macOS regression): written-files 1`] = `
1371+
Map {
1372+
"/fake/output/dts/pkg-import-test.module.scss.d.ts" => "declare interface IStyles {
1373+
box: string;
1374+
}
1375+
declare const styles: IStyles;
1376+
export default styles;",
1377+
"/fake/output/css/pkg-import-test.module.css" => ".box {
1378+
color: #ff0000;
1379+
font-size: 12px;
1380+
}
1381+
/*# sourceMappingURL=pkg-import-test.module.css.map */
1382+
",
1383+
"/fake/output/css/pkg-import-test.module.css.map" => "{\\"version\\":3,\\"sourceRoot\\":\\"\\",\\"sources\\":[\\"fixtures/pkg-import-test.module.scss\\",\\"fixtures/node_modules/@test/tokens/index.scss\\"],\\"names\\":[],\\"mappings\\":\\"AAEA;EACE,OCHW;EDIX,WCHU\\",\\"sourcesContent\\":[\\"@use 'pkg:@test/tokens' as t;\\\\n\\\\n.box {\\\\n color: t.$test-color;\\\\n font-size: t.$test-size;\\\\n}\\\\n\\",\\"$test-color: #ff0000;\\\\n$test-size: 12px;\\\\n\\"],\\"file\\":\\"pkg-import-test.module.css\\"}",
1384+
}
1385+
`;
1386+
13341387
exports[`SassProcessor sourceMap option uses the correct map filename when doNotTrimOriginalFileExtension is true: terminal-output 1`] = `
13351388
Array [
13361389
"[verbose] Checking for changes to 1 files...[n]",
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
@use 'pkg:@test/tokens' as t;
2+
3+
.box {
4+
color: t.$test-color;
5+
font-size: t.$test-size;
6+
}

0 commit comments

Comments
 (0)