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: platforms, 72 }); 73 74 const hasPlatformSpecificGitIgnores = hasAllPlatformSpecificGitIgnores( 75 templateDirectory, 76 platforms 77 ); 78 debug(`All platforms have an internal gitignore: ${hasPlatformSpecificGitIgnores}`); 79 80 // TODO: Remove gitignore modifications -- maybe move to `npx expo-doctor` 81 const gitignore = hasPlatformSpecificGitIgnores 82 ? null 83 : mergeGitIgnorePaths( 84 path.join(projectRoot, '.gitignore'), 85 path.join(templateDirectory, '.gitignore') 86 ); 87 88 return { ...copyResults, gitignore }; 89} 90 91async function copyPathsFromTemplateAsync( 92 /** File path to the project. */ 93 projectRoot: string, 94 { 95 templateDirectory, 96 copyFilePaths, 97 }: { 98 /** File path to the template project. */ 99 templateDirectory: string; 100 /** List of relative paths to copy from the template to the project. */ 101 copyFilePaths: string[]; 102 } 103): Promise<Pick<CopyFilesResults, 'copiedPaths' | 'skippedPaths'>> { 104 const copiedPaths = []; 105 const skippedPaths = []; 106 for (const copyFilePath of copyFilePaths) { 107 const projectPath = path.join(projectRoot, copyFilePath); 108 if (!(await directoryExistsAsync(projectPath))) { 109 copiedPaths.push(copyFilePath); 110 copySync(path.join(templateDirectory, copyFilePath), projectPath); 111 } else { 112 skippedPaths.push(copyFilePath); 113 } 114 } 115 debug(`Copied files:`, copiedPaths); 116 debug(`Skipped files:`, copiedPaths); 117 return { copiedPaths, skippedPaths }; 118} 119