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