xref: /expo/packages/@expo/config/src/paths/paths.ts (revision 8a424beb)
1082815dcSEvan Baconimport fs from 'fs';
2082815dcSEvan Baconimport path from 'path';
3082815dcSEvan Baconimport resolveFrom from 'resolve-from';
4082815dcSEvan Bacon
5*8a424bebSJames Ideimport { getBareExtensions } from './extensions';
6082815dcSEvan Baconimport { getConfig } from '../Config';
7082815dcSEvan Baconimport { ProjectConfig } from '../Config.types';
8082815dcSEvan Bacon
9082815dcSEvan Bacon// https://github.com/facebook/create-react-app/blob/9750738cce89a967cc71f28390daf5d4311b193c/packages/react-scripts/config/paths.js#L22
10082815dcSEvan Baconexport function ensureSlash(inputPath: string, needsSlash: boolean): string {
11082815dcSEvan Bacon  const hasSlash = inputPath.endsWith('/');
12082815dcSEvan Bacon  if (hasSlash && !needsSlash) {
13082815dcSEvan Bacon    return inputPath.substr(0, inputPath.length - 1);
14082815dcSEvan Bacon  } else if (!hasSlash && needsSlash) {
15082815dcSEvan Bacon    return `${inputPath}/`;
16082815dcSEvan Bacon  } else {
17082815dcSEvan Bacon    return inputPath;
18082815dcSEvan Bacon  }
19082815dcSEvan Bacon}
20082815dcSEvan Bacon
21082815dcSEvan Baconexport function getPossibleProjectRoot(): string {
22082815dcSEvan Bacon  return fs.realpathSync(process.cwd());
23082815dcSEvan Bacon}
24082815dcSEvan Bacon
25082815dcSEvan Baconconst nativePlatforms = ['ios', 'android'];
26082815dcSEvan Bacon
27082815dcSEvan Baconexport function resolveEntryPoint(
28082815dcSEvan Bacon  projectRoot: string,
29036e9444SEvan Bacon  { platform, projectConfig }: { platform: string; projectConfig?: Partial<ProjectConfig> }
30082815dcSEvan Bacon) {
31082815dcSEvan Bacon  const platforms = nativePlatforms.includes(platform) ? [platform, 'native'] : [platform];
32082815dcSEvan Bacon  return getEntryPoint(projectRoot, ['./index'], platforms, projectConfig);
33082815dcSEvan Bacon}
34082815dcSEvan Bacon
35082815dcSEvan Baconexport function getEntryPoint(
36082815dcSEvan Bacon  projectRoot: string,
37082815dcSEvan Bacon  entryFiles: string[],
38082815dcSEvan Bacon  platforms: string[],
39036e9444SEvan Bacon  projectConfig?: Partial<ProjectConfig>
40082815dcSEvan Bacon): string | null {
41082815dcSEvan Bacon  const extensions = getBareExtensions(platforms);
42082815dcSEvan Bacon  return getEntryPointWithExtensions(projectRoot, entryFiles, extensions, projectConfig);
43082815dcSEvan Bacon}
44082815dcSEvan Bacon
45082815dcSEvan Bacon// Used to resolve the main entry file for a project.
46082815dcSEvan Baconexport function getEntryPointWithExtensions(
47082815dcSEvan Bacon  projectRoot: string,
48082815dcSEvan Bacon  entryFiles: string[],
49082815dcSEvan Bacon  extensions: string[],
50036e9444SEvan Bacon  projectConfig?: Partial<ProjectConfig>
51082815dcSEvan Bacon): string {
52ca1df09aSEvan Bacon  if (!projectConfig) {
53ca1df09aSEvan Bacon    // drop all logging abilities
54ca1df09aSEvan Bacon    const original = process.stdout.write;
55ca1df09aSEvan Bacon    process.stdout.write = () => true;
56ca1df09aSEvan Bacon    try {
57ca1df09aSEvan Bacon      projectConfig = getConfig(projectRoot, { skipSDKVersionRequirement: true });
58ca1df09aSEvan Bacon    } finally {
59ca1df09aSEvan Bacon      process.stdout.write = original;
60082815dcSEvan Bacon    }
61082815dcSEvan Bacon  }
62ca1df09aSEvan Bacon
63fdde7bb6SEvan Bacon  const { pkg } = projectConfig;
64ca1df09aSEvan Bacon
65ca1df09aSEvan Bacon  if (pkg) {
66082815dcSEvan Bacon    // If the config doesn't define a custom entry then we want to look at the `package.json`s `main` field, and try again.
67082815dcSEvan Bacon    const { main } = pkg;
68082815dcSEvan Bacon    if (main && typeof main === 'string') {
69082815dcSEvan Bacon      // 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.
70082815dcSEvan Bacon      let entry = getFileWithExtensions(projectRoot, main, extensions);
71082815dcSEvan Bacon      if (!entry) {
72082815dcSEvan Bacon        // Allow for paths like: `{ "main": "expo/AppEntry" }`
73082815dcSEvan Bacon        entry = resolveFromSilentWithExtensions(projectRoot, main, extensions);
74082815dcSEvan Bacon        if (!entry)
75082815dcSEvan Bacon          throw new Error(
76082815dcSEvan Bacon            `Cannot resolve entry file: The \`main\` field defined in your \`package.json\` points to a non-existent path.`
77082815dcSEvan Bacon          );
78082815dcSEvan Bacon      }
79082815dcSEvan Bacon      return entry;
80082815dcSEvan Bacon    }
81082815dcSEvan Bacon  }
82082815dcSEvan Bacon
83082815dcSEvan Bacon  // Now we will start looking for a default entry point using the provided `entryFiles` argument.
84082815dcSEvan Bacon  // This will add support for create-react-app (src/index.js) and react-native-cli (index.js) which don't define a main.
85082815dcSEvan Bacon  for (const fileName of entryFiles) {
86082815dcSEvan Bacon    const entry = resolveFromSilentWithExtensions(projectRoot, fileName, extensions);
87082815dcSEvan Bacon    if (entry) return entry;
88082815dcSEvan Bacon  }
89082815dcSEvan Bacon
90082815dcSEvan Bacon  try {
91082815dcSEvan Bacon    // If none of the default files exist then we will attempt to use the main Expo entry point.
92082815dcSEvan Bacon    // This requires `expo` to be installed in the project to work as it will use `node_module/expo/AppEntry.js`
93082815dcSEvan Bacon    // Doing this enables us to create a bare minimum Expo project.
94082815dcSEvan Bacon
95082815dcSEvan Bacon    // 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.
96082815dcSEvan Bacon    return resolveFrom(projectRoot, 'expo/AppEntry');
97082815dcSEvan Bacon  } catch {
98082815dcSEvan Bacon    throw new Error(
99ca1df09aSEvan Bacon      `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.`
100082815dcSEvan Bacon    );
101082815dcSEvan Bacon  }
102082815dcSEvan Bacon}
103082815dcSEvan Bacon
104082815dcSEvan Bacon// Resolve from but with the ability to resolve like a bundler
105082815dcSEvan Baconexport function resolveFromSilentWithExtensions(
106082815dcSEvan Bacon  fromDirectory: string,
107082815dcSEvan Bacon  moduleId: string,
108082815dcSEvan Bacon  extensions: string[]
109082815dcSEvan Bacon): string | null {
110082815dcSEvan Bacon  for (const extension of extensions) {
111082815dcSEvan Bacon    const modulePath = resolveFrom.silent(fromDirectory, `${moduleId}.${extension}`);
112082815dcSEvan Bacon    if (modulePath && modulePath.endsWith(extension)) {
113082815dcSEvan Bacon      return modulePath;
114082815dcSEvan Bacon    }
115082815dcSEvan Bacon  }
116082815dcSEvan Bacon  return resolveFrom.silent(fromDirectory, moduleId) || null;
117082815dcSEvan Bacon}
118082815dcSEvan Bacon
119082815dcSEvan Bacon// Statically attempt to resolve a module but with the ability to resolve like a bundler.
120082815dcSEvan Bacon// This won't use node module resolution.
121082815dcSEvan Baconexport function getFileWithExtensions(
122082815dcSEvan Bacon  fromDirectory: string,
123082815dcSEvan Bacon  moduleId: string,
124082815dcSEvan Bacon  extensions: string[]
125082815dcSEvan Bacon): string | null {
126082815dcSEvan Bacon  const modulePath = path.join(fromDirectory, moduleId);
127082815dcSEvan Bacon  if (fs.existsSync(modulePath)) {
128082815dcSEvan Bacon    return modulePath;
129082815dcSEvan Bacon  }
130082815dcSEvan Bacon  for (const extension of extensions) {
131082815dcSEvan Bacon    const modulePath = path.join(fromDirectory, `${moduleId}.${extension}`);
132082815dcSEvan Bacon    if (fs.existsSync(modulePath)) {
133082815dcSEvan Bacon      return modulePath;
134082815dcSEvan Bacon    }
135082815dcSEvan Bacon  }
136082815dcSEvan Bacon  return null;
137082815dcSEvan Bacon}
138