10a6ddb20SEvan Bacon/** 20a6ddb20SEvan Bacon * Copyright © 2022 650 Industries. 30a6ddb20SEvan Bacon * 40a6ddb20SEvan Bacon * This source code is licensed under the MIT license found in the 50a6ddb20SEvan Bacon * LICENSE file in the root directory of this source tree. 60a6ddb20SEvan Bacon */ 7fe427a9eSEric Samelsonimport { getConfig } from '@expo/config'; 86a750d06SEvan Baconimport * as runtimeEnv from '@expo/env'; 99580591fSEvan Baconimport { SerialAsset } from '@expo/metro-config/build/serializer/serializerAssets'; 1033643b60SEvan Baconimport chalk from 'chalk'; 11fa47afa8SEvan Baconimport fetch from 'node-fetch'; 126a750d06SEvan Baconimport path from 'path'; 138d307f52SEvan Bacon 1446f023faSEvan Baconimport { exportAllApiRoutesAsync, rebundleApiRoute } from './bundleApiRoutes'; 1546f023faSEvan Baconimport { createRouteHandlerMiddleware } from './createServerRouteMiddleware'; 1646f023faSEvan Baconimport { fetchManifest } from './fetchRouterManifest'; 178a424bebSJames Ideimport { instantiateMetroAsync } from './instantiateMetro'; 188a424bebSJames Ideimport { metroWatchTypeScriptFiles } from './metroWatchTypeScriptFiles'; 1946f023faSEvan Baconimport { getRouterDirectoryWithManifest, isApiRouteConvention } from './router'; 2046f023faSEvan Baconimport { observeApiRouteChanges, observeFileChanges } from './waitForMetroToObserveTypeScriptFile'; 2133643b60SEvan Baconimport { Log } from '../../../log'; 22fe427a9eSEric Samelsonimport getDevClientProperties from '../../../utils/analytics/getDevClientProperties'; 23fe427a9eSEric Samelsonimport { logEventAsync } from '../../../utils/analytics/rudderstackClient'; 2473eead7fSEvan Baconimport { CommandError } from '../../../utils/errors'; 258d307f52SEvan Baconimport { getFreePortAsync } from '../../../utils/port'; 268d307f52SEvan Baconimport { BundlerDevServer, BundlerStartOptions, DevServerInstance } from '../BundlerDevServer'; 279580591fSEvan Baconimport { getStaticRenderFunctions } from '../getStaticRenderFunctions'; 282e1e108bSEvan Baconimport { ContextModuleSourceMapsMiddleware } from '../middleware/ContextModuleSourceMapsMiddleware'; 29e87e8ea8SEvan Baconimport { CreateFileMiddleware } from '../middleware/CreateFileMiddleware'; 3042637653SEvan Baconimport { FaviconMiddleware } from '../middleware/FaviconMiddleware'; 316d6b81f9SEvan Baconimport { HistoryFallbackMiddleware } from '../middleware/HistoryFallbackMiddleware'; 328d307f52SEvan Baconimport { InterstitialPageMiddleware } from '../middleware/InterstitialPageMiddleware'; 33465d3694SEvan Baconimport { 34465d3694SEvan Bacon createBundleUrlPath, 35465d3694SEvan Bacon resolveMainModuleName, 36465d3694SEvan Bacon shouldEnableAsyncImports, 37465d3694SEvan Bacon} from '../middleware/ManifestMiddleware'; 38fd055557SKudo Chienimport { ReactDevToolsPageMiddleware } from '../middleware/ReactDevToolsPageMiddleware'; 39fe427a9eSEric Samelsonimport { 40fe427a9eSEric Samelson DeepLinkHandler, 41fe427a9eSEric Samelson RuntimeRedirectMiddleware, 42fe427a9eSEric Samelson} from '../middleware/RuntimeRedirectMiddleware'; 436d6b81f9SEvan Baconimport { ServeStaticMiddleware } from '../middleware/ServeStaticMiddleware'; 44edeec536SEvan Baconimport { prependMiddleware } from '../middleware/mutations'; 4594b54ec3SEvan Baconimport { startTypescriptTypeGenerationAsync } from '../type-generation/startTypescriptTypeGeneration'; 4633643b60SEvan Bacon 4746f023faSEvan Baconexport class ForwardHtmlError extends CommandError { 488e209b4cSEvan Bacon constructor( 498e209b4cSEvan Bacon message: string, 508e209b4cSEvan Bacon public html: string, 518e209b4cSEvan Bacon public statusCode: number 528e209b4cSEvan Bacon ) { 5373eead7fSEvan Bacon super(message); 5473eead7fSEvan Bacon } 5573eead7fSEvan Bacon} 5673eead7fSEvan Bacon 5733643b60SEvan Baconconst debug = require('debug')('expo:start:server:metro') as typeof console.log; 588d307f52SEvan Bacon 598d307f52SEvan Bacon/** Default port to use for apps running in Expo Go. */ 6047d62600SKudo Chienconst EXPO_GO_METRO_PORT = 8081; 618d307f52SEvan Bacon 628d307f52SEvan Bacon/** Default port to use for apps that run in standard React Native projects or Expo Dev Clients. */ 638d307f52SEvan Baconconst DEV_CLIENT_METRO_PORT = 8081; 648d307f52SEvan Bacon 658d307f52SEvan Baconexport class MetroBundlerDevServer extends BundlerDevServer { 6633643b60SEvan Bacon private metro: import('metro').Server | null = null; 6733643b60SEvan Bacon 688d307f52SEvan Bacon get name(): string { 698d307f52SEvan Bacon return 'metro'; 708d307f52SEvan Bacon } 718d307f52SEvan Bacon 723d6e487dSEvan Bacon async resolvePortAsync(options: Partial<BundlerStartOptions> = {}): Promise<number> { 738d307f52SEvan Bacon const port = 748d307f52SEvan Bacon // If the manually defined port is busy then an error should be thrown... 758d307f52SEvan Bacon options.port ?? 768d307f52SEvan Bacon // Otherwise use the default port based on the runtime target. 778d307f52SEvan Bacon (options.devClient 788d307f52SEvan Bacon ? // Don't check if the port is busy if we're using the dev client since most clients are hardcoded to 8081. 798d307f52SEvan Bacon Number(process.env.RCT_METRO_PORT) || DEV_CLIENT_METRO_PORT 8047d62600SKudo Chien : // Otherwise (running in Expo Go) use a free port that falls back on the classic 8081 port. 818d307f52SEvan Bacon await getFreePortAsync(EXPO_GO_METRO_PORT)); 828d307f52SEvan Bacon 833d6e487dSEvan Bacon return port; 843d6e487dSEvan Bacon } 853d6e487dSEvan Bacon 8646f023faSEvan Bacon async getExpoRouterRoutesManifestAsync({ appDir }: { appDir: string }) { 8746f023faSEvan Bacon const manifest = await fetchManifest(this.projectRoot, { 8846f023faSEvan Bacon asJson: true, 8946f023faSEvan Bacon appDir, 9046f023faSEvan Bacon }); 9146f023faSEvan Bacon 9246f023faSEvan Bacon if (!manifest) { 9346f023faSEvan Bacon throw new CommandError( 9446f023faSEvan Bacon 'EXPO_ROUTER_SERVER_MANIFEST', 9546f023faSEvan Bacon 'Unexpected error: server manifest could not be fetched.' 9646f023faSEvan Bacon ); 9746f023faSEvan Bacon } 9846f023faSEvan Bacon 9946f023faSEvan Bacon return manifest; 10046f023faSEvan Bacon } 10146f023faSEvan Bacon 10246f023faSEvan Bacon async exportExpoRouterApiRoutesAsync({ 10346f023faSEvan Bacon mode, 10446f023faSEvan Bacon appDir, 10546f023faSEvan Bacon }: { 10646f023faSEvan Bacon mode: 'development' | 'production'; 10746f023faSEvan Bacon appDir: string; 10846f023faSEvan Bacon }) { 10946f023faSEvan Bacon return exportAllApiRoutesAsync(this.projectRoot, { 11046f023faSEvan Bacon mode, 11146f023faSEvan Bacon appDir, 11246f023faSEvan Bacon port: this.getInstance()?.location.port, 11346f023faSEvan Bacon shouldThrow: true, 11446f023faSEvan Bacon }); 11546f023faSEvan Bacon } 11646f023faSEvan Bacon 1179580591fSEvan Bacon async composeResourcesWithHtml({ 1189580591fSEvan Bacon mode, 1199580591fSEvan Bacon resources, 1209580591fSEvan Bacon template, 1219580591fSEvan Bacon devBundleUrl, 1227c98c357SEvan Bacon basePath, 1239580591fSEvan Bacon }: { 1249580591fSEvan Bacon mode: 'development' | 'production'; 1259580591fSEvan Bacon resources: SerialAsset[]; 1269580591fSEvan Bacon template: string; 1277c98c357SEvan Bacon /** asset prefix used for deploying to non-standard origins like GitHub pages. */ 1287c98c357SEvan Bacon basePath: string; 1299580591fSEvan Bacon devBundleUrl?: string; 130cf472be6SEvan Bacon }): Promise<string> { 131cf472be6SEvan Bacon if (!resources) { 132cf472be6SEvan Bacon return ''; 133cf472be6SEvan Bacon } 1349580591fSEvan Bacon const isDev = mode === 'development'; 1359580591fSEvan Bacon return htmlFromSerialAssets(resources, { 1369580591fSEvan Bacon dev: isDev, 1379580591fSEvan Bacon template, 1387c98c357SEvan Bacon basePath, 1399580591fSEvan Bacon bundleUrl: isDev ? devBundleUrl : undefined, 1409580591fSEvan Bacon }); 1419580591fSEvan Bacon } 1429580591fSEvan Bacon 1431a3d836eSEvan Bacon async getStaticRenderFunctionAsync({ 1441a3d836eSEvan Bacon mode, 1451a3d836eSEvan Bacon minify = mode !== 'development', 1461a3d836eSEvan Bacon }: { 1471a3d836eSEvan Bacon mode: 'development' | 'production'; 1481a3d836eSEvan Bacon minify?: boolean; 1491a3d836eSEvan Bacon }) { 1509580591fSEvan Bacon const url = this.getDevServerUrl()!; 1519580591fSEvan Bacon 1527179edeaSEvan Bacon const { getStaticContent, getManifest } = await getStaticRenderFunctions( 1537179edeaSEvan Bacon this.projectRoot, 1547179edeaSEvan Bacon url, 1557179edeaSEvan Bacon { 1561a3d836eSEvan Bacon minify, 1579580591fSEvan Bacon dev: mode !== 'production', 1589580591fSEvan Bacon // Ensure the API Routes are included 1599580591fSEvan Bacon environment: 'node', 1607179edeaSEvan Bacon } 1617179edeaSEvan Bacon ); 1627179edeaSEvan Bacon return { 1637179edeaSEvan Bacon // Get routes from Expo Router. 16446f023faSEvan Bacon manifest: await getManifest({ fetchData: true, preserveApiRoutes: false }), 1657179edeaSEvan Bacon // Get route generating function 1667179edeaSEvan Bacon async renderAsync(path: string) { 1679580591fSEvan Bacon return await getStaticContent(new URL(path, url)); 1687179edeaSEvan Bacon }, 1699580591fSEvan Bacon }; 1709580591fSEvan Bacon } 1719580591fSEvan Bacon 1721a3d836eSEvan Bacon async getStaticResourcesAsync({ 1731a3d836eSEvan Bacon mode, 1741a3d836eSEvan Bacon minify = mode !== 'development', 175573b0ea7SEvan Bacon includeMaps, 1761a3d836eSEvan Bacon }: { 1771a3d836eSEvan Bacon mode: string; 1781a3d836eSEvan Bacon minify?: boolean; 179573b0ea7SEvan Bacon includeMaps?: boolean; 1801a3d836eSEvan Bacon }): Promise<SerialAsset[]> { 1819580591fSEvan Bacon const devBundleUrlPathname = createBundleUrlPath({ 1829580591fSEvan Bacon platform: 'web', 1839580591fSEvan Bacon mode, 1841a3d836eSEvan Bacon minify, 1859580591fSEvan Bacon environment: 'client', 1861a3d836eSEvan Bacon serializerOutput: 'static', 187573b0ea7SEvan Bacon serializerIncludeMaps: includeMaps, 1889580591fSEvan Bacon mainModuleName: resolveMainModuleName(this.projectRoot, getConfig(this.projectRoot), 'web'), 189465d3694SEvan Bacon lazy: shouldEnableAsyncImports(this.projectRoot), 1909580591fSEvan Bacon }); 1919580591fSEvan Bacon 1929580591fSEvan Bacon const bundleUrl = new URL(devBundleUrlPathname, this.getDevServerUrl()!); 1939580591fSEvan Bacon 1949580591fSEvan Bacon // Fetch the generated HTML from our custom Metro serializer 1959580591fSEvan Bacon const results = await fetch(bundleUrl.toString()); 1969580591fSEvan Bacon 1979580591fSEvan Bacon const txt = await results.text(); 1989580591fSEvan Bacon 19973eead7fSEvan Bacon // console.log('STAT:', results.status, results.statusText); 200cf472be6SEvan Bacon let data: any; 2019580591fSEvan Bacon try { 202cf472be6SEvan Bacon data = JSON.parse(txt); 2039580591fSEvan Bacon } catch (error: any) { 20473eead7fSEvan Bacon debug(txt); 20573eead7fSEvan Bacon 20673eead7fSEvan Bacon // Metro can throw this error when the initial module id cannot be resolved. 20773eead7fSEvan Bacon if (!results.ok && txt.startsWith('<!DOCTYPE html>')) { 20873eead7fSEvan Bacon throw new ForwardHtmlError( 20973eead7fSEvan Bacon `Metro failed to bundle the project. Check the console for more information.`, 21073eead7fSEvan Bacon txt, 21173eead7fSEvan Bacon results.status 21273eead7fSEvan Bacon ); 21373eead7fSEvan Bacon } 21473eead7fSEvan Bacon 2151a3d836eSEvan Bacon Log.error( 2161a3d836eSEvan Bacon 'Failed to generate resources with Metro, the Metro config may not be using the correct serializer. Ensure the metro.config.js is extending the expo/metro-config and is not overriding the serializer.' 2171a3d836eSEvan Bacon ); 2189580591fSEvan Bacon throw error; 2199580591fSEvan Bacon } 220cf472be6SEvan Bacon 221cf472be6SEvan Bacon // NOTE: This could potentially need more validation in the future. 222cf472be6SEvan Bacon if (Array.isArray(data)) { 223cf472be6SEvan Bacon return data; 224cf472be6SEvan Bacon } 225cf472be6SEvan Bacon 226cf472be6SEvan Bacon if (data != null && (data.errors || data.type?.match(/.*Error$/))) { 227cf472be6SEvan Bacon // { 228cf472be6SEvan Bacon // type: 'InternalError', 229cf472be6SEvan Bacon // errors: [], 230cf472be6SEvan Bacon // message: 'Metro has encountered an error: While trying to resolve module `stylis` from file `/Users/evanbacon/Documents/GitHub/lab/emotion-error-test/node_modules/@emotion/cache/dist/emotion-cache.browser.esm.js`, the package `/Users/evanbacon/Documents/GitHub/lab/emotion-error-test/node_modules/stylis/package.json` was successfully found. However, this package itself specifies a `main` module field that could not be resolved (`/Users/evanbacon/Documents/GitHub/lab/emotion-error-test/node_modules/stylis/dist/stylis.mjs`. Indeed, none of these files exist:\n' + 231cf472be6SEvan Bacon // '\n' + 232cf472be6SEvan Bacon // ' * /Users/evanbacon/Documents/GitHub/lab/emotion-error-test/node_modules/stylis/dist/stylis.mjs(.web.ts|.ts|.web.tsx|.tsx|.web.js|.js|.web.jsx|.jsx|.web.json|.json|.web.cjs|.cjs|.web.scss|.scss|.web.sass|.sass|.web.css|.css)\n' + 233cf472be6SEvan Bacon // ' * /Users/evanbacon/Documents/GitHub/lab/emotion-error-test/node_modules/stylis/dist/stylis.mjs/index(.web.ts|.ts|.web.tsx|.tsx|.web.js|.js|.web.jsx|.jsx|.web.json|.json|.web.cjs|.cjs|.web.scss|.scss|.web.sass|.sass|.web.css|.css): /Users/evanbacon/Documents/GitHub/lab/emotion-error-test/node_modules/metro/src/node-haste/DependencyGraph.js (289:17)\n' + 234cf472be6SEvan Bacon // '\n' + 235cf472be6SEvan Bacon // '\x1B[0m \x1B[90m 287 |\x1B[39m }\x1B[0m\n' + 236cf472be6SEvan Bacon // '\x1B[0m \x1B[90m 288 |\x1B[39m \x1B[36mif\x1B[39m (error \x1B[36minstanceof\x1B[39m \x1B[33mInvalidPackageError\x1B[39m) {\x1B[0m\n' + 237cf472be6SEvan Bacon // '\x1B[0m\x1B[31m\x1B[1m>\x1B[22m\x1B[39m\x1B[90m 289 |\x1B[39m \x1B[36mthrow\x1B[39m \x1B[36mnew\x1B[39m \x1B[33mPackageResolutionError\x1B[39m({\x1B[0m\n' + 238cf472be6SEvan Bacon // '\x1B[0m \x1B[90m |\x1B[39m \x1B[31m\x1B[1m^\x1B[22m\x1B[39m\x1B[0m\n' + 239cf472be6SEvan Bacon // '\x1B[0m \x1B[90m 290 |\x1B[39m packageError\x1B[33m:\x1B[39m error\x1B[33m,\x1B[39m\x1B[0m\n' + 240cf472be6SEvan Bacon // '\x1B[0m \x1B[90m 291 |\x1B[39m originModulePath\x1B[33m:\x1B[39m \x1B[36mfrom\x1B[39m\x1B[33m,\x1B[39m\x1B[0m\n' + 241cf472be6SEvan Bacon // '\x1B[0m \x1B[90m 292 |\x1B[39m targetModuleName\x1B[33m:\x1B[39m to\x1B[33m,\x1B[39m\x1B[0m' 242cf472be6SEvan Bacon // } 243cf472be6SEvan Bacon // The Metro logger already showed this error. 244cf472be6SEvan Bacon throw new Error(data.message); 245cf472be6SEvan Bacon } 246cf472be6SEvan Bacon 247cf472be6SEvan Bacon throw new Error( 248cf472be6SEvan Bacon 'Invalid resources returned from the Metro serializer. Expected array, found: ' + data 249cf472be6SEvan Bacon ); 2509580591fSEvan Bacon } 2519580591fSEvan Bacon 2520a6ddb20SEvan Bacon async getStaticPageAsync( 2530a6ddb20SEvan Bacon pathname: string, 2540a6ddb20SEvan Bacon { 2550a6ddb20SEvan Bacon mode, 2561a3d836eSEvan Bacon minify = mode !== 'development', 2577c98c357SEvan Bacon basePath, 2580a6ddb20SEvan Bacon }: { 2590a6ddb20SEvan Bacon mode: 'development' | 'production'; 2601a3d836eSEvan Bacon minify?: boolean; 2617c98c357SEvan Bacon basePath: string; 2620a6ddb20SEvan Bacon } 2630a6ddb20SEvan Bacon ) { 2649580591fSEvan Bacon const devBundleUrlPathname = createBundleUrlPath({ 2659580591fSEvan Bacon platform: 'web', 2669580591fSEvan Bacon mode, 2679580591fSEvan Bacon environment: 'client', 2689580591fSEvan Bacon mainModuleName: resolveMainModuleName(this.projectRoot, getConfig(this.projectRoot), 'web'), 269465d3694SEvan Bacon lazy: shouldEnableAsyncImports(this.projectRoot), 2709580591fSEvan Bacon }); 2710a6ddb20SEvan Bacon 2729580591fSEvan Bacon const bundleStaticHtml = async (): Promise<string> => { 2739580591fSEvan Bacon const { getStaticContent } = await getStaticRenderFunctions( 2749580591fSEvan Bacon this.projectRoot, 2759580591fSEvan Bacon this.getDevServerUrl()!, 2769580591fSEvan Bacon { 2771a3d836eSEvan Bacon minify: false, 2780a6ddb20SEvan Bacon dev: mode !== 'production', 27957eba0f9SEvan Bacon // Ensure the API Routes are included 28057eba0f9SEvan Bacon environment: 'node', 2819580591fSEvan Bacon } 2829580591fSEvan Bacon ); 2830a6ddb20SEvan Bacon 2849580591fSEvan Bacon const location = new URL(pathname, this.getDevServerUrl()!); 2859580591fSEvan Bacon return await getStaticContent(location); 2869580591fSEvan Bacon }; 2879580591fSEvan Bacon 2881a3d836eSEvan Bacon const [resources, staticHtml] = await Promise.all([ 2891a3d836eSEvan Bacon this.getStaticResourcesAsync({ mode, minify }), 2901a3d836eSEvan Bacon bundleStaticHtml(), 2911a3d836eSEvan Bacon ]); 2929580591fSEvan Bacon const content = await this.composeResourcesWithHtml({ 2939580591fSEvan Bacon mode, 2949580591fSEvan Bacon resources, 2959580591fSEvan Bacon template: staticHtml, 2969580591fSEvan Bacon devBundleUrl: devBundleUrlPathname, 2977c98c357SEvan Bacon basePath, 2989580591fSEvan Bacon }); 2999580591fSEvan Bacon return { 3009580591fSEvan Bacon content, 3019580591fSEvan Bacon resources, 3029580591fSEvan Bacon }; 3030a6ddb20SEvan Bacon } 3040a6ddb20SEvan Bacon 3056a750d06SEvan Bacon async watchEnvironmentVariables() { 3066a750d06SEvan Bacon if (!this.instance) { 3076a750d06SEvan Bacon throw new Error( 3086a750d06SEvan Bacon 'Cannot observe environment variable changes without a running Metro instance.' 3096a750d06SEvan Bacon ); 3106a750d06SEvan Bacon } 3116a750d06SEvan Bacon if (!this.metro) { 3126a750d06SEvan Bacon // This can happen when the run command is used and the server is already running in another 3136a750d06SEvan Bacon // process. 3146a750d06SEvan Bacon debug('Skipping Environment Variable observation because Metro is not running (headless).'); 3156a750d06SEvan Bacon return; 3166a750d06SEvan Bacon } 3176a750d06SEvan Bacon 3186a750d06SEvan Bacon const envFiles = runtimeEnv 3196a750d06SEvan Bacon .getFiles(process.env.NODE_ENV) 3206a750d06SEvan Bacon .map((fileName) => path.join(this.projectRoot, fileName)); 3216a750d06SEvan Bacon 3226a750d06SEvan Bacon observeFileChanges( 3236a750d06SEvan Bacon { 3246a750d06SEvan Bacon metro: this.metro, 3256a750d06SEvan Bacon server: this.instance.server, 3266a750d06SEvan Bacon }, 3276a750d06SEvan Bacon envFiles, 3286a750d06SEvan Bacon () => { 3296a750d06SEvan Bacon debug('Reloading environment variables...'); 3306a750d06SEvan Bacon // Force reload the environment variables. 3316a750d06SEvan Bacon runtimeEnv.load(this.projectRoot, { force: true }); 3326a750d06SEvan Bacon } 3336a750d06SEvan Bacon ); 3346a750d06SEvan Bacon } 3356a750d06SEvan Bacon 3363d6e487dSEvan Bacon protected async startImplementationAsync( 3373d6e487dSEvan Bacon options: BundlerStartOptions 3383d6e487dSEvan Bacon ): Promise<DevServerInstance> { 3393d6e487dSEvan Bacon options.port = await this.resolvePortAsync(options); 3403d6e487dSEvan Bacon this.urlCreator = this.getUrlCreator(options); 3418d307f52SEvan Bacon 3428d307f52SEvan Bacon const parsedOptions = { 3433d6e487dSEvan Bacon port: options.port, 3448d307f52SEvan Bacon maxWorkers: options.maxWorkers, 3458d307f52SEvan Bacon resetCache: options.resetDevServer, 3468d307f52SEvan Bacon 3478d307f52SEvan Bacon // Use the unversioned metro config. 3488d307f52SEvan Bacon // TODO: Deprecate this property when expo-cli goes away. 3498d307f52SEvan Bacon unversioned: false, 3508d307f52SEvan Bacon }; 3518d307f52SEvan Bacon 35224228e75SEvan Bacon // Required for symbolication: 35324228e75SEvan Bacon process.env.EXPO_DEV_SERVER_ORIGIN = `http://localhost:${options.port}`; 35424228e75SEvan Bacon 35533643b60SEvan Bacon const { metro, server, middleware, messageSocket } = await instantiateMetroAsync( 35603d43e7dSCedric van Putten this, 357429dc7fcSEvan Bacon parsedOptions, 358429dc7fcSEvan Bacon { 359429dc7fcSEvan Bacon isExporting: !!options.isExporting, 360429dc7fcSEvan Bacon } 3618d307f52SEvan Bacon ); 3628d307f52SEvan Bacon 3638d307f52SEvan Bacon const manifestMiddleware = await this.getManifestMiddlewareAsync(options); 3648d307f52SEvan Bacon 3652e1e108bSEvan Bacon // Important that we noop source maps for context modules as soon as possible. 3662e1e108bSEvan Bacon prependMiddleware(middleware, new ContextModuleSourceMapsMiddleware().getHandler()); 3672e1e108bSEvan Bacon 3688d307f52SEvan Bacon // We need the manifest handler to be the first middleware to run so our 3698d307f52SEvan Bacon // routes take precedence over static files. For example, the manifest is 3708d307f52SEvan Bacon // served from '/' and if the user has an index.html file in their project 3718d307f52SEvan Bacon // then the manifest handler will never run, the static middleware will run 3728d307f52SEvan Bacon // and serve index.html instead of the manifest. 3738d307f52SEvan Bacon // https://github.com/expo/expo/issues/13114 3740a6ddb20SEvan Bacon prependMiddleware(middleware, manifestMiddleware.getHandler()); 3758d307f52SEvan Bacon 376212e3a1aSEric Samelson middleware.use( 377212e3a1aSEric Samelson new InterstitialPageMiddleware(this.projectRoot, { 378212e3a1aSEric Samelson // TODO: Prevent this from becoming stale. 379212e3a1aSEric Samelson scheme: options.location.scheme ?? null, 380212e3a1aSEric Samelson }).getHandler() 381212e3a1aSEric Samelson ); 382fd055557SKudo Chien middleware.use(new ReactDevToolsPageMiddleware(this.projectRoot).getHandler()); 3838d307f52SEvan Bacon 3848d307f52SEvan Bacon const deepLinkMiddleware = new RuntimeRedirectMiddleware(this.projectRoot, { 385fe427a9eSEric Samelson onDeepLink: getDeepLinkHandler(this.projectRoot), 3868d307f52SEvan Bacon getLocation: ({ runtime }) => { 3878d307f52SEvan Bacon if (runtime === 'custom') { 38829975bfdSEvan Bacon return this.urlCreator?.constructDevClientUrl(); 3898d307f52SEvan Bacon } else { 39029975bfdSEvan Bacon return this.urlCreator?.constructUrl({ 3918d307f52SEvan Bacon scheme: 'exp', 3928d307f52SEvan Bacon }); 3938d307f52SEvan Bacon } 3948d307f52SEvan Bacon }, 3958d307f52SEvan Bacon }); 3968d307f52SEvan Bacon middleware.use(deepLinkMiddleware.getHandler()); 3978d307f52SEvan Bacon 398e87e8ea8SEvan Bacon middleware.use(new CreateFileMiddleware(this.projectRoot).getHandler()); 399e87e8ea8SEvan Bacon 4006d6b81f9SEvan Bacon // Append support for redirecting unhandled requests to the index.html page on web. 4016d6b81f9SEvan Bacon if (this.isTargetingWeb()) { 4029580591fSEvan Bacon const { exp } = getConfig(this.projectRoot, { skipSDKVersionRequirement: true }); 40346f023faSEvan Bacon const useServerRendering = ['static', 'server'].includes(exp.web?.output ?? ''); 4049580591fSEvan Bacon 4056d6b81f9SEvan Bacon // This MUST be after the manifest middleware so it doesn't have a chance to serve the template `public/index.html`. 4066d6b81f9SEvan Bacon middleware.use(new ServeStaticMiddleware(this.projectRoot).getHandler()); 4076d6b81f9SEvan Bacon 40842637653SEvan Bacon // This should come after the static middleware so it doesn't serve the favicon from `public/favicon.ico`. 40942637653SEvan Bacon middleware.use(new FaviconMiddleware(this.projectRoot).getHandler()); 41042637653SEvan Bacon 4112d4e7de9SEvan Bacon if (useServerRendering) { 41246f023faSEvan Bacon const appDir = getRouterDirectoryWithManifest(this.projectRoot, exp); 41346f023faSEvan Bacon middleware.use( 41446f023faSEvan Bacon createRouteHandlerMiddleware(this.projectRoot, { 41546f023faSEvan Bacon ...options, 41646f023faSEvan Bacon appDir, 41746f023faSEvan Bacon getWebBundleUrl: manifestMiddleware.getWebBundleUrl.bind(manifestMiddleware), 41846f023faSEvan Bacon getStaticPageAsync: (pathname) => { 41946f023faSEvan Bacon return this.getStaticPageAsync(pathname, { 4209580591fSEvan Bacon mode: options.mode ?? 'development', 42146f023faSEvan Bacon minify: options.minify, 42246f023faSEvan Bacon // No base path in development 4237c98c357SEvan Bacon basePath: '', 4249580591fSEvan Bacon }); 42546f023faSEvan Bacon }, 42646f023faSEvan Bacon }) 427e24c47a6SEvan Bacon ); 42846f023faSEvan Bacon 42946f023faSEvan Bacon // @ts-expect-error: TODO 43046f023faSEvan Bacon if (exp.web?.output === 'server') { 43146f023faSEvan Bacon // Cache observation for API Routes... 43246f023faSEvan Bacon observeApiRouteChanges( 43346f023faSEvan Bacon this.projectRoot, 43446f023faSEvan Bacon { 43546f023faSEvan Bacon metro, 43646f023faSEvan Bacon server, 43746f023faSEvan Bacon }, 43846f023faSEvan Bacon async (filepath, op) => { 43946f023faSEvan Bacon if (isApiRouteConvention(filepath)) { 44046f023faSEvan Bacon debug(`[expo-cli] ${op} ${filepath}`); 44146f023faSEvan Bacon if (op === 'change' || op === 'add') { 44246f023faSEvan Bacon rebundleApiRoute(this.projectRoot, filepath, { 44346f023faSEvan Bacon ...options, 44446f023faSEvan Bacon appDir, 4450a6ddb20SEvan Bacon }); 4460a6ddb20SEvan Bacon } 4470a6ddb20SEvan Bacon 44846f023faSEvan Bacon if (op === 'delete') { 44946f023faSEvan Bacon // TODO: Cancel the bundling of the deleted route. 45046f023faSEvan Bacon } 45146f023faSEvan Bacon } 45246f023faSEvan Bacon } 45346f023faSEvan Bacon ); 45446f023faSEvan Bacon } 4552d4e7de9SEvan Bacon } else { 4566d6b81f9SEvan Bacon // This MUST run last since it's the fallback. 4570a6ddb20SEvan Bacon middleware.use( 4580a6ddb20SEvan Bacon new HistoryFallbackMiddleware(manifestMiddleware.getHandler().internal).getHandler() 4590a6ddb20SEvan Bacon ); 4600a6ddb20SEvan Bacon } 4616d6b81f9SEvan Bacon } 4628d307f52SEvan Bacon // Extend the close method to ensure that we clean up the local info. 4638d307f52SEvan Bacon const originalClose = server.close.bind(server); 4648d307f52SEvan Bacon 4658d307f52SEvan Bacon server.close = (callback?: (err?: Error) => void) => { 4668d307f52SEvan Bacon return originalClose((err?: Error) => { 4678d307f52SEvan Bacon this.instance = null; 46833643b60SEvan Bacon this.metro = null; 4698d307f52SEvan Bacon callback?.(err); 4708d307f52SEvan Bacon }); 4718d307f52SEvan Bacon }; 4728d307f52SEvan Bacon 47333643b60SEvan Bacon this.metro = metro; 4743d6e487dSEvan Bacon return { 4758d307f52SEvan Bacon server, 4768d307f52SEvan Bacon location: { 4778d307f52SEvan Bacon // The port is the main thing we want to send back. 4783d6e487dSEvan Bacon port: options.port, 4798d307f52SEvan Bacon // localhost isn't always correct. 4808d307f52SEvan Bacon host: 'localhost', 4818d307f52SEvan Bacon // http is the only supported protocol on native. 4823d6e487dSEvan Bacon url: `http://localhost:${options.port}`, 4838d307f52SEvan Bacon protocol: 'http', 4848d307f52SEvan Bacon }, 4858d307f52SEvan Bacon middleware, 4868d307f52SEvan Bacon messageSocket, 4873d6e487dSEvan Bacon }; 4888d307f52SEvan Bacon } 4898d307f52SEvan Bacon 4901117330aSMark Lawlor public async waitForTypeScriptAsync(): Promise<boolean> { 49133643b60SEvan Bacon if (!this.instance) { 49233643b60SEvan Bacon throw new Error('Cannot wait for TypeScript without a running server.'); 49333643b60SEvan Bacon } 4941117330aSMark Lawlor 4951117330aSMark Lawlor return new Promise<boolean>((resolve) => { 49633643b60SEvan Bacon if (!this.metro) { 49733643b60SEvan Bacon // This can happen when the run command is used and the server is already running in another 49833643b60SEvan Bacon // process. In this case we can't wait for the TypeScript check to complete because we don't 49933643b60SEvan Bacon // have access to the Metro server. 50033643b60SEvan Bacon debug('Skipping TypeScript check because Metro is not running (headless).'); 5011117330aSMark Lawlor return resolve(false); 50233643b60SEvan Bacon } 50333643b60SEvan Bacon 5041117330aSMark Lawlor const off = metroWatchTypeScriptFiles({ 5051117330aSMark Lawlor projectRoot: this.projectRoot, 5061117330aSMark Lawlor server: this.instance!.server, 5071117330aSMark Lawlor metro: this.metro, 5081117330aSMark Lawlor tsconfig: true, 5091117330aSMark Lawlor throttle: true, 5101117330aSMark Lawlor eventTypes: ['change', 'add'], 5111117330aSMark Lawlor callback: async () => { 51233643b60SEvan Bacon // Run once, this prevents the TypeScript project prerequisite from running on every file change. 51333643b60SEvan Bacon off(); 51433643b60SEvan Bacon const { TypeScriptProjectPrerequisite } = await import( 515*1a3a1db5SEvan Bacon '../../doctor/typescript/TypeScriptProjectPrerequisite.js' 51633643b60SEvan Bacon ); 51733643b60SEvan Bacon 51833643b60SEvan Bacon try { 51933643b60SEvan Bacon const req = new TypeScriptProjectPrerequisite(this.projectRoot); 52033643b60SEvan Bacon await req.bootstrapAsync(); 5211117330aSMark Lawlor resolve(true); 52233643b60SEvan Bacon } catch (error: any) { 52333643b60SEvan Bacon // Ensure the process doesn't fail if the TypeScript check fails. 52433643b60SEvan Bacon // This could happen during the install. 52533643b60SEvan Bacon Log.log(); 52633643b60SEvan Bacon Log.error( 52733643b60SEvan Bacon chalk.red`Failed to automatically setup TypeScript for your project. Try restarting the dev server to fix.` 52833643b60SEvan Bacon ); 52933643b60SEvan Bacon Log.exception(error); 5301117330aSMark Lawlor resolve(false); 53133643b60SEvan Bacon } 5321117330aSMark Lawlor }, 5331117330aSMark Lawlor }); 5341117330aSMark Lawlor }); 53533643b60SEvan Bacon } 5361117330aSMark Lawlor 5371117330aSMark Lawlor public async startTypeScriptServices() { 53887669a95SMark Lawlor return startTypescriptTypeGenerationAsync({ 53987669a95SMark Lawlor server: this.instance?.server, 5401117330aSMark Lawlor metro: this.metro, 5411117330aSMark Lawlor projectRoot: this.projectRoot, 5421117330aSMark Lawlor }); 54333643b60SEvan Bacon } 54433643b60SEvan Bacon 5458d307f52SEvan Bacon protected getConfigModuleIds(): string[] { 5468d307f52SEvan Bacon return ['./metro.config.js', './metro.config.json', './rn-cli.config.js']; 5478d307f52SEvan Bacon } 5488d307f52SEvan Bacon} 549fe427a9eSEric Samelson 550fe427a9eSEric Samelsonexport function getDeepLinkHandler(projectRoot: string): DeepLinkHandler { 551fe427a9eSEric Samelson return async ({ runtime }) => { 552fe427a9eSEric Samelson if (runtime === 'expo') return; 553fe427a9eSEric Samelson const { exp } = getConfig(projectRoot); 554fe427a9eSEric Samelson await logEventAsync('dev client start command', { 555fe427a9eSEric Samelson status: 'started', 556fe427a9eSEric Samelson ...getDevClientProperties(projectRoot, exp), 557fe427a9eSEric Samelson }); 558fe427a9eSEric Samelson }; 559fe427a9eSEric Samelson} 5609580591fSEvan Bacon 5619580591fSEvan Baconfunction htmlFromSerialAssets( 5629580591fSEvan Bacon assets: SerialAsset[], 5637c98c357SEvan Bacon { 5647c98c357SEvan Bacon dev, 5657c98c357SEvan Bacon template, 5667c98c357SEvan Bacon basePath, 5677c98c357SEvan Bacon bundleUrl, 5687c98c357SEvan Bacon }: { 5697c98c357SEvan Bacon dev: boolean; 5707c98c357SEvan Bacon template: string; 5717c98c357SEvan Bacon basePath: string; 5727c98c357SEvan Bacon /** This is dev-only. */ 5737c98c357SEvan Bacon bundleUrl?: string; 5747c98c357SEvan Bacon } 5759580591fSEvan Bacon) { 5769580591fSEvan Bacon // Combine the CSS modules into tags that have hot refresh data attributes. 5779580591fSEvan Bacon const styleString = assets 5789580591fSEvan Bacon .filter((asset) => asset.type === 'css') 5799580591fSEvan Bacon .map(({ metadata, filename, source }) => { 5809580591fSEvan Bacon if (dev) { 5819580591fSEvan Bacon return `<style data-expo-css-hmr="${metadata.hmrId}">` + source + '\n</style>'; 5829580591fSEvan Bacon } else { 5839580591fSEvan Bacon return [ 5847c98c357SEvan Bacon `<link rel="preload" href="${basePath}/${filename}" as="style">`, 5857c98c357SEvan Bacon `<link rel="stylesheet" href="${basePath}/${filename}">`, 5869580591fSEvan Bacon ].join(''); 5879580591fSEvan Bacon } 5889580591fSEvan Bacon }) 5899580591fSEvan Bacon .join(''); 5909580591fSEvan Bacon 5919580591fSEvan Bacon const jsAssets = assets.filter((asset) => asset.type === 'js'); 5929580591fSEvan Bacon 5939580591fSEvan Bacon const scripts = bundleUrl 5949580591fSEvan Bacon ? `<script src="${bundleUrl}" defer></script>` 5959580591fSEvan Bacon : jsAssets 5969580591fSEvan Bacon .map(({ filename }) => { 5977c98c357SEvan Bacon return `<script src="${basePath}/${filename}" defer></script>`; 5989580591fSEvan Bacon }) 5999580591fSEvan Bacon .join(''); 6009580591fSEvan Bacon 6019580591fSEvan Bacon return template 6029580591fSEvan Bacon .replace('</head>', `${styleString}</head>`) 6039580591fSEvan Bacon .replace('</body>', `${scripts}\n</body>`); 6049580591fSEvan Bacon} 605