1import spawnAsync from '@expo/spawn-async'; 2import fs from 'fs-extra'; 3import getenv from 'getenv'; 4import path from 'path'; 5 6import { installDependencies } from './packageManager'; 7import { PackageManagerName } from './resolvePackageManager'; 8import { SubstitutionData } from './types'; 9import { newStep } from './utils'; 10 11const debug = require('debug')('create-expo-module:createExampleApp') as typeof console.log; 12 13// These dependencies will be removed from the example app (`expo init` adds them) 14const DEPENDENCIES_TO_REMOVE = ['expo-status-bar', 'expo-splash-screen']; 15const EXPO_BETA = getenv.boolish('EXPO_BETA', false); 16 17/** 18 * Initializes a new Expo project as an example app. 19 */ 20export async function createExampleApp( 21 data: SubstitutionData, 22 targetDir: string, 23 packageManager: PackageManagerName 24): Promise<void> { 25 // Package name for the example app 26 const exampleProjectSlug = `${data.project.slug}-example`; 27 28 // `expo init` creates a new folder with the same name as the project slug 29 const appTmpPath = path.join(targetDir, exampleProjectSlug); 30 31 // Path to the target example dir 32 const appTargetPath = path.join(targetDir, 'example'); 33 34 if (!(await fs.pathExists(appTargetPath))) { 35 // The template doesn't include the example app, so just skip this phase 36 return; 37 } 38 39 await newStep('Initializing the example app', async (step) => { 40 const templateVersion = EXPO_BETA ? 'next' : 'latest'; 41 const template = `expo-template-blank-typescript@${templateVersion}`; 42 debug(`Using example template: ${template}`); 43 await spawnAsync( 44 packageManager, 45 ['create', 'expo-app', '--', exampleProjectSlug, '--template', template, '--yes'], 46 { 47 cwd: targetDir, 48 stdio: 'ignore', 49 } 50 ); 51 step.succeed('Initialized the example app'); 52 }); 53 54 await newStep('Configuring the example app', async (step) => { 55 // "example" folder already exists and contains template files, 56 // that should replace these created by `expo init`. 57 await moveFiles(appTargetPath, appTmpPath); 58 59 // Cleanup the "example" dir 60 await fs.rmdir(appTargetPath); 61 62 // Clean up the ".git" from example app 63 // note, this directory has contents, rmdir will throw 64 await fs.remove(path.join(appTmpPath, '.git')); 65 66 // Move the temporary example app to "example" dir 67 await fs.rename(appTmpPath, appTargetPath); 68 69 await addMissingAppConfigFields(appTargetPath, data); 70 71 step.succeed('Configured the example app'); 72 }); 73 74 await prebuildExampleApp(appTargetPath); 75 76 await modifyPackageJson(appTargetPath); 77 78 await newStep('Installing dependencies in the example app', async (step) => { 79 await installDependencies(packageManager, appTargetPath); 80 await podInstall(appTargetPath); 81 step.succeed('Installed dependencies in the example app'); 82 }); 83} 84 85/** 86 * Copies files from one directory to another. 87 */ 88async function moveFiles(fromPath: string, toPath: string): Promise<void> { 89 for (const file of await fs.readdir(fromPath)) { 90 await fs.move(path.join(fromPath, file), path.join(toPath, file), { 91 overwrite: true, 92 }); 93 } 94} 95 96/** 97 * Adds missing configuration that are required to run `expo prebuild`. 98 */ 99async function addMissingAppConfigFields(appPath: string, data: SubstitutionData): Promise<void> { 100 const appConfigPath = path.join(appPath, 'app.json'); 101 const appConfig = await fs.readJson(appConfigPath); 102 const appId = `${data.project.package}.example`; 103 104 // Android package name needs to be added to app.json 105 if (!appConfig.expo.android) { 106 appConfig.expo.android = {}; 107 } 108 appConfig.expo.android.package = appId; 109 110 // Specify iOS bundle identifier 111 if (!appConfig.expo.ios) { 112 appConfig.expo.ios = {}; 113 } 114 appConfig.expo.ios.bundleIdentifier = appId; 115 116 await fs.writeJson(appConfigPath, appConfig, { 117 spaces: 2, 118 }); 119} 120 121/** 122 * Applies necessary changes to **package.json** of the example app. 123 * It means setting the autolinking config and removing unnecessary dependencies. 124 */ 125async function modifyPackageJson(appPath: string): Promise<void> { 126 const packageJsonPath = path.join(appPath, 'package.json'); 127 const packageJson = await fs.readJson(packageJsonPath); 128 129 if (!packageJson.expo) { 130 packageJson.expo = {}; 131 } 132 133 // Set the native modules dir to the root folder, 134 // so that the autolinking can detect and link the module. 135 packageJson.expo.autolinking = { 136 nativeModulesDir: '..', 137 }; 138 139 // Remove unnecessary dependencies 140 for (const dependencyToRemove of DEPENDENCIES_TO_REMOVE) { 141 delete packageJson.dependencies[dependencyToRemove]; 142 } 143 144 await fs.writeJson(packageJsonPath, packageJson, { 145 spaces: 2, 146 }); 147} 148 149/** 150 * Runs `npx expo prebuild` in the example app. 151 */ 152async function prebuildExampleApp(exampleAppPath: string): Promise<void> { 153 await newStep('Prebuilding the example app', async (step) => { 154 await spawnAsync('npx', ['expo', 'prebuild', '--no-install'], { 155 cwd: exampleAppPath, 156 stdio: ['ignore', 'ignore', 'pipe'], 157 }); 158 step.succeed('Prebuilt the example app'); 159 }); 160} 161 162/** 163 * Runs `pod install` in the iOS project at the given path. 164 */ 165async function podInstall(appPath: string): Promise<void> { 166 await spawnAsync('pod', ['install'], { 167 cwd: path.join(appPath, 'ios'), 168 stdio: ['ignore', 'ignore', 'pipe'], 169 }); 170} 171