effect-start 0.10.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.
- package/package.json +5 -3
- package/src/FileHttpRouter.test.ts +4 -4
- package/src/FileHttpRouter.ts +6 -8
- package/src/FileRouter.ts +4 -6
- package/src/FileRouterCodegen.test.ts +2 -2
- package/src/HttpAppExtra.test.ts +84 -0
- package/src/HttpAppExtra.ts +399 -47
- package/src/Route.test.ts +59 -33
- package/src/Route.ts +59 -49
- package/src/RouteRender.ts +6 -4
- package/src/Router.test.ts +416 -0
- package/src/Router.ts +279 -0
- package/src/RouterPattern.test.ts +29 -3
- package/src/RouterPattern.ts +30 -5
- package/src/TestHttpClient.test.ts +29 -0
- package/src/TestHttpClient.ts +122 -73
- package/src/assets.d.ts +39 -0
- package/src/bun/BunHttpServer.test.ts +74 -0
- package/src/bun/BunHttpServer.ts +22 -9
- package/src/bun/BunRoute.test.ts +307 -134
- package/src/bun/BunRoute.ts +240 -139
- package/src/bun/BunRoute_bundles.test.ts +181 -181
- package/src/index.ts +14 -14
- package/src/middlewares/BasicAuthMiddleware.test.ts +74 -0
- package/src/middlewares/BasicAuthMiddleware.ts +36 -0
package/src/Route.ts
CHANGED
|
@@ -2,9 +2,7 @@ import * as HttpMethod from "@effect/platform/HttpMethod"
|
|
|
2
2
|
import * as HttpMiddleware from "@effect/platform/HttpMiddleware"
|
|
3
3
|
import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
|
|
4
4
|
import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
|
|
5
|
-
|
|
6
5
|
import * as Effect from "effect/Effect"
|
|
7
|
-
import * as Function from "effect/Function"
|
|
8
6
|
import * as Pipeable from "effect/Pipeable"
|
|
9
7
|
import * as Predicate from "effect/Predicate"
|
|
10
8
|
import * as Schema from "effect/Schema"
|
|
@@ -71,6 +69,7 @@ export type RouteMethod =
|
|
|
71
69
|
| "*"
|
|
72
70
|
| HttpMethod.HttpMethod
|
|
73
71
|
|
|
72
|
+
// TODO: This should be a RouterPattern and moved to its file?
|
|
74
73
|
export type RoutePattern = `/${string}`
|
|
75
74
|
|
|
76
75
|
/**
|
|
@@ -185,7 +184,7 @@ type RouteBuilder = {
|
|
|
185
184
|
get: typeof get
|
|
186
185
|
put: typeof put
|
|
187
186
|
patch: typeof patch
|
|
188
|
-
|
|
187
|
+
delete: typeof _delete
|
|
189
188
|
options: typeof options
|
|
190
189
|
head: typeof head
|
|
191
190
|
|
|
@@ -285,20 +284,26 @@ export function matches(
|
|
|
285
284
|
}
|
|
286
285
|
|
|
287
286
|
export const post = makeMethodModifier("POST")
|
|
288
|
-
|
|
289
287
|
export const get = makeMethodModifier("GET")
|
|
290
288
|
export const put = makeMethodModifier("PUT")
|
|
291
289
|
export const patch = makeMethodModifier("PATCH")
|
|
292
|
-
export const del = makeMethodModifier("DELETE")
|
|
293
290
|
export const options = makeMethodModifier("OPTIONS")
|
|
294
291
|
export const head = makeMethodModifier("HEAD")
|
|
292
|
+
const _delete = makeMethodModifier("DELETE")
|
|
293
|
+
export {
|
|
294
|
+
_delete as delete,
|
|
295
|
+
}
|
|
295
296
|
|
|
296
297
|
export const text = makeMediaFunction<"GET", "text/plain", string>(
|
|
297
298
|
"GET",
|
|
298
299
|
"text/plain",
|
|
299
300
|
)
|
|
300
301
|
|
|
301
|
-
export const html = makeMediaFunction<
|
|
302
|
+
export const html = makeMediaFunction<
|
|
303
|
+
"GET",
|
|
304
|
+
"text/html",
|
|
305
|
+
string | GenericJsxObject
|
|
306
|
+
>(
|
|
302
307
|
"GET",
|
|
303
308
|
"text/html",
|
|
304
309
|
)
|
|
@@ -525,7 +530,7 @@ const SetProto = {
|
|
|
525
530
|
get,
|
|
526
531
|
put,
|
|
527
532
|
patch,
|
|
528
|
-
|
|
533
|
+
delete: _delete,
|
|
529
534
|
options,
|
|
530
535
|
head,
|
|
531
536
|
|
|
@@ -611,21 +616,21 @@ export type DecodeRouteSchemas<Schemas extends RouteSchemas> =
|
|
|
611
616
|
|
|
612
617
|
/**
|
|
613
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
|
|
614
622
|
*/
|
|
615
623
|
export type RouteContext<
|
|
616
624
|
Input extends RouteContextDecoded = {},
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
*/
|
|
627
|
-
type ExtractStructFields<S> = S extends Schema.Struct<infer Fields> ? Fields
|
|
628
|
-
: 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
|
|
629
634
|
|
|
630
635
|
/**
|
|
631
636
|
* Merges two RouteSchemas types.
|
|
@@ -789,6 +794,31 @@ function makeSet<
|
|
|
789
794
|
) as RouteSet<M, Schemas>
|
|
790
795
|
}
|
|
791
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
|
+
|
|
792
822
|
/**
|
|
793
823
|
* Factory function that creates Route for a specific method & media.
|
|
794
824
|
* Accepts Effect, function that returns Effect, and effectful generator.
|
|
@@ -809,13 +839,17 @@ function makeMediaFunction<
|
|
|
809
839
|
>(
|
|
810
840
|
this: S,
|
|
811
841
|
handler: S extends RouteSet<infer _Routes, infer Schemas> ?
|
|
842
|
+
| A
|
|
812
843
|
| Effect.Effect<A, E, R>
|
|
813
|
-
| ((
|
|
844
|
+
| ((
|
|
845
|
+
context: RouteContext<DecodeRouteSchemas<Schemas>, ExpectedValue>,
|
|
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
|
-
| ((context: RouteContext<{}>) =>
|
|
852
|
+
| ((context: RouteContext<{}, ExpectedValue>) =>
|
|
819
853
|
| Effect.Effect<A, E, R>
|
|
820
854
|
| Generator<YieldWrap<Effect.Effect<A, E, R>>, A, never>),
|
|
821
855
|
): S extends RouteSet<infer Routes, infer Schemas> ? RouteSet<[
|
|
@@ -836,23 +870,6 @@ function makeMediaFunction<
|
|
|
836
870
|
>,
|
|
837
871
|
], RouteSchemas.Empty>
|
|
838
872
|
{
|
|
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>
|
|
853
|
-
}
|
|
854
|
-
: () => handler
|
|
855
|
-
|
|
856
873
|
const baseRoutes = isRouteSet(this)
|
|
857
874
|
? this.set
|
|
858
875
|
: [] as const
|
|
@@ -866,7 +883,7 @@ function makeMediaFunction<
|
|
|
866
883
|
make({
|
|
867
884
|
method,
|
|
868
885
|
media,
|
|
869
|
-
handler:
|
|
886
|
+
handler: normalizeHandler(handler),
|
|
870
887
|
schemas: baseSchema,
|
|
871
888
|
}),
|
|
872
889
|
] as ReadonlyArray<Route.Default>,
|
|
@@ -952,12 +969,12 @@ function makeMethodModifier<
|
|
|
952
969
|
}
|
|
953
970
|
}
|
|
954
971
|
|
|
955
|
-
export type
|
|
972
|
+
export type GenericJsxObject = {
|
|
956
973
|
type: any
|
|
957
974
|
props: any
|
|
958
975
|
}
|
|
959
976
|
|
|
960
|
-
export function
|
|
977
|
+
export function isGenericJsxObject(value: unknown): value is GenericJsxObject {
|
|
961
978
|
return typeof value === "object"
|
|
962
979
|
&& value !== null
|
|
963
980
|
&& "type" in value
|
|
@@ -1021,14 +1038,7 @@ export function layer(
|
|
|
1021
1038
|
const layerRoutes: Route.Default[] = []
|
|
1022
1039
|
|
|
1023
1040
|
for (const routeSet of routeSets) {
|
|
1024
|
-
|
|
1025
|
-
layerRoutes.push(make({
|
|
1026
|
-
method: route.method,
|
|
1027
|
-
media: route.media,
|
|
1028
|
-
handler: route.handler,
|
|
1029
|
-
schemas: {},
|
|
1030
|
-
}))
|
|
1031
|
-
}
|
|
1041
|
+
layerRoutes.push(...routeSet.set)
|
|
1032
1042
|
}
|
|
1033
1043
|
|
|
1034
1044
|
const middlewareFunctions = routeMiddleware.map((spec) => spec.middleware)
|
package/src/RouteRender.ts
CHANGED
|
@@ -14,12 +14,17 @@ export function render<E, R>(
|
|
|
14
14
|
return Effect.gen(function*() {
|
|
15
15
|
const raw = yield* route.handler(context)
|
|
16
16
|
|
|
17
|
+
// Allow handlers to return HttpServerResponse directly (e.g. BunRoute proxy)
|
|
18
|
+
if (HttpServerResponse.isServerResponse(raw)) {
|
|
19
|
+
return raw
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
switch (route.media) {
|
|
18
23
|
case "text/plain":
|
|
19
24
|
return HttpServerResponse.text(raw as string)
|
|
20
25
|
|
|
21
26
|
case "text/html":
|
|
22
|
-
if (Route.
|
|
27
|
+
if (Route.isGenericJsxObject(raw)) {
|
|
23
28
|
return HttpServerResponse.html(HyperHtml.renderToString(raw))
|
|
24
29
|
}
|
|
25
30
|
return HttpServerResponse.html(raw as string)
|
|
@@ -29,9 +34,6 @@ export function render<E, R>(
|
|
|
29
34
|
|
|
30
35
|
case "*":
|
|
31
36
|
default:
|
|
32
|
-
if (HttpServerResponse.isServerResponse(raw)) {
|
|
33
|
-
return raw
|
|
34
|
-
}
|
|
35
37
|
return HttpServerResponse.text(String(raw))
|
|
36
38
|
}
|
|
37
39
|
})
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import * as t from "bun:test"
|
|
2
|
+
import * as Effect from "effect/Effect"
|
|
3
|
+
import * as Route from "./Route.ts"
|
|
4
|
+
import * as Router from "./Router.ts"
|
|
5
|
+
|
|
6
|
+
t.describe("Router", () => {
|
|
7
|
+
t.describe("mount", () => {
|
|
8
|
+
t.test("creates router with single route", () => {
|
|
9
|
+
const router = Router.mount("/hello", Route.text("Hello World"))
|
|
10
|
+
|
|
11
|
+
t.expect(router.entries).toHaveLength(1)
|
|
12
|
+
t.expect(router.entries[0].path).toBe("/hello")
|
|
13
|
+
t.expect(router.entries[0].route.set).toHaveLength(1)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
t.test("chains multiple routes", () => {
|
|
17
|
+
const router = Router
|
|
18
|
+
.mount("/hello", Route.text("Hello"))
|
|
19
|
+
.mount("/world", Route.text("World"))
|
|
20
|
+
|
|
21
|
+
t.expect(router.entries).toHaveLength(2)
|
|
22
|
+
t.expect(router.entries[0].path).toBe("/hello")
|
|
23
|
+
t.expect(router.entries[1].path).toBe("/world")
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
t.test("merges routes at same path", () => {
|
|
27
|
+
const router = Router
|
|
28
|
+
.mount("/api", Route.get(Route.json({ method: "get" })))
|
|
29
|
+
.mount("/api", Route.post(Route.json({ method: "post" })))
|
|
30
|
+
|
|
31
|
+
t.expect(router.entries).toHaveLength(1)
|
|
32
|
+
t.expect(router.entries[0].path).toBe("/api")
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
t.describe("mounts", () => {
|
|
37
|
+
t.test("exposes mounted routes as Record", () => {
|
|
38
|
+
const router = Router
|
|
39
|
+
.mount("/hello", Route.text("Hello"))
|
|
40
|
+
.mount("/world", Route.text("World"))
|
|
41
|
+
|
|
42
|
+
t.expect(router.mounts["/hello"]).toBeDefined()
|
|
43
|
+
t.expect(router.mounts["/world"]).toBeDefined()
|
|
44
|
+
t.expect(router.mounts["/hello"].set).toHaveLength(1)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
t.test("mounts contain routes with layers applied", async () => {
|
|
48
|
+
const layer = Route.layer(
|
|
49
|
+
Route.html(function*(c) {
|
|
50
|
+
const inner = yield* c.next()
|
|
51
|
+
return `<wrap>${inner}</wrap>`
|
|
52
|
+
}),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const router = Router
|
|
56
|
+
.use(layer)
|
|
57
|
+
.mount("/page", Route.html(Effect.succeed("content")))
|
|
58
|
+
|
|
59
|
+
const mountedRoute = router.mounts["/page"]
|
|
60
|
+
t.expect(mountedRoute).toBeDefined()
|
|
61
|
+
t.expect(mountedRoute.set).toHaveLength(1)
|
|
62
|
+
|
|
63
|
+
const route = mountedRoute.set[0]
|
|
64
|
+
const mockContext: Route.RouteContext = {
|
|
65
|
+
request: {} as any,
|
|
66
|
+
url: new URL("http://localhost/page"),
|
|
67
|
+
slots: {},
|
|
68
|
+
next: () => Effect.void,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const result = await Effect.runPromise(
|
|
72
|
+
route.handler(mockContext) as Effect.Effect<unknown>,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
t.expect(result).toBe("<wrap>content</wrap>")
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
t.describe("use", () => {
|
|
80
|
+
t.test("adds global layer", () => {
|
|
81
|
+
const layer = Route.layer(
|
|
82
|
+
Route.html(function*(c) {
|
|
83
|
+
const inner = yield* c.next()
|
|
84
|
+
return `<html><body>${inner}</body></html>`
|
|
85
|
+
}),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const router = Router.use(layer)
|
|
89
|
+
|
|
90
|
+
t.expect(router.globalLayers).toHaveLength(1)
|
|
91
|
+
t.expect(router.entries).toHaveLength(0)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
t.test("applies layer to subsequently mounted routes", () => {
|
|
95
|
+
const layer = Route.layer(
|
|
96
|
+
Route.html(function*(c) {
|
|
97
|
+
const inner = yield* c.next()
|
|
98
|
+
return `<html><body>${inner}</body></html>`
|
|
99
|
+
}),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
const router = Router
|
|
103
|
+
.use(layer)
|
|
104
|
+
.mount("/", Route.text("Hello world!"))
|
|
105
|
+
|
|
106
|
+
t.expect(router.globalLayers).toHaveLength(1)
|
|
107
|
+
t.expect(router.entries).toHaveLength(1)
|
|
108
|
+
t.expect(router.entries[0].layers).toHaveLength(1)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
t.test("layer only applies to routes mounted after use()", async () => {
|
|
112
|
+
const layer = Route.layer(
|
|
113
|
+
Route.html(function*(c) {
|
|
114
|
+
const inner = yield* c.next()
|
|
115
|
+
return `<wrap>${inner}</wrap>`
|
|
116
|
+
}),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const router = Router
|
|
120
|
+
.mount("/before", Route.html(Effect.succeed("before-content")))
|
|
121
|
+
.use(layer)
|
|
122
|
+
.mount("/after", Route.html(Effect.succeed("after-content")))
|
|
123
|
+
|
|
124
|
+
const mockContext = (path: string): Route.RouteContext => ({
|
|
125
|
+
request: {} as any,
|
|
126
|
+
url: new URL(`http://localhost${path}`),
|
|
127
|
+
slots: {},
|
|
128
|
+
next: () => Effect.void,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const beforeRoute = router.mounts["/before"].set[0]
|
|
132
|
+
const afterRoute = router.mounts["/after"].set[0]
|
|
133
|
+
|
|
134
|
+
const beforeResult = await Effect.runPromise(
|
|
135
|
+
beforeRoute.handler(mockContext("/before")) as Effect.Effect<unknown>,
|
|
136
|
+
)
|
|
137
|
+
const afterResult = await Effect.runPromise(
|
|
138
|
+
afterRoute.handler(mockContext("/after")) as Effect.Effect<unknown>,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
t.expect(beforeResult).toBe("before-content")
|
|
142
|
+
t.expect(afterResult).toBe("<wrap>after-content</wrap>")
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
t.describe("layer application - runtime behavior", () => {
|
|
147
|
+
t.test("layer handler wraps route handler", async () => {
|
|
148
|
+
const layer = Route.layer(
|
|
149
|
+
Route.html(function*(c) {
|
|
150
|
+
const inner = yield* c.next()
|
|
151
|
+
return `<wrap>${inner}</wrap>`
|
|
152
|
+
}),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
const router = Router
|
|
156
|
+
.use(layer)
|
|
157
|
+
.mount("/page", Route.html(Effect.succeed("content")))
|
|
158
|
+
|
|
159
|
+
const mountedRoute = router.mounts["/page"]
|
|
160
|
+
const route = mountedRoute.set[0]
|
|
161
|
+
|
|
162
|
+
const mockContext: Route.RouteContext = {
|
|
163
|
+
request: {} as any,
|
|
164
|
+
url: new URL("http://localhost/page"),
|
|
165
|
+
slots: {},
|
|
166
|
+
next: () => Effect.succeed("unused"),
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const result = await Effect.runPromise(
|
|
170
|
+
route.handler(mockContext) as Effect.Effect<unknown>,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
t.expect(result).toBe("<wrap>content</wrap>")
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
t.test("multiple layers are applied in order", async () => {
|
|
177
|
+
const outerLayer = Route.layer(
|
|
178
|
+
Route.html(function*(c) {
|
|
179
|
+
const inner = yield* c.next()
|
|
180
|
+
return `<outer>${inner}</outer>`
|
|
181
|
+
}),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
const innerLayer = Route.layer(
|
|
185
|
+
Route.html(function*(c) {
|
|
186
|
+
const inner = yield* c.next()
|
|
187
|
+
return `<inner>${inner}</inner>`
|
|
188
|
+
}),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
const router = Router
|
|
192
|
+
.use(outerLayer)
|
|
193
|
+
.use(innerLayer)
|
|
194
|
+
.mount("/page", Route.html(Effect.succeed("content")))
|
|
195
|
+
|
|
196
|
+
const mountedRoute = router.mounts["/page"]
|
|
197
|
+
const route = mountedRoute.set[0]
|
|
198
|
+
|
|
199
|
+
const mockContext: Route.RouteContext = {
|
|
200
|
+
request: {} as any,
|
|
201
|
+
url: new URL("http://localhost/page"),
|
|
202
|
+
slots: {},
|
|
203
|
+
next: () => Effect.succeed("unused"),
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const result = await Effect.runPromise(
|
|
207
|
+
route.handler(mockContext) as Effect.Effect<unknown>,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
t.expect(result).toBe("<outer><inner>content</inner></outer>")
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
t.test("layer only applies to matching media type", async () => {
|
|
214
|
+
const htmlLayer = Route.layer(
|
|
215
|
+
Route.html(function*(c) {
|
|
216
|
+
const inner = yield* c.next()
|
|
217
|
+
return `<wrap>${inner}</wrap>`
|
|
218
|
+
}),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
const router = Router
|
|
222
|
+
.use(htmlLayer)
|
|
223
|
+
.mount("/api", Route.json({ data: "value" }))
|
|
224
|
+
|
|
225
|
+
const mountedRoute = router.mounts["/api"]
|
|
226
|
+
const route = mountedRoute.set[0]
|
|
227
|
+
|
|
228
|
+
const mockContext: Route.RouteContext = {
|
|
229
|
+
request: {} as any,
|
|
230
|
+
url: new URL("http://localhost/api"),
|
|
231
|
+
slots: {},
|
|
232
|
+
next: () => Effect.succeed("unused"),
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const result = await Effect.runPromise(
|
|
236
|
+
route.handler(mockContext) as Effect.Effect<unknown>,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
t.expect(result).toEqual({ data: "value" })
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
t.test("route without layer is not wrapped", async () => {
|
|
243
|
+
const router = Router.mount("/hello", Route.text("Hello"))
|
|
244
|
+
|
|
245
|
+
const mountedRoute = router.mounts["/hello"]
|
|
246
|
+
const route = mountedRoute.set[0]
|
|
247
|
+
|
|
248
|
+
const mockContext: Route.RouteContext = {
|
|
249
|
+
request: {} as any,
|
|
250
|
+
url: new URL("http://localhost/hello"),
|
|
251
|
+
slots: {},
|
|
252
|
+
next: () => Effect.succeed("unused"),
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const result = await Effect.runPromise(
|
|
256
|
+
route.handler(mockContext) as Effect.Effect<unknown>,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
t.expect(result).toBe("Hello")
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
t.describe("type inference", () => {
|
|
264
|
+
t.test("infers never for routes without requirements", () => {
|
|
265
|
+
const router = Router.mount("/hello", Route.text("Hello"))
|
|
266
|
+
|
|
267
|
+
type RouterError = Router.RouterBuilder.Error<typeof router>
|
|
268
|
+
type RouterContext = Router.RouterBuilder.Context<typeof router>
|
|
269
|
+
|
|
270
|
+
const _checkError: RouterError = undefined as never
|
|
271
|
+
const _checkContext: RouterContext = undefined as never
|
|
272
|
+
|
|
273
|
+
t.expect(true).toBe(true)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
t.test("infers error type from route handler", () => {
|
|
277
|
+
class MyError {
|
|
278
|
+
readonly _tag = "MyError"
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const router = Router.mount(
|
|
282
|
+
"/fail",
|
|
283
|
+
Route.text(Effect.fail(new MyError())),
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
type RouterError = Router.RouterBuilder.Error<typeof router>
|
|
287
|
+
|
|
288
|
+
const _checkError: MyError extends RouterError ? true : false = true
|
|
289
|
+
|
|
290
|
+
t.expect(true).toBe(true)
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
t.test("infers context type from route handler", () => {
|
|
294
|
+
class MyService extends Effect.Tag("MyService")<
|
|
295
|
+
MyService,
|
|
296
|
+
{ getValue(): string }
|
|
297
|
+
>() {}
|
|
298
|
+
|
|
299
|
+
const router = Router.mount(
|
|
300
|
+
"/service",
|
|
301
|
+
Route.text(
|
|
302
|
+
Effect.gen(function*() {
|
|
303
|
+
const svc = yield* MyService
|
|
304
|
+
return svc.getValue()
|
|
305
|
+
}),
|
|
306
|
+
),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
type RouterContext = Router.RouterBuilder.Context<typeof router>
|
|
310
|
+
|
|
311
|
+
const _checkContext: MyService extends RouterContext ? true : false = true
|
|
312
|
+
|
|
313
|
+
t.expect(true).toBe(true)
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
t.test("unions error types from multiple routes", () => {
|
|
317
|
+
class ErrorA {
|
|
318
|
+
readonly _tag = "ErrorA"
|
|
319
|
+
}
|
|
320
|
+
class ErrorB {
|
|
321
|
+
readonly _tag = "ErrorB"
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const router = Router
|
|
325
|
+
.mount("/a", Route.text(Effect.fail(new ErrorA())))
|
|
326
|
+
.mount("/b", Route.text(Effect.fail(new ErrorB())))
|
|
327
|
+
|
|
328
|
+
type RouterError = Router.RouterBuilder.Error<typeof router>
|
|
329
|
+
|
|
330
|
+
const _checkA: ErrorA extends RouterError ? true : false = true
|
|
331
|
+
const _checkB: ErrorB extends RouterError ? true : false = true
|
|
332
|
+
|
|
333
|
+
t.expect(true).toBe(true)
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
t.test("unions context types from multiple routes", () => {
|
|
337
|
+
class ServiceA extends Effect.Tag("ServiceA")<
|
|
338
|
+
ServiceA,
|
|
339
|
+
{ getA(): string }
|
|
340
|
+
>() {}
|
|
341
|
+
class ServiceB extends Effect.Tag("ServiceB")<
|
|
342
|
+
ServiceB,
|
|
343
|
+
{ getB(): string }
|
|
344
|
+
>() {}
|
|
345
|
+
|
|
346
|
+
const router = Router
|
|
347
|
+
.mount(
|
|
348
|
+
"/a",
|
|
349
|
+
Route.text(
|
|
350
|
+
Effect.gen(function*() {
|
|
351
|
+
const svc = yield* ServiceA
|
|
352
|
+
return svc.getA()
|
|
353
|
+
}),
|
|
354
|
+
),
|
|
355
|
+
)
|
|
356
|
+
.mount(
|
|
357
|
+
"/b",
|
|
358
|
+
Route.text(
|
|
359
|
+
Effect.gen(function*() {
|
|
360
|
+
const svc = yield* ServiceB
|
|
361
|
+
return svc.getB()
|
|
362
|
+
}),
|
|
363
|
+
),
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
type RouterContext = Router.RouterBuilder.Context<typeof router>
|
|
367
|
+
|
|
368
|
+
const _checkA: ServiceA extends RouterContext ? true : false = true
|
|
369
|
+
const _checkB: ServiceB extends RouterContext ? true : false = true
|
|
370
|
+
|
|
371
|
+
t.expect(true).toBe(true)
|
|
372
|
+
})
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
t.describe("fromManifest", () => {
|
|
376
|
+
t.test("loads routes from manifest", async () => {
|
|
377
|
+
const manifest: Router.RouterManifest = {
|
|
378
|
+
routes: [
|
|
379
|
+
{
|
|
380
|
+
path: "/test",
|
|
381
|
+
load: () => Promise.resolve({ default: Route.text("Test") }),
|
|
382
|
+
},
|
|
383
|
+
],
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const router = await Effect.runPromise(Router.fromManifest(manifest))
|
|
387
|
+
|
|
388
|
+
t.expect(router.entries).toHaveLength(1)
|
|
389
|
+
t.expect(router.entries[0].path).toBe("/test")
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
t.test("loads layers from manifest", async () => {
|
|
393
|
+
const layer = Route.layer(
|
|
394
|
+
Route.html(function*(c) {
|
|
395
|
+
const inner = yield* c.next()
|
|
396
|
+
return `<wrap>${inner}</wrap>`
|
|
397
|
+
}),
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
const manifest: Router.RouterManifest = {
|
|
401
|
+
routes: [
|
|
402
|
+
{
|
|
403
|
+
path: "/test",
|
|
404
|
+
load: () => Promise.resolve({ default: Route.text("Test") }),
|
|
405
|
+
layers: [() => Promise.resolve({ default: layer })],
|
|
406
|
+
},
|
|
407
|
+
],
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const router = await Effect.runPromise(Router.fromManifest(manifest))
|
|
411
|
+
|
|
412
|
+
t.expect(router.entries).toHaveLength(1)
|
|
413
|
+
t.expect(router.entries[0].layers).toHaveLength(1)
|
|
414
|
+
})
|
|
415
|
+
})
|
|
416
|
+
})
|