{"version":3,"file":"router.mjs","names":["createRouter","createRou3Router"],"sources":["../src/router.ts"],"sourcesContent":["import {\n\taddRoute,\n\tcreateRouter as createRou3Router,\n\tfindAllRoutes,\n\tfindRoute,\n} from \"rou3\";\nimport type { Endpoint } from \"./endpoint\";\nimport { createEndpoint } from \"./endpoint\";\nimport type { MiddlewareHandler } from \"./middleware\";\nimport { generator, getHTML } from \"./openapi\";\nimport { toResponse } from \"./to-response\";\nimport { getBody, isAPIError, isRequest } from \"./utils\";\n\nexport interface RouterConfig {\n\tthrowError?: boolean;\n\tbasePath?: string;\n\trouterMiddleware?: Array<{\n\t\tpath: string;\n\t\tmiddleware: MiddlewareHandler;\n\t}>;\n\t/**\n\t * additional Context that needs to passed to endpoints\n\t *\n\t * this will be available on `ctx.context` on endpoints\n\t */\n\trouterContext?: Record<string, any>;\n\t/**\n\t * A callback to run before any response\n\t */\n\tonResponse?: (response: Response, request: Request) => any | Promise<any>;\n\t/**\n\t * A callback to run before any request\n\t */\n\tonRequest?: (request: Request) => any | Promise<any>;\n\t/**\n\t * A callback to run when an error is thrown in the router or middleware.\n\t *\n\t * @param error - the error that was thrown in the router or middleware.\n\t * @returns a Response object that will be returned to the client.\n\t */\n\tonError?: (\n\t\terror: unknown,\n\t\trequest: Request,\n\t) => void | Promise<void> | Response | Promise<Response>;\n\t/**\n\t * List of allowed media types (MIME types) for the router\n\t *\n\t * if provided, only the media types in the list will be allowed to be passed in the body.\n\t *\n\t * If an endpoint has allowed media types, it will override the router's allowed media types.\n\t *\n\t * @example\n\t * ```ts\n\t * const router = createRouter({\n\t * \t\tallowedMediaTypes: [\"application/json\", \"application/x-www-form-urlencoded\"],\n\t * \t})\n\t */\n\tallowedMediaTypes?: string[];\n\t/**\n\t * Skip trailing slashes\n\t *\n\t * @default false\n\t */\n\tskipTrailingSlashes?: boolean;\n\t/**\n\t * Open API route configuration\n\t */\n\topenapi?: {\n\t\t/**\n\t\t * Disable openapi route\n\t\t *\n\t\t * @default false\n\t\t */\n\t\tdisabled?: boolean;\n\t\t/**\n\t\t * A path to display open api using scalar\n\t\t *\n\t\t * @default \"/api/reference\"\n\t\t */\n\t\tpath?: string;\n\t\t/**\n\t\t * Scalar Configuration\n\t\t */\n\t\tscalar?: {\n\t\t\t/**\n\t\t\t * Title\n\t\t\t * @default \"Open API Reference\"\n\t\t\t */\n\t\t\ttitle?: string;\n\t\t\t/**\n\t\t\t * Description\n\t\t\t *\n\t\t\t * @default \"Better Call Open API Reference\"\n\t\t\t */\n\t\t\tdescription?: string;\n\t\t\t/**\n\t\t\t * Logo URL\n\t\t\t */\n\t\t\tlogo?: string;\n\t\t\t/**\n\t\t\t * Scalar theme\n\t\t\t * @default \"saturn\"\n\t\t\t */\n\t\t\ttheme?: string;\n\t\t};\n\t};\n}\n\nexport const createRouter = <\n\tE extends Record<string, Endpoint>,\n\tConfig extends RouterConfig,\n>(\n\tendpoints: E,\n\tconfig?: Config,\n) => {\n\tif (!config?.openapi?.disabled) {\n\t\tconst openapi = {\n\t\t\tpath: \"/api/reference\",\n\t\t\t...config?.openapi,\n\t\t};\n\t\t//@ts-expect-error\n\t\tendpoints[\"openapi\"] = createEndpoint(\n\t\t\topenapi.path,\n\t\t\t{\n\t\t\t\tmethod: \"GET\",\n\t\t\t},\n\t\t\tasync (c) => {\n\t\t\t\tconst schema = await generator(endpoints);\n\t\t\t\treturn new Response(getHTML(schema, openapi.scalar), {\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t\"Content-Type\": \"text/html\",\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t},\n\t\t);\n\t}\n\tconst router = createRou3Router<Endpoint>();\n\tconst middlewareRouter = createRou3Router<MiddlewareHandler>();\n\n\tfor (const endpoint of Object.values(endpoints)) {\n\t\tif (!endpoint.options || !endpoint.path) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (endpoint.options?.metadata?.SERVER_ONLY) continue;\n\n\t\tconst methods = Array.isArray(endpoint.options?.method)\n\t\t\t? endpoint.options.method\n\t\t\t: [endpoint.options?.method];\n\n\t\tfor (const method of methods) {\n\t\t\taddRoute(router, method, endpoint.path, endpoint);\n\t\t}\n\t}\n\n\tif (config?.routerMiddleware?.length) {\n\t\tfor (const { path, middleware } of config.routerMiddleware) {\n\t\t\taddRoute(middlewareRouter, \"*\", path, middleware);\n\t\t}\n\t}\n\n\t// Normalize the configured base path once. `basePath` is configuration, not\n\t// per-request input, so trailing-slash normalization belongs here rather than\n\t// in the request hot path. An empty result (unset, \"/\", or all slashes) means\n\t// \"no base path\": route on the full pathname.\n\tconst basePath =\n\t\tconfig?.basePath && config.basePath !== \"/\"\n\t\t\t? config.basePath.replace(/\\/+$/, \"\")\n\t\t\t: \"\";\n\n\tconst processRequest = async (request: Request) => {\n\t\tconst url = new URL(request.url);\n\t\tconst pathname = url.pathname;\n\t\t// Strip `basePath` only when it is a leading, \"/\"-boundary prefix of the\n\t\t// request pathname. A pathname that does not start with the configured\n\t\t// basePath is outside this router and resolves to a 404, so a path like\n\t\t// \"/x/api/test\" never reaches \"/test\". The \"/\" boundary also rejects a\n\t\t// path where basePath is only a leading substring, not a full segment.\n\t\t// The previous implementation stripped basePath wherever it occurred\n\t\t// (`pathname.split(basePath)`).\n\t\tlet path: string;\n\t\tif (basePath) {\n\t\t\tif (!pathname.startsWith(`${basePath}/`)) {\n\t\t\t\treturn new Response(null, { status: 404, statusText: \"Not Found\" });\n\t\t\t}\n\t\t\tpath = pathname.slice(basePath.length);\n\t\t} else {\n\t\t\tpath = pathname;\n\t\t}\n\n\t\t// Reject empty paths and paths with consecutive slashes.\n\t\tif (path.length === 0 || /\\/{2,}/.test(path)) {\n\t\t\treturn new Response(null, { status: 404, statusText: \"Not Found\" });\n\t\t}\n\n\t\tconst route = findRoute(router, request.method, path);\n\t\tconst hasTrailingSlash = path.endsWith(\"/\");\n\t\tconst routeHasTrailingSlash = route?.data?.path?.endsWith(\"/\");\n\n\t\t// If the path has a trailing slash and the route doesn't have a trailing slash and skipTrailingSlashes is not set, return 404\n\t\tif (\n\t\t\thasTrailingSlash !== routeHasTrailingSlash &&\n\t\t\t!config?.skipTrailingSlashes\n\t\t) {\n\t\t\treturn new Response(null, { status: 404, statusText: \"Not Found\" });\n\t\t}\n\t\tif (!route?.data)\n\t\t\treturn new Response(null, { status: 404, statusText: \"Not Found\" });\n\n\t\tconst query: Record<string, string | string[]> = {};\n\t\turl.searchParams.forEach((value, key) => {\n\t\t\tif (key in query) {\n\t\t\t\tif (Array.isArray(query[key])) {\n\t\t\t\t\t(query[key] as string[]).push(value);\n\t\t\t\t} else {\n\t\t\t\t\tquery[key] = [query[key] as string, value];\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tquery[key] = value;\n\t\t\t}\n\t\t});\n\n\t\tconst handler = route.data;\n\n\t\ttry {\n\t\t\t// Determine which allowedMediaTypes to use: endpoint-level overrides router-level\n\t\t\tconst allowedMediaTypes =\n\t\t\t\thandler.options.metadata?.allowedMediaTypes ||\n\t\t\t\tconfig?.allowedMediaTypes;\n\t\t\tconst context = {\n\t\t\t\tpath,\n\t\t\t\tmethod: request.method as \"GET\",\n\t\t\t\theaders: request.headers,\n\t\t\t\tparams: route.params ? { ...route.params } : {},\n\t\t\t\trequest: request,\n\t\t\t\tbody: handler.options.disableBody\n\t\t\t\t\t? undefined\n\t\t\t\t\t: await getBody(\n\t\t\t\t\t\t\thandler.options.cloneRequest ? request.clone() : request,\n\t\t\t\t\t\t\tallowedMediaTypes,\n\t\t\t\t\t\t),\n\t\t\t\tquery,\n\t\t\t\t_flag: \"router\" as const,\n\t\t\t\tasResponse: true,\n\t\t\t\tcontext: config?.routerContext,\n\t\t\t};\n\t\t\tconst middlewareRoutes = findAllRoutes(middlewareRouter, \"*\", path);\n\t\t\tif (middlewareRoutes?.length) {\n\t\t\t\tfor (const { data: middleware, params } of middlewareRoutes) {\n\t\t\t\t\tconst res = await (middleware as Endpoint)({\n\t\t\t\t\t\t...context,\n\t\t\t\t\t\tparams: params ? { ...params } : {},\n\t\t\t\t\t\tasResponse: false,\n\t\t\t\t\t});\n\n\t\t\t\t\tif (res instanceof Response) return res;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst response = (await handler(context)) as Response;\n\t\t\treturn response;\n\t\t} catch (error) {\n\t\t\tif (config?.onError) {\n\t\t\t\ttry {\n\t\t\t\t\tconst errorResponse = await config.onError(error, request);\n\n\t\t\t\t\tif (errorResponse instanceof Response) {\n\t\t\t\t\t\treturn toResponse(errorResponse);\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (isAPIError(error)) {\n\t\t\t\t\t\treturn toResponse(error);\n\t\t\t\t\t}\n\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (config?.throwError) {\n\t\t\t\tthrow error;\n\t\t\t}\n\n\t\t\tif (isAPIError(error)) {\n\t\t\t\treturn toResponse(error);\n\t\t\t}\n\n\t\t\tconsole.error(`# SERVER_ERROR: `, error);\n\t\t\treturn new Response(null, {\n\t\t\t\tstatus: 500,\n\t\t\t\tstatusText: \"Internal Server Error\",\n\t\t\t});\n\t\t}\n\t};\n\n\treturn {\n\t\thandler: async (request: Request) => {\n\t\t\tconst onReq = await config?.onRequest?.(request);\n\t\t\tif (onReq instanceof Response) {\n\t\t\t\treturn onReq;\n\t\t\t}\n\t\t\tconst req = isRequest(onReq) ? onReq : request;\n\t\t\tconst res = await processRequest(req);\n\t\t\tconst onRes = await config?.onResponse?.(res, req);\n\t\t\tif (onRes instanceof Response) {\n\t\t\t\treturn onRes;\n\t\t\t}\n\t\t\treturn res;\n\t\t},\n\t\tendpoints,\n\t};\n};\n\nexport type Router = ReturnType<typeof createRouter>;\n"],"mappings":"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"}