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