import { ExpoConfig , getConfig } from '@expo/config' ; import JsonFile from '@expo/json-file' ; import fs from 'fs-extra' ; import { sync as globSync } from 'glob' ; import path from 'path' ; import { AssetBundle , EmbeddedAssets , ExponentTools , IosPlist , IosWorkspace , LoggerDetach as logger , StandaloneBuildFlags , StandaloneContext , UrlUtils , } from '../internal' ; const SERVICE_CONTEXT_PROJECT_NAME = 'exponent-view-template' ; async function ensureBuildConstantsExistsIOSAsync ( configFilePath ) { const doesBuildConstantsExist = fs . existsSync ( path . join ( configFilePath , 'EXBuildConstants.plist' ) ) ; if ( ! doesBuildConstantsExist ) { await IosPlist . createBlankAsync ( configFilePath , 'EXBuildConstants' ) ; logger . info ( 'Created `EXBuildConstants.plist` because it did not exist yet' ) ; } } async function _getIosExpoKitVersionThrowErrorAsync ( iosProjectDirectory ) { let expoKitVersion = '' ; const podfileLockPath = path . join ( iosProjectDirectory , 'Podfile.lock' ) ; try { const podfileLock = await fs . readFile ( podfileLockPath , 'utf8' ) ; const expoKitVersionRegex = /ExpoKit\/Core\W?\(([0-9.]+)\)/gi ; const match = expoKitVersionRegex . exec ( podfileLock ) ; if ( ! match ) { throw new Error ( 'ExpoKit/Core not found' ) ; } expoKitVersion = match [ 1 ] ; } catch ( e ) { throw new Error ( `Unable to read ExpoKit version from Podfile.lock. Make sure your project depends on ExpoKit. (${ e })` ) ; } return expoKitVersion ; } async function readNullableConfigJsonAsync ( projectDir ) { try { return getConfig ( projectDir ) ; } catch { return null ; } } async function prepareDetachedBuildIosAsync ( projectDir , args ) { const config = await readNullableConfigJsonAsync ( projectDir ) ; if ( config && config . exp . name !== SERVICE_CONTEXT_PROJECT_NAME ) { return prepareDetachedUserContextIosAsync ( projectDir , config . exp , args ) ; } else { return prepareDetachedServiceContextIosAsync ( projectDir , args ) ; } } async function prepareDetachedServiceContextIosAsync ( projectDir , args ) { const expoRootDir = path . join ( projectDir , '..' , '..' ) ; const workspaceSourcePath = path . join ( projectDir , 'ios' ) ; const buildFlags = StandaloneBuildFlags . createIos ( 'Release' , { workspaceSourcePath } ) ; const context = StandaloneContext . createServiceContext ( expoRootDir , null , null , null , 'none' , buildFlags , null , null ) ; const { iosProjectDirectory , supportingDirectory } = IosWorkspace . getPaths ( context ) ; const expoKitVersion = await _getIosExpoKitVersionThrowErrorAsync ( iosProjectDirectory ) ; const prodApiKeys = await _readDefaultApiKeysAsync ( path . join ( context . data . expoSourcePath , '__internal__' , 'keys.json' ) ) ; const { exp } = getConfig ( expoRootDir , { skipSDKVersionRequirement : true } ) ; await IosPlist . modifyAsync ( supportingDirectory , 'EXBuildConstants' , constantsConfig => { const contextType = constantsConfig . STANDALONE_CONTEXT_TYPE ; if ( contextType !== 'service' ) { throw new Error ( 'Unable to configure a project which has no app.json and also no STANDALONE_CONTEXT_TYPE.' ) ; } constantsConfig . EXPO_RUNTIME_VERSION = expoKitVersion ; constantsConfig . API_SERVER_ENDPOINT = process . env . ENVIRONMENT === 'staging' ? 'https://staging.exp.host/--/api/v2/' : 'https://exp.host/--/api/v2/' ; if ( prodApiKeys ) { constantsConfig . DEFAULT_API_KEYS = prodApiKeys ; } if ( exp && exp . sdkVersion ) { constantsConfig . TEMPORARY_SDK_VERSION = exp . sdkVersion ; } return constantsConfig ; } ) ; } async function _readDefaultApiKeysAsync ( jsonFilePath ) { if ( fs . existsSync ( jsonFilePath ) ) { const keys = { } ; const allKeys = await new JsonFile ( jsonFilePath ) . readAsync ( ) ; const validKeys = [ 'AMPLITUDE_KEY' , 'GOOGLE_MAPS_IOS_API_KEY' ] ; for ( const key in allKeys ) { if ( allKeys . hasOwnProperty ( key ) && validKeys . includes ( key ) ) { keys [ key ] = allKeys [ key ] ; } } return keys ; } return null ; } async function prepareDetachedUserContextIosAsync ( projectDir , exp , args ) { const context = StandaloneContext . createUserContext ( projectDir , exp ) ; const { iosProjectDirectory , supportingDirectory } = IosWorkspace . getPaths ( context ) ; logger . info ( `Preparing iOS build at ${ iosProjectDirectory }...` ) ; const podsDirectory = path . join ( iosProjectDirectory , 'Pods' ) ; if ( ! ExponentTools . isDirectory ( podsDirectory ) ) { throw new Error ( `Can't find directory ${ podsDirectory }, make sure you've run pod install.` ) ; } const rnPodDirectory = path . join ( podsDirectory , 'React' ) ; if ( ExponentTools . isDirectory ( rnPodDirectory ) ) { const rnFilesToDelete = globSync ( '**/*.@(js|json)' , { absolute : true , cwd : rnPodDirectory } ) ; if ( rnFilesToDelete ) { for ( let i = 0 ; i < rnFilesToDelete . length ; i ++ ) { await fs . unlink ( rnFilesToDelete [ i ] ) ; } } } if ( ! args . skipXcodeConfig ) { const expoKitVersion = await _getIosExpoKitVersionThrowErrorAsync ( iosProjectDirectory ) ; const devUrl = await UrlUtils . constructManifestUrlAsync ( projectDir ) ; const defaultApiKeys = await _readDefaultApiKeysAsync ( path . join ( podsDirectory , 'ExpoKit' , 'template-files' , 'keys.json' ) ) ; await ensureBuildConstantsExistsIOSAsync ( supportingDirectory ) ; await IosPlist . modifyAsync ( supportingDirectory , 'EXBuildConstants' , constantsConfig => { constantsConfig . developmentUrl = devUrl ; constantsConfig . EXPO_RUNTIME_VERSION = expoKitVersion ; if ( defaultApiKeys ) { constantsConfig . DEFAULT_API_KEYS = defaultApiKeys ; } if ( exp . sdkVersion ) { constantsConfig . TEMPORARY_SDK_VERSION = exp . sdkVersion ; } return constantsConfig ; } ) ; } } export async function prepareDetachedBuildAsync ( projectDir , args ) { if ( args . platform === 'ios' ) { await prepareDetachedBuildIosAsync ( projectDir , args ) ; } else { const expoBuildConstantsMatches = globSync ( 'android/**/DetachBuildConstants.java' , { absolute : true , cwd : projectDir } ) ; if ( expoBuildConstantsMatches && expoBuildConstantsMatches . length ) { const expoBuildConstants = expoBuildConstantsMatches [ 0 ] ; const devUrl = await UrlUtils . constructManifestUrlAsync ( projectDir ) ; await ExponentTools . regexFileAsync ( /DEVELOPMENT_URL = "[^"]*";/ , `DEVELOPMENT_URL = "${ devUrl }";` , expoBuildConstants ) ; } } } export async function bundleAssetsAsync ( projectDir , args ) { const options = await readNullableConfigJsonAsync ( projectDir ) ; if ( ! options || options . exp . name === SERVICE_CONTEXT_PROJECT_NAME ) { return ; } const { exp } = options ; const bundledManifestPath = EmbeddedAssets . getEmbeddedManifestPath ( args . platform , projectDir , exp as any ) ; if ( ! bundledManifestPath ) { logger . warn ( `Skipped assets bundling because the '${ args . platform }.publishManifestPath' key is not specified in the app manifest.` ) ; return ; } let manifest ; try { manifest = JSON . parse ( await fs . readFile ( bundledManifestPath , 'utf8' ) ) ; } catch ( ex ) { throw new Error ( `Error reading the manifest file. Make sure the path '${ bundledManifestPath }' is correct.\n\nError: ${ ex . message }` ) ; } if ( ! manifest || ! Object . keys ( manifest ) . length ) { throw new Error ( `The manifest at '${ bundledManifestPath }' was empty or invalid.` ) ; } await AssetBundle . bundleAsync ( null , manifest . bundledAssets , args . dest , getExportUrl ( manifest ) ) ; } /**
 * This function extracts the exported public URL that is set in the manifest
 * when the developer runs `expo export --public-url x`. We use this to ensure
 * that we fetch the resources from the appropriate place when doing builds
 * against self-hosted apps.
 */ function getExportUrl ( manifest ) { const { bundleUrl } = manifest ; if ( bundleUrl . includes ( AssetBundle . DEFAULT_CDN_HOST ) ) { return null ; } try { const bundleUrlParts = bundleUrl . split ( '/' ) ; return bundleUrlParts . slice ( 0 , bundleUrlParts . length - 2 ) . join ( '/' ) ; } catch ( e ) { throw Error ( `Expected bundleUrl to be of the format https://domain/bundles/bundle-hash-id, ${ bundleUrl } does not follow this format.` ) ; } }