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