xref: /expo/packages/@expo/config/src/paths/paths.ts (revision 4bf00a55)
1import fs from 'fs';
2import path from 'path';
3import resolveFrom from 'resolve-from';
4
5import { getBareExtensions } from './extensions';
6import { getConfig } from '../Config';
7import { ProjectConfig } from '../Config.types';
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?: Partial<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?: Partial<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?: Partial<ProjectConfig>
51): string {
52  if (!projectConfig) {
53    // drop all logging abilities
54    const original = process.stdout.write;
55    process.stdout.write = () => true;
56    try {
57      projectConfig = getConfig(projectRoot, { skipSDKVersionRequirement: true });
58    } finally {
59      process.stdout.write = original;
60    }
61  }
62
63  const { pkg } = projectConfig;
64
65  if (pkg) {
66    // If the config doesn't define a custom entry then we want to look at the `package.json`s `main` field, and try again.
67    const { main } = pkg;
68    if (main && typeof main === 'string') {
69      // 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.
70      let entry = getFileWithExtensions(projectRoot, main, extensions);
71      if (!entry) {
72        // Allow for paths like: `{ "main": "expo/AppEntry" }`
73        entry = resolveFromSilentWithExtensions(projectRoot, main, extensions);
74        if (!entry)
75          throw new Error(
76            `Cannot resolve entry file: The \`main\` field defined in your \`package.json\` points to a non-existent path.`
77          );
78      }
79      return entry;
80    }
81  }
82
83  // Now we will start looking for a default entry point using the provided `entryFiles` argument.
84  // This will add support for create-react-app (src/index.js) and react-native-cli (index.js) which don't define a main.
85  for (const fileName of entryFiles) {
86    const entry = resolveFromSilentWithExtensions(projectRoot, fileName, extensions);
87    if (entry) return entry;
88  }
89
90  try {
91    // If none of the default files exist then we will attempt to use the main Expo entry point.
92    // This requires `expo` to be installed in the project to work as it will use `node_module/expo/AppEntry.js`
93    // Doing this enables us to create a bare minimum Expo project.
94
95    // 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.
96    return resolveFrom(projectRoot, 'expo/AppEntry');
97  } catch {
98    throw new Error(
99      `The project entry file could not be resolved. Please define it in the \`main\` field of the \`package.json\`, create an \`index.js\`, or install the \`expo\` package.`
100    );
101  }
102}
103
104// Resolve from but with the ability to resolve like a bundler
105export function resolveFromSilentWithExtensions(
106  fromDirectory: string,
107  moduleId: string,
108  extensions: string[]
109): string | null {
110  for (const extension of extensions) {
111    const modulePath = resolveFrom.silent(fromDirectory, `${moduleId}.${extension}`);
112    if (modulePath && modulePath.endsWith(extension)) {
113      return modulePath;
114    }
115  }
116  return resolveFrom.silent(fromDirectory, moduleId) || null;
117}
118
119// Statically attempt to resolve a module but with the ability to resolve like a bundler.
120// This won't use node module resolution.
121export function getFileWithExtensions(
122  fromDirectory: string,
123  moduleId: string,
124  extensions: string[]
125): string | null {
126  const modulePath = path.join(fromDirectory, moduleId);
127  if (fs.existsSync(modulePath)) {
128    return modulePath;
129  }
130  for (const extension of extensions) {
131    const modulePath = path.join(fromDirectory, `${moduleId}.${extension}`);
132    if (fs.existsSync(modulePath)) {
133      return modulePath;
134    }
135  }
136  return null;
137}
138