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