1import path from 'path';
2
3import type { ServerLike } from '../BundlerDevServer';
4
5const debug = require('debug')('expo:start:server:metro:waitForTypescript') as typeof console.log;
6
7/**
8 * Use the native file watcher / Metro ruleset to detect if a
9 * TypeScript file is added to the project during development.
10 */
11export function observeApiRouteChanges(
12  projectRoot: string,
13  runner: {
14    metro: import('metro').Server;
15    server: ServerLike;
16  },
17  callback: (filepath: string, operation: string) => Promise<void>
18): () => void {
19  const watcher = runner.metro.getBundler().getBundler().getWatcher();
20
21  const appDir = path.join(projectRoot, 'app');
22  const listener = ({
23    eventsQueue,
24  }: {
25    eventsQueue: {
26      filePath: string;
27      metadata?: {
28        type: 'f' | 'd' | 'l'; // Regular file / Directory / Symlink
29      } | null;
30      type: string;
31    }[];
32  }) => {
33    for (const event of eventsQueue) {
34      if (
35        // event.type === 'add' &&
36        // event.metadata?.type !== 'd' &&
37        // We need to ignore node_modules because Metro will add all of the files in node_modules to the watcher.
38        !/node_modules/.test(event.filePath) &&
39        event.filePath.startsWith(appDir)
40      ) {
41        const { filePath } = event;
42        callback(filePath, event.type);
43      }
44    }
45  };
46
47  watcher.addListener('change', listener);
48
49  const off = () => {
50    watcher.removeListener('change', listener);
51  };
52
53  runner.server.addListener?.('close', off);
54  return off;
55}
56
57/**
58 * Use the native file watcher / Metro ruleset to detect if a
59 * TypeScript file is added to the project during development.
60 */
61export function waitForMetroToObserveTypeScriptFile(
62  projectRoot: string,
63  runner: {
64    metro: import('metro').Server;
65    server: ServerLike;
66  },
67  callback: () => Promise<void>
68): () => void {
69  const watcher = runner.metro.getBundler().getBundler().getWatcher();
70
71  const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
72
73  const listener = ({
74    eventsQueue,
75  }: {
76    eventsQueue: {
77      filePath: string;
78      metadata?: {
79        type: 'f' | 'd' | 'l'; // Regular file / Directory / Symlink
80      } | null;
81      type: string;
82    }[];
83  }) => {
84    for (const event of eventsQueue) {
85      if (
86        event.type === 'add' &&
87        event.metadata?.type !== 'd' &&
88        // We need to ignore node_modules because Metro will add all of the files in node_modules to the watcher.
89        !/node_modules/.test(event.filePath)
90      ) {
91        const { filePath } = event;
92        // Is TypeScript?
93        if (
94          // If the user adds a TypeScript file to the observable files in their project.
95          /\.tsx?$/.test(filePath) ||
96          // Or if the user adds a tsconfig.json file to the project root.
97          filePath === tsconfigPath
98        ) {
99          debug('Detected TypeScript file added to the project: ', filePath);
100          callback();
101          off();
102          return;
103        }
104      }
105    }
106  };
107
108  debug('Waiting for TypeScript files to be added to the project...');
109  watcher.addListener('change', listener);
110
111  const off = () => {
112    watcher.removeListener('change', listener);
113  };
114
115  runner.server.addListener?.('close', off);
116  return off;
117}
118
119export function observeFileChanges(
120  runner: {
121    metro: import('metro').Server;
122    server: ServerLike;
123  },
124  files: string[],
125  callback: () => void | Promise<void>
126): () => void {
127  const watcher = runner.metro.getBundler().getBundler().getWatcher();
128
129  const listener = ({
130    eventsQueue,
131  }: {
132    eventsQueue: {
133      filePath: string;
134      metadata?: {
135        type: 'f' | 'd' | 'l'; // Regular file / Directory / Symlink
136      } | null;
137      type: string;
138    }[];
139  }) => {
140    for (const event of eventsQueue) {
141      if (
142        // event.type === 'add' &&
143        event.metadata?.type !== 'd' &&
144        // We need to ignore node_modules because Metro will add all of the files in node_modules to the watcher.
145        !/node_modules/.test(event.filePath)
146      ) {
147        const { filePath } = event;
148        // Is TypeScript?
149        if (files.includes(filePath)) {
150          debug('Observed change:', filePath);
151          callback();
152          return;
153        }
154      }
155    }
156  };
157
158  debug('Watching file changes:', files);
159  watcher.addListener('change', listener);
160
161  const off = () => {
162    watcher.removeListener('change', listener);
163  };
164
165  runner.server.addListener?.('close', off);
166  return off;
167}
168