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