1/** 2 * Copyright © 2022 650 Industries. 3 * 4 * This source code is licensed under the MIT license found in the 5 * LICENSE file in the root directory of this source tree. 6 */ 7import { ExpoResponse } from '@expo/server'; 8import { createRequestHandler } from '@expo/server/build/vendor/http'; 9import requireString from 'require-from-string'; 10import resolve from 'resolve'; 11import { promisify } from 'util'; 12 13import { ForwardHtmlError } from './MetroBundlerDevServer'; 14import { bundleApiRoute } from './bundleApiRoutes'; 15import { fetchManifest } from './fetchRouterManifest'; 16import { getErrorOverlayHtmlAsync, logMetroError, logMetroErrorAsync } from './metroErrorInterface'; 17import { Log } from '../../../log'; 18 19const debug = require('debug')('expo:start:server:metro') as typeof console.log; 20 21const resolveAsync = promisify(resolve) as any as ( 22 id: string, 23 opts: resolve.AsyncOpts 24) => Promise<string | null>; 25 26export function createRouteHandlerMiddleware( 27 projectRoot: string, 28 options: { 29 mode?: string; 30 appDir: string; 31 port?: number; 32 getWebBundleUrl: () => string; 33 getStaticPageAsync: (pathname: string) => Promise<{ content: string }>; 34 } 35) { 36 return createRequestHandler( 37 { build: '' }, 38 { 39 async getRoutesManifest() { 40 const manifest = await fetchManifest<RegExp>(projectRoot, options); 41 debug('manifest', manifest); 42 // NOTE: no app dir if null 43 // TODO: Redirect to 404 page 44 return manifest; 45 }, 46 async getHtml(request) { 47 try { 48 const { content } = await options.getStaticPageAsync(request.url); 49 return content; 50 } catch (error: any) { 51 // Forward the Metro server response as-is. It won't be pretty, but at least it will be accurate. 52 if (error instanceof ForwardHtmlError) { 53 return new ExpoResponse(error.html, { 54 status: error.statusCode, 55 headers: { 56 'Content-Type': 'text/html', 57 }, 58 }); 59 } 60 61 try { 62 return new ExpoResponse( 63 await getErrorOverlayHtmlAsync({ 64 error, 65 projectRoot, 66 }), 67 { 68 status: 500, 69 headers: { 70 'Content-Type': 'text/html', 71 }, 72 } 73 ); 74 } catch (staticError: any) { 75 // Fallback error for when Expo Router is misconfigured in the project. 76 return new ExpoResponse( 77 '<span><h3>Internal Error:</h3><b>Project is not setup correctly for static rendering (check terminal for more info):</b><br/>' + 78 error.message + 79 '<br/><br/>' + 80 staticError.message + 81 '</span>', 82 { 83 status: 500, 84 headers: { 85 'Content-Type': 'text/html', 86 }, 87 } 88 ); 89 } 90 } 91 }, 92 logApiRouteExecutionError(error) { 93 logMetroError(projectRoot, { error }); 94 }, 95 async getApiRoute(route) { 96 const resolvedFunctionPath = await resolveAsync(route.page, { 97 extensions: ['.js', '.jsx', '.ts', '.tsx'], 98 basedir: options.appDir, 99 }); 100 101 const middlewareContents = await bundleApiRoute( 102 projectRoot, 103 resolvedFunctionPath!, 104 options 105 ); 106 if (!middlewareContents) { 107 // TODO: Error handling 108 return null; 109 } 110 111 try { 112 debug(`Bundling middleware at: ${resolvedFunctionPath}`); 113 return requireString(middlewareContents); 114 } catch (error: any) { 115 if (error instanceof Error) { 116 await logMetroErrorAsync({ projectRoot, error }); 117 } else { 118 Log.error('Failed to load middleware: ' + error); 119 } 120 return new ExpoResponse( 121 'Failed to load middleware: ' + resolvedFunctionPath + '\n\n' + error.message, 122 { 123 status: 500, 124 headers: { 125 'Content-Type': 'text/html', 126 }, 127 } 128 ); 129 } 130 }, 131 } 132 ); 133} 134