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