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