1import path from 'path';
2
3import type { ServerLike } from '../BundlerDevServer';
4
5const debug = require('debug')(
6  'expo:start:server:metro:metroWatchTypeScriptFiles'
7) as typeof console.log;
8
9export interface MetroWatchTypeScriptFilesOptions {
10  projectRoot: string;
11  metro: import('metro').Server;
12  server: ServerLike;
13  /* Include tsconfig.json in the watcher */
14  tsconfig?: boolean;
15  callback: (event: WatchEvent) => void;
16  /* Array of eventTypes to watch. Defaults to all events */
17  eventTypes?: string[];
18  /* Throlle the callback. When true and  a group of events are recieved, callback it will only be called with the
19   * first event */
20  throttle?: boolean;
21}
22
23interface WatchEvent {
24  filePath: string;
25  metadata?: {
26    type: 'f' | 'd' | 'l'; // Regular file / Directory / Symlink
27  } | null;
28  type: string;
29}
30
31/**
32 * Use the native file watcher / Metro ruleset to detect if a
33 * TypeScript file is added to the project during development.
34 */
35export function metroWatchTypeScriptFiles({
36  metro,
37  server,
38  projectRoot,
39  callback,
40  tsconfig = false,
41  throttle = false,
42  eventTypes = ['add', 'change', 'delete'],
43}: MetroWatchTypeScriptFilesOptions): () => void {
44  const watcher = metro.getBundler().getBundler().getWatcher();
45
46  const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
47
48  const listener = ({ eventsQueue }: { eventsQueue: WatchEvent[] }) => {
49    for (const event of eventsQueue) {
50      if (
51        eventTypes.includes(event.type) &&
52        event.metadata?.type !== 'd' &&
53        // We need to ignore node_modules because Metro will add all of the files in node_modules to the watcher.
54        !/node_modules/.test(event.filePath) &&
55        // Ignore declaration files
56        !/\.d\.ts$/.test(event.filePath)
57      ) {
58        const { filePath } = event;
59        // Is TypeScript?
60        if (
61          // If the user adds a TypeScript file to the observable files in their project.
62          /\.tsx?$/.test(filePath) ||
63          // Or if the user adds a tsconfig.json file to the project root.
64          (tsconfig && filePath === tsconfigPath)
65        ) {
66          debug('Detected TypeScript file changed in the project: ', filePath);
67          callback(event);
68
69          if (throttle) {
70            return;
71          }
72        }
73      }
74    }
75  };
76
77  debug('Waiting for TypeScript files to be added to the project...');
78  watcher.addListener('change', listener);
79  watcher.addListener('add', listener);
80
81  const off = () => {
82    watcher.removeListener('change', listener);
83    watcher.removeListener('add', listener);
84  };
85
86  server.addListener?.('close', off);
87  return off;
88}
89