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