@universal-middleware/express
Version:
Express adapter for universal middlewares
580 lines (574 loc) • 17.3 kB
JavaScript
// 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
};