Command
serve (also affects build --watch)
Is this a regression?
Yes. Before 5623c19 error results were not cached, so a rebuild after fixing the file re-ran the compile. That commit started caching error results and compensated by giving the Sass and Less error paths explicit watchFiles (fileWithError ? [filePath, fileWithError] : [filePath]) — the postcss error path never received the same treatment.
Description
A postcss plugin that derives CSS from a non-CSS file and registers it via the standard { type: 'dependency' } result-message protocol (Tailwind configs, design-token pipelines, CSS-from-TS emitters) rebuilds correctly while it succeeds. When the plugin throws — the dependency file has a syntax error mid-edit, the everyday case — the error result is cached with no watch files, so fixing the dependency file can never clear the error. Every subsequent rebuild replays the stale cached error in ~1ms (the dev-server overlay keeps showing it, pointing at code that no longer exists), until the .css entry file itself is re-saved or the server restarted. The served stylesheet stays frozen at the last successful compile.
Root cause
Both pieces are in @angular/build (verified on 22.0.5 and 22.0.8; the code is unchanged on main):
src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts — compileString(): on success, postcss dependency messages become loadResult.watchFiles; on the catch path the returned { errors } result carries no watchFiles (the messages died with the rejected postcss run). After the merge in processStylesheet() the error result's watch set is just [filename] — the .css entry itself.
src/tools/esbuild/load-result-cache.ts — createCachedLoad() caches error results (only null/undefined are excluded), and MemoryLoadResultCache.invalidate() deletes the #fileDependencies entry for the changed file when it fires. After the failed compile is cached, the dependency file maps to no cache entry: later edits to it (including the fix) invalidate nothing, and the cached error is replayed indefinitely.
The file watcher itself is fine — build-action deliberately keeps watching all previous files while errors are present, so rebuilds do trigger; they just serve the cached error.
Suggested fix directions (either suffices)
- Mirror the Sass/Less pattern where the information exists:
CssSyntaxError carries error.file, so the catch can return watchFiles: [filename, error.file]. That still leaves generic plugin throws (this repro; any JSON.parse/module-load failure inside a plugin) with no file information, hence:
- Make the cache resilient instead: let
MemoryLoadResultCache remember the last known watch set per result key across invalidation, and union it into a newly cached result that contains errors. Over-watching a stale set for one rebuild costs one extra recompile; under-watching pins the error until a manual touch of the entry file.
Minimal Reproduction
https://github.com/Siddhu-K/ng-postcss-stale-error-repro
The repro is ng new-minimal plus three files: a 25-line plain-JS postcss plugin (theme-plugin.cjs) that replaces an @inject-theme; marker in styles.css with a custom property read from theme.json and pushes a dependency message for it; .postcssrc.json registering the plugin; theme.json.
npm install && npm start
- Edit
theme.json "color": "blue" → rebuild fires, page updates. (Dependency watching works on the success path.)
- Delete a
" in theme.json → rebuild fails with the JSON parse error. Correct.
- Restore the file (any valid content) → rebuild fires and fails again with the same stale error in ~1ms. Repeat edits change nothing; the overlay keeps the stale error; the served CSS still has the value from step 2.
- Re-save
src/styles.css unchanged → next rebuild succeeds.
Expected at step 4: the rebuild re-runs the stylesheet compile against the fixed file and succeeds — exactly what happens for a Sass or Less syntax error, which carry watchFiles: [filePath, fileWithError] on their error results since 5623c19.
Exception or Error
# theme.json on disk at this point: { "color": "green" } (valid — already fixed)
X [ERROR] Unexpected token 'b', "{ "color": blue" }
" is not valid JSON [plugin angular-css]
angular:styles/global:styles:1:8:
1 │ @import 'src/styles.css';
╵ ~~~~~~~~~~~~~~~~
Your Environment
Angular CLI: 22.0.8
@angular/build: 22.0.8 (also verified on 22.0.5)
Angular: 22.0.8
Node: 24.16.0
Package Manager: npm 11.13.0
OS: Windows 11 x64 (mechanism is platform-independent)
Anything else relevant?
Real-world impact: any postcss-based derived-CSS pipeline (design tokens, theme compilers, Tailwind-config-style setups) hits this on every mid-edit syntax error in the derived-from file — the developer fixes their file, the error persists, and the natural conclusion is that their fix is wrong. The only escapes are re-saving the .css entry file or restarting the dev server.
Command
serve (also affects
build --watch)Is this a regression?
Yes. Before 5623c19 error results were not cached, so a rebuild after fixing the file re-ran the compile. That commit started caching error results and compensated by giving the Sass and Less error paths explicit
watchFiles(fileWithError ? [filePath, fileWithError] : [filePath]) — the postcss error path never received the same treatment.Description
A postcss plugin that derives CSS from a non-CSS file and registers it via the standard
{ type: 'dependency' }result-message protocol (Tailwind configs, design-token pipelines, CSS-from-TS emitters) rebuilds correctly while it succeeds. When the plugin throws — the dependency file has a syntax error mid-edit, the everyday case — the error result is cached with no watch files, so fixing the dependency file can never clear the error. Every subsequent rebuild replays the stale cached error in ~1ms (the dev-server overlay keeps showing it, pointing at code that no longer exists), until the.cssentry file itself is re-saved or the server restarted. The served stylesheet stays frozen at the last successful compile.Root cause
Both pieces are in
@angular/build(verified on 22.0.5 and 22.0.8; the code is unchanged onmain):src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts—compileString(): on success, postcssdependencymessages becomeloadResult.watchFiles; on the catch path the returned{ errors }result carries no watchFiles (the messages died with the rejected postcss run). After the merge inprocessStylesheet()the error result's watch set is just[filename]— the.cssentry itself.src/tools/esbuild/load-result-cache.ts—createCachedLoad()caches error results (onlynull/undefinedare excluded), andMemoryLoadResultCache.invalidate()deletes the#fileDependenciesentry for the changed file when it fires. After the failed compile is cached, the dependency file maps to no cache entry: later edits to it (including the fix) invalidate nothing, and the cached error is replayed indefinitely.The file watcher itself is fine —
build-actiondeliberately keeps watching all previous files while errors are present, so rebuilds do trigger; they just serve the cached error.Suggested fix directions (either suffices)
CssSyntaxErrorcarrieserror.file, so the catch can returnwatchFiles: [filename, error.file]. That still leaves generic plugin throws (this repro; anyJSON.parse/module-load failure inside a plugin) with no file information, hence:MemoryLoadResultCacheremember the last known watch set per result key across invalidation, and union it into a newly cached result that containserrors. Over-watching a stale set for one rebuild costs one extra recompile; under-watching pins the error until a manual touch of the entry file.Minimal Reproduction
https://github.com/Siddhu-K/ng-postcss-stale-error-repro
The repro is
ng new-minimal plus three files: a 25-line plain-JS postcss plugin (theme-plugin.cjs) that replaces an@inject-theme;marker instyles.csswith a custom property read fromtheme.jsonand pushes adependencymessage for it;.postcssrc.jsonregistering the plugin;theme.json.npm install && npm starttheme.json"color": "blue"→ rebuild fires, page updates. (Dependency watching works on the success path.)"intheme.json→ rebuild fails with the JSON parse error. Correct.src/styles.cssunchanged → next rebuild succeeds.Expected at step 4: the rebuild re-runs the stylesheet compile against the fixed file and succeeds — exactly what happens for a Sass or Less syntax error, which carry
watchFiles: [filePath, fileWithError]on their error results since 5623c19.Exception or Error
Your Environment
Anything else relevant?
Real-world impact: any postcss-based derived-CSS pipeline (design tokens, theme compilers, Tailwind-config-style setups) hits this on every mid-edit syntax error in the derived-from file — the developer fixes their file, the error persists, and the natural conclusion is that their fix is wrong. The only escapes are re-saving the
.cssentry file or restarting the dev server.