UNPKG

@universal-middleware/express

Version:
580 lines (574 loc) 17.3 kB
// src/common.ts import { bindUniversal, contextSymbol, getAdapterRuntime, universalSymbol } from "@universal-middleware/core"; // src/request.ts 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 || req.protocol || trustProxy && parseForwardedHeader("proto") || // 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)); } }); } // src/response.ts import { nodeHeadersToWeb } from "@universal-middleware/core"; 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); }); } } // src/common.ts var requestSymbol = Symbol.for("unRequest"); var pendingMiddlewaresSymbol = Symbol.for("unPendingMiddlewares"); var wrappedResponseSymbol = 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 createHandler(handlerFactory, options = {}) { const requestAdapter = createRequestAdapter(options); return (...args) => { const handler = handlerFactory(...args); return bindUniversal(handler, async function universalHandlerExpress(req, res, next) { try { req[contextSymbol] ??= {}; const request = requestAdapter(req); const response = await this[universalSymbol]( request, req[contextSymbol], getRuntime(req, res) ); if (!response) { nextOr404(res, next); } else { await sendResponse(response, res); } } catch (error) { if (next) { next(error); } else { console.error(error); if (!res.headersSent) { res.statusCode = 500; } if (!res.writableEnded) { 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 }) }); } // src/utils.ts import { ServerResponse } from "node:http"; import { PassThrough, Readable } from "node:stream"; var statusCodesWithoutBody = [ 100, // Continue 101, // Switching Protocols 102, // Processing (WebDAV) 103, // Early Hints 204, // No Content 205, // Reset Content 304 // Not Modified ]; function connectToWeb(handler) { return async (request, _context, runtime) => { const req = runtime && "req" in runtime && runtime.req ? runtime.req : createIncomingMessage(request); const { res, onReadable } = createServerResponse(req); return new Promise(async (resolve, reject) => { onReadable(({ readable, headers, statusCode }) => { const responseBody = statusCodesWithoutBody.includes(statusCode) ? null : "from" in ReadableStream ? ( // biome-ignore lint/suspicious/noExplicitAny: definition clash between Web and Node ReadableStream.from(readable) ) : ( // biome-ignore lint/suspicious/noExplicitAny: definition clash between Web and Node Readable.toWeb(readable) ); resolve( new Response(responseBody, { status: statusCode, headers: flattenHeaders(headers) }) ); }); const next = (error) => { if (error) { reject(error instanceof Error ? error : new Error(String(error))); } else { resolve(void 0); } }; try { const handled = await handler(req, res, next); if (handled === false) { res.destroy(); resolve(void 0); } } catch (e) { next(e); } }); }; } function createIncomingMessage(request) { const parsedUrl = new URL(request.url, "http://localhost"); const pathnameAndQuery = (parsedUrl.pathname || "") + (parsedUrl.search || ""); const body = request.body ? Readable.fromWeb(request.body) : Readable.from([]); return Object.assign(body, { url: pathnameAndQuery, method: request.method, headers: Object.fromEntries(request.headers) }); } function createServerResponse(incomingMessage) { const res = new ServerResponse(incomingMessage); const passThrough = new PassThrough(); let handled = false; const onReadable = (cb) => { const handleReadable = () => { if (handled) return; handled = true; cb({ readable: Readable.from(passThrough), headers: res.getHeaders(), statusCode: res.statusCode }); }; passThrough.once("readable", handleReadable); passThrough.once("end", handleReadable); }; passThrough.once("finish", () => { res.emit("finish"); }); passThrough.once("close", () => { res.destroy(); res.emit("close"); }); passThrough.on("drain", () => { res.emit("drain"); }); res.write = passThrough.write.bind(passThrough); res.end = passThrough.end.bind(passThrough); res.writeHead = function writeHead(statusCode, statusMessage, headers) { res.statusCode = statusCode; if (typeof statusMessage === "object") { headers = statusMessage; statusMessage = void 0; } if (headers) { for (const [key, value] of Object.entries(headers)) { if (value !== void 0) { res.setHeader(key, value); } } } return res; }; return { res, onReadable }; } function flattenHeaders(headers) { const flatHeaders = []; for (const [key, value] of Object.entries(headers)) { if (value === void 0 || value === null) { continue; } if (Array.isArray(value)) { for (const v of value) { if (v != null) { flatHeaders.push([key, String(v)]); } } } else { flatHeaders.push([key, String(value)]); } } return flatHeaders; } function isExpressV4(app) { return "del" in app; } function isExpressV5(app) { return !isExpressV4(app); } // src/router.ts import { apply as applyCore, getUniversal, UniversalRouter, universalSymbol as universalSymbol2 } from "@universal-middleware/core"; var UniversalExpressRouter = class extends UniversalRouter { #app; constructor(app) { super(false); this.#app = app; } use(middleware) { this.#app.use(createMiddleware(() => getUniversal(middleware))()); return this; } applyCatchAll() { if (isExpressV5(this.#app)) { this.#app.all("/{*catchAll}", createHandler(() => this[universalSymbol2])()); } if (isExpressV4(this.#app)) { this.#app.all("/**", createHandler(() => this[universalSymbol2])()); } return this; } }; function apply(app, middlewares) { const router = new UniversalExpressRouter(app); applyCore(router, middlewares, true); Promise.resolve().then(() => router.applyCatchAll()); } export { apply, connectToWeb, createHandler, createIncomingMessage, createMiddleware, createRequestAdapter, createServerResponse, getContext, sendResponse };