1import { PackageJSONConfig } from '@expo/config'; 2import { ModPlatform } from '@expo/config-plugins'; 3import { MergeResults } from '@expo/config-plugins/build/utils/generateCode'; 4import { getBareExtensions, getFileWithExtensions } from '@expo/config/paths'; 5import chalk from 'chalk'; 6import fs from 'fs'; 7import path from 'path'; 8 9import * as Log from '../log'; 10import { copySync, directoryExistsAsync } from '../utils/dir'; 11import { mergeGitIgnorePaths } from '../utils/mergeGitIgnorePaths'; 12import { isPkgMainExpoAppEntry } from './updatePackageJson'; 13 14type CopyFilesResults = { 15 /** Merge results for the root `.gitignore` file */ 16 gitignore: MergeResults | null; 17 /** List of file paths that were copied from the template into the project. */ 18 copiedPaths: string[]; 19 /** List of file paths that were skipped due to a number of factors. */ 20 skippedPaths: string[]; 21}; 22 23/** 24 * Return true if the given platforms all have an internal `.gitignore` file. 25 * 26 * @param projectRoot 27 * @param platforms 28 */ 29function hasAllPlatformSpecificGitIgnores(projectRoot: string, platforms: ModPlatform[]): boolean { 30 return platforms.reduce<boolean>( 31 (p, platform) => p && fs.existsSync(path.join(projectRoot, platform, '.gitignore')), 32 true 33 ); 34} 35 36/** Create a custom log message based on the copy file results. */ 37export function createCopyFilesSuccessMessage( 38 platforms: ModPlatform[], 39 { skippedPaths, gitignore }: CopyFilesResults 40): string { 41 let message = `Created native project${platforms.length > 1 ? 's' : ''}`; 42 43 if (skippedPaths.length) { 44 message += chalk.dim( 45 ` | ${skippedPaths.map((path) => chalk.bold(`/${path}`)).join(', ')} already created` 46 ); 47 } 48 if (!gitignore) { 49 message += chalk.dim(` | gitignore skipped`); 50 } else if (!gitignore.didMerge) { 51 message += chalk.dim(` | gitignore already synced`); 52 } else if (gitignore.didMerge && gitignore.didClear) { 53 message += chalk.dim(` | synced gitignore`); 54 } 55 return message; 56} 57 58/** Copy template files into the project and possibly merge the `.gitignore` files. */ 59export async function copyTemplateFilesAsync( 60 projectRoot: string, 61 { 62 pkg, 63 templateDirectory, 64 platforms, 65 }: { 66 /** Project `package.json` as JSON. */ 67 pkg: PackageJSONConfig; 68 /** File path to the template directory. */ 69 templateDirectory: string; 70 /** List of platforms to copy against. */ 71 platforms: ModPlatform[]; 72 } 73): Promise<CopyFilesResults> { 74 const copyFilePaths = getFilePathsToCopy(projectRoot, pkg, platforms); 75 76 const copyResults = await copyPathsFromTemplateAsync(projectRoot, { 77 templateDirectory, 78 copyFilePaths, 79 }); 80 81 const hasPlatformSpecificGitIgnores = hasAllPlatformSpecificGitIgnores( 82 templateDirectory, 83 platforms 84 ); 85 Log.debug(`All platforms have an internal gitignore: ${hasPlatformSpecificGitIgnores}`); 86 const gitignore = hasPlatformSpecificGitIgnores 87 ? null 88 : mergeGitIgnorePaths( 89 path.join(projectRoot, '.gitignore'), 90 path.join(templateDirectory, '.gitignore') 91 ); 92 93 return { ...copyResults, gitignore }; 94} 95 96async function copyPathsFromTemplateAsync( 97 /** File path to the project. */ 98 projectRoot: string, 99 { 100 templateDirectory, 101 copyFilePaths, 102 }: { 103 /** File path to the template project. */ 104 templateDirectory: string; 105 /** List of relative paths to copy from the template to the project. */ 106 copyFilePaths: string[]; 107 } 108): Promise<Pick<CopyFilesResults, 'copiedPaths' | 'skippedPaths'>> { 109 const copiedPaths = []; 110 const skippedPaths = []; 111 for (const copyFilePath of copyFilePaths) { 112 const projectPath = path.join(projectRoot, copyFilePath); 113 if (!(await directoryExistsAsync(projectPath))) { 114 copiedPaths.push(copyFilePath); 115 copySync(path.join(templateDirectory, copyFilePath), projectPath); 116 } else { 117 skippedPaths.push(copyFilePath); 118 } 119 } 120 return { copiedPaths, skippedPaths }; 121} 122 123/** Get a list of relative file paths to copy from the template folder. Example: `['ios', 'android', 'index.js']` */ 124function getFilePathsToCopy(projectRoot: string, pkg: PackageJSONConfig, platforms: ModPlatform[]) { 125 const targetPaths: string[] = [...platforms]; 126 127 const bareEntryFile = resolveBareEntryFile(projectRoot, pkg.main); 128 // Only create index.js if we cannot resolve the existing entry point (after replacing the expo entry). 129 if (!bareEntryFile) { 130 targetPaths.push('index.js'); 131 } 132 133 return targetPaths; 134} 135 136export function resolveBareEntryFile(projectRoot: string, main: any) { 137 // expo app entry is not needed for bare projects. 138 if (isPkgMainExpoAppEntry(main)) { 139 return null; 140 } 141 // Look at the `package.json`s `main` field for the main file. 142 const resolvedMainField = main ?? './index'; 143 // Get a list of possible extensions for the main file. 144 const extensions = getBareExtensions(['ios', 'android']); 145 // 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. 146 return getFileWithExtensions(projectRoot, resolvedMainField, extensions); 147} 148