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