UNPKG

@elysiajs/node

Version:

Elysia adapter to run Elysia on Node.js

935 lines (928 loc) 31.7 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { default: () => index_default, node: () => node }); module.exports = __toCommonJS(index_exports); var import_web_standard = require("elysia/adapter/web-standard"); var import_utils8 = require("elysia/utils"); var import_server = require("crossws/server"); // src/ws.ts var import_crossws = require("crossws"); var import_elysia = require("elysia"); var import_utils = require("elysia/adapter/utils"); var import_utils2 = require("elysia/utils"); var import_ws = require("elysia/ws"); function createWebSocketAdapter() { const store = {}; function handler(app, path, options) { const { parse, body, response, ...rest } = options; const validateMessage = (0, import_elysia.getSchemaValidator)(body, { // @ts-expect-error private property modules: app.definitions.typebox, // @ts-expect-error private property models: app.definitions.type, normalize: app.config.normalize }); const validateResponse = (0, import_elysia.getSchemaValidator)(response, { // @ts-expect-error private property modules: app.definitions.typebox, // @ts-expect-error private property models: app.definitions.type, normalize: app.config.normalize }); const toServerWebSocket = (peer, context) => { const ws = peer; ws.data = context; ws.sendText = ws.send; ws.sendBinary = ws.send; ws.publishText = ws.publish; ws.publishBinary = ws.publish; ws.isSubscribed = (topic) => peer.topics.has(topic); ws.cork = () => { console.log("ws.cork is not supported yet"); }; return ws; }; app.route( "WS", path, // @ts-ignore async (context) => { const { set: set2, path: path2, qi, headers, query, params } = context; const id = context.request.wsId; context.validator = validateResponse; if (options.upgrade) { if (typeof options.upgrade === "function") { const temp = options.upgrade(context); if (temp instanceof Promise) await temp; } else if (options.upgrade) Object.assign( set2.headers, options.upgrade ); } if (set2.cookie && (0, import_utils2.isNotEmpty)(set2.cookie)) { const cookie = (0, import_elysia.serializeCookie)(set2.cookie); if (cookie) set2.headers["set-cookie"] = cookie; } if (set2.headers["set-cookie"] && Array.isArray(set2.headers["set-cookie"])) set2.headers = (0, import_utils.parseSetCookies)( new Headers(set2.headers), set2.headers["set-cookie"] ); const handleResponse2 = (0, import_ws.createHandleWSResponse)(validateResponse); const parseMessage = parse ? (0, import_ws.createWSMessageParser)(parse) : void 0; if (typeof options.beforeHandle === "function") { const result = options.beforeHandle(context); if (result instanceof Promise) await result; } const errorHandlers = [ ...options.error ? Array.isArray(options.error) ? options.error : [options.error] : [], ...(app.event.error ?? []).map( (x) => typeof x === "function" ? x : x.fn ) ].filter((x) => x); const handleErrors = errorHandlers.length ? async (ws, error) => { for (const handleError of errorHandlers) { let response2 = handleError( Object.assign(context, { error }) ); if (response2 instanceof Promise) response2 = await response2; await handleResponse2(ws, response2); if (response2) break; } } : void 0; store[id] = { data: context, validateResponse, ping(ws, data) { return options.ping?.(ws, data); }, pong(ws, data) { return options.pong?.(ws, data); }, async open(_ws) { const ws = toServerWebSocket(_ws, context); try { await handleResponse2( ws, options.open?.(new import_ws.ElysiaWS(ws, context)) ); } catch (error) { handleErrors?.(ws, error); } }, async message(ws, message) { if (message.includes("ping")) { try { return void ws.pong(message); } catch (error) { handleErrors?.(ws, error); } } if (parseMessage) message = await parseMessage(ws, message); if (message) { if (validateMessage?.Check(message) === false) return void ws.send( new import_elysia.ValidationError( "message", validateMessage, message ).message ); } try { await handleResponse2( ws, options.message?.( new import_ws.ElysiaWS(ws, context, message), message ) ); } catch (error) { handleErrors?.(ws, error); } }, async drain(ws) { try { await handleResponse2( ws, options.drain?.( new import_ws.ElysiaWS(ws, context) ) ); } catch (error) { handleErrors?.(ws, error); } }, async close(ws, code, reason) { try { await handleResponse2( ws, options.close?.( new import_ws.ElysiaWS(ws, context), code, reason ) ); } catch (error) { handleErrors?.(ws, error); } }, error(ws, error) { handleErrors?.(ws, error); } }; return ""; }, { ...rest, websocket: options } ); } function createConfig(app) { return (0, import_crossws.defineHooks)({ async upgrade(request) { const id = request.wsId = (0, import_utils2.randomId)(); const response = await app.handle(request); const context = store[id]; if (!context) return response; return { context, headers: context.data.set.headers }; }, open(ws) { const context = ws.context; context.open(ws); }, message(ws, message) { const context = ws.context; context.message(ws, message.text()); }, close(ws, detail) { const context = ws.context; context.close( // ws is parsed in context.open ws, detail.code, detail.reason ); }, error(ws, error) { const context = ws.context; context.error?.(ws, error); } }); } return { handler, createConfig, context: store }; } // src/handle.ts var import_srvx2 = require("srvx"); var import_utils5 = require("elysia/adapter/utils"); // src/utils.ts var import_srvx = require("srvx"); var import_utils3 = require("elysia/utils"); var import_utils4 = require("elysia/adapter/utils"); var handleFile = (response, set2) => { if (response instanceof Promise) return response.then((res) => handleFile(res, set2)); const size = response.size; const immutable = set2 && (set2.status === 206 || set2.status === 304 || set2.status === 412 || set2.status === 416); const defaultHeader = immutable ? { "transfer-encoding": "chunked" } : { "accept-ranges": "bytes", "content-range": size ? `bytes 0-${size - 1}/${size}` : void 0, "transfer-encoding": "chunked" }; if (!set2 && !size) return new import_srvx.FastResponse(response); if (!set2) return new import_srvx.FastResponse(response, { headers: defaultHeader }); if (set2.headers instanceof Headers) { let setHeaders = defaultHeader; setHeaders = {}; for (const [key, value] of set2.headers.entries()) if (key in set2.headers) setHeaders[key] = value; if (immutable) { delete set2.headers["content-length"]; delete set2.headers["accept-ranges"]; } return new import_srvx.FastResponse(response, { status: set2.status, headers: setHeaders }); } if ((0, import_utils3.isNotEmpty)(set2.headers)) return new import_srvx.FastResponse(response, { status: set2.status, headers: Object.assign(defaultHeader, set2.headers) }); return new import_srvx.FastResponse(response, { status: set2.status, headers: defaultHeader }); }; function mergeHeaders(responseHeaders, setHeaders) { const headers = new Headers(Object.fromEntries(responseHeaders.entries())); if (setHeaders instanceof Headers) for (const key of setHeaders.keys()) { if (key === "set-cookie") { if (headers.has("set-cookie")) continue; for (const cookie of setHeaders.getSetCookie()) headers.append("set-cookie", cookie); } else if (!responseHeaders.has(key)) headers.set(key, setHeaders?.get(key) ?? ""); } else for (const key in setHeaders) if (key === "set-cookie") headers.append(key, setHeaders[key]); else if (!responseHeaders.has(key)) headers.set(key, setHeaders[key]); return headers; } function mergeStatus(responseStatus, setStatus) { if (typeof setStatus === "string") setStatus = import_utils3.StatusMap[setStatus]; if (responseStatus === 200) return setStatus; return responseStatus; } var createResponseHandler = (handler) => { const handleStream2 = (0, import_utils4.createStreamHandler)(handler); return (response, set2, request) => { const newResponse = new import_srvx.FastResponse(response.body, { headers: mergeHeaders(response.headers, set2.headers), status: mergeStatus(response.status, set2.status) }); if (!newResponse.headers.has("content-length") && newResponse.headers.get("transfer-encoding") === "chunked") return handleStream2( (0, import_utils4.streamResponse)(newResponse), (0, import_utils4.responseToSetHeaders)(newResponse, set2), request, // @ts-ignore true // don't auto-format SSE for pre-formatted Response ); return newResponse; }; }; // src/handle.ts var import_file = require("elysia/universal/file"); var import_utils7 = require("elysia/utils"); var import_cookies = require("elysia/cookies"); var import_error = require("elysia/error"); var handleElysiaFile = (file, set2 = { headers: {} }) => { const path = file.path; const contentType = import_file.mime[path.slice(path.lastIndexOf(".") + 1)]; if (contentType) set2.headers["content-type"] = contentType; if (file.stats && set2 && set2.status !== 206 && set2.status !== 304 && set2.status !== 412 && set2.status !== 416) return file.stats.then((stat) => { const size = stat.size; if (size !== void 0) { set2.headers["content-range"] = `bytes 0-${size - 1}/${size}`; set2.headers["content-length"] = size; } return handleFile(file.value, set2); }); return handleFile(file.value, set2); }; var mapResponse = (response, set2, request) => { if ((0, import_utils7.isNotEmpty)(set2.headers) || set2.status !== 200 || set2.cookie) { (0, import_utils5.handleSet)(set2); switch (response?.constructor?.name) { case "String": set2.headers["content-type"] = "text/plain"; return new import_srvx2.FastResponse(response, set2); case "Array": case "Object": set2.headers["content-type"] = "application/json"; return new import_srvx2.FastResponse(JSON.stringify(response), set2); case "ElysiaFile": return handleElysiaFile(response, set2); case "File": return handleFile(response, set2); case "Blob": return handleFile(response, set2); case "ElysiaCustomStatusResponse": set2.status = response.code; return mapResponse( response.response, set2, request ); case "ReadableStream": if (!set2.headers["content-type"]?.startsWith( "text/event-stream" )) set2.headers["content-type"] = "text/event-stream; charset=utf-8"; request?.signal?.addEventListener( "abort", { handleEvent() { if (request?.signal && !request?.signal?.aborted) response.cancel(); } }, { once: true } ); return new import_srvx2.FastResponse(response, set2); case void 0: if (!response) return new import_srvx2.FastResponse(null, set2); return new import_srvx2.FastResponse(JSON.stringify(response), set2); case "Response": return handleResponse(response, set2, request); case "Error": return errorToResponse(response, set2); case "Promise": return response.then( (x) => mapResponse(x, set2, request) ); case "Function": return mapResponse(response(), set2, request); case "Number": case "Boolean": return new import_srvx2.FastResponse( response.toString(), set2 ); case "Cookie": if (response instanceof import_cookies.Cookie) return new import_srvx2.FastResponse(response.value, set2); return new import_srvx2.FastResponse(response?.toString(), set2); case "FormData": return new import_srvx2.FastResponse(response, set2); default: if (response instanceof import_srvx2.FastResponse) return handleResponse(response, set2, request); if (response instanceof Promise) return response.then((x) => mapResponse(x, set2)); if (response instanceof Error) return errorToResponse(response, set2); if (response instanceof import_error.ElysiaCustomStatusResponse) { set2.status = response.code; return mapResponse( response.response, set2, request ); } if (typeof response?.next === "function") return handleStream(response, set2, request); if (typeof response?.then === "function") return response.then((x) => mapResponse(x, set2)); if (typeof response?.toResponse === "function") return mapResponse(response.toResponse(), set2); if ("charCodeAt" in response) { const code = response.charCodeAt(0); if (code === 123 || code === 91) { if (!set2.headers["Content-Type"]) set2.headers["Content-Type"] = "application/json"; return new import_srvx2.FastResponse( JSON.stringify(response), set2 ); } } return new import_srvx2.FastResponse(response, set2); } } if (response instanceof import_srvx2.FastResponse && !response.headers.has("content-length") && response.headers.get("transfer-encoding") === "chunked") return handleStream( (0, import_utils5.streamResponse)(response), (0, import_utils5.responseToSetHeaders)(response, set2), request ); if ( // @ts-expect-error typeof response?.next === "function" || response instanceof ReadableStream ) return handleStream(response, set2, request); return mapCompactResponse(response, request); }; var mapEarlyResponse = (response, set2, request) => { if (response === void 0 || response === null) return; if ((0, import_utils7.isNotEmpty)(set2.headers) || set2.status !== 200 || set2.cookie) { (0, import_utils5.handleSet)(set2); switch (response?.constructor?.name) { case "String": set2.headers["content-type"] = "text/plain"; return new import_srvx2.FastResponse(response, set2); case "Array": case "Object": set2.headers["content-type"] = "application/json"; return new import_srvx2.FastResponse(JSON.stringify(response), set2); case "ElysiaFile": return handleElysiaFile(response, set2); case "File": return handleFile(response, set2); case "Blob": return handleFile(response, set2); case "ElysiaCustomStatusResponse": set2.status = response.code; return mapEarlyResponse( response.response, set2, request ); case "ReadableStream": if (!set2.headers["content-type"]?.startsWith( "text/event-stream" )) set2.headers["content-type"] = "text/event-stream; charset=utf-8"; request?.signal?.addEventListener( "abort", { handleEvent() { if (request?.signal && !request?.signal?.aborted) response.cancel(); } }, { once: true } ); return new import_srvx2.FastResponse(response, set2); case void 0: if (!response) return; return new import_srvx2.FastResponse(JSON.stringify(response), set2); case "Response": return handleResponse(response, set2, request); case "Promise": return response.then( (x) => mapEarlyResponse(x, set2) ); case "Error": return errorToResponse(response, set2); case "Function": return mapEarlyResponse(response(), set2); case "Number": case "Boolean": return new import_srvx2.FastResponse( response.toString(), set2 ); case "FormData": return new import_srvx2.FastResponse(response); case "Cookie": if (response instanceof import_cookies.Cookie) return new import_srvx2.FastResponse(response.value, set2); return new import_srvx2.FastResponse(response?.toString(), set2); default: if (response instanceof import_srvx2.FastResponse) return handleResponse(response, set2, request); if (response instanceof Promise) return response.then((x) => mapEarlyResponse(x, set2)); if (response instanceof Error) return errorToResponse(response, set2); if (response instanceof import_error.ElysiaCustomStatusResponse) { set2.status = response.code; return mapEarlyResponse( response.response, set2, request ); } if (typeof response?.next === "function") return handleStream(response, set2, request); if (typeof response?.then === "function") return response.then((x) => mapEarlyResponse(x, set2)); if (typeof response?.toResponse === "function") return mapEarlyResponse(response.toResponse(), set2); if ("charCodeAt" in response) { const code = response.charCodeAt(0); if (code === 123 || code === 91) { if (!set2.headers["Content-Type"]) set2.headers["Content-Type"] = "application/json"; return new import_srvx2.FastResponse( JSON.stringify(response), set2 ); } } return new import_srvx2.FastResponse(response, set2); } } else switch (response?.constructor?.name) { case "String": set2.headers["content-type"] = "text/plain"; return new import_srvx2.FastResponse(response); case "Array": case "Object": set2.headers["content-type"] = "application/json"; return new import_srvx2.FastResponse(JSON.stringify(response), set2); case "ElysiaFile": return handleElysiaFile(response, set2); case "File": return handleFile(response, set2); case "Blob": return handleFile(response, set2); case "ElysiaCustomStatusResponse": set2.status = response.code; return mapEarlyResponse( response.response, set2, request ); case "ReadableStream": request?.signal?.addEventListener( "abort", { handleEvent() { if (request?.signal && !request?.signal?.aborted) response.cancel(); } }, { once: true } ); return new import_srvx2.FastResponse(response, { headers: { "Content-Type": "text/event-stream; charset=utf-8" } }); case void 0: if (!response) return new import_srvx2.FastResponse(""); return new import_srvx2.FastResponse(JSON.stringify(response), { headers: { "content-type": "application/json" } }); case "Response": if (!response.headers.has("content-length") && response.headers.get("transfer-encoding") === "chunked") return handleStream( (0, import_utils5.streamResponse)(response), (0, import_utils5.responseToSetHeaders)(response), request ); return response; case "Promise": return response.then((x) => { const r = mapEarlyResponse(x, set2); if (r !== void 0) return r; }); case "Error": return errorToResponse(response, set2); case "Function": return mapCompactResponse(response(), request); case "Number": case "Boolean": return new import_srvx2.FastResponse(response.toString()); case "Cookie": if (response instanceof import_cookies.Cookie) return new import_srvx2.FastResponse(response.value, set2); return new import_srvx2.FastResponse(response?.toString(), set2); case "FormData": return new import_srvx2.FastResponse(response); default: if (response instanceof import_srvx2.FastResponse) return response; if (response instanceof Promise) return response.then((x) => mapEarlyResponse(x, set2)); if (response instanceof Error) return errorToResponse(response, set2); if (response instanceof import_error.ElysiaCustomStatusResponse) { set2.status = response.code; return mapEarlyResponse( response.response, set2, request ); } if (typeof response?.next === "function") return handleStream(response, set2, request); if (typeof response?.then === "function") return response.then((x) => mapEarlyResponse(x, set2)); if (typeof response?.toResponse === "function") return mapEarlyResponse(response.toResponse(), set2); if ("charCodeAt" in response) { const code = response.charCodeAt(0); if (code === 123 || code === 91) { if (!set2.headers["Content-Type"]) set2.headers["Content-Type"] = "application/json"; return new import_srvx2.FastResponse( JSON.stringify(response), set2 ); } } return new import_srvx2.FastResponse(response); } }; var mapCompactResponse = (response, request) => { switch (response?.constructor?.name) { case "String": return new import_srvx2.FastResponse(response, { headers: { "Content-Type": "text/plain" } }); case "Object": case "Array": return new import_srvx2.FastResponse(JSON.stringify(response), { headers: { "Content-Type": "application/json" } }); case "ElysiaFile": return handleElysiaFile(response); case "File": return handleFile(response); case "Blob": return handleFile(response); case "ElysiaCustomStatusResponse": return mapResponse( response.response, { status: response.code, headers: {} } ); case "ReadableStream": request?.signal?.addEventListener( "abort", { handleEvent() { if (request?.signal && !request?.signal?.aborted) response.cancel(); } }, { once: true } ); return new import_srvx2.FastResponse(response, { headers: { "Content-Type": "text/event-stream; charset=utf-8" } }); case void 0: if (!response) return new import_srvx2.FastResponse(""); return new import_srvx2.FastResponse(JSON.stringify(response), { headers: { "content-type": "application/json" } }); case "Response": if (response.headers.get("transfer-encoding") === "chunked") return handleStream( (0, import_utils5.streamResponse)(response), (0, import_utils5.responseToSetHeaders)(response), request ); return response; case "Error": return errorToResponse(response); case "Promise": return response.then( (x) => mapCompactResponse(x, request) ); // ? Maybe response or Blob case "Function": return mapCompactResponse(response(), request); case "Number": case "Boolean": return new import_srvx2.FastResponse(response.toString()); case "FormData": return new import_srvx2.FastResponse(response); default: if (response instanceof import_srvx2.FastResponse) return response; if (response instanceof Promise) return response.then( (x) => mapCompactResponse(x, request) ); if (response instanceof Error) return errorToResponse(response); if (response instanceof import_error.ElysiaCustomStatusResponse) return mapResponse( response.response, { status: response.code, headers: {} } ); if (typeof response?.next === "function") return handleStream(response, void 0, request); if (typeof response?.then === "function") return response.then((x) => mapResponse(x, set)); if (typeof response?.toResponse === "function") return mapCompactResponse(response.toResponse()); if ("charCodeAt" in response) { const code = response.charCodeAt(0); if (code === 123 || code === 91) { return new import_srvx2.FastResponse(JSON.stringify(response), { headers: { "Content-Type": "application/json" } }); } } return new import_srvx2.FastResponse(response); } }; var errorToResponse = (error, set2) => new import_srvx2.FastResponse( JSON.stringify({ name: error?.name, message: error?.message, cause: error?.cause }), { status: set2?.status !== 200 ? set2?.status ?? 500 : 500, headers: set2?.headers } ); var createStaticHandler = (handle, hooks, setHeaders = {}) => { if (typeof handle === "function") return; const response = mapResponse(handle, { headers: setHeaders }); if (!hooks.parse?.length && !hooks.transform?.length && !hooks.beforeHandle?.length && !hooks.afterHandle?.length) return response.clone.bind(response); }; var handleResponse = createResponseHandler({ mapResponse, mapCompactResponse }); var handleStream = (0, import_utils5.createStreamHandler)({ mapResponse, mapCompactResponse }); // src/index.ts var node = () => { const ws = createWebSocketAdapter(); return { ...import_web_standard.WebStandardAdapter, name: "@elysiajs/node", handler: { mapCompactResponse, mapEarlyResponse, mapResponse, createStaticHandler }, ws: ws.handler, listen(app) { return (options, callback) => { if (typeof options === "string") { if (!(0, import_utils8.isNumericString)(options)) throw new Error("Port must be a numeric value"); options = parseInt(options); } const serverOptions = typeof options === "number" ? { port: options, silent: true, websocket: ws.createConfig(app), fetch: app.fetch, reusePort: true } : { reusePort: true, ...options, silent: true, websocket: ws.createConfig(app), fetch: app.fetch }; let server = (0, import_server.serve)(serverOptions); const nodeServer = server.node?.server; const hostname = server.serveOptions.host ?? "localhost"; const port = server.options.port; const serverInfo = { ...server, id: (0, import_utils8.randomId)(), development: process.env.NODE_ENV !== "production", fetch: app.fetch, hostname, get pendingRequests() { const { promise, resolve, reject } = Promise.withResolvers(); nodeServer?.getConnections((error, total) => { if (error) reject(error); resolve(total); }); return promise; }, get pendingWebSockets() { return 0; }, port, publish() { return 0; }, ref() { nodeServer?.ref(); }, unref() { nodeServer?.unref(); }, reload() { nodeServer?.close(); server = (0, import_server.serve)(serverOptions); return serverInfo; }, requestIP() { throw new Error( "This adapter doesn't support Bun requestIP method" ); }, stop() { return server.close(); }, upgrade() { throw new Error( "This adapter doesn't support Web Standard Upgrade method" ); }, url: new URL( `http://${hostname === "::" ? "localhost" : hostname}:${port}` ), [Symbol.dispose]() { server.close(); }, // @ts-ignore raw: server }; if (callback) callback(serverInfo); app.router.http.build?.(); if (app.event.start) for (let i = 0; i < app.event.start.length; i++) app.event.start[i].fn(this); process.on("beforeExit", () => { server.close(); if (app.event.stop) for (let i = 0; i < app.event.stop.length; i++) app.event.stop[i].fn(this); }); }; } }; }; var index_default = node; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { node });