1*46f023faSEvan Baconimport { 2*46f023faSEvan Bacon AbortController, 3*46f023faSEvan Bacon Headers, 4*46f023faSEvan Bacon RequestInit, 5*46f023faSEvan Bacon Response, 6*46f023faSEvan Bacon writeReadableStreamToWritable, 7*46f023faSEvan Bacon} from '@remix-run/node'; 8*46f023faSEvan Baconimport type * as express from 'express'; 9*46f023faSEvan Bacon 10*46f023faSEvan Baconimport { createRequestHandler as createExpoHandler } from '..'; 11*46f023faSEvan Baconimport { ExpoRequest } from '../environment'; 12*46f023faSEvan Bacon 13*46f023faSEvan Baconexport type RequestHandler = ( 14*46f023faSEvan Bacon req: express.Request, 15*46f023faSEvan Bacon res: express.Response, 16*46f023faSEvan Bacon next: express.NextFunction 17*46f023faSEvan Bacon) => Promise<void>; 18*46f023faSEvan Bacon 19*46f023faSEvan Bacon/** 20*46f023faSEvan Bacon * Returns a request handler for Express that serves the response using Remix. 21*46f023faSEvan Bacon */ 22*46f023faSEvan Baconexport function createRequestHandler( 23*46f023faSEvan Bacon { build }: { build: string }, 24*46f023faSEvan Bacon setup?: Parameters<typeof createExpoHandler>[1] 25*46f023faSEvan Bacon): RequestHandler { 26*46f023faSEvan Bacon const handleRequest = createExpoHandler(build, setup); 27*46f023faSEvan Bacon 28*46f023faSEvan Bacon return async (req: express.Request, res: express.Response, next: express.NextFunction) => { 29*46f023faSEvan Bacon if (!req?.url || !req.method) { 30*46f023faSEvan Bacon return next(); 31*46f023faSEvan Bacon } 32*46f023faSEvan Bacon try { 33*46f023faSEvan Bacon const request = convertRequest(req, res); 34*46f023faSEvan Bacon 35*46f023faSEvan Bacon const response = await handleRequest(request); 36*46f023faSEvan Bacon 37*46f023faSEvan Bacon await respond(res, response); 38*46f023faSEvan Bacon } catch (error: unknown) { 39*46f023faSEvan Bacon // Express doesn't support async functions, so we have to pass along the 40*46f023faSEvan Bacon // error manually using next(). 41*46f023faSEvan Bacon next(error); 42*46f023faSEvan Bacon } 43*46f023faSEvan Bacon }; 44*46f023faSEvan Bacon} 45*46f023faSEvan Bacon 46*46f023faSEvan Baconexport function convertHeaders(requestHeaders: express.Request['headers']): Headers { 47*46f023faSEvan Bacon const headers = new Headers(); 48*46f023faSEvan Bacon 49*46f023faSEvan Bacon for (const [key, values] of Object.entries(requestHeaders)) { 50*46f023faSEvan Bacon if (values) { 51*46f023faSEvan Bacon if (Array.isArray(values)) { 52*46f023faSEvan Bacon for (const value of values) { 53*46f023faSEvan Bacon headers.append(key, value); 54*46f023faSEvan Bacon } 55*46f023faSEvan Bacon } else { 56*46f023faSEvan Bacon headers.set(key, values); 57*46f023faSEvan Bacon } 58*46f023faSEvan Bacon } 59*46f023faSEvan Bacon } 60*46f023faSEvan Bacon 61*46f023faSEvan Bacon return headers; 62*46f023faSEvan Bacon} 63*46f023faSEvan Bacon 64*46f023faSEvan Baconexport function convertRequest(req: express.Request, res: express.Response): ExpoRequest { 65*46f023faSEvan Bacon const url = new URL(`${req.protocol}://${req.get('host')}${req.url}`); 66*46f023faSEvan Bacon 67*46f023faSEvan Bacon // Abort action/loaders once we can no longer write a response 68*46f023faSEvan Bacon const controller = new AbortController(); 69*46f023faSEvan Bacon res.on('close', () => controller.abort()); 70*46f023faSEvan Bacon 71*46f023faSEvan Bacon const init: RequestInit = { 72*46f023faSEvan Bacon method: req.method, 73*46f023faSEvan Bacon headers: convertHeaders(req.headers), 74*46f023faSEvan Bacon // Cast until reason/throwIfAborted added 75*46f023faSEvan Bacon // https://github.com/mysticatea/abort-controller/issues/36 76*46f023faSEvan Bacon signal: controller.signal as RequestInit['signal'], 77*46f023faSEvan Bacon }; 78*46f023faSEvan Bacon 79*46f023faSEvan Bacon if (req.method !== 'GET' && req.method !== 'HEAD') { 80*46f023faSEvan Bacon init.body = req; 81*46f023faSEvan Bacon } 82*46f023faSEvan Bacon 83*46f023faSEvan Bacon return new ExpoRequest(url.href, init); 84*46f023faSEvan Bacon} 85*46f023faSEvan Bacon 86*46f023faSEvan Baconexport async function respond(res: express.Response, expoRes: Response): Promise<void> { 87*46f023faSEvan Bacon res.statusMessage = expoRes.statusText; 88*46f023faSEvan Bacon res.status(expoRes.status); 89*46f023faSEvan Bacon 90*46f023faSEvan Bacon for (const [key, values] of Object.entries(expoRes.headers.raw())) { 91*46f023faSEvan Bacon for (const value of values) { 92*46f023faSEvan Bacon res.append(key, value); 93*46f023faSEvan Bacon } 94*46f023faSEvan Bacon } 95*46f023faSEvan Bacon 96*46f023faSEvan Bacon if (expoRes.body) { 97*46f023faSEvan Bacon await writeReadableStreamToWritable(expoRes.body, res); 98*46f023faSEvan Bacon } else { 99*46f023faSEvan Bacon res.end(); 100*46f023faSEvan Bacon } 101*46f023faSEvan Bacon} 102