From fc11e53cdfac2e4982e485bc6e9cb003a74d02de Mon Sep 17 00:00:00 2001 From: MeAkib Date: Wed, 21 Oct 2026 00:25:03 +0600 Subject: [PATCH 1/2] fix(@angular/build): rebuild stylesheets when files are added to a PostCSS directory dependency Previously, a PostCSS `dir-dependency` message (used by Tailwind CSS v4 for its content sources) was expanded with a glob and only the files that existed at build time were watched. Files created later, or moved by renaming their folder, did not invalidate the cached stylesheet, so classes used only in them were not generated until the stylesheet itself changed. The directory is now also added to the watch files, and the load result cache invalidates an entry when a changed path is inside one of its watched directories. Fixes #34446 --- .../behavior/rebuild-global_styles_spec.ts | 62 +++++++++++++++++++ .../src/tools/esbuild/load-result-cache.ts | 27 +++++--- .../tools/esbuild/load-result-cache_spec.ts | 16 +++++ .../stylesheets/stylesheet-plugin-factory.ts | 2 + 4 files changed, 99 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 2de5c5f229ca..3cb5d41de1fe 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 @@ -198,6 +198,68 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { ); }); + it('rebuilds PostCSS stylesheet when a file is added to a plugin directory dependency', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + styles: ['src/styles.css'], + }); + + // Emits a class for each source file, similar to Tailwind CSS v4 content scanning + await harness.writeFile( + 'test-plugin.js', + ` + const fs = require('fs'); + const path = require('path'); + module.exports = () => { + return { + postcssPlugin: 'test-plugin', + Once(root, { result }) { + const dir = path.join(path.dirname(root.source.input.file), 'sources'); + result.messages.push({ + type: 'dir-dependency', + dir, + glob: '**/*.txt', + }); + for (const file of fs.readdirSync(dir, { recursive: true })) { + if (file.endsWith('.txt')) { + const name = fs.readFileSync(path.join(dir, file), 'utf-8').trim(); + root.append('.' + name + ' { color: red; }'); + } + } + }, + }; + }; + module.exports.postcss = true; + `, + ); + await harness.writeFile( + '.postcssrc.json', + JSON.stringify({ + plugins: { + './test-plugin.js': {}, + }, + }), + ); + await harness.writeFile('src/styles.css', '/* base */'); + await harness.writeFile('src/sources/foo/a.txt', 'foo-class'); + + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/styles.css').content.toContain('.foo-class'); + harness.expectFile('dist/browser/styles.css').content.not.toContain('.bar-class'); + + await harness.writeFile('src/sources/bar/b.txt', 'bar-class'); + }, + ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/styles.css').content.toContain('.foo-class'); + harness.expectFile('dist/browser/styles.css').content.toContain('.bar-class'); + }, + ]); + }); + it('rebuilds PostCSS stylesheet after CSS syntax 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 402069cff1a5..95c562df5269 100644 --- a/packages/angular/build/src/tools/esbuild/load-result-cache.ts +++ b/packages/angular/build/src/tools/esbuild/load-result-cache.ts @@ -7,7 +7,7 @@ */ import type { OnLoadResult, PluginBuild } from 'esbuild'; -import { normalize } from 'node:path'; +import { dirname, normalize } from 'node:path'; export interface LoadResultCache { get(path: string): OnLoadResult | Promise | undefined; @@ -108,16 +108,27 @@ export class MemoryLoadResultCache implements LoadResultCache { } invalidate(path: string): boolean { - const affectedPaths = this.#fileDependencies.get(path); - if (!affectedPaths) { - return false; - } + let invalid = false; + + // Check parent directories to support directory dependencies (PostCSS `dir-dependency`) + let current = path; + while (true) { + const affectedPaths = this.#fileDependencies.get(current); + if (affectedPaths) { + for (const affected of affectedPaths) { + this.#loadResults.delete(affected); + } + invalid = true; + } - for (const affected of affectedPaths) { - this.#loadResults.delete(affected); + const parent = dirname(current); + if (parent === current) { + break; + } + current = parent; } - return true; + return invalid; } get watchFiles(): string[] { diff --git a/packages/angular/build/src/tools/esbuild/load-result-cache_spec.ts b/packages/angular/build/src/tools/esbuild/load-result-cache_spec.ts index 8a8682beed61..876308f94512 100644 --- a/packages/angular/build/src/tools/esbuild/load-result-cache_spec.ts +++ b/packages/angular/build/src/tools/esbuild/load-result-cache_spec.ts @@ -55,6 +55,22 @@ describe('MemoryLoadResultCache', () => { expect(cache.get('file:/test/styles.css')).toBeUndefined(); }); + it('should invalidate cached results when a file inside a dependency directory changes', async () => { + const result = { + contents: 'body { color: red; }', + loader: 'css' as const, + watchFiles: ['/test/styles.css', '/test/src'], + }; + + await cache.put('file:/test/styles.css', result); + + expect(cache.invalidate('/test/other/a.ts')).toBeFalse(); + expect(cache.get('file:/test/styles.css')).toBe(result); + + expect(cache.invalidate('/test/src/app/new/a.ts')).toBeTrue(); + expect(cache.get('file:/test/styles.css')).toBeUndefined(); + }); + it('should preserve previous watch files when caching an error result', async () => { const successResult = { contents: 'body { color: red; }', diff --git a/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts b/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts index 79337fe1652e..b3f320146510 100644 --- a/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts +++ b/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts @@ -401,6 +401,8 @@ async function compileString( typeof resultMessage['glob'] === 'string' ) { loadResult.watchFiles ??= []; + // Watch the directory itself so files added or moved into it later also cause a rebuild + loadResult.watchFiles.push(resultMessage['dir']); const dependencies = await glob(resultMessage['glob'], { absolute: true, cwd: resultMessage['dir'], From 8462bbf10dced04af0254e3b7f8cc2223dc635d7 Mon Sep 17 00:00:00 2001 From: Shuaib Hasan Akib <48770016+MeAkib@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:30:15 +0600 Subject: [PATCH 2/2] fixup! fix(@angular/build): rebuild stylesheets when files are added to a PostCSS directory dependency --- packages/angular/build/src/tools/esbuild/load-result-cache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 95c562df5269..c413f9375778 100644 --- a/packages/angular/build/src/tools/esbuild/load-result-cache.ts +++ b/packages/angular/build/src/tools/esbuild/load-result-cache.ts @@ -111,7 +111,7 @@ export class MemoryLoadResultCache implements LoadResultCache { let invalid = false; // Check parent directories to support directory dependencies (PostCSS `dir-dependency`) - let current = path; + let current = normalize(path); while (true) { const affectedPaths = this.#fileDependencies.get(current); if (affectedPaths) {