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    // `tsconfig.json` is special cased and don't not follow the template
90    id: 'tsconfig.json',
91    dependencies: [],
92    destination: () => 'tsconfig.json',
93    file: () => '',
94  },
95];
96
97/** Generate the prompt choices. */
98function createChoices(
99  projectRoot: string,
100  props: DestinationResolutionProps
101): ExpoChoice<number>[] {
102  return TEMPLATES.map((template, index) => {
103    const destination = template.destination(props);
104    const localProjectFile = path.resolve(projectRoot, destination);
105    const exists = fs.existsSync(localProjectFile);
106
107    return {
108      title: destination,
109      value: index,
110      description: exists ? chalk.red('This will overwrite the existing file') : undefined,
111    };
112  });
113}
114
115/** Prompt to select templates to add. */
116export async function selectTemplatesAsync(projectRoot: string, props: DestinationResolutionProps) {
117  const options = createChoices(projectRoot, props);
118
119  const { answer } = await prompt({
120    type: 'multiselect',
121    name: 'answer',
122    message: 'Which files would you like to generate?',
123    hint: '- Space to select. Return to submit',
124    warn: 'File already exists.',
125    limit: options.length,
126    instructions: '',
127    choices: options,
128  });
129  return answer;
130}
131