xref: /expo/packages/@expo/config/src/paths/paths.ts (revision cc2b5a7f)
1import fs from 'fs';
2import path from 'path';
3import resolveFrom from 'resolve-from';
4
5import { getConfig } from '../Config';
6import { ProjectConfig } from '../Config.types';
7import { ConfigError } from '../Errors';
8import { getBareExtensions } from './extensions';
9
10// https://github.com/facebook/create-react-app/blob/9750738cce89a967cc71f28390daf5d4311b193c/packages/react-scripts/config/paths.js#L22
11export function ensureSlash(inputPath: string, needsSlash: boolean): string {
12  const hasSlash = inputPath.endsWith('/');
13  if (hasSlash && !needsSlash) {
14    return inputPath.substr(0, inputPath.length - 1);
15  } else if (!hasSlash && needsSlash) {
16    return `${inputPath}/`;
17  } else {
18    return inputPath;
19  }
20}
21
22export function getPossibleProjectRoot(): string {
23  return fs.realpathSync(process.cwd());
24}
25
26const nativePlatforms = ['ios', 'android'];
27
28export function resolveEntryPoint(
29  projectRoot: string,
30  { platform, projectConfig }: { platform: string; projectConfig?: Partial<ProjectConfig> }
31) {
32  const platforms = nativePlatforms.includes(platform) ? [platform, 'native'] : [platform];
33  return getEntryPoint(projectRoot, ['./index'], platforms, projectConfig);
34}
35
36export function getEntryPoint(
37  projectRoot: string,
38  entryFiles: string[],
39  platforms: string[],
40  projectConfig?: Partial<ProjectConfig>
41): string | null {
42  const extensions = getBareExtensions(platforms);
43  return getEntryPointWithExtensions(projectRoot, entryFiles, extensions, projectConfig);
44}
45
46// Used to resolve the main entry file for a project.
47export function getEntryPointWithExtensions(
48  projectRoot: string,
49  entryFiles: string[],
50  extensions: string[],
51  projectConfig?: Partial<ProjectConfig>
52): string {
53  if (!projectConfig) {
54    // drop all logging abilities
55    const original = process.stdout.write;
56    process.stdout.write = () => true;
57    try {
58      projectConfig = getConfig(projectRoot, { skipSDKVersionRequirement: true });
59    } finally {
60      process.stdout.write = original;
61    }
62  }
63
64  const { exp, pkg } = projectConfig;
65
66  if (typeof exp?.entryPoint === 'string') {
67    // We want to stop reading the app.json for determining the entry file in SDK +49
68    throw new ConfigError(
69      'expo.entryPoint has been removed in favor of the main field in the package.json.',
70      'DEPRECATED'
71    );
72  }
73
74  if (pkg) {
75    // If the config doesn't define a custom entry then we want to look at the `package.json`s `main` field, and try again.
76    const { main } = pkg;
77    if (main && typeof main === 'string') {
78      // 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.
79      let entry = getFileWithExtensions(projectRoot, main, extensions);
80      if (!entry) {
81        // Allow for paths like: `{ "main": "expo/AppEntry" }`
82        entry = resolveFromSilentWithExtensions(projectRoot, main, extensions);
83        if (!entry)
84          throw new Error(
85            `Cannot resolve entry file: The \`main\` field defined in your \`package.json\` points to a non-existent path.`
86          );
87      }
88      return entry;
89    }
90  }
91
92  // Now we will start looking for a default entry point using the provided `entryFiles` argument.
93  // This will add support for create-react-app (src/index.js) and react-native-cli (index.js) which don't define a main.
94  for (const fileName of entryFiles) {
95    const entry = resolveFromSilentWithExtensions(projectRoot, fileName, extensions);
96    if (entry) return entry;
97  }
98
99  try {
100    // If none of the default files exist then we will attempt to use the main Expo entry point.
101    // This requires `expo` to be installed in the project to work as it will use `node_module/expo/AppEntry.js`
102    // Doing this enables us to create a bare minimum Expo project.
103
104    // 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.
105    return resolveFrom(projectRoot, 'expo/AppEntry');
106  } catch {
107    throw new Error(
108      `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.`
109    );
110  }
111}
112
113// Resolve from but with the ability to resolve like a bundler
114export function resolveFromSilentWithExtensions(
115  fromDirectory: string,
116  moduleId: string,
117  extensions: string[]
118): string | null {
119  for (const extension of extensions) {
120    const modulePath = resolveFrom.silent(fromDirectory, `${moduleId}.${extension}`);
121    if (modulePath && modulePath.endsWith(extension)) {
122      return modulePath;
123    }
124  }
125  return resolveFrom.silent(fromDirectory, moduleId) || null;
126}
127
128// Statically attempt to resolve a module but with the ability to resolve like a bundler.
129// This won't use node module resolution.
130export function getFileWithExtensions(
131  fromDirectory: string,
132  moduleId: string,
133  extensions: string[]
134): string | null {
135  const modulePath = path.join(fromDirectory, moduleId);
136  if (fs.existsSync(modulePath)) {
137    return modulePath;
138  }
139  for (const extension of extensions) {
140    const modulePath = path.join(fromDirectory, `${moduleId}.${extension}`);
141    if (fs.existsSync(modulePath)) {
142      return modulePath;
143    }
144  }
145  return null;
146}
147