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