effect-start 0.9.0 → 0.10.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 (44) hide show
  1. package/package.json +12 -13
  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 +81 -12
  7. package/src/FileHttpRouter.ts +115 -26
  8. package/src/FileRouter.ts +60 -162
  9. package/src/FileRouterCodegen.test.ts +250 -64
  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/HttpUtils.test.ts +68 -0
  17. package/src/HttpUtils.ts +15 -0
  18. package/src/HyperHtml.ts +24 -5
  19. package/src/JsModule.test.ts +1 -1
  20. package/src/NodeFileSystem.ts +764 -0
  21. package/src/Random.ts +59 -0
  22. package/src/Route.test.ts +471 -0
  23. package/src/Route.ts +298 -153
  24. package/src/RouteRender.ts +38 -0
  25. package/src/Router.ts +11 -33
  26. package/src/RouterPattern.test.ts +629 -0
  27. package/src/RouterPattern.ts +391 -0
  28. package/src/Start.ts +14 -52
  29. package/src/bun/BunBundle.test.ts +0 -3
  30. package/src/bun/BunHttpServer.ts +246 -0
  31. package/src/bun/BunHttpServer_web.ts +384 -0
  32. package/src/bun/BunRoute.test.ts +341 -0
  33. package/src/bun/BunRoute.ts +326 -0
  34. package/src/bun/BunRoute_bundles.test.ts +218 -0
  35. package/src/bun/BunRuntime.ts +33 -0
  36. package/src/bun/BunTailwindPlugin.test.ts +1 -1
  37. package/src/bun/_empty.html +1 -0
  38. package/src/bun/index.ts +2 -1
  39. package/src/testing.ts +12 -3
  40. package/src/Datastar.test.ts +0 -267
  41. package/src/Datastar.ts +0 -68
  42. package/src/bun/BunFullstackServer.ts +0 -45
  43. package/src/bun/BunFullstackServer_httpServer.ts +0 -541
  44. package/src/jsx-datastar.d.ts +0 -63
package/src/Route.ts CHANGED
@@ -1,13 +1,15 @@
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
6
  import * as Effect from "effect/Effect"
7
+ import * as Function from "effect/Function"
6
8
  import * as Pipeable from "effect/Pipeable"
7
9
  import * as Predicate from "effect/Predicate"
8
10
  import * as Schema from "effect/Schema"
9
11
  import type { YieldWrap } from "effect/Utils"
10
- import * as HyperHtml from "./HyperHtml.ts"
12
+ import * as RouteRender from "./RouteRender.ts"
11
13
 
12
14
  export {
13
15
  pipe,
@@ -63,12 +65,13 @@ type Self =
63
65
 
64
66
  const TypeId: unique symbol = Symbol.for("effect-start/Route")
65
67
  const RouteSetTypeId: unique symbol = Symbol.for("effect-start/RouteSet")
68
+ const RouteLayerTypeId: unique symbol = Symbol.for("effect-start/RouteLayer")
66
69
 
67
70
  export type RouteMethod =
68
71
  | "*"
69
72
  | HttpMethod.HttpMethod
70
73
 
71
- export type RoutePath = `/${string}`
74
+ export type RoutePattern = `/${string}`
72
75
 
73
76
  /**
74
77
  * Route media type used for content negotiation.
@@ -76,55 +79,24 @@ export type RoutePath = `/${string}`
76
79
  * for the same path & method, depending on the `Accept` header
77
80
  * of the request.
78
81
  */
79
- type RouteMedia =
82
+ export type RouteMedia =
80
83
  | "*"
81
84
  | "text/plain"
82
85
  | "text/html"
83
86
  | "application/json"
84
87
 
88
+ /**
89
+ * A handler function that produces a raw value.
90
+ * The value will be rendered to an HttpServerResponse by RouteRender
91
+ * based on the route's media type.
92
+ *
93
+ * Receives RouteContext which includes an optional next() for layers.
94
+ */
85
95
  export type RouteHandler<
86
96
  A = unknown,
87
97
  E = any,
88
98
  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
- }
99
+ > = (context: RouteContext) => Effect.Effect<A, E, R>
128
100
 
