1import chalk from 'chalk';
2import fs from 'fs';
3import path from 'path';
4import resolveFrom from 'resolve-from';
5
6import prompt, { ExpoChoice } from '../utils/prompts';
7
8const debug = require('debug')('expo:customize:templates');
9
10export type DestinationResolutionProps = {
11  /** Web 'public' folder path (defaults to `/web`). This technically can be changed but shouldn't be. */
12  webStaticPath: string;
13};
14
15function importFromExpoWebpackConfig(projectRoot: string, folder: string, moduleId: string) {
16  try {
17    const filePath = resolveFrom(projectRoot, `@expo/webpack-config/${folder}/${moduleId}`);
18    debug(`Using @expo/webpack-config template for "${moduleId}": ${filePath}`);
19    return filePath;
20  } catch {
21    debug(`@expo/webpack-config template for "${moduleId}" not found, falling back on @expo/cli`);
22  }
23  return importFromVendor(projectRoot, moduleId);
24}
25
26function importFromVendor(projectRoot: string, moduleId: string) {
27  try {
28    const filePath = resolveFrom(projectRoot, '@expo/cli/static/template/' + moduleId);
29    debug(`Using @expo/cli template for "${moduleId}": ${filePath}`);
30    return filePath;
31  } catch {
32    // For dev mode, testing and other cases where @expo/cli is not installed
33    const filePath = require.resolve(`@expo/cli/static/template/${moduleId}`);
34    debug(
35      `Local @expo/cli template for "${moduleId}" not found, falling back on template relative to @expo/cli: ${filePath}`
36    );
37
38    return filePath;
39  }
40}
41
42export const TEMPLATES: {
43  /** Unique ID for easily indexing. */
44  id: string;
45  /** Template file path to copy into the project. */
46  file: (projectRoot: string) => string;
47  /** Output location for the file in the user project. */
48  destination: (props: DestinationResolutionProps) => string;
49  /** List of dependencies to install in the project. These are used inside of the template file. */
50  dependencies: string[];
51}[] = [
52  {
53    id: 'babel.config.js',
54    file: (projectRoot) => importFromVendor(projectRoot, 'babel.config.js'),
55    destination: () => 'babel.config.js',
56    dependencies: [
57      // Even though this is installed in `expo`, we should add it for now.
58      'babel-preset-expo',
59    ],
60  },
61  {
62    id: 'webpack.config.js',
63    file: (projectRoot) =>
64      importFromExpoWebpackConfig(projectRoot, 'template', 'webpack.config.js'),
65    destination: () => 'webpack.config.js',
66    dependencies: ['@expo/webpack-config'],
67  },
68  {
69    id: 'metro.config.js',
70    dependencies: ['@expo/metro-config'],
71    destination: () => 'metro.config.js',
72    file: (projectRoot) => importFromVendor(projectRoot, 'metro.config.js'),
73  },
74  {
75    id: 'serve.json',
76    file: (projectRoot) => importFromExpoWebpackConfig(projectRoot, 'web-default', 'serve.json'),
77    // web/serve.json
78    destination: ({ webStaticPath }) => webStaticPath + '/serve.json',
79    dependencies: [],
80  },
81  {
82    id: 'index.html',
83    file: (projectRoot) => importFromExpoWebpackConfig(projectRoot, 'web-default', 'index.html'),
84    // web/index.html
85    destination: ({ webStaticPath }) => webStaticPath + '/index.html',
86    dependencies: [],
87  },
88];
89
90/** Generate the prompt choices. */
91function createChoices(
92  projectRoot: string,
93  props: DestinationResolutionProps
94): ExpoChoice<number>[] {
95  return TEMPLATES.map((template, index) => {
96    const destination = template.destination(props);
97    const localProjectFile = path.resolve(projectRoot, destination);
98    const exists = fs.existsSync(localProjectFile);
99
100    return {
101      title: destination,
102      value: index,
103      description: exists ? chalk.red('This will overwrite the existing file') : undefined,
104    };
105  });
106}
107
108/** Prompt to select templates to add. */
109export async function selectTemplatesAsync(projectRoot: string, props: DestinationResolutionProps) {
110  const options = createChoices(projectRoot, props);
111
112  const { answer } = await prompt({
113    type: 'multiselect',
114    name: 'answer',
115    message: 'Which files would you like to generate?',
116    hint: '- Space to select. Return to submit',
117    warn: 'File already exists.',
118    limit: options.length,
119    instructions: '',
120    choices: options,
121  });
122  return answer;
123}
124