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/bun/BunRoute.test.ts
CHANGED
|
@@ -1,69 +1,119 @@
|
|
|
1
|
+
import * as HttpMiddleware from "@effect/platform/HttpMiddleware"
|
|
2
|
+
import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
|
|
1
3
|
import type { HTMLBundle } from "bun"
|
|
2
4
|
import * as t from "bun:test"
|
|
3
5
|
import * as Effect from "effect/Effect"
|
|
6
|
+
import * as Layer from "effect/Layer"
|
|
4
7
|
import * as Route from "../Route.ts"
|
|
5
|
-
import
|
|
8
|
+
import * as Router from "../Router.ts"
|
|
9
|
+
import * as BunHttpServer from "./BunHttpServer.ts"
|
|
6
10
|
import * as BunRoute from "./BunRoute.ts"
|
|
7
11
|
|
|
8
|
-
t.describe(`${BunRoute.loadBundle.name}`, () => {
|
|
9
|
-
t.test("creates BunRoute from HTMLBundle", () => {
|
|
10
|
-
const mockBundle = { index: "index.html" } as HTMLBundle
|
|
11
|
-
const bunRoute = BunRoute.loadBundle(() => Promise.resolve(mockBundle))
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
t.expect(
|
|
17
|
-
t.expect(bunRoute.media as string).toBe("text/html")
|
|
13
|
+
t.describe(`${BunRoute.validateBunPattern.name}`, () => {
|
|
14
|
+
t.test("allows exact paths", () => {
|
|
15
|
+
const result = BunRoute.validateBunPattern("/users")
|
|
16
|
+
t.expect(result._tag).toBe("None")
|
|
18
17
|
})
|
|
19
18
|
|
|
20
|
-
t.test("
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
Promise.resolve({ default: mockBundle })
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
const loaded = await bunRoute.load()
|
|
27
|
-
t.expect(loaded).toBe(mockBundle)
|
|
19
|
+
t.test("allows full-segment params", () => {
|
|
20
|
+
const result = BunRoute.validateBunPattern("/users/[id]")
|
|
21
|
+
t.expect(result._tag).toBe("None")
|
|
28
22
|
})
|
|
29
23
|
|
|
30
|
-
t.test("
|
|
31
|
-
const
|
|
32
|
-
|
|
24
|
+
t.test("allows rest params", () => {
|
|
25
|
+
const result = BunRoute.validateBunPattern("/docs/[...path]")
|
|
26
|
+
t.expect(result._tag).toBe("None")
|
|
27
|
+
})
|
|
33
28
|
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
t.test("rejects prefixed params", () => {
|
|
30
|
+
const result = BunRoute.validateBunPattern("/users/pk_[id]")
|
|
31
|
+
t.expect(result._tag).toBe("Some")
|
|
32
|
+
if (result._tag === "Some") {
|
|
33
|
+
t.expect(result.value.reason).toBe("UnsupportedPattern")
|
|
34
|
+
t.expect(result.value.pattern).toBe("/users/pk_[id]")
|
|
35
|
+
}
|
|
36
36
|
})
|
|
37
|
-
})
|
|
38
37
|
|
|
39
|
-
t.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
t.test("rejects suffixed params", () => {
|
|
39
|
+
const result = BunRoute.validateBunPattern("/users/[id]_details")
|
|
40
|
+
t.expect(result._tag).toBe("Some")
|
|
41
|
+
if (result._tag === "Some") {
|
|
42
|
+
t.expect(result.value.reason).toBe("UnsupportedPattern")
|
|
43
|
+
}
|
|
44
|
+
})
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
t.test("rejects dot suffix on params", () => {
|
|
47
|
+
const result = BunRoute.validateBunPattern("/api/[id].json")
|
|
48
|
+
t.expect(result._tag).toBe("Some")
|
|
49
|
+
if (result._tag === "Some") {
|
|
50
|
+
t.expect(result.value.reason).toBe("UnsupportedPattern")
|
|
51
|
+
}
|
|
45
52
|
})
|
|
46
53
|
|
|
47
|
-
t.test("
|
|
48
|
-
const
|
|
54
|
+
t.test("rejects tilde suffix on params", () => {
|
|
55
|
+
const result = BunRoute.validateBunPattern("/api/[id]~test")
|
|
56
|
+
t.expect(result._tag).toBe("Some")
|
|
57
|
+
if (result._tag === "Some") {
|
|
58
|
+
t.expect(result.value.reason).toBe("UnsupportedPattern")
|
|
59
|
+
}
|
|
60
|
+
})
|
|
49
61
|
|
|
50
|
-
|
|
62
|
+
t.test("allows optional params (implemented via two patterns)", () => {
|
|
63
|
+
const result = BunRoute.validateBunPattern("/users/[[id]]")
|
|
64
|
+
t.expect(result._tag).toBe("None")
|
|
51
65
|
})
|
|
52
66
|
|
|
53
|
-
t.test("
|
|
54
|
-
|
|
55
|
-
t.expect(
|
|
56
|
-
t.expect(BunRoute.isBunRoute({})).toBe(false)
|
|
57
|
-
t.expect(BunRoute.isBunRoute("string")).toBe(false)
|
|
67
|
+
t.test("allows optional rest params (implemented via two patterns)", () => {
|
|
68
|
+
const result = BunRoute.validateBunPattern("/docs/[[...path]]")
|
|
69
|
+
t.expect(result._tag).toBe("None")
|
|
58
70
|
})
|
|
59
71
|
})
|
|
60
72
|
|
|
61
73
|
t.describe(`${BunRoute.routesFromRouter.name}`, () => {
|
|
74
|
+
t.test("fails with RouterError for unsupported patterns", async () => {
|
|
75
|
+
const result = await Effect.runPromise(
|
|
76
|
+
BunRoute
|
|
77
|
+
.routesFromRouter(
|
|
78
|
+
Router.mount("/users/pk_[id]", Route.text("user")),
|
|
79
|
+
)
|
|
80
|
+
.pipe(
|
|
81
|
+
Effect.either,
|
|
82
|
+
Effect.provide(BunHttpServer.layer({ port: 0 })),
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
t.expect(result._tag).toBe("Left")
|
|
87
|
+
if (result._tag === "Left") {
|
|
88
|
+
t.expect(result.left._tag).toBe("RouterError")
|
|
89
|
+
t.expect(result.left.reason).toBe("UnsupportedPattern")
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
t.it(
|
|
94
|
+
"converts text route to fetch handler",
|
|
95
|
+
() =>
|
|
96
|
+
Effect.runPromise(
|
|
97
|
+
Effect
|
|
98
|
+
.gen(function*() {
|
|
99
|
+
const bunServer = yield* BunHttpServer.BunServer
|
|
100
|
+
return 23
|
|
101
|
+
})
|
|
102
|
+
.pipe(
|
|
103
|
+
Effect.provide(
|
|
104
|
+
Layer.mergeAll(
|
|
105
|
+
BunHttpServer.layer({
|
|
106
|
+
port: 0,
|
|
107
|
+
}),
|
|
108
|
+
),
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
|
|
62
114
|
t.test("converts text route to fetch handler", async () => {
|
|
63
115
|
const fetch = await makeFetch(
|
|
64
|
-
|
|
65
|
-
{ path: "/hello", routes: Route.text(Effect.succeed("Hello World")) },
|
|
66
|
-
]),
|
|
116
|
+
Router.mount("/hello", Route.text("Hello World")),
|
|
67
117
|
)
|
|
68
118
|
|
|
69
119
|
const response = await fetch("/hello")
|
|
@@ -74,12 +124,7 @@ t.describe(`${BunRoute.routesFromRouter.name}`, () => {
|
|
|
74
124
|
|
|
75
125
|
t.test("converts json route to fetch handler", async () => {
|
|
76
126
|
const fetch = await makeFetch(
|
|
77
|
-
|
|
78
|
-
{
|
|
79
|
-
path: "/api/data",
|
|
80
|
-
routes: Route.json(Effect.succeed({ message: "ok", count: 42 })),
|
|
81
|
-
},
|
|
82
|
-
]),
|
|
127
|
+
Router.mount("/api/data", Route.json({ message: "ok", count: 42 })),
|
|
83
128
|
)
|
|
84
129
|
|
|
85
130
|
const response = await fetch("/api/data")
|
|
@@ -90,9 +135,7 @@ t.describe(`${BunRoute.routesFromRouter.name}`, () => {
|
|
|
90
135
|
|
|
91
136
|
t.test("converts html route to fetch handler", async () => {
|
|
92
137
|
const fetch = await makeFetch(
|
|
93
|
-
|
|
94
|
-
{ path: "/page", routes: Route.html(Effect.succeed("<h1>Title</h1>")) },
|
|
95
|
-
]),
|
|
138
|
+
Router.mount("/page", Route.html(Effect.succeed("<h1>Title</h1>"))),
|
|
96
139
|
)
|
|
97
140
|
|
|
98
141
|
const response = await fetch("/page")
|
|
@@ -103,14 +146,12 @@ t.describe(`${BunRoute.routesFromRouter.name}`, () => {
|
|
|
103
146
|
|
|
104
147
|
t.test("handles method-specific routes", async () => {
|
|
105
148
|
const fetch = await makeFetch(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
},
|
|
113
|
-
]),
|
|
149
|
+
Router.mount(
|
|
150
|
+
"/users",
|
|
151
|
+
Route.get(Route.json({ users: [] })).post(
|
|
152
|
+
Route.json({ created: true }),
|
|
153
|
+
),
|
|
154
|
+
),
|
|
114
155
|
)
|
|
115
156
|
|
|
116
157
|
const getResponse = await fetch("/users")
|
|
@@ -121,16 +162,10 @@ t.describe(`${BunRoute.routesFromRouter.name}`, () => {
|
|
|
121
162
|
})
|
|
122
163
|
|
|
123
164
|
t.test("converts path syntax to Bun format", async () => {
|
|
124
|
-
const routes = await
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
{
|
|
129
|
-
path: "/docs/[...path]",
|
|
130
|
-
routes: Route.text(Effect.succeed("docs")),
|
|
131
|
-
},
|
|
132
|
-
]),
|
|
133
|
-
),
|
|
165
|
+
const routes = await makeBunRoutes(
|
|
166
|
+
Router
|
|
167
|
+
.mount("/users/[id]", Route.text("user"))
|
|
168
|
+
.mount("/docs/[...path]", Route.text("docs")),
|
|
134
169
|
)
|
|
135
170
|
|
|
136
171
|
t.expect(routes["/users/:id"]).toBeDefined()
|
|
@@ -141,17 +176,16 @@ t.describe(`${BunRoute.routesFromRouter.name}`, () => {
|
|
|
141
176
|
|
|
142
177
|
t.test("creates proxy and internal routes for BunRoute", async () => {
|
|
143
178
|
const mockBundle = { index: "index.html" } as HTMLBundle
|
|
144
|
-
const bunRoute = BunRoute.
|
|
179
|
+
const bunRoute = BunRoute.html(() => Promise.resolve(mockBundle))
|
|
145
180
|
|
|
146
|
-
const routes = await
|
|
147
|
-
|
|
148
|
-
makeRouter([{ path: "/app", routes: bunRoute }]),
|
|
149
|
-
),
|
|
181
|
+
const routes = await makeBunRoutes(
|
|
182
|
+
Router.mount("/app", bunRoute),
|
|
150
183
|
)
|
|
151
184
|
|
|
152
185
|
const internalPath = Object.keys(routes).find((k) =>
|
|
153
|
-
k.includes("
|
|
186
|
+
k.includes(".BunRoute-")
|
|
154
187
|
)
|
|
188
|
+
|
|
155
189
|
t.expect(internalPath).toBeDefined()
|
|
156
190
|
t.expect(routes[internalPath!]).toBe(mockBundle)
|
|
157
191
|
t.expect(typeof routes["/app"]).toBe("function")
|
|
@@ -159,29 +193,18 @@ t.describe(`${BunRoute.routesFromRouter.name}`, () => {
|
|
|
159
193
|
|
|
160
194
|
t.test("handles mixed BunRoute and regular routes", async () => {
|
|
161
195
|
const mockBundle = { index: "index.html" } as HTMLBundle
|
|
162
|
-
const bunRoute = BunRoute.
|
|
163
|
-
|
|
164
|
-
const routes = await
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
path: "/app",
|
|
169
|
-
load: () => Promise.resolve({ default: bunRoute }),
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
path: "/api/health",
|
|
173
|
-
load: () =>
|
|
174
|
-
Promise.resolve({
|
|
175
|
-
default: Route.json(Effect.succeed({ ok: true })),
|
|
176
|
-
}),
|
|
177
|
-
},
|
|
178
|
-
],
|
|
179
|
-
}),
|
|
196
|
+
const bunRoute = BunRoute.html(() => Promise.resolve(mockBundle))
|
|
197
|
+
|
|
198
|
+
const routes = await makeBunRoutes(
|
|
199
|
+
Router
|
|
200
|
+
.mount("/app", bunRoute)
|
|
201
|
+
.mount("/api/health", Route.json({ ok: true })),
|
|
180
202
|
)
|
|
181
203
|
|
|
182
204
|
const internalPath = Object.keys(routes).find((k) =>
|
|
183
|
-
k.includes("
|
|
205
|
+
k.includes(".BunRoute-")
|
|
184
206
|
)
|
|
207
|
+
|
|
185
208
|
t.expect(internalPath).toBeDefined()
|
|
186
209
|
t.expect(routes[internalPath!]).toBe(mockBundle)
|
|
187
210
|
t.expect(typeof routes["/app"]).toBe("function")
|
|
@@ -191,15 +214,13 @@ t.describe(`${BunRoute.routesFromRouter.name}`, () => {
|
|
|
191
214
|
|
|
192
215
|
t.test("groups multiple methods under same path", async () => {
|
|
193
216
|
const fetch = await makeFetch(
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
},
|
|
202
|
-
]),
|
|
217
|
+
Router.mount(
|
|
218
|
+
"/resource",
|
|
219
|
+
Route
|
|
220
|
+
.get(Route.text("get"))
|
|
221
|
+
.post(Route.text("post"))
|
|
222
|
+
.delete(Route.text("delete")),
|
|
223
|
+
),
|
|
203
224
|
)
|
|
204
225
|
|
|
205
226
|
const getRes = await fetch("/resource")
|
|
@@ -215,10 +236,7 @@ t.describe(`${BunRoute.routesFromRouter.name}`, () => {
|
|
|
215
236
|
t.describe("fetch handler Response", () => {
|
|
216
237
|
t.test("returns Response instance", async () => {
|
|
217
238
|
const fetch = await makeFetch(
|
|
218
|
-
|
|
219
|
-
path: "/test",
|
|
220
|
-
routes: Route.text(Effect.succeed("test")),
|
|
221
|
-
}]),
|
|
239
|
+
Router.mount("/test", Route.text("test")),
|
|
222
240
|
)
|
|
223
241
|
|
|
224
242
|
const response = await fetch("/test")
|
|
@@ -228,10 +246,7 @@ t.describe("fetch handler Response", () => {
|
|
|
228
246
|
|
|
229
247
|
t.test("text response has correct content-type", async () => {
|
|
230
248
|
const fetch = await makeFetch(
|
|
231
|
-
|
|
232
|
-
path: "/text",
|
|
233
|
-
routes: Route.text(Effect.succeed("hello")),
|
|
234
|
-
}]),
|
|
249
|
+
Router.mount("/text", Route.text("hello")),
|
|
235
250
|
)
|
|
236
251
|
|
|
237
252
|
const response = await fetch("/text")
|
|
@@ -241,10 +256,7 @@ t.describe("fetch handler Response", () => {
|
|
|
241
256
|
|
|
242
257
|
t.test("json response has correct content-type", async () => {
|
|
243
258
|
const fetch = await makeFetch(
|
|
244
|
-
|
|
245
|
-
path: "/json",
|
|
246
|
-
routes: Route.json(Effect.succeed({ data: 1 })),
|
|
247
|
-
}]),
|
|
259
|
+
Router.mount("/json", Route.json({ data: 1 })),
|
|
248
260
|
)
|
|
249
261
|
|
|
250
262
|
const response = await fetch("/json")
|
|
@@ -254,10 +266,7 @@ t.describe("fetch handler Response", () => {
|
|
|
254
266
|
|
|
255
267
|
t.test("html response has correct content-type", async () => {
|
|
256
268
|
const fetch = await makeFetch(
|
|
257
|
-
|
|
258
|
-
path: "/html",
|
|
259
|
-
routes: Route.html(Effect.succeed("<p>hi</p>")),
|
|
260
|
-
}]),
|
|
269
|
+
Router.mount("/html", Route.html(Effect.succeed("<p>hi</p>"))),
|
|
261
270
|
)
|
|
262
271
|
|
|
263
272
|
const response = await fetch("/html")
|
|
@@ -267,10 +276,7 @@ t.describe("fetch handler Response", () => {
|
|
|
267
276
|
|
|
268
277
|
t.test("response body is readable", async () => {
|
|
269
278
|
const fetch = await makeFetch(
|
|
270
|
-
|
|
271
|
-
path: "/body",
|
|
272
|
-
routes: Route.text(Effect.succeed("readable body")),
|
|
273
|
-
}]),
|
|
279
|
+
Router.mount("/body", Route.text("readable body")),
|
|
274
280
|
)
|
|
275
281
|
|
|
276
282
|
const response = await fetch("/body")
|
|
@@ -283,7 +289,7 @@ t.describe("fetch handler Response", () => {
|
|
|
283
289
|
|
|
284
290
|
t.test("response ok is true for 200 status", async () => {
|
|
285
291
|
const fetch = await makeFetch(
|
|
286
|
-
|
|
292
|
+
Router.mount("/ok", Route.text("ok")),
|
|
287
293
|
)
|
|
288
294
|
|
|
289
295
|
const response = await fetch("/ok")
|
|
@@ -293,18 +299,174 @@ t.describe("fetch handler Response", () => {
|
|
|
293
299
|
})
|
|
294
300
|
})
|
|
295
301
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
302
|
+
t.describe("Route.layer httpMiddleware", () => {
|
|
303
|
+
t.test("applies middleware headers to child routes", async () => {
|
|
304
|
+
const addHeader = HttpMiddleware.make((app) =>
|
|
305
|
+
Effect.gen(function*() {
|
|
306
|
+
const response = yield* app
|
|
307
|
+
return HttpServerResponse.setHeader(response, "X-Layer-Applied", "true")
|
|
308
|
+
})
|
|
309
|
+
) as Route.HttpMiddlewareFunction
|
|
310
|
+
|
|
311
|
+
const router = Router
|
|
312
|
+
.use(Route.layer(Route.http(addHeader)))
|
|
313
|
+
.mount("/child", Route.text("child content"))
|
|
314
|
+
|
|
315
|
+
const fetch = await makeFetch(router)
|
|
316
|
+
const response = await fetch("/child")
|
|
317
|
+
|
|
318
|
+
t.expect(response.headers.get("X-Layer-Applied")).toBe("true")
|
|
319
|
+
t.expect(await response.text()).toBe("child content")
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
t.test("middleware only applies to children, not siblings", async () => {
|
|
323
|
+
const addHeader = HttpMiddleware.make((app) =>
|
|
324
|
+
Effect.gen(function*() {
|
|
325
|
+
const response = yield* app
|
|
326
|
+
return HttpServerResponse.setHeader(response, "X-Layer-Applied", "true")
|
|
327
|
+
})
|
|
328
|
+
) as Route.HttpMiddlewareFunction
|
|
329
|
+
|
|
330
|
+
const router = Router
|
|
331
|
+
.mount("/outside", Route.text("outside content"))
|
|
332
|
+
.use(Route.layer(Route.http(addHeader)))
|
|
333
|
+
.mount("/inside", Route.text("inside content"))
|
|
334
|
+
|
|
335
|
+
const fetch = await makeFetch(router)
|
|
336
|
+
|
|
337
|
+
const insideResponse = await fetch("/inside")
|
|
338
|
+
t.expect(insideResponse.headers.get("X-Layer-Applied")).toBe("true")
|
|
339
|
+
|
|
340
|
+
const outsideResponse = await fetch("/outside")
|
|
341
|
+
t.expect(outsideResponse.headers.get("X-Layer-Applied")).toBeNull()
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
t.test("multiple middleware are applied in order", async () => {
|
|
345
|
+
const addHeader1 = HttpMiddleware.make((app) =>
|
|
346
|
+
Effect.gen(function*() {
|
|
347
|
+
const response = yield* app
|
|
348
|
+
return HttpServerResponse.setHeader(response, "X-First", "1")
|
|
349
|
+
})
|
|
350
|
+
) as Route.HttpMiddlewareFunction
|
|
351
|
+
|
|
352
|
+
const addHeader2 = HttpMiddleware.make((app) =>
|
|
353
|
+
Effect.gen(function*() {
|
|
354
|
+
const response = yield* app
|
|
355
|
+
return HttpServerResponse.setHeader(response, "X-Second", "2")
|
|
356
|
+
})
|
|
357
|
+
) as Route.HttpMiddlewareFunction
|
|
358
|
+
|
|
359
|
+
const router = Router
|
|
360
|
+
.use(Route.layer(Route.http(addHeader1), Route.http(addHeader2)))
|
|
361
|
+
.mount("/test", Route.text("test"))
|
|
362
|
+
|
|
363
|
+
const fetch = await makeFetch(router)
|
|
364
|
+
const response = await fetch("/test")
|
|
365
|
+
|
|
366
|
+
t.expect(response.headers.get("X-First")).toBe("1")
|
|
367
|
+
t.expect(response.headers.get("X-Second")).toBe("2")
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
t.test("nested layers apply all middleware", async () => {
|
|
371
|
+
const outerHeader = HttpMiddleware.make((app) =>
|
|
372
|
+
Effect.gen(function*() {
|
|
373
|
+
const response = yield* app
|
|
374
|
+
return HttpServerResponse.setHeader(response, "X-Outer", "outer")
|
|
375
|
+
})
|
|
376
|
+
) as Route.HttpMiddlewareFunction
|
|
377
|
+
|
|
378
|
+
const innerHeader = HttpMiddleware.make((app) =>
|
|
379
|
+
Effect.gen(function*() {
|
|
380
|
+
const response = yield* app
|
|
381
|
+
return HttpServerResponse.setHeader(response, "X-Inner", "inner")
|
|
382
|
+
})
|
|
383
|
+
) as Route.HttpMiddlewareFunction
|
|
384
|
+
|
|
385
|
+
const router = Router
|
|
386
|
+
.use(Route.layer(Route.http(outerHeader)))
|
|
387
|
+
.use(Route.layer(Route.http(innerHeader)))
|
|
388
|
+
.mount("/nested", Route.text("nested"))
|
|
389
|
+
|
|
390
|
+
const fetch = await makeFetch(router)
|
|
391
|
+
const response = await fetch("/nested")
|
|
392
|
+
|
|
393
|
+
t.expect(response.headers.get("X-Outer")).toBe("outer")
|
|
394
|
+
t.expect(response.headers.get("X-Inner")).toBe("inner")
|
|
395
|
+
})
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
t.describe("BunRoute placeholder replacement", () => {
|
|
399
|
+
t.test("%yield% is replaced with nested route content via layer", async () => {
|
|
400
|
+
const layoutBundle = BunRoute.html(() =>
|
|
401
|
+
import("../../static/LayoutSlots.html")
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
const router = Router
|
|
405
|
+
.use(Route.layer(layoutBundle))
|
|
406
|
+
.mount("/page", Route.html(Effect.succeed("<p>Child Content</p>")))
|
|
407
|
+
|
|
408
|
+
await Effect.runPromise(
|
|
409
|
+
Effect
|
|
410
|
+
.gen(function*() {
|
|
411
|
+
const bunServer = yield* BunHttpServer.BunServer
|
|
412
|
+
const routes = yield* BunRoute.routesFromRouter(router)
|
|
413
|
+
bunServer.addRoutes(routes)
|
|
414
|
+
|
|
415
|
+
const baseUrl =
|
|
416
|
+
`http://${bunServer.server.hostname}:${bunServer.server.port}`
|
|
417
|
+
const response = yield* Effect.promise(() => fetch(`${baseUrl}/page`))
|
|
418
|
+
|
|
419
|
+
const html = yield* Effect.promise(() => response.text())
|
|
420
|
+
|
|
421
|
+
t.expect(html).toContain("<body>")
|
|
422
|
+
t.expect(html).toContain("<p>Child Content</p>")
|
|
423
|
+
t.expect(html).not.toContain("%yield%")
|
|
424
|
+
t.expect(html).not.toContain("%slots.")
|
|
425
|
+
})
|
|
426
|
+
.pipe(
|
|
427
|
+
Effect.scoped,
|
|
428
|
+
Effect.provide(BunHttpServer.layer({ port: 0 })),
|
|
429
|
+
),
|
|
430
|
+
)
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
t.test("%yield% and %slots% are replaced with empty when no nested content", async () => {
|
|
434
|
+
const layoutBundle = BunRoute.html(() =>
|
|
435
|
+
import("../../static/LayoutSlots.html")
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
const router = Router.mount("/layout", layoutBundle)
|
|
439
|
+
|
|
440
|
+
await Effect.runPromise(
|
|
441
|
+
Effect
|
|
442
|
+
.gen(function*() {
|
|
443
|
+
const bunServer = yield* BunHttpServer.BunServer
|
|
444
|
+
const routes = yield* BunRoute.routesFromRouter(router)
|
|
445
|
+
bunServer.addRoutes(routes)
|
|
446
|
+
|
|
447
|
+
const baseUrl =
|
|
448
|
+
`http://${bunServer.server.hostname}:${bunServer.server.port}`
|
|
449
|
+
const response = yield* Effect.promise(() =>
|
|
450
|
+
fetch(`${baseUrl}/layout`)
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
const html = yield* Effect.promise(() => response.text())
|
|
454
|
+
|
|
455
|
+
t.expect(html).toContain("<title></title>")
|
|
456
|
+
t.expect(html).toContain("<body>")
|
|
457
|
+
t.expect(html).not.toContain("%yield%")
|
|
458
|
+
t.expect(html).not.toContain("%slots.")
|
|
459
|
+
})
|
|
460
|
+
.pipe(
|
|
461
|
+
Effect.scoped,
|
|
462
|
+
Effect.provide(BunHttpServer.layer({ port: 0 })),
|
|
463
|
+
),
|
|
464
|
+
)
|
|
465
|
+
})
|
|
306
466
|
})
|
|
307
467
|
|
|
468
|
+
|
|
469
|
+
|
|
308
470
|
type FetchFn = (path: string, init?: { method?: string }) => Promise<Response>
|
|
309
471
|
|
|
310
472
|
type HandlerFn = (
|
|
@@ -312,8 +474,19 @@ type HandlerFn = (
|
|
|
312
474
|
server: unknown,
|
|
313
475
|
) => Response | Promise<Response>
|
|
314
476
|
|
|
315
|
-
|
|
316
|
-
|
|
477
|
+
|
|
478
|
+
async function makeBunRoutes(
|
|
479
|
+
router: Router.RouterBuilder.Any,
|
|
480
|
+
): Promise<BunRoute.BunRoutes> {
|
|
481
|
+
return Effect.runPromise(
|
|
482
|
+
BunRoute.routesFromRouter(router).pipe(
|
|
483
|
+
Effect.provide(BunHttpServer.layer({ port: 0 })),
|
|
484
|
+
),
|
|
485
|
+
)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function makeFetch(router: Router.RouterBuilder.Any): Promise<FetchFn> {
|
|
489
|
+
const routes = await makeBunRoutes(router)
|
|
317
490
|
const mockServer = {} as import("bun").Server<unknown>
|
|
318
491
|
|
|
319
492
|
return async (path, init) => {
|