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