effect-start 0.9.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.
Files changed (54) hide show
  1. package/package.json +15 -14
  2. package/src/BundleHttp.test.ts +1 -1
  3. package/src/Commander.test.ts +15 -15
  4. package/src/Commander.ts +58 -88
  5. package/src/EncryptedCookies.test.ts +4 -4
  6. package/src/FileHttpRouter.test.ts +85 -16
  7. package/src/FileHttpRouter.ts +119 -32
  8. package/src/FileRouter.ts +62 -166
  9. package/src/FileRouterCodegen.test.ts +252 -66
  10. package/src/FileRouterCodegen.ts +13 -56
  11. package/src/FileRouterPattern.test.ts +116 -0
  12. package/src/FileRouterPattern.ts +59 -0
  13. package/src/FileRouter_path.test.ts +63 -102
  14. package/src/FileSystemExtra.test.ts +226 -0
  15. package/src/FileSystemExtra.ts +24 -60
  16. package/src/HttpAppExtra.test.ts +84 -0
  17. package/src/HttpAppExtra.ts +399 -47
  18. package/src/HttpUtils.test.ts +68 -0
  19. package/src/HttpUtils.ts +15 -0
  20. package/src/HyperHtml.ts +24 -5
  21. package/src/JsModule.test.ts +1 -1
  22. package/src/NodeFileSystem.ts +764 -0
  23. package/src/Random.ts +59 -0
  24. package/src/Route.test.ts +515 -18
  25. package/src/Route.ts +321 -166
  26. package/src/RouteRender.ts +40 -0
  27. package/src/Router.test.ts +416 -0
  28. package/src/Router.ts +288 -31
  29. package/src/RouterPattern.test.ts +655 -0
  30. package/src/RouterPattern.ts +416 -0
  31. package/src/Start.ts +14 -52
  32. package/src/TestHttpClient.test.ts +29 -0
  33. package/src/TestHttpClient.ts +122 -73
  34. package/src/assets.d.ts +39 -0
  35. package/src/bun/BunBundle.test.ts +0 -3
  36. package/src/bun/BunHttpServer.test.ts +74 -0
  37. package/src/bun/BunHttpServer.ts +259 -0
  38. package/src/bun/BunHttpServer_web.ts +384 -0
  39. package/src/bun/BunRoute.test.ts +514 -0
  40. package/src/bun/BunRoute.ts +427 -0
  41. package/src/bun/BunRoute_bundles.test.ts +218 -0
  42. package/src/bun/BunRuntime.ts +33 -0
  43. package/src/bun/BunTailwindPlugin.test.ts +1 -1
  44. package/src/bun/_empty.html +1 -0
  45. package/src/bun/index.ts +2 -1
  46. package/src/index.ts +14 -14
  47. package/src/middlewares/BasicAuthMiddleware.test.ts +74 -0
  48. package/src/middlewares/BasicAuthMiddleware.ts +36 -0
  49. package/src/testing.ts +12 -3
  50. package/src/Datastar.test.ts +0 -267
  51. package/src/Datastar.ts +0 -68
  52. package/src/bun/BunFullstackServer.ts +0 -45
  53. package/src/bun/BunFullstackServer_httpServer.ts +0 -541
  54. package/src/jsx-datastar.d.ts +0 -63
@@ -0,0 +1,40 @@
1
+ import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
2
+ import * as Effect from "effect/Effect"
3
+ import * as HyperHtml from "./HyperHtml.ts"
4
+ import * as Route from "./Route.ts"
5
+
6
+ /**
7
+ * Renders a route handler to an HttpServerResponse.
8
+ * Converts the raw handler value to a response based on the route's media type.
9
+ */
10
+ export function render<E, R>(
11
+ route: Route.Route<any, any, Route.RouteHandler<any, E, R>, any>,
12
+ context: Route.RouteContext,
13
+ ): Effect.Effect<HttpServerResponse.HttpServerResponse, E, R> {
14
+ return Effect.gen(function*() {
15
+ const raw = yield* route.handler(context)
16
+
17
+ // Allow handlers to return HttpServerResponse directly (e.g. BunRoute proxy)
18
+ if (HttpServerResponse.isServerResponse(raw)) {
19
+ return raw
20
+ }
21
+
22
+ switch (route.media) {
23
+ case "text/plain":
24
+ return HttpServerResponse.text(raw as string)
25
+
26
+ case "text/html":
27
+ if (Route.isGenericJsxObject(raw)) {
28
+ return HttpServerResponse.html(HyperHtml.renderToString(raw))
29
+ }
30
+ return HttpServerResponse.html(raw as string)
31
+
32
+ case "application/json":
33
+ return HttpServerResponse.unsafeJson(raw)
34
+
35
+ case "*":
36
+ default:
37
+ return HttpServerResponse.text(String(raw))
38
+ }
39
+ })
40
+ }
@@ -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
+ })