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