effect-start 0.14.0 → 0.15.0

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 (81) hide show
  1. package/package.json +8 -9
  2. package/src/Commander.test.ts +507 -245
  3. package/src/ContentNegotiation.test.ts +500 -0
  4. package/src/ContentNegotiation.ts +535 -0
  5. package/src/FileRouter.ts +16 -12
  6. package/src/{FileRouterCodegen.test.ts → FileRouterCodegen.todo.ts} +384 -219
  7. package/src/FileRouterCodegen.ts +6 -6
  8. package/src/FileRouterPattern.test.ts +93 -62
  9. package/src/FileRouter_files.test.ts +5 -5
  10. package/src/FileRouter_path.test.ts +121 -69
  11. package/src/FileRouter_tree.test.ts +62 -56
  12. package/src/FileSystemExtra.test.ts +46 -30
  13. package/src/Http.test.ts +24 -0
  14. package/src/Http.ts +25 -0
  15. package/src/HttpAppExtra.test.ts +39 -20
  16. package/src/HttpAppExtra.ts +0 -1
  17. package/src/HttpUtils.test.ts +35 -18
  18. package/src/HttpUtils.ts +2 -0
  19. package/src/PathPattern.test.ts +648 -0
  20. package/src/PathPattern.ts +483 -0
  21. package/src/Route.ts +258 -1073
  22. package/src/RouteBody.test.ts +182 -0
  23. package/src/RouteBody.ts +106 -0
  24. package/src/RouteHook.test.ts +40 -0
  25. package/src/RouteHook.ts +105 -0
  26. package/src/RouteHttp.test.ts +443 -0
  27. package/src/RouteHttp.ts +219 -0
  28. package/src/RouteMount.test.ts +468 -0
  29. package/src/RouteMount.ts +313 -0
  30. package/src/RouteSchema.test.ts +81 -0
  31. package/src/RouteSchema.ts +44 -0
  32. package/src/RouteTree.test.ts +346 -0
  33. package/src/RouteTree.ts +165 -0
  34. package/src/RouteTrie.test.ts +322 -0
  35. package/src/RouteTrie.ts +224 -0
  36. package/src/RouterPattern.test.ts +569 -548
  37. package/src/RouterPattern.ts +7 -7
  38. package/src/Start.ts +3 -3
  39. package/src/TuplePathPattern.ts +64 -0
  40. package/src/Values.ts +16 -0
  41. package/src/bun/BunBundle.test.ts +36 -42
  42. package/src/bun/BunBundle.ts +2 -2
  43. package/src/bun/BunBundle_imports.test.ts +4 -6
  44. package/src/bun/BunHttpServer.test.ts +183 -6
  45. package/src/bun/BunHttpServer.ts +56 -32
  46. package/src/bun/BunHttpServer_web.ts +18 -6
  47. package/src/bun/BunImportTrackerPlugin.test.ts +3 -3
  48. package/src/bun/BunRoute.ts +29 -210
  49. package/src/{BundleHttp.test.ts → bundler/BundleHttp.test.ts} +34 -60
  50. package/src/{BundleHttp.ts → bundler/BundleHttp.ts} +1 -2
  51. package/src/client/index.ts +1 -1
  52. package/src/{Effect_HttpRouter.test.ts → effect/HttpRouter.test.ts} +69 -90
  53. package/src/experimental/EncryptedCookies.test.ts +125 -64
  54. package/src/experimental/SseHttpResponse.ts +0 -1
  55. package/src/hyper/Hyper.ts +89 -0
  56. package/src/{HyperHtml.test.ts → hyper/HyperHtml.test.ts} +13 -13
  57. package/src/{HyperHtml.ts → hyper/HyperHtml.ts} +2 -2
  58. package/src/{jsx.d.ts → hyper/jsx.d.ts} +1 -1
  59. package/src/index.ts +2 -4
  60. package/src/middlewares/BasicAuthMiddleware.test.ts +29 -19
  61. package/src/{NodeFileSystem.ts → node/FileSystem.ts} +6 -2
  62. package/src/testing/TestHttpClient.test.ts +26 -26
  63. package/src/testing/TestLogger.test.ts +27 -11
  64. package/src/x/datastar/Datastar.test.ts +47 -48
  65. package/src/x/datastar/Datastar.ts +1 -1
  66. package/src/x/tailwind/TailwindPlugin.test.ts +56 -58
  67. package/src/x/tailwind/plugin.ts +1 -1
  68. package/src/FileHttpRouter.test.ts +0 -239
  69. package/src/FileHttpRouter.ts +0 -194
  70. package/src/Hyper.ts +0 -194
  71. package/src/Route.test.ts +0 -1370
  72. package/src/RouteRender.ts +0 -40
  73. package/src/Router.test.ts +0 -375
  74. package/src/Router.ts +0 -255
  75. package/src/bun/BunRoute.test.ts +0 -480
  76. package/src/bun/BunRoute_bundles.test.ts +0 -219
  77. /package/src/{Bundle.ts → bundler/Bundle.ts} +0 -0
  78. /package/src/{BundleFiles.ts → bundler/BundleFiles.ts} +0 -0
  79. /package/src/{HyperNode.ts → hyper/HyperNode.ts} +0 -0
  80. /package/src/{jsx-runtime.ts → hyper/jsx-runtime.ts} +0 -0
  81. /package/src/{NodeUtils.ts → node/Utils.ts} +0 -0
package/src/Route.ts CHANGED
@@ -1,1146 +1,331 @@
1
- import * as HttpMethod from "@effect/platform/HttpMethod"
2
- import * as HttpMiddleware from "@effect/platform/HttpMiddleware"
3
- import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
4
- import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
5
- import * as Effect from "effect/Effect"
1
+ import * as Context from "effect/Context"
2
+ import type * as Effect from "effect/Effect"
3
+ import * as Layer from "effect/Layer"
6
4
  import * as Pipeable from "effect/Pipeable"
7
5
  import * as Predicate from "effect/Predicate"
8
- import * as Schema from "effect/Schema"
9
- import type { YieldWrap } from "effect/Utils"
10
- import * as RouteRender from "./RouteRender.ts"
6
+ import * as RouteBody from "./RouteBody.ts"
7
+ import * as RouteTree from "./RouteTree.ts"
8
+ import * as Values from "./Values.ts"
11
9
 
