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