From 9bd8fab62bcdb2c1fedfdbe4e10bf6742dcaf4ff Mon Sep 17 00:00:00 2001 From: Maruthan G Date: Thu, 2 Apr 2026 15:57:32 +0530 Subject: [PATCH 1/3] fix(@angular/build): invalidate cached SCSS errors when source files are corrected Normalize the file path in MemoryLoadResultCache.invalidate() to match how watch file paths are stored in put(). Without normalization, paths produced by fileURLToPath() or path.join() during Sass error reporting may use different separators than the normalized paths stored as keys in #fileDependencies. This mismatch caused the invalidation lookup to fail, leaving stale error results in the load cache after fixing an SCSS partial. The fix ensures the path is normalized before the lookup so the correct cache entries are cleared and the next rebuild picks up the fix. Fixes #32744 --- .../behavior/rebuild-global_styles_spec.ts | 63 +++++++++++++++++++ .../src/tools/esbuild/load-result-cache.ts | 10 ++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts index 22c4c32202bd..b16c8ab2d40c 100644 --- a/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts @@ -87,6 +87,69 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { ); }); + it('recovers from error in SCSS partial after fix on rebuild using @use', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + styles: ['src/styles.scss'], + }); + + await harness.writeFile('src/styles.scss', "@use './variables' as v;\nh1 { color: v.$primary; }"); + await harness.writeFile('src/variables.scss', '$primary: aqua;'); + + await harness.executeWithCases( + [ + async ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/styles.css').content.toContain('color: aqua'); + + // Introduce a syntax error in the imported partial + await harness.writeFile('src/variables.scss', '$primary: aqua\n$broken;'); + }, + async ({ result }) => { + expect(result?.success).toBe(false); + + // Fix the partial — the cached error should be cleared + await harness.writeFile('src/variables.scss', '$primary: blue;'); + }, + ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/styles.css').content.not.toContain('color: aqua'); + harness.expectFile('dist/browser/styles.css').content.toContain('color: blue'); + }, + ], + { outputLogsOnFailure: false }, + ); + }); + + it('recovers from error in SCSS partial after fix on initial build using @use', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + styles: ['src/styles.scss'], + }); + + await harness.writeFile('src/styles.scss', "@use './variables' as v;\nh1 { color: v.$primary; }"); + // Start with an error in the partial + await harness.writeFile('src/variables.scss', '$primary: aqua\n$broken;'); + + await harness.executeWithCases( + [ + async ({ result }) => { + expect(result?.success).toBe(false); + + // Fix the partial + await harness.writeFile('src/variables.scss', '$primary: aqua;'); + }, + ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/styles.css').content.toContain('color: aqua'); + }, + ], + { outputLogsOnFailure: false }, + ); + }); + it('rebuilds dependent Sass stylesheets after error on initial build from import', async () => { harness.useTarget('build', { ...BASE_OPTIONS, diff --git a/packages/angular/build/src/tools/esbuild/load-result-cache.ts b/packages/angular/build/src/tools/esbuild/load-result-cache.ts index 30067486a384..d130926ca598 100644 --- a/packages/angular/build/src/tools/esbuild/load-result-cache.ts +++ b/packages/angular/build/src/tools/esbuild/load-result-cache.ts @@ -70,7 +70,13 @@ export class MemoryLoadResultCache implements LoadResultCache { } invalidate(path: string): boolean { - const affectedPaths = this.#fileDependencies.get(path); + // Normalize the path to match how watch file paths are stored in `put()`. + // Without normalization, paths produced by `fileURLToPath()` or `path.join()` + // during error reporting may use different separators than the normalized paths + // stored as keys in `#fileDependencies`, causing the lookup to fail and leaving + // stale error results in the cache after the source file is corrected. + const normalizedPath = normalize(path); + const affectedPaths = this.#fileDependencies.get(normalizedPath); let found = false; if (affectedPaths) { @@ -79,7 +85,7 @@ export class MemoryLoadResultCache implements LoadResultCache { found = true; } } - this.#fileDependencies.delete(path); + this.#fileDependencies.delete(normalizedPath); } return found; From fc6936dcfd6d8056372421d50c3b92b8f328c100 Mon Sep 17 00:00:00 2001 From: Maruthan G Date: Thu, 2 Apr 2026 16:35:31 +0530 Subject: [PATCH 2/3] refactor(@angular/build): extract repeated SCSS/CSS test strings into constants Extract inline string literals used across the two new SCSS error-cache recovery tests into named constants (SCSS_AQUA, SCSS_BROKEN, CSS_AQUA, CSS_BLUE) to reduce duplication and improve readability. --- .../behavior/rebuild-global_styles_spec.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts index b16c8ab2d40c..538be9c69c0e 100644 --- a/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts @@ -87,6 +87,11 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { ); }); + const SCSS_AQUA = '$primary: aqua;'; + const SCSS_BROKEN = '$primary: aqua\n$broken;'; + const CSS_AQUA = 'color: aqua'; + const CSS_BLUE = 'color: blue'; + it('recovers from error in SCSS partial after fix on rebuild using @use', async () => { harness.useTarget('build', { ...BASE_OPTIONS, @@ -95,16 +100,16 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { }); await harness.writeFile('src/styles.scss', "@use './variables' as v;\nh1 { color: v.$primary; }"); - await harness.writeFile('src/variables.scss', '$primary: aqua;'); + await harness.writeFile('src/variables.scss', SCSS_AQUA); await harness.executeWithCases( [ async ({ result }) => { expect(result?.success).toBe(true); - harness.expectFile('dist/browser/styles.css').content.toContain('color: aqua'); + harness.expectFile('dist/browser/styles.css').content.toContain(CSS_AQUA); // Introduce a syntax error in the imported partial - await harness.writeFile('src/variables.scss', '$primary: aqua\n$broken;'); + await harness.writeFile('src/variables.scss', SCSS_BROKEN); }, async ({ result }) => { expect(result?.success).toBe(false); @@ -114,8 +119,8 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { }, ({ result }) => { expect(result?.success).toBe(true); - harness.expectFile('dist/browser/styles.css').content.not.toContain('color: aqua'); - harness.expectFile('dist/browser/styles.css').content.toContain('color: blue'); + harness.expectFile('dist/browser/styles.css').content.not.toContain(CSS_AQUA); + harness.expectFile('dist/browser/styles.css').content.toContain(CSS_BLUE); }, ], { outputLogsOnFailure: false }, @@ -131,7 +136,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { await harness.writeFile('src/styles.scss', "@use './variables' as v;\nh1 { color: v.$primary; }"); // Start with an error in the partial - await harness.writeFile('src/variables.scss', '$primary: aqua\n$broken;'); + await harness.writeFile('src/variables.scss', SCSS_BROKEN); await harness.executeWithCases( [ @@ -139,11 +144,11 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { expect(result?.success).toBe(false); // Fix the partial - await harness.writeFile('src/variables.scss', '$primary: aqua;'); + await harness.writeFile('src/variables.scss', SCSS_AQUA); }, ({ result }) => { expect(result?.success).toBe(true); - harness.expectFile('dist/browser/styles.css').content.toContain('color: aqua'); + harness.expectFile('dist/browser/styles.css').content.toContain(CSS_AQUA); }, ], { outputLogsOnFailure: false }, From c09320d661f1eb9e7cc61426c893f75be591db3b Mon Sep 17 00:00:00 2001 From: Maruthan G Date: Mon, 6 Apr 2026 14:02:16 +0530 Subject: [PATCH 3/3] refactor(@angular/build): extract repeated CSS test strings into constants Move duplicated 'color: aqua' and 'color: blue' assertion strings into CSS_AQUA and CSS_BLUE constants for improved maintainability. --- .../behavior/rebuild-global_styles_spec.ts | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts index 538be9c69c0e..e297c4d4c16e 100644 --- a/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts @@ -11,6 +11,11 @@ import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setu describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Behavior: "Rebuilds when global stylesheets change"', () => { + const CSS_AQUA = 'color: aqua'; + const CSS_BLUE = 'color: blue'; + const SCSS_AQUA = '$primary: aqua;'; + const SCSS_BROKEN = '$primary: aqua\n$broken;'; + beforeEach(async () => { // Application code is not needed for styles tests await harness.writeFile('src/main.ts', 'console.log("TEST");'); @@ -30,8 +35,8 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { [ async ({ result }) => { expect(result?.success).toBe(true); - harness.expectFile('dist/browser/styles.css').content.toContain('color: aqua'); - harness.expectFile('dist/browser/styles.css').content.not.toContain('color: blue'); + harness.expectFile('dist/browser/styles.css').content.toContain(CSS_AQUA); + harness.expectFile('dist/browser/styles.css').content.not.toContain(CSS_BLUE); await harness.writeFile( 'src/a.scss', @@ -45,8 +50,8 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { }, ({ result }) => { expect(result?.success).toBe(true); - harness.expectFile('dist/browser/styles.css').content.not.toContain('color: aqua'); - harness.expectFile('dist/browser/styles.css').content.toContain('color: blue'); + harness.expectFile('dist/browser/styles.css').content.not.toContain(CSS_AQUA); + harness.expectFile('dist/browser/styles.css').content.toContain(CSS_BLUE); }, ], { outputLogsOnFailure: false }, @@ -72,26 +77,21 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { }, async ({ result }) => { expect(result?.success).toBe(true); - harness.expectFile('dist/browser/styles.css').content.toContain('color: aqua'); - harness.expectFile('dist/browser/styles.css').content.not.toContain('color: blue'); + harness.expectFile('dist/browser/styles.css').content.toContain(CSS_AQUA); + harness.expectFile('dist/browser/styles.css').content.not.toContain(CSS_BLUE); await harness.writeFile('src/a.scss', '$primary: blue;\\nh1 { color: $primary; }'); }, ({ result }) => { expect(result?.success).toBe(true); - harness.expectFile('dist/browser/styles.css').content.not.toContain('color: aqua'); - harness.expectFile('dist/browser/styles.css').content.toContain('color: blue'); + harness.expectFile('dist/browser/styles.css').content.not.toContain(CSS_AQUA); + harness.expectFile('dist/browser/styles.css').content.toContain(CSS_BLUE); }, ], { outputLogsOnFailure: false }, ); }); - const SCSS_AQUA = '$primary: aqua;'; - const SCSS_BROKEN = '$primary: aqua\n$broken;'; - const CSS_AQUA = 'color: aqua'; - const CSS_BLUE = 'color: blue'; - it('recovers from error in SCSS partial after fix on rebuild using @use', async () => { harness.useTarget('build', { ...BASE_OPTIONS, @@ -178,23 +178,23 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { }, async ({ result }) => { expect(result?.success).toBe(true); - harness.expectFile('dist/browser/styles.css').content.toContain('color: aqua'); - harness.expectFile('dist/browser/styles.css').content.not.toContain('color: blue'); + harness.expectFile('dist/browser/styles.css').content.toContain(CSS_AQUA); + harness.expectFile('dist/browser/styles.css').content.not.toContain(CSS_BLUE); harness.expectFile('dist/browser/other.css').content.toContain('color: green'); - harness.expectFile('dist/browser/other.css').content.toContain('color: aqua'); - harness.expectFile('dist/browser/other.css').content.not.toContain('color: blue'); + harness.expectFile('dist/browser/other.css').content.toContain(CSS_AQUA); + harness.expectFile('dist/browser/other.css').content.not.toContain(CSS_BLUE); await harness.writeFile('src/a.scss', '$primary: blue;\\nh1 { color: $primary; }'); }, ({ result }) => { expect(result?.success).toBe(true); - harness.expectFile('dist/browser/styles.css').content.not.toContain('color: aqua'); - harness.expectFile('dist/browser/styles.css').content.toContain('color: blue'); + harness.expectFile('dist/browser/styles.css').content.not.toContain(CSS_AQUA); + harness.expectFile('dist/browser/styles.css').content.toContain(CSS_BLUE); harness.expectFile('dist/browser/other.css').content.toContain('color: green'); - harness.expectFile('dist/browser/other.css').content.not.toContain('color: aqua'); - harness.expectFile('dist/browser/other.css').content.toContain('color: blue'); + harness.expectFile('dist/browser/other.css').content.not.toContain(CSS_AQUA); + harness.expectFile('dist/browser/other.css').content.toContain(CSS_BLUE); }, ], { outputLogsOnFailure: false },