@mpen/routekit 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/dist/bin.d.mts +4 -0
  2. package/dist/client/react.d.mts +178 -0
  3. package/dist/client/react.mjs +142 -0
  4. package/dist/client.d.mts +433 -0
  5. package/dist/client.mjs +264 -0
  6. package/dist/content-BuDOmhH_.mjs +102 -0
  7. package/dist/core-CzUCxvGk.d.mts +140 -0
  8. package/dist/core-DbmQauwS.mjs +81 -0
  9. package/dist/handlers.d.mts +72 -0
  10. package/dist/handlers.mjs +153 -0
  11. package/dist/index.d.mts +3 -0
  12. package/dist/index.mjs +1152 -0
  13. package/dist/middleware.d.mts +388 -0
  14. package/dist/middleware.mjs +1222 -0
  15. package/dist/request-Dn0zc-xm.mjs +1025 -0
  16. package/dist/response/content.d.mts +79 -0
  17. package/dist/response/content.mjs +2 -0
  18. package/dist/response/json-rpc.d.mts +1 -0
  19. package/dist/response/json-rpc.mjs +1 -0
  20. package/dist/response/problem/valibot.d.mts +230 -0
  21. package/dist/response/problem/valibot.mjs +258 -0
  22. package/dist/response/problem.d.mts +415 -0
  23. package/dist/response/problem.mjs +183 -0
  24. package/dist/response/status.d.mts +45 -0
  25. package/dist/response/status.mjs +2 -0
  26. package/dist/responses-B379Ep9Y.d.mts +296 -0
  27. package/dist/responses-BpVrgeYi.mjs +101 -0
  28. package/dist/router-Cwb7ak0J.d.mts +1819 -0
  29. package/dist/routes.d.mts +282 -0
  30. package/dist/routes.mjs +311 -0
  31. package/dist/status-C-8mw-FB.mjs +59 -0
  32. package/dist/valibot-D7liFYyB.d.mts +290 -0
  33. package/dist/valibot-Du97X-TS.mjs +326 -0
  34. package/package.json +8 -2
  35. package/src/bin/gen-api-client.test.ts +0 -70
  36. package/src/bin/gen-api-client.ts +0 -986
  37. package/src/client/headers.ts +0 -31
  38. package/src/client/index.ts +0 -8
  39. package/src/client/promise.ts +0 -11
  40. package/src/client/react/index.test.tsx +0 -266
  41. package/src/client/react/index.ts +0 -431
  42. package/src/client/responses.test.ts +0 -151
  43. package/src/client/responses.ts +0 -278
  44. package/src/client/transport.ts +0 -74
  45. package/src/client/transports/body-codec.ts +0 -61
  46. package/src/client/transports/fetch.ts +0 -113
  47. package/src/client/tsconfig.json +0 -9
  48. package/src/client/types.ts +0 -15
  49. package/src/client/url.ts +0 -31
  50. package/src/index.ts +0 -63
  51. package/src/router/fetch-types.ts +0 -13
  52. package/src/router/handlers/index.ts +0 -2
  53. package/src/router/handlers/openapi/index.ts +0 -2
  54. package/src/router/handlers/openapi/openapi.ts +0 -293
  55. package/src/router/integration/zod-openapi.test.ts +0 -74
  56. package/src/router/lib/charset.test.ts +0 -22
  57. package/src/router/lib/charset.ts +0 -133
  58. package/src/router/lib/collections.ts +0 -3
  59. package/src/router/lib/format.test.ts +0 -67
  60. package/src/router/lib/format.ts +0 -35
  61. package/src/router/lib/host.ts +0 -4
  62. package/src/router/lib/json-schema.ts +0 -6
  63. package/src/router/lib/media-type.test.ts +0 -122
  64. package/src/router/lib/media-type.ts +0 -289
  65. package/src/router/lib/pathname.test.ts +0 -18
  66. package/src/router/lib/pathname.ts +0 -19
  67. package/src/router/lib/route-names.ts +0 -70
  68. package/src/router/lib/route-normalize.test.ts +0 -36
  69. package/src/router/lib/route-normalize.ts +0 -67
  70. package/src/router/lib/schema-merge.ts +0 -56
  71. package/src/router/middleware/accept-ctx.test.ts +0 -33
  72. package/src/router/middleware/accept-ctx.ts +0 -12
  73. package/src/router/middleware/body-limit.test.ts +0 -112
  74. package/src/router/middleware/body-limit.ts +0 -121
  75. package/src/router/middleware/content-type-context.ts +0 -0
  76. package/src/router/middleware/cors.test.ts +0 -269
  77. package/src/router/middleware/cors.ts +0 -490
  78. package/src/router/middleware/csrf.test.ts +0 -106
  79. package/src/router/middleware/csrf.ts +0 -192
  80. package/src/router/middleware/define.ts +0 -249
  81. package/src/router/middleware/index.ts +0 -34
  82. package/src/router/middleware/jsxhtml-response.ts +0 -0
  83. package/src/router/middleware/oas-swagger.ts +0 -0
  84. package/src/router/middleware/rate-limit.test.ts +0 -886
  85. package/src/router/middleware/rate-limit.ts +0 -920
  86. package/src/router/middleware/request-id-ctx.test.ts +0 -183
  87. package/src/router/middleware/request-id-ctx.ts +0 -135
  88. package/src/router/middleware/request-logger-format.test.ts +0 -16
  89. package/src/router/middleware/request-logger-format.ts +0 -269
  90. package/src/router/middleware/request-logger.test.ts +0 -267
  91. package/src/router/middleware/request-logger.ts +0 -131
  92. package/src/router/middleware/start-time-ctx.ts +0 -5
  93. package/src/router/request.ts +0 -611
  94. package/src/router/response/core.ts +0 -181
  95. package/src/router/response/directives.ts +0 -233
  96. package/src/router/response/formats/content/bodyless.ts +0 -54
  97. package/src/router/response/formats/content/content.ts +0 -79
  98. package/src/router/response/formats/content/index.ts +0 -2
  99. package/src/router/response/formats/json-rpc/index.ts +0 -2
  100. package/src/router/response/formats/problem/badRequest.ts +0 -90
  101. package/src/router/response/formats/problem/conflict.ts +0 -90
  102. package/src/router/response/formats/problem/created.ts +0 -40
  103. package/src/router/response/formats/problem/index.ts +0 -27
  104. package/src/router/response/formats/problem/notFound.ts +0 -90
  105. package/src/router/response/formats/problem/permissionDenied.ts +0 -90
  106. package/src/router/response/formats/problem/problem.test.ts +0 -888
  107. package/src/router/response/formats/problem/rateLimited.ts +0 -90
  108. package/src/router/response/formats/problem/responses.ts +0 -219
  109. package/src/router/response/formats/problem/root-errors.ts +0 -48
  110. package/src/router/response/formats/problem/sessionExpired.ts +0 -90
  111. package/src/router/response/formats/problem/types.ts +0 -170
  112. package/src/router/response/formats/problem/unauthenticated.ts +0 -90
  113. package/src/router/response/formats/problem/valibot.ts +0 -410
  114. package/src/router/response/formats/status/index.ts +0 -1
  115. package/src/router/response/formats/status/responses.ts +0 -59
  116. package/src/router/response/formats/status/status.test.ts +0 -21
  117. package/src/router/response/framers.ts +0 -85
  118. package/src/router/response/index.ts +0 -28
  119. package/src/router/response/openapi.test.ts +0 -96
  120. package/src/router/response/openapi.ts +0 -1
  121. package/src/router/response/serializers.ts +0 -66
  122. package/src/router/response/stream.ts +0 -35
  123. package/src/router/router.test.ts +0 -1571
  124. package/src/router/router.ts +0 -1965
  125. package/src/router/routes/index.ts +0 -46
  126. package/src/router/routes/valibot/index.ts +0 -18
  127. package/src/router/routes/valibot/valibot.ts +0 -1393
  128. package/src/router/routes/valibot.test.ts +0 -286
  129. package/src/router/routes/zod/index.ts +0 -18
  130. package/src/router/routes/zod/zod.ts +0 -1318
  131. package/src/router/routes/zod.test.ts +0 -280
  132. package/src/router/server-interface.ts +0 -31
  133. package/src/router/types.ts +0 -657
