effect-start 0.9.0 → 0.11.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 (54) hide show
  1. package/package.json +15 -14
  2. package/src/BundleHttp.test.ts +1 -1
  3. package/src/Commander.test.ts +15 -15
  4. package/src/Commander.ts +58 -88
  5. package/src/EncryptedCookies.test.ts +4 -4
  6. package/src/FileHttpRouter.test.ts +85 -16
  7. package/src/FileHttpRouter.ts +119 -32
  8. package/src/FileRouter.ts +62 -166
  9. package/src/FileRouterCodegen.test.ts +252 -66
  10. package/src/FileRouterCodegen.ts +13 -56
  11. package/src/FileRouterPattern.test.ts +116 -0
  12. package/src/FileRouterPattern.ts +59 -0
  13. package/src/FileRouter_path.test.ts +63 -102
  14. package/src/FileSystemExtra.test.ts +226 -0
  15. package/src/FileSystemExtra.ts +24 -60
  16. package/src/HttpAppExtra.test.ts +84 -0
  17. package/src/HttpAppExtra.ts +399 -47
  18. package/src/HttpUtils.test.ts +68 -0
  19. package/src/HttpUtils.ts +15 -0
  20. package/src/HyperHtml.ts +24 -5
  21. package/src/JsModule.test.ts +1 -1
  22. package/src/NodeFileSystem.ts +764 -0
  23. package/src/Random.ts +59 -0
  24. package/src/Route.test.ts +515 -18
  25. package/src/Route.ts +321 -166
  26. package/src/RouteRender.ts +40 -0
  27. package/src/Router.test.ts +416 -0
  28. package/src/Router.ts +288 -31
  29. package/src/RouterPattern.test.ts +655 -0
  30. package/src/RouterPattern.ts +416 -0
  31. package/src/Start.ts +14 -52
  32. package/src/TestHttpClient.test.ts +29 -0
  33. package/src/TestHttpClient.ts +122 -73
  34. package/src/assets.d.ts +39 -0
  35. package/src/bun/BunBundle.test.ts +0 -3
  36. package/src/bun/BunHttpServer.test.ts +74 -0
  37. package/src/bun/BunHttpServer.ts +259 -0
  38. package/src/bun/BunHttpServer_web.ts +384 -0
  39. package/src/bun/BunRoute.test.ts +514 -0
  40. package/src/bun/BunRoute.ts +427 -0
  41. package/src/bun/BunRoute_bundles.test.ts +218 -0
  42. package/src/bun/BunRuntime.ts +33 -0
  43. package/src/bun/BunTailwindPlugin.test.ts +1 -1
  44. package/src/bun/_empty.html +1 -0
  45. package/src/bun/index.ts +2 -1
  46. package/src/index.ts +14 -14
  47. package/src/middlewares/BasicAuthMiddleware.test.ts +74 -0
  48. package/src/middlewares/BasicAuthMiddleware.ts +36 -0
  49. package/src/testing.ts +12 -3
  50. package/src/Datastar.test.ts +0 -267
  51. package/src/Datastar.ts +0 -68
  52. package/src/bun/BunFullstackServer.ts +0 -45
  53. package/src/bun/BunFullstackServer_httpServer.ts +0 -541
  54. package/src/jsx-datastar.d.ts +0 -63
package/src/Route.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import * as HttpMethod from "@effect/platform/HttpMethod"
2
+ import * as HttpMiddleware from "@effect/platform/HttpMiddleware"
2
3
  import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
3
- import * as HttpServerRespondable from "@effect/platform/HttpServerRespondable"
4
4
  import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
5
5
  import * as Effect from "effect/Effect"
6
6
  import * as Pipeable from "effect/Pipeable"
7
7
  import * as Predicate from "effect/Predicate"
8
8
  import * as Schema from "effect/Schema"
9
9
  import type { YieldWrap } from "effect/Utils"
10
- import * as HyperHtml from "./HyperHtml.ts"
10
+ import * as RouteRender from "./RouteRender.ts"
11
11
 
