18d307f52SEvan Baconimport assert from 'assert'; 28d307f52SEvan Baconimport resolveFrom from 'resolve-from'; 38d307f52SEvan Bacon 48a424bebSJames Ideimport { AsyncNgrok } from './AsyncNgrok'; 58a424bebSJames Ideimport { DevelopmentSession } from './DevelopmentSession'; 68a424bebSJames Ideimport { CreateURLOptions, UrlCreator } from './UrlCreator'; 78a424bebSJames Ideimport { PlatformBundlers } from './platformBundlers'; 88d307f52SEvan Baconimport * as Log from '../../log'; 98d307f52SEvan Baconimport { FileNotifier } from '../../utils/FileNotifier'; 10d04463cbSEvan Baconimport { resolveWithTimeout } from '../../utils/delay'; 118d307f52SEvan Baconimport { env } from '../../utils/env'; 1229975bfdSEvan Baconimport { CommandError } from '../../utils/errors'; 13065a44f7SCedric van Puttenimport { openBrowserAsync } from '../../utils/open'; 143d6e487dSEvan Baconimport { 153d6e487dSEvan Bacon BaseOpenInCustomProps, 163d6e487dSEvan Bacon BaseResolveDeviceProps, 173d6e487dSEvan Bacon PlatformManager, 183d6e487dSEvan Bacon} from '../platforms/PlatformManager'; 198d307f52SEvan Bacon 20474a7a4bSEvan Baconconst debug = require('debug')('expo:start:server:devServer') as typeof console.log; 21474a7a4bSEvan Bacon 22*edeec536SEvan Baconexport type MessageSocket = { 23*edeec536SEvan Bacon broadcast: (method: string, params?: Record<string, any> | undefined) => void; 24*edeec536SEvan Bacon}; 25*edeec536SEvan Bacon 268d307f52SEvan Baconexport type ServerLike = { 2729975bfdSEvan Bacon close(callback?: (err?: Error) => void): void; 2833643b60SEvan Bacon addListener?(event: string, listener: (...args: any[]) => void): unknown; 298d307f52SEvan Bacon}; 308d307f52SEvan Bacon 318d307f52SEvan Baconexport type DevServerInstance = { 328d307f52SEvan Bacon /** Bundler dev server instance. */ 338d307f52SEvan Bacon server: ServerLike; 348d307f52SEvan Bacon /** Dev server URL location properties. */ 358d307f52SEvan Bacon location: { 368d307f52SEvan Bacon url: string; 378d307f52SEvan Bacon port: number; 388d307f52SEvan Bacon protocol: 'http' | 'https'; 398d307f52SEvan Bacon host?: string; 408d307f52SEvan Bacon }; 418d307f52SEvan Bacon /** Additional middleware that's attached to the `server`. */ 428d307f52SEvan Bacon middleware: any; 438d307f52SEvan Bacon /** Message socket for communicating with the runtime. */ 448d307f52SEvan Bacon messageSocket: MessageSocket; 458d307f52SEvan Bacon}; 468d307f52SEvan Bacon 478d307f52SEvan Baconexport interface BundlerStartOptions { 488d307f52SEvan Bacon /** Should the dev server use `https` protocol. */ 498d307f52SEvan Bacon https?: boolean; 508d307f52SEvan Bacon /** Should start the dev servers in development mode (minify). */ 518d307f52SEvan Bacon mode?: 'development' | 'production'; 528d307f52SEvan Bacon /** Is dev client enabled. */ 538d307f52SEvan Bacon devClient?: boolean; 548d307f52SEvan Bacon /** Should run dev servers with clean caches. */ 558d307f52SEvan Bacon resetDevServer?: boolean; 56e377ff85SWill Schurman /** Code signing private key path (defaults to same directory as certificate) */ 57e377ff85SWill Schurman privateKeyPath?: string; 588d307f52SEvan Bacon 598d307f52SEvan Bacon /** Max amount of workers (threads) to use with Metro bundler, defaults to undefined for max workers. */ 608d307f52SEvan Bacon maxWorkers?: number; 618d307f52SEvan Bacon /** Port to start the dev server on. */ 628d307f52SEvan Bacon port?: number; 638d307f52SEvan Bacon 643d6e487dSEvan Bacon /** Should start a headless dev server e.g. mock representation to approximate info from a server running in a different process. */ 653d6e487dSEvan Bacon headless?: boolean; 668d307f52SEvan Bacon /** Should instruct the bundler to create minified bundles. */ 678d307f52SEvan Bacon minify?: boolean; 688d307f52SEvan Bacon 69429dc7fcSEvan Bacon /** Will the bundler be used for exporting. NOTE: This is an odd option to pass to the dev server. */ 70429dc7fcSEvan Bacon isExporting?: boolean; 71429dc7fcSEvan Bacon 728d307f52SEvan Bacon // Webpack options 738d307f52SEvan Bacon /** Should modify and create PWA icons. */ 748d307f52SEvan Bacon isImageEditingEnabled?: boolean; 758d307f52SEvan Bacon 768d307f52SEvan Bacon location: CreateURLOptions; 778d307f52SEvan Bacon} 788d307f52SEvan Bacon 798d307f52SEvan Baconconst PLATFORM_MANAGERS = { 808d307f52SEvan Bacon simulator: () => 818d307f52SEvan Bacon require('../platforms/ios/ApplePlatformManager') 828d307f52SEvan Bacon .ApplePlatformManager as typeof import('../platforms/ios/ApplePlatformManager').ApplePlatformManager, 838d307f52SEvan Bacon emulator: () => 848d307f52SEvan Bacon require('../platforms/android/AndroidPlatformManager') 858d307f52SEvan Bacon .AndroidPlatformManager as typeof import('../platforms/android/AndroidPlatformManager').AndroidPlatformManager, 868d307f52SEvan Bacon}; 878d307f52SEvan Bacon 888d307f52SEvan Baconexport abstract class BundlerDevServer { 898d307f52SEvan Bacon /** Name of the bundler. */ 908d307f52SEvan Bacon abstract get name(): string; 918d307f52SEvan Bacon 928d307f52SEvan Bacon /** Ngrok instance for managing tunnel connections. */ 938d307f52SEvan Bacon protected ngrok: AsyncNgrok | null = null; 948d307f52SEvan Bacon /** Interfaces with the Expo 'Development Session' API. */ 958d307f52SEvan Bacon protected devSession: DevelopmentSession | null = null; 968d307f52SEvan Bacon /** Http server and related info. */ 978d307f52SEvan Bacon protected instance: DevServerInstance | null = null; 988d307f52SEvan Bacon /** Native platform interfaces for opening projects. */ 998d307f52SEvan Bacon private platformManagers: Record<string, PlatformManager<any>> = {}; 1008d307f52SEvan Bacon /** Manages the creation of dev server URLs. */ 1018d307f52SEvan Bacon protected urlCreator?: UrlCreator | null = null; 1028d307f52SEvan Bacon 1035404abc1SEvan Bacon private notifier: FileNotifier | null = null; 1045404abc1SEvan Bacon 1058d307f52SEvan Bacon constructor( 1068d307f52SEvan Bacon /** Project root folder. */ 1078d307f52SEvan Bacon public projectRoot: string, 1086d6b81f9SEvan Bacon /** A mapping of bundlers to platforms. */ 1096d6b81f9SEvan Bacon public platformBundlers: PlatformBundlers, 1108d307f52SEvan Bacon // TODO: Replace with custom scheme maybe... 1118d307f52SEvan Bacon public isDevClient?: boolean 1128d307f52SEvan Bacon ) {} 1138d307f52SEvan Bacon 1148d307f52SEvan Bacon protected setInstance(instance: DevServerInstance) { 1158d307f52SEvan Bacon this.instance = instance; 1168d307f52SEvan Bacon } 1178d307f52SEvan Bacon 1188d307f52SEvan Bacon /** Get the manifest middleware function. */ 1198d307f52SEvan Bacon protected async getManifestMiddlewareAsync( 1209ba03fb0SWill Schurman options: Pick<BundlerStartOptions, 'minify' | 'mode' | 'privateKeyPath'> = {} 1218d307f52SEvan Bacon ) { 1229ba03fb0SWill Schurman const Middleware = require('./middleware/ExpoGoManifestHandlerMiddleware') 1239ba03fb0SWill Schurman .ExpoGoManifestHandlerMiddleware as typeof import('./middleware/ExpoGoManifestHandlerMiddleware').ExpoGoManifestHandlerMiddleware; 1248d307f52SEvan Bacon 1258d307f52SEvan Bacon const urlCreator = this.getUrlCreator(); 1268d307f52SEvan Bacon const middleware = new Middleware(this.projectRoot, { 1278d307f52SEvan Bacon constructUrl: urlCreator.constructUrl.bind(urlCreator), 1288d307f52SEvan Bacon mode: options.mode, 1298d307f52SEvan Bacon minify: options.minify, 1308d307f52SEvan Bacon isNativeWebpack: this.name === 'webpack' && this.isTargetingNative(), 131e377ff85SWill Schurman privateKeyPath: options.privateKeyPath, 1328d307f52SEvan Bacon }); 1330a6ddb20SEvan Bacon return middleware; 1348d307f52SEvan Bacon } 1358d307f52SEvan Bacon 1368d307f52SEvan Bacon /** Start the dev server using settings defined in the start command. */ 1373d6e487dSEvan Bacon public async startAsync(options: BundlerStartOptions): Promise<DevServerInstance> { 1383d6e487dSEvan Bacon await this.stopAsync(); 1393d6e487dSEvan Bacon 1403d6e487dSEvan Bacon let instance: DevServerInstance; 1413d6e487dSEvan Bacon if (options.headless) { 1423d6e487dSEvan Bacon instance = await this.startHeadlessAsync(options); 1433d6e487dSEvan Bacon } else { 1443d6e487dSEvan Bacon instance = await this.startImplementationAsync(options); 1453d6e487dSEvan Bacon } 1463d6e487dSEvan Bacon 1473d6e487dSEvan Bacon this.setInstance(instance); 1483d6e487dSEvan Bacon await this.postStartAsync(options); 1493d6e487dSEvan Bacon return instance; 1503d6e487dSEvan Bacon } 1513d6e487dSEvan Bacon 1523d6e487dSEvan Bacon protected abstract startImplementationAsync( 1533d6e487dSEvan Bacon options: BundlerStartOptions 1543d6e487dSEvan Bacon ): Promise<DevServerInstance>; 1553d6e487dSEvan Bacon 1561117330aSMark Lawlor public async waitForTypeScriptAsync(): Promise<boolean> { 1571117330aSMark Lawlor return false; 1581117330aSMark Lawlor } 1591117330aSMark Lawlor 16094b54ec3SEvan Bacon public abstract startTypeScriptServices(): Promise<void>; 16133643b60SEvan Bacon 1626a750d06SEvan Bacon public async watchEnvironmentVariables(): Promise<void> { 1636a750d06SEvan Bacon // noop -- We've only implemented this functionality in Metro. 1646a750d06SEvan Bacon } 1656a750d06SEvan Bacon 1663d6e487dSEvan Bacon /** 1673d6e487dSEvan Bacon * Creates a mock server representation that can be used to estimate URLs for a server started in another process. 1683d6e487dSEvan Bacon * This is used for the run commands where you can reuse the server from a previous run. 1693d6e487dSEvan Bacon */ 1703d6e487dSEvan Bacon private async startHeadlessAsync(options: BundlerStartOptions): Promise<DevServerInstance> { 1713d6e487dSEvan Bacon if (!options.port) 1723d6e487dSEvan Bacon throw new CommandError('HEADLESS_SERVER', 'headless dev server requires a port option'); 1733d6e487dSEvan Bacon this.urlCreator = this.getUrlCreator(options); 1743d6e487dSEvan Bacon 1753d6e487dSEvan Bacon return { 1763d6e487dSEvan Bacon // Create a mock server 1773d6e487dSEvan Bacon server: { 1783d6e487dSEvan Bacon close: () => { 1793d6e487dSEvan Bacon this.instance = null; 1803d6e487dSEvan Bacon }, 18133643b60SEvan Bacon addListener() {}, 1823d6e487dSEvan Bacon }, 1833d6e487dSEvan Bacon location: { 1843d6e487dSEvan Bacon // The port is the main thing we want to send back. 1853d6e487dSEvan Bacon port: options.port, 1863d6e487dSEvan Bacon // localhost isn't always correct. 1873d6e487dSEvan Bacon host: 'localhost', 1883d6e487dSEvan Bacon // http is the only supported protocol on native. 1893d6e487dSEvan Bacon url: `http://localhost:${options.port}`, 1903d6e487dSEvan Bacon protocol: 'http', 1913d6e487dSEvan Bacon }, 1923d6e487dSEvan Bacon middleware: {}, 1933d6e487dSEvan Bacon messageSocket: { 1943d6e487dSEvan Bacon broadcast: () => { 1953d6e487dSEvan Bacon throw new CommandError('HEADLESS_SERVER', 'Cannot broadcast messages to headless server'); 1963d6e487dSEvan Bacon }, 1973d6e487dSEvan Bacon }, 1983d6e487dSEvan Bacon }; 1993d6e487dSEvan Bacon } 2008d307f52SEvan Bacon 201161657f3SEvan Bacon /** 202161657f3SEvan Bacon * Runs after the `startAsync` function, performing any additional common operations. 203161657f3SEvan Bacon * You can assume the dev server is started by the time this function is called. 204161657f3SEvan Bacon */ 2058d307f52SEvan Bacon protected async postStartAsync(options: BundlerStartOptions) { 206161657f3SEvan Bacon if ( 207161657f3SEvan Bacon options.location.hostType === 'tunnel' && 208e32ccf9fSEvan Bacon !env.EXPO_OFFLINE && 209161657f3SEvan Bacon // This is a hack to prevent using tunnel on web since we block it upstream for some reason. 210161657f3SEvan Bacon this.isTargetingNative() 211161657f3SEvan Bacon ) { 2128d307f52SEvan Bacon await this._startTunnelAsync(); 2138d307f52SEvan Bacon } 2148d307f52SEvan Bacon await this.startDevSessionAsync(); 2158d307f52SEvan Bacon 2168d307f52SEvan Bacon this.watchConfig(); 2178d307f52SEvan Bacon } 2188d307f52SEvan Bacon 2198d307f52SEvan Bacon protected abstract getConfigModuleIds(): string[]; 2208d307f52SEvan Bacon 2218d307f52SEvan Bacon protected watchConfig() { 2225404abc1SEvan Bacon this.notifier?.stopObserving(); 2235404abc1SEvan Bacon this.notifier = new FileNotifier(this.projectRoot, this.getConfigModuleIds()); 2245404abc1SEvan Bacon this.notifier.startObserving(); 2258d307f52SEvan Bacon } 2268d307f52SEvan Bacon 2278d307f52SEvan Bacon /** Create ngrok instance and start the tunnel server. Exposed for testing. */ 2288d307f52SEvan Bacon public async _startTunnelAsync(): Promise<AsyncNgrok | null> { 2298d307f52SEvan Bacon const port = this.getInstance()?.location.port; 2308d307f52SEvan Bacon if (!port) return null; 231474a7a4bSEvan Bacon debug('[ngrok] connect to port: ' + port); 2328d307f52SEvan Bacon this.ngrok = new AsyncNgrok(this.projectRoot, port); 2338d307f52SEvan Bacon await this.ngrok.startAsync(); 2348d307f52SEvan Bacon return this.ngrok; 2358d307f52SEvan Bacon } 2368d307f52SEvan Bacon 2378d307f52SEvan Bacon protected async startDevSessionAsync() { 2388d307f52SEvan Bacon // This is used to make Expo Go open the project in either Expo Go, or the web browser. 2398d307f52SEvan Bacon // Must come after ngrok (`startTunnelAsync`) setup. 2405404abc1SEvan Bacon this.devSession?.stopNotifying?.(); 2418d307f52SEvan Bacon this.devSession = new DevelopmentSession( 2428d307f52SEvan Bacon this.projectRoot, 2438d307f52SEvan Bacon // This URL will be used on external devices so the computer IP won't be relevant. 2448d307f52SEvan Bacon this.isTargetingNative() 2458d307f52SEvan Bacon ? this.getNativeRuntimeUrl() 24681e9e3beSEvan Bacon : this.getDevServerUrl({ hostType: 'localhost' }), 247674b4f9dSEvan Bacon () => { 248674b4f9dSEvan Bacon // TODO: This appears to be happening consistently after an hour. 249674b4f9dSEvan Bacon // We should investigate why this is happening and fix it on our servers. 250674b4f9dSEvan Bacon // Log.error( 251674b4f9dSEvan Bacon // chalk.red( 252674b4f9dSEvan Bacon // '\nAn unexpected error occurred while updating the Dev Session API. This project will not appear in the "Development servers" section of the Expo Go app until this process has been restarted.' 253674b4f9dSEvan Bacon // ) 254674b4f9dSEvan Bacon // ); 255674b4f9dSEvan Bacon // Log.exception(error); 25681e9e3beSEvan Bacon this.devSession?.closeAsync().catch((error) => { 25781e9e3beSEvan Bacon debug('[dev-session] error closing: ' + error.message); 25881e9e3beSEvan Bacon }); 25981e9e3beSEvan Bacon } 2608d307f52SEvan Bacon ); 2618d307f52SEvan Bacon 2628d307f52SEvan Bacon await this.devSession.startAsync({ 2638d307f52SEvan Bacon runtime: this.isTargetingNative() ? 'native' : 'web', 2648d307f52SEvan Bacon }); 2658d307f52SEvan Bacon } 2668d307f52SEvan Bacon 2678d307f52SEvan Bacon public isTargetingNative() { 2688d307f52SEvan Bacon // Temporary hack while we implement multi-bundler dev server proxy. 2698d307f52SEvan Bacon return true; 2708d307f52SEvan Bacon } 2718d307f52SEvan Bacon 2728d307f52SEvan Bacon public isTargetingWeb() { 2736d6b81f9SEvan Bacon return this.platformBundlers.web === this.name; 2748d307f52SEvan Bacon } 2758d307f52SEvan Bacon 2768d307f52SEvan Bacon /** 2778d307f52SEvan Bacon * Sends a message over web sockets to any connected device, 2788d307f52SEvan Bacon * does nothing when the dev server is not running. 2798d307f52SEvan Bacon * 2808d307f52SEvan Bacon * @param method name of the command. In RN projects `reload`, and `devMenu` are available. In Expo Go, `sendDevCommand` is available. 2818d307f52SEvan Bacon * @param params 2828d307f52SEvan Bacon */ 2838d307f52SEvan Bacon public broadcastMessage( 2848d307f52SEvan Bacon method: 'reload' | 'devMenu' | 'sendDevCommand', 2858d307f52SEvan Bacon params?: Record<string, any> 2868d307f52SEvan Bacon ) { 2878d307f52SEvan Bacon this.getInstance()?.messageSocket.broadcast(method, params); 2888d307f52SEvan Bacon } 2898d307f52SEvan Bacon 2908d307f52SEvan Bacon /** Get the running dev server instance. */ 2918d307f52SEvan Bacon public getInstance() { 2928d307f52SEvan Bacon return this.instance; 2938d307f52SEvan Bacon } 2948d307f52SEvan Bacon 2958d307f52SEvan Bacon /** Stop the running dev server instance. */ 2968d307f52SEvan Bacon async stopAsync() { 2975404abc1SEvan Bacon // Stop file watching. 2985404abc1SEvan Bacon this.notifier?.stopObserving(); 2995404abc1SEvan Bacon 300edc92349SJuwan Wheatley // Stop the dev session timer and tell Expo API to remove dev session. 301edc92349SJuwan Wheatley await this.devSession?.closeAsync(); 3028d307f52SEvan Bacon 3038d307f52SEvan Bacon // Stop ngrok if running. 3048d307f52SEvan Bacon await this.ngrok?.stopAsync().catch((e) => { 3058d307f52SEvan Bacon Log.error(`Error stopping ngrok:`); 3068d307f52SEvan Bacon Log.exception(e); 3078d307f52SEvan Bacon }); 3088d307f52SEvan Bacon 309d04463cbSEvan Bacon return resolveWithTimeout( 310d04463cbSEvan Bacon () => 311d04463cbSEvan Bacon new Promise<void>((resolve, reject) => { 3128d307f52SEvan Bacon // Close the server. 313474a7a4bSEvan Bacon debug(`Stopping dev server (bundler: ${this.name})`); 314d04463cbSEvan Bacon 3158d307f52SEvan Bacon if (this.instance?.server) { 3168d307f52SEvan Bacon this.instance.server.close((error) => { 317474a7a4bSEvan Bacon debug(`Stopped dev server (bundler: ${this.name})`); 3188d307f52SEvan Bacon this.instance = null; 3198d307f52SEvan Bacon if (error) { 3208d307f52SEvan Bacon reject(error); 3218d307f52SEvan Bacon } else { 3228d307f52SEvan Bacon resolve(); 3238d307f52SEvan Bacon } 3248d307f52SEvan Bacon }); 3258d307f52SEvan Bacon } else { 326474a7a4bSEvan Bacon debug(`Stopped dev server (bundler: ${this.name})`); 3278d307f52SEvan Bacon this.instance = null; 3288d307f52SEvan Bacon resolve(); 3298d307f52SEvan Bacon } 330d04463cbSEvan Bacon }), 331d04463cbSEvan Bacon { 332d04463cbSEvan Bacon // NOTE(Bacon): Metro dev server doesn't seem to be closing in time. 333d04463cbSEvan Bacon timeout: 1000, 334d04463cbSEvan Bacon errorMessage: `Timeout waiting for '${this.name}' dev server to close`, 335d04463cbSEvan Bacon } 336d04463cbSEvan Bacon ); 3378d307f52SEvan Bacon } 3388d307f52SEvan Bacon 339a7e47f4dSEvan Bacon public getUrlCreator(options: Partial<Pick<BundlerStartOptions, 'port' | 'location'>> = {}) { 3403d6e487dSEvan Bacon if (!this.urlCreator) { 3413d6e487dSEvan Bacon assert(options?.port, 'Dev server instance not found'); 3423d6e487dSEvan Bacon this.urlCreator = new UrlCreator(options.location, { 3433d6e487dSEvan Bacon port: options.port, 3443d6e487dSEvan Bacon getTunnelUrl: this.getTunnelUrl.bind(this), 3453d6e487dSEvan Bacon }); 3463d6e487dSEvan Bacon } 3478d307f52SEvan Bacon return this.urlCreator; 3488d307f52SEvan Bacon } 3498d307f52SEvan Bacon 3508d307f52SEvan Bacon public getNativeRuntimeUrl(opts: Partial<CreateURLOptions> = {}) { 3518d307f52SEvan Bacon return this.isDevClient 3528d307f52SEvan Bacon ? this.getUrlCreator().constructDevClientUrl(opts) ?? this.getDevServerUrl() 3538d307f52SEvan Bacon : this.getUrlCreator().constructUrl({ ...opts, scheme: 'exp' }); 3548d307f52SEvan Bacon } 3558d307f52SEvan Bacon 3568d307f52SEvan Bacon /** Get the URL for the running instance of the dev server. */ 3578d307f52SEvan Bacon public getDevServerUrl(options: { hostType?: 'localhost' } = {}): string | null { 35829975bfdSEvan Bacon const instance = this.getInstance(); 35929975bfdSEvan Bacon if (!instance?.location) { 3608d307f52SEvan Bacon return null; 3618d307f52SEvan Bacon } 36229975bfdSEvan Bacon const { location } = instance; 3638d307f52SEvan Bacon if (options.hostType === 'localhost') { 3648d307f52SEvan Bacon return `${location.protocol}://localhost:${location.port}`; 3658d307f52SEvan Bacon } 3668d307f52SEvan Bacon return location.url ?? null; 3678d307f52SEvan Bacon } 3688d307f52SEvan Bacon 36957a0d514SKudo Chien /** Get the base URL for JS inspector */ 37057a0d514SKudo Chien public getJsInspectorBaseUrl(): string { 37157a0d514SKudo Chien if (this.name !== 'metro') { 37257a0d514SKudo Chien throw new CommandError( 37357a0d514SKudo Chien 'DEV_SERVER', 37457a0d514SKudo Chien `Cannot get the JS inspector base url - bundler[${this.name}]` 37557a0d514SKudo Chien ); 37657a0d514SKudo Chien } 37757a0d514SKudo Chien return this.getUrlCreator().constructUrl({ scheme: 'http' }); 37857a0d514SKudo Chien } 37957a0d514SKudo Chien 3808d307f52SEvan Bacon /** Get the tunnel URL from ngrok. */ 3818d307f52SEvan Bacon public getTunnelUrl(): string | null { 3828d307f52SEvan Bacon return this.ngrok?.getActiveUrl() ?? null; 3838d307f52SEvan Bacon } 3848d307f52SEvan Bacon 3858d307f52SEvan Bacon /** Open the dev server in a runtime. */ 3868d307f52SEvan Bacon public async openPlatformAsync( 3878d307f52SEvan Bacon launchTarget: keyof typeof PLATFORM_MANAGERS | 'desktop', 3888d307f52SEvan Bacon resolver: BaseResolveDeviceProps<any> = {} 3898d307f52SEvan Bacon ) { 3908d307f52SEvan Bacon if (launchTarget === 'desktop') { 391a91e9b85SEvan Bacon const serverUrl = this.getDevServerUrl({ hostType: 'localhost' }); 392a91e9b85SEvan Bacon // Allow opening the tunnel URL when using Metro web. 393a91e9b85SEvan Bacon const url = this.name === 'metro' ? this.getTunnelUrl() ?? serverUrl : serverUrl; 39429975bfdSEvan Bacon await openBrowserAsync(url!); 3958d307f52SEvan Bacon return { url }; 3968d307f52SEvan Bacon } 3978d307f52SEvan Bacon 3988d307f52SEvan Bacon const runtime = this.isTargetingNative() ? (this.isDevClient ? 'custom' : 'expo') : 'web'; 3998d307f52SEvan Bacon const manager = await this.getPlatformManagerAsync(launchTarget); 4008d307f52SEvan Bacon return manager.openAsync({ runtime }, resolver); 4018d307f52SEvan Bacon } 4028d307f52SEvan Bacon 4033d6e487dSEvan Bacon /** Open the dev server in a runtime. */ 4043d6e487dSEvan Bacon public async openCustomRuntimeAsync( 4053d6e487dSEvan Bacon launchTarget: keyof typeof PLATFORM_MANAGERS, 4063d6e487dSEvan Bacon launchProps: Partial<BaseOpenInCustomProps> = {}, 4073d6e487dSEvan Bacon resolver: BaseResolveDeviceProps<any> = {} 4083d6e487dSEvan Bacon ) { 4093d6e487dSEvan Bacon const runtime = this.isTargetingNative() ? (this.isDevClient ? 'custom' : 'expo') : 'web'; 4103d6e487dSEvan Bacon if (runtime !== 'custom') { 4113d6e487dSEvan Bacon throw new CommandError( 4123d6e487dSEvan Bacon `dev server cannot open custom runtimes either because it does not target native platforms or because it is not targeting dev clients. (target: ${runtime})` 4133d6e487dSEvan Bacon ); 4143d6e487dSEvan Bacon } 4153d6e487dSEvan Bacon 4163d6e487dSEvan Bacon const manager = await this.getPlatformManagerAsync(launchTarget); 4173d6e487dSEvan Bacon return manager.openAsync({ runtime: 'custom', props: launchProps }, resolver); 4183d6e487dSEvan Bacon } 4193d6e487dSEvan Bacon 420212e3a1aSEric Samelson /** Get the URL for opening in Expo Go. */ 421212e3a1aSEric Samelson protected getExpoGoUrl(): string { 422212e3a1aSEric Samelson return this.getUrlCreator().constructUrl({ scheme: 'exp' }); 423212e3a1aSEric Samelson } 424212e3a1aSEric Samelson 4258d307f52SEvan Bacon /** Should use the interstitial page for selecting which runtime to use. */ 426212e3a1aSEric Samelson protected isRedirectPageEnabled(): boolean { 4278d307f52SEvan Bacon return ( 428212e3a1aSEric Samelson !env.EXPO_NO_REDIRECT_PAGE && 429212e3a1aSEric Samelson // if user passed --dev-client flag, skip interstitial page 430212e3a1aSEric Samelson !this.isDevClient && 4318d307f52SEvan Bacon // Checks if dev client is installed. 432212e3a1aSEric Samelson !!resolveFrom.silent(this.projectRoot, 'expo-dev-client') 4338d307f52SEvan Bacon ); 4348d307f52SEvan Bacon } 4358d307f52SEvan Bacon 436212e3a1aSEric Samelson /** Get the redirect URL when redirecting is enabled. */ 437212e3a1aSEric Samelson public getRedirectUrl(platform: keyof typeof PLATFORM_MANAGERS | null = null): string | null { 438212e3a1aSEric Samelson if (!this.isRedirectPageEnabled()) { 439212e3a1aSEric Samelson debug('Redirect page is disabled'); 440212e3a1aSEric Samelson return null; 4418d307f52SEvan Bacon } 4428d307f52SEvan Bacon 443212e3a1aSEric Samelson return ( 444212e3a1aSEric Samelson this.getUrlCreator().constructLoadingUrl( 445212e3a1aSEric Samelson {}, 446212e3a1aSEric Samelson platform === 'emulator' ? 'android' : platform === 'simulator' ? 'ios' : null 447212e3a1aSEric Samelson ) ?? null 448212e3a1aSEric Samelson ); 4498d307f52SEvan Bacon } 4508d307f52SEvan Bacon 451fd055557SKudo Chien public getReactDevToolsUrl(): string { 452fd055557SKudo Chien return new URL( 453fd055557SKudo Chien '_expo/react-devtools', 454fd055557SKudo Chien this.getUrlCreator().constructUrl({ scheme: 'http' }) 455fd055557SKudo Chien ).toString(); 456fd055557SKudo Chien } 457fd055557SKudo Chien 4588d307f52SEvan Bacon protected async getPlatformManagerAsync(platform: keyof typeof PLATFORM_MANAGERS) { 4598d307f52SEvan Bacon if (!this.platformManagers[platform]) { 4608d307f52SEvan Bacon const Manager = PLATFORM_MANAGERS[platform](); 46129975bfdSEvan Bacon const port = this.getInstance()?.location.port; 46229975bfdSEvan Bacon if (!port || !this.urlCreator) { 46329975bfdSEvan Bacon throw new CommandError( 46429975bfdSEvan Bacon 'DEV_SERVER', 46529975bfdSEvan Bacon 'Cannot interact with native platforms until dev server has started' 46629975bfdSEvan Bacon ); 46729975bfdSEvan Bacon } 468474a7a4bSEvan Bacon debug(`Creating platform manager (platform: ${platform}, port: ${port})`); 46929975bfdSEvan Bacon this.platformManagers[platform] = new Manager(this.projectRoot, port, { 4708d307f52SEvan Bacon getCustomRuntimeUrl: this.urlCreator.constructDevClientUrl.bind(this.urlCreator), 471212e3a1aSEric Samelson getExpoGoUrl: this.getExpoGoUrl.bind(this), 472212e3a1aSEric Samelson getRedirectUrl: this.getRedirectUrl.bind(this, platform), 4738d307f52SEvan Bacon getDevServerUrl: this.getDevServerUrl.bind(this, { hostType: 'localhost' }), 47429975bfdSEvan Bacon }); 4758d307f52SEvan Bacon } 4768d307f52SEvan Bacon return this.platformManagers[platform]; 4778d307f52SEvan Bacon } 4788d307f52SEvan Bacon} 479