1#!/usr/bin/env node 2import chalk from 'chalk'; 3import fs from 'fs'; 4import path from 'path'; 5 6import { 7 downloadAndExtractExampleAsync, 8 ensureExampleExists, 9 promptExamplesAsync, 10} from './Examples'; 11import * as Template from './Template'; 12import { promptTemplateAsync } from './legacyTemplates'; 13import { Log } from './log'; 14import { 15 installDependenciesAsync, 16 PackageManagerName, 17 resolvePackageManager, 18} from './resolvePackageManager'; 19import { assertFolderEmpty, assertValidName, resolveProjectRootAsync } from './resolveProjectRoot'; 20import { 21 AnalyticsEventPhases, 22 AnalyticsEventTypes, 23 identify, 24 initializeAnalyticsIdentityAsync, 25 track, 26} from './telemetry'; 27import { initGitRepoAsync } from './utils/git'; 28import { withSectionLog } from './utils/log'; 29 30export type Options = { 31 install: boolean; 32 template?: string | true; 33 example?: string | true; 34 yes: boolean; 35}; 36 37const debug = require('debug')('expo:init:create') as typeof console.log; 38 39async function resolveProjectRootArgAsync( 40 inputPath: string, 41 { yes }: Pick<Options, 'yes'> 42): Promise<string> { 43 if (!inputPath && yes) { 44 const projectRoot = path.resolve(process.cwd()); 45 const folderName = path.basename(projectRoot); 46 assertValidName(folderName); 47 assertFolderEmpty(projectRoot, folderName); 48 return projectRoot; 49 } else { 50 return await resolveProjectRootAsync(inputPath); 51 } 52} 53 54async function setupDependenciesAsync(projectRoot: string, props: Pick<Options, 'install'>) { 55 // Install dependencies 56 const shouldInstall = props.install; 57 const packageManager = resolvePackageManager(); 58 let podsInstalled: boolean = false; 59 const needsPodsInstalled = await fs.existsSync(path.join(projectRoot, 'ios')); 60 if (shouldInstall) { 61 await installNodeDependenciesAsync(projectRoot, packageManager); 62 if (needsPodsInstalled) { 63 podsInstalled = await installCocoaPodsAsync(projectRoot); 64 } 65 } 66 const cdPath = getChangeDirectoryPath(projectRoot); 67 console.log(); 68 Template.logProjectReady({ cdPath, packageManager }); 69 if (!shouldInstall) { 70 logNodeInstallWarning(cdPath, packageManager, needsPodsInstalled && !podsInstalled); 71 } 72} 73 74export async function createAsync(inputPath: string, options: Options): Promise<void> { 75 if (options.example && options.template) { 76 throw new Error('Cannot use both --example and --template'); 77 } 78 79 if (options.example) { 80 return await createExampleAsync(inputPath, options); 81 } 82 83 return await createTemplateAsync(inputPath, options); 84} 85 86async function createTemplateAsync(inputPath: string, props: Options): Promise<void> { 87 let resolvedTemplate: string | null = null; 88 // @ts-ignore: This guards against someone passing --template without a name after it. 89 if (props.template === true) { 90 resolvedTemplate = await promptTemplateAsync(); 91 } else { 92 resolvedTemplate = props.template ?? null; 93 } 94 95 const projectRoot = await resolveProjectRootArgAsync(inputPath, props); 96 await fs.promises.mkdir(projectRoot, { recursive: true }); 97 98 // Setup telemetry attempt after a reasonable point. 99 // Telemetry is used to ensure safe feature deprecation since the command is unversioned. 100 // All telemetry can be disabled across Expo tooling by using the env var $EXPO_NO_TELEMETRY. 101 await initializeAnalyticsIdentityAsync(); 102 identify(); 103 track({ 104 event: AnalyticsEventTypes.CREATE_EXPO_APP, 105 properties: { phase: AnalyticsEventPhases.ATTEMPT, template: resolvedTemplate }, 106 }); 107 108 await withSectionLog( 109 () => Template.extractAndPrepareTemplateAppAsync(projectRoot, { npmPackage: resolvedTemplate }), 110 { 111 pending: chalk.bold('Locating project files.'), 112 success: 'Downloaded and extracted project files.', 113 error: (error) => 114 `Something went wrong in downloading and extracting the project files: ${error.message}`, 115 } 116 ); 117 118 await setupDependenciesAsync(projectRoot, props); 119 120 // for now, we will just init a git repo if they have git installed and the 121 // project is not inside an existing git tree, and do it silently. we should 122 // at some point check if git is installed and actually bail out if not, because 123 // npm install will fail with a confusing error if so. 124 try { 125 // check if git is installed 126 // check if inside git repo 127 await initGitRepoAsync(projectRoot); 128 } catch (error) { 129 debug(`Error initializing git: %O`, error); 130 // todo: check if git is installed, bail out 131 } 132} 133 134async function createExampleAsync(inputPath: string, props: Options): Promise<void> { 135 let resolvedExample = ''; 136 if (props.example === true) { 137 resolvedExample = await promptExamplesAsync(); 138 } else if (props.example) { 139 resolvedExample = props.example; 140 } 141 142 await ensureExampleExists(resolvedExample); 143 144 const projectRoot = await resolveProjectRootArgAsync(inputPath, props); 145 await fs.promises.mkdir(projectRoot, { recursive: true }); 146 147 // Setup telemetry attempt after a reasonable point. 148 // Telemetry is used to ensure safe feature deprecation since the command is unversioned. 149 // All telemetry can be disabled across Expo tooling by using the env var $EXPO_NO_TELEMETRY. 150 await initializeAnalyticsIdentityAsync(); 151 identify(); 152 track({ 153 event: AnalyticsEventTypes.CREATE_EXPO_APP, 154 properties: { phase: AnalyticsEventPhases.ATTEMPT, example: resolvedExample }, 155 }); 156 157 await withSectionLog(() => downloadAndExtractExampleAsync(projectRoot, resolvedExample), { 158 pending: chalk.bold('Locating example files...'), 159 success: 'Downloaded and extracted example files.', 160 error: (error) => 161 `Something went wrong in downloading and extracting the example files: ${error.message}`, 162 }); 163 164 await setupDependenciesAsync(projectRoot, props); 165 166 // for now, we will just init a git repo if they have git installed and the 167 // project is not inside an existing git tree, and do it silently. we should 168 // at some point check if git is installed and actually bail out if not, because 169 // npm install will fail with a confusing error if so. 170 try { 171 // check if git is installed 172 // check if inside git repo 173 await initGitRepoAsync(projectRoot); 174 } catch (error) { 175 debug(`Error initializing git: %O`, error); 176 // todo: check if git is installed, bail out 177 } 178} 179 180function getChangeDirectoryPath(projectRoot: string): string { 181 const cdPath = path.relative(process.cwd(), projectRoot); 182 if (cdPath.length <= projectRoot.length) { 183 return cdPath; 184 } 185 return projectRoot; 186} 187 188async function installNodeDependenciesAsync( 189 projectRoot: string, 190 packageManager: PackageManagerName 191): Promise<void> { 192 try { 193 await installDependenciesAsync(projectRoot, packageManager, { silent: false }); 194 } catch (error: any) { 195 debug(`Error installing node modules: %O`, error); 196 Log.error( 197 `Something went wrong installing JavaScript dependencies. Check your ${packageManager} logs. Continuing to create the app.` 198 ); 199 Log.exception(error); 200 } 201} 202 203async function installCocoaPodsAsync(projectRoot: string): Promise<boolean> { 204 let podsInstalled = false; 205 try { 206 podsInstalled = await Template.installPodsAsync(projectRoot); 207 } catch (error) { 208 debug(`Error installing CocoaPods: %O`, error); 209 } 210 211 return podsInstalled; 212} 213 214export function logNodeInstallWarning( 215 cdPath: string, 216 packageManager: PackageManagerName, 217 needsPods: boolean 218): void { 219 console.log(`\n⚠️ Before running your app, make sure you have modules installed:\n`); 220 console.log(` cd ${cdPath || '.'}${path.sep}`); 221 console.log(` ${packageManager} install`); 222 if (needsPods && process.platform === 'darwin') { 223 console.log(` npx pod-install`); 224 } 225 console.log(); 226} 227