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