From a7e96ba4dc61987fd7784781617dd890611a93b7 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:19:35 +0000 Subject: [PATCH 1/3] fix(@angular/build): attach location to Sass errors and guard stack parsing Populate the structured esbuild `location` on Sass `Exception` errors using a shared `spanToLocation` helper (matching Sass warnings) so that failed stylesheet files are tracked on error messages. Also add a bounds check on `index < segments.length` in `extractFilesFromStack` to avoid throwing a `TypeError` when a stack trace line does not contain a `line:col` segment. --- .../esbuild/stylesheets/sass-language.ts | 34 +++++---- .../esbuild/stylesheets/sass-language_spec.ts | 71 +++++++++++++++++++ 2 files changed, 90 insertions(+), 15 deletions(-) diff --git a/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts b/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts index 432ae7ba79b8..5973c4bf6faf 100644 --- a/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts +++ b/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.dev/license */ -import type { OnLoadResult, PartialMessage, PartialNote, ResolveResult } from 'esbuild'; +import type { Location, OnLoadResult, PartialMessage, PartialNote, ResolveResult } from 'esbuild'; import { dirname, join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; -import type { CompileResult, Exception, Syntax } from 'sass-embedded'; +import type { CompileResult, Exception, SourceSpan, Syntax } from 'sass-embedded'; import { MemoryCache } from '../../../utils/cache'; import type { SassCompiler } from '../../sass/sass-service'; import { StylesheetLanguage, StylesheetPluginOptions } from './stylesheet-plugin-factory'; @@ -233,13 +233,7 @@ async function compileString( warnings.push({ text: deprecation ? 'Deprecation' : text, - location: span && { - file: span.url && fileURLToPath(span.url), - lineText: span.context, - // Sass line numbers are 0-based while esbuild's are 1-based - line: span.start.line + 1, - column: span.start.column, - }, + location: span && spanToLocation(span), notes, }); }, @@ -254,11 +248,10 @@ async function compileString( }; } catch (error) { if (isSassException(error)) { - const fileWithError = error.span.url ? fileURLToPath(error.span.url) : undefined; - + const location = error.span && spanToLocation(error.span); const watchFiles = [filePath, ...extractFilesFromStack(error.sassStack)]; - if (fileWithError) { - watchFiles.push(fileWithError); + if (location?.file) { + watchFiles.push(location.file); } return { @@ -266,6 +259,7 @@ async function compileString( errors: [ { text: error.message, + location, }, ], warnings, @@ -277,6 +271,16 @@ async function compileString( } } +function spanToLocation(span: SourceSpan): Partial { + return { + file: span.url && fileURLToPath(span.url), + lineText: span.context, + // Sass line numbers are 0-based while esbuild's are 1-based + line: span.start.line + 1, + column: span.start.column, + }; +} + function sourceMapToUrlComment(sourceMap: Exclude): string { const urlSourceMap = Buffer.from(JSON.stringify(sourceMap), 'utf-8').toString('base64'); @@ -298,11 +302,11 @@ function* extractFilesFromStack(stack: string): Iterable { // Paths may contain spaces. All segments before location are part of the file path. let path = ''; let index = 0; - while (!segments[index].match(/\d+:\d+/)) { + while (index < segments.length && !segments[index].match(/\d+:\d+/)) { path += segments[index++]; } - if (path) { + if (path && index < segments.length) { // Stack paths from sass are relative to the current working directory (not input file or workspace root) yield join(cwd, path); } diff --git a/packages/angular/build/src/tools/esbuild/stylesheets/sass-language_spec.ts b/packages/angular/build/src/tools/esbuild/stylesheets/sass-language_spec.ts index 216a2d18fc47..ec424208eadc 100644 --- a/packages/angular/build/src/tools/esbuild/stylesheets/sass-language_spec.ts +++ b/packages/angular/build/src/tools/esbuild/stylesheets/sass-language_spec.ts @@ -351,4 +351,75 @@ describe('sass-language', () => { } }); }); + + describe('error handling', () => { + const buildStub = { + initialOptions: { absWorkingDir: '/app' }, + resolve: async () => ({ path: undefined, errors: [], warnings: [] }), + } as unknown as PluginBuild; + + afterAll(() => { + shutdownSassWorkerPool(); + }); + + it('should populate structured location on Sass compilation errors', async () => { + const result = await SassStylesheetLanguage.process?.( + 'body {\n color: $undefined-var;\n}', + '/app/src/styles.scss', + 'scss', + { sourcemap: false }, + buildStub, + ); + + expect(result?.errors?.length).toBe(1); + expect(result?.errors?.[0].text).toContain('Undefined variable.'); + expect(result?.errors?.[0].location).toEqual({ + file: '/app/src/styles.scss', + line: 2, + column: 9, + lineText: jasmine.stringContaining('color: $undefined-var;'), + }); + expect(result?.watchFiles).toContain('/app/src/styles.scss'); + }); + + it('should skip stack trace lines without line:col and extract valid stack files', async () => { + spyOn(SassCompiler.prototype, 'compileStringAsync').and.rejectWith({ + message: 'Custom Sass error', + sassMessage: 'Custom Sass error', + sassStack: 'malformed stack line without location\nsrc/partials/_vars.scss 3:5 @use\n', + span: { + url: pathToFileURL('/app/src/partials/_vars.scss'), + context: '$bad: ;', + start: { line: 2, column: 4, offset: 10 }, + end: { line: 2, column: 5, offset: 11 }, + text: ';', + }, + }); + + const result = await SassStylesheetLanguage.process?.( + '@use "partials/vars";', + '/app/src/styles.scss', + 'scss', + { sourcemap: false }, + buildStub, + ); + + expect(result?.errors).toEqual([ + { + text: 'Custom Sass error', + location: { + file: '/app/src/partials/_vars.scss', + lineText: '$bad: ;', + line: 3, + column: 4, + }, + }, + ]); + expect(result?.watchFiles).toEqual([ + '/app/src/styles.scss', + join(process.cwd(), 'src/partials/_vars.scss'), + '/app/src/partials/_vars.scss', + ]); + }); + }); }); From 5322da7809c3e4a36da3375ca71141d6a8959107 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:51:39 +0000 Subject: [PATCH 2/3] fixup! fix(@angular/build): attach location to Sass errors and guard stack parsing --- .../build/src/tools/esbuild/stylesheets/sass-language.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts b/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts index 5973c4bf6faf..a89b6ad2847a 100644 --- a/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts +++ b/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts @@ -303,7 +303,7 @@ function* extractFilesFromStack(stack: string): Iterable { let path = ''; let index = 0; while (index < segments.length && !segments[index].match(/\d+:\d+/)) { - path += segments[index++]; + path += (path ? ' ' : '') + segments[index++]; } if (path && index < segments.length) { From 049a7a1f0c784e2375a3b1020ce454a6f671b622 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:04:19 +0000 Subject: [PATCH 3/3] fixup! fix(@angular/build): attach location to Sass errors and guard stack parsing --- .../esbuild/stylesheets/sass-language.ts | 24 +++++-------------- 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts b/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts index a89b6ad2847a..5c09413bbb9b 100644 --- a/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts +++ b/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts @@ -248,9 +248,9 @@ async function compileString( }; } catch (error) { if (isSassException(error)) { - const location = error.span && spanToLocation(error.span); + const location = spanToLocation(error.span); const watchFiles = [filePath, ...extractFilesFromStack(error.sassStack)]; - if (location?.file) { + if (location.file) { watchFiles.push(location.file); } @@ -291,24 +291,12 @@ function* extractFilesFromStack(stack: string): Iterable { const lines = stack.split('\n'); const cwd = process.cwd(); - // Stack line has format of " " + // Stack line has format of ` : ` for (const line of lines) { - const segments = line.split(' '); - if (segments.length < 3) { - break; - } - - // Extract path from stack line. - // Paths may contain spaces. All segments before location are part of the file path. - let path = ''; - let index = 0; - while (index < segments.length && !segments[index].match(/\d+:\d+/)) { - path += (path ? ' ' : '') + segments[index++]; - } - - if (path && index < segments.length) { + const match = line.trim().match(/^(.+?)\s+\d+:\d+/); + if (match) { // Stack paths from sass are relative to the current working directory (not input file or workspace root) - yield join(cwd, path); + yield join(cwd, match[1]); } } }