effect-start 0.14.0 → 0.15.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 (81) hide show
  1. package/package.json +8 -9
  2. package/src/Commander.test.ts +507 -245
  3. package/src/ContentNegotiation.test.ts +500 -0
  4. package/src/ContentNegotiation.ts +535 -0
  5. package/src/FileRouter.ts +16 -12
  6. package/src/{FileRouterCodegen.test.ts → FileRouterCodegen.todo.ts} +384 -219
  7. package/src/FileRouterCodegen.ts +6 -6
  8. package/src/FileRouterPattern.test.ts +93 -62
  9. package/src/FileRouter_files.test.ts +5 -5
  10. package/src/FileRouter_path.test.ts +121 -69
  11. package/src/FileRouter_tree.test.ts +62 -56
  12. package/src/FileSystemExtra.test.ts +46 -30
  13. package/src/Http.test.ts +24 -0
  14. package/src/Http.ts +25 -0
  15. package/src/HttpAppExtra.test.ts +39 -20
  16. package/src/HttpAppExtra.ts +0 -1
  17. package/src/HttpUtils.test.ts +35 -18
  18. package/src/HttpUtils.ts +2 -0
  19. package/src/PathPattern.test.ts +648 -0
  20. package/src/PathPattern.ts +483 -0
  21. package/src/Route.ts +258 -1073
  22. package/src/RouteBody.test.ts +182 -0
  23. package/src/RouteBody.ts +106 -0
  24. package/src/RouteHook.test.ts +40 -0
  25. package/src/RouteHook.ts +105 -0
  26. package/src/RouteHttp.test.ts +443 -0
  27. package/src/RouteHttp.ts +219 -0
  28. package/src/RouteMount.test.ts +468 -0
  29. package/src/RouteMount.ts +313 -0
  30. package/src/RouteSchema.test.ts +81 -0
  31. package/src/RouteSchema.ts +44 -0
  32. package/src/RouteTree.test.ts +346 -0
  33. package/src/RouteTree.ts +165 -0
  34. package/src/RouteTrie.test.ts +322 -0
  35. package/src/RouteTrie.ts +224 -0
  36. package/src/RouterPattern.test.ts +569 -548
  37. package/src/RouterPattern.ts +7 -7
  38. package/src/Start.ts +3 -3
  39. package/src/TuplePathPattern.ts +64 -0
  40. package/src/Values.ts +16 -0
  41. package/src/bun/BunBundle.test.ts +36 -42
  42. package/src/bun/BunBundle.ts +2 -2
  43. package/src/bun/BunBundle_imports.test.ts +4 -6
  44. package/src/bun/BunHttpServer.test.ts +183 -6
  45. package/src/bun/BunHttpServer.ts +56 -32
  46. package/src/bun/BunHttpServer_web.ts +18 -6
  47. package/src/bun/BunImportTrackerPlugin.test.ts +3 -3
  48. package/src/bun/BunRoute.ts +29 -210
  49. package/src/{BundleHttp.test.ts → bundler/BundleHttp.test.ts} +34 -60
  50. package/src/{BundleHttp.ts → bundler/BundleHttp.ts} +1 -2
  51. package/src/client/index.ts +1 -1
  52. package/src/{Effect_HttpRouter.test.ts → effect/HttpRouter.test.ts} +69 -90
  53. package/src/experimental/EncryptedCookies.test.ts +125 -64
  54. package/src/experimental/SseHttpResponse.ts +0 -1
  55. package/src/hyper/Hyper.ts +89 -0
  56. package/src/{HyperHtml.test.ts → hyper/HyperHtml.test.ts} +13 -13
  57. package/src/{HyperHtml.ts → hyper/HyperHtml.ts} +2 -2
  58. package/src/{jsx.d.ts → hyper/jsx.d.ts} +1 -1
  59. package/src/index.ts +2 -4
  60. package/src/middlewares/BasicAuthMiddleware.test.ts +29 -19
  61. package/src/{NodeFileSystem.ts → node/FileSystem.ts} +6 -2
  62. package/src/testing/TestHttpClient.test.ts +26 -26
  63. package/src/testing/TestLogger.test.ts +27 -11
  64. package/src/x/datastar/Datastar.test.ts +47 -48
  65. package/src/x/datastar/Datastar.ts +1 -1
  66. package/src/x/tailwind/TailwindPlugin.test.ts +56 -58
  67. package/src/x/tailwind/plugin.ts +1 -1
  68. package/src/FileHttpRouter.test.ts +0 -239
  69. package/src/FileHttpRouter.ts +0 -194
  70. package/src/Hyper.ts +0 -194
  71. package/src/Route.test.ts +0 -1370
  72. package/src/RouteRender.ts +0 -40
  73. package/src/Router.test.ts +0 -375
  74. package/src/Router.ts +0 -255
  75. package/src/bun/BunRoute.test.ts +0 -480
  76. package/src/bun/BunRoute_bundles.test.ts +0 -219
  77. /package/src/{Bundle.ts → bundler/Bundle.ts} +0 -0
  78. /package/src/{BundleFiles.ts → bundler/BundleFiles.ts} +0 -0
  79. /package/src/{HyperNode.ts → hyper/HyperNode.ts} +0 -0
  80. /package/src/{jsx-runtime.ts → hyper/jsx-runtime.ts} +0 -0
  81. /package/src/{NodeUtils.ts → node/Utils.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import type * as Route from "./Route.ts"
1
+ export type RouterPattern = `/${string}`
2
2
 
3
3
  export type ParamDelimiter = "_" | "-" | "." | "," | ";" | "!" | "@" | "~"
4
4
  export type ParamPrefix = `${string}${ParamDelimiter}` | ""
@@ -206,7 +206,7 @@ function colonParamSegment(segment: Segment): string {
206
206
  * - `[[...param]]` → `/`, `/*`
207
207
  * - `pk_[id]` → `pk_:id`
208
208
  */
209
- export function toColon(path: Route.RoutePattern): string[] {
209
+ export function toColon(path: RouterPattern): string[] {
210
210
  return buildPaths(parse(path), colonParamSegment, "/*")
211
211
  }
212
212
 
@@ -221,7 +221,7 @@ export const toHono = toColon
221
221
  * - `[[...param]]` → `/`, `/*param`
222
222
  * - `pk_[id]` → `pk_:id`
223
223
  */
224
- export function toExpress(path: Route.RoutePattern): string[] {
224
+ export function toExpress(path: RouterPattern): string[] {
225
225
  const segments = parse(path)
226
226
  const optionalRestIndex = segments.findIndex(
227
227
  (s) => s._tag === "RestSegment" && s.optional,
@@ -291,7 +291,7 @@ export function toExpress(path: Route.RoutePattern): string[] {
291
291
  * - `[[...param]]` → `/`, `/*`
292
292
  * - `pk_[id]` → `pk_:id`
293
293
  */
294
- export function toEffect(path: Route.RoutePattern): string[] {
294
+ export function toEffect(path: RouterPattern): string[] {
295
295
  return buildPaths(parse(path), colonParamSegment, "/*")
296
296
  }
297
297
 
@@ -304,7 +304,7 @@ export function toEffect(path: Route.RoutePattern): string[] {
304
304
  * - `[[...param]]` → `:param*`
305
305
  * - `pk_[id]` → `pk_:id`
306
306
  */
307
- export function toURLPattern(path: Route.RoutePattern): string[] {
307
+ export function toURLPattern(path: RouterPattern): string[] {
308
308
  const segments = parse(path)
309
309
  const joined = segments
310
310
  .map((segment) => {
@@ -332,7 +332,7 @@ export function toURLPattern(path: Route.RoutePattern): string[] {
332
332
  * - `[[...param]]` → `/`, `$`
333
333
  * - `pk_[id]` → (not supported, emits `pk_$id`)
334
334
  */
335
- export function toRemix(path: Route.RoutePattern): string[] {
335
+ export function toRemix(path: RouterPattern): string[] {
336
336
  const segments = parse(path)
337
337
  const optionalRestIndex = segments.findIndex(
338
338
  (s) => s._tag === "RestSegment" && s.optional,
@@ -377,7 +377,7 @@ export function toRemix(path: Route.RoutePattern): string[] {
377
377
  * - `[[...param]]` → `/`, `/*` (two routes)
378
378
  * - `pk_[id]` → `pk_:id`
379
379
  */
380
- export function toBun(path: Route.RoutePattern): string[] {
380
+ export function toBun(path: RouterPattern): string[] {
381
381
  const segments = parse(path)
382
382
 
383
383
  const optionalIndex = segments.findIndex(
package/src/Start.ts CHANGED
@@ -8,7 +8,7 @@ import * as Function from "effect/Function"
8
8
  import * as Layer from "effect/Layer"
9
9
  import * as BunHttpServer from "./bun/BunHttpServer.ts"
10
10
  import * as BunRuntime from "./bun/BunRuntime.ts"
11
- import * as NodeFileSystem from "./NodeFileSystem.ts"
11
+ import * as NodeFileSystem from "./node/FileSystem.ts"
12
12
  import * as StartApp from "./StartApp.ts"
13
13
 
14
14
  export function layer<
@@ -45,13 +45,13 @@ export function serve<ROut, E>(
45
45
  )
46
46
 
47
47
  return Function.pipe(
48
- BunHttpServer.layerFileRouter(),
48
+ BunHttpServer.layerAuto(),
49
49
  HttpServer.withLogAddress,
50
50
  Layer.provide(appLayer),
51
51
  Layer.provide([
52
52
  FetchHttpClient.layer,
53
53
  HttpRouter.Default.Live,
54
- BunHttpServer.layerServer(),
54
+ BunHttpServer.layer(),
55
55
  NodeFileSystem.layer,
56
56
  StartApp.layer(),
57
57
  ]),
@@ -0,0 +1,64 @@
1
+ export type PathTuple = ReadonlyArray<
2
+ string | [string, string?, string?] | [[string]]
3
+ >
4
+
5
+ export function format(tuple: PathTuple): `/${string}` {
6
+ return "/" + tuple
7
+ .map((el) => {
8
+ if (typeof el === "string") return el
9
+ if (Array.isArray(el[0])) return "[[" + el[0][0] + "]]"
10
+ const [name, suffix, prefix] = el
11
+ return (prefix ?? "") + "[" + name + "]" + (suffix ?? "")
12
+ })
13
+ .join("/") as `/${string}`
14
+ }
15
+
16
+ export function toColon(tuple: PathTuple): string {
17
+ return "/" + tuple
18
+ .map((el) => {
19
+ if (typeof el === "string") return el
20
+ if (Array.isArray(el[0])) return "*"
21
+ const [name, suffix, prefix] = el
22
+ return (prefix ?? "") + ":" + name + (suffix ?? "")
23
+ })
24
+ .join("/")
25
+ }
26
+
27
+ export const toHono = toColon
28
+
29
+ export function toExpress(tuple: PathTuple): string {
30
+ return "/" + tuple
31
+ .map((el) => {
32
+ if (typeof el === "string") return el
33
+ if (Array.isArray(el[0])) return "*" + el[0][0]
34
+ const [name, suffix, prefix] = el
35
+ return (prefix ?? "") + ":" + name + (suffix ?? "")
36
+ })
37
+ .join("/")
38
+ }
39
+
40
+ export const toEffect = toColon
41
+
42
+ export function toURLPattern(tuple: PathTuple): string {
43
+ return "/" + tuple
44
+ .map((el) => {
45
+ if (typeof el === "string") return el
46
+ if (Array.isArray(el[0])) return ":" + el[0][0] + "+"
47
+ const [name, suffix, prefix] = el
48
+ return (prefix ?? "") + ":" + name + (suffix ?? "")
49
+ })
50
+ .join("/")
51
+ }
52
+
53
+ export function toRemix(tuple: PathTuple): string {
54
+ return "/" + tuple
55
+ .map((el) => {
56
+ if (typeof el === "string") return el
57
+ if (Array.isArray(el[0])) return "$"
58
+ const [name, suffix, prefix] = el
59
+ return (prefix ?? "") + "$" + name + (suffix ?? "")
60
+ })
61
+ .join("/")
62
+ }
63
+
64
+ export const toBun = toColon
package/src/Values.ts ADDED
@@ -0,0 +1,16 @@
1
+ type JsonPrimitives =
2
+ | string
3
+ | number
4
+ | boolean
5
+ | null
6
+
7
+ export type Json =
8
+ | JsonPrimitives
9
+ | Json[]
10
+ | {
11
+ [key: string]:
12
+ | Json
13
+ // undefined won't be included in JSON objects but this will allow
14
+ // to use Json type in functions that return object of multiple shapes
15
+ | undefined
16
+ }
@@ -1,17 +1,17 @@
1
1
  import * as HttpRouter from "@effect/platform/HttpRouter"
2
- import * as t from "bun:test"
2
+ import * as test from "bun:test"
3
3
  import * as Effect from "effect/Effect"
4
4
  import * as Layer from "effect/Layer"
5
5
  import * as NFS from "node:fs/promises"
6
6
  import * as NOS from "node:os"
7
7
  import * as NPath from "node:path"
8
- import * as Bundle from "../Bundle.ts"
9
- import * as BundleHttp from "../BundleHttp.ts"
8
+ import * as Bundle from "../bundler/Bundle.ts"
9
+ import * as BundleHttp from "../bundler/BundleHttp.ts"
10
10
  import * as TestHttpClient from "../testing/TestHttpClient.ts"
11
11
  import * as BunBundle from "./BunBundle.ts"
12
12
 
13
- t.describe("BunBundle manifest structure", () => {
14
- t.it("should generate manifest with inputs and outputs arrays", async () => {
13
+ test.describe("BunBundle manifest structure", () => {
14
+ test.it("should generate manifest with inputs and outputs arrays", async () => {
15
15
  const tmpDir = await NFS.mkdtemp(
16
16
  NPath.join(NOS.tmpdir(), "effect-start-test-"),
17
17
  )
@@ -41,43 +41,41 @@ export const greeting = "Hello World";`
41
41
  }),
42
42
  )
43
43
 
44
- t
44
+ test
45
45
  .expect(bundle.entrypoints)
46
46
  .toBeObject()
47
- t
47
+ test
48
48
  .expect(bundle.artifacts)
49
49
  .toBeArray()
50
-
51
- t
50
+ test
52
51
  .expect(Object.keys(bundle.entrypoints).length)
53
52
  .toBe(1)
54
- t
53
+ test
55
54
  .expect(bundle.artifacts.length)
56
55
  .toBe(3)
57
56
 
58
57
  const entrypointKeys = Object.keys(bundle.entrypoints)
59
58
  const firstEntrypoint = entrypointKeys[0]
60
59
 
61
- t
60
+ test
62
61
  .expect(firstEntrypoint)
63
62
  .toBeString()
64
- t
63
+ test
65
64
  .expect(bundle.entrypoints[firstEntrypoint])
66
65
  .toBeString()
67
66
 
68
67
  const firstArtifact = bundle.artifacts[0]
69
68
 
70
- t
69
+ test
71
70
  .expect(firstArtifact)
72
71
  .toHaveProperty("path")
73
- t
72
+ test
74
73
  .expect(firstArtifact)
75
74
  .toHaveProperty("type")
76
- t
75
+ test
77
76
  .expect(firstArtifact)
78
77
  .toHaveProperty("size")
79
-
80
- t
78
+ test
81
79
  .expect(firstArtifact.size)
82
80
  .toBeGreaterThan(0)
83
81
  } finally {
@@ -88,7 +86,7 @@ export const greeting = "Hello World";`
88
86
  }
89
87
  })
90
88
 
91
- t.it("should serve manifest via HTTP with correct structure", async () => {
89
+ test.it("should serve manifest via HTTP with correct structure", async () => {
92
90
  const tmpDir = await NFS.mkdtemp(
93
91
  NPath.join(NOS.tmpdir(), "effect-start-test-"),
94
92
  )
@@ -136,46 +134,44 @@ export const greeting = "Hello World";`
136
134
  ),
137
135
  )
138
136
 
139
- t
137
+ test
140
138
  .expect(result)
141
139
  .toHaveProperty("entrypoints")
142
- t
140
+ test
143
141
  .expect(result)
144
142
  .toHaveProperty("artifacts")
145
-
146
- t
143
+ test
147
144
  .expect(result.entrypoints)
148
145
  .toBeObject()
149
- t
146
+ test
150
147
  .expect(result.artifacts)
151
148
  .toBeArray()
152
-
153
- t
149
+ test
154
150
  .expect(Object.keys(result.entrypoints).length)
155
151
  .toBe(1)
156
- t
152
+ test
157
153
  .expect(result.artifacts.length)
158
154
  .toBe(3)
159
155
 
160
156
  const entrypointKeys = Object.keys(result.entrypoints)
161
157
  const firstKey = entrypointKeys[0]
162
158
 
163
- t
159
+ test
164
160
  .expect(firstKey)
165
161
  .toBeString()
166
- t
162
+ test
167
163
  .expect(result.entrypoints[firstKey])
168
164
  .toBeString()
169
165
 
170
166
  const artifact = result.artifacts[0]
171
167
 
172
- t
168
+ test
173
169
  .expect(artifact)
174
170
  .toHaveProperty("path")
175
- t
171
+ test
176
172
  .expect(artifact)
177
173
  .toHaveProperty("type")
178
- t
174
+ test
179
175
  .expect(artifact)
180
176
  .toHaveProperty("size")
181
177
  } finally {
@@ -186,7 +182,7 @@ export const greeting = "Hello World";`
186
182
  }
187
183
  })
188
184
 
189
- t.it("should resolve entrypoints to artifacts correctly", async () => {
185
+ test.it("should resolve entrypoints to artifacts correctly", async () => {
190
186
  const tmpDir = await NFS.mkdtemp(
191
187
  NPath.join(NOS.tmpdir(), "effect-start-test-"),
192
188
  )
@@ -210,18 +206,17 @@ export const greeting = "Hello World";`
210
206
  const expectedOutput = bundle.entrypoints[firstEntrypoint]
211
207
  const resolvedOutput = bundle.resolve(firstEntrypoint)
212
208
 
213
- t
209
+ test
214
210
  .expect(resolvedOutput)
215
211
  .toBe(expectedOutput)
216
212
 
217
213
  const artifact = bundle.getArtifact(resolvedOutput!)
218
214
 
219
- t
215
+ test
220
216
  .expect(artifact)
221
217
  .not
222
218
  .toBeNull()
223
-
224
- t
219
+ test
225
220
  .expect(artifact)
226
221
  .toBeTruthy()
227
222
  } finally {
@@ -232,7 +227,7 @@ export const greeting = "Hello World";`
232
227
  }
233
228
  })
234
229
 
235
- t.it("should include all artifact metadata", async () => {
230
+ test.it("should include all artifact metadata", async () => {
236
231
  const tmpDir = await NFS.mkdtemp(
237
232
  NPath.join(NOS.tmpdir(), "effect-start-test-"),
238
233
  )
@@ -251,17 +246,16 @@ export const greeting = "Hello World";`
251
246
 
252
247
  const artifact = bundle.artifacts[0]
253
248
 
254
- t
249
+ test
255
250
  .expect(artifact.path)
256
251
  .toBeString()
257
- t
252
+ test
258
253
  .expect(artifact.type)
259
254
  .toBeString()
260
- t
255
+ test
261
256
  .expect(artifact.size)
262
257
  .toBeNumber()
263
-
264
- t
258
+ test
265
259
  .expect(artifact.type)
266
260
  .toContain("javascript")
267
261
  } finally {
@@ -18,8 +18,8 @@ import * as NPath from "node:path"
18
18
  import type {
19
19
  BundleContext,
20
20
  BundleManifest,
21
- } from "../Bundle.ts"
22
- import * as Bundle from "../Bundle.ts"
21
+ } from "../bundler/Bundle.ts"
22
+ import * as Bundle from "../bundler/Bundle.ts"
23
23
  import * as FileSystemExtra from "../FileSystemExtra.ts"
24
24
  import { BunImportTrackerPlugin } from "./index.ts"
25
25
 
@@ -1,11 +1,11 @@
1
- import * as t from "bun:test"
1
+ import * as test from "bun:test"
2
2
  import { effectFn } from "../testing"
3
3
  import * as BunBundle from "./BunBundle.ts"
4
4
  import * as BunImportTrackerPlugin from "./BunImportTrackerPlugin.ts"
5
5
 
6
6
  const effect = effectFn()
7
7
 
8
- t.it("imports", () =>
8
+ test.it("imports", () =>
9
9
  effect(function*() {
10
10
  const importTracker = BunImportTrackerPlugin.make()
11
11
  yield* BunBundle.build({
@@ -22,10 +22,8 @@ t.it("imports", () =>
22
22
  e0,
23
23
  ] = importTracker.state.entries()
24
24
 
25
- t
26
- .expect(
27
- e0,
28
- )
25
+ test
26
+ .expect(e0)
29
27
  .toEqual([
30
28
  "src/bun/BunBundle_imports.test.ts",
31
29
  [
@@ -1,11 +1,13 @@
1
- import * as t from "bun:test"
1
+ import * as test from "bun:test"
2
2
  import * as Effect from "effect/Effect"
3
+ import * as Layer from "effect/Layer"
4
+ import * as Route from "../Route.ts"
3
5
  import * as BunHttpServer from "./BunHttpServer.ts"
4
6
 
5
- t.describe("BunHttpServer smart port selection", () => {
7
+ test.describe("smart port selection", () => {
6
8
  // Skip when running in TTY because the random port logic requires !isTTY && CLAUDECODE,
7
9
  // and process.stdout.isTTY cannot be mocked
8
- t.test.skipIf(process.stdout.isTTY)(
10
+ test.test.skipIf(process.stdout.isTTY)(
9
11
  "uses random port when PORT not set, isTTY=false, CLAUDECODE set",
10
12
  async () => {
11
13
  const originalPort = process.env.PORT
@@ -24,7 +26,10 @@ t.describe("BunHttpServer smart port selection", () => {
24
26
  ),
25
27
  )
26
28
 
27
- t.expect(port).not.toBe(3000)
29
+ test
30
+ .expect(port)
31
+ .not
32
+ .toBe(3000)
28
33
  } finally {
29
34
  if (originalPort !== undefined) {
30
35
  process.env.PORT = originalPort
@@ -40,7 +45,7 @@ t.describe("BunHttpServer smart port selection", () => {
40
45
  },
41
46
  )
42
47
 
43
- t.test("uses explicit PORT even when CLAUDECODE is set", async () => {
48
+ test.test("uses explicit PORT even when CLAUDECODE is set", async () => {
44
49
  const originalPort = process.env.PORT
45
50
  const originalClaudeCode = process.env.CLAUDECODE
46
51
 
@@ -57,7 +62,9 @@ t.describe("BunHttpServer smart port selection", () => {
57
62
  ),
58
63
  )
59
64
 
60
- t.expect(port).toBe(5678)
65
+ test
66
+ .expect(port)
67
+ .toBe(5678)
61
68
  } finally {
62
69
  if (originalPort !== undefined) {
63
70
  process.env.PORT = originalPort
@@ -72,3 +79,173 @@ t.describe("BunHttpServer smart port selection", () => {
72
79
  }
73
80
  })
74
81
  })
82
+
83
+ const BunHttpServerTest = Layer.scoped(
84
+ BunHttpServer.BunHttpServer,
85
+ BunHttpServer.make({ port: 0 }),
86
+ )
87
+
88
+ const testLayer = (routes: ReturnType<typeof Route.tree>) =>
89
+ Layer.provideMerge(
90
+ BunHttpServer.layerRoutes(routes),
91
+ BunHttpServerTest,
92
+ )
93
+
94
+ test.describe("routes", () => {
95
+ test.test("serves static text route", async () => {
96
+ const routes = Route.tree({
97
+ "/": Route.get(Route.text("Hello, World!")),
98
+ })
99
+
100
+ const response = await Effect.runPromise(
101
+ Effect.scoped(
102
+ Effect
103
+ .gen(function*() {
104
+ const bunServer = yield* BunHttpServer.BunHttpServer
105
+ return yield* Effect.promise(() =>
106
+ fetch(`http://localhost:${bunServer.server.port}/`)
107
+ )
108
+ })
109
+ .pipe(Effect.provide(testLayer(routes))),
110
+ ),
111
+ )
112
+
113
+ test.expect(response.status).toBe(200)
114
+ test.expect(await response.text()).toBe("Hello, World!")
115
+ })
116
+
117
+ test.test("serves JSON route", async () => {
118
+ const routes = Route.tree({
119
+ "/api/data": Route.get(Route.json({ message: "success", value: 42 })),
120
+ })
121
+
122
+ const response = await Effect.runPromise(
123
+ Effect.scoped(
124
+ Effect
125
+ .gen(function*() {
126
+ const bunServer = yield* BunHttpServer.BunHttpServer
127
+ return yield* Effect.promise(() =>
128
+ fetch(`http://localhost:${bunServer.server.port}/api/data`)
129
+ )
130
+ })
131
+ .pipe(Effect.provide(testLayer(routes))),
132
+ ),
133
+ )
134
+
135
+ test.expect(response.status).toBe(200)
136
+ test.expect(response.headers.get("Content-Type")).toBe("application/json")
137
+ test.expect(await response.json()).toEqual({
138
+ message: "success",
139
+ value: 42,
140
+ })
141
+ })
142
+
143
+ test.test("returns 404 for unknown routes", async () => {
144
+ const routes = Route.tree({
145
+ "/": Route.get(Route.text("Home")),
146
+ })
147
+
148
+ const response = await Effect.runPromise(
149
+ Effect.scoped(
150
+ Effect
151
+ .gen(function*() {
152
+ const bunServer = yield* BunHttpServer.BunHttpServer
153
+ return yield* Effect.promise(() =>
154
+ fetch(`http://localhost:${bunServer.server.port}/unknown`)
155
+ )
156
+ })
157
+ .pipe(Effect.provide(testLayer(routes))),
158
+ ),
159
+ )
160
+
161
+ test.expect(response.status).toBe(404)
162
+ })
163
+
164
+ test.test("handles content negotiation", async () => {
165
+ const routes = Route.tree({
166
+ "/data": Route
167
+ .get(Route.json({ type: "json" }))
168
+ .get(Route.html("<div>html</div>")),
169
+ })
170
+
171
+ const [jsonResponse, htmlResponse] = await Effect.runPromise(
172
+ Effect.scoped(
173
+ Effect
174
+ .gen(function*() {
175
+ const bunServer = yield* BunHttpServer.BunHttpServer
176
+ const baseUrl = `http://localhost:${bunServer.server.port}`
177
+
178
+ const json = yield* Effect.promise(() =>
179
+ fetch(`${baseUrl}/data`, {
180
+ headers: { Accept: "application/json" },
181
+ })
182
+ )
183
+
184
+ const html = yield* Effect.promise(() =>
185
+ fetch(`${baseUrl}/data`, {
186
+ headers: { Accept: "text/html" },
187
+ })
188
+ )
189
+
190
+ return [json, html] as const
191
+ })
192
+ .pipe(Effect.provide(testLayer(routes))),
193
+ ),
194
+ )
195
+
196
+ test.expect(jsonResponse.headers.get("Content-Type")).toBe(
197
+ "application/json",
198
+ )
199
+ test.expect(await jsonResponse.json()).toEqual({ type: "json" })
200
+
201
+ test.expect(htmlResponse.headers.get("Content-Type")).toBe(
202
+ "text/html; charset=utf-8",
203
+ )
204
+ test.expect(await htmlResponse.text()).toBe("<div>html</div>")
205
+ })
206
+
207
+ test.test("returns 406 for unacceptable content type", async () => {
208
+ const routes = Route.tree({
209
+ "/data": Route.get(Route.json({ type: "json" })),
210
+ })
211
+
212
+ const response = await Effect.runPromise(
213
+ Effect.scoped(
214
+ Effect
215
+ .gen(function*() {
216
+ const bunServer = yield* BunHttpServer.BunHttpServer
217
+ return yield* Effect.promise(() =>
218
+ fetch(`http://localhost:${bunServer.server.port}/data`, {
219
+ headers: { Accept: "image/png" },
220
+ })
221
+ )
222
+ })
223
+ .pipe(Effect.provide(testLayer(routes))),
224
+ ),
225
+ )
226
+
227
+ test.expect(response.status).toBe(406)
228
+ })
229
+
230
+ test.test("handles parameterized routes", async () => {
231
+ const routes = Route.tree({
232
+ "/users/:id": Route.get(Route.text("user")),
233
+ })
234
+
235
+ const response = await Effect.runPromise(
236
+ Effect.scoped(
237
+ Effect
238
+ .gen(function*() {
239
+ const bunServer = yield* BunHttpServer.BunHttpServer
240
+ return yield* Effect.promise(() =>
241
+ fetch(`http://localhost:${bunServer.server.port}/users/123`)
242
+ )
243
+ })
244
+ .pipe(Effect.provide(testLayer(routes))),
245
+ ),
246
+ )
247
+
248
+ test.expect(response.status).toBe(200)
249
+ test.expect(await response.text()).toBe("user")
250
+ })
251
+ })