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.
- package/package.json +12 -13
- package/src/BundleHttp.test.ts +1 -1
- package/src/Commander.test.ts +15 -15
- package/src/Commander.ts +58 -88
- package/src/EncryptedCookies.test.ts +4 -4
- package/src/FileHttpRouter.test.ts +81 -12
- package/src/FileHttpRouter.ts +115 -26
- package/src/FileRouter.ts +60 -162
- package/src/FileRouterCodegen.test.ts +250 -64
- package/src/FileRouterCodegen.ts +13 -56
- package/src/FileRouterPattern.test.ts +116 -0
- package/src/FileRouterPattern.ts +59 -0
- package/src/FileRouter_path.test.ts +63 -102
- package/src/FileSystemExtra.test.ts +226 -0
- package/src/FileSystemExtra.ts +24 -60
- package/src/HttpUtils.test.ts +68 -0
- package/src/HttpUtils.ts +15 -0
- package/src/HyperHtml.ts +24 -5
- package/src/JsModule.test.ts +1 -1
- package/src/NodeFileSystem.ts +764 -0
- package/src/Random.ts +59 -0
- package/src/Route.test.ts +471 -0
- package/src/Route.ts +298 -153
- package/src/RouteRender.ts +38 -0
- package/src/Router.ts +11 -33
- package/src/RouterPattern.test.ts +629 -0
- package/src/RouterPattern.ts +391 -0
- package/src/Start.ts +14 -52
- package/src/bun/BunBundle.test.ts +0 -3
- package/src/bun/BunHttpServer.ts +246 -0
- package/src/bun/BunHttpServer_web.ts +384 -0
- package/src/bun/BunRoute.test.ts +341 -0
- package/src/bun/BunRoute.ts +326 -0
- package/src/bun/BunRoute_bundles.test.ts +218 -0
- package/src/bun/BunRuntime.ts +33 -0
- package/src/bun/BunTailwindPlugin.test.ts +1 -1
- package/src/bun/_empty.html +1 -0
- package/src/bun/index.ts +2 -1
- package/src/testing.ts +12 -3
- package/src/Datastar.test.ts +0 -267
- package/src/Datastar.ts +0 -68
- package/src/bun/BunFullstackServer.ts +0 -45
- package/src/bun/BunFullstackServer_httpServer.ts +0 -541
- 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
|
|
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
|
|
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
|
|
406
|
+
baseRoutes as ReadonlyArray<Route.Default>,
|
|
393
407
|
{
|
|
394
408
|
...baseSchema,
|
|
395
409
|
[key]: schema,
|
|
396
|
-
}
|
|
397
|
-
) as
|
|
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
|
|
457
|
+
baseRoutes as ReadonlyArray<Route.Default>,
|
|
444
458
|
{
|
|
445
459
|
...baseSchema,
|
|
446
460
|
[key]: schema,
|
|
447
|
-
}
|
|
448
|
-
) as
|
|
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
|
|
505
|
+
baseRoutes as ReadonlyArray<Route.Default>,
|
|
492
506
|
{
|
|
493
507
|
...baseSchema,
|
|
494
508
|
[key]: schema,
|
|
495
|
-
}
|
|
496
|
-
) as
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
834
|
+
RouteHandler<A, E, R>,
|
|
837
835
|
RouteSchemas.Empty
|
|
838
836
|
>,
|
|
839
837
|
], RouteSchemas.Empty>
|
|
840
838
|
{
|
|
841
|
-
const
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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
|
-
|
|
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:
|
|
873
|
-
schemas: baseSchema
|
|
869
|
+
handler: normalizedHandler,
|
|
870
|
+
schemas: baseSchema,
|
|
874
871
|
}),
|
|
875
|
-
] as
|
|
876
|
-
baseSchema
|
|
877
|
-
) as
|
|
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)
|
|
946
|
+
schemas: mergeSchemas(baseSchema, route.schemas),
|
|
974
947
|
})
|
|
975
948
|
}),
|
|
976
|
-
] as
|
|
977
|
-
baseSchema
|
|
978
|
-
) as
|
|
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:
|
|
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
|
+
}
|