import { ExpoConfig , getConfig } from '@expo/config' ; import assert from 'assert' ; import Joi from 'joi' ; import os from 'os' ; import QueryString from 'querystring' ; import resolveFrom from 'resolve-from' ; import url from 'url' ; import { ConnectionStatus , ip , ProjectSettings , ProjectUtils , Versions , XDLError , } from './internal' ; interface URLOptions extends Omit < ProjectSettings . ProjectSettings , 'urlRandomness' > { urlType : null | 'exp' | 'http' | 'no-protocol' | 'redirect' | 'custom' ; } interface MetroQueryOptions { dev ? : boolean ; strict ? : boolean ; minify ? : boolean ; } export async function constructBundleUrlAsync ( projectRoot , opts , requestHostname ? ) { return await constructUrlAsync ( projectRoot , opts , true , requestHostname ) ; } export async function constructDeepLinkAsync ( projectRoot , opts ? , requestHostname ? ) { const { devClient } = await ProjectSettings . readAsync ( projectRoot ) ; if ( devClient ) { return constructDevClientUrlAsync ( projectRoot , opts , requestHostname ) ; } else { return constructManifestUrlAsync ( projectRoot , opts , requestHostname ) ; } } export async function constructManifestUrlAsync ( projectRoot , opts ? , requestHostname ? ) { return await constructUrlAsync ( projectRoot , opts ?? null , false , requestHostname ) ; } export async function constructDevClientUrlAsync ( projectRoot , opts ? , requestHostname ? ) { let _scheme ; if ( opts ?. scheme ) { _scheme = opts ?. scheme ; } else { const { scheme } = await ProjectSettings . readAsync ( projectRoot ) ; if ( ! scheme || typeof scheme !== 'string' ) { throw new XDLError ( 'NO_DEV_CLIENT_SCHEME' , 'No scheme specified for development client' ) ; } _scheme = scheme ; } const protocol = resolveProtocol ( projectRoot , { scheme : _scheme , urlType : 'custom' } ) ; const manifestUrl = await constructManifestUrlAsync ( projectRoot , { ... opts , urlType : 'http' } , requestHostname ) ; return `${ protocol }://expo-development-client/?url=${ encodeURIComponent ( manifestUrl ) }` ; } export async function constructHostUriAsync ( projectRoot , requestHostname ? ) { const urlString = await constructUrlAsync ( projectRoot , null , false , requestHostname ) ; const urlObj = url . parse ( urlString ) ; urlObj . protocol = '' ; urlObj . slashes = false ; return url . format ( urlObj ) ; } export async function constructLogUrlAsync ( projectRoot , requestHostname ? ) { const baseUrl = await constructUrlAsync ( projectRoot , { urlType : 'http' } , false , requestHostname ) ; return `${ baseUrl }/logs` ; } export async function constructLoadingUrlAsync ( projectRoot , platform , requestHostname ? ) { const baseUrl = await constructUrlAsync ( projectRoot , { urlType : 'http' } , false , requestHostname ) ; return `${ baseUrl }/_expo/loading?platform=${ platform }` ; } export async function constructUrlWithExtensionAsync ( projectRoot , entryPoint , ext , requestHostname ? , metroQueryOptions ? ) { const defaultOpts = { dev : false , minify : true } ; metroQueryOptions = metroQueryOptions || defaultOpts ; let bundleUrl = await constructBundleUrlAsync ( projectRoot , { hostType : 'localhost' , urlType : 'http' } , requestHostname ) ; const mainModulePath = stripJSExtension ( entryPoint ) ; bundleUrl += `/${ mainModulePath }.${ ext }` ; const queryParams = constructBundleQueryParams ( projectRoot , metroQueryOptions ) ; return `${ bundleUrl }?${ queryParams }` ; } export async function constructPublishUrlAsync ( projectRoot , entryPoint , requestHostname ? , metroQueryOptions ? ) { return await constructUrlWithExtensionAsync ( projectRoot , entryPoint , 'bundle' , requestHostname , metroQueryOptions ) ; } export async function constructSourceMapUrlAsync ( projectRoot , entryPoint , requestHostname ? ) { return await constructUrlWithExtensionAsync ( projectRoot , entryPoint , 'map' , requestHostname ) ; } export async function constructAssetsUrlAsync ( projectRoot , entryPoint , requestHostname ? ) { return await constructUrlWithExtensionAsync ( projectRoot , entryPoint , 'assets' , requestHostname ) ; } export async function constructDebuggerHostAsync ( projectRoot , requestHostname ? ) { return await constructUrlAsync ( projectRoot , { urlType : 'no-protocol' } , true , requestHostname ) ; } export function constructBundleQueryParams ( projectRoot , opts ) { const { exp } = getConfig ( projectRoot , { skipSDKVersionRequirement : true } ) ; return constructBundleQueryParamsWithConfig ( projectRoot , opts , exp ) ; } export function constructBundleQueryParamsWithConfig ( projectRoot , opts , exp ) { const queryParams = { dev : ! ! opts . dev , hot : false } ; if ( 'strict' in opts ) { queryParams . strict = ! ! opts . strict ; } if ( 'minify' in opts ) { queryParams . minify = ! ! opts . minify ; } if ( Versions . lteSdkVersion ( exp , '10.0.0' ) ) { queryParams . includeAssetFileHashes = true ; } else if ( Versions . lteSdkVersion ( exp , '32.0.0' ) ) { const pluginModule = resolveFrom ( projectRoot , 'expo/tools/hashAssetFiles' ) ; queryParams . assetPlugin = encodeURIComponent ( pluginModule ) ; } return QueryString . stringify ( queryParams ) ; } export async function constructWebAppUrlAsync ( projectRoot , options = { } ) { const packagerInfo = await ProjectSettings . readPackagerInfoAsync ( projectRoot ) ; if ( ! packagerInfo . webpackServerPort ) { return null ; } const { https , hostType } = await ProjectSettings . readAsync ( projectRoot ) ; const host = ( options . hostType ?? hostType ) === 'localhost' ? 'localhost' : ip . address ( ) ; let urlType = 'http' ; if ( https === true ) { urlType = 'https' ; } return `${ urlType }://${ host }:${ packagerInfo . webpackServerPort }` ; } function assertValidOptions ( opts ) { const schema = Joi . object ( ) . keys ( { devClient : Joi . boolean ( ) . optional ( ) , scheme : Joi . string ( ) . optional ( ) . allow ( null ) , urlType : Joi . any ( ) . valid ( 'exp' , 'http' , 'redirect' , 'no-protocol' ) . allow ( null ) , lanType : Joi . any ( ) . valid ( 'ip' , 'hostname' ) , hostType : Joi . any ( ) . valid ( 'localhost' , 'lan' , 'tunnel' ) , dev : Joi . boolean ( ) , strict : Joi . boolean ( ) , minify : Joi . boolean ( ) , https : Joi . boolean ( ) . optional ( ) , urlRandomness : Joi . string ( ) . optional ( ) . allow ( null ) } ) ; const { error } = schema . validate ( opts ) ; if ( error ) { throw new XDLError ( 'INVALID_OPTIONS' , error . toString ( ) ) ; } return opts as URLOptions ; } async function ensureOptionsAsync ( projectRoot , opts ) { if ( opts ) { assertValidOptions ( opts ) ; } const defaultOpts = await ProjectSettings . readAsync ( projectRoot ) ; if ( ! opts ) { return { urlType : null , ... defaultOpts } ; } const optionsWithDefaults = { ... defaultOpts , ... opts } ; return assertValidOptions ( optionsWithDefaults ) ; } function resolveProtocol ( projectRoot , { urlType , ... options } ) { if ( urlType === 'http' ) { return 'http' ; } else if ( urlType === 'no-protocol' ) { return null ; } else if ( urlType === 'custom' ) { return options . scheme ; } let protocol = 'exp' ; const { exp } = getConfig ( projectRoot , { skipSDKVersionRequirement : true } ) ; const { scheme , detach , sdkVersion } = exp ; if ( detach ) { const schemes = ( Array . isArray ( scheme ) ? scheme : [ scheme ] ) . filter ( ( scheme ) => typeof scheme === 'string' && ! ! scheme ) ; const firstScheme = schemes [ 0 ] ; if ( firstScheme && ! Versions . lteSdkVersion ( { sdkVersion } , '26.0.0' ) ) { protocol = firstScheme ; } else if ( detach . scheme ) { protocol = detach . scheme ; } } return protocol ; } export async function constructUrlAsync ( projectRoot , incomingOpts , isPackager , requestHostname ? ) { const opts = await ensureOptionsAsync ( projectRoot , incomingOpts ) ; const packagerInfo = await ProjectSettings . readPackagerInfoAsync ( projectRoot ) ; let protocol = resolveProtocol ( projectRoot , opts ) ; let hostname ; let port ; const proxyURL = isPackager ? process . env . EXPO_PACKAGER_PROXY_URL : process . env . EXPO_MANIFEST_PROXY_URL ; if ( proxyURL ) { const parsedProxyURL = url . parse ( proxyURL ) ; hostname = parsedProxyURL . hostname ; port = parsedProxyURL . port ; if ( parsedProxyURL . protocol === 'https:' ) { if ( protocol === 'http' ) { protocol = 'https' ; } if ( ! port ) { port = '443' ; } } } else if ( opts . hostType === 'localhost' || requestHostname === 'localhost' ) { hostname = '127.0.0.1' ; port = isPackager ? packagerInfo . packagerPort : packagerInfo . expoServerPort ; } else if ( opts . hostType === 'lan' || ConnectionStatus . isOffline ( ) ) { if ( process . env . EXPO_PACKAGER_HOSTNAME ) { hostname = process . env . EXPO_PACKAGER_HOSTNAME . trim ( ) ; } else if ( process . env . REACT_NATIVE_PACKAGER_HOSTNAME ) { hostname = process . env . REACT_NATIVE_PACKAGER_HOSTNAME . trim ( ) ; } else if ( opts . lanType === 'ip' ) { if ( requestHostname ) { hostname = requestHostname ; } else { hostname = ip . address ( ) ; } } else { hostname = os . hostname ( ) ; } port = isPackager ? packagerInfo . packagerPort : packagerInfo . expoServerPort ; } else { const ngrokUrl = isPackager ? packagerInfo . packagerNgrokUrl : packagerInfo . expoServerNgrokUrl ; if ( ! ngrokUrl || typeof ngrokUrl !== 'string' ) { ProjectUtils . logWarning ( projectRoot , 'expo' , 'Tunnel URL not found (it might not be ready yet), falling back to LAN URL.' , 'tunnel-url-not-found' ) ; return constructUrlAsync ( projectRoot , { ... opts , hostType : 'lan' } , isPackager , requestHostname ) ; } else { ProjectUtils . clearNotification ( projectRoot , 'tunnel-url-not-found' ) ; const pnu = url . parse ( ngrokUrl ) ; hostname = pnu . hostname ; port = pnu . port ; } } const url_ = joinURLComponents ( { protocol , hostname , port } ) ; if ( opts . urlType === 'redirect' ) { return createRedirectURL ( url_ ) ; } return url_ ; } function createRedirectURL ( url ) { return `https://exp.host/--/to-exp/${ encodeURIComponent ( url ) }` ; } function joinURLComponents ( { protocol , hostname , port , } ) { assert ( hostname , 'hostname cannot be inferred.' ) ; const validPort = port ?? '80' ; const validProtocol = protocol ? `${ protocol }://` : '' ; return `${ validProtocol }${ hostname }:${ validPort }` ; } export function stripJSExtension ( entryPoint ) { return entryPoint . replace ( /\.js$/ , '' ) ; } export function isHttps ( urlString ) { return isURL ( urlString , { protocols : [ 'https' ] } ) ; } export function isURL ( urlString , { protocols , requireProtocol } ) { try { new url . URL ( urlString ) ; const parsed = url . parse ( urlString ) ; if ( ! parsed . protocol && ! requireProtocol ) { return true ; } return protocols ? parsed . protocol ? protocols . map ( x => `${ x . toLowerCase ( ) }:` ) . includes ( parsed . protocol ) : false : true ; } catch ( err ) { return false ; } }