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