UNPKG

@universal-middleware/express

Version:
426 lines (418 loc) 12 kB
import { createRequestAdapter } from "./chunk-N42HUXDK.js"; // src/common.ts import { bindUniversal, contextSymbol, getAdapterRuntime, universalSymbol } from "@universal-middleware/core"; // src/const.ts import { env, requestSymbol } from "@universal-middleware/node"; var pendingMiddlewaresSymbol = /* @__PURE__ */ Symbol.for("unPendingMiddlewares"); var wrappedResponseSymbol = /* @__PURE__ */ Symbol.for("unWrappedResponse"); // src/response.ts import { responseAdapter, sendResponse, setResponseHeaders } from "@universal-middleware/node"; 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 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; setResponseHeaders(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(); } } // src/common.ts 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, res); 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, res); 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/router.ts import { apply as applyCore, getUniversal, UniversalRouter, universalSymbol as universalSymbol2 } from "@universal-middleware/core"; // 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 : ( // biome-ignore lint/suspicious/noExplicitAny: srvx request request.runtime?.node?.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 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 };