From c596af1aba885b5f6b6d79995e1faa9e70353e7a Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:57:18 -0400 Subject: [PATCH] fix(@angular/build): recursively ignore output and cache paths in watch mode When watching for file changes, @parcel/watcher and chokidar use glob matching against the ignored patterns. Passing exact directory paths without recursive glob wildcards (`/**`) fails to match nested output artifacts and cache files on disk. Additionally, unnormalized Windows path separators in directory paths prevent POSIX glob engines from matching. When directory watching or NG_BUILD_WATCH_ROOT is enabled, mutations inside the output directory (such as emitted bundles or deleted files during rebuild cleanup) were falsely detected as modified source files, triggering spurious rebuilds and causing race conditions on missing output files. Output and cache directory paths are now POSIX-normalized and configured with recursive glob patterns (`/**`) so all nested files and subdirectories are properly ignored by the watcher. --- .../src/builders/application/build-action.ts | 8 +++-- .../build/src/tools/esbuild/watcher.ts | 13 ++++++- .../build/src/tools/esbuild/watcher_spec.ts | 34 +++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/packages/angular/build/src/builders/application/build-action.ts b/packages/angular/build/src/builders/application/build-action.ts index af0ce30f687d..e71663765274 100644 --- a/packages/angular/build/src/builders/application/build-action.ts +++ b/packages/angular/build/src/builders/application/build-action.ts @@ -108,10 +108,14 @@ export async function* runEsBuildBuildAction( logger.info('Watch mode enabled. Watching for file changes...'); } + const normalizedOutputBase = toPosixPath(outputOptions.base); + const normalizedCacheBase = toPosixPath(cacheOptions.basePath); const ignored: string[] = [ // Ignore the output and cache paths to avoid infinite rebuild cycles - outputOptions.base, - cacheOptions.basePath, + normalizedOutputBase, + `${normalizedOutputBase}/**`, + normalizedCacheBase, + `${normalizedCacheBase}/**`, `${toPosixPath(workspaceRoot)}/**/.*/**`, ]; diff --git a/packages/angular/build/src/tools/esbuild/watcher.ts b/packages/angular/build/src/tools/esbuild/watcher.ts index 2ba0764d9023..b6e26f5c72af 100644 --- a/packages/angular/build/src/tools/esbuild/watcher.ts +++ b/packages/angular/build/src/tools/esbuild/watcher.ts @@ -10,6 +10,7 @@ import type * as ParcelWatcher from '@parcel/watcher'; import type * as Chokidar from 'chokidar'; import * as fs from 'node:fs'; import * as path from 'node:path'; +import picomatch from 'picomatch'; import { toPosixPath } from '../../utils/path'; export class ChangedFiles { @@ -517,9 +518,19 @@ async function createChokidarWatcher( const rootDirPosix = toPosixPathNormalized(rootDir); const rootDirLookupKey = toLookupKey(rootDirPosix, isCaseSensitive); + const ignored = options?.ignored?.map((pattern) => { + if (/[*?[\]{}()]/.test(pattern)) { + const isMatch = picomatch(pattern, { dot: true }); + + return (filePath: string) => isMatch(toPosixPathNormalized(filePath)); + } + + return { path: toPosixPathNormalized(pattern), recursive: true }; + }); + const watcher = chokidar.watch(rootDir, { ignoreInitial: true, - ignored: options?.ignored, + ignored, followSymlinks: options?.followSymlinks, usePolling: !!options?.polling, interval: options?.interval, diff --git a/packages/angular/build/src/tools/esbuild/watcher_spec.ts b/packages/angular/build/src/tools/esbuild/watcher_spec.ts index 92f7dcaf5d38..2c82510a5ae5 100644 --- a/packages/angular/build/src/tools/esbuild/watcher_spec.ts +++ b/packages/angular/build/src/tools/esbuild/watcher_spec.ts @@ -448,5 +448,39 @@ describe('Watcher', () => { await watcher.close(); }, 10000); + + it('should ignore changes matching glob patterns in polling mode (chokidar)', async () => { + const ignoredDir = path.join(tempDir, 'dist'); + fs.mkdirSync(ignoredDir); + const ignoredFile = path.join(ignoredDir, 'bundle.js'); + const watchedFile = path.join(tempDir, 'src.ts'); + fs.writeFileSync(ignoredFile, 'initial-dist'); + fs.writeFileSync(watchedFile, 'initial-src'); + + const watcher = await createWatcher({ + polling: true, + interval: 50, + cwd: tempDir, + ignored: [`${toPosixPathNormalized(ignoredDir)}/**`], + }); + + watcher.add(tempDir); + await setTimeout(100); + + const iterator = watcher[Symbol.asyncIterator](); + const nextPromise = iterator.next(); + + // Trigger changes in ignored file and watched file + fs.writeFileSync(ignoredFile, 'updated-dist'); + fs.writeFileSync(watchedFile, 'updated-src'); + + const result = await nextPromise; + expect(result.done).toBeFalsy(); + const emitted = result.value?.all ?? []; + expect(emitted.some((f: string) => f.includes('src.ts'))).toBeTrue(); + expect(emitted.some((f: string) => f.includes('bundle.js'))).toBeFalse(); + + await watcher.close(); + }, 10000); }); });