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