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