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