From 67230b2394515892db0d4ad23682a45f750be606 Mon Sep 17 00:00:00 2001 From: David First Date: Fri, 5 Dec 2025 14:50:07 -0500 Subject: [PATCH 1/8] fix(watcher): use Watchman when available and reduce FSEvents usage - Auto-detect Watchman on macOS and use it instead of FSEvents to avoid ~500 stream limit - Switch scope watcher to polling mode (300ms interval) to reduce FSEvents consumption - Enhance error message to recommend installing Watchman via 'brew install watchman' --- scopes/scope/scope/scope.main.runtime.ts | 5 +- scopes/workspace/watcher/fsevents-error.ts | 4 ++ scopes/workspace/watcher/watcher.ts | 58 +++++++++++++++++----- 3 files changed, 53 insertions(+), 14 deletions(-) diff --git a/scopes/scope/scope/scope.main.runtime.ts b/scopes/scope/scope/scope.main.runtime.ts index f31a6db838aa..5fa4de48a361 100644 --- a/scopes/scope/scope/scope.main.runtime.ts +++ b/scopes/scope/scope/scope.main.runtime.ts @@ -566,7 +566,10 @@ export class ScopeMain implements ComponentFactory { const globalConfigFile = globalStore.getPath(); const scopeJsonPath = scopeStore.getPath(); const pathsToWatch = [scopeIndexFile, remoteLanesDir, globalConfigFile, scopeJsonPath]; - const watcher = chokidar.watch(pathsToWatch, watchOptions); + // Use polling to reduce FSEvents stream consumption on macOS. + // These files change infrequently (mainly during import/export operations), + // so the small CPU overhead of polling is acceptable. + const watcher = chokidar.watch(pathsToWatch, { ...watchOptions, usePolling: true, interval: 300 }); watcher.on('ready', () => { this.logger.debug(`watchSystemFiles has started, watching ${pathsToWatch.join(', ')}`); }); diff --git a/scopes/workspace/watcher/fsevents-error.ts b/scopes/workspace/watcher/fsevents-error.ts index a793d88a13aa..b1016aa4639d 100644 --- a/scopes/workspace/watcher/fsevents-error.ts +++ b/scopes/workspace/watcher/fsevents-error.ts @@ -108,6 +108,10 @@ Each running watcher process may consume multiple streams internally.`; } message += `\n +${chalk.green('Recommended solution:')} Install Watchman to avoid FSEvents limits entirely. +Watchman is a file watching service that uses a single daemon for all watchers: + ${chalk.cyan('brew install watchman')} + ${chalk.yellow('Note:')} If you're using "bit start" or "bit run", you don't need "bit watch" separately. With the VS Code Bit extension, you can use "Compile on Change" instead of running a watcher manually. diff --git a/scopes/workspace/watcher/watcher.ts b/scopes/workspace/watcher/watcher.ts index 57d91a623881..0ac29c4eefca 100644 --- a/scopes/workspace/watcher/watcher.ts +++ b/scopes/workspace/watcher/watcher.ts @@ -26,8 +26,9 @@ import type { CheckTypes } from './check-types'; import type { WatcherMain } from './watcher.main.runtime'; import { WatchQueue } from './watch-queue'; import type { Logger } from '@teambit/logger'; -import type { Event } from '@parcel/watcher'; +import type { Event, SubscribeOptions } from '@parcel/watcher'; import ParcelWatcher from '@parcel/watcher'; +import { execSync } from 'child_process'; import { sendEventsToClients } from '@teambit/harmony.modules.send-server-sent-events'; import { WatcherDaemon, WatcherClient, getOrCreateWatcherConnection, type WatcherError } from './watcher-daemon'; import { formatFSEventsErrorMessage } from './fsevents-error'; @@ -130,6 +131,41 @@ export class Watcher { ]; } + /** + * Get Parcel watcher options, preferring Watchman on macOS when available. + * On macOS, FSEvents is the default but has a system-wide limit of ~500 streams. + * Watchman is a single-daemon solution that avoids this limit. + */ + private getParcelWatcherOptions(): SubscribeOptions { + const options: SubscribeOptions = { + ignore: this.getParcelIgnorePatterns(), + }; + + // On macOS, prefer Watchman if available to avoid FSEvents stream limit + if (process.platform === 'darwin') { + if (this.isWatchmanAvailable()) { + options.backend = 'watchman'; + this.logger.debug('Using Watchman backend for file watching'); + } else { + this.logger.debug('Using FSEvents backend for file watching (Watchman not available)'); + } + } + + return options; + } + + /** + * Check if Watchman is installed and running + */ + private isWatchmanAvailable(): boolean { + try { + execSync('watchman version', { stdio: 'ignore' }); + return true; + } catch { + return false; + } + } + async watch() { await this.setRootDirs(); const componentIds = Object.values(this.rootDirs); @@ -178,9 +214,7 @@ export class Watcher { // Original direct Parcel watcher logic (fallback) try { - await ParcelWatcher.subscribe(this.workspace.path, this.onParcelWatch.bind(this), { - ignore: this.getParcelIgnorePatterns(), - }); + await ParcelWatcher.subscribe(this.workspace.path, this.onParcelWatch.bind(this), this.getParcelWatcherOptions()); // Write initial snapshot for FSEvents buffer overflow recovery await this.writeSnapshotIfNeeded(); @@ -210,9 +244,7 @@ export class Watcher { this.parcelSubscription = await ParcelWatcher.subscribe( this.workspace.path, this.onParcelWatchAsDaemon.bind(this), - { - ignore: this.getParcelIgnorePatterns(), - } + this.getParcelWatcherOptions() ); // Write initial snapshot for FSEvents buffer overflow recovery @@ -880,9 +912,7 @@ export class Watcher { } try { - await ParcelWatcher.writeSnapshot(this.workspace.path, this.snapshotPath, { - ignore: this.getParcelIgnorePatterns(), - }); + await ParcelWatcher.writeSnapshot(this.workspace.path, this.snapshotPath, this.getParcelWatcherOptions()); this.logger.debug('Watcher snapshot written successfully'); } catch (err: any) { this.logger.debug(`Failed to write watcher snapshot: ${err.message}`); @@ -931,9 +961,11 @@ export class Watcher { } // Get all events since last snapshot - const missedEvents = await ParcelWatcher.getEventsSince(this.workspace.path, this.snapshotPath, { - ignore: this.getParcelIgnorePatterns(), - }); + const missedEvents = await ParcelWatcher.getEventsSince( + this.workspace.path, + this.snapshotPath, + this.getParcelWatcherOptions() + ); // Write new snapshot immediately after reading events to prevent re-processing same events await this.writeSnapshotIfNeeded(); From 23ad66ca0e8380172f1fe88a985db213cfb34df2 Mon Sep 17 00:00:00 2001 From: David First Date: Fri, 5 Dec 2025 15:09:16 -0500 Subject: [PATCH 2/8] fix(watcher): cache Watchman availability and add timeout --- scopes/workspace/watcher/watcher.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/scopes/workspace/watcher/watcher.ts b/scopes/workspace/watcher/watcher.ts index 0ac29c4eefca..7a75cef631b7 100644 --- a/scopes/workspace/watcher/watcher.ts +++ b/scopes/workspace/watcher/watcher.ts @@ -99,6 +99,8 @@ export class Watcher { private parcelSubscription: { unsubscribe: () => Promise } | null = null; // Signal handlers for cleanup (to avoid accumulation) private signalCleanupHandler: (() => void) | null = null; + // Cached Watchman availability (checked once per process lifetime) + private watchmanAvailable: boolean | null = null; constructor( private workspace: Workspace, private pubsub: PubsubMain, @@ -155,15 +157,20 @@ export class Watcher { } /** - * Check if Watchman is installed and running + * Check if Watchman is installed and running. + * Result is cached to avoid repeated shell executions. */ private isWatchmanAvailable(): boolean { + if (this.watchmanAvailable !== null) { + return this.watchmanAvailable; + } try { - execSync('watchman version', { stdio: 'ignore' }); - return true; + execSync('watchman version', { stdio: 'ignore', timeout: 5000 }); + this.watchmanAvailable = true; } catch { - return false; + this.watchmanAvailable = false; } + return this.watchmanAvailable; } async watch() { From a5fadac6a5dabf3df8dafe1ef29a656c40926fcc Mon Sep 17 00:00:00 2001 From: David First Date: Fri, 5 Dec 2025 16:39:24 -0500 Subject: [PATCH 3/8] fix: use correct Options type from @parcel/watcher --- scopes/workspace/watcher/watcher.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scopes/workspace/watcher/watcher.ts b/scopes/workspace/watcher/watcher.ts index 7a75cef631b7..18bbd6e5c330 100644 --- a/scopes/workspace/watcher/watcher.ts +++ b/scopes/workspace/watcher/watcher.ts @@ -26,7 +26,7 @@ import type { CheckTypes } from './check-types'; import type { WatcherMain } from './watcher.main.runtime'; import { WatchQueue } from './watch-queue'; import type { Logger } from '@teambit/logger'; -import type { Event, SubscribeOptions } from '@parcel/watcher'; +import type { Event, Options as ParcelWatcherOptions } from '@parcel/watcher'; import ParcelWatcher from '@parcel/watcher'; import { execSync } from 'child_process'; import { sendEventsToClients } from '@teambit/harmony.modules.send-server-sent-events'; @@ -138,8 +138,8 @@ export class Watcher { * On macOS, FSEvents is the default but has a system-wide limit of ~500 streams. * Watchman is a single-daemon solution that avoids this limit. */ - private getParcelWatcherOptions(): SubscribeOptions { - const options: SubscribeOptions = { + private getParcelWatcherOptions(): ParcelWatcherOptions { + const options: ParcelWatcherOptions = { ignore: this.getParcelIgnorePatterns(), }; From a32c71c3e4e020513e2eeb2d28cf30f7765b857d Mon Sep 17 00:00:00 2001 From: David First Date: Fri, 5 Dec 2025 17:27:55 -0500 Subject: [PATCH 4/8] fix: add shell:false to execSync and fix comment --- scopes/workspace/watcher/watcher.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scopes/workspace/watcher/watcher.ts b/scopes/workspace/watcher/watcher.ts index 18bbd6e5c330..c8312c8d826b 100644 --- a/scopes/workspace/watcher/watcher.ts +++ b/scopes/workspace/watcher/watcher.ts @@ -157,7 +157,7 @@ export class Watcher { } /** - * Check if Watchman is installed and running. + * Check if Watchman is installed. * Result is cached to avoid repeated shell executions. */ private isWatchmanAvailable(): boolean { @@ -165,7 +165,7 @@ export class Watcher { return this.watchmanAvailable; } try { - execSync('watchman version', { stdio: 'ignore', timeout: 5000 }); + execSync('watchman version', { stdio: 'ignore', timeout: 5000, shell: false }); this.watchmanAvailable = true; } catch { this.watchmanAvailable = false; From 36897337bd40959ab35766f62119d68f568f728e Mon Sep 17 00:00:00 2001 From: David First Date: Mon, 8 Dec 2025 09:44:33 -0500 Subject: [PATCH 5/8] fix: remove shell:false as it expects string, not boolean --- scopes/workspace/watcher/watcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scopes/workspace/watcher/watcher.ts b/scopes/workspace/watcher/watcher.ts index c8312c8d826b..411d5671428b 100644 --- a/scopes/workspace/watcher/watcher.ts +++ b/scopes/workspace/watcher/watcher.ts @@ -165,7 +165,7 @@ export class Watcher { return this.watchmanAvailable; } try { - execSync('watchman version', { stdio: 'ignore', timeout: 5000, shell: false }); + execSync('watchman version', { stdio: 'ignore', timeout: 5000 }); this.watchmanAvailable = true; } catch { this.watchmanAvailable = false; From d6038458fdd4d9d68067b99d49da935f4c553d5d Mon Sep 17 00:00:00 2001 From: David First Date: Mon, 8 Dec 2025 09:48:24 -0500 Subject: [PATCH 6/8] fix: use spawnSync instead of execSync for shell-free execution --- scopes/workspace/watcher/watcher.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/scopes/workspace/watcher/watcher.ts b/scopes/workspace/watcher/watcher.ts index 411d5671428b..7b4f54dcbdb4 100644 --- a/scopes/workspace/watcher/watcher.ts +++ b/scopes/workspace/watcher/watcher.ts @@ -28,7 +28,7 @@ import { WatchQueue } from './watch-queue'; import type { Logger } from '@teambit/logger'; import type { Event, Options as ParcelWatcherOptions } from '@parcel/watcher'; import ParcelWatcher from '@parcel/watcher'; -import { execSync } from 'child_process'; +import { spawnSync } from 'child_process'; import { sendEventsToClients } from '@teambit/harmony.modules.send-server-sent-events'; import { WatcherDaemon, WatcherClient, getOrCreateWatcherConnection, type WatcherError } from './watcher-daemon'; import { formatFSEventsErrorMessage } from './fsevents-error'; @@ -158,18 +158,15 @@ export class Watcher { /** * Check if Watchman is installed. - * Result is cached to avoid repeated shell executions. + * Result is cached to avoid repeated executions. */ private isWatchmanAvailable(): boolean { if (this.watchmanAvailable !== null) { return this.watchmanAvailable; } - try { - execSync('watchman version', { stdio: 'ignore', timeout: 5000 }); - this.watchmanAvailable = true; - } catch { - this.watchmanAvailable = false; - } + // Use spawnSync with shell: false (default) for security - prevents command injection + const result = spawnSync('watchman', ['version'], { stdio: 'ignore', timeout: 5000 }); + this.watchmanAvailable = result.status === 0; return this.watchmanAvailable; } From 75a28643b26a720f0a8aa0a402087310e13a76d5 Mon Sep 17 00:00:00 2001 From: David First Date: Mon, 8 Dec 2025 15:41:35 -0500 Subject: [PATCH 7/8] fix: add error handling for spawnSync in Watchman check --- e2e/harmony/jest.e2e.ts | 2 +- scopes/workspace/watcher/watcher.ts | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/e2e/harmony/jest.e2e.ts b/e2e/harmony/jest.e2e.ts index 2ecbf0c8e0cb..fbb53bf19729 100644 --- a/e2e/harmony/jest.e2e.ts +++ b/e2e/harmony/jest.e2e.ts @@ -45,7 +45,7 @@ describe('Jest Tester', function () { const output = helper.command.test('', true); expect(output).to.have.string('✓ should pass'); }); - it('bit build should show the passing component via Jest output', () => { + it.only('bit build should show the passing component via Jest output', () => { const output = helper.command.build('', undefined, true); expect(output).to.have.string('✓ should pass'); }); diff --git a/scopes/workspace/watcher/watcher.ts b/scopes/workspace/watcher/watcher.ts index 7b4f54dcbdb4..de16d9709836 100644 --- a/scopes/workspace/watcher/watcher.ts +++ b/scopes/workspace/watcher/watcher.ts @@ -164,9 +164,14 @@ export class Watcher { if (this.watchmanAvailable !== null) { return this.watchmanAvailable; } - // Use spawnSync with shell: false (default) for security - prevents command injection - const result = spawnSync('watchman', ['version'], { stdio: 'ignore', timeout: 5000 }); - this.watchmanAvailable = result.status === 0; + try { + // Use spawnSync with shell: false (default) for security - prevents command injection + const result = spawnSync('watchman', ['version'], { stdio: 'ignore', timeout: 5000 }); + // Check for spawn errors (e.g., command not found) or non-zero exit status + this.watchmanAvailable = !result.error && result.status === 0; + } catch { + this.watchmanAvailable = false; + } return this.watchmanAvailable; } From 0ed6a80e4e695201ab596a2451f45eddfee06bfd Mon Sep 17 00:00:00 2001 From: David First Date: Mon, 8 Dec 2025 17:29:39 -0500 Subject: [PATCH 8/8] remove .only --- e2e/harmony/jest.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/harmony/jest.e2e.ts b/e2e/harmony/jest.e2e.ts index fbb53bf19729..2ecbf0c8e0cb 100644 --- a/e2e/harmony/jest.e2e.ts +++ b/e2e/harmony/jest.e2e.ts @@ -45,7 +45,7 @@ describe('Jest Tester', function () { const output = helper.command.test('', true); expect(output).to.have.string('✓ should pass'); }); - it.only('bit build should show the passing component via Jest output', () => { + it('bit build should show the passing component via Jest output', () => { const output = helper.command.build('', undefined, true); expect(output).to.have.string('✓ should pass'); });