1import { ExpoConfig } from '@expo/config'; 2import chalk from 'chalk'; 3import { sync as globSync } from 'glob'; 4import path from 'path'; 5import resolveFrom from 'resolve-from'; 6 7import { Log } from '../../../log'; 8import { directoryExistsSync } from '../../../utils/dir'; 9 10const debug = require('debug')('expo:start:server:metro:router') as typeof console.log; 11 12/** 13 * Get the relative path for requiring the `/app` folder relative to the `expo-router/entry` file. 14 * This mechanism does require the server to restart after the `expo-router` package is installed. 15 */ 16export function getAppRouterRelativeEntryPath( 17 projectRoot: string, 18 routerDirectory: string = getRouterDirectory(projectRoot) 19): string | undefined { 20 // Auto pick App entry 21 const routerEntry = 22 resolveFrom.silent(projectRoot, 'expo-router/entry') ?? getFallbackEntryRoot(projectRoot); 23 if (!routerEntry) { 24 return undefined; 25 } 26 // It doesn't matter if the app folder exists. 27 const appFolder = path.join(projectRoot, routerDirectory); 28 const appRoot = path.relative(path.dirname(routerEntry), appFolder); 29 debug('routerEntry', routerEntry, appFolder, appRoot); 30 return appRoot; 31} 32 33/** If the `expo-router` package is not installed, then use the `expo` package to determine where the node modules are relative to the project. */ 34function getFallbackEntryRoot(projectRoot: string): string { 35 const expoRoot = resolveFrom.silent(projectRoot, 'expo/package.json'); 36 if (expoRoot) { 37 return path.join(path.dirname(path.dirname(expoRoot)), 'expo-router/entry'); 38 } 39 return path.join(projectRoot, 'node_modules/expo-router/entry'); 40} 41 42export function getRouterDirectoryModuleIdWithManifest( 43 projectRoot: string, 44 exp: ExpoConfig 45): string { 46 return exp.extra?.router?.unstable_src ?? getRouterDirectory(projectRoot); 47} 48 49export function getRouterDirectoryWithManifest(projectRoot: string, exp: ExpoConfig): string { 50 return path.join(projectRoot, getRouterDirectoryModuleIdWithManifest(projectRoot, exp)); 51} 52 53export function getRouterDirectory(projectRoot: string): string { 54 // more specific directories first 55 if (directoryExistsSync(path.join(projectRoot, 'src/app'))) { 56 Log.log(chalk.gray('Using src/app as the root directory for Expo Router.')); 57 return 'src/app'; 58 } 59 60 Log.debug('Using app as the root directory for Expo Router.'); 61 return 'app'; 62} 63 64export function isApiRouteConvention(name: string): boolean { 65 return /\+api\.[tj]sx?$/.test(name); 66} 67 68export function getApiRoutesForDirectory(cwd: string) { 69 return globSync('**/*+api.@(ts|tsx|js|jsx)', { 70 cwd, 71 absolute: true, 72 }); 73} 74 75// Used to emulate a context module, but way faster. TODO: May need to adjust the extensions to stay in sync with Metro. 76export function getRoutePaths(cwd: string) { 77 return globSync('**/*.@(ts|tsx|js|jsx)', { 78 cwd, 79 }).map((p) => './' + normalizePaths(p)); 80} 81 82function normalizePaths(p: string) { 83 return p.replace(/\\/g, '/'); 84} 85