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