1import spawnAsync from '@expo/spawn-async';
2import fs from 'fs-extra';
3import path from 'path';
4
5import { installDependencies } from './packageManager';
6import { PackageManagerName } from './resolvePackageManager';
7import { SubstitutionData } from './types';
8import { newStep } from './utils';
9
10// These dependencies will be removed from the example app (`expo init` adds them)
11const DEPENDENCIES_TO_REMOVE = ['expo-status-bar', 'expo-splash-screen'];
12
13/**
14 * Initializes a new Expo project as an example app.
15 */
16export async function createExampleApp(
17  data: SubstitutionData,
18  targetDir: string,
19  packageManager: PackageManagerName
20): Promise<void> {
21  // Package name for the example app
22  const exampleProjectSlug = `${data.project.slug}-example`;
23
24  // `expo init` creates a new folder with the same name as the project slug
25  const appTmpPath = path.join(targetDir, exampleProjectSlug);
26
27  // Path to the target example dir
28  const appTargetPath = path.join(targetDir, 'example');
29
30  if (!(await fs.pathExists(appTargetPath))) {
31    // The template doesn't include the example app, so just skip this phase
32    return;
33  }
34
35  await newStep('Initializing the example app', async (step) => {
36    await spawnAsync(
37      packageManager,
38      ['create', 'expo-app', '--', exampleProjectSlug, '--template', 'blank-typescript', '--yes'],
39      {
40        cwd: targetDir,
41        stdio: 'ignore',
42      }
43    );
44    step.succeed('Initialized the example app');
45  });
46
47  await newStep('Configuring the example app', async (step) => {
48    // "example" folder already exists and contains template files,
49    // that should replace these created by `expo init`.
50    await moveFiles(appTargetPath, appTmpPath);
51
52    // Cleanup the "example" dir
53    await fs.rmdir(appTargetPath);
54
55    // Clean up the ".git" from example app
56    // note, this directory has contents, rmdir will throw
57    await fs.remove(path.join(appTmpPath, '.git'));
58
59    // Move the temporary example app to "example" dir
60    await fs.rename(appTmpPath, appTargetPath);
61
62    await addMissingAppConfigFields(appTargetPath, data);
63
64    step.succeed('Configured the example app');
65  });
66
67  await prebuildExampleApp(appTargetPath);
68
69  await modifyPackageJson(appTargetPath);
70
71  await newStep('Installing dependencies in the example app', async (step) => {
72    await installDependencies(packageManager, appTargetPath);
73    await podInstall(appTargetPath);
74    step.succeed('Installed dependencies in the example app');
75  });
76}
77
78/**
79 * Copies files from one directory to another.
80 */
81async function moveFiles(fromPath: string, toPath: string): Promise<void> {
82  for (const file of await fs.readdir(fromPath)) {
83    await fs.move(path.join(fromPath, file), path.join(toPath, file), {
84      overwrite: true,
85    });
86  }
87}
88
89/**
90 * Adds missing configuration that are required to run `expo prebuild`.
91 */
92async function addMissingAppConfigFields(appPath: string, data: SubstitutionData): Promise<void> {
93  const appConfigPath = path.join(appPath, 'app.json');
94  const appConfig = await fs.readJson(appConfigPath);
95  const appId = `${data.project.package}.example`;
96
97  // Android package name needs to be added to app.json
98  if (!appConfig.expo.android) {
99    appConfig.expo.android = {};
100  }
101  appConfig.expo.android.package = appId;
102
103  // Specify iOS bundle identifier
104  if (!appConfig.expo.ios) {
105    appConfig.expo.ios = {};
106  }
107  appConfig.expo.ios.bundleIdentifier = appId;
108
109  await fs.writeJson(appConfigPath, appConfig, {
110    spaces: 2,
111  });
112}
113
114/**
115 * Applies necessary changes to **package.json** of the example app.
116 * It means setting the autolinking config and removing unnecessary dependencies.
117 */
118async function modifyPackageJson(appPath: string): Promise<void> {
119  const packageJsonPath = path.join(appPath, 'package.json');
120  const packageJson = await fs.readJson(packageJsonPath);
121
122  if (!packageJson.expo) {
123    packageJson.expo = {};
124  }
125
126  // Set the native modules dir to the root folder,
127  // so that the autolinking can detect and link the module.
128  packageJson.expo.autolinking = {
129    nativeModulesDir: '..',
130  };
131
132  // Remove unnecessary dependencies
133  for (const dependencyToRemove of DEPENDENCIES_TO_REMOVE) {
134    delete packageJson.dependencies[dependencyToRemove];
135  }
136
137  await fs.writeJson(packageJsonPath, packageJson, {
138    spaces: 2,
139  });
140}
141
142/**
143 * Runs `expo prebuild` in the example app.
144 */
145async function prebuildExampleApp(exampleAppPath: string): Promise<void> {
146  await newStep('Prebuilding the example app', async (step) => {
147    await spawnAsync('expo', ['prebuild', '--no-install'], {
148      cwd: exampleAppPath,
149      stdio: ['ignore', 'ignore', 'pipe'],
150    });
151    step.succeed('Prebuilt the example app');
152  });
153}
154
155/**
156 * Runs `pod install` in the iOS project at the given path.
157 */
158async function podInstall(appPath: string): Promise<void> {
159  await spawnAsync('pod', ['install'], {
160    cwd: path.join(appPath, 'ios'),
161    stdio: ['ignore', 'ignore', 'pipe'],
162  });
163}
164