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..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,15 +77,78 @@ 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 }, + ); + }); + + 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', SCSS_AQUA); + + await harness.executeWithCases( + [ + async ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/styles.css').content.toContain(CSS_AQUA); + + // Introduce a syntax error in the imported partial + await harness.writeFile('src/variables.scss', SCSS_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(CSS_AQUA); + harness.expectFile('dist/browser/styles.css').content.toContain(CSS_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', SCSS_BROKEN); + + await harness.executeWithCases( + [ + async ({ result }) => { + expect(result?.success).toBe(false); + + // Fix the partial + await harness.writeFile('src/variables.scss', SCSS_AQUA); + }, + ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/styles.css').content.toContain(CSS_AQUA); }, ], { outputLogsOnFailure: false }, @@ -110,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 }, 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;