1import chalk from 'chalk';
2import { watchFile } from 'fs';
3import path from 'path';
4import resolveFrom from 'resolve-from';
5
6import { memoize } from './fn';
7import * as Log from '../log';
8
9const debug = require('debug')('expo:utils:fileNotifier') as typeof console.log;
10
11/** Observes and reports file changes. */
12export class FileNotifier {
13  static instances: FileNotifier[] = [];
14
15  static stopAll() {
16    for (const instance of FileNotifier.instances) {
17      instance.stopObserving();
18    }
19  }
20
21  private unsubscribe: (() => void) | null = null;
22
23  constructor(
24    /** Project root to resolve the module IDs relative to. */
25    private projectRoot: string,
26    /** List of module IDs sorted by priority. Only the first file that exists will be observed. */
27    private moduleIds: string[],
28    private settings: {
29      /** An additional warning message to add to the notice. */
30      additionalWarning?: string;
31    } = {}
32  ) {
33    FileNotifier.instances.push(this);
34  }
35
36  /** Get the file in the project. */
37  private resolveFilePath(): string | null {
38    for (const moduleId of this.moduleIds) {
39      const filePath = resolveFrom.silent(this.projectRoot, moduleId);
40      if (filePath) {
41        return filePath;
42      }
43    }
44    return null;
45  }
46
47  public startObserving(callback?: (cur: any, prev: any) => void) {
48    const configPath = this.resolveFilePath();
49    if (configPath) {
50      debug(`Observing ${configPath}`);
51      return this.watchFile(configPath, callback);
52    }
53    return configPath;
54  }
55
56  public stopObserving() {
57    this.unsubscribe?.();
58  }
59
60  /** Watch the file and warn to reload the CLI if it changes. */
61  public watchFile = memoize(this.startWatchingFile.bind(this));
62
63  private startWatchingFile(filePath: string, callback?: (cur: any, prev: any) => void): string {
64    const configName = path.relative(this.projectRoot, filePath);
65    const listener = (cur: any, prev: any) => {
66      if (prev.size || cur.size) {
67        Log.log(
68          `\u203A Detected a change in ${chalk.bold(
69            configName
70          )}. Restart the server to see the new results.` + (this.settings.additionalWarning || '')
71        );
72      }
73    };
74
75    const watcher = watchFile(filePath, callback ?? listener);
76
77    this.unsubscribe = () => {
78      watcher.unref();
79    };
80
81    return filePath;
82  }
83}
84