diff --git a/packages/core/lib/bundler/bundler.ts b/packages/core/lib/bundler/bundler.ts index 70cdc84b..c9ecf32e 100644 --- a/packages/core/lib/bundler/bundler.ts +++ b/packages/core/lib/bundler/bundler.ts @@ -91,7 +91,6 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter { this.on('report', (event) => { this.broadcastToReporter(event); }); - this.setupWatcher(); } private broadcastToReporter(event: ReportableEvent): void { @@ -121,17 +120,23 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter { } private setupWatcher(): void { + logger.debug('setup watcher'); FileSystemWatcher.getInstance() - .setHandler((changedFile, stats) => { - if (this.buildTasks.size > 0) { + .setHandler((event, changedFile, stats) => { + if (this.buildTasks.size > 0 && event === 'change') { ReactNativeEsbuildBundler.shared.watcher = { changed: changedFile, stats, }; + } else { + ReactNativeEsbuildBundler.shared.watcher = { + changed: null, + stats: undefined, + }; } - for (const { context } of this.buildTasks.values()) { - context.rebuild(); + for (const { context, handler } of this.buildTasks.values()) { + context.rebuild().catch((error) => handler?.rejecter?.(error)); } }) .watch(this.root); @@ -312,20 +317,22 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter { if (!this.buildTasks.has(targetTaskId)) { logger.debug(`bundle task not registered (id: ${targetTaskId})`); + this.setupWatcher(); const buildOptions = await this.getBuildOptionsForBundler( 'watch', bundleOptions, additionalData, ); + const handler = createPromiseHandler(); const context = await esbuild.context(buildOptions); this.buildTasks.set(targetTaskId, { context, - handler: createPromiseHandler(), + handler, status: 'pending', buildCount: 0, }); // trigger first build - context.rebuild(); + context.rebuild().catch((error) => handler.rejecter?.(error)); logger.debug(`bundle task is now watching (id: ${targetTaskId})`); } diff --git a/packages/core/lib/watcher/FileSystemWatcher.ts b/packages/core/lib/watcher/FileSystemWatcher.ts index ef52e73d..12ddddc9 100644 --- a/packages/core/lib/watcher/FileSystemWatcher.ts +++ b/packages/core/lib/watcher/FileSystemWatcher.ts @@ -13,9 +13,11 @@ const WATCH_EXTENSIONS_REGEXP = new RegExp( ); export class FileSystemWatcher { + public static DEBOUNCE_DELAY = 300; private static instance: FileSystemWatcher | null = null; private watcher: chokidar.FSWatcher | null = null; - private onWatch?: (path: string, stats?: Stats) => void; + private onWatch?: (event: string, path: string, stats?: Stats) => void; + private debounceTimer?: NodeJS.Timeout; private constructor() { // empty constructor @@ -28,15 +30,24 @@ export class FileSystemWatcher { return FileSystemWatcher.instance; } - private handleWatch(path: string, stats?: Stats): void { + private handleWatch(event: string, path: string, stats?: Stats): void { logger.debug('event received from watcher', { path }); - if (!WATCH_EXTENSIONS_REGEXP.test(path)) { + if ( + this.debounceTimer !== undefined || + !WATCH_EXTENSIONS_REGEXP.test(path) + ) { return; } - this.onWatch?.(path, stats); + + this.debounceTimer = setTimeout(() => { + this.debounceTimer = undefined; + this.onWatch?.(event, path, stats); + }, FileSystemWatcher.DEBOUNCE_DELAY); } - setHandler(handler: (path: string, stats?: Stats) => void): this { + setHandler( + handler: (event: string, path: string, stats?: Stats) => void, + ): this { this.onWatch = handler; return this; } @@ -49,23 +60,34 @@ export class FileSystemWatcher { const ignoreDirectories = [path.join(targetPath, LOCAL_CACHE_DIR)]; + const addListener = ( + event: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', + ): void => { + if (!this.watcher) return; + this.watcher.on(event, (path, stats) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- allow + this.handleWatch(event, path, stats); + }); + }; + this.watcher = chokidar .watch(targetPath, { alwaysStat: true, ignoreInitial: true, ignored: ignoreDirectories, }) - .on('add', this.handleWatch.bind(this) as typeof this.handleWatch) - .on('addDir', this.handleWatch.bind(this) as typeof this.handleWatch) - .on('change', this.handleWatch.bind(this) as typeof this.handleWatch) - .on('unlink', this.handleWatch.bind(this) as typeof this.handleWatch) - .on('unlinkDir', this.handleWatch.bind(this) as typeof this.handleWatch) .on('ready', () => { logger.debug(`watching '${targetPath}'`); }) .on('error', (error) => { logger.error('unexpected error on watcher', error); }); + + addListener('addDir'); + addListener('unlinkDir'); + addListener('add'); + addListener('change'); + addListener('unlink'); } close(): void {