129
101
  /**
130
102
  * Helper type for a value that can be a single item or an array.
@@ -262,7 +234,58 @@ export namespace RouteSet {
262
234
  & RouteBuilder
263
235
  }
264
236
 
237
+ /**
238
+ * Type for HTTP middleware function
239
+ */
240
+ export type HttpMiddlewareFunction = ReturnType<typeof HttpMiddleware.make>
241
+
242
+ /**
243
+ * Marker type for route middleware specification.
244
+ * Used to distinguish middleware from routes in Route.layer() arguments.
245
+ */
246
+ export interface RouteMiddleware {
247
+ readonly _tag: "RouteMiddleware"
248
+ readonly middleware: HttpMiddlewareFunction
249
+ }
250
+
251
+ export type RouteLayer<
252
+ M extends ReadonlyArray<Route.Default> = ReadonlyArray<Route.Default>,
253
+ Schemas extends RouteSchemas = RouteSchemas.Empty,
254
+ > =
255
+ & Pipeable.Pipeable
256
+ & {
257
+ [RouteLayerTypeId]: typeof RouteLayerTypeId
258
+ [RouteSetTypeId]: typeof RouteSetTypeId
259
+ set: M
260
+ schema: Schemas
261
+ httpMiddleware?: HttpMiddlewareFunction
262
+ }
263
+ & RouteBuilder
264
+
265
+ export const isRouteLayer = (u: unknown): u is RouteLayer =>
266
+ Predicate.hasProperty(u, RouteLayerTypeId)
267
+
268
+ /**
269
+ * Check if two routes match based on method and media type.
270
+ * Returns true if both method and media type match, accounting for wildcards.
271
+ */
272
+ export function matches(
273
+ a: Route.Default,
274
+ b: Route.Default,
275
+ ): boolean {
276
+ const methodMatches = a.method === "*"
277
+ || b.method === "*"
278
+ || a.method === b.method
279
+
280
+ const mediaMatches = a.media === "*"
281
+ || b.media === "*"
282
+ || a.media === b.media
283
+
284
+ return methodMatches && mediaMatches
285
+ }
286
+
265
287
  export const post = makeMethodModifier("POST")
288
+
266
289
  export const get = makeMethodModifier("GET")
267
290
  export const put = makeMethodModifier("PUT")
268
291
  export const patch = makeMethodModifier("PATCH")
@@ -270,28 +293,19 @@ export const del = makeMethodModifier("DELETE")
270
293
  export const options = makeMethodModifier("OPTIONS")
271
294
  export const head = makeMethodModifier("HEAD")
272
295
 
