diff --git a/common/changes/@rushstack/heft-sass-plugin/fix-sourcemap-linux-crash_2026-06-01-00-00.json b/common/changes/@rushstack/heft-sass-plugin/fix-sourcemap-linux-crash_2026-06-01-00-00.json new file mode 100644 index 0000000000..3dc5639dfd --- /dev/null +++ b/common/changes/@rushstack/heft-sass-plugin/fix-sourcemap-linux-crash_2026-06-01-00-00.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "Fix sourceMap: true crashing on Linux/macOS when compiled .scss files use @use or @import", + "type": "patch", + "packageName": "@rushstack/heft-sass-plugin" + } + ], + "packageName": "@rushstack/heft-sass-plugin", + "email": "cmalonzo@microsoft.com" +} diff --git a/heft-plugins/heft-sass-plugin/src/SassProcessor.ts b/heft-plugins/heft-sass-plugin/src/SassProcessor.ts index 129548b6b0..60daaad966 100644 --- a/heft-plugins/heft-sass-plugin/src/SassProcessor.ts +++ b/heft-plugins/heft-sass-plugin/src/SassProcessor.ts @@ -242,7 +242,10 @@ export class SassProcessor { return { contents: record.content, - syntax: determineSyntaxFromFilePath(absolutePath) + syntax: determineSyntaxFromFilePath(absolutePath), + // Without sourceMapUrl, sass-embedded falls back to a data: URL for this file in the + // source map. data: URLs crash heftUrlToPath on Linux/macOS (non-empty URL host). + sourceMapUrl: url }; }; @@ -860,6 +863,7 @@ export class SassProcessor { // Rewrite heft: URL sources to paths relative to the map file's directory // so that source-map-loader can resolve them back to the original .scss. const rewrittenSources: string[] = result.sourceMap.sources.map((source) => { + if (!source.startsWith('heft:')) return source; const absoluteSourcePath: string = heftUrlToPath(source); return Path.convertToSlashes(path.relative(mapDir, absoluteSourcePath)); }); diff --git a/heft-plugins/heft-sass-plugin/src/test/SassProcessor.test.ts b/heft-plugins/heft-sass-plugin/src/test/SassProcessor.test.ts index 1ff1c32625..edb9450d4f 100644 --- a/heft-plugins/heft-sass-plugin/src/test/SassProcessor.test.ts +++ b/heft-plugins/heft-sass-plugin/src/test/SassProcessor.test.ts @@ -759,6 +759,24 @@ describe(SassProcessor.name, () => { expect(parsedMap.sources[0]).toMatch(/classes-and-exports\.module\.scss$/); }); + it('emits a valid .css.map when the entry file @uses a partial (Linux/macOS regression)', async () => { + // use-with-partial.module.scss uses @use 'partial', which goes through loadAsync. + // Without sourceMapUrl on the ImporterResult, sass-embedded falls back to a data: URL + // for the partial in the source map; heftUrlToPath then crashes on Linux/macOS. + const { processor } = createProcessor(terminalProvider, { sourceMap: true }); + await compileFixtureAsync(processor, 'use-with-partial.module.scss'); + + const mapPaths: string[] = getAllWrittenPathsMatching('.css.map'); + expect(mapPaths).toHaveLength(1); + + const mapJson: string = getWrittenFile('use-with-partial.module.css.map'); + const parsedMap: { version: number; mappings: string; sources: string[] } = JSON.parse(mapJson); + expect(parsedMap.version).toBe(3); + expect(parsedMap.mappings).toBeTruthy(); + // Both the entry file and the partial must resolve to real paths, not data: URLs + expect(parsedMap.sources.every((s: string) => !s.startsWith('data:'))).toBe(true); + }); + it('does not emit .css.map or sourceMappingURL comment by default', async () => { const { processor } = createProcessor(terminalProvider); await compileFixtureAsync(processor, 'classes-and-exports.module.scss'); diff --git a/heft-plugins/heft-sass-plugin/src/test/__snapshots__/SassProcessor.test.ts.snap b/heft-plugins/heft-sass-plugin/src/test/__snapshots__/SassProcessor.test.ts.snap index f6df8f5d7c..098a07c9a6 100644 --- a/heft-plugins/heft-sass-plugin/src/test/__snapshots__/SassProcessor.test.ts.snap +++ b/heft-plugins/heft-sass-plugin/src/test/__snapshots__/SassProcessor.test.ts.snap @@ -1331,6 +1331,35 @@ export default styles;", } `; +exports[`SassProcessor sourceMap option emits a valid .css.map when the entry file @uses a partial (Linux/macOS regression): terminal-output 1`] = ` +Array [ + "[verbose] Checking for changes to 1 files...[n]", + "[ log] Compiling 1 files...[n]", +] +`; + +exports[`SassProcessor sourceMap option emits a valid .css.map when the entry file @uses a partial (Linux/macOS regression): written-files 1`] = ` +Map { + "/fake/output/dts/use-with-partial.module.scss.d.ts" => "declare interface IStyles { + container: string; + header: string; +} +declare const styles: IStyles; +export default styles;", + "/fake/output/css/use-with-partial.module.css" => ".container { + color: #0078d4; + padding: 8px; +} + +.header { + border-bottom: 1px solid #0078d4; +} +/*# sourceMappingURL=use-with-partial.module.css.map */ +", + "/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\\"}", +} +`; + exports[`SassProcessor sourceMap option uses the correct map filename when doNotTrimOriginalFileExtension is true: terminal-output 1`] = ` Array [ "[verbose] Checking for changes to 1 files...[n]",