Skip to content

Commit 4e92a64

Browse files
committed
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
1 parent 0a137f9 commit 4e92a64

2 files changed

Lines changed: 38 additions & 1 deletion

File tree

packages/angular/build/src/tools/esbuild/watcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ class WatcherQueue {
252252
}
253253

254254
export async function createWatcher(options?: WatcherOptions): Promise<BuildWatcher> {
255-
if (options?.polling) {
255+
if (options?.polling || options?.followSymlinks) {
256256
return createChokidarWatcher(options);
257257
}
258258

packages/angular/build/src/tools/esbuild/watcher_spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,5 +482,42 @@ describe('Watcher', () => {
482482

483483
await watcher.close();
484484
}, 10000);
485+
486+
it('should detect changes behind a directory symlink when followSymlinks is true', async () => {
487+
const externalDir = fs.realpathSync(
488+
fs.mkdtempSync(path.join(os.tmpdir(), 'watcher-external-')),
489+
);
490+
491+
try {
492+
const externalTargetFile = path.join(externalDir, 'index.ts');
493+
fs.writeFileSync(externalTargetFile, 'export const a = 1;');
494+
495+
const symlinkDir = path.join(tempDir, 'symlinked-lib');
496+
fs.symlinkSync(externalDir, symlinkDir, 'junction');
497+
498+
const symlinkedFile = path.join(symlinkDir, 'index.ts');
499+
500+
const watcher = await createWatcher({
501+
followSymlinks: true,
502+
cwd: tempDir,
503+
});
504+
505+
watcher.add(symlinkedFile);
506+
await setTimeout(150);
507+
508+
const iterator = watcher[Symbol.asyncIterator]();
509+
const nextPromise = iterator.next();
510+
511+
fs.writeFileSync(externalTargetFile, 'export const a = 2;');
512+
513+
const result = await nextPromise;
514+
expect(result.done).toBeFalsy();
515+
expect(result.value?.all.some((f: string) => f.includes('index.ts'))).toBeTrue();
516+
517+
await watcher.close();
518+
} finally {
519+
fs.rmSync(externalDir, { recursive: true, force: true });
520+
}
521+
}, 10000);
485522
});
486523
});

0 commit comments

Comments
 (0)