import { ExpoAppManifest , getConfig , PackageJSONConfig , ProjectTarget } from '@expo/config' ; import { AndroidConfig , IOSConfig , WarningAggregator } from '@expo/config-plugins' ; import plist from '@expo/plist' ; import fs from 'fs-extra' ; import path from 'path' ; import semver from 'semver' ; import { ExponentTools , IosPlist , IosWorkspace , Logger as logger , StandaloneContext , writeArtifactSafelyAsync , } from './internal' ; const PLACEHOLDER_URL = 'YOUR-APP-URL-HERE' ; const FYI_URL = 'https://expo.fyi/expo-updates-config' ; export type EmbeddedAssetsConfiguration = { projectRoot : string ; pkg : PackageJSONConfig ; exp : ExpoAppManifest ; releaseChannel ? : string ; iosManifestUrl : string ; iosManifest : any ; iosBundle : string | Uint8Array ; androidManifestUrl : string ; androidManifest : any ; androidBundle : string | Uint8Array ; target : ProjectTarget ; } ; export async function configureAsync ( config ) { await _maybeWriteArtifactsToDiskAsync ( config ) ; await _maybeConfigureExpoKitEmbeddedAssetsAsync ( config ) ; await _maybeRunModifiedExpoUpdatesPluginAsync ( config ) ; } export function getEmbeddedManifestPath ( platform , projectRoot , exp ) { if ( platform === 'ios' ) { return exp . ios && exp . ios . publishManifestPath ? exp . ios . publishManifestPath : _getDefaultEmbeddedManifestPath ( platform , projectRoot , exp ) ; } else if ( platform === 'android' ) { return exp . android && exp . android . publishManifestPath ? exp . android . publishManifestPath : _getDefaultEmbeddedManifestPath ( platform , projectRoot , exp ) ; } return _getDefaultEmbeddedManifestPath ( platform , projectRoot , exp ) ; } function _getDefaultEmbeddedManifestPath ( platform , projectRoot , exp ) { return path . join ( _getDefaultEmbeddedAssetDir ( platform , projectRoot , exp ) , 'app.manifest' ) ; } function _getDefaultEmbeddedBundlePath ( platform , projectRoot , exp ) { return path . join ( _getDefaultEmbeddedAssetDir ( platform , projectRoot , exp ) , 'app.bundle' ) ; } function _getDefaultEmbeddedAssetDir ( platform , projectRoot , exp ) { if ( platform === 'ios' ) { const { iosSupportingDirectory } = getIOSPaths ( projectRoot ) ; return iosSupportingDirectory ; } else if ( platform === 'android' ) { return path . join ( projectRoot , 'android' , 'app' , 'src' , 'main' , 'assets' ) ; } else { throw new Error ( 'Embedding assets is not supported for platform ' + platform ) ; } } export function shouldEmbedAssetsForExpoUpdates ( projectRoot , exp , pkg , target ) { if ( ! pkg . dependencies ?. [ 'expo-updates' ] || target !== 'bare' ) { return false ; } const expoUpdatesVersion = semver . coerce ( pkg . dependencies [ 'expo-updates' ] ) ; if ( expoUpdatesVersion && semver . satisfies ( expoUpdatesVersion , '~0.1.0' ) ) { return true ; } if ( fs . existsSync ( _getDefaultEmbeddedBundlePath ( 'android' , projectRoot , exp ) ) || fs . existsSync ( _getDefaultEmbeddedManifestPath ( 'android' , projectRoot , exp ) ) || fs . existsSync ( _getDefaultEmbeddedBundlePath ( 'ios' , projectRoot , exp ) ) || fs . existsSync ( _getDefaultEmbeddedManifestPath ( 'ios' , projectRoot , exp ) ) ) { return true ; } return false ; } async function _maybeWriteArtifactsToDiskAsync ( config ) { const { projectRoot , pkg , exp , iosManifest , iosBundle , androidManifest , androidBundle , target , } = config ; let androidBundlePath ; let androidManifestPath ; let iosBundlePath ; let iosManifestPath ; if ( shouldEmbedAssetsForExpoUpdates ( projectRoot , exp , pkg , target ) ) { const defaultAndroidDir = _getDefaultEmbeddedAssetDir ( 'android' , projectRoot , exp ) ; const defaultIosDir = _getDefaultEmbeddedAssetDir ( 'ios' , projectRoot , exp ) ; await fs . ensureDir ( defaultIosDir ) ; await fs . ensureDir ( defaultAndroidDir ) ; androidBundlePath = _getDefaultEmbeddedBundlePath ( 'android' , projectRoot , exp ) ; androidManifestPath = _getDefaultEmbeddedManifestPath ( 'android' , projectRoot , exp ) ; iosBundlePath = _getDefaultEmbeddedBundlePath ( 'ios' , projectRoot , exp ) ; iosManifestPath = _getDefaultEmbeddedManifestPath ( 'ios' , projectRoot , exp ) ; if ( ! fs . existsSync ( iosBundlePath ) || ! fs . existsSync ( iosManifestPath ) ) { logger . global . warn ( 'Creating app.manifest and app.bundle inside of your ios/<project>/Supporting directory.\nBe sure to add these files to your Xcode project. More info at https://expo.fyi/embedded-assets' ) ; } } if ( exp . android ?. publishBundlePath ) { androidBundlePath = exp . android . publishBundlePath ; } if ( exp . android ?. publishManifestPath ) { androidManifestPath = exp . android . publishManifestPath ; } if ( exp . ios ?. publishBundlePath ) { iosBundlePath = exp . ios . publishBundlePath ; } if ( exp . ios ?. publishManifestPath ) { iosManifestPath = exp . ios . publishManifestPath ; } if ( androidBundlePath ) { await writeArtifactSafelyAsync ( projectRoot , 'android.publishBundlePath' , androidBundlePath , androidBundle ) ; } if ( androidManifestPath ) { await writeArtifactSafelyAsync ( projectRoot , 'android.publishManifestPath' , androidManifestPath , JSON . stringify ( androidManifest ) ) ; } if ( iosBundlePath ) { await writeArtifactSafelyAsync ( projectRoot , 'ios.publishBundlePath' , iosBundlePath , iosBundle ) ; } if ( iosManifestPath ) { await writeArtifactSafelyAsync ( projectRoot , 'ios.publishManifestPath' , iosManifestPath , JSON . stringify ( iosManifest ) ) ; } } async function _maybeConfigureExpoKitEmbeddedAssetsAsync ( config ) { const { projectRoot , exp , releaseChannel , androidManifestUrl , androidManifest } = config ; const context = StandaloneContext . createUserContext ( projectRoot , exp ) ; const { supportingDirectory } = IosWorkspace . getPaths ( context ) ; if ( releaseChannel && fs . existsSync ( path . join ( supportingDirectory , 'EXShell.plist' ) ) ) { await IosPlist . modifyAsync ( supportingDirectory , 'EXShell' , ( shellPlist ) => { shellPlist . releaseChannel = releaseChannel ; return shellPlist ; } ) ; } const constantsPath = path . join ( projectRoot , 'android' , 'app' , 'src' , 'main' , 'java' , 'host' , 'exp' , 'exponent' , 'generated' , 'AppConstants.java' ) ; if ( fs . existsSync ( constantsPath ) ) { await ExponentTools . deleteLinesInFileAsync ( `START EMBEDDED RESPONSES` , `END EMBEDDED RESPONSES` , constantsPath ) ; await ExponentTools . regexFileAsync ( '// ADD EMBEDDED RESPONSES HERE' , `
      // ADD EMBEDDED RESPONSES HERE
      // START EMBEDDED RESPONSES
      embeddedResponses.add(new Constants.EmbeddedResponse("${ androidManifestUrl }", "assets://shell-app-manifest.json", "application/json"));
      embeddedResponses.add(new Constants.EmbeddedResponse("${ androidManifest . bundleUrl }", "assets://shell-app.bundle", "application/javascript"));
      // END EMBEDDED RESPONSES` , constantsPath ) ; if ( releaseChannel ) { await ExponentTools . regexFileAsync ( /RELEASE_CHANNEL = "[^"]*"/ , `RELEASE_CHANNEL = "${ releaseChannel }"` , constantsPath ) ; } } } /**
 * Guess if this is a user's first publish and run a slightly modified expo-updates plugin.
 * If it is not their first publish and a config mismatch is noticed, log warnings.
 */ async function _maybeRunModifiedExpoUpdatesPluginAsync ( config ) { if ( ! config . pkg . dependencies ?. [ 'expo-updates' ] || config . target === 'managed' ) { return ; } const { projectRoot , exp , releaseChannel , iosManifestUrl , androidManifestUrl } = config ; const { iosSupportingDirectory : supportingDirectory } = getIOSPaths ( projectRoot ) ; let isLikelyFirstIOSPublish = false ; const expoPlistPath = path . join ( supportingDirectory , 'Expo.plist' ) ; if ( fs . existsSync ( expoPlistPath ) ) { let expoPlistForProject = plist . parse ( await fs . readFileSync ( expoPlistPath , 'utf8' ) ) ; const currentlyConfiguredExpoPlist = { ... expoPlistForProject } ; expoPlistForProject = IOSConfig . Updates . setUpdatesConfig ( projectRoot , exp , expoPlistForProject , null ) ; expoPlistForProject [ IOSConfig . Updates . Config . UPDATE_URL ] = iosManifestUrl ; if ( releaseChannel ) { expoPlistForProject [ IOSConfig . Updates . Config . RELEASE_CHANNEL ] = releaseChannel ; } const configuredIOSUpdatesURL = currentlyConfiguredExpoPlist [ IOSConfig . Updates . Config . UPDATE_URL ] ; if ( configuredIOSUpdatesURL === PLACEHOLDER_URL ) { isLikelyFirstIOSPublish = true ; fs . writeFileSync ( expoPlistPath , plist . build ( expoPlistForProject ) ) ; } else { const { UPDATE_URL , SDK_VERSION , RUNTIME_VERSION , RELEASE_CHANNEL , } = IOSConfig . Updates . Config ; for ( const key of [ UPDATE_URL , SDK_VERSION , RUNTIME_VERSION , RELEASE_CHANNEL ] ) { let currentlyConfiguredValue = currentlyConfiguredExpoPlist [ key ] ; const inferredValue = expoPlistForProject [ key ] ; if ( key === RELEASE_CHANNEL && inferredValue ) { currentlyConfiguredValue = currentlyConfiguredValue ?? 'default' ; } if ( currentlyConfiguredValue !== inferredValue ) { let message ; switch ( key ) { case RELEASE_CHANNEL : { message = `The value passed to the --release-channel flag is to "${ inferredValue }", but it is set to "${ currentlyConfiguredValue }".` ; break ; } case UPDATE_URL : case SDK_VERSION : case RUNTIME_VERSION : default : message = `${ key } is inferred to be "${ inferredValue }", but it is set to "${ currentlyConfiguredValue }".` ; } WarningAggregator . addWarningIOS ( `Expo.plist key: "${ key }"` , message , FYI_URL ) ; } } } } let isLikelyFirstAndroidPublish = false ; const androidManifestXmlPath = path . join ( projectRoot , 'android' , 'app' , 'src' , 'main' , 'AndroidManifest.xml' ) ; const AndroidManifestKeyForUpdateURL = AndroidConfig . Updates . Config . UPDATE_URL ; if ( fs . existsSync ( androidManifestXmlPath ) ) { const currentlyConfiguredAndroidManifest = await AndroidConfig . Manifest . readAndroidManifestAsync ( androidManifestXmlPath ) ; const currentConfiguredManifestApplication = AndroidConfig . Manifest . getMainApplicationOrThrow ( currentlyConfiguredAndroidManifest ) ; const currentlyConfiguredMetaDataAttributes = currentConfiguredManifestApplication [ 'meta-data' ] ?. map ( md => md [ '$' ] ) ?? [ ] ; const inferredAndroidManifest = AndroidConfig . Updates . setUpdatesConfig ( projectRoot , exp , currentlyConfiguredAndroidManifest , null ) ; const inferredMainApplication = AndroidConfig . Manifest . getMainApplicationOrThrow ( inferredAndroidManifest ) ; AndroidConfig . Manifest . addMetaDataItemToMainApplication ( inferredMainApplication , AndroidManifestKeyForUpdateURL , androidManifestUrl ) ; if ( releaseChannel ) { AndroidConfig . Manifest . addMetaDataItemToMainApplication ( inferredMainApplication , AndroidConfig . Updates . Config . RELEASE_CHANNEL , releaseChannel ) ; } const currentlyConfiguredAndroidUpdateURL = currentlyConfiguredMetaDataAttributes . find ( x => x [ 'android:name' ] === AndroidConfig . Updates . Config . UPDATE_URL ) ?. [ 'android:value' ] ; if ( currentlyConfiguredAndroidUpdateURL === PLACEHOLDER_URL ) { isLikelyFirstAndroidPublish = true ; await AndroidConfig . Manifest . writeAndroidManifestAsync ( androidManifestXmlPath , inferredAndroidManifest ) ; } else { const inferredMainApplication = AndroidConfig . Manifest . getMainApplicationOrThrow ( inferredAndroidManifest ) ; const inferredMetaDataAttributes = inferredMainApplication [ 'meta-data' ] ?. map ( md => md [ '$' ] ) ! ; const { UPDATE_URL , SDK_VERSION , RUNTIME_VERSION , RELEASE_CHANNEL , } = AndroidConfig . Updates . Config ; for ( const key of [ UPDATE_URL , SDK_VERSION , RUNTIME_VERSION , RELEASE_CHANNEL ] ) { const inferredValue = inferredMetaDataAttributes . find ( x => x [ 'android:name' ] === key ) ?. [ 'android:value' ] ; let currentlyConfiguredValue = currentlyConfiguredMetaDataAttributes . find ( x => x [ 'android:name' ] === key ) ?. [ 'android:value' ] ; if ( key === RELEASE_CHANNEL && inferredValue ) { currentlyConfiguredValue = currentlyConfiguredValue ?? 'default' ; } if ( inferredValue !== currentlyConfiguredValue ) { let message ; switch ( key ) { case RELEASE_CHANNEL : { message = `The value passed to the --release-channel flag is "${ inferredValue }", but it is set to "${ currentlyConfiguredValue }".` ; break ; } case UPDATE_URL : case SDK_VERSION : case RUNTIME_VERSION : default : message = `The inferred value is "${ inferredValue }", but it is set to "${ currentlyConfiguredValue }".` ; } WarningAggregator . addWarningAndroid ( `AndroidManifest.xml key "${ key }"` , message , FYI_URL ) ; } } } } if ( isLikelyFirstIOSPublish || isLikelyFirstAndroidPublish ) { let platformSpecificMessage ; if ( isLikelyFirstIOSPublish && ! isLikelyFirstAndroidPublish ) { platformSpecificMessage = '🚀 It looks like this your first iOS publish for this project! ' + `We've automatically set some configuration values in Expo.plist. ` ; } else if ( ! isLikelyFirstIOSPublish && isLikelyFirstAndroidPublish ) { platformSpecificMessage = '🚀 It looks like this your first Android publish for this project! ' + `We've automatically set some configuration values in AndroidManifest.xml. ` ; } else { platformSpecificMessage = '🚀 It looks like this your first publish for this project! ' + `We've automatically set some configuration values in Expo.plist and AndroidManifest.xml. ` ; } logger . global . warn ( platformSpecificMessage + `You'll need to make and release a new build before your users can download the update ` + 'you just published.' ) ; } } /** The code below here is duplicated from expo-cli currently **/ export function getIOSPaths ( projectRoot ) { const { exp } = getConfig ( projectRoot , { skipSDKVersionRequirement : true } ) ; const projectName = exp . name ; if ( ! projectName ) { throw new Error ( 'Your project needs a name in app.json/app.config.js.' ) ; } const iosProjectDirectory = path . join ( projectRoot , 'ios' , IOSConfig . XcodeUtils . sanitizedName ( projectName ) ) ; const iosSupportingDirectory = path . join ( projectRoot , 'ios' , IOSConfig . XcodeUtils . sanitizedName ( projectName ) , 'Supporting' ) ; const iconPath = path . join ( iosProjectDirectory , 'Assets.xcassets' , 'AppIcon.appiconset' ) ; return { projectName , iosProjectDirectory , iosSupportingDirectory , iconPath } ; }