1import { ModPlatform } from '@expo/config-plugins'; 2import { MergeResults } from '@expo/config-plugins/build/utils/generateCode'; 3import chalk from 'chalk'; 4import fs from 'fs'; 5import path from 'path'; 6 7import { copySync, directoryExistsAsync } from '../utils/dir'; 8import { mergeGitIgnorePaths } from '../utils/mergeGitIgnorePaths'; 9 10const debug = require('debug')('expo:prebuild:copyTemplateFiles') as typeof console.log; 11 12type CopyFilesResults = { 13 /** Merge results for the root `.gitignore` file */ 14 gitignore: MergeResults | null; 15 /** List of file paths that were copied from the template into the project. */ 16 copiedPaths: string[]; 17 /** List of file paths that were skipped due to a number of factors. */ 18 skippedPaths: string[]; 19}; 20 21/** 22 * Return true if the given platforms all have an internal `.gitignore` file. 23 * 24 * @param projectRoot 25 * @param platforms 26 */ 27function hasAllPlatformSpecificGitIgnores(projectRoot: string, platforms: ModPlatform[]): boolean { 28 return platforms.reduce<boolean>( 29 (p, platform) => p && fs.existsSync(path.join(projectRoot, platform, '.gitignore')), 30 true 31 ); 32} 33 34/** Create a custom log message based on the copy file results. */ 35export function createCopyFilesSuccessMessage( 36 platforms: ModPlatform[], 37 { skippedPaths, gitignore }: CopyFilesResults 38): string { 39 let message = `Created native project${platforms.length > 1 ? 's' : ''}`; 40 41 if (skippedPaths.length) { 42 message += chalk.dim( 43 ` | ${skippedPaths.map((path) => chalk.bold(`/${path}`)).join(', ')} already created` 44 ); 45 } 46 if (!gitignore) { 47 message += chalk.dim(` | gitignore skipped`); 48 } else if (!gitignore.didMerge) { 49 message += chalk.dim(` | gitignore already synced`); 50 } else if (gitignore.didMerge && gitignore.didClear) { 51 message += chalk.dim(` | synced gitignore`); 52 } 53 return message; 54} 55 56/** Copy template files into the project and possibly merge the `.gitignore` files. */ 57export async function copyTemplateFilesAsync( 58 projectRoot: string, 59 { 60 templateDirectory, 61 platforms, 62 }: { 63 /** File path to the template directory. */ 64 templateDirectory: string; 65 /** List of platforms to copy against. */ 66 platforms: ModPlatform[]; 67 } 68): Promise<CopyFilesResults> { 69 const copyResults = await copyPathsFromTemplateAsync(projectRoot, { 70 templateDirectory, 71 copyFilePaths: [ 72 // Just copy over the native folders from the template 73 // we copy the metro file in a different way. 74 ...platforms, 75 ], 76 }); 77 78 const hasPlatformSpecificGitIgnores = hasAllPlatformSpecificGitIgnores( 79 templateDirectory, 80 platforms 81 ); 82 debug(`All platforms have an internal gitignore: ${hasPlatformSpecificGitIgnores}`); 83 const gitignore = hasPlatformSpecificGitIgnores 84 ? null 85 : mergeGitIgnorePaths( 86 path.join(projectRoot, '.gitignore'), 87 path.join(templateDirectory, '.gitignore') 88 ); 89 90 return { ...copyResults, gitignore }; 91} 92 93async function copyPathsFromTemplateAsync( 94 /** File path to the project. */ 95 projectRoot: string, 96 { 97 templateDirectory, 98 copyFilePaths, 99 }: { 100 /** File path to the template project. */ 101 templateDirectory: string; 102 /** List of relative paths to copy from the template to the project. */ 103 copyFilePaths: string[]; 104 } 105): Promise<Pick<CopyFilesResults, 'copiedPaths' | 'skippedPaths'>> { 106 const copiedPaths = []; 107 const skippedPaths = []; 108 for (const copyFilePath of copyFilePaths) { 109 const projectPath = path.join(projectRoot, copyFilePath); 110 if (!(await directoryExistsAsync(projectPath))) { 111 copiedPaths.push(copyFilePath); 112 copySync(path.join(templateDirectory, copyFilePath), projectPath); 113 } else { 114 skippedPaths.push(copyFilePath); 115 } 116 } 117 debug(`Copied files:`, copiedPaths); 118 debug(`Skipped files:`, copiedPaths); 119 return { copiedPaths, skippedPaths }; 120} 121