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 `npx 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('npx', ['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