fix(@angular/build): invalidate cached SCSS errors when source files are corrected#32924
fix(@angular/build): invalidate cached SCSS errors when source files are corrected#32924maruthang wants to merge 1 commit intoangular:mainfrom
Conversation
…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 angular#32744
There was a problem hiding this comment.
Code Review
This pull request fixes a caching issue in MemoryLoadResultCache where path separator inconsistencies prevented proper invalidation of dependencies, leading to stale error results. The fix involves normalizing paths before lookup and deletion. Additionally, new regression tests were added to verify that SCSS partials correctly recover from syntax errors during rebuilds and initial builds. I have no further feedback, though the suggestion to extract repeated test strings into constants is a good opportunity to improve test maintainability.
| 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 }, | ||
| ); | ||
| }); |
There was a problem hiding this comment.
There's some duplication of test data strings (like SCSS content and expected CSS output) between the two new tests. To improve maintainability and readability, consider defining these repeated strings as constants at the top of the describe block or before the tests. For example:
const SCSS_AQUA = '$primary: aqua;';
const SCSS_BROKEN = '$primary: aqua\n$broken;';
const CSS_AQUA = 'color: aqua';
Problem
When a SCSS partial (or any Sass dependency) contains an error during
ng serve, fixing the file does not clear the error in watch mode. The stale error remains cached even after the source is corrected. Fixes #32744.Root Cause
MemoryLoadResultCache.put()stores file dependencies usingnormalize(watchFile)to ensure OS-consistent path separators. However,invalidate()looked up dependencies using the raw, un-normalized path. During Sass error reporting,watchFilesare populated viaextractFilesFromStack()(which usespath.join(cwd, relativePath)) andfileURLToPath()— both produce paths that may use different separators. The resulting path mismatch causedinvalidate()to silently fail to find the cache entry, leaving the stale error result in#loadResults. On the next rebuild,createCachedLoad()returned the stale (error) result from the cache instead of re-compiling the now-fixed SCSS.Fix
Normalize the path inside
invalidate()(using the samenormalize()call thatput()uses) before looking up the key in#fileDependencies. This ensures the invalidation lookup always matches the stored entry regardless of how the path was formatted by the caller.Changes
packages/angular/build/src/tools/esbuild/load-result-cache.ts— normalizepathininvalidate()to match how keys are stored input()packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts— add two regression tests covering the@usepartial error scenario (rebuild after error introduced mid-watch, and recovery from error present at initial build)Test plan
bazel test //packages/angular/build:application_integration_tests— new testsrecovers from error in SCSS partial after fix on rebuild using @useandrecovers from error in SCSS partial after fix on initial build using @usepassrebuilds Sass stylesheet after error on rebuild from importand related tests continue to pass