@@ -1,1965 +0,0 @@
1
- import { CommonHeaders, HttpMethod, HttpStatus, StatusText } from '@mpen/http'
2
- import { ConsoleLogger, type Logger } from '@mpen/logger'
3
- import type { SimpleServerInterface } from './server-interface'
4
- import { joinPrefixPathname, stripPrefixPathname } from './lib/pathname'
5
- import { normalizeRoute } from './lib/route-normalize'
6
- import { mergeRouteSchemas } from './lib/schema-merge'
7
- import { isDeclaredMiddleware, type DeclaredMiddleware } from './middleware/define'
8
- import {
9
- mediaRangeAccepts,
10
- mediaRangeQuality,
11
- mediaRangeToContentType,
12
- mediaTypeMatches,
13
- parseAcceptHeader,
14
- parseContentType,
15
- } from './lib/media-type'
16
- import {
17
- createRoutekitRequest,
18
- defaultRequestBodyParsers,
19
- RequestBodyError,
20
- responseFromRequestBodyError,
21
- type RequestBodyParser,
22
- type RoutekitRequest,
23
- } from './request'
24
- import type {
25
- AddedContextFromMiddlewareInput,
26
- AnyContext,
27
- ContextMiddleware,
28
- Handler,
29
- HandlerBody,
30
- HandlerFinalResult,
31
- HandlerResult,
32
- HandlerContext,
33
- HandlerYield,
34
- MiddlewareEntry,
35
- MiddlewareInput,
36
- NormalizedRoute,
37
- RequestContext,
38
- RequestBodyParserInput,
39
- RequestMiddleware,
40
- RequestMiddlewareEntry,
41
- RequestMiddlewareInput,
42
- Route,
43
- RouteOptions,
44
- RouterExtension,
45
- RouterMountOptions,
46
- ResponseBodySerializerInput,
47
- } from './types'
48
- import {
49
- defaultResponseBodySerializers,
50
- isChunkDirective,
51
- isHeadersDirective,
52
- isHeadDirective,
53
- isResponseBodyInit,
54
- isRoutekitBody,
55
- isRoutekitResponse,
56
- isStatusDirective,
57
- isStreamDirective,
58
- response as routekitResponse,
59
- type ResponseBodySerializer,
60
- type RoutekitResponse,
61
- type StreamFramer,
62
- } from './response'
63
- import { text } from './response/formats/content'
64
- import type { RouterBodyInit } from './fetch-types'
65
-
66
- type ExecutableMiddleware = ContextMiddleware<any, any> | DeclaredMiddleware<any, any>
67
- type ExecutableRequestMiddleware = RequestMiddleware<any, any>
68
-
69
- type RouteEntry =
70
- | { kind: 'route'; route: NormalizedRoute<any>; middleware: ExecutableMiddleware[] }
71
- | { kind: 'router'; prefix?: string; router: Router<any> }
72
-
73
- type MatchResult = {
74
- kind: 'match'
75
- route: NormalizedRoute<any>
76
- routePath: string
77
- match: URLPatternResult | null
78
- middleware: ExecutableMiddleware[]
79
- router: Router<any>
80
- config: RuntimeConfig
81
- }
82
-
83
- type HandlerRegistration = {
84
- handler: Handler<any, any>
85
- router: Router<any>
86
- }
87
-
88
- type RuntimeConfig = {
89
- requestBodyParsers: RequestBodyParser[]
90
- responseBodySerializers: ResponseBodySerializer[]
91
- logger: Logger
92
- requestMiddleware: ExecutableRequestMiddleware[]
93
- onNotFoundHandler?: HandlerRegistration
94
- onMethodNotAllowedHandler?: HandlerRegistration
95
- onUnsupportedMediaTypeHandler?: HandlerRegistration
96
- onInternalErrorHandler?: HandlerRegistration
97
- }
98
-
99
- type ResponseBodySerializerCandidate = {
100
- serializer: ResponseBodySerializer
101
- mediaType: string
102
- acceptQuality: number
103
- mediaTypeQuality: number
104
- acceptIndex: number
105
- serializerIndex: number
106
- mediaTypeIndex: number
107
- }
108
-
109
- type MatchAttempt = {
110
- match: MatchResult | null
111
- unsupportedMediaType: MatchResult | null
112
- methodNotAllowedConfig: RuntimeConfig | null
113
- notFoundConfig: RuntimeConfig
114
- }
115
-
116
- type MethodRouteInput<Ctx extends object> = Handler<any, Ctx> | RouteOptions<any, any, any>
117
-
118
- function normalizeMiddlewareList<Ctx extends object>(
119
- middleware: MiddlewareInput<Ctx>,
120
- ): ExecutableMiddleware[] {
121
- if (!middleware) return []
122
- if (Array.isArray(middleware)) {
123
- return middleware.filter(Boolean) as ExecutableMiddleware[]
124
- }
125
- return [middleware as ExecutableMiddleware]
126
- }
127
-
128
- function normalizeRequestMiddlewareList<Ctx extends object>(
129
- middleware: RequestMiddlewareInput<Ctx>,
130
- ): ExecutableRequestMiddleware[] {
131
- if (!middleware) return []
132
- if (Array.isArray(middleware)) {
133
- return middleware.filter(Boolean) as ExecutableRequestMiddleware[]
134
- }
135
- return [middleware as ExecutableRequestMiddleware]
136
- }
137
-
138
- function normalizeRequestBodyParsers(input: RequestBodyParserInput): RequestBodyParser[] {
139
- if (!input) return []
140
- if (Array.isArray(input)) return input.filter(Boolean) as RequestBodyParser[]
141
- return [input as RequestBodyParser]
142
- }
143
-
144
- function normalizeResponseBodySerializers(
145
- input: ResponseBodySerializerInput,
146
- ): ResponseBodySerializer[] {
147
- if (!input) return []
148
- if (Array.isArray(input)) return input.filter(Boolean) as ResponseBodySerializer[]
149
- return [input as ResponseBodySerializer]
150
- }
151
-
152
- function isHandler<Ctx extends object>(input: MethodRouteInput<Ctx>): input is Handler<any, Ctx> {
153
- return typeof input === 'function'
154
- }
155
-
156
- /**
157
- * Router that matches requests against registered routes and executes middleware.
158
- *
159
- * @example
160
- * ```ts
161
- * const router = new Router()
162
- * router.add({method: HttpMethod.GET, path: '/', handler: async ({request}) => new Response(request.url)})
163
- * ```
164
- */
165
- export class Router<Ctx extends object = AnyContext> implements SimpleServerInterface {
166
- private _entries: RouteEntry[] = []
167
- private _middleware: ExecutableMiddleware[] = []
168
- private _requestMiddleware: ExecutableRequestMiddleware[] = []
169
- private _responseBodySerializers: ResponseBodySerializer[] = []
170
- private _responseBodySerializersMode: 'inherit' | 'replace' = 'inherit'
171
- private _requestBodyParsers: RequestBodyParser[] = []
172
- private _requestBodyParsersMode: 'inherit' | 'replace' = 'inherit'
173
- private _logger?: Logger
174
- private readonly _defaultLogger = new ConsoleLogger()
175
- private _onNotFoundHandler?: Handler<any, Ctx>
176
- private _onMethodNotAllowedHandler?: Handler<any, Ctx>
177
- private _onUnsupportedMediaTypeHandler?: Handler<any, Ctx>
178
- private _onInternalErrorHandler?: Handler<any, Ctx>
179
-
180
- /**
181
- * Create a new router instance.
182
- */
183
- constructor() {}
184
-
185
- /**
186
- * Install an extension that configures this router instance.
187
- *
188
- * @example
189
- * ```ts
190
- * router.install(problemRootErrors())
191
- * ```
192
- *
193
- * @param extension - Extension function that mutates or configures this router.
194
- * @returns The router instance for chaining.
195
- */
196
- install(extension: RouterExtension<Ctx>): this {
197
- extension(this)
198
- return this
199
- }
200
-
201
- /**
202
- * Register a handler for requests that do not match any route.
203
- *
204
- * @example
205
- * ```ts
206
- * router.onNotFound(() => new Response('missing', {status: 404}))
207
- * ```
208
- *
209
- * @param handler - Handler invoked when no route matches.
210
- * @returns The router instance for chaining.
211
- */
212
- onNotFound(handler: Handler<any, Ctx>): this {
213
- this._onNotFoundHandler = handler
214
- return this
215
- }
216
-
217
- /**
218
- * Register a handler for requests that match a path but use an unsupported method.
219
- *
220
- * @example
221
- * ```ts
222
- * router.onMethodNotAllowed(() => new Response('nope', {status: 405}))
223
- * ```
224
- *
225
- * @param handler - Handler invoked when a route exists but the method does not match.
226
- * @returns The router instance for chaining.
227
- */
228
- onMethodNotAllowed(handler: Handler<any, Ctx>): this {
229
- this._onMethodNotAllowedHandler = handler
230
- return this
231
- }
232
-
233
- /**
234
- * Register a handler for requests with unsupported request body media types.
235
- *
236
- * @example
237
- * ```ts
238
- * router.onUnsupportedMediaType(() => new Response('unsupported', {status: 415}))
239
- * ```
240
- *
241
- * @param handler - Handler invoked when the incoming `Content-Type` is not accepted.
242
- * @returns The router instance for chaining.
243
- */
244
- onUnsupportedMediaType(handler: Handler<any, Ctx>): this {
245
- this._onUnsupportedMediaTypeHandler = handler
246
- return this
247
- }
248
-
249
- /**
250
- * Register a handler for unhandled errors thrown by route handlers.
251
- *
252
- * @example
253
- * ```ts
254
- * router.onInternalError(() => new Response('oops', {status: 500}))
255
- * ```
256
- *
257
- * @param handler - Handler invoked when a route handler throws.
258
- * @returns The router instance for chaining.
259
- */
260
- onInternalError(handler: Handler<any, Ctx>): this {
261
- this._onInternalErrorHandler = handler
262
- return this
263
- }
264
-
265
- /**
266
- * Add a request body parser to the inherited parser list.
267
- *
268
- * @example
269
- * ```ts
270
- * router.addRequestBodyParser(jsonRequestBodyParser())
271
- * ```
272
- *
273
- * @param parser - Request body parser to append.
274
- * @returns The router instance for chaining.
275
- */
276
- addRequestBodyParser(parser: RequestBodyParser): this {
277
- return this.addRequestBodyParsers([parser])
278
- }
279
-
280
- /**
281
- * Add request body parsers to the inherited parser list.
282
- *
283
- * @example
284
- * ```ts
285
- * router.addRequestBodyParsers([jsonRequestBodyParser(), textRequestBodyParser()])
286
- * ```
287
- *
288
- * @param parsers - Request body parsers to append.
289
- * @returns The router instance for chaining.
290
- */
291
- addRequestBodyParsers(parsers: RequestBodyParserInput): this {
292
- this._requestBodyParsers.push(...normalizeRequestBodyParsers(parsers))
293
- return this
294
- }
295
-
296
- /**
297
- * Replace the inherited request body parser list for this router subtree.
298
- *
299
- * @example
300
- * ```ts
301
- * router.setRequestBodyParsers([jsonRequestBodyParser()])
302
- * ```
303
- *
304
- * @param parsers - Exact request body parsers to use for this router subtree.
305
- * @returns The router instance for chaining.
306
- */
307
- setRequestBodyParsers(parsers: RequestBodyParserInput): this {
308
- this._requestBodyParsers = normalizeRequestBodyParsers(parsers)
309
- this._requestBodyParsersMode = 'replace'
310
- return this
311
- }
312
-
313
- /**
314
- * Add a response body serializer to the inherited serializer list.
315
- *
316
- * @example
317
- * ```ts
318
- * router.addResponseBodySerializer(jsonResponseBodySerializer())
319
- * ```
320
- *
321
- * @param serializer - Response body serializer to append.
322
- * @returns The router instance for chaining.
323
- */
324
- addResponseBodySerializer(serializer: ResponseBodySerializer): this {
325
- return this.addResponseBodySerializers([serializer])
326
- }
327
-
328
- /**
329
- * Add response body serializers to the inherited serializer list.
330
- *
331
- * @example
332
- * ```ts
333
- * router.addResponseBodySerializers([jsonResponseBodySerializer(), yamlSerializer])
334
- * ```
335
- *
336
- * @param serializers - Response body serializers to append.
337
- * @returns The router instance for chaining.
338
- */
339
- addResponseBodySerializers(serializers: ResponseBodySerializerInput): this {
340
- this._responseBodySerializers.push(...normalizeResponseBodySerializers(serializers))
341
- return this
342
- }
343
-
344
- /**
345
- * Replace the inherited response body serializer list for this router subtree.
346
- *
347
- * @example
348
- * ```ts
349
- * router.setResponseBodySerializers([jsonResponseBodySerializer()])
350
- * ```
351
- *
352
- * @param serializers - Exact response body serializers to use for this router subtree.
353
- * @returns The router instance for chaining.
354
- */
355
- setResponseBodySerializers(serializers: ResponseBodySerializerInput): this {
356
- this._responseBodySerializers = normalizeResponseBodySerializers(serializers)
357
- this._responseBodySerializersMode = 'replace'
358
- return this
359
- }
360
-
361
- /**
362
- * Set the logger for this router subtree.
363
- *
364
- * @example
365
- * ```ts
366
- * router.setLogger(logger)
367
- * ```
368
- *
369
- * @param logger - Logger exposed through request contexts and used for internal server errors.
370
- * @returns The router instance for chaining.
371
- */
372
- setLogger(logger: Logger): this {
373
- this._logger = logger
374
- return this
375
- }
376
-
377
- /**
378
- * Register middleware on the current router.
379
- *
380
- * @example
381
- * ```ts
382
- * router.use(ctx => {
383
- * ctx.traceId = crypto.randomUUID()
384
- * })
385
- * ```
386
- *
387
- * @param middleware - Middleware or a list of middleware to register.
388
- * @returns The router instance for chaining.
389
- */
390
- use<AddedCtx extends object>(
391
- middleware: ContextMiddleware<AddedCtx, Ctx> | null | undefined | false,
392
- ): Router<Ctx & AddedCtx>
393
- use<AddedCtx extends object>(
394
- middleware: DeclaredMiddleware<AddedCtx, Ctx>,
395
- ): Router<Ctx & AddedCtx>
396
- /**
397
- * Register middleware on the current router.
398
- *
399
- * @example
400
- * ```ts
401
- * router.use([auth(), logging()])
402
- * ```
403
- *
404
- * @param middleware - Middleware list to register.
405
- * @returns The router instance for chaining.
406
- */
407
- use<List extends readonly MiddlewareEntry<Ctx>[]>(
408
- middleware: List,
409
- ): Router<Ctx & AddedContextFromMiddlewareInput<List>>
410
- use(middleware: MiddlewareInput<Ctx>): Router<any> {
411
- const list = normalizeMiddlewareList(middleware)
412
- this._middleware.push(...list)
413
- return this
414
- }
415
-
416
- /**
417
- * Register middleware that surrounds final response production for every request.
418
- *
419
- * @example
420
- * ```ts
421
- * router.useRequest(requestIdCtx())
422
- * router.useRequest(requestLogger())
423
- * ```
424
- *
425
- * @param middleware - Request-boundary middleware or a middleware list.
426
- * @returns The router instance with added request context fields.
427
- */
428
- useRequest<AddedCtx extends object>(
429
- middleware: RequestMiddleware<AddedCtx, Ctx> | null | undefined | false,
430
- ): Router<Ctx & AddedCtx>
431
- useRequest<List extends readonly RequestMiddlewareEntry<Ctx>[]>(
432
- middleware: List,
433
- ): Router<Ctx & AddedContextFromMiddlewareInput<List>>
434
- useRequest(middleware: RequestMiddlewareInput<Ctx>): Router<any> {
435
- this._requestMiddleware.push(...normalizeRequestMiddlewareList(middleware))
436
- return this
437
- }
438
-
439
- /**
440
- * Mount a router at the current path.
441
- *
442
- * @example
443
- * ```ts
444
- * router.mount(apiRouter)
445
- * ```
446
- *
447
- * @param router - Router to mount.
448
- * @returns The router instance for chaining.
449
- */
450
- mount(router: Router<any>): this
451
- /**
452
- * Mount an inline router at the current path.
453
- *
454
- * @example
455
- * ```ts
456
- * router.mount(admin => {
457
- * admin.get('/admin/users', handler)
458
- * })
459
- * ```
460
- *
461
- * @param configure - Callback used to register routes on the mounted router.
462
- * @returns The router instance for chaining.
463
- */
464
- mount(configure: (router: Router<Ctx>) => void): this
465
- /**
466
- * Mount a router under a pathname prefix.
467
- *
468
- * @example
469
- * ```ts
470
- * router.mount('/api', apiRouter)
471
- * ```
472
- *
473
- * @param prefix - Pathname prefix to strip before routing.
474
- * @param router - Router to mount.
475
- * @returns The router instance for chaining.
476
- */
477
- mount(prefix: string, router: Router<any>): this
478
- /**
479
- * Mount an inline router under a pathname prefix.
480
- *
481
- * @example
482
- * ```ts
483
- * router.mount('/api', api => {
484
- * api.get('/items', handler)
485
- * })
486
- * ```
487
- *
488
- * @param prefix - Pathname prefix to strip before routing.
489
- * @param configure - Callback used to register routes on the mounted router.
490
- * @returns The router instance for chaining.
491
- */
492
- mount(prefix: string, configure: (router: Router<Ctx>) => void): this
493
- /**
494
- * Mount an inline router with additional middleware.
495
- *
496
- * @example
497
- * ```ts
498
- * router.mount({prefix: '/admin', middleware: auth()}, admin => {
499
- * admin.get('/users', ({userId}) => ok({userId}))
500
- * })
501
- * ```
502
- *
503
- * @param options - Mount options for the inline router.
504
- * @param configure - Callback used to register routes on the mounted router.
505
- * @returns The router instance for chaining.
506
- */
507
- mount<AddedCtx extends object>(
508
- options: RouterMountOptions<Ctx> & {
509
- middleware:
510
- | ContextMiddleware<AddedCtx, Ctx>
511
- | DeclaredMiddleware<AddedCtx, Ctx>
512
- | null
513
- | undefined
514
- | false
515
- },
516
- configure: (router: Router<Ctx & AddedCtx>) => void,
517
- ): this
518
- /**
519
- * Mount an inline router with a middleware list.
520
- *
521
- * @example
522
- * ```ts
523
- * router.mount({middleware: [auth(), logging()] as const}, scoped => {
524
- * scoped.get('/me', handler)
525
- * })
526
- * ```
527
- *
528
- * @param options - Mount options for the inline router.
529
- * @param configure - Callback used to register routes on the mounted router.
530
- * @returns The router instance for chaining.
531
- */
532
- mount<List extends readonly MiddlewareEntry<Ctx>[]>(
533
- options: RouterMountOptions<Ctx> & { middleware: List },
534
- configure: (router: Router<Ctx & AddedContextFromMiddlewareInput<List>>) => void,
535
- ): this
536
- /**
537
- * Mount an inline router with options and no additional typed middleware.
538
- *
539
- * @example
540
- * ```ts
541
- * router.mount({prefix: '/reports'}, reports => {
542
- * reports.get('/export', handler)
543
- * })
544
- * ```
545
- *
546
- * @param options - Mount options for the inline router.
547
- * @param configure - Callback used to register routes on the mounted router.
548
- * @returns The router instance for chaining.
549
- */
550
- mount(options: RouterMountOptions<Ctx>, configure: (router: Router<Ctx>) => void): this
551
- /**
552
- * Mount an existing router with options.
553
- *
554
- * @example
555
- * ```ts
556
- * router.mount({prefix: '/api', middleware: logging()}, apiRouter)
557
- * ```
558
- *
559
- * @param options - Mount options for the existing router.
560
- * @param router - Router to mount.
561
- * @returns The router instance for chaining.
562
- */
563
- mount(options: RouterMountOptions<Ctx>, router: Router<any>): this
564
- mount(
565
- prefixOrOptionsOrRouter:
566
- | string
567
- | RouterMountOptions<Ctx>
568
- | Router<any>
569
- | ((router: Router<any>) => void),
570
- routerOrConfigure?: Router<any> | ((router: Router<any>) => void),
571
- ): this {
572
- const mountOptions: RouterMountOptions<Ctx> =
573
- typeof prefixOrOptionsOrRouter === 'string'
574
- ? { prefix: prefixOrOptionsOrRouter }
575
- : prefixOrOptionsOrRouter instanceof Router ||
576
- typeof prefixOrOptionsOrRouter === 'function'
577
- ? {}
578
- : prefixOrOptionsOrRouter
579
- const target =
580
- prefixOrOptionsOrRouter instanceof Router ||
581
- typeof prefixOrOptionsOrRouter === 'function'
582
- ? prefixOrOptionsOrRouter
583
- : routerOrConfigure
584
-
585
- if (target instanceof Router && !mountOptions.middleware) {
586
- this._entries.push({ kind: 'router', prefix: mountOptions.prefix, router: target })
587
- return this
588
- }
589
-
590
- const scope = new Router<any>()
591
- if (mountOptions.middleware) {
592
- scope.use(normalizeMiddlewareList(mountOptions.middleware as MiddlewareInput<any>))
593
- }
594
- if (target instanceof Router) {
595
- scope.mount(target)
596
- } else {
597
- target?.(scope)
598
- }
599
- this._entries.push({ kind: 'router', prefix: mountOptions.prefix, router: scope })
600
- return this
601
- }
602
-
603
- /**
604
- * Add a route definition to this router.
605
- *
606
- * @example
607
- * ```ts
608
- * router.add({method: HttpMethod.POST, path: '/items', handler: ({request}) => new Response(request.url)})
609
- * ```
610
- *
611
- * @param route - Route definition to normalize and register.
612
- * @returns The router instance for chaining.
613
- */
614
- add<
615
- RouteMiddleware extends MiddlewareInput<Ctx> = undefined,
616
- HandlerCtx extends object = Ctx & AddedContextFromMiddlewareInput<RouteMiddleware>,
617
- >(route: Route<Ctx, RouteMiddleware, HandlerCtx>): this {
618
- this._entries.push({
619
- kind: 'route',
620
- route: normalizeRoute(route),
621
- middleware: normalizeMiddlewareList(route.middleware as MiddlewareInput<any>),
622
- })
623
- return this
624
- }
625
-
626
- private _addMethod<
627
- RouteMiddleware extends MiddlewareInput<Ctx> = undefined,
628
- HandlerCtx extends object = Ctx & AddedContextFromMiddlewareInput<RouteMiddleware>,
629
- >(
630
- method: HttpMethod,
631
- path: NonNullable<Route<Ctx>['path']>,
632
- input: Handler<any, Ctx> | RouteOptions<Ctx, RouteMiddleware, HandlerCtx>,
633
- ): this {
634
- return this.add({
635
- ...(isHandler(input) ? { handler: input } : input),
636
- path,
637
- method,
638
- } as Route<Ctx, RouteMiddleware, HandlerCtx>)
639
- }
640
-
641
- /**
642
- * Add a GET route handler to this router.
643
- *
644
- * @param path - URL path pattern to match.
645
- * @param handler - Handler invoked when the route matches.
646
- * @returns The router instance for chaining.
647
- */
648
- get(path: NonNullable<Route<Ctx>['path']>, handler: Handler<any, Ctx>): this
649
- /**
650
- * Add a GET route definition to this router.
651
- *
652
- * @param path - URL path pattern to match.
653
- * @param options - Route options registered with the GET method.
654
- * @returns The router instance for chaining.
655
- */
656
- get<
657
- RouteMiddleware extends MiddlewareInput<Ctx> = undefined,
658
- HandlerCtx extends object = Ctx & AddedContextFromMiddlewareInput<RouteMiddleware>,
659
- >(
660
- path: NonNullable<Route<Ctx>['path']>,
661
- options: RouteOptions<Ctx, RouteMiddleware, HandlerCtx>,
662
- ): this
663
- get(path: NonNullable<Route<Ctx>['path']>, input: MethodRouteInput<Ctx>): this {
664
- return this._addMethod(HttpMethod.GET, path, input)
665
- }
666
-
667
- /**
668
- * Add a HEAD route handler to this router.
669
- *
670
- * @param path - URL path pattern to match.
671
- * @param handler - Handler invoked when the route matches.
672
- * @returns The router instance for chaining.
673
- */
674
- head(path: NonNullable<Route<Ctx>['path']>, handler: Handler<any, Ctx>): this
675
- /**
676
- * Add a HEAD route definition to this router.
677
- *
678
- * @param path - URL path pattern to match.
679
- * @param options - Route options registered with the HEAD method.
680
- * @returns The router instance for chaining.
681
- */
682
- head<
683
- RouteMiddleware extends MiddlewareInput<Ctx> = undefined,
684
- HandlerCtx extends object = Ctx & AddedContextFromMiddlewareInput<RouteMiddleware>,
685
- >(
686
- path: NonNullable<Route<Ctx>['path']>,
687
- options: RouteOptions<Ctx, RouteMiddleware, HandlerCtx>,
688
- ): this
689
- head(path: NonNullable<Route<Ctx>['path']>, input: MethodRouteInput<Ctx>): this {
690
- return this._addMethod(HttpMethod.HEAD, path, input)
691
- }
692
-
693
- /**
694
- * Add a POST route handler to this router.
695
- *
696
- * @param path - URL path pattern to match.
697
- * @param handler - Handler invoked when the route matches.
698
- * @returns The router instance for chaining.
699
- */
700
- post(path: NonNullable<Route<Ctx>['path']>, handler: Handler<any, Ctx>): this
701
- /**
702
- * Add a POST route definition to this router.
703
- *
704
- * @param path - URL path pattern to match.
705
- * @param options - Route options registered with the POST method.
706
- * @returns The router instance for chaining.
707
- */
708
- post<
709
- RouteMiddleware extends MiddlewareInput<Ctx> = undefined,
710
- HandlerCtx extends object = Ctx & AddedContextFromMiddlewareInput<RouteMiddleware>,
711
- >(
712
- path: NonNullable<Route<Ctx>['path']>,
713
- options: RouteOptions<Ctx, RouteMiddleware, HandlerCtx>,
714
- ): this
715
- post(path: NonNullable<Route<Ctx>['path']>, input: MethodRouteInput<Ctx>): this {
716
- return this._addMethod(HttpMethod.POST, path, input)
717
- }
718
-
719
- /**
720
- * Add a PUT route handler to this router.
721
- *
722
- * @param path - URL path pattern to match.
723
- * @param handler - Handler invoked when the route matches.
724
- * @returns The router instance for chaining.
725
- */
726
- put(path: NonNullable<Route<Ctx>['path']>, handler: Handler<any, Ctx>): this
727
- /**
728
- * Add a PUT route definition to this router.
729
- *
730
- * @param path - URL path pattern to match.
731
- * @param options - Route options registered with the PUT method.
732
- * @returns The router instance for chaining.
733
- */
734
- put<
735
- RouteMiddleware extends MiddlewareInput<Ctx> = undefined,
736
- HandlerCtx extends object = Ctx & AddedContextFromMiddlewareInput<RouteMiddleware>,
737
- >(
738
- path: NonNullable<Route<Ctx>['path']>,
739
- options: RouteOptions<Ctx, RouteMiddleware, HandlerCtx>,
740
- ): this
741
- put(path: NonNullable<Route<Ctx>['path']>, input: MethodRouteInput<Ctx>): this {
742
- return this._addMethod(HttpMethod.PUT, path, input)
743
- }
744
-
745
- /**
746
- * Add a DELETE route handler to this router.
747
- *
748
- * @param path - URL path pattern to match.
749
- * @param handler - Handler invoked when the route matches.
750
- * @returns The router instance for chaining.
751
- */
752
- delete(path: NonNullable<Route<Ctx>['path']>, handler: Handler<any, Ctx>): this
753
- /**
754
- * Add a DELETE route definition to this router.
755
- *
756
- * @param path - URL path pattern to match.
757
- * @param options - Route options registered with the DELETE method.
758
- * @returns The router instance for chaining.
759
- */
760
- delete<
761
- RouteMiddleware extends MiddlewareInput<Ctx> = undefined,
762
- HandlerCtx extends object = Ctx & AddedContextFromMiddlewareInput<RouteMiddleware>,
763
- >(
764
- path: NonNullable<Route<Ctx>['path']>,
765
- options: RouteOptions<Ctx, RouteMiddleware, HandlerCtx>,
766
- ): this
767
- delete(path: NonNullable<Route<Ctx>['path']>, input: MethodRouteInput<Ctx>): this {
768
- return this._addMethod(HttpMethod.DELETE, path, input)
769
- }
770
-
771
- /**
772
- * Add a PATCH route handler to this router.
773
- *
774
- * @param path - URL path pattern to match.
775
- * @param handler - Handler invoked when the route matches.
776
- * @returns The router instance for chaining.
777
- */
778
- patch(path: NonNullable<Route<Ctx>['path']>, handler: Handler<any, Ctx>): this
779
- /**
780
- * Add a PATCH route definition to this router.
781
- *
782
- * @param path - URL path pattern to match.
783
- * @param options - Route options registered with the PATCH method.
784
- * @returns The router instance for chaining.
785
- */
786
- patch<
787
- RouteMiddleware extends MiddlewareInput<Ctx> = undefined,
788
- HandlerCtx extends object = Ctx & AddedContextFromMiddlewareInput<RouteMiddleware>,
789
- >(
790
- path: NonNullable<Route<Ctx>['path']>,
791
- options: RouteOptions<Ctx, RouteMiddleware, HandlerCtx>,
792
- ): this
793
- patch(path: NonNullable<Route<Ctx>['path']>, input: MethodRouteInput<Ctx>): this {
794
- return this._addMethod(HttpMethod.PATCH, path, input)
795
- }
796
-
797
- /**
798
- * Return a flattened list of registered routes.
799
- *
800
- * @example
801
- * ```ts
802
- * const routes = router.getRoutes()
803
- * ```
804
- *
805
- * @returns Array of normalized routes.
806
- */
807
- getRoutes(): NormalizedRoute<any>[] {
808
- return this._getRoutes([])
809
- }
810
-
811
- private _getRoutes(parentMiddleware: ExecutableMiddleware[]): NormalizedRoute<any>[] {
812
- const routes: NormalizedRoute<any>[] = []
813
- const middleware = [...parentMiddleware, ...this._middleware]
814
- for (const entry of this._entries) {
815
- if (entry.kind === 'route') {
816
- const routeMiddleware = [...middleware, ...entry.middleware]
817
- const schema = mergeRouteSchemas(
818
- ...routeMiddleware.flatMap((value) =>
819
- isDeclaredMiddleware(value) &&
820
- (!value.schemaAppliesTo || value.schemaAppliesTo(entry.route))
821
- ? [value.schema]
822
- : [],
823
- ),
824
- entry.route.schema,
825
- )
826
- routes.push({
827
- ...entry.route,
828
- ...(schema === undefined ? {} : { schema }),
829
- })
830
- continue
831
- }
832
-
833
- for (const subRoute of entry.router._getRoutes(middleware)) {
834
- if (!entry.prefix) {
835
- routes.push(subRoute)
836
- continue
837
- }
838
-
839
- const path = new URLPattern({
840
- pathname: joinPrefixPathname(entry.prefix, subRoute.path.pathname),
841
- })
842
- routes.push({
843
- ...subRoute,
844
- path,
845
- })
846
- }
847
- }
848
- return routes
849
- }
850
-
851
- private _defaultRuntimeConfig(): RuntimeConfig {
852
- return {
853
- requestBodyParsers: defaultRequestBodyParsers(),
854
- responseBodySerializers: defaultResponseBodySerializers(),
855
- logger: this._defaultLogger,
856
- requestMiddleware: [],
857
- }
858
- }
859
-
860
- private _handlerRegistration(
861
- handler: Handler<any, any> | undefined,
862
- ): HandlerRegistration | undefined {
863
- return handler ? { handler, router: this } : undefined
864
- }
865
-
866
- private _resolveRuntimeConfig(parent: RuntimeConfig): RuntimeConfig {
867
- const requestBodyParsers =
868
- this._requestBodyParsersMode === 'replace'
869
- ? [...this._requestBodyParsers]
870
- : [...parent.requestBodyParsers, ...this._requestBodyParsers]
871
- const responseBodySerializers =
872
- this._responseBodySerializersMode === 'replace'
873
- ? [...this._responseBodySerializers]
874
- : [...parent.responseBodySerializers, ...this._responseBodySerializers]
875
-
876
- return {
877
- requestBodyParsers,
878
- responseBodySerializers,
879
- logger: this._logger ?? parent.logger,
880
- requestMiddleware: [...parent.requestMiddleware, ...this._requestMiddleware],
881
- onNotFoundHandler:
882
- this._handlerRegistration(this._onNotFoundHandler) ?? parent.onNotFoundHandler,
883
- onMethodNotAllowedHandler:
884
- this._handlerRegistration(this._onMethodNotAllowedHandler) ??
885
- parent.onMethodNotAllowedHandler,
886
- onUnsupportedMediaTypeHandler:
887
- this._handlerRegistration(this._onUnsupportedMediaTypeHandler) ??
888
- parent.onUnsupportedMediaTypeHandler,
889
- onInternalErrorHandler:
890
- this._handlerRegistration(this._onInternalErrorHandler) ??
891
- parent.onInternalErrorHandler,
892
- }
893
- }
894
-
895
- private _match(
896
- request: RoutekitRequest,
897
- method: HttpMethod,
898
- url: URL,
899
- parentConfig: RuntimeConfig,
900
- ):
901
- | MatchResult
902
- | { kind: 'method_not_allowed'; config: RuntimeConfig }
903
- | { kind: 'unsupported_media_type'; found: MatchResult }
904
- | { kind: 'not_found'; config: RuntimeConfig } {
905
- const isHead = method === HttpMethod.HEAD
906
- if (isHead) {
907
- const headOnly = this._matchWithMethodCheck(
908
- request,
909
- url,
910
- (routeMethod) => this._methodMatches(routeMethod, HttpMethod.HEAD),
911
- parentConfig,
912
- [],
913
- )
914
- if (headOnly.match) return headOnly.match
915
- const getOnly = this._matchWithMethodCheck(
916
- request,
917
- url,
918
- (routeMethod) => this._methodMatches(routeMethod, HttpMethod.GET),
919
- parentConfig,
920
- [],
921
- )
922
- if (getOnly.match) return getOnly.match
923
- if (headOnly.unsupportedMediaType)
924
- return { kind: 'unsupported_media_type', found: headOnly.unsupportedMediaType }
925
- if (getOnly.unsupportedMediaType)
926
- return { kind: 'unsupported_media_type', found: getOnly.unsupportedMediaType }
927
- const methodNotAllowedConfig =
928
- headOnly.methodNotAllowedConfig ?? getOnly.methodNotAllowedConfig
929
- if (methodNotAllowedConfig) {
930
- return { kind: 'method_not_allowed', config: methodNotAllowedConfig }
931
- }
932
- return { kind: 'not_found', config: headOnly.notFoundConfig }
933
- }
934
- const match = this._matchWithMethodCheck(
935
- request,
936
- url,
937
- (routeMethod) => this._methodMatches(routeMethod, method),
938
- parentConfig,
939
- [],
940
- )
941
- if (match.match) return match.match
942
- if (match.unsupportedMediaType)
943
- return { kind: 'unsupported_media_type', found: match.unsupportedMediaType }
944
- if (match.methodNotAllowedConfig) {
945
- return { kind: 'method_not_allowed', config: match.methodNotAllowedConfig }
946
- }
947
- return { kind: 'not_found', config: match.notFoundConfig }
948
- }
949
-
950
- private _collectAllowedMethods(url: URL): Set<HttpMethod> {
951
- const methods = new Set<HttpMethod>()
952
- function addMethods(routeMethod?: HttpMethod | HttpMethod[]) {
953
- if (!routeMethod) return
954
- const normalized = Array.isArray(routeMethod) ? routeMethod : [routeMethod]
955
- for (const method of normalized) {
956
- methods.add(method)
957
- }
958
- }
959
- function visit(entries: RouteEntry[], currentUrl: URL) {
960
- for (const entry of entries) {
961
- if (entry.kind === 'route') {
962
- if (entry.route.match) continue
963
- if (!entry.route.path.exec(currentUrl)) continue
964
- addMethods(entry.route.method)
965
- continue
966
- }
967
-
968
- if (entry.prefix) {
969
- const subPathname = stripPrefixPathname(entry.prefix, currentUrl.pathname)
970
- if (subPathname == null) continue
971
- const subUrl = new URL(currentUrl.toString())
972
- subUrl.pathname = subPathname
973
- visit(entry.router._entries, subUrl)
974
- } else {
975
- visit(entry.router._entries, currentUrl)
976
- }
977
- }
978
- }
979
- visit(this._entries, url)
980
- return methods
981
- }
982
-
983
- private _acceptMatches(route: NormalizedRoute<any>, request: RoutekitRequest): boolean {
984
- if (!route.accept || route.accept.length === 0) return true
985
- const contentTypeHeader = request.headers.get(CommonHeaders.CONTENT_TYPE)
986
- if (!contentTypeHeader) return false
987
- const contentType = parseContentType(contentTypeHeader)
988
- if (!contentType) return false
989
- return route.accept.some((accept) => mediaTypeMatches(accept, contentType))
990
- }
991
-
992
- private _formatAllowMethods(methods: Set<HttpMethod>): string {
993
- const order = [
994
- HttpMethod.GET,
995
- HttpMethod.HEAD,
996
- HttpMethod.POST,
997
- HttpMethod.PUT,
998
- HttpMethod.PATCH,
999
- HttpMethod.DELETE,
1000
- HttpMethod.OPTIONS,
1001
- ]
1002
- const ordered: HttpMethod[] = []
1003
- for (const method of order) {
1004
- if (methods.has(method)) ordered.push(method)
1005
- }
1006
- const remaining = [...methods].filter((method) => !order.includes(method)).sort()
1007
- return [...ordered, ...remaining].join(', ')
1008
- }
1009
-
1010
- private _buildHandlerContext(
1011
- request: RoutekitRequest,
1012
- url: URL,
1013
- path: Record<string, string>,
1014
- config: RuntimeConfig,
1015
- routePath?: string,
1016
- ): HandlerContext<Ctx> {
1017
- return {
1018
- request,
1019
- url,
1020
- path,
1021
- logger: config.logger.withContext({
1022
- 'http.request.method': request.method,
1023
- 'url.path': url.pathname,
1024
- ...(routePath == null ? {} : { 'http.route': routePath }),
1025
- }),
1026
- } as HandlerContext<Ctx>
1027
- }
1028
-
1029
- private _runRequestMiddleware(
1030
- middleware: ExecutableRequestMiddleware[],
1031
- ctx: HandlerContext<any>,
1032
- respond: () => Promise<Response>,
1033
- ): Promise<Response> {
1034
- let index = -1
1035
- const dispatch = async (nextIndex: number): Promise<Response> => {
1036
- if (nextIndex <= index) throw new Error('next() called multiple times')
1037
- index = nextIndex
1038
-
1039
- const current = middleware[nextIndex]
1040
- if (current == null) {
1041
- return nextIndex >= middleware.length
1042
- ? await respond()
1043
- : await dispatch(nextIndex + 1)
1044
- }
1045
- return await current(ctx, () => dispatch(nextIndex + 1))
1046
- }
1047
-
1048
- return dispatch(0)
1049
- }
1050
-
1051
- private async _executeHandler(
1052
- handler: Handler<any, any>,
1053
- middleware: ExecutableMiddleware[],
1054
- ctx: HandlerContext<any>,
1055
- router: Router<any>,
1056
- request: RoutekitRequest,
1057
- config: RuntimeConfig,
1058
- ): Promise<Response> {
1059
- const result = await this._run(handler, middleware, ctx, router)
1060
- if (result instanceof Response) {
1061
- return result
1062
- }
1063
- if (this._isAsyncGenerator(result)) {
1064
- const response = await this._responseFromGenerator(result, request, config)
1065
- if (response) return response
1066
- return new Response(null, { status: HttpStatus.CLIENT_CLOSED_REQUEST })
1067
- }
1068
- return await this._finalizeResult(result, request, config)
1069
- }
1070
-
1071
- private _statusResponse(status: HttpStatus, request?: RoutekitRequest): Response {
1072
- const result = text(StatusText[status] ?? `HTTP Status ${status}`, { status })
1073
- return new Response(
1074
- request?.method.toUpperCase() === HttpMethod.HEAD ? null : (result.body as string),
1075
- {
1076
- status,
1077
- headers: result.headers,
1078
- },
1079
- )
1080
- }
1081
-
1082
- private _logInternalServerError(error: unknown, logger: Logger): void {
1083
- try {
1084
- logger.error('Routekit internal server error', error)
1085
- } catch {
1086
- // Logging must not change the response produced for an internal server error.
1087
- }
1088
- }
1089
-
1090
- private async _finalizeResult(
1091
- result: HandlerFinalResult,
1092
- request: RoutekitRequest,
1093
- config: RuntimeConfig,
1094
- ): Promise<Response> {
1095
- if (result instanceof Response) {
1096
- return result
1097
- }
1098
- if (this._isAsyncGenerator(result)) {
1099
- const response = await this._responseFromGenerator(result, request, config)
1100
- if (response) return response
1101
- return new Response(null, { status: HttpStatus.CLIENT_CLOSED_REQUEST })
1102
- }
1103
- if (isRoutekitResponse(result)) {
1104
- return await this._responseFromRoutekitResponse(result, request, config)
1105
- }
1106
- if (isRoutekitBody(result)) {
1107
- return await this._responseFromRoutekitResponse(
1108
- routekitResponse(result.value),
1109
- request,
1110
- config,
1111
- )
1112
- }
1113
- if (isResponseBodyInit(result)) {
1114
- const body = this._toResponseBody(result)
1115
- const headers = new Headers()
1116
- this._setContentLength(headers, body)
1117
- return new Response(body, {
1118
- status: HttpStatus.OK,
1119
- headers,
1120
- })
1121
- }
1122
- return await this._responseFromRoutekitResponse(routekitResponse(result), request, config)
1123
- }
1124
-
1125
- private async _responseFromRoutekitResponse(
1126
- result: RoutekitResponse,
1127
- request: RoutekitRequest,
1128
- config: RuntimeConfig,
1129
- ): Promise<Response> {
1130
- const isHead = request.method.toUpperCase() === HttpMethod.HEAD
1131
- const headers = new Headers(result.headers)
1132
- const status = result.status
1133
-
1134
- if (headers.has(CommonHeaders.CONTENT_TYPE)) {
1135
- if (!isResponseBodyInit(result.body)) {
1136
- throw new TypeError(
1137
- 'Routekit response has Content-Type set, so body must be a native Response body.',
1138
- )
1139
- }
1140
- const body = this._toResponseBody(result.body)
1141
- this._setContentLength(headers, body)
1142
- return new Response(isHead ? null : body, {
1143
- status,
1144
- headers,
1145
- })
1146
- }
1147
-
1148
- if (result.body === undefined) {
1149
- return new Response(null, { status, headers })
1150
- }
1151
-
1152
- const serializer = this._selectSerializer(result.body, request, config)
1153
- if (!serializer) {
1154
- return this._statusResponse(HttpStatus.NOT_ACCEPTABLE, request)
1155
- }
1156
- headers.set(CommonHeaders.CONTENT_TYPE, serializer.mediaType)
1157
- this._appendVary(headers, CommonHeaders.ACCEPT)
1158
- const body = await serializer.serializer.serialize(result.body)
1159
- this._setContentLength(headers, body)
1160
-
1161
- return new Response(isHead ? null : body, {
1162
- status,
1163
- headers,
1164
- })
1165
- }
1166
-
1167
- private _appendVary(headers: Headers, value: string): void {
1168
- const current = headers.get(CommonHeaders.VARY)
1169
- if (!current) {
1170
- headers.set(CommonHeaders.VARY, value)
1171
- return
1172
- }
1173
- const entries = current.split(',').map((entry) => entry.trim().toLowerCase())
1174
- if (entries.includes(value.toLowerCase())) return
1175
- headers.set(CommonHeaders.VARY, `${current}, ${value}`)
1176
- }
1177
-
1178
- private _selectSerializer(
1179
- body: unknown,
1180
- request: RoutekitRequest,
1181
- config: RuntimeConfig,
1182
- ): { serializer: ResponseBodySerializer; mediaType: string } | null {
1183
- const accepted = parseAcceptHeader(request.headers.get(CommonHeaders.ACCEPT) ?? '*/*')
1184
- const ranges = accepted.length ? accepted : parseAcceptHeader('*/*')
1185
- let best: ResponseBodySerializerCandidate | null = null
1186
- for (const [acceptIndex, accept] of ranges.entries()) {
1187
- if (accept.q === 0) continue
1188
- for (const [serializerIndex, serializer] of config.responseBodySerializers.entries()) {
1189
- if (serializer.canSerialize && !serializer.canSerialize(body)) continue
1190
- for (const [mediaTypeIndex, mediaTypeRange] of serializer.mediaTypes.entries()) {
1191
- const mediaTypeQuality = mediaRangeQuality(mediaTypeRange)
1192
- if (mediaTypeQuality === 0) continue
1193
- if (mediaRangeAccepts(accept, mediaTypeRange)) {
1194
- const mediaType = mediaRangeToContentType(mediaTypeRange)
1195
- if (!mediaType) continue
1196
- const candidate = {
1197
- serializer,
1198
- mediaType,
1199
- acceptQuality: accept.q,
1200
- mediaTypeQuality,
1201
- acceptIndex,
1202
- serializerIndex,
1203
- mediaTypeIndex,
1204
- }
1205
- if (this._isBetterSerializerCandidate(candidate, best)) {
1206
- best = candidate
1207
- }
1208
- }
1209
- }
1210
- }
1211
- }
1212
- return best ? { serializer: best.serializer, mediaType: best.mediaType } : null
1213
- }
1214
-
1215
- private _isBetterSerializerCandidate(
1216
- candidate: ResponseBodySerializerCandidate,
1217
- best: ResponseBodySerializerCandidate | null,
1218
- ): boolean {
1219
- if (!best) return true
1220
- if (candidate.acceptQuality !== best.acceptQuality) {
1221
- return candidate.acceptQuality > best.acceptQuality
1222
- }
1223
- if (candidate.mediaTypeQuality !== best.mediaTypeQuality) {
1224
- return candidate.mediaTypeQuality > best.mediaTypeQuality
1225
- }
1226
- if (candidate.acceptIndex !== best.acceptIndex) {
1227
- return candidate.acceptIndex < best.acceptIndex
1228
- }
1229
- if (candidate.serializerIndex !== best.serializerIndex) {
1230
- return candidate.serializerIndex < best.serializerIndex
1231
- }
1232
- return candidate.mediaTypeIndex < best.mediaTypeIndex
1233
- }
1234
-
1235
- private async _tryCustomHandler(
1236
- registration: HandlerRegistration | undefined,
1237
- ctx: HandlerContext<any>,
1238
- config: RuntimeConfig,
1239
- request: RoutekitRequest,
1240
- ): Promise<Response | null> {
1241
- if (!registration) return null
1242
- try {
1243
- return await this._executeHandler(
1244
- registration.handler,
1245
- [],
1246
- ctx,
1247
- registration.router,
1248
- request,
1249
- config,
1250
- )
1251
- } catch (error) {
1252
- if (error instanceof RequestBodyError) return responseFromRequestBodyError(error)
1253
- this._logInternalServerError(error, ctx.logger)
1254
- return this._statusResponse(HttpStatus.INTERNAL_SERVER_ERROR, request)
1255
- }
1256
- }
1257
-
1258
- private _requestWithBodyParsers(
1259
- request: RoutekitRequest,
1260
- config: RuntimeConfig,
1261
- ): RoutekitRequest {
1262
- return createRoutekitRequest(request.raw, request.url, config.requestBodyParsers)
1263
- }
1264
-
1265
- private async _handleNotFound(
1266
- request: RoutekitRequest,
1267
- url: URL,
1268
- config: RuntimeConfig,
1269
- ): Promise<Response> {
1270
- request = this._requestWithBodyParsers(request, config)
1271
- const ctx = this._buildHandlerContext(request, url, {}, config)
1272
- return await this._runRequestMiddleware(config.requestMiddleware, ctx, async () => {
1273
- const custom = await this._tryCustomHandler(
1274
- config.onNotFoundHandler,
1275
- ctx,
1276
- config,
1277
- request,
1278
- )
1279
- if (custom) return custom
1280
- return this._statusResponse(HttpStatus.NOT_FOUND, request)
1281
- })
1282
- }
1283
-
1284
- private async _handleMethodNotAllowed(
1285
- request: RoutekitRequest,
1286
- url: URL,
1287
- config: RuntimeConfig,
1288
- ): Promise<Response> {
1289
- request = this._requestWithBodyParsers(request, config)
1290
- const ctx = this._buildHandlerContext(request, url, {}, config)
1291
- return await this._runRequestMiddleware(config.requestMiddleware, ctx, async () => {
1292
- const custom = await this._tryCustomHandler(
1293
- config.onMethodNotAllowedHandler,
1294
- ctx,
1295
- config,
1296
- request,
1297
- )
1298
- if (custom) return custom
1299
- return this._statusResponse(HttpStatus.METHOD_NOT_ALLOWED, request)
1300
- })
1301
- }
1302
-
1303
- private async _handleInternalError(
1304
- config: RuntimeConfig,
1305
- ctx: HandlerContext<any>,
1306
- request: RoutekitRequest,
1307
- error: unknown,
1308
- ): Promise<Response> {
1309
- if (error instanceof RequestBodyError) return responseFromRequestBodyError(error)
1310
- this._logInternalServerError(error, ctx.logger)
1311
- const custom = await this._tryCustomHandler(
1312
- config.onInternalErrorHandler,
1313
- ctx,
1314
- config,
1315
- request,
1316
- )
1317
- if (custom) return custom
1318
- return this._statusResponse(HttpStatus.INTERNAL_SERVER_ERROR, request)
1319
- }
1320
-
1321
- private async _handleUnsupportedMediaType(
1322
- found: MatchResult,
1323
- request: RoutekitRequest,
1324
- url: URL,
1325
- ): Promise<Response> {
1326
- request = this._requestWithBodyParsers(request, found.config)
1327
- const rawPathParams = found.match?.pathname.groups ?? {}
1328
- const path = Object.fromEntries(
1329
- Object.entries(rawPathParams).filter(([, value]) => value !== undefined),
1330
- ) as Record<string, string>
1331
- const handlerCtx = this._buildHandlerContext(
1332
- request,
1333
- url,
1334
- path,
1335
- found.config,
1336
- found.routePath,
1337
- )
1338
- return await this._runRequestMiddleware(
1339
- found.config.requestMiddleware,
1340
- handlerCtx,
1341
- async () => {
1342
- const custom = await this._tryCustomHandler(
1343
- found.config.onUnsupportedMediaTypeHandler,
1344
- handlerCtx,
1345
- found.config,
1346
- request,
1347
- )
1348
- if (custom) return custom
1349
- return this._statusResponse(HttpStatus.UNSUPPORTED_MEDIA_TYPE, request)
1350
- },
1351
- )
1352
- }
1353
-
1354
- private async _handleMatch(
1355
- found: MatchResult,
1356
- request: RoutekitRequest,
1357
- url: URL,
1358
- ): Promise<Response> {
1359
- request = this._requestWithBodyParsers(request, found.config)
1360
- const rawPathParams = found.match?.pathname.groups ?? {}
1361
- const path = Object.fromEntries(
1362
- Object.entries(rawPathParams).filter(([, value]) => value !== undefined),
1363
- ) as Record<string, string>
1364
- const handlerCtx = this._buildHandlerContext(
1365
- request,
1366
- url,
1367
- path,
1368
- found.config,
1369
- found.routePath,
1370
- )
1371
-
1372
- return await this._runRequestMiddleware(
1373
- found.config.requestMiddleware,
1374
- handlerCtx,
1375
- async () => {
1376
- try {
1377
- mergeRouteSchemas(
1378
- ...found.middleware.flatMap((value) =>
1379
- isDeclaredMiddleware(value) &&
1380
- (!value.schemaAppliesTo || value.schemaAppliesTo(found.route))
1381
- ? [value.schema]
1382
- : [],
1383
- ),
1384
- found.route.schema,
1385
- )
1386
- return await this._executeHandler(
1387
- found.route.handler,
1388
- found.middleware,
1389
- handlerCtx,
1390
- found.router,
1391
- request,
1392
- found.config,
1393
- )
1394
- } catch (error) {
1395
- return await this._handleInternalError(found.config, handlerCtx, request, error)
1396
- }
1397
- },
1398
- )
1399
- }
1400
-
1401
- private _methodMatches(
1402
- routeMethod: HttpMethod | HttpMethod[] | undefined,
1403
- method: HttpMethod,
1404
- ): boolean {
1405
- if (!routeMethod) return true
1406
- const normalized = Array.isArray(routeMethod) ? routeMethod : [routeMethod]
1407
- return normalized.some((routeValue) => routeValue === method)
1408
- }
1409
-
1410
- private _withRoutePrefix(match: MatchResult, prefix?: string): MatchResult {
1411
- return prefix == null
1412
- ? match
1413
- : {
1414
- ...match,
1415
- routePath: joinPrefixPathname(prefix, match.routePath),
1416
- }
1417
- }
1418
-
1419
- private _matchWithMethodCheck(
1420
- request: RoutekitRequest,
1421
- url: URL,
1422
- methodCheck: (routeMethod?: HttpMethod | HttpMethod[]) => boolean,
1423
- parentConfig: RuntimeConfig,
1424
- parentMiddleware: ExecutableMiddleware[],
1425
- ): MatchAttempt {
1426
- const config = this._resolveRuntimeConfig(parentConfig)
1427
- const middleware = [...parentMiddleware, ...this._middleware]
1428
- let methodNotAllowedConfig: RuntimeConfig | null = null
1429
- let unsupportedMediaType: MatchResult | null = null
1430
- let notFoundConfig = config
1431
- for (const entry of this._entries) {
1432
- if (entry.kind === 'route') {
1433
- const route = entry.route
1434
- const routeMiddleware = [...middleware, ...entry.middleware]
1435
- const pathMatch = route.path.exec(url)
1436
- if (route.match) {
1437
- if (!route.match(request)) continue
1438
- return {
1439
- match: {
1440
- kind: 'match',
1441
- route,
1442
- routePath: route.path.pathname,
1443
- match: pathMatch,
1444
- middleware: routeMiddleware,
1445
- router: this,
1446
- config,
1447
- },
1448
- unsupportedMediaType,
1449
- methodNotAllowedConfig,
1450
- notFoundConfig,
1451
- }
1452
- }
1453
- if (!pathMatch) continue
1454
- if (!methodCheck(route.method)) {
1455
- methodNotAllowedConfig ??= config
1456
- continue
1457
- }
1458
- if (!this._acceptMatches(route, request)) {
1459
- unsupportedMediaType ??= {
1460
- kind: 'match',
1461
- route,
1462
- routePath: route.path.pathname,
1463
- match: pathMatch,
1464
- middleware: routeMiddleware,
1465
- router: this,
1466
- config,
1467
- }
1468
- continue
1469
- }
1470
- return {
1471
- match: {
1472
- kind: 'match',
1473
- route,
1474
- routePath: route.path.pathname,
1475
- match: pathMatch,
1476
- middleware: routeMiddleware,
1477
- router: this,
1478
- config,
1479
- },
1480
- unsupportedMediaType,
1481
- methodNotAllowedConfig,
1482
- notFoundConfig,
1483
- }
1484
- }
1485
-
1486
- if (entry.prefix) {
1487
- const subPathname = stripPrefixPathname(entry.prefix, url.pathname)
1488
- if (subPathname == null) continue
1489
- const subUrl = new URL(url.toString())
1490
- subUrl.pathname = subPathname
1491
- const subMatch = entry.router._matchWithMethodCheck(
1492
- request,
1493
- subUrl,
1494
- methodCheck,
1495
- config,
1496
- middleware,
1497
- )
1498
- if (subMatch.match) {
1499
- return {
1500
- match: this._withRoutePrefix(subMatch.match, entry.prefix),
1501
- unsupportedMediaType,
1502
- methodNotAllowedConfig,
1503
- notFoundConfig,
1504
- }
1505
- }
1506
- if (subMatch.unsupportedMediaType && !unsupportedMediaType) {
1507
- unsupportedMediaType = this._withRoutePrefix(
1508
- subMatch.unsupportedMediaType,
1509
- entry.prefix,
1510
- )
1511
- }
1512
- methodNotAllowedConfig ??= subMatch.methodNotAllowedConfig
1513
- notFoundConfig = subMatch.notFoundConfig
1514
- } else {
1515
- const subMatch = entry.router._matchWithMethodCheck(
1516
- request,
1517
- url,
1518
- methodCheck,
1519
- config,
1520
- middleware,
1521
- )
1522
- if (subMatch.match) {
1523
- return {
1524
- match: subMatch.match,
1525
- unsupportedMediaType,
1526
- methodNotAllowedConfig,
1527
- notFoundConfig,
1528
- }
1529
- }
1530
- if (subMatch.unsupportedMediaType && !unsupportedMediaType) {
1531
- unsupportedMediaType = subMatch.unsupportedMediaType
1532
- }
1533
- methodNotAllowedConfig ??= subMatch.methodNotAllowedConfig
1534
- notFoundConfig = subMatch.notFoundConfig
1535
- }
1536
- }
1537
- return { match: null, unsupportedMediaType, methodNotAllowedConfig, notFoundConfig }
1538
- }
1539
-
1540
- private _run(
1541
- handler: Handler<any, any>,
1542
- middleware: ExecutableMiddleware[],
1543
- ctx: HandlerContext<Ctx>,
1544
- router: Router<any>,
1545
- ): Promise<HandlerResult> {
1546
- let idx = -1
1547
- const dispatch = async (i: number): Promise<HandlerResult> => {
1548
- if (i <= idx) throw new Error('next() called multiple times')
1549
- idx = i
1550
-
1551
- if (i === middleware.length) {
1552
- return await Promise.resolve().then(() => handler.call(router, ctx as any))
1553
- }
1554
-
1555
- const mw = middleware[i]
1556
- if (!mw) {
1557
- return await dispatch(i + 1)
1558
- }
1559
- if (isDeclaredMiddleware(mw)) {
1560
- return await mw(ctx as HandlerContext<any>, () => dispatch(i + 1))
1561
- }
1562
- await mw(ctx as RequestContext<any>)
1563
- return await dispatch(i + 1)
1564
- }
1565
-
1566
- return dispatch(0)
1567
- }
1568
-
1569
- private _isAsyncGenerator(
1570
- value: unknown,
1571
- ): value is AsyncGenerator<HandlerYield, HandlerFinalResult> {
1572
- return !!value && typeof (value as AsyncGenerator)[Symbol.asyncIterator] === 'function'
1573
- }
1574
-
1575
- private async _closeGenerator(
1576
- generator: AsyncGenerator<HandlerYield, HandlerFinalResult>,
1577
- ): Promise<void> {
1578
- try {
1579
- await generator.return?.(undefined as unknown as HandlerFinalResult)
1580
- } catch {
1581
- // Ignore generator errors during teardown.
1582
- }
1583
- }
1584
-
1585
- private _toResponseBody(body: HandlerBody | null): RouterBodyInit | null {
1586
- if (body == null) return null
1587
- return body
1588
- }
1589
-
1590
- private _setContentLength(headers: Headers, body: RouterBodyInit | null): void {
1591
- if (headers.has(CommonHeaders.CONTENT_LENGTH) || body == null) {
1592
- return
1593
- }
1594
-
1595
- const size = this._getBodyByteLength(body)
1596
- if (size != null) {
1597
- headers.set(CommonHeaders.CONTENT_LENGTH, String(size))
1598
- }
1599
- }
1600
-
1601
- private _getBodyByteLength(body: RouterBodyInit): number | undefined {
1602
- if (typeof body === 'string') {
1603
- return new TextEncoder().encode(body).byteLength
1604
- }
1605
-
1606
- if (body instanceof Blob) {
1607
- return body.size
1608
- }
1609
-
1610
- if (body instanceof URLSearchParams) {
1611
- return new TextEncoder().encode(body.toString()).byteLength
1612
- }
1613
-
1614
- if (body instanceof ArrayBuffer) {
1615
- return body.byteLength
1616
- }
1617
-
1618
- if (ArrayBuffer.isView(body)) {
1619
- return body.byteLength
1620
- }
1621
-
1622
- return undefined
1623
- }
1624
-
1625
- private _isBodyChunk(value: unknown): value is Uint8Array | Buffer | string {
1626
- return typeof value === 'string' || value instanceof Uint8Array
1627
- }
1628
-
1629
- private _toBodyChunk(value: Uint8Array | Buffer | string): Uint8Array {
1630
- if (typeof value === 'string') {
1631
- return new TextEncoder().encode(value)
1632
- }
1633
- return value
1634
- }
1635
-
1636
- private _mergeHeaders(target: Headers, source: Headers): void {
1637
- source.forEach((value, key) => target.set(key, value))
1638
- }
1639
-
1640
- private async _frameChunk(
1641
- value: unknown,
1642
- framer: StreamFramer | undefined,
1643
- ): Promise<Uint8Array> {
1644
- if (!framer) {
1645
- if (!this._isBodyChunk(value)) {
1646
- throw new TypeError(
1647
- 'Generator yielded a structured chunk without stream(). Use stream(sseFramer()) or yield raw string/bytes chunks.',
1648
- )
1649
- }
1650
- return this._toBodyChunk(value)
1651
- }
1652
- if (framer.canFrame && !framer.canFrame(value)) {
1653
- throw new TypeError('Stream framer cannot encode the yielded chunk.')
1654
- }
1655
- return this._toBodyChunk(await framer.frame(value as never))
1656
- }
1657
-
1658
- private async _responseFromGenerator(
1659
- generator: AsyncGenerator<HandlerYield, HandlerFinalResult>,
1660
- request: RoutekitRequest,
1661
- config: RuntimeConfig,
1662
- ): Promise<Response | null> {
1663
- const isHead = request.method.toUpperCase() === HttpMethod.HEAD
1664
- let status: number | HttpStatus | undefined
1665
- let statusSet = false
1666
- const headers = new Headers()
1667
- let framer: StreamFramer | undefined
1668
- let bodyStream: ReadableStream<Uint8Array> | undefined
1669
- let bodyController: ReadableStreamDefaultController<Uint8Array> | undefined
1670
- let responseResolved = false
1671
- let resolveResponse: ((value: Response | null) => void) | undefined
1672
- let rejectResponse: ((reason?: unknown) => void) | undefined
1673
- const responsePromise = new Promise<Response | null>((resolve, reject) => {
1674
- resolveResponse = resolve
1675
- rejectResponse = reject
1676
- })
1677
- let abortListener: (() => void) | undefined
1678
- const abortSignal = request.signal
1679
- const abortPromise = abortSignal
1680
- ? new Promise<'aborted'>((resolve) => {
1681
- if (abortSignal.aborted) {
1682
- resolve('aborted')
1683
- return
1684
- }
1685
- abortListener = () => resolve('aborted')
1686
- abortSignal.addEventListener('abort', abortListener, { once: true })
1687
- })
1688
- : null
1689
-
1690
- if (abortSignal?.aborted) {
1691
- await this._closeGenerator(generator)
1692
- return null
1693
- }
1694
-
1695
- const resolveResponseOnce = (response: Response | null) => {
1696
- if (responseResolved) return
1697
- responseResolved = true
1698
- resolveResponse?.(response)
1699
- }
1700
-
1701
- const rejectResponseOnce = (error: unknown) => {
1702
- if (responseResolved) {
1703
- bodyController?.error?.(error)
1704
- return
1705
- }
1706
- responseResolved = true
1707
- rejectResponse?.(error)
1708
- }
1709
-
1710
- const buildResponseInit = (): ResponseInit => ({
1711
- status: status ?? HttpStatus.OK,
1712
- headers,
1713
- })
1714
-
1715
- const ensureStreamResponse = () => {
1716
- if (responseResolved) return
1717
- if (!bodyStream) {
1718
- bodyStream = new ReadableStream<Uint8Array>({
1719
- start(controller) {
1720
- bodyController = controller
1721
- },
1722
- })
1723
- }
1724
- resolveResponseOnce(new Response(bodyStream, buildResponseInit()))
1725
- }
1726
-
1727
- const setStatus = (value: number | HttpStatus) => {
1728
- if (bodyStream) {
1729
- throw new TypeError('Generator cannot set status after streaming has started.')
1730
- }
1731
- if (statusSet) {
1732
- throw new TypeError('Generator response status was already set.')
1733
- }
1734
- status = value
1735
- statusSet = true
1736
- }
1737
-
1738
- const mergeHeaders = (value: Headers) => {
1739
- if (bodyStream) {
1740
- throw new TypeError('Generator cannot set headers after streaming has started.')
1741
- }
1742
- this._mergeHeaders(headers, value)
1743
- }
1744
-
1745
- const finalizeReturnedValue = async (value: HandlerFinalResult) => {
1746
- if (isRoutekitResponse(value)) {
1747
- if (statusSet) {
1748
- throw new TypeError(
1749
- 'Generator returned a Routekit response after status was already set.',
1750
- )
1751
- }
1752
- const mergedHeaders = new Headers(headers)
1753
- this._mergeHeaders(mergedHeaders, value.headers)
1754
- return await this._responseFromRoutekitResponse(
1755
- routekitResponse(value.body, {
1756
- status: value.status,
1757
- headers: mergedHeaders,
1758
- }),
1759
- request,
1760
- config,
1761
- )
1762
- }
1763
- if (value instanceof Response) {
1764
- if (statusSet || [...headers.keys()].length > 0) {
1765
- throw new TypeError(
1766
- 'Generator returned a native Response after response metadata was already set.',
1767
- )
1768
- }
1769
- return value
1770
- }
1771
- const returnedBody = isRoutekitBody(value) ? value.value : value
1772
- return await this._responseFromRoutekitResponse(
1773
- routekitResponse(returnedBody, {
1774
- status: status ?? HttpStatus.OK,
1775
- headers,
1776
- }),
1777
- request,
1778
- config,
1779
- )
1780
- }
1781
-
1782
- try {
1783
- const pump = async () => {
1784
- try {
1785
- while (true) {
1786
- const next = abortPromise
1787
- ? await Promise.race([generator.next(), abortPromise])
1788
- : await generator.next()
1789
- if (next === 'aborted') {
1790
- await this._closeGenerator(generator)
1791
- bodyController?.error?.(new Error('Request aborted'))
1792
- resolveResponseOnce(null)
1793
- return
1794
- }
1795
-
1796
- if (next.done) {
1797
- if (bodyStream) {
1798
- if (next.value !== undefined) {
1799
- throw new TypeError(
1800
- 'Generator cannot return a final body after yielding chunks.',
1801
- )
1802
- }
1803
- if (framer?.close && !isHead) {
1804
- const finalChunk = await framer.close()
1805
- if (finalChunk !== undefined) {
1806
- bodyController?.enqueue(this._toBodyChunk(finalChunk))
1807
- }
1808
- }
1809
- bodyController?.close()
1810
- if (!responseResolved) {
1811
- resolveResponseOnce(
1812
- new Response(bodyStream, buildResponseInit()),
1813
- )
1814
- }
1815
- return
1816
- }
1817
-
1818
- resolveResponseOnce(await finalizeReturnedValue(next.value))
1819
- return
1820
- }
1821
-
1822
- const yielded = next.value
1823
- if (yielded === undefined) {
1824
- continue
1825
- }
1826
-
1827
- if (isStatusDirective(yielded)) {
1828
- setStatus(yielded.status)
1829
- continue
1830
- }
1831
-
1832
- if (isHeadersDirective(yielded)) {
1833
- mergeHeaders(yielded.headers)
1834
- if (isHead) {
1835
- await this._closeGenerator(generator)
1836
- resolveResponseOnce(new Response(null, buildResponseInit()))
1837
- return
1838
- }
1839
- continue
1840
- }
1841
-
1842
- if (isHeadDirective(yielded)) {
1843
- setStatus(yielded.status)
1844
- mergeHeaders(yielded.headers)
1845
- if (isHead) {
1846
- await this._closeGenerator(generator)
1847
- resolveResponseOnce(new Response(null, buildResponseInit()))
1848
- return
1849
- }
1850
- continue
1851
- }
1852
-
1853
- if (isStreamDirective(yielded)) {
1854
- if (bodyStream) {
1855
- throw new TypeError(
1856
- 'Generator cannot select a stream framer after streaming has started.',
1857
- )
1858
- }
1859
- framer = yielded.framer
1860
- mergeHeaders(yielded.headers)
1861
- headers.set(CommonHeaders.CONTENT_TYPE, framer.contentType)
1862
- if (isHead) {
1863
- await this._closeGenerator(generator)
1864
- resolveResponseOnce(new Response(null, buildResponseInit()))
1865
- return
1866
- }
1867
- continue
1868
- }
1869
-
1870
- if (isChunkDirective(yielded)) {
1871
- if (isHead) {
1872
- await this._closeGenerator(generator)
1873
- resolveResponseOnce(new Response(null, buildResponseInit()))
1874
- return
1875
- }
1876
- ensureStreamResponse()
1877
- bodyController?.enqueue(await this._frameChunk(yielded.value, framer))
1878
- continue
1879
- }
1880
-
1881
- throw new TypeError(
1882
- 'Generator yielded an unsupported value. Use status(), headers(), head(), stream(), or chunk().',
1883
- )
1884
- }
1885
- } catch (err) {
1886
- rejectResponseOnce(err)
1887
- }
1888
- }
1889
-
1890
- void pump()
1891
- return await responsePromise
1892
- } finally {
1893
- if (abortSignal && abortListener) {
1894
- abortSignal.removeEventListener('abort', abortListener)
1895
- }
1896
- }
1897
- }
1898
-
1899
- private async _handleRequest(request: Request): Promise<Response> {
1900
- const url = new URL(request.url)
1901
- const method = request.method.toUpperCase() as HttpMethod
1902
- const rootConfig = this._resolveRuntimeConfig(this._defaultRuntimeConfig())
1903
- const routekitRequest = createRoutekitRequest(request, url, rootConfig.requestBodyParsers)
1904
-
1905
- if (method === HttpMethod.OPTIONS) {
1906
- const explicit = this._match(routekitRequest, method, url, this._defaultRuntimeConfig())
1907
- if (explicit.kind === 'match') {
1908
- return await this._handleMatch(explicit, routekitRequest, url)
1909
- }
1910
- if (explicit.kind === 'unsupported_media_type') {
1911
- return await this._handleUnsupportedMediaType(explicit.found, routekitRequest, url)
1912
- }
1913
- const allowedMethods = this._collectAllowedMethods(url)
1914
- if (allowedMethods.size === 0) {
1915
- return await this._handleNotFound(routekitRequest, url, explicit.config)
1916
- }
1917
- if (allowedMethods.has(HttpMethod.GET)) {
1918
- allowedMethods.add(HttpMethod.HEAD)
1919
- }
1920
- allowedMethods.add(HttpMethod.OPTIONS)
1921
- const optionsRequest = this._requestWithBodyParsers(routekitRequest, explicit.config)
1922
- const ctx = this._buildHandlerContext(optionsRequest, url, {}, explicit.config)
1923
- return await this._runRequestMiddleware(
1924
- explicit.config.requestMiddleware,
1925
- ctx,
1926
- async () =>
1927
- new Response(null, {
1928
- status: HttpStatus.NO_CONTENT,
1929
- headers: {
1930
- 'access-control-allow-methods':
1931
- this._formatAllowMethods(allowedMethods),
1932
- },
1933
- }),
1934
- )
1935
- }
1936
-
1937
- const found = this._match(routekitRequest, method, url, this._defaultRuntimeConfig())
1938
- if (found.kind === 'not_found') {
1939
- return await this._handleNotFound(routekitRequest, url, found.config)
1940
- }
1941
- if (found.kind === 'method_not_allowed') {
1942
- return await this._handleMethodNotAllowed(routekitRequest, url, found.config)
1943
- }
1944
- if (found.kind === 'unsupported_media_type') {
1945
- return await this._handleUnsupportedMediaType(found.found, routekitRequest, url)
1946
- }
1947
- return await this._handleMatch(found, routekitRequest, url)
1948
- }
1949
-
1950
- /**
1951
- * Handle an incoming request by matching routes and executing middleware.
1952
- *
1953
- * @example
1954
- * ```ts
1955
- * const response = await router.fetch(new Request('https://example.com/'))
1956
- * ```
1957
- *
1958
- * @param request - Incoming request to route.
1959
- * @returns The response produced by the matched handler.
1960
- */
1961
- fetch = (request: Request): Promise<Response> => {
1962
- // Use = syntax to force `this` binding so that Bun can execute the router directly
1963
- return this._handleRequest(request)
1964
- }
1965
- }