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