diff --git a/src/main/ipc/filesystem-watcher-ignore.test.ts b/src/main/ipc/filesystem-watcher-ignore.test.ts index 6784143e0..6a3ab329f 100644 --- a/src/main/ipc/filesystem-watcher-ignore.test.ts +++ b/src/main/ipc/filesystem-watcher-ignore.test.ts @@ -32,8 +32,18 @@ describe('buildParcelWatcherIgnoreOption', () => { } }) - it('passes the plain list through on other platforms', () => { - setPlatform('linux') - expect(buildParcelWatcherIgnoreOption(WATCHER_IGNORE_DIRS)).toEqual(WATCHER_IGNORE_DIRS) + it('expands to nested globs on Linux/Windows so nested node_modules/.git are excluded', () => { + for (const platform of ['linux', 'win32'] as const) { + setPlatform(platform) + const option = buildParcelWatcherIgnoreOption(WATCHER_IGNORE_DIRS) + // @parcel/watcher resolves plain names to top-level-only absolute paths on + // these platforms, so nested dirs must be matched via depth-agnostic globs. + for (const dir of WATCHER_IGNORE_DIRS) { + expect(option).toContain(`**/${dir}`) + expect(option).toContain(`**/${dir}/**`) + } + // No plain (glob-free) entries — those would only exclude the top level. + expect(option.every((entry) => entry.includes('*'))).toBe(true) + } }) }) diff --git a/src/main/ipc/filesystem-watcher-ignore.ts b/src/main/ipc/filesystem-watcher-ignore.ts index 61339afb3..9828f4a59 100644 --- a/src/main/ipc/filesystem-watcher-ignore.ts +++ b/src/main/ipc/filesystem-watcher-ignore.ts @@ -25,7 +25,12 @@ export const MACOS_FSEVENTS_EXCLUSION_PATH_LIMIT = 8 export function buildParcelWatcherIgnoreOption(ignoreDirs: readonly string[]): string[] { if (process.platform !== 'darwin') { - return [...ignoreDirs] + // Linux/Windows: @parcel/watcher resolves plain names to top-level-only + // absolute paths, so nested node_modules/.git/build/etc. (monorepos) get + // fully watched — on Linux that means one inotify watch per nested dir, + // exhausting fs.inotify.max_user_watches. Nested globs match at any depth + // (and **/dir still matches the top-level dir), pruning those subtrees. + return ignoreDirs.flatMap((dir) => [`**/${dir}`, `**/${dir}/**`]) } return [ ...ignoreDirs.slice(0, MACOS_FSEVENTS_EXCLUSION_PATH_LIMIT), diff --git a/src/relay/fs-handler.ts b/src/relay/fs-handler.ts index a1b4ee086..0c4a263f1 100644 --- a/src/relay/fs-handler.ts +++ b/src/relay/fs-handler.ts @@ -29,6 +29,10 @@ import { RelayStreamRegistry } from './fs-stream-registry' import { scanWorkspaceSpaceDirectory } from './workspace-space-scan' import { buildRelayCommandEnv } from './relay-command-env' import { assertNoClobberRenameDestinationAvailable } from '../shared/filesystem-rename-collision' +import { + WATCHER_IGNORE_DIRS, + buildParcelWatcherIgnoreOption +} from '../main/ipc/filesystem-watcher-ignore' type WatchState = { rootPath: string @@ -408,7 +412,9 @@ export class FsHandler { })) this.dispatcher.notify('fs.changed', { events: mapped }) }, - { ignore: ['.git', 'node_modules', 'dist', 'build', '.next', '.cache', '__pycache__'] } + // Why: align remote-Linux watchers with the shared nested-glob exclusion + // so nested node_modules/.git don't exhaust inotify on large codebases. + { ignore: buildParcelWatcherIgnoreOption(WATCHER_IGNORE_DIRS) } ) watchState.unwatchFn = () => { void subscription.unsubscribe()