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