273
- export const text = makeMediaFunction(
296
+ export const text = makeMediaFunction<"GET", "text/plain", string>(
274
297
  "GET",
275
298
  "text/plain",
276
- makeValueHandler<string>(HttpServerResponse.text),
277
299
  )
278
300
 
279
- export const html = makeMediaFunction(
301
+ export const html = makeMediaFunction<"GET", "text/html", string | JsxObject>(
280
302
  "GET",
281
303
  "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
304
  )
290
305
 
291
- export const json = makeMediaFunction(
306
+ export const json = makeMediaFunction<"GET", "application/json", JsonValue>(
292
307
  "GET",
293
308
  "application/json",
294
- makeValueHandler<JsonValue>((raw) => HttpServerResponse.unsafeJson(raw)),
295
309
  )
296
310
 
297
311
  /**
@@ -379,7 +393,7 @@ function makeSingleStringSchemaModifier<
379
393
  {
380
394
  const baseRoutes = isRouteSet(this)
381
395
  ? this.set
382
- : []
396
+ : [] as const
383
397
  const baseSchema = isRouteSet(this)
384
398
  ? this.schema
385
399
  : {} as RouteSchemas.Empty
@@ -389,12 +403,12 @@ function makeSingleStringSchemaModifier<
389
403
  : Schema.Struct(fieldsOrSchema as Schema.Struct.Fields)
390
404
 
391
405
  return makeSet(
392
- baseRoutes as any,
406
+ baseRoutes as ReadonlyArray<Route.Default>,
393
407
  {
394
408
  ...baseSchema,
395
409
  [key]: schema,
396
- } as any,
397
- ) as any
410
+ },
411
+ ) as never
398
412
  }
399
413
  }
400
414
 
@@ -430,7 +444,7 @@ function makeMultiStringSchemaModifier<
430
444
  {
431
445
  const baseRoutes = isRouteSet(this)
432
446
  ? this.set
433
- : []
447
+ : [] as const
434
448
  const baseSchema = isRouteSet(this)
435
449
  ? this.schema
436
450
  : {} as RouteSchemas.Empty
@@ -440,12 +454,12 @@ function makeMultiStringSchemaModifier<
440
454
  : Schema.Struct(fieldsOrSchema as Schema.Struct.Fields)
441
455
 
442
456
  return makeSet(
443
- baseRoutes as any,
457
+ baseRoutes as ReadonlyArray<Route.Default>,
444
458
  {
445
459
  ...baseSchema,
446
460
  [key]: schema,
447
- } as any,
448
- ) as any
461
+ },
462
+ ) as never
449
463
  }
450
464
  }
451
465
 
@@ -478,7 +492,7 @@ function makeUnionSchemaModifier<
478
492
  {
479
493
  const baseRoutes = isRouteSet(this)
480
494
  ? this.set
481
- : []
495
+ : [] as const
482
496
  const baseSchema = isRouteSet(this)
483
497
  ? this.schema
484
498
  : {} as RouteSchemas.Empty
@@ -488,12 +502,12 @@ function makeUnionSchemaModifier<
488
502
  : Schema.Struct(fieldsOrSchema as Schema.Struct.Fields)
489
503
 
490
504
  return makeSet(
491
- baseRoutes as any,
505
+ baseRoutes as ReadonlyArray<Route.Default>,
492
506
  {
493
507
  ...baseSchema,
494
508
  [key]: schema,
495
- } as any,
496
- ) as any
509
+ },
510
+ ) as never
497
511
  }
498
512
  }
499
513
 
@@ -538,6 +552,13 @@ const RouteProto = Object.assign(
538
552
  } satisfies Route.Proto,
539
553
  )
540
554
 
555
+ const RouteLayerProto = Object.assign(
556
+ Object.create(SetProto),
557
+ {
558
+ [RouteLayerTypeId]: RouteLayerTypeId,
559
+ },
560
+ )
561
+
541
562
  export function isRoute(input: unknown): input is Route {
542
563
  return Predicate.hasProperty(input, TypeId)
543
564
  }
@@ -558,24 +579,6 @@ export type JsonValue =
558
579
  [key: string]: JsonValue
559
580
  }
560
581
 
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
582
  type RouteContextDecoded = {
580
583
  readonly pathParams?: Record<string, any>
581
584
  readonly urlParams?: Record<string, any>
@@ -607,13 +610,15 @@ export type DecodeRouteSchemas<Schemas extends RouteSchemas> =
607
610
  : {})
608
611
 
609
612
  /**
610
- * Context passed to route handler generator functions.
613
+ * Context passed to route handler functions.
611
614
  */
612
615
  export type RouteContext<
613
616
  Input extends RouteContextDecoded = {},
614
617
  > = {
615
618
  request: HttpServerRequest.HttpServerRequest
616
619
  get url(): URL
620
+ slots: Record<string, string>
621
+ next: () => Effect.Effect<any, any, any>
617
622
  } & Input
618
623
 
619
624
  /**
@@ -730,7 +735,7 @@ function mergeSchemas<
730
735
  return result
731
736
  }
732
737
 
733
- function make<
738
+ export function make<
734
739
  Method extends RouteMethod = "*",
735
740
  Media extends RouteMedia = "*",
736
741
  Handler extends RouteHandler = never,
@@ -786,38 +791,31 @@ function makeSet<
786
791
 
787
792
  /**
788
793
  * Factory function that creates Route for a specific method & media.
789
- * Supports both Effect values and generator functions that receive context.
794
+ * Accepts Effect, function that returns Effect, and effectful generator.
790
795
  */
791
796
  function makeMediaFunction<
792
797
  Method extends HttpMethod.HttpMethod,
793
798
  Media extends RouteMedia,
794
- HandlerFn extends (
795
- handler: any,
796
- ) => any,
799
+ ExpectedValue,
797
800
  >(
798
801
  method: Method,
799
802
  media: Media,
800
- handlerFn: HandlerFn,
801
803
  ) {
802
804
  return function<
803
805
  S extends Self,
804
- A,
806
+ A extends ExpectedValue,
805
807
  E = never,
806
808
  R = never,
807
809
  >(
808
810
  this: S,
809
811
  handler: S extends RouteSet<infer _Routes, infer Schemas> ?
810
812
  | Effect.Effect<A, E, R>
811
- | ((
812
- context: RouteContext<DecodeRouteSchemas<Schemas>>,
813
- ) =>
813
+ | ((context: RouteContext<DecodeRouteSchemas<Schemas>>) =>
814
814
  | Effect.Effect<A, E, R>
815
815
  | Generator<YieldWrap<Effect.Effect<A, E, R>>, A, never>)
816
816
  :
817
817
  | Effect.Effect<A, E, R>
818
- | ((
819
- context: RouteContext<{}>,
820
- ) =>
818
+ | ((context: RouteContext<{}>) =>
821
819
  | Effect.Effect<A, E, R>
822
820
  | Generator<YieldWrap<Effect.Effect<A, E, R>>, A, never>),
823
821
  ): S extends RouteSet<infer Routes, infer Schemas> ? RouteSet<[
@@ -825,7 +823,7 @@ function makeMediaFunction<
825
823
  Route<
826
824
  Method,
827
825
  Media,
828
- ReturnType<HandlerFn>,
826
+ RouteHandler<A, E, R>,
829
827
  Schemas
830
828
  >,
831
829
  ], Schemas>
@@ -833,32 +831,31 @@ function makeMediaFunction<
833
831
  Route<
834
832
  Method,
835
833
  Media,
836
- ReturnType<HandlerFn>,
834
+ RouteHandler<A, E, R>,
837
835
  RouteSchemas.Empty
838
836
  >,
839
837
  ], RouteSchemas.Empty>
840
838
  {
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
- },
839
+ const normalizedHandler: RouteHandler<A, E, R> =
840
+ typeof handler === "function"
841
+ ? (context: RouteContext): Effect.Effect<A, E, R> => {
842
+ const result = handler(context)
843
+ if (Effect.isEffect(result)) {
844
+ return result
845
+ }
846
+ return Effect.gen(() =>
847
+ result as Generator<
848
+ YieldWrap<Effect.Effect<A, E, R>>,
849
+ A,
850
+ never
851
+ >
852
+ ) as Effect.Effect<A, E, R>
849
853
  }
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
854
+ : () => handler
858
855
 
859
856
  const baseRoutes = isRouteSet(this)
860
857
  ? this.set
861
- : []
858
+ : [] as const
862
859
  const baseSchema = isRouteSet(this)
863
860
  ? this.schema
864
861
  : {} as RouteSchemas.Empty
@@ -869,36 +866,12 @@ function makeMediaFunction<
869
866
  make({
870
867
  method,
871
868
  media,
872
- handler: handlerFn(effect as any) as any,
873
- schemas: baseSchema as any,
869
+ handler: normalizedHandler,
870
+ schemas: baseSchema,
874
871
  }),
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>
872
+ ] as ReadonlyArray<Route.Default>,
873
+ baseSchema,
874
+ ) as never
902
875
  }
903
876
  }
904
877
 
@@ -970,23 +943,195 @@ function makeMethodModifier<
970
943
  return make({
971
944
  ...route,
972
945
  method,
973
- schemas: mergeSchemas(baseSchema, route.schemas) as any,
946
+ schemas: mergeSchemas(baseSchema, route.schemas),
974
947
  })
975
948
  }),
