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