UNPKG

elysia

Version:

Ergonomic Framework for Human

672 lines (669 loc) 23.8 kB
// src/universal/utils.ts var isBun = typeof Bun < "u"; // src/universal/file.ts var mime = { aac: "audio/aac", abw: "application/x-abiword", ai: "application/postscript", arc: "application/octet-stream", avi: "video/x-msvideo", azw: "application/vnd.amazon.ebook", bin: "application/octet-stream", bz: "application/x-bzip", bz2: "application/x-bzip2", csh: "application/x-csh", css: "text/css", csv: "text/csv", doc: "application/msword", dll: "application/octet-stream", eot: "application/vnd.ms-fontobject", epub: "application/epub+zip", gif: "image/gif", htm: "text/html", html: "text/html", ico: "image/x-icon", ics: "text/calendar", jar: "application/java-archive", jpeg: "image/jpeg", jpg: "image/jpeg", js: "application/javascript", json: "application/json", mid: "audio/midi", midi: "audio/midi", mp2: "audio/mpeg", mp3: "audio/mpeg", mp4: "video/mp4", mpa: "video/mpeg", mpe: "video/mpeg", mpeg: "video/mpeg", mpkg: "application/vnd.apple.installer+xml", odp: "application/vnd.oasis.opendocument.presentation", ods: "application/vnd.oasis.opendocument.spreadsheet", odt: "application/vnd.oasis.opendocument.text", oga: "audio/ogg", ogv: "video/ogg", ogx: "application/ogg", otf: "font/otf", png: "image/png", pdf: "application/pdf", ppt: "application/vnd.ms-powerpoint", rar: "application/x-rar-compressed", rtf: "application/rtf", sh: "application/x-sh", svg: "image/svg+xml", swf: "application/x-shockwave-flash", tar: "application/x-tar", tif: "image/tiff", tiff: "image/tiff", ts: "application/typescript", ttf: "font/ttf", txt: "text/plain", vsd: "application/vnd.visio", wav: "audio/x-wav", weba: "audio/webm", webm: "video/webm", webp: "image/webp", woff: "font/woff", woff2: "font/woff2", xhtml: "application/xhtml+xml", xls: "application/vnd.ms-excel", xlsx: "application/vnd.ms-excel", xlsx_OLD: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", xml: "application/xml", xul: "application/vnd.mozilla.xul+xml", zip: "application/zip", "3gp": "video/3gpp", "3gp_DOES_NOT_CONTAIN_VIDEO": "audio/3gpp", "3gp2": "video/3gpp2", "3gp2_DOES_NOT_CONTAIN_VIDEO": "audio/3gpp2", "7z": "application/x-7z-compressed" }, getFileExtension = (path) => { let index = path.lastIndexOf("."); return index === -1 ? "" : path.slice(index + 1); }; var createReadStream, stat, ElysiaFile = class { constructor(path) { this.path = path; if (isBun) this.value = Bun.file(path); else if (typeof window < "u") console.warn("Browser environment does not support file"); else if (!createReadStream || !stat) try { this.value = import("fs").then((fs) => (createReadStream = fs.createReadStream, fs.createReadStream(path))), this.stats = import("fs/promises").then((fs) => (stat = fs.stat, fs.stat(path))); } catch { } else this.value = createReadStream(path), this.stats = stat(path); } get type() { return ( // @ts-ignore mime[getFileExtension(this.path)] || "application/octet-stream" ); } get length() { return isBun ? this.value.size : this.stats?.then((x) => x.size) ?? 0; } }; // src/utils.ts var hasHeaderShorthand = "toJSON" in new Headers(), replaceUrlPath = (url, pathname) => { let urlObject = new URL(url); return urlObject.pathname = pathname, urlObject.toString(); }, isClass = (v) => typeof v == "function" && /^\s*class\s+/.test(v.toString()) || // Handle Object.create(null) v.toString && // Handle import * as Sentry from '@sentry/bun' // This also handle [object Date], [object Array] // and FFI value like [object Prisma] v.toString().startsWith("[object ") && v.toString() !== "[object Object]" || // If object prototype is not pure, then probably a class-like object isNotEmpty(Object.getPrototypeOf(v)), isObject = (item) => item && typeof item == "object" && !Array.isArray(item), mergeDeep = (target, source, options) => { let skipKeys = options?.skipKeys, override = options?.override ?? !0; if (!isObject(target) || !isObject(source)) return target; for (let [key, value] of Object.entries(source)) if (!skipKeys?.includes(key)) { if (!isObject(value) || !(key in target) || isClass(value)) { (override || !(key in target)) && (target[key] = value); continue; } target[key] = mergeDeep( target[key], value, { skipKeys, override } ); } return target; }, mergeCookie = (a, b) => { let v = mergeDeep(Object.assign({}, a), b, { skipKeys: ["properties"] }); return v.properties && delete v.properties, v; }, mergeObjectArray = (a, b) => { if (!b) return a; let array = [], checksums = []; if (a) { Array.isArray(a) || (a = [a]); for (let item of a) array.push(item), item.checksum && checksums.push(item.checksum); } if (b) { Array.isArray(b) || (b = [b]); for (let item of b) checksums.includes(item.checksum) || array.push(item); } return array; }, primitiveHooks = [ "start", "request", "parse", "transform", "resolve", "beforeHandle", "afterHandle", "mapResponse", "afterResponse", "trace", "error", "stop", "body", "headers", "params", "query", "response", "type", "detail" ], primitiveHookMap = primitiveHooks.reduce( (acc, x) => (acc[x] = !0, acc), {} ), mergeResponse = (a, b) => { let isRecordNumber = (x) => typeof x == "object" && Object.keys(x).every(isNumericString); return isRecordNumber(a) && isRecordNumber(b) ? Object.assign(a, b) : a && !isRecordNumber(a) && isRecordNumber(b) ? Object.assign({ 200: a }, b) : b ?? a; }, mergeSchemaValidator = (a, b) => !a && !b ? { body: void 0, headers: void 0, params: void 0, query: void 0, cookie: void 0, response: void 0 } : { body: b?.body ?? a?.body, headers: b?.headers ?? a?.headers, params: b?.params ?? a?.params, query: b?.query ?? a?.query, cookie: b?.cookie ?? a?.cookie, // @ts-ignore ? This order is correct - SaltyAom response: mergeResponse( // @ts-ignore a?.response, // @ts-ignore b?.response ) }, mergeHook = (a, b) => { if (!Object.values(b).find((x) => x != null)) return { ...a }; let hook = { ...a, ...b, // Merge local hook first // @ts-ignore body: b?.body ?? a?.body, // @ts-ignore headers: b?.headers ?? a?.headers, // @ts-ignore params: b?.params ?? a?.params, // @ts-ignore query: b?.query ?? a?.query, // @ts-ignore cookie: b?.cookie ?? a?.cookie, // ? This order is correct - SaltyAom response: mergeResponse( // @ts-ignore a?.response, // @ts-ignore b?.response ), type: a?.type || b?.type, detail: mergeDeep( // @ts-ignore b?.detail ?? {}, // @ts-ignore a?.detail ?? {} ), parse: mergeObjectArray(a?.parse, b?.parse), transform: mergeObjectArray(a?.transform, b?.transform), beforeHandle: mergeObjectArray( mergeObjectArray( // @ts-ignore fnToContainer(a?.resolve, "resolve"), a?.beforeHandle ), mergeObjectArray( fnToContainer(b.resolve, "resolve"), b?.beforeHandle ) ), afterHandle: mergeObjectArray(a?.afterHandle, b?.afterHandle), mapResponse: mergeObjectArray(a?.mapResponse, b?.mapResponse), afterResponse: mergeObjectArray( a?.afterResponse, b?.afterResponse ), trace: mergeObjectArray(a?.trace, b?.trace), error: mergeObjectArray(a?.error, b?.error) }; return hook.resolve && delete hook.resolve, hook; }, lifeCycleToArray = (a) => { a.parse && !Array.isArray(a.parse) && (a.parse = [a.parse]), a.transform && !Array.isArray(a.transform) && (a.transform = [a.transform]), a.afterHandle && !Array.isArray(a.afterHandle) && (a.afterHandle = [a.afterHandle]), a.mapResponse && !Array.isArray(a.mapResponse) && (a.mapResponse = [a.mapResponse]), a.afterResponse && !Array.isArray(a.afterResponse) && (a.afterResponse = [a.afterResponse]), a.trace && !Array.isArray(a.trace) && (a.trace = [a.trace]), a.error && !Array.isArray(a.error) && (a.error = [a.error]); let beforeHandle = []; return a.resolve && (beforeHandle = fnToContainer( // @ts-expect-error Array.isArray(a.resolve) ? a.resolve : [a.resolve], "resolve" ), delete a.resolve), a.beforeHandle && (beforeHandle.length ? beforeHandle = beforeHandle.concat( Array.isArray(a.beforeHandle) ? a.beforeHandle : [a.beforeHandle] ) : beforeHandle = Array.isArray(a.beforeHandle) ? a.beforeHandle : [a.beforeHandle]), beforeHandle.length && (a.beforeHandle = beforeHandle), a; }, isBun2 = typeof Bun < "u", hasBunHash = isBun2 && typeof Bun.hash == "function", checksum = (s) => { if (hasBunHash) return Bun.hash(s); let h = 9; for (let i = 0; i < s.length; ) h = Math.imul(h ^ s.charCodeAt(i++), 9 ** 9); return h = h ^ h >>> 9; }, injectChecksum = (checksum2, x) => { if (!x) return; if (!Array.isArray(x)) { let fn = x; return checksum2 && !fn.checksum && (fn.checksum = checksum2), fn.scope === "scoped" && (fn.scope = "local"), fn; } let fns = [...x]; for (let fn of fns) checksum2 && !fn.checksum && (fn.checksum = checksum2), fn.scope === "scoped" && (fn.scope = "local"); return fns; }, mergeLifeCycle = (a, b, checksum2) => ({ start: mergeObjectArray( a.start, injectChecksum(checksum2, b?.start) ), request: mergeObjectArray( a.request, injectChecksum(checksum2, b?.request) ), parse: mergeObjectArray( a.parse, injectChecksum(checksum2, b?.parse) ), transform: mergeObjectArray( a.transform, injectChecksum(checksum2, b?.transform) ), beforeHandle: mergeObjectArray( mergeObjectArray( // @ts-ignore fnToContainer(a.resolve, "resolve"), a.beforeHandle ), injectChecksum( checksum2, mergeObjectArray( fnToContainer(b?.resolve, "resolve"), b?.beforeHandle ) ) ), afterHandle: mergeObjectArray( a.afterHandle, injectChecksum(checksum2, b?.afterHandle) ), mapResponse: mergeObjectArray( a.mapResponse, injectChecksum(checksum2, b?.mapResponse) ), afterResponse: mergeObjectArray( a.afterResponse, injectChecksum(checksum2, b?.afterResponse) ), // Already merged on Elysia._use, also logic is more complicated, can't directly merge trace: mergeObjectArray( a.trace, injectChecksum(checksum2, b?.trace) ), error: mergeObjectArray( a.error, injectChecksum(checksum2, b?.error) ), stop: mergeObjectArray( a.stop, injectChecksum(checksum2, b?.stop) ) }), asHookType = (fn, inject, { skipIfHasType = !1 }) => { if (!fn) return fn; if (!Array.isArray(fn)) return skipIfHasType ? fn.scope ??= inject : fn.scope = inject, fn; for (let x of fn) skipIfHasType ? x.scope ??= inject : x.scope = inject; return fn; }, filterGlobal = (fn) => { if (!fn) return fn; if (!Array.isArray(fn)) switch (fn.scope) { case "global": case "scoped": return { ...fn }; default: return { fn }; } let array = []; for (let x of fn) switch (x.scope) { case "global": case "scoped": array.push({ ...x }); break; } return array; }, filterGlobalHook = (hook) => ({ // rest is validator ...hook, type: hook?.type, detail: hook?.detail, parse: filterGlobal(hook?.parse), transform: filterGlobal(hook?.transform), beforeHandle: filterGlobal(hook?.beforeHandle), afterHandle: filterGlobal(hook?.afterHandle), mapResponse: filterGlobal(hook?.mapResponse), afterResponse: filterGlobal(hook?.afterResponse), error: filterGlobal(hook?.error), trace: filterGlobal(hook?.trace) }), StatusMap = { Continue: 100, "Switching Protocols": 101, Processing: 102, "Early Hints": 103, OK: 200, Created: 201, Accepted: 202, "Non-Authoritative Information": 203, "No Content": 204, "Reset Content": 205, "Partial Content": 206, "Multi-Status": 207, "Already Reported": 208, "Multiple Choices": 300, "Moved Permanently": 301, Found: 302, "See Other": 303, "Not Modified": 304, "Temporary Redirect": 307, "Permanent Redirect": 308, "Bad Request": 400, Unauthorized: 401, "Payment Required": 402, Forbidden: 403, "Not Found": 404, "Method Not Allowed": 405, "Not Acceptable": 406, "Proxy Authentication Required": 407, "Request Timeout": 408, Conflict: 409, Gone: 410, "Length Required": 411, "Precondition Failed": 412, "Payload Too Large": 413, "URI Too Long": 414, "Unsupported Media Type": 415, "Range Not Satisfiable": 416, "Expectation Failed": 417, "I'm a teapot": 418, "Misdirected Request": 421, "Unprocessable Content": 422, Locked: 423, "Failed Dependency": 424, "Too Early": 425, "Upgrade Required": 426, "Precondition Required": 428, "Too Many Requests": 429, "Request Header Fields Too Large": 431, "Unavailable For Legal Reasons": 451, "Internal Server Error": 500, "Not Implemented": 501, "Bad Gateway": 502, "Service Unavailable": 503, "Gateway Timeout": 504, "HTTP Version Not Supported": 505, "Variant Also Negotiates": 506, "Insufficient Storage": 507, "Loop Detected": 508, "Not Extended": 510, "Network Authentication Required": 511 }, InvertedStatusMap = Object.fromEntries( Object.entries(StatusMap).map(([k, v]) => [v, k]) ); function removeTrailingEquals(digest) { let trimmedDigest = digest; for (; trimmedDigest.endsWith("="); ) trimmedDigest = trimmedDigest.slice(0, -1); return trimmedDigest; } var encoder = new TextEncoder(), signCookie = async (val, secret) => { if (typeof val != "string") throw new TypeError("Cookie value must be provided as a string."); if (secret === null) throw new TypeError("Secret key must be provided."); let secretKey = await crypto.subtle.importKey( "raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, !1, ["sign"] ), hmacBuffer = await crypto.subtle.sign( "HMAC", secretKey, encoder.encode(val) ); return val + "." + removeTrailingEquals(Buffer.from(hmacBuffer).toString("base64")); }, unsignCookie = async (input, secret) => { if (typeof input != "string") throw new TypeError("Signed cookie string must be provided."); if (secret === null) throw new TypeError("Secret key must be provided."); let tentativeValue = input.slice(0, input.lastIndexOf(".")); return await signCookie(tentativeValue, secret) === input ? tentativeValue : !1; }, traceBackMacro = (extension, property, manage) => { if (!(!extension || typeof extension != "object" || !property)) for (let [key, value] of Object.entries(property)) { if (primitiveHookMap[key] || !(key in extension)) continue; let v = extension[key]; if (typeof v == "function") { let hook = v(value); if (typeof hook == "object") for (let [k, v2] of Object.entries(hook)) manage(k)({ fn: v2 }); } delete property[key]; } }, createMacroManager = ({ globalHook, localHook }) => (stackName) => (type, fn) => { if (typeof type == "function" && (type = { fn: type }), stackName === "resolve" && (type = { ...type, subType: "resolve" }), localHook[stackName] || (localHook[stackName] = []), typeof localHook[stackName] == "function" && (localHook[stackName] = [localHook[stackName]]), Array.isArray(localHook[stackName]) || (localHook[stackName] = [localHook[stackName]]), "fn" in type || Array.isArray(type)) { Array.isArray(type) ? localHook[stackName] = localHook[stackName].concat(type) : localHook[stackName].push(type); return; } let { insert = "after", stack = "local" } = type; typeof fn == "function" && (fn = { fn }), stack === "global" ? Array.isArray(fn) ? insert === "before" ? globalHook[stackName] = fn.concat( globalHook[stackName] ) : globalHook[stackName] = globalHook[stackName].concat(fn) : insert === "before" ? globalHook[stackName].unshift(fn) : globalHook[stackName].push(fn) : Array.isArray(fn) ? insert === "before" ? localHook[stackName] = fn.concat(localHook[stackName]) : localHook[stackName] = localHook[stackName].concat(fn) : insert === "before" ? localHook[stackName].unshift(fn) : localHook[stackName].push(fn); }, parseNumericString = (message) => { if (typeof message == "number") return message; if (message.length < 16) { if (message.trim().length === 0) return null; let length = Number(message); return Number.isNaN(length) ? null : length; } if (message.length === 16) { if (message.trim().length === 0) return null; let number = Number(message); return Number.isNaN(number) || number.toString() !== message ? null : number; } return null; }, isNumericString = (message) => parseNumericString(message) !== null, PromiseGroup = class { constructor(onError = console.error, onFinally = () => { }) { this.onError = onError; this.onFinally = onFinally; this.root = null; this.promises = []; } /** * The number of promises still being awaited. */ get size() { return this.promises.length; } /** * Add a promise to the group. * @returns The promise that was added. */ add(promise) { return this.promises.push(promise), this.root ||= this.drain(), this.promises.length === 1 && this.then(this.onFinally), promise; } async drain() { for (; this.promises.length > 0; ) { try { await this.promises[0]; } catch (error) { this.onError(error); } this.promises.shift(); } this.root = null; } // Allow the group to be awaited. then(onfulfilled, onrejected) { return (this.root ?? Promise.resolve()).then(onfulfilled, onrejected); } }, fnToContainer = (fn, subType) => { if (!fn) return fn; if (!Array.isArray(fn)) { if (typeof fn == "function" || typeof fn == "string") return subType ? { fn, subType } : { fn }; if ("fn" in fn) return fn; } let fns = []; for (let x of fn) typeof x == "function" || typeof x == "string" ? fns.push(subType ? { fn: x, subType } : { fn: x }) : "fn" in x && fns.push(x); return fns; }, localHookToLifeCycleStore = (a) => (a.start && (a.start = fnToContainer(a.start)), a.request && (a.request = fnToContainer(a.request)), a.parse && (a.parse = fnToContainer(a.parse)), a.transform && (a.transform = fnToContainer(a.transform)), a.beforeHandle && (a.beforeHandle = fnToContainer(a.beforeHandle)), a.afterHandle && (a.afterHandle = fnToContainer(a.afterHandle)), a.mapResponse && (a.mapResponse = fnToContainer(a.mapResponse)), a.afterResponse && (a.afterResponse = fnToContainer(a.afterResponse)), a.trace && (a.trace = fnToContainer(a.trace)), a.error && (a.error = fnToContainer(a.error)), a.stop && (a.stop = fnToContainer(a.stop)), a), lifeCycleToFn = (a) => { let lifecycle = /* @__PURE__ */ Object.create(null); return a.start?.map && (lifecycle.start = a.start.map((x) => x.fn)), a.request?.map && (lifecycle.request = a.request.map((x) => x.fn)), a.parse?.map && (lifecycle.parse = a.parse.map((x) => x.fn)), a.transform?.map && (lifecycle.transform = a.transform.map((x) => x.fn)), a.beforeHandle?.map && (lifecycle.beforeHandle = a.beforeHandle.map((x) => x.fn)), a.afterHandle?.map && (lifecycle.afterHandle = a.afterHandle.map((x) => x.fn)), a.mapResponse?.map && (lifecycle.mapResponse = a.mapResponse.map((x) => x.fn)), a.afterResponse?.map && (lifecycle.afterResponse = a.afterResponse.map((x) => x.fn)), a.error?.map && (lifecycle.error = a.error.map((x) => x.fn)), a.stop?.map && (lifecycle.stop = a.stop.map((x) => x.fn)), a.trace?.map ? lifecycle.trace = a.trace.map((x) => x.fn) : lifecycle.trace = [], lifecycle; }, cloneInference = (inference) => ({ body: inference.body, cookie: inference.cookie, headers: inference.headers, query: inference.query, set: inference.set, server: inference.server, path: inference.path, route: inference.route, url: inference.url }), redirect = (url, status = 302) => Response.redirect(url, status), ELYSIA_FORM_DATA = Symbol("ElysiaFormData"), ELYSIA_REQUEST_ID = Symbol("ElysiaRequestId"), form = (items) => { let formData = new FormData(); if (formData[ELYSIA_FORM_DATA] = {}, items) for (let [key, value] of Object.entries(items)) { if (Array.isArray(value)) { formData[ELYSIA_FORM_DATA][key] = []; for (let v of value) value instanceof File ? formData.append(key, value, value.name) : value instanceof ElysiaFile ? formData.append(key, value.value, value.value?.name) : formData.append(key, value), formData[ELYSIA_FORM_DATA][key].push(value); continue; } value instanceof File ? formData.append(key, value, value.name) : value instanceof ElysiaFile ? formData.append(key, value.value, value.value?.name) : formData.append(key, value), formData[ELYSIA_FORM_DATA][key] = value; } return formData; }, randomId = () => { let uuid = crypto.randomUUID(); return uuid.slice(0, 8) + uuid.slice(24, 32); }, deduplicateChecksum = (array) => { if (!array.length) return []; let hashes = []; for (let i = 0; i < array.length; i++) { let item = array[i]; item.checksum && (hashes.includes(item.checksum) && (array.splice(i, 1), i--), hashes.push(item.checksum)); } return array; }, promoteEvent = (events, as = "scoped") => { if (events) { if (as === "scoped") { for (let event of events) "scope" in event && event.scope === "local" && (event.scope = "scoped"); return; } for (let event of events) "scope" in event && (event.scope = "global"); } }, getLoosePath = (path) => path.charCodeAt(path.length - 1) === 47 ? path.slice(0, path.length - 1) : path + "/", isNotEmpty = (obj) => { if (!obj) return !1; for (let _ in obj) return !0; return !1; }, encodePath = (path, { dynamic = !1 } = {}) => { let encoded = encodeURIComponent(path).replace(/%2F/g, "/"); return dynamic && (encoded = encoded.replace(/%3A/g, ":").replace(/%3F/g, "?")), encoded; }, supportPerMethodInlineHandler = (() => { if (typeof Bun > "u") return !0; let semver = Bun.version.split("."); return !(+semver[0] < 1 || +semver[1] < 2 || +semver[2] < 14); })(), sse = (payload) => (typeof payload == "string" && (payload = { data: payload }), payload.id === void 0 && (payload.id = randomId()), payload.toStream = () => { let payloadString = ""; return payload.id !== void 0 && payload.id !== null && (payloadString += `id: ${payload.id} `), payload.event && (payloadString += `event: ${payload.event} `), payload.retry !== void 0 && (payloadString += `retry: ${payload.retry} `), payload.data === null ? payloadString += `data: null ` : typeof payload.data == "string" ? payloadString += `data: ${payload.data} ` : typeof payload.data == "object" && (payloadString += `data: ${JSON.stringify(payload.data)} `), payloadString && (payloadString += ` `), payloadString; }, payload); export { ELYSIA_FORM_DATA, ELYSIA_REQUEST_ID, InvertedStatusMap, PromiseGroup, StatusMap, asHookType, checksum, cloneInference, createMacroManager, deduplicateChecksum, encodePath, filterGlobalHook, fnToContainer, form, getLoosePath, hasHeaderShorthand, injectChecksum, isClass, isNotEmpty, isNumericString, lifeCycleToArray, lifeCycleToFn, localHookToLifeCycleStore, mergeCookie, mergeDeep, mergeHook, mergeLifeCycle, mergeObjectArray, mergeResponse, mergeSchemaValidator, primitiveHooks, promoteEvent, randomId, redirect, replaceUrlPath, signCookie, sse, supportPerMethodInlineHandler, traceBackMacro, unsignCookie };