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      'expo',
38      ['init', exampleProjectSlug, '--template', 'expo-template-blank-typescript'],
39      {
40        cwd: targetDir,
41        stdio: ['ignore', 'ignore', 'inherit'],
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    // Move the temporary example app to "example" dir
56    await fs.rename(appTmpPath, appTargetPath);
57
58    await addMissingAppConfigFields(appTargetPath, data);
59
60    step.succeed('Configured the example app');
61  });
62
63  await prebuildExampleApp(appTargetPath);
64
65  await modifyPackageJson(appTargetPath);
66
67  await newStep('Installing dependencies in the example app', async (step) => {
68    await installDependencies(packageManager, appTargetPath);
69    await podInstall(appTargetPath);
70    step.succeed('Installed dependencies in the example app');
71  });
72}
73
74/**
75 * Copies files from one directory to another.
76 */
77async function moveFiles(fromPath: string, toPath: string): Promise<void> {
78  for (const file of await fs.readdir(fromPath)) {
79    await fs.move(path.join(fromPath, file), path.join(toPath, file), {
80      overwrite: true,
81    });
82  }
83}
84
85/**
86 * Adds missing configuration that are required to run `expo prebuild`.
87 */
88async function addMissingAppConfigFields(appPath: string, data: SubstitutionData): Promise<void> {
89  const appConfigPath = path.join(appPath, 'app.json');
90  const appConfig = await fs.readJson(appConfigPath);
91  const appId = `${data.project.package}.example`;
92
93  // Android package name needs to be added to app.json
94  if (!appConfig.expo.android) {
95    appConfig.expo.android = {};
96  }
97  appConfig.expo.android.package = appId;
98
99  // Specify iOS bundle identifier
100  if (!appConfig.expo.ios) {
101    appConfig.expo.ios = {};
102  }
103  appConfig.expo.ios.bundleIdentifier = appId;
104
105  await fs.writeJson(appConfigPath, appConfig, {
106    spaces: 2,
107  });
108}
109
110/**
111 * Applies necessary changes to **package.json** of the example app.
112 * It means setting the autolinking config and removing unnecessary dependencies.
113 */
114async function modifyPackageJson(appPath: string): Promise<void> {
115  const packageJsonPath = path.join(appPath, 'package.json');
116  const packageJson = await fs.readJson(packageJsonPath);
117
118  if (!packageJson.expo) {
119    packageJson.expo = {};
120  }
121
122  // Set the native modules dir to the root folder,
123  // so that the autolinking can detect and link the module.
124  packageJson.expo.autolinking = {
125    nativeModulesDir: '..',
126  };
127
128  // Remove unnecessary dependencies
129  for (const dependencyToRemove of DEPENDENCIES_TO_REMOVE) {
130    delete packageJson.dependencies[dependencyToRemove];
131  }
132
133  await fs.writeJson(packageJsonPath, packageJson, {
134    spaces: 2,
135  });
136}
137
138/**
139 * Runs `expo prebuild` in the example app.
140 */
141async function prebuildExampleApp(exampleAppPath: string): Promise<void> {
142  await newStep('Prebuilding the example app', async (step) => {
143    await spawnAsync('expo', ['prebuild', '--no-install'], {
144      cwd: exampleAppPath,
145      stdio: ['ignore', 'ignore', 'pipe'],
146    });
147    step.succeed('Prebuilt the example app');
148  });
149}
150
151/**
152 * Runs `pod install` in the iOS project at the given path.
153 */
154async function podInstall(appPath: string): Promise<void> {
155  await spawnAsync('pod', ['install'], {
156    cwd: path.join(appPath, 'ios'),
157    stdio: ['ignore', 'ignore', 'pipe'],
158  });
159}
160