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