12
12
  export {
13
13
  pipe,
@@ -63,12 +63,14 @@ type Self =
63
63
 
64
64
  const TypeId: unique symbol = Symbol.for("effect-start/Route")
65
65
  const RouteSetTypeId: unique symbol = Symbol.for("effect-start/RouteSet")
66
+ const RouteLayerTypeId: unique symbol = Symbol.for("effect-start/RouteLayer")
66
67
 
67
68
  export type RouteMethod =
68
69
  | "*"
69
70
  | HttpMethod.HttpMethod
70
71
 
71
- export type RoutePath = `/${string}`
72
+ // TODO: This should be a RouterPattern and moved to its file?
73
+ export type RoutePattern = `/${string}`
72
74
 
73
75
  /**
74
76
  * Route media type used for content negotiation.
@@ -76,55 +78,24 @@ export type RoutePath = `/${string}`
76
78
  * for the same path & method, depending on the `Accept` header
77
79
  * of the request.
78
80
  */
79
- type RouteMedia =
81
+ export type RouteMedia =
80
82
  | "*"
81
83
  | "text/plain"
82
84
  | "text/html"
83
85
  | "application/json"
84
86
 
87
+ /**
88
+ * A handler function that produces a raw value.
89
+ * The value will be rendered to an HttpServerResponse by RouteRender
90
+ * based on the route's media type.
91
+ *
92
+ * Receives RouteContext which includes an optional next() for layers.
93
+ */
85
94
  export type RouteHandler<
86
95
  A = unknown,
87
96
  E = any,
88
97
  R = any,
89
- > =
90
- /**
91
- * A handler that contains raw value.
92
- * Can be consumed from other handlers to build more complex responses.
93
- * For example, a Route can render markdown for API/AI consumption
94
- * and another Route can wrap it in HTML for browsers.
95
- */
96
- | RouteHandler.Value<A, E, R>
97
- /**
98
- * A handler returns `HttpServerResponse`.
99
- * Should not be consumed with caution: if body is a stream,
100
- * consuming it in another handler may break the stream.
101
- */
102
- | RouteHandler.Encoded<E, R>
103
-
104
- export namespace RouteHandler {
105
- export type Value<
106
- A = unknown,
107
- E = any,
108
- R = any,
109
- > = Effect.Effect<
110
- {
111
- [HttpServerRespondable.symbol]: () => Effect.Effect<
112
- HttpServerResponse.HttpServerResponse,
113
- E,
114
- R
115
- >
116
- raw: A
117
- },
118
- E,
119
- R
120
- >
121
-
122
- export type Encoded<E = any, R = any> = Effect.Effect<
123
- HttpServerResponse.HttpServerResponse,
124
- E,
125
- R
126
- >
127
- }
98
+ > = (context: RouteContext) => Effect.Effect<A, E, R>
128
99
 
129
100
  /**
130
101
  * Helper type for a value that can be a single item or an array.
@@ -213,7 +184,7 @@ type RouteBuilder = {
213
184
  get: typeof get
214
185
  put: typeof put
215
186
  patch: typeof patch
216
- del: typeof del
187
+ delete: typeof _delete
217
188
  options: typeof options
218
189
  head: typeof head
219
190
 
@@ -262,36 +233,84 @@ export namespace RouteSet {
262
233
  & RouteBuilder
263
234
  }
264
235
 
236
+ /**
237
+ * Type for HTTP middleware function
238
+ */
239
+ export type HttpMiddlewareFunction = ReturnType<typeof HttpMiddleware.make>
240
+
241
+ /**
242
+ * Marker type for route middleware specification.
243
+ * Used to distinguish middleware from routes in Route.layer() arguments.
244
+ */
245
+ export interface RouteMiddleware {
246
+ readonly _tag: "RouteMiddleware"
247
+ readonly middleware: HttpMiddlewareFunction
248
+ }
249
+
250
+ export type RouteLayer<
251
+ M extends ReadonlyArray<Route.Default> = ReadonlyArray<Route.Default>,
252
+ Schemas extends RouteSchemas = RouteSchemas.Empty,
253
+ > =
254
+ & Pipeable.Pipeable
255
+ & {
256
+ [RouteLayerTypeId]: typeof RouteLayerTypeId
257
+ [RouteSetTypeId]: typeof RouteSetTypeId
258
+ set: M
259
+ schema: Schemas
260
+ httpMiddleware?: HttpMiddlewareFunction
261
+ }
262
+ & RouteBuilder
263
+
264
+ export const isRouteLayer = (u: unknown): u is RouteLayer =>
265
+ Predicate.hasProperty(u, RouteLayerTypeId)
266
+
267
+ /**
268
+ * Check if two routes match based on method and media type.
269
+ * Returns true if both method and media type match, accounting for wildcards.
270
+ */
271
+ export function matches(
272
+ a: Route.Default,
273
+ b: Route.Default,
274
+ ): boolean {
275
+ const methodMatches = a.method === "*"
276
+ || b.method === "*"
277
+ || a.method === b.method
278
+
279
+ const mediaMatches = a.media === "*"
280
+ || b.media === "*"
281
+ || a.media === b.media
282
+
283
+ return methodMatches && mediaMatches
284
+ }
285
+
265
286
  export const post = makeMethodModifier("POST")
266
287
  export const get = makeMethodModifier("GET")
267
288
  export const put = makeMethodModifier("PUT")
268
289
  export const patch = makeMethodModifier("PATCH")
269
- export const del = makeMethodModifier("DELETE")
270
290
  export const options = makeMethodModifier("OPTIONS")
271
291
  export const head = makeMethodModifier("HEAD")
292
+ const _delete = makeMethodModifier("DELETE")
293
+ export {
294
+ _delete as delete,
295
+ }
272
296
 
273
- export const text = makeMediaFunction(
297
+ export const text = makeMediaFunction<"GET", "text/plain", string>(
274
298
  "GET",
275
299
  "text/plain",
276
- makeValueHandler<string>(HttpServerResponse.text),
277
300
  )
278
301
 
279
- export const html = makeMediaFunction(
302
+ export const html = makeMediaFunction<
303
+ "GET",
304
+ "text/html",
305
+ string | GenericJsxObject
306
+ >(
280
307
  "GET",
281
308
  "text/html",
282
- makeValueHandler<string | JsxObject>((raw) => {
283
- // Check if it's a JSX element (has type and props properties)
284
- if (isJsxObject(raw)) {
285
- return HttpServerResponse.html(HyperHtml.renderToString(raw))
286
- }
287
- return HttpServerResponse.html(raw as string)
288
- }),
289
309
  )
290
310
 
291
- export const json = makeMediaFunction(
311
+ export const json = makeMediaFunction<"GET", "application/json", JsonValue>(
292
312
  "GET",
293
313
  "application/json",
294
- makeValueHandler<JsonValue>((raw) => HttpServerResponse.unsafeJson(raw)),
295
314
  )
296
315
 
297
316
  /**
@@ -379,7 +398,7 @@ function makeSingleStringSchemaModifier<
379
398
  {
380
399
  const baseRoutes = isRouteSet(this)
381
400
  ? this.set
382
- : []
401
+ : [] as const
383
402
  const baseSchema = isRouteSet(this)
384
403
  ? this.schema
385
404
  : {} as RouteSchemas.Empty
@@ -389,12 +408,12 @@ function makeSingleStringSchemaModifier<
389
408
  : Schema.Struct(fieldsOrSchema as Schema.Struct.Fields)
390
409
 
391
410
  return makeSet(
392
- baseRoutes as any,
411
+ baseRoutes as ReadonlyArray<Route.Default>,
393
412
  {
394
413
  ...baseSchema,
395
414
  [key]: schema,
396
- } as any,
397
- ) as any
415
+ },
416
+ ) as never
398
417
  }
399
418
  }
400
419
 
@@ -430,7 +449,7 @@ function makeMultiStringSchemaModifier<
430
449
  {
431
450
  const baseRoutes = isRouteSet(this)
432
451
  ? this.set
433
- : []
452
+ : [] as const
434
453
  const baseSchema = isRouteSet(this)
435
454
  ? this.schema
436
455
  : {} as RouteSchemas.Empty
@@ -440,12 +459,12 @@ function makeMultiStringSchemaModifier<
440
459
  : Schema.Struct(fieldsOrSchema as Schema.Struct.Fields)
441
460
 
442
461
  return makeSet(
443
- baseRoutes as any,
462
+ baseRoutes as ReadonlyArray<Route.Default>,
444
463
  {
445
464
  ...baseSchema,
446
465
  [key]: schema,
447
- } as any,
448
- ) as any
466
+ },
467
+ ) as never
449
468
  }
450
469
  }
451
470
 
@@ -478,7 +497,7 @@ function makeUnionSchemaModifier<
478
497
  {
479
498
  const baseRoutes = isRouteSet(this)
480
499
  ? this.set
481
- : []
500
+ : [] as const
482
501
  const baseSchema = isRouteSet(this)
483
502
  ? this.schema
484
503
  : {} as RouteSchemas.Empty
@@ -488,12 +507,12 @@ function makeUnionSchemaModifier<
488
507
  : Schema.Struct(fieldsOrSchema as Schema.Struct.Fields)
489
508
 
490
509
  return makeSet(
491
- baseRoutes as any,
510
+ baseRoutes as ReadonlyArray<Route.Default>,
492
511
  {
493
512
  ...baseSchema,
494
513
  [key]: schema,
495
- } as any,
496
- ) as any
514
+ },
515
+ ) as never
497
516
  }
498
517
  }
499
518
 
@@ -511,7 +530,7 @@ const SetProto = {
511
530
  get,
512
531
  put,
513
532
  patch,
514
- del,
533
+ delete: _delete,
515
534
  options,
516
535
  head,
517
536
 
@@ -538,6 +557,13 @@ const RouteProto = Object.assign(
538
557
  } satisfies Route.Proto,
539
558
  )
540
559
 
560
+ const RouteLayerProto = Object.assign(
561
+ Object.create(SetProto),
562
+ {
563
+ [RouteLayerTypeId]: RouteLayerTypeId,
564
+ },
565
+ )
566
+
541
567
  export function isRoute(input: unknown): input is Route {
542
568
  return Predicate.hasProperty(input, TypeId)
543
569
  }
@@ -558,24 +584,6 @@ export type JsonValue =
558
584
  [key: string]: JsonValue
559
585
  }
560
586
 
561
- /**
562
- * Constructs a URL from HttpServerRequest.
563
- * Handles relative URLs by using headers to determine the base URL.
564
- */
565
- function makeUrlFromRequest(
566
- request: HttpServerRequest.HttpServerRequest,
567
- ): URL {
568
- const origin = request.headers.origin
569
- ?? request.headers.host
570
- ?? "http://localhost"
571
- const protocol = request.headers["x-forwarded-proto"] ?? "http"
572
- const host = request.headers.host ?? "localhost"
573
- const base = origin.startsWith("http")
574
- ? origin
575
- : `${protocol}://${host}`
576
- return new URL(request.url, base)
577
- }
578
-
579
587
  type RouteContextDecoded = {
580
588
  readonly pathParams?: Record<string, any>
581
589
  readonly urlParams?: Record<string, any>
@@ -607,20 +615,22 @@ export type DecodeRouteSchemas<Schemas extends RouteSchemas> =
607
615
  : {})
608
616
 
609
617
  /**
610
- * Context passed to route handler generator functions.
618
+ * Context passed to route handler functions.
619
+ *
620
+ * @template {Input} Decoded schema values (pathParams, urlParams, etc.)
621
+ * @template {Next} Return type of next() based on media type
611
622
  */
612
623
  export type RouteContext<
613
624
  Input extends RouteContextDecoded = {},
614
- > = {
615
- request: HttpServerRequest.HttpServerRequest
616
- get url(): URL
617
- } & Input
618
-
619
- /**
620
- * Extracts fields from a Schema.Struct or returns never if not a struct.
621
- */
622
- type ExtractStructFields<S> = S extends Schema.Struct<infer Fields> ? Fields
623
- : never
625
+ Next = unknown,
626
+ > =
627
+ & {
628
+ request: HttpServerRequest.HttpServerRequest
629
+ get url(): URL
630
+ slots: Record<string, string>
631
+ next: <E = unknown, R = unknown>() => Effect.Effect<Next, E, R>
632
+ }
633
+ & Input
624
634
 
625
635
  /**
626
636
  * Merges two RouteSchemas types.
@@ -730,7 +740,7 @@ function mergeSchemas<
730
740
  return result
731
741
  }
732
742
 
733
- function make<
743
+ export function make<
734
744
  Method extends RouteMethod = "*",
735
745
  Media extends RouteMedia = "*",
736
746
  Handler extends RouteHandler = never,
@@ -784,40 +794,62 @@ function makeSet<
784
794
  ) as RouteSet<M, Schemas>
785
795
  }
786
796
 
797
+ type HandlerInput<A, E, R> =
798
+ | A
799
+ | Effect.Effect<A, E, R>
800
+ | ((context: RouteContext) =>
801
+ | Effect.Effect<A, E, R>
802
+ | Generator<YieldWrap<Effect.Effect<A, E, R>>, A, never>)
803
+
804
+ function normalizeHandler<A, E, R>(
805
+ handler: HandlerInput<A, E, R>,
806
+ ): RouteHandler<A, E, R> {
807
+ if (typeof handler === "function") {
808
+ return (context): Effect.Effect<A, E, R> => {
809
+ const result = (handler as Function)(context)
810
+ if (Effect.isEffect(result)) {
811
+ return result as Effect.Effect<A, E, R>
812
+ }
813
+ return Effect.gen(() => result) as Effect.Effect<A, E, R>
814
+ }
815
+ }
816
+ if (Effect.isEffect(handler)) {
817
+ return () => handler
818
+ }
819
+ return () => Effect.succeed(handler as A)
820
+ }
821
+
787
822
  /**
788
823
  * Factory function that creates Route for a specific method & media.
789
- * Supports both Effect values and generator functions that receive context.
824
+ * Accepts Effect, function that returns Effect, and effectful generator.
790
825
  */
791
826
  function makeMediaFunction<
792
827
  Method extends HttpMethod.HttpMethod,
793
828
  Media extends RouteMedia,
794
- HandlerFn extends (
795
- handler: any,
796
- ) => any,
829
+ ExpectedValue,
797
830
  >(
798
831
  method: Method,
799
832
  media: Media,
800
- handlerFn: HandlerFn,
801
833
  ) {
802
834
  return function<
803
835
  S extends Self,
804
- A,
836
+ A extends ExpectedValue,
805
837
  E = never,
806
838
  R = never,
807
839
  >(
808
840
  this: S,
809
841
  handler: S extends RouteSet<infer _Routes, infer Schemas> ?
842
+ | A
810
843
  | Effect.Effect<A, E, R>
811
844
  | ((
812
- context: RouteContext<DecodeRouteSchemas<Schemas>>,
845
+ context: RouteContext<DecodeRouteSchemas<Schemas>, ExpectedValue>,
813
846
  ) =>
814
847
  | Effect.Effect<A, E, R>
815
848
  | Generator<YieldWrap<Effect.Effect<A, E, R>>, A, never>)
816
849
  :
850
+ | A
817
851
  | Effect.Effect<A, E, R>
818
- | ((
819
- context: RouteContext<{}>,
820
- ) =>
852
+ | ((context: RouteContext<{}, ExpectedValue>) =>
821
853
  | Effect.Effect<A, E, R>
822
854
  | Generator<YieldWrap<Effect.Effect<A, E, R>>, A, never>),
823
855
  ): S extends RouteSet<infer Routes, infer Schemas> ? RouteSet<[
@@ -825,7 +857,7 @@ function makeMediaFunction<
825
857
  Route<
826
858
  Method,
827
859
  Media,
828
- ReturnType<HandlerFn>,
860
+ RouteHandler<A, E, R>,
829
861
  Schemas
830
862
  >,
831
863
  ], Schemas>
@@ -833,32 +865,14 @@ function makeMediaFunction<
833
865
  Route<
834
866
  Method,
835
867
  Media,
836
- ReturnType<HandlerFn>,
868
+ RouteHandler<A, E, R>,
837
869
  RouteSchemas.Empty
838
870
  >,
839
871
  ], RouteSchemas.Empty>
840
872
  {
841
- const effect = typeof handler === "function"
842
- ? Effect.gen(function*() {
843
- const request = yield* HttpServerRequest.HttpServerRequest
844
- const context: RouteContext<any> = {
845
- request,
846
- get url() {
847
- return makeUrlFromRequest(request)
848
- },
849
- }
850
- const result = handler(context)
851
- return yield* (typeof result === "object"
852
- && result !== null
853
- && Symbol.iterator in result
854
- ? Effect.gen(() => result as any)
855
- : result as Effect.Effect<A, E, R>)
856
- })
857
- : handler
858
-
859
873
  const baseRoutes = isRouteSet(this)
860
874
  ? this.set
861
- : []
875
+ : [] as const
862
876
  const baseSchema = isRouteSet(this)
863
877
  ? this.schema
864
878
  : {} as RouteSchemas.Empty
@@ -869,36 +883,12 @@ function makeMediaFunction<
869
883
  make({
870
884
  method,
871
885
  media,
872
- handler: handlerFn(effect as any) as any,
873
- schemas: baseSchema as any,
886
+ handler: normalizeHandler(handler),
887
+ schemas: baseSchema,
874
888
  }),
875
- ] as any,
876
- baseSchema as any,
877
- ) as any
878
- }
879
- }
880
-
881
- /**
882
- * Factory to create RouteHandler.Value.
883
- * Useful for structural handlers like JSON
884
- * or content that can be embedded in other formats,
885
- * like text or HTML.
886
- */
887
- function makeValueHandler<ExpectedRaw = string>(
888
- responseFn: (raw: ExpectedRaw) => HttpServerResponse.HttpServerResponse,
889
- ) {
890
- return <A extends ExpectedRaw, E = never, R = never>(
891
- handler: Effect.Effect<A, E, R>,
892
- ): RouteHandler.Value<A, E, R> => {
893
- return Effect.gen(function*() {
894
- const raw = yield* handler
895
-
896
- return {
897
- [HttpServerRespondable.symbol]: () =>
898
- Effect.succeed(responseFn(raw as ExpectedRaw)),
899
- raw,
900
- }
901
- }) as RouteHandler.Value<A, E, R>
889
+ ] as ReadonlyArray<Route.Default>,
890
+ baseSchema,
891
+ ) as never
902
892
  }
903
893
  }
