routup
Version:
Routup is a minimalistic http based routing framework.
1 lines • 185 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","sources":["../src/constants.ts","../src/request/helpers/cache.ts","../src/utils/cookie.ts","../src/utils/object.ts","../src/utils/etag/module.ts","../src/utils/etag/utils.ts","../src/utils/trust-proxy/module.ts","../src/utils/is-instance.ts","../src/utils/mime.ts","../src/utils/method.ts","../src/utils/next.ts","../src/utils/path.ts","../src/utils/promise.ts","../src/utils/stream.ts","../src/utils/url.ts","../src/utils/web.ts","../src/request/helpers/env.ts","../src/request/helpers/header.ts","../src/request/helpers/negotiator.ts","../src/request/helpers/header-accept.ts","../src/request/helpers/header-accept-charset.ts","../src/request/helpers/header-accept-encoding.ts","../src/request/helpers/header-accept-language.ts","../src/request/helpers/header-content-type.ts","../src/router-options/module.ts","../src/request/helpers/router.ts","../src/request/helpers/hostname.ts","../src/request/helpers/http2.ts","../src/request/helpers/ip.ts","../src/request/helpers/mount-path.ts","../src/request/helpers/params.ts","../src/request/helpers/path.ts","../src/request/helpers/protocol.ts","../src/request/module.ts","../src/error/module.ts","../src/error/is.ts","../src/error/create.ts","../src/response/helpers/cache.ts","../src/response/helpers/gone.ts","../src/response/helpers/event-stream/utils.ts","../src/response/helpers/event-stream/module.ts","../src/response/helpers/event-stream/factory.ts","../src/response/helpers/header.ts","../src/response/helpers/utils.ts","../src/response/helpers/header-attachment.ts","../src/response/helpers/header-content-type.ts","../src/response/helpers/send-stream.ts","../src/response/helpers/send-web-blob.ts","../src/response/helpers/send-web-response.ts","../src/response/helpers/send.ts","../src/response/helpers/send-accepted.ts","../src/response/helpers/send-created.ts","../src/response/helpers/send-file.ts","../src/response/helpers/send-format.ts","../src/response/helpers/send-redirect.ts","../src/response/module.ts","../src/dispatcher/event/dispatch.ts","../src/dispatcher/event/is.ts","../src/dispatcher/event/module.ts","../src/dispatcher/event/error.ts","../src/adapters/node/module.ts","../src/adapters/raw/header.ts","../src/adapters/raw/module.ts","../src/adapters/web/module.ts","../src/handler/constants.ts","../src/hook/constants.ts","../src/hook/module.ts","../src/path/matcher.ts","../src/path/utils.ts","../src/handler/module.ts","../src/handler/core/define.ts","../src/handler/error/define.ts","../src/handler/is.ts","../src/plugin/is.ts","../src/router-options/transform.ts","../src/router/constants.ts","../src/router/utils.ts","../src/router/module.ts"],"sourcesContent":["export enum MethodName {\n GET = 'GET',\n POST = 'POST',\n PUT = 'PUT',\n PATCH = 'PATCH',\n DELETE = 'DELETE',\n OPTIONS = 'OPTIONS',\n HEAD = 'HEAD',\n}\n\nexport enum HeaderName {\n ACCEPT = 'accept',\n ACCEPT_CHARSET = 'accept-charset',\n ACCEPT_ENCODING = 'accept-encoding',\n ACCEPT_LANGUAGE = 'accept-language',\n ACCEPT_RANGES = 'accept-ranges',\n ALLOW = 'allow',\n CACHE_CONTROL = 'cache-control',\n CONTENT_DISPOSITION = 'content-disposition',\n CONTENT_ENCODING = 'content-encoding',\n CONTENT_LENGTH = 'content-length',\n CONTENT_RANGE = 'content-range',\n CONTENT_TYPE = 'content-type',\n CONNECTION = 'connection',\n COOKIE = 'cookie',\n ETag = 'etag',\n HOST = 'host',\n IF_MODIFIED_SINCE = 'if-modified-since',\n IF_NONE_MATCH = 'if-none-match',\n LAST_MODIFIED = 'last-modified',\n LOCATION = 'location',\n RANGE = 'range',\n RATE_LIMIT_LIMIT = 'ratelimit-limit',\n RATE_LIMIT_REMAINING = 'ratelimit-remaining',\n RATE_LIMIT_RESET = 'ratelimit-reset',\n RETRY_AFTER = 'retry-after',\n SET_COOKIE = 'set-cookie',\n TRANSFER_ENCODING = 'transfer-encoding',\n X_ACCEL_BUFFERING = 'x-accel-buffering',\n X_FORWARDED_HOST = 'x-forwarded-host',\n X_FORWARDED_FOR = 'x-forwarded-for',\n X_FORWARDED_PROTO = 'x-forwarded-proto',\n}\n","import { HeaderName } from '../../constants';\n\nimport type { Request } from '../types';\n\nexport function isRequestCacheable(req: Request, modifiedTime: string | Date) : boolean {\n const modifiedSince = req.headers[HeaderName.IF_MODIFIED_SINCE];\n if (!modifiedSince) {\n return false;\n }\n\n modifiedTime = typeof modifiedTime === 'string' ?\n new Date(modifiedTime) :\n modifiedTime;\n\n return new Date(modifiedSince) >= modifiedTime;\n}\n","/*\n Set-Cookie header field-values are sometimes comma joined in one string. This splits them without choking on commas\n that are within a single set-cookie field-value, such as in the Expires portion.\n\n This is uncommon, but explicitly allowed - see https://tools.ietf.org/html/rfc2616#section-4.2\n Node.js does this for every header *except* set-cookie - see https://github.com/nodejs/node/blob/d5e363b77ebaf1caf67cd7528224b651c86815c1/lib/_http_incoming.js#L128\n React Native's fetch does this for *every* header, including set-cookie.\n\n Based on: https://github.com/google/j2objc/commit/16820fdbc8f76ca0c33472810ce0cb03d20efe25\n Credits to: https://github.com/tomball for original and https://github.com/chrusart for JavaScript implementation\n*/\nexport function splitCookiesString(input?: string | string[]) : string[] {\n if (Array.isArray(input)) {\n return input.flatMap((el) => splitCookiesString(el));\n }\n\n if (typeof input !== 'string') {\n return [];\n }\n\n const cookiesStrings = [];\n let pos = 0;\n let start;\n let ch;\n let lastComma;\n let nextStart;\n let cookiesSeparatorFound;\n\n const skipWhitespace = () => {\n while (pos < input.length && /\\s/.test(input.charAt(pos))) {\n pos += 1;\n }\n return pos < input.length;\n };\n\n const notSpecialChar = () => {\n ch = input.charAt(pos);\n\n return ch !== '=' && ch !== ';' && ch !== ',';\n };\n\n while (pos < input.length) {\n start = pos;\n cookiesSeparatorFound = false;\n\n while (skipWhitespace()) {\n ch = input.charAt(pos);\n if (ch === ',') {\n // ',' is a cookie separator if we have later first '=', not ';' or ','\n lastComma = pos;\n pos += 1;\n\n skipWhitespace();\n nextStart = pos;\n\n while (pos < input.length && notSpecialChar()) {\n pos += 1;\n }\n\n // currently special character\n if (pos < input.length && input.charAt(pos) === '=') {\n // we found cookies separator\n cookiesSeparatorFound = true;\n // pos is inside the next cookie, so back up and return it.\n pos = nextStart;\n cookiesStrings.push(input.substring(start, lastComma));\n start = pos;\n } else {\n // in param ',' or param separator ';',\n // we continue from that comma\n pos = lastComma + 1;\n }\n } else {\n pos += 1;\n }\n }\n\n if (!cookiesSeparatorFound || pos >= input.length) {\n cookiesStrings.push(input.substring(start, input.length));\n }\n }\n\n return cookiesStrings;\n}\n","export function isObject(item: unknown) : item is Record<string, any> {\n return (\n !!item &&\n typeof item === 'object' &&\n !Array.isArray(item)\n );\n}\n\nexport function setProperty(\n record: Record<PropertyKey, any>,\n property: PropertyKey,\n value: any,\n): void {\n (record as any)[property] = value;\n}\n\nexport function getProperty<T = any>(\n req: Record<PropertyKey, any>,\n property: PropertyKey,\n): T {\n return (req as any)[property];\n}\n","import { Buffer } from 'buffer';\nimport { subtle } from 'uncrypto';\nimport { type Stats } from 'node:fs';\nimport { isObject } from '../object';\nimport type { EtagOptions } from './type';\n\n/**\n * Determine if object is a Stats object.\n *\n * @param {object} obj\n * @return {boolean}\n * @api private\n */\nfunction isStatsObject(obj: unknown) : obj is Stats {\n // quack quack\n return isObject(obj) &&\n 'ctime' in obj && Object.prototype.toString.call(obj.ctime) === '[object Date]' &&\n 'mtime' in obj && Object.prototype.toString.call(obj.mtime) === '[object Date]' &&\n 'ino' in obj && typeof obj.ino === 'number' &&\n 'size' in obj && typeof obj.size === 'number';\n}\n\nasync function sha1(str: string) : Promise<string> {\n const enc = new TextEncoder();\n const hash = await subtle.digest('SHA-1', enc.encode(str));\n\n return btoa(String.fromCharCode(...new Uint8Array(hash)));\n}\n\n/**\n * Generate an ETag.\n */\nexport async function generateETag(input: string | Buffer | Stats) : Promise<string> {\n if (isStatsObject(input)) {\n const mtime = input.mtime.getTime().toString(16);\n const size = input.size.toString(16);\n\n return `\"${size}-${mtime}\"`;\n }\n\n if (input.length === 0) {\n // fast-path empty\n return '\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"';\n }\n\n const entity = Buffer.isBuffer(input) ?\n input.toString('utf-8') :\n input;\n\n // compute hash of entity\n const hash = await sha1(entity);\n\n return `\"${entity.length.toString(16)}-${hash.substring(0, 27)}\"`;\n}\n\n/**\n * Create a simple ETag.\n */\nexport async function createEtag(\n input: string | Buffer | Stats,\n options?: EtagOptions,\n) : Promise<string> {\n options = options || {};\n\n const weak = typeof options.weak === 'boolean' ?\n options.weak :\n isStatsObject(input);\n\n // generate entity tag\n const tag = await generateETag(input);\n\n return weak ?\n `W/${tag}` :\n tag;\n}\n","import { Buffer } from 'buffer';\nimport { merge } from 'smob';\nimport { isObject } from '../object';\nimport { createEtag } from './module';\nimport type { EtagFn, EtagOptions } from './type';\n\nexport function buildEtagFn(input?: boolean | EtagOptions | EtagFn) : EtagFn {\n if (typeof input === 'function') {\n return input;\n }\n\n input = input ?? true;\n\n if (input === false) {\n return () => Promise.resolve(undefined);\n }\n\n let options : EtagOptions = {\n weak: true,\n };\n\n if (isObject(input)) {\n options = merge(input, options);\n }\n\n return async (body: any, encoding?: BufferEncoding, size?: number) => {\n const buff = Buffer.isBuffer(body) ?\n body :\n Buffer.from(body, encoding);\n\n if (typeof options.threshold !== 'undefined') {\n size = size ?? Buffer.byteLength(buff);\n\n if (size <= options.threshold) {\n return undefined;\n }\n }\n\n return createEtag(buff, options);\n };\n}\n","import { compile } from 'proxy-addr';\nimport type { TrustProxyFn } from './type';\n\nexport function buildTrustProxyFn(\n input?: boolean | number | string | string[] | TrustProxyFn,\n) : TrustProxyFn {\n if (typeof input === 'function') {\n return input;\n }\n\n if (input === true) {\n return () => true;\n }\n\n if (typeof input === 'number') {\n return (_address, hop) => hop < (input as number);\n }\n\n if (typeof input === 'string') {\n input = input.split(',')\n .map((value) => value.trim());\n }\n\n return compile(input || []);\n}\n","import { isObject } from './object';\n\nexport function isInstance(\n input: unknown,\n sym: symbol,\n) {\n if (!isObject(input)) {\n return false;\n }\n\n return (input as { '@instanceof': symbol })['@instanceof'] === sym;\n}\n","import { get, getType } from 'mime-explorer';\n\nexport function getMimeType(type: string) : string | undefined {\n if (type.indexOf('/') !== -1) {\n return type;\n }\n\n return getType(type);\n}\n\nexport function getCharsetForMimeType(type: string) : string | undefined {\n if ((/^text\\/|^application\\/(javascript|json)/).test(type)) {\n return 'utf-8';\n }\n\n const meta = get(type);\n if (\n meta &&\n meta.charset\n ) {\n return meta.charset.toLowerCase();\n }\n\n return undefined;\n}\n","import type { MethodName } from '../constants';\n\nexport function toMethodName(input: string | undefined) : MethodName | undefined;\nexport function toMethodName(input: string | undefined, alt: MethodName) : MethodName;\nexport function toMethodName(\n input?: string,\n alt?: MethodName,\n) : MethodName | undefined {\n if (input) {\n return input.toUpperCase() as MethodName;\n }\n\n return alt;\n}\n","import type { Next } from '../types';\n\nexport const nextPlaceholder: Next = (_err?: Error) => {};\n","/**\n * Based on https://github.com/unjs/pathe v1.1.1 (055f50a6f1131f4e5c56cf259dd8816168fba329)\n */\n\nfunction normalizeWindowsPath(input = '') {\n if (!input || !input.includes('\\\\')) {\n return input;\n }\n\n return input.replace(/\\\\/g, '/');\n}\n\nconst EXTNAME_RE = /.(\\.[^./]+)$/;\nexport function extname(input: string) {\n const match = EXTNAME_RE.exec(normalizeWindowsPath(input));\n return (match && match[1]) || '';\n}\n\nexport function basename(input: string, extension? :string) {\n const lastSegment = normalizeWindowsPath(input)\n .split('/')\n .pop();\n\n if (!lastSegment) {\n return input;\n }\n\n return extension && lastSegment.endsWith(extension) ?\n lastSegment.slice(0, -extension.length) :\n lastSegment;\n}\n","import { isObject } from './object';\n\nexport function isPromise(p: unknown) : p is Promise<unknown> {\n return isObject(p) &&\n (\n p instanceof Promise ||\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n typeof p.then === 'function'\n );\n}\n","import type { ReadableStream as WebReadableStream } from 'stream/web';\nimport type { Readable as NodeReadable } from 'node:stream';\nimport type { Readable } from 'readable-stream';\nimport { isObject } from './object';\n\nexport function isNodeStream(input: unknown): input is NodeReadable | Readable {\n return isObject(input) &&\n typeof input.pipe === 'function' &&\n typeof input.read === 'function';\n}\n\nexport function isWebStream(input: unknown): input is ReadableStream | WebReadableStream {\n return isObject(input) && typeof input.pipeTo === 'function';\n}\n\nexport function isStream(data: any): data is NodeReadable | Readable | ReadableStream | WebReadableStream {\n return isNodeStream(data) || isWebStream(data);\n}\n","const TRAILING_SLASH_RE = /\\/$|\\/\\?/;\n\nexport function hasTrailingSlash(input = '', queryParams = false): boolean {\n if (!queryParams) {\n return input.endsWith('/');\n }\n\n return TRAILING_SLASH_RE.test(input);\n}\n\nexport function withoutTrailingSlash(input = '', queryParams = false): string {\n if (!queryParams) {\n return (hasTrailingSlash(input) ? input.slice(0, -1) : input) || '/';\n }\n\n if (!hasTrailingSlash(input, true)) {\n return input || '/';\n }\n\n const [s0, ...s] = input.split('?');\n\n return (s0.slice(0, -1) || '/') + (s.length ? `?${s.join('?')}` : '');\n}\n\nexport function withTrailingSlash(input = '', queryParams = false): string {\n if (!queryParams) {\n return input.endsWith('/') ? input : (`${input}/`);\n }\n\n if (hasTrailingSlash(input, true)) {\n return input || '/';\n }\n\n const [s0, ...s] = input.split('?');\n return `${s0}/${s.length ? `?${s.join('?')}` : ''}`;\n}\n\nexport function hasLeadingSlash(input = ''): boolean {\n return input.startsWith('/');\n}\n\nexport function withoutLeadingSlash(input = ''): string {\n return (hasLeadingSlash(input) ? input.substring(1) : input) || '/';\n}\n\nexport function withLeadingSlash(input = ''): string {\n return hasLeadingSlash(input) ? input : `/${input}`;\n}\n\nexport function cleanDoubleSlashes(input = ''): string {\n if (input.indexOf('://') !== -1) {\n return input.split('://')\n .map((str) => cleanDoubleSlashes(str))\n .join('://');\n }\n\n return input.replace(/\\/+/g, '/');\n}\n","import type { WebBlob, WebResponse } from '../types';\n\nexport function isWebBlob(input: unknown) : input is WebBlob {\n return typeof Blob !== 'undefined' && input instanceof Blob;\n}\n\nexport function isWebResponse(input: unknown): input is WebResponse {\n return typeof Response !== 'undefined' && input instanceof Response;\n}\n","import { hasOwnProperty, merge } from 'smob';\nimport { getProperty, setProperty } from '../../utils';\n\nimport type { Request } from '../types';\n\nconst symbol = Symbol.for('ReqEnv');\n\nexport function setRequestEnv(req: Request, key: string | symbol, value: unknown) : void;\nexport function setRequestEnv(req: Request, record: Record<string | symbol, any>, append?: boolean) : void;\nexport function setRequestEnv(req: Request, key: Record<string | symbol, any> | string | symbol, value?: boolean | unknown) : void {\n const propertyValue = getProperty(req, symbol);\n\n if (propertyValue) {\n if (typeof key === 'object') {\n if (value) {\n setProperty(req, symbol, merge(propertyValue, key));\n } else {\n setProperty(req, symbol, key);\n }\n } else {\n propertyValue[key] = value;\n setProperty(req, symbol, propertyValue);\n }\n\n return;\n }\n\n if (typeof key === 'object') {\n setProperty(req, symbol, key);\n return;\n }\n\n setProperty(req, symbol, {\n [key]: value,\n });\n}\n\nexport function useRequestEnv(req: Request) : Record<string, any>;\nexport function useRequestEnv(req: Request, key: PropertyKey) : unknown | undefined;\nexport function useRequestEnv(req: Request, key?: PropertyKey) {\n const propertyValue = getProperty(req, symbol);\n if (propertyValue) {\n if (typeof key !== 'undefined') {\n return propertyValue[key];\n }\n\n return propertyValue;\n }\n\n if (typeof key !== 'undefined') {\n return undefined;\n }\n\n return {};\n}\n\nexport function unsetRequestEnv(req: Request, key: PropertyKey) {\n const propertyValue = getProperty(req, symbol);\n if (hasOwnProperty(propertyValue, key)) {\n delete propertyValue[key];\n }\n}\n","import type { IncomingHttpHeaders } from 'node:http';\n\nimport type { Request } from '../types';\n\nexport function getRequestHeader<K extends keyof IncomingHttpHeaders>(\n req: Request,\n name: K,\n) : IncomingHttpHeaders[K] {\n return req.headers[name];\n}\n\nexport function setRequestHeader<K extends keyof IncomingHttpHeaders>(\n req: Request,\n name: K,\n value: IncomingHttpHeaders[K],\n) {\n req.headers[name] = value;\n}\n","import Negotiator from 'negotiator';\nimport { getProperty, setProperty } from '../../utils';\n\nimport type { Request } from '../types';\n\nconst symbol = Symbol.for('ReqNegotiator');\n\nexport function useRequestNegotiator(req: Request) : Negotiator {\n let value = getProperty(req, symbol);\n if (value) {\n return value;\n }\n\n value = new Negotiator(req);\n setProperty(req, symbol, value);\n return value;\n}\n","import { HeaderName } from '../../constants';\nimport { getMimeType } from '../../utils';\nimport type { Request } from '../types';\nimport { getRequestHeader } from './header';\nimport { useRequestNegotiator } from './negotiator';\n\nexport function getRequestAcceptableContentTypes(req: Request) : string[] {\n const negotiator = useRequestNegotiator(req);\n\n return negotiator.mediaTypes();\n}\n\nexport function getRequestAcceptableContentType(req: Request, input?: string | string[]) : string | undefined {\n input = input || [];\n\n const items = Array.isArray(input) ? input : [input];\n\n if (items.length === 0) {\n return getRequestAcceptableContentTypes(req).shift();\n }\n\n const header = getRequestHeader(req, HeaderName.ACCEPT);\n if (!header) {\n return items[0];\n }\n\n let polluted = false;\n const mimeTypes : string[] = [];\n for (let i = 0; i < items.length; i++) {\n const mimeType = getMimeType(items[i]);\n if (mimeType) {\n mimeTypes.push(mimeType);\n } else {\n polluted = true;\n }\n }\n\n const negotiator = useRequestNegotiator(req);\n const matches = negotiator.mediaTypes(mimeTypes);\n if (matches.length > 0) {\n if (polluted) {\n return items[0];\n }\n\n return items[mimeTypes.indexOf(matches[0])];\n }\n\n return undefined;\n}\n","import type { Request } from '../types';\nimport { useRequestNegotiator } from './negotiator';\n\nexport function getRequestAcceptableCharsets(req: Request) : string[] {\n const negotiator = useRequestNegotiator(req);\n\n return negotiator.charsets();\n}\n\nexport function getRequestAcceptableCharset(req: Request, input: string | string[]) : string | undefined {\n input = input || [];\n\n const items = Array.isArray(input) ? input : [input];\n\n if (items.length === 0) {\n return getRequestAcceptableCharsets(req).shift();\n }\n\n const negotiator = useRequestNegotiator(req);\n return negotiator.charsets(items).shift() || undefined;\n}\n","import type { IncomingMessage } from 'node:http';\nimport { useRequestNegotiator } from './negotiator';\n\nexport function getRequestAcceptableEncodings(req: IncomingMessage) : string[] {\n const negotiator = useRequestNegotiator(req);\n return negotiator.encodings();\n}\n\nexport function getRequestAcceptableEncoding(req: IncomingMessage, input: string | string[]) : string | undefined {\n input = input || [];\n\n const items = Array.isArray(input) ? input : [input];\n\n if (items.length === 0) {\n return getRequestAcceptableEncodings(req).shift();\n }\n\n const negotiator = useRequestNegotiator(req);\n return negotiator.encodings(items).shift() || undefined;\n}\n","import type { Request } from '../types';\nimport { useRequestNegotiator } from './negotiator';\n\nexport function getRequestAcceptableLanguages(req: Request) : string[] {\n const negotiator = useRequestNegotiator(req);\n return negotiator.languages();\n}\n\nexport function getRequestAcceptableLanguage(req: Request, input?: string | string[]) : string | undefined {\n input = input || [];\n\n const items = Array.isArray(input) ? input : [input];\n\n if (items.length === 0) {\n return getRequestAcceptableLanguages(req).shift();\n }\n\n const negotiator = useRequestNegotiator(req);\n return negotiator.languages(items).shift() || undefined;\n}\n","import { HeaderName } from '../../constants';\nimport { getMimeType } from '../../utils';\nimport type { Request } from '../types';\nimport { getRequestHeader } from './header';\n\nexport function matchRequestContentType(req: Request, contentType: string) : boolean {\n const header = getRequestHeader(req, HeaderName.CONTENT_TYPE);\n if (!header) {\n return true;\n }\n\n /* istanbul ignore next */\n if (Array.isArray(header)) {\n if (header.length === 0) {\n return true;\n }\n\n return header[0] === getMimeType(contentType);\n }\n\n return header.split('; ').shift() === getMimeType(contentType);\n}\n","import { hasOwnProperty } from 'smob';\nimport { buildEtagFn } from '../utils';\nimport type { RouterOptions } from './type';\n\nconst defaults : RouterOptions = {\n trustProxy: () => false,\n subdomainOffset: 2,\n etag: buildEtagFn(),\n proxyIpMax: 0,\n};\n\nconst instances : Record<number, Partial<RouterOptions>> = {};\n\nexport function setRouterOptions(id: number, input: Partial<RouterOptions>) {\n instances[id] = input;\n}\n\nexport function unsetRouterOptions(id: number) {\n delete instances[id];\n}\n\nexport function findRouterOption<\nK extends keyof RouterOptions,\n>(key: K, path?: number[]) : RouterOptions[K] {\n if (!path || path.length === 0) {\n return defaults[key];\n }\n\n if (path.length > 0) {\n for (let i = path.length; i >= 0; i--) {\n if (\n hasOwnProperty(instances, path[i]) &&\n typeof instances[path[i]][key] !== 'undefined'\n ) {\n return instances[path[i]][key] as RouterOptions[K];\n }\n }\n }\n\n return defaults[key];\n}\n","import { getProperty, setProperty } from '../../utils';\nimport type { Request } from '../types';\n\nconst routerSymbol = Symbol.for('ReqRouterID');\nexport function setRequestRouterPath(req: Request, path: number[]) {\n setProperty(req, routerSymbol, path);\n}\n\nexport function useRequestRouterPath(req: Request) : number[] | undefined {\n return getProperty(req, routerSymbol);\n}\n","import { HeaderName } from '../../constants';\nimport { findRouterOption } from '../../router-options';\nimport type { TrustProxyFn, TrustProxyInput } from '../../utils';\nimport { buildTrustProxyFn } from '../../utils';\nimport type { Request } from '../types';\nimport { useRequestRouterPath } from './router';\n\ntype RequestHostNameOptions = {\n trustProxy?: TrustProxyInput\n};\n\nexport function getRequestHostName(req: Request, options?: RequestHostNameOptions) : string | undefined {\n options = options || {};\n\n let trustProxy : TrustProxyFn;\n if (typeof options.trustProxy !== 'undefined') {\n trustProxy = buildTrustProxyFn(options.trustProxy);\n } else {\n trustProxy = findRouterOption(\n 'trustProxy',\n useRequestRouterPath(req),\n );\n }\n\n let hostname = req.headers[HeaderName.X_FORWARDED_HOST];\n if (!hostname || !req.socket.remoteAddress || !trustProxy(req.socket.remoteAddress, 0)) {\n hostname = req.headers[HeaderName.HOST];\n } else {\n hostname = Array.isArray(hostname) ? hostname.pop() : hostname;\n if (hostname && hostname.indexOf(',') !== -1) {\n hostname = hostname.substring(0, hostname.indexOf(',')).trimEnd();\n }\n }\n\n if (!hostname) {\n return undefined;\n }\n\n // IPv6 literal support\n const offset = hostname[0] === '[' ?\n hostname.indexOf(']') + 1 :\n 0;\n const index = hostname.indexOf(':', offset);\n\n return index !== -1 ?\n hostname.substring(0, index) :\n hostname;\n}\n","import type { Request } from '../types';\nimport { getRequestHeader } from './header';\n\nexport function isRequestHTTP2(req: Request) {\n return (\n typeof getRequestHeader(req, ':path') !== 'undefined' &&\n typeof getRequestHeader(req, ':method') !== 'undefined'\n );\n}\n","import { all } from 'proxy-addr';\nimport { findRouterOption } from '../../router-options';\nimport type { TrustProxyFn, TrustProxyInput } from '../../utils';\nimport { buildTrustProxyFn } from '../../utils';\nimport type { Request } from '../types';\nimport { useRequestRouterPath } from './router';\n\ntype RequestIpOptions = {\n trustProxy?: TrustProxyInput\n};\n\nexport function getRequestIP(req: Request, options?: RequestIpOptions) : string {\n options = options || {};\n\n let trustProxy : TrustProxyFn;\n if (typeof options.trustProxy !== 'undefined') {\n trustProxy = buildTrustProxyFn(options.trustProxy);\n } else {\n trustProxy = findRouterOption(\n 'trustProxy',\n useRequestRouterPath(req),\n );\n }\n\n const addrs = all(req, trustProxy);\n return addrs[addrs.length - 1];\n}\n","import { getProperty, setProperty } from '../../utils';\nimport type { Request } from '../types';\n\nconst symbol = Symbol.for('ReqMountPath');\n\nexport function useRequestMountPath(req: Request) : string {\n return getProperty<string>(req, symbol) || '/';\n}\n\nexport function setRequestMountPath(req: Request, basePath: string) {\n setProperty(req, symbol, basePath);\n}\n","import { getProperty, setProperty } from '../../utils';\nimport type { Request } from '../types';\n\nconst symbol = Symbol.for('ReqParams');\n\nexport function useRequestParams(req: Request) : Record<string, any> {\n return getProperty(req, symbol) ||\n getProperty(req, 'params') ||\n {};\n}\n\nexport function useRequestParam(req: Request, key: string) : any {\n return useRequestParams(req)[key];\n}\n\nexport function setRequestParams(\n req: Request,\n data: Record<string, any>,\n) {\n setProperty(req, symbol, data);\n}\n\nexport function setRequestParam(\n req: Request,\n key: string,\n value: any,\n) {\n const params = useRequestParams(req);\n params[key] = value;\n\n setRequestParams(req, params);\n}\n","import { getProperty, setProperty } from '../../utils';\nimport type { Request } from '../types';\n\nconst PathSymbol = Symbol.for('ReqPath');\n\nexport function useRequestPath(req: Request) : string {\n const path = getProperty(req, 'path') ||\n getProperty(req, PathSymbol);\n\n if (path) {\n return path;\n }\n\n if (typeof req.url === 'undefined') {\n return '/';\n }\n\n const parsed = new URL(req.url, 'http://localhost/');\n setProperty(req, PathSymbol, parsed.pathname);\n\n return parsed.pathname;\n}\n","import { hasOwnProperty } from 'smob';\nimport { HeaderName } from '../../constants';\nimport { findRouterOption } from '../../router-options';\nimport type { TrustProxyFn, TrustProxyInput } from '../../utils';\nimport { buildTrustProxyFn } from '../../utils';\nimport type { Request } from '../types';\nimport { useRequestRouterPath } from './router';\n\ntype RequestProtocolOptions = {\n trustProxy?: TrustProxyInput,\n default?: string\n};\n\nexport function getRequestProtocol(\n req: Request,\n options?: RequestProtocolOptions,\n) : string {\n options = options || {};\n\n let trustProxy : TrustProxyFn;\n if (typeof options.trustProxy !== 'undefined') {\n trustProxy = buildTrustProxyFn(options.trustProxy);\n } else {\n trustProxy = findRouterOption(\n 'trustProxy',\n useRequestRouterPath(req),\n );\n }\n\n let protocol : string | undefined = options.default;\n /* istanbul ignore next */\n if (\n hasOwnProperty(req.socket, 'encrypted') &&\n !!req.socket.encrypted\n ) {\n protocol = 'https';\n } else if (!protocol) {\n protocol = 'http';\n }\n\n if (!req.socket.remoteAddress || !trustProxy(req.socket.remoteAddress, 0)) {\n return protocol;\n }\n\n let header = req.headers[HeaderName.X_FORWARDED_PROTO];\n /* istanbul ignore next */\n if (Array.isArray(header)) {\n header = header.pop();\n }\n\n if (!header) {\n return protocol;\n }\n\n const index = header.indexOf(',');\n\n return index !== -1 ?\n header.substring(0, index).trim() :\n header.trim();\n}\n","import type { IncomingMessage } from 'node:http';\nimport type { Readable as NodeReadable } from 'node:stream';\nimport { Readable } from 'readable-stream';\nimport type { ReadableStream as NodeWebReadableStream } from 'stream/web';\nimport { isWebStream } from '../utils';\nimport type { RequestCreateContext } from './types';\n\nexport function createRequest(context: RequestCreateContext) : IncomingMessage {\n let readable: NodeReadable;\n if (context.body) {\n if (isWebStream(context.body)) {\n readable = (Readable as unknown as typeof NodeReadable).fromWeb(context.body as NodeWebReadableStream);\n } else {\n readable = (Readable as unknown as typeof NodeReadable).from(context.body);\n }\n } else {\n readable = new Readable();\n }\n\n const headers : Record<string, string | string[] | undefined> = context.headers || {};\n\n const rawHeaders : string[] = [];\n let keys = Object.keys(headers);\n for (let i = 0; i < keys.length; i++) {\n const header = headers[keys[i]];\n if (Array.isArray(header)) {\n for (let j = 0; j < header.length; j++) {\n rawHeaders.push(keys[i], header[j]);\n }\n } else if (typeof header === 'string') {\n rawHeaders.push(keys[i], header);\n }\n }\n\n const headersDistinct : Record<string, string[]> = {};\n keys = Object.keys(headers);\n for (let i = 0; i < keys.length; i++) {\n const header = headers[keys[i]];\n if (Array.isArray(header)) {\n headersDistinct[keys[i]] = header;\n }\n\n if (typeof header === 'string') {\n headersDistinct[keys[i]] = [header];\n }\n }\n\n Object.defineProperty(readable, 'connection', {\n get() {\n return {\n remoteAddress: '127.0.0.1',\n };\n },\n });\n\n Object.defineProperty(readable, 'socket', {\n get() {\n return {\n remoteAddress: '127.0.0.1',\n };\n },\n });\n\n Object.assign(readable, {\n aborted: false,\n complete: true,\n headers,\n headersDistinct,\n httpVersion: '1.1',\n httpVersionMajor: 1,\n httpVersionMinor: 1,\n method: context.method || 'GET',\n rawHeaders,\n rawTrailers: [],\n trailers: {},\n trailersDistinct: {},\n url: context.url || '/',\n setTimeout(_msecs: number, _callback?: () => void) {\n return this as IncomingMessage;\n },\n } satisfies Omit<IncomingMessage, keyof Readable | 'socket' | 'connection'>);\n\n return readable as IncomingMessage;\n}\n","import { HTTPError } from '@ebec/http';\n\nexport class RoutupError extends HTTPError {\n\n}\n","import { RoutupError } from './module';\n\nexport function isError(input: unknown) : input is RoutupError {\n return input instanceof RoutupError;\n}\n","import type { Input } from '@ebec/http';\nimport { isObject } from '../utils';\nimport { isError } from './is';\nimport { RoutupError } from './module';\n\n/**\n * Create an internal error object by\n * - an existing error (accessible via cause property)\n * - options\n * - message\n *\n * @param input\n */\nexport function createError(input: Input | unknown) : RoutupError {\n if (isError(input)) {\n return input;\n }\n\n if (typeof input === 'string') {\n return new RoutupError(input);\n }\n\n if (!isObject(input)) {\n return new RoutupError();\n }\n\n return new RoutupError({ cause: input }, input);\n}\n","import type { Response } from '../types';\n\nexport type ResponseCacheHeadersOptions = {\n maxAge?: number,\n modifiedTime?: string | Date,\n cacheControls?: string[]\n};\n\nexport function setResponseCacheHeaders(res: Response, options?: ResponseCacheHeadersOptions) {\n options = options || {};\n\n const cacheControls = ['public'].concat(options.cacheControls || []);\n\n if (options.maxAge !== undefined) {\n cacheControls.push(`max-age=${+options.maxAge}`, `s-maxage=${+options.maxAge}`);\n }\n\n if (options.modifiedTime) {\n const modifiedTime = typeof options.modifiedTime === 'string' ?\n new Date(options.modifiedTime) :\n options.modifiedTime;\n\n res.setHeader('last-modified', modifiedTime.toUTCString());\n }\n\n res.setHeader('cache-control', cacheControls.join(', '));\n}\n","import { getProperty, setProperty } from '../../utils';\nimport type { Response } from '../types';\n\nconst symbol = Symbol.for('ResGone');\nexport function isResponseGone(res: Response) {\n if (res.headersSent || res.writableEnded) {\n return true;\n }\n\n return getProperty(res, symbol) ?? false;\n}\n\nexport function setResponseGone(res: Response, value: boolean) {\n setProperty(res, symbol, value);\n}\n","import type { EventStreamMessage } from './types';\n\nexport function serializeEventStreamMessage(message: EventStreamMessage): string {\n let result = '';\n\n if (message.id) {\n result += `id: ${message.id}\\n`;\n }\n\n if (message.event) {\n result += `event: ${message.event}\\n`;\n }\n\n if (\n typeof message.retry === 'number' &&\n Number.isInteger(message.retry)\n ) {\n result += `retry: ${message.retry}\\n`;\n }\n\n result += `data: ${message.data}\\n\\n`;\n\n return result;\n}\n","import { PassThrough } from 'readable-stream';\nimport { HeaderName } from '../../../constants';\nimport { isRequestHTTP2 } from '../../../request';\nimport type { Response } from '../../types';\nimport { setResponseGone } from '../gone';\nimport type { EventStreamListener, EventStreamMessage } from './types';\nimport { serializeEventStreamMessage } from './utils';\n\nexport class EventStream {\n protected response: Response;\n\n protected passThrough : PassThrough;\n\n protected flushed : boolean;\n\n protected eventHandlers : Record<string, EventStreamListener[]>;\n\n constructor(response: Response) {\n this.response = response;\n\n this.passThrough = new PassThrough({\n encoding: 'utf-8',\n });\n\n this.flushed = false;\n\n this.eventHandlers = {};\n\n this.open();\n }\n\n protected open() {\n this.response.req.on('close', () => this.end());\n this.response.req.on('error', (err) => {\n this.emit('error', err);\n this.end();\n });\n\n this.passThrough.on('data', (chunk) => this.response.write(chunk));\n this.passThrough.on('error', (err) => {\n this.emit('error', err);\n this.end();\n });\n this.passThrough.on('close', () => this.end());\n\n this.response.setHeader(\n HeaderName.CONTENT_TYPE,\n 'text/event-stream',\n );\n this.response.setHeader(\n HeaderName.CACHE_CONTROL,\n 'private, no-cache, no-store, no-transform, must-revalidate, max-age=0',\n );\n this.response.setHeader(\n HeaderName.X_ACCEL_BUFFERING,\n 'no',\n );\n\n if (!isRequestHTTP2(this.response.req)) {\n this.response.setHeader(\n HeaderName.CONNECTION,\n 'keep-alive',\n );\n }\n\n this.response.statusCode = 200;\n }\n\n write(message: EventStreamMessage) : void;\n\n write(message: string) : void;\n\n write(message: string | EventStreamMessage) : void {\n if (typeof message === 'string') {\n this.write({ data: message });\n return;\n }\n\n if (\n !this.passThrough.closed &&\n this.passThrough.writable\n ) {\n this.passThrough.write(serializeEventStreamMessage(message));\n }\n }\n\n end() {\n if (this.flushed) return;\n\n this.flushed = true;\n\n if (!this.passThrough.closed) {\n this.passThrough.end();\n }\n\n this.emit('close');\n\n setResponseGone(this.response, true);\n\n this.response.end();\n }\n\n on(event: 'close', listener: EventStreamListener) : void;\n\n on(event: 'error', listener: EventStreamListener) : void;\n\n on(event: string, listener: EventStreamListener) : void {\n if (typeof this.eventHandlers[event] === 'undefined') {\n this.eventHandlers[event] = [];\n }\n\n this.eventHandlers[event].push(listener);\n }\n\n protected emit(event: string, ...args: any[]) : void {\n if (typeof this.eventHandlers[event] === 'undefined') {\n return;\n }\n\n const listeners = this.eventHandlers[event].slice();\n for (let i = 0; i < listeners.length; i++) {\n listeners[i].apply(this, args as any);\n }\n }\n}\n","import type { Response } from '../../types';\nimport { EventStream } from './module';\n\nexport function createEventStream(response: Response) {\n return new EventStream(response);\n}\n","import type { OutgoingHttpHeader } from 'node:http';\n\nimport type { Response } from '../types';\n\nexport function appendResponseHeader(\n res: Response,\n name: string,\n value: OutgoingHttpHeader,\n) {\n let header = res.getHeader(name);\n if (!header) {\n res.setHeader(name, value);\n\n return;\n }\n\n if (!Array.isArray(header)) {\n header = [header.toString()];\n }\n\n res.setHeader(name, [...header, value] as readonly string[]);\n}\n\nexport function appendResponseHeaderDirective(\n res: Response,\n name: string,\n value: OutgoingHttpHeader,\n) {\n let header = res.getHeader(name);\n if (!header) {\n if (Array.isArray(value)) {\n res.setHeader(name, value.join('; '));\n return;\n }\n\n res.setHeader(name, value);\n return;\n }\n\n if (!Array.isArray(header)) {\n if (typeof header === 'string') {\n // split header by directive(s)\n header = header.split('; ');\n }\n\n if (typeof header === 'number') {\n header = [header.toString()];\n }\n }\n\n if (Array.isArray(value)) {\n header.push(...value);\n } else {\n header.push(`${value}`);\n }\n\n header = [...new Set(header)];\n\n res.setHeader(name, header.join('; '));\n}\n","import { HeaderName } from '../../constants';\nimport { extname, getCharsetForMimeType, getMimeType } from '../../utils';\nimport type { Response } from '../types';\n\nexport function setResponseContentTypeByFileName(res: Response, fileName: string) {\n const ext = extname(fileName);\n if (ext) {\n let type = getMimeType(ext.substring(1));\n if (type) {\n const charset = getCharsetForMimeType(type);\n if (charset) {\n type += `; charset=${charset}`;\n }\n res.setHeader(HeaderName.CONTENT_TYPE, type);\n }\n }\n}\n","import { HeaderName } from '../../constants';\n\nimport type { Response } from '../types';\nimport { setResponseContentTypeByFileName } from './utils';\n\nexport function setResponseHeaderAttachment(res: Response, filename?: string) {\n if (typeof filename === 'string') {\n setResponseContentTypeByFileName(res, filename);\n }\n\n res.setHeader(\n HeaderName.CONTENT_DISPOSITION,\n `attachment${filename ? `; filename=\"${filename}\"` : ''}`,\n );\n}\n","import { HeaderName } from '../../constants';\nimport { getMimeType } from '../../utils';\nimport type { Response } from '../types';\n\nexport function setResponseHeaderContentType(res: Response, input: string, ifNotExists?: boolean) {\n if (ifNotExists) {\n const header = res.getHeader(HeaderName.CONTENT_TYPE);\n if (header) {\n return;\n }\n }\n\n const contentType = getMimeType(input);\n if (contentType) {\n res.setHeader(HeaderName.CONTENT_TYPE, contentType);\n }\n}\n","import type { Readable as NodeReadable } from 'stream';\nimport type { NodeReadableStream, WebReadableStream } from '../../types';\nimport { isWebStream } from '../../utils';\nimport type { Response } from '../types';\n\nexport async function sendStream(\n res: Response,\n stream: NodeReadableStream | WebReadableStream,\n next?: (err?: Error) => Promise<unknown> | unknown,\n) : Promise<unknown> {\n if (isWebStream(stream)) {\n return stream\n .pipeTo(\n new WritableStream({\n write(chunk) {\n res.write(chunk);\n },\n }),\n )\n .then(() => {\n if (next) {\n return next();\n }\n\n res.end();\n return Promise.resolve();\n })\n .catch((err) => {\n if (next) {\n return next(err);\n }\n\n return Promise.reject(err);\n });\n }\n\n return new Promise<void>((resolve, reject) => {\n stream.on('open', () => {\n (stream as NodeReadable).pipe(res);\n });\n\n /* istanbul ignore next */\n stream.on('error', (err) => {\n if (next) {\n Promise.resolve()\n .then(() => next(err))\n .then(() => resolve())\n .catch((e) => reject(e));\n\n return;\n }\n\n res.end();\n\n reject(err);\n });\n\n stream.on('close', () => {\n if (next) {\n Promise.resolve()\n .then(() => next())\n .then(() => resolve())\n .catch((e) => reject(e));\n\n return;\n }\n\n res.end();\n\n resolve();\n });\n });\n}\n","import type { WebBlob } from '../../types';\nimport type { Response } from '../types';\nimport { setResponseHeaderContentType } from './header-content-type';\nimport { sendStream } from './send-stream';\n\nexport async function sendWebBlob(res: Response, blob: WebBlob) : Promise<void> {\n setResponseHeaderContentType(res, blob.type);\n\n await sendStream(res, blob.stream());\n}\n","import { HeaderName } from '../../constants';\nimport type { WebResponse } from '../../types';\nimport { splitCookiesString } from '../../utils';\nimport type { Response } from '../types';\nimport { sendStream } from './send-stream';\n\nexport async function sendWebResponse(res: Response, webResponse: WebResponse) : Promise<void> {\n if (webResponse.redirected) {\n res.setHeader(HeaderName.LOCATION, webResponse.url);\n }\n\n if (webResponse.status) {\n res.statusCode = webResponse.status;\n }\n\n if (webResponse.statusText) {\n res.statusMessage = webResponse.statusText;\n }\n\n webResponse.headers.forEach((value, key) => {\n if (key === HeaderName.SET_COOKIE) {\n res.appendHeader(key, splitCookiesString(value));\n } else {\n res.setHeader(key, value);\n }\n });\n\n if (webResponse.body) {\n await sendStream(res, webResponse.body);\n return Promise.resolve();\n }\n\n res.end();\n\n return Promise.resolve();\n}\n","import { Buffer } from 'buffer';\nimport { HeaderName } from '../../constants';\nimport { findRouterOption } from '../../router-options';\nimport { useRequestRouterPath } from '../../request';\nimport { isStream, isWebBlob, isWebResponse } from '../../utils';\nimport type { Response } from '../types';\nimport { isResponseGone } from './gone';\nimport { appendResponseHeaderDirective } from './header';\nimport { setResponseHeaderContentType } from './header-content-type';\nimport { sendStream } from './send-stream';\nimport { sendWebBlob } from './send-web-blob';\nimport { sendWebResponse } from './send-web-response';\n\nexport async function send(res: Response, chunk?: any) : Promise<void> {\n switch (typeof chunk) {\n case 'string': {\n setResponseHeaderContentType(res, 'html', true);\n break;\n }\n case 'boolean':\n case 'number':\n case 'object': {\n if (chunk !== null) {\n if (chunk instanceof Error) {\n throw chunk;\n }\n\n if (isStream(chunk)) {\n await sendStream(res, chunk);\n return;\n }\n\n if (isWebBlob(chunk)) {\n await sendWebBlob(res, chunk);\n return;\n }\n\n if (isWebResponse(chunk)) {\n await sendWebResponse(res, chunk);\n return;\n }\n\n if (Buffer.isBuffer(chunk)) {\n setResponseHeaderContentType(res, 'bin', true);\n } else {\n chunk = JSON.stringify(chunk);\n\n setResponseHeaderContentType(res, 'application/json', true);\n }\n }\n break;\n }\n }\n\n let encoding : BufferEncoding | undefined;\n\n if (typeof chunk === 'string') {\n res.setHeader(HeaderName.CONTENT_ENCODING, 'utf-8');\n\n appendResponseHeaderDirective(res, HeaderName.CONTENT_TYPE, 'charset=utf-8');\n\n encoding = 'utf-8';\n }\n\n // populate Content-Length\n let len : number | undefined;\n if (\n chunk !== undefined &&\n chunk !== null\n ) {\n if (Buffer.isBuffer(chunk)) {\n // get length of Buffer\n len = chunk.length;\n } else if (chunk.length < 1000) {\n // just calculate length when no ETag + small chunk\n len = Buffer.byteLength(chunk, encoding);\n } else {\n // convert chunk to Buffer and calculate\n chunk = Buffer.from(chunk, encoding);\n encoding = undefined;\n len = chunk.length;\n }\n\n res.setHeader(HeaderName.CONTENT_LENGTH, `${len}`);\n }\n\n if (typeof len !== 'undefined') {\n const etagFn = findRouterOption(\n 'etag',\n useRequestRouterPath(res.req),\n