12
- export {
13
- pipe,
14
- } from "effect/Function"
15
-
16
- type RouteModule = typeof import("./Route.ts")
17
-
18
- /**
19
- * 'this' argument type for {@link RouteBuilder} functions.
20
- * Its value depend on how the function is called as described below.
21
- */
22
- type Self =
23
- /**
24
- * Called as {@link RouteSet} method:
25
- *
26
- * @example
27
- * ```ts
28
- * let route: Route
29
- *
30
- * route.text("Hello")
31
- *
32
- * ```
33
- */
34
- | RouteSet.Default
35
- | RouteSet<Route.Empty, RouteSchemas>
36
- /**
37
- * Called from namespaced import.
38
- *
39
- * @example
40
- * ```ts
41
- * import * as Route from "./Route.ts"
42
- *
43
- * let route: Route
44
- *
45
- * Route.text("Hello")
46
- *
47
- * ```
48
- */
49
- | RouteModule
50
- /**
51
- * Called directly from exported function. Don't do it.
52
- *
53
- * @example
54
- * ```ts
55
- * import { text } from "./Route.ts"
56
- *
57
- * text("Hello")
58
- *
59
- * ```
60
- */
61
- | undefined
62
-
63
- const TypeId: unique symbol = Symbol.for("effect-start/Route")
64
- const RouteSetTypeId: unique symbol = Symbol.for("effect-start/RouteSet")
65
- const RouteLayerTypeId: unique symbol = Symbol.for("effect-start/RouteLayer")
66
-
67
- export type RouteMethod =
68
- | "*"
69
- | HttpMethod.HttpMethod
70
-
71
- // TODO: This should be a RouterPattern and moved to its file?
72
- export type RoutePattern = `/${string}`
73
-
74
- /**
75
- * Route media type used for content negotiation.
76
- * This allows to create routes that serve different media types
77
- * for the same path & method, depending on the `Accept` header
78
- * of the request.
79
- */
80
- export type RouteMedia =
81
- | "*"
82
- | "text/plain"
83
- | "text/html"
84
- | "application/json"
10
+ export const RouteItems: unique symbol = Symbol()
11
+ export const RouteDescriptor: unique symbol = Symbol()
12
+ // only for structural type matching
13
+ export const RouteBindings: unique symbol = Symbol()
85
14
 
86
- /**
87
- * A handler function that produces a raw value.
88
- * The value will be rendered to an HttpServerResponse by RouteRender
89
- * based on the route's media type.
90
- *
91
- * Receives RouteContext which includes an optional next() for layers.
92
- */
93
- export type RouteHandler<
94
- A = unknown,
95
- E = any,
96
- R = any,
97
- > = (context: RouteContext) => Effect.Effect<A, E, R>
15
+ export const TypeId: unique symbol = Symbol.for("effect-start/RouteSet")
98
16
 
