UNPKG

@universal-middleware/sirv

Version:
361 lines (358 loc) 11.5 kB
import { bindUniversal, contextSymbol, universalSymbol, getAdapterRuntime, nodeHeadersToWeb } from './chunk-APV2JR36.js'; import 'node:http'; import 'node:stream'; var deno = typeof Deno !== "undefined"; var bun = typeof Bun !== "undefined"; function createRequestAdapter(options = {}) { const { origin = env.ORIGIN, trustProxy = env.TRUST_PROXY === "1" } = options; let { protocol: protocolOverride, host: hostOverride } = origin ? new URL(origin) : {}; if (protocolOverride) { protocolOverride = protocolOverride.slice(0, -1); } let warned = false; return function requestAdapter(req) { if (req[requestSymbol]) { return req[requestSymbol]; } function parseForwardedHeader(name) { return (headers[`x-forwarded-${name}`] || "").split(",", 1)[0].trim(); } let headers = req.headers; if (headers[":method"]) { headers = Object.fromEntries(Object.entries(headers).filter(([key]) => !key.startsWith(":"))); } const protocol = protocolOverride || trustProxy && parseForwardedHeader("proto") || req.protocol || // biome-ignore lint/suspicious/noExplicitAny: encrypted can exist in some express versions req.socket?.encrypted && "https" || "http"; let host = hostOverride || trustProxy && parseForwardedHeader("host") || headers.host; if (!host && !warned) { console.warn( "Could not automatically determine the origin host, using 'localhost'. Use the 'origin' option or the 'ORIGIN' environment variable to set the origin explicitly." ); warned = true; host = "localhost"; } const request = new Request(`${protocol}://${host}${req.originalUrl ?? req.url}`, { method: req.method, headers, body: convertBody(req), // @ts-expect-error duplex: "half" }); req[requestSymbol] = request; return request; }; } function convertBody(req) { if (req.method === "GET" || req.method === "HEAD") { return; } if (req.rawBody !== void 0) { return req.rawBody; } if (!bun && !deno) { return req; } return new ReadableStream({ start(controller) { req.on("data", (chunk) => controller.enqueue(chunk)); req.on("end", () => controller.close()); req.on("error", (err) => controller.error(err)); } }); } var deno2 = typeof Deno !== "undefined"; async function sendResponse(fetchResponse, nodeResponse) { const fetchBody = fetchResponse.body; let body = null; if (!fetchBody) { body = null; } else if (typeof fetchBody.pipe === "function") { body = fetchBody; } else if (typeof fetchBody.pipeTo === "function") { const { Readable: Readable2 } = await import('node:stream'); if (!deno2 && Readable2.fromWeb) { body = Readable2.fromWeb(fetchBody); } else { const reader = fetchBody.getReader(); body = new Readable2({ async read() { try { const { done, value } = await reader.read(); if (done) { this.push(null); } else { const canContinue = this.push(value); if (!canContinue) { reader.releaseLock(); } } } catch (e) { this.destroy(e); } } }); } } else if (fetchBody) { const { Readable: Readable2 } = await import('node:stream'); body = Readable2.from(fetchBody); } setHeaders(fetchResponse, nodeResponse); if (body) { body.pipe(nodeResponse); await new Promise((resolve, reject) => { body.on("error", (err) => { nodeResponse.destroy(err); reject(err); }); nodeResponse.on("error", (err) => { body.destroy(err); reject(err); }); nodeResponse.on("finish", resolve); nodeResponse.on("drain", () => { body.resume(); }); }); } else { nodeResponse.setHeader("content-length", "0"); nodeResponse.end(); } } function createTransformStream() { const textEncoder = new TextEncoder(); return new TransformStream({ transform(chunk, ctrl) { if (typeof chunk === "string") { ctrl.enqueue(textEncoder.encode(chunk)); } else if (chunk instanceof Uint8Array) { ctrl.enqueue(chunk); } else { ctrl.enqueue(new Uint8Array(chunk)); } } }); } function override(nodeResponse, key, forwardTo) { const original = nodeResponse[key]; nodeResponse[key] = (...args) => { if (!nodeResponse.headersSent) { nodeResponse.writeHead(nodeResponse.statusCode); } if (args[0] && args[0].length > 0) { forwardTo.write(args[0]).catch(console.error); } if (key === "end") { forwardTo.close().catch(() => { }); } return true; }; return { original(...args) { original.apply(nodeResponse, args); }, restore() { nodeResponse[key] = original; } }; } function overrideWriteHead(nodeResponse, callback) { const original = nodeResponse.writeHead; let alreadyCalled = false; nodeResponse.writeHead = () => { if (!alreadyCalled) { callback().catch(console.error); alreadyCalled = true; } return nodeResponse; }; return { original(...args) { original.apply(nodeResponse, args); }, restore() { nodeResponse.writeHead = original; } }; } function getFullUrl(pathnameOrFull, req) { try { return new URL(pathnameOrFull).href; } catch { const protocol = req.socket?.encrypted || req.headers["x-forwarded-proto"] === "https" ? "https" : "http"; const host = req.headers["x-forwarded-host"] || req.headers.host || "localhost"; const baseUrl = `${protocol}://${host}`; return new URL(pathnameOrFull, baseUrl).href; } } function responseAdapter(nodeResponse, bodyInit) { if ([301, 302, 303, 307, 308].includes(nodeResponse.statusCode) && nodeResponse.req) { const location = nodeResponse.getHeader("location"); if (location) { const fullUrl = getFullUrl(location, nodeResponse.req); return Response.redirect(fullUrl, nodeResponse.statusCode); } } return new Response([204, 304].includes(nodeResponse.statusCode) ? null : bodyInit, { status: nodeResponse.statusCode, statusText: nodeResponse.statusMessage, headers: nodeHeadersToWeb(nodeResponse.getHeaders()) }); } function wrapResponse(nodeResponse, next) { if (nodeResponse[wrappedResponseSymbol]) return; nodeResponse[wrappedResponseSymbol] = true; const body = createTransformStream(); const writer = body.writable.getWriter(); const [reader1, reader2] = body.readable.tee(); const original = { write: override(nodeResponse, "write", writer), end: override(nodeResponse, "end", writer), writeHead: overrideWriteHead(nodeResponse, triggerPendingMiddlewares) }; async function triggerPendingMiddlewares() { if (!nodeResponse[pendingMiddlewaresSymbol]) { return; } const middlewares = nodeResponse[pendingMiddlewaresSymbol]; delete nodeResponse[pendingMiddlewaresSymbol]; let response; try { response = responseAdapter(nodeResponse, reader1); for (const middleware of middlewares) { const tmp = await middleware(response); if (tmp) response = tmp; } } catch (e) { response = void 0; await writer.abort(); original.writeHead.restore(); original.write.restore(); original.end.restore(); if (next) { next(e); } else { throw e; } } if (!response) return; const readableToOriginal = response.body ?? reader2; setHeaders(response, nodeResponse, true); original.writeHead.restore(); nodeResponse.flushHeaders(); const wait = readableToOriginal.pipeTo( new WritableStream({ write(chunk) { original.write.original(chunk); }, close() { original.end.original(); }, abort() { original.end.original(); } }) ); await wait; original.write.restore(); original.end.restore(); } } function setHeaders(fetchResponse, nodeResponse, mirror = false) { nodeResponse.statusCode = fetchResponse.status; if (fetchResponse.statusText) { nodeResponse.statusMessage = fetchResponse.statusText; } const nodeResponseHeaders = new Set(Object.keys(nodeResponse.getHeaders())); const setCookie = fetchResponse.headers.getSetCookie(); for (const cookie of setCookie) { nodeResponse.appendHeader("set-cookie", cookie); } fetchResponse.headers.forEach((value, key) => { nodeResponseHeaders.delete(key); if (key === "set-cookie") return; nodeResponse.setHeader(key, value); }); if (mirror) { nodeResponseHeaders.forEach((key) => { nodeResponse.removeHeader(key); }); } } var requestSymbol = /* @__PURE__ */ Symbol.for("unRequest"); var pendingMiddlewaresSymbol = /* @__PURE__ */ Symbol.for("unPendingMiddlewares"); var wrappedResponseSymbol = /* @__PURE__ */ Symbol.for("unWrappedResponse"); var env = typeof globalThis.process?.env !== "undefined" ? globalThis.process.env : typeof import.meta?.env !== "undefined" ? import.meta.env : {}; function nextOr404(res, next) { if (next) { next(); } else { res.statusCode = 404; res.end(); } } function createMiddleware(middlewareFactory, options = {}) { const requestAdapter = createRequestAdapter(options); return (...args) => { const middleware = middlewareFactory(...args); return bindUniversal(middleware, async function universalMiddlewareExpress(req, res, next) { try { req[contextSymbol] ??= {}; const request = requestAdapter(req); const response = await this[universalSymbol](request, getContext(req), getRuntime(req, res)); if (!response) { return nextOr404(res, next); } if (typeof response === "function") { if (res.headersSent) { throw new Error( "Universal Middleware called after headers have been sent. Please open an issue at https://github.com/magne4000/universal-middleware" ); } if (req.complete === void 0) req.complete = req._readableState?.ended ?? true; wrapResponse(res, next); res[pendingMiddlewaresSymbol] ??= []; res[pendingMiddlewaresSymbol].push(response); return nextOr404(res, next); } if (response instanceof Response) { await sendResponse(response, res); } else { req[contextSymbol] = response; return nextOr404(res, next); } } catch (error) { if (next) { next(error); } else { console.error(error); if (!res.headersSent) { res.statusCode = 500; } if (!res.writableEnded) { res.end(); } } } }); }; } function getContext(req) { return req[contextSymbol]; } function getRuntime(request, response) { return getAdapterRuntime("express", { params: request.params, // biome-ignore lint/suspicious/noExplicitAny: cast req: request, // biome-ignore lint/suspicious/noExplicitAny: cast res: response, express: Object.freeze({ // biome-ignore lint/suspicious/noExplicitAny: cast req: request, // biome-ignore lint/suspicious/noExplicitAny: cast res: response }) }); } export { createMiddleware, createRequestAdapter };