904
894
 
@@ -970,23 +960,188 @@ function makeMethodModifier<
970
960
  return make({
971
961
  ...route,
972
962
  method,
973
- schemas: mergeSchemas(baseSchema, route.schemas) as any,
963
+ schemas: mergeSchemas(baseSchema, route.schemas),
974
964
  })
975
965
  }),
976
- ] as any,
977
- baseSchema as any,
978
- ) as any
966
+ ] as ReadonlyArray<Route.Default>,
967
+ baseSchema,
968
+ ) as never
979
969
  }
980
970
  }
981
971
 
982
- type JsxObject = {
972
+ export type GenericJsxObject = {
983
973
  type: any
984
974
  props: any
985
975
  }
986
976
 
987
- function isJsxObject(value: any) {
977
+ export function isGenericJsxObject(value: unknown): value is GenericJsxObject {
988
978
  return typeof value === "object"
989
979
  && value !== null
990
980
  && "type" in value
991
981
  && "props" in value
992
982
  }
983
+
984
+ /**
985
+ * Create HTTP middleware spec for Route.layer().
986
+ * Multiple middleware can be composed by passing multiple Route.http() to Route.layer().
987
+ *
988
+ * @example
989
+ * Route.layer(
990
+ * Route.http(middleware1),
991
+ * Route.http(middleware2),
992
+ * Route.http(middleware3)
993
+ * )
994
+ */
995
+ export function http(
996
+ middleware: HttpMiddlewareFunction,
997
+ ): RouteMiddleware {
998
+ return {
999
+ _tag: "RouteMiddleware",
1000
+ middleware,
1001
+ }
1002
+ }
1003
+
1004
+ /**
1005
+ * Create a RouteLayer from routes and middleware.
1006
+ *
1007
+ * Accepts:
1008
+ * - Route.http(middleware) - HTTP middleware to apply to all child routes
1009
+ * - Route.html/text/json/etc handlers - Wrapper routes that receive props.children
1010
+ * - Other RouteSets - Routes to include in the layer
1011
+ *
1012
+ * Multiple middleware are composed in order - first middleware wraps second, etc.
1013
+ * Routes in the layer act as wrappers for child routes with matching method + media type.
1014
+ *
1015
+ * @example
1016
+ * Route.layer(
1017
+ * Route.http(loggingMiddleware),
1018
+ * Route.http(authMiddleware),
1019
+ * Route.html(function*(props) {
1020
+ * return <html><body>{props.children}</body></html>
1021
+ * })
1022
+ * )
1023
+ */
1024
+ export function layer(
1025
+ ...items: Array<RouteMiddleware | RouteSet.Default>
1026
+ ): RouteLayer {
1027
+ const routeMiddleware: RouteMiddleware[] = []
1028
+ const routeSets: RouteSet.Default[] = []
1029
+
1030
+ for (const item of items) {
1031
+ if ("_tag" in item && item._tag === "RouteMiddleware") {
1032
+ routeMiddleware.push(item)
1033
+ } else if (isRouteSet(item)) {
1034
+ routeSets.push(item)
1035
+ }
1036
+ }
1037
+
1038
+ const layerRoutes: Route.Default[] = []
1039
+
1040
+ for (const routeSet of routeSets) {
1041
+ layerRoutes.push(...routeSet.set)
1042
+ }
1043
+
1044
+ const middlewareFunctions = routeMiddleware.map((spec) => spec.middleware)
1045
+ const httpMiddleware = middlewareFunctions.length === 0
1046
+ ? undefined
1047
+ : middlewareFunctions.length === 1
1048
+ ? middlewareFunctions[0]
1049
+ : (app: any) => middlewareFunctions.reduceRight((acc, mw) => mw(acc), app)
1050
+
1051
+ return Object.assign(
1052
+ Object.create(RouteLayerProto),
1053
+ {
1054
+ set: layerRoutes,
1055
+ schema: {},
1056
+ httpMiddleware,
1057
+ },
1058
+ )
1059
+ }
1060
+
1061
+ /**
1062
+ * Extract method union from a RouteSet's routes
1063
+ */
1064
+
1065
+ type ExtractMethods<T extends ReadonlyArray<Route.Default>> =
1066
+ T[number]["method"]
1067
+
1068
+ /**
1069
+ * Extract media union from a RouteSet's routes
1070
+ */
1071
+ type ExtractMedia<T extends ReadonlyArray<Route.Default>> = T[number]["media"]
1072
+
1073
+ /**
1074
+ * Merge two RouteSets into one with content negotiation.
1075
+ * Properly infers union types for method/media and merges schemas.
1076
+ */
1077
+ export function merge<
1078
+ RoutesA extends ReadonlyArray<Route.Default>,
1079
+ SchemasA extends RouteSchemas,
1080
+ RoutesB extends ReadonlyArray<Route.Default>,
1081
+ SchemasB extends RouteSchemas,
1082
+ >(
1083
+ self: RouteSet<RoutesA, SchemasA>,
1084
+ other: RouteSet<RoutesB, SchemasB>,
1085
+ ): RouteSet<
1086
+ [
1087
+ Route<
1088
+ ExtractMethods<RoutesA> | ExtractMethods<RoutesB>,
1089
+ ExtractMedia<RoutesA> | ExtractMedia<RoutesB>,
1090
+ RouteHandler<HttpServerResponse.HttpServerResponse, any, never>,
1091
+ MergeSchemas<SchemasA, SchemasB>
1092
+ >,
1093
+ ],
1094
+ MergeSchemas<SchemasA, SchemasB>
1095
+ > {
1096
+ const allRoutes = [...self.set, ...other.set]
1097
+ const mergedSchemas = mergeSchemas(self.schema, other.schema)
1098
+
1099
+ const handler: RouteHandler<HttpServerResponse.HttpServerResponse> = (
1100
+ context,
1101
+ ) =>
1102
+ Effect.gen(function*() {
1103
+ const accept = context.request.headers.accept ?? ""
1104
+
1105
+ if (accept.includes("application/json")) {
1106
+ const jsonRoute = allRoutes.find((r) => r.media === "application/json")
1107
+ if (jsonRoute) {
1108
+ return yield* RouteRender.render(jsonRoute, context)
1109
+ }
1110
+ }
1111
+
1112
+ if (accept.includes("text/plain")) {
1113
+ const textRoute = allRoutes.find((r) => r.media === "text/plain")
1114
+ if (textRoute) {
1115
+ return yield* RouteRender.render(textRoute, context)
1116
+ }
1117
+ }
1118
+
1119
+ if (
1120
+ accept.includes("text/html") || accept.includes("*/*") || !accept
1121
+ ) {
1122
+ const htmlRoute = allRoutes.find((r) => r.media === "text/html")
1123
+ if (htmlRoute) {
1124
+ return yield* RouteRender.render(htmlRoute, context)
1125
+ }
1126
+ }
1127
+
1128
+ const firstRoute = allRoutes[0]
1129
+ if (firstRoute) {
1130
+ return yield* RouteRender.render(firstRoute, context)
1131
+ }
1132
+
1133
+ return HttpServerResponse.empty({ status: 406 })
1134
+ })
1135
+
1136
+ return makeSet(
1137
+ [
1138
+ make({
1139
+ method: allRoutes[0]?.method ?? "*",
1140
+ media: allRoutes[0]?.media ?? "*",
1141
+ handler,
1142
+ schemas: mergedSchemas,
1143
+ }),
1144
+ ] as any,
1145
+ mergedSchemas,
1146
+ ) as any
1147
+ }