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