976
- ] as any,
977
- baseSchema as any,
978
- ) as any
949
+ ] as ReadonlyArray<Route.Default>,
950
+ baseSchema,
951
+ ) as never
979
952
  }
980
953
  }
981
954
 
982
- type JsxObject = {
955
+ export type JsxObject = {
983
956
  type: any
984
957
  props: any
985
958
  }
986
959
 
987
- function isJsxObject(value: any) {
960
+ export function isJsxObject(value: unknown): value is JsxObject {
988
961
  return typeof value === "object"
989
962
  && value !== null
990
963
  && "type" in value
991
964
  && "props" in value
992
965
  }
966
+
967
+ /**
968
+ * Create HTTP middleware spec for Route.layer().
969
+ * Multiple middleware can be composed by passing multiple Route.http() to Route.layer().
970
+ *
971
+ * @example
972
+ * Route.layer(
973
+ * Route.http(middleware1),
974
+ * Route.http(middleware2),
975
+ * Route.http(middleware3)
976
+ * )
977
+ */
978
+ export function http(
979
+ middleware: HttpMiddlewareFunction,
980
+ ): RouteMiddleware {
981
+ return {
982
+ _tag: "RouteMiddleware",
983
+ middleware,
984
+ }
985
+ }
986
+
987
+ /**
988
+ * Create a RouteLayer from routes and middleware.
989
+ *
990
+ * Accepts:
991
+ * - Route.http(middleware) - HTTP middleware to apply to all child routes
992
+ * - Route.html/text/json/etc handlers - Wrapper routes that receive props.children
993
+ * - Other RouteSets - Routes to include in the layer
994
+ *
995
+ * Multiple middleware are composed in order - first middleware wraps second, etc.
996
+ * Routes in the layer act as wrappers for child routes with matching method + media type.
997
+ *
998
+ * @example
999
+ * Route.layer(
1000
+ * Route.http(loggingMiddleware),
1001
+ * Route.http(authMiddleware),
1002
+ * Route.html(function*(props) {
1003
+ * return <html><body>{props.children}</body></html>
1004
+ * })
1005
+ * )
1006
+ */
1007
+ export function layer(
1008
+ ...items: Array<RouteMiddleware | RouteSet.Default>
1009
+ ): RouteLayer {
1010
+ const routeMiddleware: RouteMiddleware[] = []
1011
+ const routeSets: RouteSet.Default[] = []
1012
+
1013
+ for (const item of items) {
1014
+ if ("_tag" in item && item._tag === "RouteMiddleware") {
1015
+ routeMiddleware.push(item)
1016
+ } else if (isRouteSet(item)) {
1017
+ routeSets.push(item)
1018
+ }
1019
+ }
1020
+
1021
+ const layerRoutes: Route.Default[] = []
1022
+
1023
+ for (const routeSet of routeSets) {
1024
+ for (const route of routeSet.set) {
1025
+ layerRoutes.push(make({
1026
+ method: route.method,
1027
+ media: route.media,
1028
+ handler: route.handler,
1029
+ schemas: {},
1030
+ }))
1031
+ }
1032
+ }
1033
+
1034
+ const middlewareFunctions = routeMiddleware.map((spec) => spec.middleware)
1035
+ const httpMiddleware = middlewareFunctions.length === 0
1036
+ ? undefined
1037
+ : middlewareFunctions.length === 1
1038
+ ? middlewareFunctions[0]
1039
+ : (app: any) => middlewareFunctions.reduceRight((acc, mw) => mw(acc), app)
1040
+
1041
+ return Object.assign(
1042
+ Object.create(RouteLayerProto),
1043
+ {
1044
+ set: layerRoutes,
1045
+ schema: {},
1046
+ httpMiddleware,
1047
+ },
1048
+ )
1049
+ }
1050
+
1051
+ /**
1052
+ * Extract method union from a RouteSet's routes
1053
+ */
1054
+
1055
+ type ExtractMethods<T extends ReadonlyArray<Route.Default>> =
1056
+ T[number]["method"]
1057
+
1058
+ /**
1059
+ * Extract media union from a RouteSet's routes
1060
+ */
1061
+ type ExtractMedia<T extends ReadonlyArray<Route.Default>> = T[number]["media"]
1062
+
1063
+ /**
1064
+ * Merge two RouteSets into one with content negotiation.
1065
+ * Properly infers union types for method/media and merges schemas.
1066
+ */
1067
+ export function merge<
1068
+ RoutesA extends ReadonlyArray<Route.Default>,
1069
+ SchemasA extends RouteSchemas,
1070
+ RoutesB extends ReadonlyArray<Route.Default>,
1071
+ SchemasB extends RouteSchemas,
1072
+ >(
1073
+ self: RouteSet<RoutesA, SchemasA>,
1074
+ other: RouteSet<RoutesB, SchemasB>,
1075
+ ): RouteSet<
1076
+ [
1077
+ Route<
1078
+ ExtractMethods<RoutesA> | ExtractMethods<RoutesB>,
1079
+ ExtractMedia<RoutesA> | ExtractMedia<RoutesB>,
1080
+ RouteHandler<HttpServerResponse.HttpServerResponse, any, never>,
1081
+ MergeSchemas<SchemasA, SchemasB>
1082
+ >,
1083
+ ],
1084
+ MergeSchemas<SchemasA, SchemasB>
1085
+ > {
1086
+ const allRoutes = [...self.set, ...other.set]
1087
+ const mergedSchemas = mergeSchemas(self.schema, other.schema)
1088
+
1089
+ const handler: RouteHandler<HttpServerResponse.HttpServerResponse> = (
1090
+ context,
1091
+ ) =>
1092
+ Effect.gen(function*() {
1093
+ const accept = context.request.headers.accept ?? ""
1094
+
1095
+ if (accept.includes("application/json")) {
1096
+ const jsonRoute = allRoutes.find((r) => r.media === "application/json")
1097
+ if (jsonRoute) {
1098
+ return yield* RouteRender.render(jsonRoute, context)
1099
+ }
1100
+ }
1101
+
1102
+ if (accept.includes("text/plain")) {
1103
+ const textRoute = allRoutes.find((r) => r.media === "text/plain")
1104
+ if (textRoute) {
1105
+ return yield* RouteRender.render(textRoute, context)
1106
+ }
1107
+ }
1108
+
1109
+ if (
1110
+ accept.includes("text/html") || accept.includes("*/*") || !accept
1111
+ ) {
1112
+ const htmlRoute = allRoutes.find((r) => r.media === "text/html")
1113
+ if (htmlRoute) {
1114
+ return yield* RouteRender.render(htmlRoute, context)
1115
+ }
1116
+ }
1117
+
1118
+ const firstRoute = allRoutes[0]
1119
+ if (firstRoute) {
1120
+ return yield* RouteRender.render(firstRoute, context)
1121
+ }
1122
+
1123
+ return HttpServerResponse.empty({ status: 406 })
1124
+ })
1125
+
1126
+ return makeSet(
1127
+ [
1128
+ make({
1129
+ method: allRoutes[0]?.method ?? "*",
1130
+ media: allRoutes[0]?.media ?? "*",
1131
+ handler,
1132
+ schemas: mergedSchemas,
1133
+ }),
1134
+ ] as any,
1135
+ mergedSchemas,
1136
+ ) as any
1137
+ }
@@ -0,0 +1,38 @@
1
+ import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
2
+ import * as Effect from "effect/Effect"
3
+ import * as HyperHtml from "./HyperHtml.ts"
4
+ import * as Route from "./Route.ts"
5
+
6
+ /**
7
+ * Renders a route handler to an HttpServerResponse.
8
+ * Converts the raw handler value to a response based on the route's media type.
9
+ */
10
+ export function render<E, R>(
11
+ route: Route.Route<any, any, Route.RouteHandler<any, E, R>, any>,
12
+ context: Route.RouteContext,
13
+ ): Effect.Effect<HttpServerResponse.HttpServerResponse, E, R> {
14
+ return Effect.gen(function*() {
15
+ const raw = yield* route.handler(context)
16
+
17
+ switch (route.media) {
18
+ case "text/plain":
19
+ return HttpServerResponse.text(raw as string)
20
+
21
+ case "text/html":
22
+ if (Route.isJsxObject(raw)) {
23
+ return HttpServerResponse.html(HyperHtml.renderToString(raw))
24
+ }
25
+ return HttpServerResponse.html(raw as string)
26
+
27
+ case "application/json":
28
+ return HttpServerResponse.unsafeJson(raw)
29
+
30
+ case "*":
31
+ default:
32
+ if (HttpServerResponse.isServerResponse(raw)) {
33
+ return raw
34
+ }
35
+ return HttpServerResponse.text(String(raw))
36
+ }
37
+ })
38
+ }