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  /** Template file path to copy into the project. */
44  file: (projectRoot: string) => string;
45  /** Output location for the file in the user project. */
46  destination: (props: DestinationResolutionProps) => string;
47  /** List of dependencies to install in the project. These are used inside of the template file. */
48  dependencies: string[];
49}[] = [
50  {
51    file: (projectRoot) => importFromVendor(projectRoot, 'babel.config.js'),
52    destination: () => 'babel.config.js',
53    dependencies: [
54      // Even though this is installed in `expo`, we should add it for now.
55      'babel-preset-expo',
56    ],
57  },
58  {
59    file: (projectRoot) =>
60      importFromExpoWebpackConfig(projectRoot, 'template', 'webpack.config.js'),
61    destination: () => 'webpack.config.js',
62    dependencies: ['@expo/webpack-config'],
63  },
64  {
65    dependencies: ['@expo/metro-config'],
66    destination: () => 'metro.config.js',
67    file: (projectRoot) => importFromVendor(projectRoot, 'metro.config.js'),
68  },
69  {
70    file: (projectRoot) => importFromExpoWebpackConfig(projectRoot, 'web-default', 'serve.json'),
71    // web/serve.json
72    destination: ({ webStaticPath }) => webStaticPath + '/serve.json',
73    dependencies: [],
74  },
75  {
76    file: (projectRoot) => importFromExpoWebpackConfig(projectRoot, 'web-default', 'index.html'),
77    // web/index.html
78    destination: ({ webStaticPath }) => webStaticPath + '/index.html',
79    dependencies: [],
80  },
81];
82
83/** Generate the prompt choices. */
84function createChoices(
85  projectRoot: string,
86  props: DestinationResolutionProps
87): ExpoChoice<number>[] {
88  return TEMPLATES.map((template, index) => {
89    const destination = template.destination(props);
90    const localProjectFile = path.resolve(projectRoot, destination);
91    const exists = fs.existsSync(localProjectFile);
92
93    return {
94      title: destination,
95      value: index,
96      description: exists ? chalk.red('This will overwrite the existing file') : undefined,
97    };
98  });
99}
100
101/** Prompt to select templates to add. */
102export async function selectTemplatesAsync(projectRoot: string, props: DestinationResolutionProps) {
103  const options = createChoices(projectRoot, props);
104
105  const { answer } = await prompt({
106    type: 'multiselect',
107    name: 'answer',
108    message: 'Which files would you like to generate?',
109    hint: '- Space to select. Return to submit',
110    warn: 'File already exists.',
111    limit: options.length,
112    instructions: '',
113    choices: options,
114  });
115  return answer;
116}
117