18d307f52SEvan Baconimport chalk from 'chalk'; 28d307f52SEvan Baconimport { watchFile } from 'fs'; 38d307f52SEvan Baconimport path from 'path'; 48d307f52SEvan Baconimport resolveFrom from 'resolve-from'; 58d307f52SEvan Bacon 68d307f52SEvan Baconimport { memoize } from './fn'; 7*8a424bebSJames Ideimport * as Log from '../log'; 88d307f52SEvan Bacon 9474a7a4bSEvan Baconconst debug = require('debug')('expo:utils:fileNotifier') as typeof console.log; 10474a7a4bSEvan Bacon 118d307f52SEvan Bacon/** Observes and reports file changes. */ 128d307f52SEvan Baconexport class FileNotifier { 13d8009c4bSEvan Bacon static instances: FileNotifier[] = []; 14d8009c4bSEvan Bacon 15d8009c4bSEvan Bacon static stopAll() { 16d8009c4bSEvan Bacon for (const instance of FileNotifier.instances) { 17d8009c4bSEvan Bacon instance.stopObserving(); 18d8009c4bSEvan Bacon } 19d8009c4bSEvan Bacon } 20d8009c4bSEvan Bacon 215404abc1SEvan Bacon private unsubscribe: (() => void) | null = null; 22d8009c4bSEvan Bacon 238d307f52SEvan Bacon constructor( 248d307f52SEvan Bacon /** Project root to resolve the module IDs relative to. */ 258d307f52SEvan Bacon private projectRoot: string, 268d307f52SEvan Bacon /** List of module IDs sorted by priority. Only the first file that exists will be observed. */ 27fdf34e39SEvan Bacon private moduleIds: string[], 28fdf34e39SEvan Bacon private settings: { 29fdf34e39SEvan Bacon /** An additional warning message to add to the notice. */ 30fdf34e39SEvan Bacon additionalWarning?: string; 31fdf34e39SEvan Bacon } = {} 32d8009c4bSEvan Bacon ) { 33d8009c4bSEvan Bacon FileNotifier.instances.push(this); 34d8009c4bSEvan Bacon } 358d307f52SEvan Bacon 368d307f52SEvan Bacon /** Get the file in the project. */ 3729975bfdSEvan Bacon private resolveFilePath(): string | null { 388d307f52SEvan Bacon for (const moduleId of this.moduleIds) { 398d307f52SEvan Bacon const filePath = resolveFrom.silent(this.projectRoot, moduleId); 408d307f52SEvan Bacon if (filePath) { 418d307f52SEvan Bacon return filePath; 428d307f52SEvan Bacon } 438d307f52SEvan Bacon } 448d307f52SEvan Bacon return null; 458d307f52SEvan Bacon } 468d307f52SEvan Bacon 47d8009c4bSEvan Bacon public startObserving(callback?: (cur: any, prev: any) => void) { 488d307f52SEvan Bacon const configPath = this.resolveFilePath(); 498d307f52SEvan Bacon if (configPath) { 50474a7a4bSEvan Bacon debug(`Observing ${configPath}`); 51d8009c4bSEvan Bacon return this.watchFile(configPath, callback); 528d307f52SEvan Bacon } 538d307f52SEvan Bacon return configPath; 548d307f52SEvan Bacon } 558d307f52SEvan Bacon 565404abc1SEvan Bacon public stopObserving() { 575404abc1SEvan Bacon this.unsubscribe?.(); 585404abc1SEvan Bacon } 595404abc1SEvan Bacon 608d307f52SEvan Bacon /** Watch the file and warn to reload the CLI if it changes. */ 618d307f52SEvan Bacon public watchFile = memoize(this.startWatchingFile.bind(this)); 628d307f52SEvan Bacon 63d8009c4bSEvan Bacon private startWatchingFile(filePath: string, callback?: (cur: any, prev: any) => void): string { 648d307f52SEvan Bacon const configName = path.relative(this.projectRoot, filePath); 655404abc1SEvan Bacon const listener = (cur: any, prev: any) => { 668d307f52SEvan Bacon if (prev.size || cur.size) { 678d307f52SEvan Bacon Log.log( 688d307f52SEvan Bacon `\u203A Detected a change in ${chalk.bold( 698d307f52SEvan Bacon configName 70fdf34e39SEvan Bacon )}. Restart the server to see the new results.` + (this.settings.additionalWarning || '') 718d307f52SEvan Bacon ); 728d307f52SEvan Bacon } 735404abc1SEvan Bacon }; 745404abc1SEvan Bacon 75d8009c4bSEvan Bacon const watcher = watchFile(filePath, callback ?? listener); 765404abc1SEvan Bacon 775404abc1SEvan Bacon this.unsubscribe = () => { 785404abc1SEvan Bacon watcher.unref(); 795404abc1SEvan Bacon }; 805404abc1SEvan Bacon 818d307f52SEvan Bacon return filePath; 828d307f52SEvan Bacon } 838d307f52SEvan Bacon} 84