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