xref: /expo/packages/@expo/config/src/paths/paths.ts (revision ffb37275)
1import fs from 'fs';
2import path from 'path';
3import resolveFrom from 'resolve-from';
4
5import { getConfig } from '../Config';
6import { ProjectConfig } from '../Config.types';
7import { getBareExtensions } from './extensions';
8
9// https://github.com/facebook/create-react-app/blob/9750738cce89a967cc71f28390daf5d4311b193c/packages/react-scripts/config/paths.js#L22
10export function ensureSlash(inputPath: string, needsSlash: boolean): string {
11  const hasSlash = inputPath.endsWith('/');
12  if (hasSlash && !needsSlash) {
13    return inputPath.substr(0, inputPath.length - 1);
14  } else if (!hasSlash && needsSlash) {
15    return `${inputPath}/`;
16  } else {
17    return inputPath;
18  }
19}
20
21export function getPossibleProjectRoot(): string {
22  return fs.realpathSync(process.cwd());
23}
24
25const nativePlatforms = ['ios', 'android'];
26
27export function resolveEntryPoint(
28  projectRoot: string,
29  { platform, projectConfig }: { platform: string; projectConfig?: ProjectConfig }
30) {
31  const platforms = nativePlatforms.includes(platform) ? [platform, 'native'] : [platform];
32  return getEntryPoint(projectRoot, ['./index'], platforms, projectConfig);
33}
34
35export function getEntryPoint(
36  projectRoot: string,
37  entryFiles: string[],
38  platforms: string[],
39  projectConfig?: ProjectConfig
40): string | null {
41  const extensions = getBareExtensions(platforms);
42  return getEntryPointWithExtensions(projectRoot, entryFiles, extensions, projectConfig);
43}
44
45// Used to resolve the main entry file for a project.
46export function getEntryPointWithExtensions(
47  projectRoot: string,
48  entryFiles: string[],
49  extensions: string[],
50  projectConfig?: ProjectConfig
51): string {
52  const { exp, pkg } = projectConfig ?? getConfig(projectRoot, { skipSDKVersionRequirement: true });
53
54  // This will first look in the `app.json`s `expo.entryPoint` field for a potential main file.
55  // We check the Expo config first in case you want your project to start differently with Expo then in a standalone environment.
56  if (exp && exp.entryPoint && typeof exp.entryPoint === 'string') {
57    // If the field exists then we want to test it against every one of the supplied extensions
58    // to ensure the bundler resolves the same way.
59    let entry = getFileWithExtensions(projectRoot, exp.entryPoint, extensions);
60    if (!entry) {
61      // Allow for paths like: `{ "main": "expo/AppEntry" }`
62      entry = resolveFromSilentWithExtensions(projectRoot, exp.entryPoint, extensions);
63
64      // If it doesn't resolve then just return the entryPoint as-is. This makes
65      // it possible for people who have an unconventional setup (eg: multiple
66      // apps in monorepo with metro at root) to customize entry point without
67      // us imposing our assumptions.
68      if (!entry) {
69        return exp.entryPoint;
70      }
71    }
72    return entry;
73  } else if (pkg) {
74    // If the config doesn't define a custom entry then we want to look at the `package.json`s `main` field, and try again.
75    const { main } = pkg;
76    if (main && typeof main === 'string') {
77      // Testing the main field against all of the provided extensions - for legacy reasons we can't use node module resolution as the package.json allows you to pass in a file without a relative path and expect it as a relative path.
78      let entry = getFileWithExtensions(projectRoot, main, extensions);
79      if (!entry) {
80        // Allow for paths like: `{ "main": "expo/AppEntry" }`
81        entry = resolveFromSilentWithExtensions(projectRoot, main, extensions);
82        if (!entry)
83          throw new Error(
84            `Cannot resolve entry file: The \`main\` field defined in your \`package.json\` points to a non-existent path.`
85          );
86      }
87      return entry;
88    }
89  }
90
91  // Now we will start looking for a default entry point using the provided `entryFiles` argument.
92  // This will add support for create-react-app (src/index.js) and react-native-cli (index.js) which don't define a main.
93  for (const fileName of entryFiles) {
94    const entry = resolveFromSilentWithExtensions(projectRoot, fileName, extensions);
95    if (entry) return entry;
96  }
97
98  try {
99    // If none of the default files exist then we will attempt to use the main Expo entry point.
100    // This requires `expo` to be installed in the project to work as it will use `node_module/expo/AppEntry.js`
101    // Doing this enables us to create a bare minimum Expo project.
102
103    // TODO(Bacon): We may want to do a check against `./App` and `expo` in the `package.json` `dependencies` as we can more accurately ensure that the project is expo-min without needing the modules installed.
104    return resolveFrom(projectRoot, 'expo/AppEntry');
105  } catch {
106    throw new Error(
107      `The project entry file could not be resolved. Please either define it in the \`package.json\` (main), \`app.json\` (expo.entryPoint), create an \`index.js\`, or install the \`expo\` package.`
108    );
109  }
110}
111
112// Resolve from but with the ability to resolve like a bundler
113export function resolveFromSilentWithExtensions(
114  fromDirectory: string,
115  moduleId: string,
116  extensions: string[]
117): string | null {
118  for (const extension of extensions) {
119    const modulePath = resolveFrom.silent(fromDirectory, `${moduleId}.${extension}`);
120    if (modulePath && modulePath.endsWith(extension)) {
121      return modulePath;
122    }
123  }
124  return resolveFrom.silent(fromDirectory, moduleId) || null;
125}
126
127// Statically attempt to resolve a module but with the ability to resolve like a bundler.
128// This won't use node module resolution.
129export function getFileWithExtensions(
130  fromDirectory: string,
131  moduleId: string,
132  extensions: string[]
133): string | null {
134  const modulePath = path.join(fromDirectory, moduleId);
135  if (fs.existsSync(modulePath)) {
136    return modulePath;
137  }
138  for (const extension of extensions) {
139    const modulePath = path.join(fromDirectory, `${moduleId}.${extension}`);
140    if (fs.existsSync(modulePath)) {
141      return modulePath;
142    }
143  }
144  return null;
145}
146