From 7ca0a1ade0a6fce08645d5cf1a04aa92d04ecbd8 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 9 Sep 2026 15:12:52 -0400 Subject: [PATCH] fix(@angular/build): use chokidar watcher when followSymlinks is enabled When `preserveSymlinks` is enabled in application build options, `followSymlinks: true` is passed to `createWatcher`. With `@parcel/watcher`, native OS directory watchers (such as FSEvents, inotify, and ReadDirectoryChangesW) do not follow directory symlinks pointing outside the workspace root. Additionally, esbuild reports watch files under their symlinked workspace paths, preventing external directory watches from attaching and causing file modifications behind the symlink to be missed. Since `chokidar` natively traverses directory symlinks and surfaces file change events relative to the watched root, `createWatcher` now falls back to Chokidar when `followSymlinks` is enabled. This restores watch rebuild detection for symlinked directories while preserving the performance benefits of `@parcel/watcher` for standard setups. Closes #34039 --- .../build/src/tools/esbuild/watcher.ts | 2 +- .../build/src/tools/esbuild/watcher_spec.ts | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/angular/build/src/tools/esbuild/watcher.ts b/packages/angular/build/src/tools/esbuild/watcher.ts index 1b9fd15d4561..fe41275e479e 100644 --- a/packages/angular/build/src/tools/esbuild/watcher.ts +++ b/packages/angular/build/src/tools/esbuild/watcher.ts @@ -252,7 +252,7 @@ class WatcherQueue { } export async function createWatcher(options?: WatcherOptions): Promise { - if (options?.polling) { + if (options?.polling || options?.followSymlinks) { return createChokidarWatcher(options); } diff --git a/packages/angular/build/src/tools/esbuild/watcher_spec.ts b/packages/angular/build/src/tools/esbuild/watcher_spec.ts index 2c82510a5ae5..5c7853012762 100644 --- a/packages/angular/build/src/tools/esbuild/watcher_spec.ts +++ b/packages/angular/build/src/tools/esbuild/watcher_spec.ts @@ -11,6 +11,7 @@ import * as os from 'node:os'; import * as path from 'node:path'; import { setTimeout } from 'node:timers/promises'; import { + type BuildWatcher, ChangedFiles, createWatcher, getDirectoryPath, @@ -482,5 +483,42 @@ describe('Watcher', () => { await watcher.close(); }, 10000); + + it('should detect changes behind a directory symlink when followSymlinks is true', async () => { + const externalDir = fs.realpathSync( + fs.mkdtempSync(path.join(os.tmpdir(), 'watcher-external-')), + ); + let watcher: BuildWatcher | undefined; + + try { + const externalTargetFile = path.join(externalDir, 'index.ts'); + fs.writeFileSync(externalTargetFile, 'export const a = 1;'); + + const symlinkDir = path.join(tempDir, 'symlinked-lib'); + fs.symlinkSync(externalDir, symlinkDir, 'junction'); + + const symlinkedFile = path.join(symlinkDir, 'index.ts'); + + watcher = await createWatcher({ + followSymlinks: true, + cwd: tempDir, + }); + + watcher.add(symlinkedFile); + await setTimeout(150); + + const iterator = watcher[Symbol.asyncIterator](); + const nextPromise = iterator.next(); + + fs.writeFileSync(externalTargetFile, 'export const a = 2;'); + + const result = await nextPromise; + expect(result.done).toBeFalsy(); + expect(result.value?.all.some((f: string) => f.includes('index.ts'))).toBeTrue(); + } finally { + await watcher?.close(); + fs.rmSync(externalDir, { recursive: true, force: true }); + } + }, 10000); }); });