99
- /**
100
- * Helper type for a value that can be a single item or an array.
101
- */
102
- export type OneOrMany<T> = T | T[] | readonly T[]
103
-
104
- export type RouteSchemas = {
105
- readonly PathParams?: Schema.Struct<any>
106
- readonly UrlParams?: Schema.Struct<any>
107
- readonly Payload?: Schema.Schema.Any
108
- readonly Success?: Schema.Schema.Any
109
- readonly Error?: Schema.Schema.Any
110
- readonly Headers?: Schema.Struct<any>
111
- }
112
-
113
- export namespace RouteSchemas {
114
- export type Empty = {
115
- readonly PathParams?: never
116
- readonly UrlParams?: never
117
- readonly Payload?: never
118
- readonly Success?: never
119
- readonly Error?: never
120
- readonly Headers?: never
17
+ export namespace RouteDescriptor {
18
+ export type Any = {
19
+ [key: string]: unknown
121
20
  }
122
21
  }
123
22
 
124
- export interface Route<
125
- out Method extends RouteMethod = "*",
126
- out Media extends RouteMedia = "*",
127
- out Handler extends RouteHandler = RouteHandler,
128
- out Schemas extends RouteSchemas = RouteSchemas.Empty,
129
- > extends RouteSet<[Route.Default], Schemas> {
130
- [TypeId]: typeof TypeId
131
- readonly method: Method
132
- readonly media: Media
133
- readonly handler: Handler
134
- readonly schemas: Schemas
135
- }
23
+ export namespace RouteSet {
24
+ export type RouteSet<
25
+ D extends RouteDescriptor.Any = {},
26
+ B = {},
27
+ M extends Route.Tuple = [],
28
+ > =
29
+ & Data<D, B, M>
30
+ & {
31
+ [TypeId]: typeof TypeId
32
+ }
33
+ & Pipeable.Pipeable
34
+ & Iterable<M[number]>
136
35
 
137
- /**
138
- * Describes a single route that varies by method & media type.
139
- *
140
- * Implements {@link RouteSet} interface that contains itself.
141
- */
142
- export namespace Route {
143
36
  export type Data<
144
- Method extends RouteMethod = RouteMethod,
145
- Media extends RouteMedia = RouteMedia,
146
- Handler extends RouteHandler = RouteHandler,
147
- Schemas extends RouteSchemas = RouteSchemas.Empty,
37
+ D extends RouteDescriptor.Any = {},
38
+ B = {},
39
+ M extends Route.Tuple = [],
148
40
  > = {
149
- readonly method: Method
150
- readonly media: Media
151
- readonly handler: Handler
152
- readonly schemas: Schemas
41
+ [RouteItems]: M
42
+ [RouteDescriptor]: D
43
+ [RouteBindings]: B
153
44
  }
154
45
 
155
- export type Default = Route<
156
- RouteMethod,
157
- RouteMedia,
158
- RouteHandler,
159
- RouteSchemas
160
- >
161
-
162
- export type Tuple = readonly [Default, ...Default[]]
163
-
164
- export type Empty = readonly []
165
-
166
46
  export type Proto =
167
47
  & Pipeable.Pipeable
48
+ & Iterable<Route.Route<any, any, any, any, any>>
168
49
  & {
169
50
  [TypeId]: typeof TypeId
170
51
  }
171
- }
172
52
 
173
- /**
174
- * Consists of function to build {@link RouteSet}.
175
- * This should include all exported functions in this module ({@link RouteModule})
176
- * that have `this` as {@link Self}.
177
- *
178
- * Method functions, like {@link post}, modify the method of existing routes.
179
- * Media functions, like {@link json}, create new routes with specific media type.
180
- */
181
- type RouteBuilder = {
182
- post: typeof post
183
- get: typeof get
184
- put: typeof put
185
- patch: typeof patch
186
- delete: typeof _delete
187
- options: typeof options
188
- head: typeof head
53
+ export type Any = RouteSet<{}, {}, Route.Tuple>
189
54
 
190
- text: typeof text
191
- html: typeof html
192
- json: typeof json
55
+ export type Infer<R> = R extends RouteSet<infer D, infer B, infer I>
56
+ ? RouteSet<D, B, I>
57
+ : R
193
58
 
194
- schemaPathParams: typeof schemaPathParams
195
- schemaUrlParams: typeof schemaUrlParams
196
- schemaPayload: typeof schemaPayload
197
- schemaSuccess: typeof schemaSuccess
198
- schemaError: typeof schemaError
199
- schemaHeaders: typeof schemaHeaders
200
- }
59
+ export type Items<
60
+ T extends Data<any, any, any>,
61
+ > = T extends Data<
62
+ any,
63
+ any,
64
+ infer M
65
+ > ? M
66
+ : never
201
67
 
202
- /**
203
- * Set of one or many {@link Route} with chainable builder functions
204
- * to modify the set or add new routes.
205
- */
206
- export type RouteSet<
207
- M extends ReadonlyArray<Route.Default>,
208
- Schemas extends RouteSchemas = RouteSchemas.Empty,
209
- > =
210
- & Pipeable.Pipeable
211
- & RouteSet.Instance<M, Schemas>
212
- & {
213
- [RouteSetTypeId]: typeof RouteSetTypeId
214
- }
215
- & RouteBuilder
68
+ export type Descriptor<
69
+ T extends Data<any, any, any>,
70
+ > = T extends Data<infer D, any, any> ? D : never
71
+ }
216
72
 
217
- export namespace RouteSet {
218
- export type Instance<
219
- M extends ReadonlyArray<Route.Default> = Route.Tuple,
220
- Schemas extends RouteSchemas = RouteSchemas.Empty,
221
- > = {
222
- set: M
223
- schema: Schemas
73
+ export namespace Route {
74
+ export interface Route<
75
+ D extends RouteDescriptor.Any = {},
76
+ B = {},
77
+ A = any,
78
+ E = never,
79
+ R = never,
80
+ > extends
81
+ RouteSet.RouteSet<D, {}, [
82
+ Route<D, B, A, E, R>,
83
+ ]>
84
+ {
85
+ readonly handler: Handler<B & D, A, E, R>
224
86
  }
225
87
 
226
- export type Default = RouteSet<Route.Tuple, RouteSchemas>
227
-
228
- export type Proto =
88
+ export type With<D extends RouteDescriptor.Any> =
89
+ & Route<any, any, any, any, any>
229
90
  & {
230
- [RouteSetTypeId]: typeof RouteSetTypeId
91
+ [RouteDescriptor]: D
231
92
  }
232
- & RouteBuilder
233
- }
234
-
235
- /**
236
- * Type for HTTP middleware function
237
- */
238
- export type HttpMiddlewareFunction = ReturnType<typeof HttpMiddleware.make>
239
-
240
- /**
241
- * Marker type for route middleware specification.
242
- * Used to distinguish middleware from routes in Route.layer() arguments.
243
- */
244
- export interface RouteMiddleware {
245
- readonly _tag: "RouteMiddleware"
246
- readonly middleware: HttpMiddlewareFunction
247
- }
248
-
249
- export type RouteLayer<
250
- M extends ReadonlyArray<Route.Default> = ReadonlyArray<Route.Default>,
251
- Schemas extends RouteSchemas = RouteSchemas.Empty,
252
- > =
253
- & Pipeable.Pipeable
254
- & {
255
- [RouteLayerTypeId]: typeof RouteLayerTypeId
256
- [RouteSetTypeId]: typeof RouteSetTypeId
257
- set: M
258
- schema: Schemas
259
- httpMiddleware?: HttpMiddlewareFunction
260
- }
261
- & RouteBuilder
262
-
263
- export const isRouteLayer = (u: unknown): u is RouteLayer =>
264
- Predicate.hasProperty(u, RouteLayerTypeId)
265
93
 
266
- /**
267
- * Check if two routes match based on method and media type.
268
- * Returns true if both method and media type match, accounting for wildcards.
269
- */
270
- export function matches(
271
- a: Route.Default,
272
- b: Route.Default,
273
- ): boolean {
274
- const methodMatches = a.method === "*"
275
- || b.method === "*"
276
- || a.method === b.method
277
-
278
- const mediaMatches = a.media === "*"
279
- || b.media === "*"
280
- || a.media === b.media
281
-
282
- return methodMatches && mediaMatches
283
- }
284
-
285
- export const post = makeMethodModifier("POST")
286
- export const get = makeMethodModifier("GET")
287
- export const put = makeMethodModifier("PUT")
288
- export const patch = makeMethodModifier("PATCH")
289
- export const options = makeMethodModifier("OPTIONS")
290
- export const head = makeMethodModifier("HEAD")
291
- const _delete = makeMethodModifier("DELETE")
292
- export {
293
- _delete as delete,
294
- }
295
-
296
- export const text = makeMediaFunction<"GET", "text/plain", string>(
297
- "GET",
298
- "text/plain",
299
- )
300
-
301
- export const html = makeMediaFunction<
302
- "GET",
303
- "text/html",
304
- string | GenericJsxObject
305
- >(
306
- "GET",
307
- "text/html",
308
- )
309
-
310
- export const json = makeMediaFunction<"GET", "application/json", JsonValue>(
311
- "GET",
312
- "application/json",
313
- )
314
-
315
- /**
316
- * Schema type that accepts string-encoded input.
317
- * Used for path parameters which are always strings.
318
- */
319
- type StringEncodedSchema =
320
- | Schema.Schema<any, string, any>
321
- // TODO: we accept PropertySignature to support Schema.optional
322
- // not great but Effect 4 should be better about it
323
- | Schema.PropertySignature.All
324
-
325
- /**
326
- * Schema type that accepts string or string array encoded input.
327
- * Used for URL params and headers which can have multiple values.
328
- */
329
- type StringOrArrayEncodedSchema =
330
- | Schema.Schema<any, OneOrMany<string>, any>
331
- | Schema.PropertySignature.All
332
-
333
- /**
334
- * Helper type to extract the Encoded type from a Schema.
335
- */
336
- type GetEncoded<S> = S extends { Encoded: infer E } ? E : never
337
-
338
- /**
339
- * Check if a schema's encoded type is string.
340
- */
341
- type IsStringEncoded<S> = S extends Schema.PropertySignature.All ? true
342
- : GetEncoded<S> extends string ? true
343
- : false
344
-
345
- /**
346
- * Check if a schema's encoded type is string or string array.
347
- */
348
- type IsStringOrArrayEncoded<S> = S extends Schema.PropertySignature.All ? true
349
- : GetEncoded<S> extends OneOrMany<string> ? true
350
- : false
351
-
352
- /**
353
- * Validate that all fields have string-encoded schemas.
354
- */
355
- type ValidateStringEncodedFields<T extends Record<PropertyKey, any>> = {
356
- [K in keyof T]: IsStringEncoded<T[K]> extends true ? T[K]
357
- : StringEncodedSchema
358
- }
359
-
360
- /**
361
- * Validate that all fields have string or array-encoded schemas.
362
- */
363
- type ValidateStringOrArrayEncodedFields<T extends Record<PropertyKey, any>> = {
364
- [K in keyof T]: IsStringOrArrayEncoded<T[K]> extends true ? T[K]
365
- : StringOrArrayEncodedSchema
366
- }
367
-
368
- function makeSingleStringSchemaModifier<
369
- K extends string,
370
- >(key: K) {
371
- return function<
372
- S extends Self,
373
- const Fields extends Record<PropertyKey, any>,
374
- >(
375
- this: S,
376
- fieldsOrSchema: Fields extends Schema.Struct<any> ? Fields
377
- : ValidateStringEncodedFields<Fields>,
378
- ): S extends RouteSet<infer Routes, infer Schemas> ? RouteSet<
379
- Routes,
380
- & Schemas
381
- & {
382
- [P in K]: Fields extends Schema.Struct<infer F> ? Schema.Struct<F>
383
- : Schema.Struct<
384
- Fields extends Record<PropertyKey, infer _> ? Fields : never
385
- >
386
- }
387
- >
388
- : RouteSet<
389
- [],
390
- {
391
- [P in K]: Fields extends Schema.Struct<infer F> ? Schema.Struct<F>
392
- : Schema.Struct<
393
- Fields extends Record<PropertyKey, infer _> ? Fields : never
394
- >
395
- }
396
- >
397
- {
398
- const baseRoutes = isRouteSet(this)
399
- ? this.set
400
- : [] as const
401
- const baseSchema = isRouteSet(this)
402
- ? this.schema
403
- : {} as RouteSchemas.Empty
404
-
405
- const schema = Schema.isSchema(fieldsOrSchema)
406
- ? fieldsOrSchema
407
- : Schema.Struct(fieldsOrSchema as Schema.Struct.Fields)
408
-
409
- return makeSet(
410
- baseRoutes as ReadonlyArray<Route.Default>,
411
- {
412
- ...baseSchema,
413
- [key]: schema,
414
- },
415
- ) as never
416
- }
417
- }
94
+ export type Tuple<
95
+ _D extends RouteDescriptor.Any = {},
96
+ > = [
97
+ ...Route<any, any, any, any, any>[],
98
+ ]
418
99
 
419
- function makeMultiStringSchemaModifier<
420
- K extends string,
421
- >(key: K) {
422
- return function<
423
- S extends Self,
424
- const Fields extends Record<PropertyKey, any>,
425
- >(
426
- this: S,
427
- fieldsOrSchema: Fields extends Schema.Struct<any> ? Fields
428
- : ValidateStringOrArrayEncodedFields<Fields>,
429
- ): S extends RouteSet<infer Routes, infer Schemas> ? RouteSet<
430
- Routes,
431
- & Schemas
432
- & {
433
- [P in K]: Fields extends Schema.Struct<infer F> ? Schema.Struct<F>
434
- : Schema.Struct<
435
- Fields extends Record<PropertyKey, infer _> ? Fields : never
436
- >
437
- }
438
- >
439
- : RouteSet<
440
- [],
441
- {
442
- [P in K]: Fields extends Schema.Struct<infer F> ? Schema.Struct<F>
443
- : Schema.Struct<
444
- Fields extends Record<PropertyKey, infer _> ? Fields : never
445
- >
446
- }
447
- >
448
- {
449
- const baseRoutes = isRouteSet(this)
450
- ? this.set
451
- : [] as const
452
- const baseSchema = isRouteSet(this)
453
- ? this.schema
454
- : {} as RouteSchemas.Empty
100
+ export type Handler<B, A, E, R> = (
101
+ context: B,
102
+ next: (context: B) => Effect.Effect<A>,
103
+ ) => Effect.Effect<A, E, R>
455
104
 
456
- const schema = Schema.isSchema(fieldsOrSchema)
457
- ? fieldsOrSchema
458
- : Schema.Struct(fieldsOrSchema as Schema.Struct.Fields)
105
+ // handler that cannot modify the context
106
+ export type HandlerImmutable<B, A, E, R> = (
107
+ context: B,
108
+ next: () => Effect.Effect<A>,
109
+ ) => Effect.Effect<A, E, R>
459
110
 
460
- return makeSet(
461
- baseRoutes as ReadonlyArray<Route.Default>,
462
- {
463
- ...baseSchema,
464
- [key]: schema,
465
- },
466
- ) as never
467
- }
468
- }
111
+ /**
112
+ * Extracts only the bindings (B) from routes, excluding descriptors.
113
+ */
114
+ export type Bindings<
115
+ T extends RouteSet.Any,
116
+ M extends Tuple = RouteSet.Items<T>,
117
+ > = M extends [
118
+ infer Head,
119
+ ...infer Tail extends Tuple,
120
+ ] ? (
121
+ Head extends Route<any, infer B, any, any, any>
122
+ ? ShallowMerge<B, Bindings<T, Tail>>
123
+ : Bindings<T, Tail>
124
+ )
125
+ : {}
469
126
 
470
- function makeUnionSchemaModifier<
471
- K extends "Payload" | "Success" | "Error",
472
- >(key: K) {
473
- return function<
474
- S extends Self,
475
- Fields extends Schema.Struct.Fields | Schema.Schema.Any,
476
- >(
477
- this: S,
478
- fieldsOrSchema: Fields,
479
- ): S extends RouteSet<infer Routes, infer Schemas> ? RouteSet<
480
- Routes,
481
- & Schemas
482
- & {
483
- [P in K]: Fields extends Schema.Schema.Any ? Fields
484
- : Fields extends Schema.Struct.Fields ? Schema.Struct<Fields>
485
- : never
486
- }
487
- >
488
- : RouteSet<
489
- [],
490
- {
491
- [P in K]: Fields extends Schema.Schema.Any ? Fields
492
- : Fields extends Schema.Struct.Fields ? Schema.Struct<Fields>
493
- : never
494
- }
127
+ /**
128
+ * Extracts the full handler context from a RouteSet.
129
+ * Merges descriptors and bindings from all routes, with later values
130
+ * taking precedence via ShallowMerge to avoid `never` from conflicting
131
+ * literal types (e.g. `{ method: "*" } & { method: "GET" }`).
132
+ */
133
+ export type Context<T extends RouteSet.Any> =
134
+ & Omit<
135
+ RouteSet.Descriptor<T>,
136
+ keyof ExtractContext<RouteSet.Items<T>>
495
137
  >
496
- {
497
- const baseRoutes = isRouteSet(this)
498
- ? this.set
499
- : [] as const
500
- const baseSchema = isRouteSet(this)
501
- ? this.schema
502
- : {} as RouteSchemas.Empty
503
-
504
- const schema = Schema.isSchema(fieldsOrSchema)
505
- ? fieldsOrSchema
506
- : Schema.Struct(fieldsOrSchema as Schema.Struct.Fields)
507
-
508
- return makeSet(
509
- baseRoutes as ReadonlyArray<Route.Default>,
510
- {
511
- ...baseSchema,
512
- [key]: schema,
513
- },
514
- ) as never
515
- }
138
+ & ExtractContext<RouteSet.Items<T>>
139
+
140
+ type ExtractContext<
141
+ M extends Tuple,
142
+ > = M extends [
143
+ infer Head,
144
+ ...infer Tail extends Tuple,
145
+ ] ? (
146
+ Head extends Route<
147
+ infer D,
148
+ infer B,
149
+ any,
150
+ any,
151
+ any
152
+ > ? ShallowMerge<
153
+ & Omit<D, keyof B>
154
+ & B,
155
+ ExtractContext<Tail>
156
+ >
157
+ : ExtractContext<Tail>
158
+ )
159
+ : {}
516
160
  }
517
161
 
518
- export const schemaPathParams = makeSingleStringSchemaModifier("PathParams")
519
- export const schemaUrlParams = makeMultiStringSchemaModifier("UrlParams")
520
- export const schemaHeaders = makeMultiStringSchemaModifier("Headers")
521
- export const schemaPayload = makeUnionSchemaModifier("Payload")
522
- export const schemaSuccess = makeUnionSchemaModifier("Success")
523
- export const schemaError = makeUnionSchemaModifier("Error")
524
-
525
- const SetProto = {
526
- [RouteSetTypeId]: RouteSetTypeId,
527
-
528
- post,
529
- get,
530
- put,
531
- patch,
532
- delete: _delete,
533
- options,
534
- head,
535
-
536
- text,
537
- html,
538
- json,
539
-
540
- schemaPathParams,
541
- schemaUrlParams,
542
- schemaPayload,
543
- schemaSuccess,
544
- schemaError,
545
- schemaHeaders,
546
- } satisfies RouteSet.Proto
547
-
548
- const RouteProto = Object.assign(
549
- Object.create(SetProto),
550
- {
551
- [TypeId]: TypeId,
552
-
553
- pipe() {
554
- return Pipeable.pipeArguments(this, arguments)
555
- },
556
- } satisfies Route.Proto,
557
- )
558
-
559
- const RouteLayerProto = Object.assign(
560
- Object.create(SetProto),
561
- {
562
- [RouteLayerTypeId]: RouteLayerTypeId,
162
+ const Proto: RouteSet.Proto = {
163
+ [TypeId]: TypeId,
164
+ pipe() {
165
+ return Pipeable.pipeArguments(this, arguments)
166
+ },
167
+ *[Symbol.iterator](this: RouteSet.Any) {
168
+ yield* items(this)
563
169
  },
564
- )
565
-
566
- export function isRoute(input: unknown): input is Route {
567
- return Predicate.hasProperty(input, TypeId)
568
170
  }
569
171
 
570
172
  export function isRouteSet(
571
173
  input: unknown,
572
- ): input is RouteSet.Default {
573
- return Predicate.hasProperty(input, RouteSetTypeId)
574
- }
575
-
576
- export type JsonValue =
577
- | string
578
- | number
579
- | boolean
580
- | null
581
- | JsonValue[]
582
- | {
583
- [key: string]: JsonValue
584
- }
585
-
586
- type RouteContextDecoded = {
587
- readonly pathParams?: Record<string, any>
588
- readonly urlParams?: Record<string, any>
589
- readonly payload?: any
590
- readonly headers?: Record<string, any>
174
+ ): input is RouteSet.Any {
175
+ return Predicate.hasProperty(input, TypeId)
591
176
  }
592
177
 
593
- /**
594
- * Decode RouteSchemas to make context in media handlers easier to read:
595
- * - Converts keys from PascalCase to camelCase
596
- * - Decodes schema types to their Type representation
597
- */
598
- export type DecodeRouteSchemas<Schemas extends RouteSchemas> =
599
- & (Schemas["PathParams"] extends Schema.Struct<any> ? {
600
- pathParams: Schema.Schema.Type<Schemas["PathParams"]>
601
- }
602
- : {})
603
- & (Schemas["UrlParams"] extends Schema.Struct<any> ? {
604
- urlParams: Schema.Schema.Type<Schemas["UrlParams"]>
605
- }
606
- : {})
607
- & (Schemas["Payload"] extends Schema.Schema.Any ? {
608
- payload: Schema.Schema.Type<Schemas["Payload"]>
609
- }
610
- : {})
611
- & (Schemas["Headers"] extends Schema.Struct<any> ? {
612
- headers: Schema.Schema.Type<Schemas["Headers"]>
613
- }
614
- : {})
615
-
616
- /**
617
- * Context passed to route handler functions.
618
- *
619
- * @template {Input} Decoded schema values (pathParams, urlParams, etc.)
620
- * @template {Next} Return type of next() based on media type
621
- */
622
- export type RouteContext<
623
- Input extends RouteContextDecoded = {},
624
- Next = unknown,
625
- > =
626
- & {
627
- request: HttpServerRequest.HttpServerRequest
628
- get url(): URL
629
- slots: Record<string, string>
630
- next: <E = unknown, R = unknown>() => Effect.Effect<Next, E, R>
631
- }
632
- & Input
633
-
634
- /**
635
- * Merges two RouteSchemas types.
636
- * For PathParams, UrlParams, and Headers: merges struct fields.
637
- * For Payload, Success, and Error: creates Schema.Union.
638
- */
639
- type MergeSchemas<
640
- A extends RouteSchemas,
641
- B extends RouteSchemas,
642
- > = {
643
- readonly PathParams: [A["PathParams"], B["PathParams"]] extends [
644
- Schema.Struct<infer AFields>,
645
- Schema.Struct<infer BFields>,
646
- ] ? Schema.Struct<AFields & BFields>
647
- : A["PathParams"] extends Schema.Struct<any> ? A["PathParams"]
648
- : B["PathParams"] extends Schema.Struct<any> ? B["PathParams"]
649
- : never
650
- readonly UrlParams: [A["UrlParams"], B["UrlParams"]] extends [
651
- Schema.Struct<infer AFields>,
652
- Schema.Struct<infer BFields>,
653
- ] ? Schema.Struct<AFields & BFields>
654
- : A["UrlParams"] extends Schema.Struct<any> ? A["UrlParams"]
655
- : B["UrlParams"] extends Schema.Struct<any> ? B["UrlParams"]
656
- : never
657
- readonly Payload: [A["Payload"], B["Payload"]] extends [
658
- Schema.Schema.Any,
659
- Schema.Schema.Any,
660
- ] ? Schema.Union<[A["Payload"], B["Payload"]]>
661
- : A["Payload"] extends Schema.Schema.Any ? A["Payload"]
662
- : B["Payload"] extends Schema.Schema.Any ? B["Payload"]
663
- : never
664
- readonly Success: [A["Success"], B["Success"]] extends [
665
- Schema.Schema.Any,
666
- Schema.Schema.Any,
667
- ] ? Schema.Union<[A["Success"], B["Success"]]>
668
- : A["Success"] extends Schema.Schema.Any ? A["Success"]
669
- : B["Success"] extends Schema.Schema.Any ? B["Success"]
670
- : never
671
- readonly Error: [A["Error"], B["Error"]] extends [
672
- Schema.Schema.Any,
673
- Schema.Schema.Any,
674
- ] ? Schema.Union<[A["Error"], B["Error"]]>
675
- : A["Error"] extends Schema.Schema.Any ? A["Error"]
676
- : B["Error"] extends Schema.Schema.Any ? B["Error"]
677
- : never
678
- readonly Headers: [A["Headers"], B["Headers"]] extends [
679
- Schema.Struct<infer AFields>,
680
- Schema.Struct<infer BFields>,
681
- ] ? Schema.Struct<AFields & BFields>
682
- : A["Headers"] extends Schema.Struct<any> ? A["Headers"]
683
- : B["Headers"] extends Schema.Struct<any> ? B["Headers"]
684
- : never
178
+ export function isRoute(
179
+ input: unknown,
180
+ ): input is Route.Route {
181
+ return isRouteSet(input)
182
+ && Predicate.hasProperty(input, "handler")
685
183
  }
686
184
 
687
- /**
688
- * Runtime function to merge two RouteSchemas.
689
- * For PathParams, UrlParams, and Headers: merges struct fields.
690
- * For Payload, Success, and Error: creates Schema.Union.
691
- */
692
- function mergeSchemas<
693
- A extends RouteSchemas,
694
- B extends RouteSchemas,
185
+ export function set<
186
+ D extends RouteDescriptor.Any = {},
187
+ B = {},
188
+ I extends Route.Tuple = [],
695
189
  >(
696
- a: A,
697
- b: B,
698
- ): MergeSchemas<A, B> {
699
- const result: any = {}
700
-
701
- const structKeys: Array<keyof RouteSchemas> = [
702
- "PathParams",
703
- "UrlParams",
704
- "Headers",
705
- ]
706
-
707
- const unionKeys: Array<keyof RouteSchemas> = [
708
- "Payload",
709
- "Success",
710
- "Error",
711
- ]
712
-
713
- for (const key of structKeys) {
714
- if (a[key] && b[key]) {
715
- const aSchema = a[key]! as Schema.Struct<any>
716
- const bSchema = b[key]! as Schema.Struct<any>
717
- const mergedFields = {
718
- ...aSchema.fields,
719
- ...bSchema.fields,
720
- }
721
- result[key] = Schema.Struct(mergedFields)
722
- } else if (a[key]) {
723
- result[key] = a[key]
724
- } else if (b[key]) {
725
- result[key] = b[key]
726
- }
727
- }
728
-
729
- for (const key of unionKeys) {
730
- if (a[key] && b[key]) {
731
- result[key] = Schema.Union(a[key]!, b[key]!)
732
- } else if (a[key]) {
733
- result[key] = a[key]
734
- } else if (b[key]) {
735
- result[key] = b[key]
736
- }
737
- }
738
-
739
- return result
190
+ items: I = [] as unknown as I,
191
+ descriptor: D = {} as D,
192
+ ): RouteSet.RouteSet<D, B, I> {
193
+ return Object.assign(
194
+ Object.create(Proto),
195
+ {
196
+ [RouteItems]: items,
197
+ [RouteDescriptor]: descriptor,
198
+ },
199
+ ) as RouteSet.RouteSet<D, B, I>
740
200
  }
741
201
 
742
202
  export function make<
743
- Method extends RouteMethod = "*",
744
- Media extends RouteMedia = "*",
745
- Handler extends RouteHandler = never,
746
- Schemas extends RouteSchemas = RouteSchemas.Empty,
203
+ D extends RouteDescriptor.Any,
204
+ B,
205
+ A,
206
+ E = never,
207
+ R = never,
747
208
  >(
748
- input: Route.Data<
749
- Method,
750
- Media,
751
- Handler,
752
- Schemas
753
- >,
754
- ): Route<
755
- Method,
756
- Media,
757
- Handler,
758
- Schemas
759
- > {
760
- const route = Object.assign(
761
- Object.create(RouteProto),
209
+ handler: Route.Handler<B & D, A, E, R>,
210
+ descriptor?: D,
211
+ ): Route.Route<D, B, A, E, R> {
212
+ const items: any = []
213
+ const route: Route.Route<D, B, A, E, R> = Object.assign(
214
+ Object.create(Proto),
762
215
  {
763
- set: [],
764
- // @ts-expect-error: assigned below
765
- schemas: input.schemas,
766
- method: input.method,
767
- media: input.media,
768
- handler: input.handler,
769
- } satisfies Route.Data,
216
+ [RouteItems]: items,
217
+ [RouteDescriptor]: descriptor,
218
+ handler,
219
+ },
770
220
  )
771
221
 
772
- route.set = [
773
- route,
774
- ]
775
- route.schemas = input.schemas
222
+ items.push(route)
776
223
 
777
224
  return route
778
225
  }
779
226
 
780
- function makeSet<
781
- M extends ReadonlyArray<Route.Default>,
782
- Schemas extends RouteSchemas = RouteSchemas.Empty,
783
- >(
784
- routes: M,
785
- schema: Schemas = {} as Schemas,
786
- ): RouteSet<M, Schemas> {
787
- return Object.assign(
788
- Object.create(SetProto),
789
- {
790
- set: routes,
791
- schema,
792
- },
793
- ) as RouteSet<M, Schemas>
794
- }
795
-
796
- type HandlerInput<A, E, R> =
797
- | A
798
- | Effect.Effect<A, E, R>
799
- | ((context: RouteContext) =>
800
- | Effect.Effect<A, E, R>
801
- | Generator<YieldWrap<Effect.Effect<A, E, R>>, A, never>)
227
+ export const empty = set()
802
228
 
803
- function normalizeHandler<A, E, R>(
804
- handler: HandlerInput<A, E, R>,
805
- ): RouteHandler<A, E, R> {
806
- if (typeof handler === "function") {
807
- return (context): Effect.Effect<A, E, R> => {
808
- const result = (handler as Function)(context)
809
- if (Effect.isEffect(result)) {
810
- return result as Effect.Effect<A, E, R>
811
- }
812
- return Effect.gen(() => result) as Effect.Effect<A, E, R>
813
- }
814
- }
815
- if (Effect.isEffect(handler)) {
816
- return () => handler
817
- }
818
- return () => Effect.succeed(handler as A)
819
- }
820
-
821
- /**
822
- * Factory function that creates Route for a specific method & media.
823
- * Accepts Effect, function that returns Effect, and effectful generator.
824
- */
825
- function makeMediaFunction<
826
- Method extends HttpMethod.HttpMethod,
827
- Media extends RouteMedia,
828
- ExpectedValue,
229
+ export function describe<
230
+ D extends RouteDescriptor.Any,
829
231
  >(
830
- method: Method,
831
- media: Media,
232
+ descriptor: D,
832
233
  ) {
833
- return function<
834
- S extends Self,
835
- A extends ExpectedValue,
836
- E = never,
837
- R = never,
838
- >(
839
- this: S,
840
- handler: S extends RouteSet<infer _Routes, infer Schemas> ?
841
- | A
842
- | Effect.Effect<A, E, R>
843
- | ((
844
- context: RouteContext<DecodeRouteSchemas<Schemas>, ExpectedValue>,
845
- ) =>
846
- | Effect.Effect<A, E, R>
847
- | Generator<YieldWrap<Effect.Effect<A, E, R>>, A, never>)
848
- :
849
- | A
850
- | Effect.Effect<A, E, R>
851
- | ((context: RouteContext<{}, ExpectedValue>) =>
852
- | Effect.Effect<A, E, R>
853
- | Generator<YieldWrap<Effect.Effect<A, E, R>>, A, never>),
854
- ): S extends RouteSet<infer Routes, infer Schemas> ? RouteSet<[
855
- ...Routes,
856
- Route<
857
- Method,
858
- Media,
859
- RouteHandler<A, E, R>,
860
- Schemas
861
- >,
862
- ], Schemas>
863
- : RouteSet<[
864
- Route<
865
- Method,
866
- Media,
867
- RouteHandler<A, E, R>,
868
- RouteSchemas.Empty
869
- >,
870
- ], RouteSchemas.Empty>
871
- {
872
- const baseRoutes = isRouteSet(this)
873
- ? this.set
874
- : [] as const
875
- const baseSchema = isRouteSet(this)
876
- ? this.schema
877
- : {} as RouteSchemas.Empty
878
-
879
- return makeSet(
880
- [
881
- ...baseRoutes,
882
- make({
883
- method,
884
- media,
885
- handler: normalizeHandler(handler),
886
- schemas: baseSchema,
887
- }),
888
- ] as ReadonlyArray<Route.Default>,
889
- baseSchema,
890
- ) as never
891
- }
234
+ return set([], descriptor)
892
235
  }
893
236
 
894
- /**
895
- * Factory function that changes method in RouteSet.
896
- */
897
- function makeMethodModifier<
898
- M extends HttpMethod.HttpMethod,
899
- >(method: M) {
900
- return function<
901
- S extends Self,
902
- T extends Route.Tuple,
903
- InSchemas extends RouteSchemas,
904
- >(
905
- this: S,
906
- routes: RouteSet<T, InSchemas>,
907
- ): S extends RouteSet<infer B, infer BaseSchemas>
908
- // append to existing RouteSet
909
- ? RouteSet<
910
- [
911
- ...B,
912
- ...{
913
- [K in keyof T]: T[K] extends Route<
914
- infer _,
915
- infer Media,
916
- infer H,
917
- infer RouteSchemas
918
- > ? Route<
919
- M,
920
- Media,
921
- H,
922
- MergeSchemas<BaseSchemas, RouteSchemas>
923
- >
924
- : T[K]
925
- },
926
- ],
927
- BaseSchemas
928
- >
929
- // otherwise create new RouteSet
930
- : RouteSet<
931
- {
932
- [K in keyof T]: T[K] extends Route<
933
- infer _,
934
- infer Media,
935
- infer H,
936
- infer RouteSchemas
937
- > ? Route<
938
- M,
939
- Media,
940
- H,
941
- RouteSchemas
942
- >
943
- : T[K]
944
- },
945
- InSchemas
946
- >
947
- {
948
- const baseRoutes = isRouteSet(this)
949
- ? this.set
950
- : [] as const
951
- const baseSchema = isRouteSet(this)
952
- ? this.schema
953
- : {} as RouteSchemas.Empty
954
-
955
- return makeSet(
956
- [
957
- ...baseRoutes,
958
- ...routes.set.map(route => {
959
- return make({
960
- ...route,
961
- method,
962
- schemas: mergeSchemas(baseSchema, route.schemas),
963
- })
964
- }),
965
- ] as ReadonlyArray<Route.Default>,
966
- baseSchema,
967
- ) as never
968
- }
969
- }
970
-
971
- export type GenericJsxObject = {
972
- type: any
973
- props: any
974
- }
975
-
976
- export function isGenericJsxObject(value: unknown): value is GenericJsxObject {
977
- return typeof value === "object"
978
- && value !== null
979
- && "type" in value
980
- && "props" in value
237
+ export function items<
238
+ T extends RouteSet.Data<any, any, any>,
239
+ >(
240
+ self: T,
241
+ ): RouteSet.Items<T> {
242
+ return self[RouteItems]
981
243
  }
982
244
 
983
- /**
984
- * Create HTTP middleware spec for Route.layer().
985
- * Multiple middleware can be composed by passing multiple Route.http() to Route.layer().
986
- *
987
- * @example
988
- * Route.layer(
989
- * Route.http(middleware1),
990
- * Route.http(middleware2),
991
- * Route.http(middleware3)
992
- * )
993
- */
994
- export function http(
995
- middleware: HttpMiddlewareFunction,
996
- ): RouteMiddleware {
997
- return {
998
- _tag: "RouteMiddleware",
999
- middleware,
1000
- }
245
+ export function descriptor<
246
+ T extends RouteSet.Data<any, any, any>,
247
+ >(
248
+ self: T,
249
+ ): T[typeof RouteDescriptor] {
250
+ return self[RouteDescriptor]
1001
251
  }
1002
252
 
1003
- /**
1004
- * Create a RouteLayer from routes and middleware.
1005
- *
1006
- * Accepts:
1007
- * - Route.http(middleware) - HTTP middleware to apply to all child routes
1008
- * - Route.html/text/json/etc handlers - Wrapper routes that receive props.children
1009
- * - Other RouteSets - Routes to include in the layer
1010
- *
1011
- * Multiple middleware are composed in order - first middleware wraps second, etc.
1012
- * Routes in the layer act as wrappers for child routes with matching method + media type.
1013
- *
1014
- * @example
1015
- * Route.layer(
1016
- * Route.http(loggingMiddleware),
1017
- * Route.http(authMiddleware),
1018
- * Route.html(function*(props) {
1019
- * return <html><body>{props.children}</body></html>
1020
- * })
1021
- * )
1022
- */
1023
- export function layer(
1024
- ...items: Array<RouteMiddleware | RouteSet.Default>
1025
- ): RouteLayer {
1026
- const routeMiddleware: RouteMiddleware[] = []
1027
- const routeSets: RouteSet.Default[] = []
253
+ export type ExtractBindings<
254
+ M extends Route.Tuple,
255
+ > = M extends [
256
+ infer Head,
257
+ ...infer Tail extends Route.Tuple,
258
+ ] ? (
259
+ Head extends Route.Route<
260
+ any,
261
+ infer B,
262
+ any,
263
+ any,
264
+ any
265
+ > ? ShallowMerge<B, ExtractBindings<Tail>>
266
+ : ExtractBindings<Tail>
267
+ )
268
+ : {}
1028
269
 
1029
- for (const item of items) {
1030
- if ("_tag" in item && item._tag === "RouteMiddleware") {
1031
- routeMiddleware.push(item)
1032
- } else if (isRouteSet(item)) {
1033
- routeSets.push(item)
1034
- }
270
+ // Shallow merge two object types.
271
+ // For overlapping keys, intersect the values.
272
+ type ShallowMerge<A, B> =
273
+ & Omit<A, keyof B>
274
+ & {
275
+ [K in keyof B]: K extends keyof A ? A[K] & B[K] : B[K]
1035
276
  }
1036
277
 
1037
- const layerRoutes: Route.Default[] = []
278
+ export type ExtractContext<
279
+ Items extends Route.Tuple,
280
+ Descriptor extends RouteDescriptor.Any,
281
+ > = ExtractBindings<Items> & Descriptor
1038
282
 
1039
- for (const routeSet of routeSets) {
1040
- layerRoutes.push(...routeSet.set)
1041
- }
1042
-
1043
- const middlewareFunctions = routeMiddleware.map((spec) => spec.middleware)
1044
- const httpMiddleware = middlewareFunctions.length === 0
1045
- ? undefined
1046
- : middlewareFunctions.length === 1
1047
- ? middlewareFunctions[0]
1048
- : (app: any) => middlewareFunctions.reduceRight((acc, mw) => mw(acc), app)
283
+ export * from "./RouteHook.ts"
284
+ export * from "./RouteSchema.ts"
1049
285
 
1050
- return Object.assign(
1051
- Object.create(RouteLayerProto),
1052
- {
1053
- set: layerRoutes,
1054
- schema: {},
1055
- httpMiddleware,
1056
- },
286
+ export {
287
+ add,
288
+ del,
289
+ get,
290
+ head,
291
+ options,
292
+ patch,
293
+ post,
294
+ put,
295
+ use,
296
+ } from "./RouteMount.ts"
297
+
298
+ export const text = RouteBody.build<string, "text">({
299
+ format: "text",
300
+ })
301
+
302
+ export const html = RouteBody.build<string, "html">({
303
+ format: "html",
304
+ })
305
+
306
+ export const json = RouteBody.build<Values.Json, "json">({
307
+ format: "json",
308
+ })
309
+
310
+ export const bytes = RouteBody.build<Uint8Array, "bytes">({
311
+ format: "bytes",
312
+ })
313
+
314
+ export class Routes extends Context.Tag("effect-start/Routes")<
315
+ Routes,
316
+ RouteTree.RouteTree
317
+ >() {}
318
+
319
+ export function layer(routes: RouteTree.RouteMap | RouteTree.RouteTree) {
320
+ return Layer.sync(
321
+ Routes,
322
+ () =>
323
+ RouteTree.isRouteTree(routes)
324
+ ? routes
325
+ : RouteTree.make(routes),
1057
326
  )
1058
327
  }
1059
328
 
1060
- /**
1061
- * Extract method union from a RouteSet's routes
1062
- */
1063
-
1064
- type ExtractMethods<T extends ReadonlyArray<Route.Default>> =
1065
- T[number]["method"]
1066
-
1067
- /**
1068
- * Extract media union from a RouteSet's routes
1069
- */
1070
- type ExtractMedia<T extends ReadonlyArray<Route.Default>> = T[number]["media"]
1071
-
1072
- /**
1073
- * Merge two RouteSets into one with content negotiation.
1074
- * Properly infers union types for method/media and merges schemas.
1075
- */
1076
- export function merge<
1077
- RoutesA extends ReadonlyArray<Route.Default>,
1078
- SchemasA extends RouteSchemas,
1079
- RoutesB extends ReadonlyArray<Route.Default>,
1080
- SchemasB extends RouteSchemas,
1081
- >(
1082
- self: RouteSet<RoutesA, SchemasA>,
1083
- other: RouteSet<RoutesB, SchemasB>,
1084
- ): RouteSet<
1085
- [
1086
- Route<
1087
- ExtractMethods<RoutesA> | ExtractMethods<RoutesB>,
1088
- ExtractMedia<RoutesA> | ExtractMedia<RoutesB>,
1089
- RouteHandler<HttpServerResponse.HttpServerResponse, any, never>,
1090
- MergeSchemas<SchemasA, SchemasB>
1091
- >,
1092
- ],
1093
- MergeSchemas<SchemasA, SchemasB>
1094
- > {
1095
- const allRoutes = [...self.set, ...other.set]
1096
- const mergedSchemas = mergeSchemas(self.schema, other.schema)
1097
-
1098
- const handler: RouteHandler<HttpServerResponse.HttpServerResponse> = (
1099
- context,
1100
- ) =>
1101
- Effect.gen(function*() {
1102
- const accept = context.request.headers.accept ?? ""
1103
-
1104
- if (accept.includes("application/json")) {
1105
- const jsonRoute = allRoutes.find((r) => r.media === "application/json")
1106
- if (jsonRoute) {
1107
- return yield* RouteRender.render(jsonRoute, context)
1108
- }
1109
- }
1110
-
1111
- if (accept.includes("text/plain")) {
1112
- const textRoute = allRoutes.find((r) => r.media === "text/plain")
1113
- if (textRoute) {
1114
- return yield* RouteRender.render(textRoute, context)
1115
- }
1116
- }
1117
-
1118
- if (
1119
- accept.includes("text/html") || accept.includes("*/*") || !accept
1120
- ) {
1121
- const htmlRoute = allRoutes.find((r) => r.media === "text/html")
1122
- if (htmlRoute) {
1123
- return yield* RouteRender.render(htmlRoute, context)
1124
- }
1125
- }
1126
-
1127
- const firstRoute = allRoutes[0]
1128
- if (firstRoute) {
1129
- return yield* RouteRender.render(firstRoute, context)
1130
- }
1131
-
1132
- return HttpServerResponse.empty({ status: 406 })
1133
- })
1134
-
1135
- return makeSet(
1136
- [
1137
- make({
1138
- method: allRoutes[0]?.method ?? "*",
1139
- media: allRoutes[0]?.media ?? "*",
1140
- handler,
1141
- schemas: mergedSchemas,
1142
- }),
1143
- ] as any,
1144
- mergedSchemas,
1145
- ) as any
1146
- }
329
+ export {
330
+ make as tree,
331
+ } from "./RouteTree.ts"