Skip to content

ng serve: postcss plugin error result is cached without watch files — fixing the dependency file never clears the error until the stylesheet itself is touched #33666

Description

@Siddhu-K

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):

  1. src/tools/esbuild/stylesheets/stylesheet-plugin-factory.tscompileString(): 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.
  2. src/tools/esbuild/load-result-cache.tscreateCachedLoad() 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.

  1. npm install && npm start
  2. Edit theme.json "color": "blue" → rebuild fires, page updates. (Dependency watching works on the success path.)
  3. Delete a " in theme.json → rebuild fails with the JSON parse error. Correct.
  4. 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.
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions