Skip to content

Commit 7ca0a1a

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 7ca0a1a

2 files changed

Lines changed: 39 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: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import * as os from 'node:os';
1111
import * as path from 'node:path';
1212
import { setTimeout } from 'node:timers/promises';
1313
import {
14+
type BuildWatcher,
1415
ChangedFiles,
1516
createWatcher,
1617
getDirectoryPath,
@@ -482,5 +483,42 @@ describe('Watcher', () => {
482483

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

0 commit comments

Comments
 (0)