effect-start 0.17.2 → 0.19.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 (80) hide show
  1. package/dist/Development.d.ts +7 -2
  2. package/dist/Development.js +12 -6
  3. package/dist/PlatformRuntime.d.ts +4 -0
  4. package/dist/PlatformRuntime.js +9 -0
  5. package/dist/Route.d.ts +6 -2
  6. package/dist/Route.js +22 -0
  7. package/dist/RouteHttp.d.ts +1 -1
  8. package/dist/RouteHttp.js +12 -19
  9. package/dist/RouteMount.d.ts +2 -1
  10. package/dist/Start.d.ts +1 -5
  11. package/dist/Start.js +1 -8
  12. package/dist/Unique.d.ts +50 -0
  13. package/dist/Unique.js +187 -0
  14. package/dist/bun/BunHttpServer.js +5 -6
  15. package/dist/bun/BunRoute.d.ts +1 -1
  16. package/dist/bun/BunRoute.js +2 -2
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.js +1 -0
  19. package/dist/node/Effectify.d.ts +209 -0
  20. package/dist/node/Effectify.js +19 -0
  21. package/dist/node/FileSystem.d.ts +3 -5
  22. package/dist/node/FileSystem.js +42 -62
  23. package/dist/node/PlatformError.d.ts +46 -0
  24. package/dist/node/PlatformError.js +43 -0
  25. package/dist/testing/TestLogger.js +1 -1
  26. package/package.json +10 -5
  27. package/src/Development.ts +13 -18
  28. package/src/PlatformRuntime.ts +11 -0
  29. package/src/Route.ts +31 -2
  30. package/src/RouteHttp.ts +15 -31
  31. package/src/RouteMount.ts +1 -1
  32. package/src/Start.ts +1 -15
  33. package/src/Unique.ts +232 -0
  34. package/src/bun/BunHttpServer.ts +6 -9
  35. package/src/bun/BunRoute.ts +3 -3
  36. package/src/index.ts +1 -0
  37. package/src/node/Effectify.ts +262 -0
  38. package/src/node/FileSystem.ts +59 -97
  39. package/src/node/PlatformError.ts +102 -0
  40. package/src/testing/TestLogger.ts +1 -1
  41. package/dist/Random.d.ts +0 -5
  42. package/dist/Random.js +0 -49
  43. package/src/Commander.test.ts +0 -1639
  44. package/src/ContentNegotiation.test.ts +0 -603
  45. package/src/Development.test.ts +0 -119
  46. package/src/Entity.test.ts +0 -592
  47. package/src/FileRouterPattern.test.ts +0 -147
  48. package/src/FileRouter_files.test.ts +0 -64
  49. package/src/FileRouter_path.test.ts +0 -145
  50. package/src/FileRouter_tree.test.ts +0 -132
  51. package/src/Http.test.ts +0 -319
  52. package/src/HttpAppExtra.test.ts +0 -103
  53. package/src/HttpUtils.test.ts +0 -85
  54. package/src/PathPattern.test.ts +0 -648
  55. package/src/Random.ts +0 -59
  56. package/src/RouteBody.test.ts +0 -232
  57. package/src/RouteHook.test.ts +0 -40
  58. package/src/RouteHttp.test.ts +0 -2909
  59. package/src/RouteMount.test.ts +0 -481
  60. package/src/RouteSchema.test.ts +0 -427
  61. package/src/RouteSse.test.ts +0 -249
  62. package/src/RouteTree.test.ts +0 -494
  63. package/src/RouteTrie.test.ts +0 -322
  64. package/src/RouterPattern.test.ts +0 -676
  65. package/src/Values.test.ts +0 -263
  66. package/src/bun/BunBundle.test.ts +0 -268
  67. package/src/bun/BunBundle_imports.test.ts +0 -48
  68. package/src/bun/BunHttpServer.test.ts +0 -251
  69. package/src/bun/BunImportTrackerPlugin.test.ts +0 -77
  70. package/src/bun/BunRoute.test.ts +0 -162
  71. package/src/bundler/BundleHttp.test.ts +0 -132
  72. package/src/effect/HttpRouter.test.ts +0 -548
  73. package/src/experimental/EncryptedCookies.test.ts +0 -488
  74. package/src/hyper/HyperHtml.test.ts +0 -209
  75. package/src/hyper/HyperRoute.test.tsx +0 -197
  76. package/src/middlewares/BasicAuthMiddleware.test.ts +0 -84
  77. package/src/testing/TestHttpClient.test.ts +0 -83
  78. package/src/testing/TestLogger.test.ts +0 -51
  79. package/src/x/datastar/Datastar.test.ts +0 -266
  80. package/src/x/tailwind/TailwindPlugin.test.ts +0 -333
@@ -1,494 +0,0 @@
1
- import * as test from "bun:test"
2
- import * as Route from "./Route.ts"
3
- import * as RouteTree from "./RouteTree.ts"
4
-
5
- test.describe("layer route", () => {
6
- test.it("merges LayerRoute into other routes", () => {
7
- const tree = RouteTree.make({
8
- "*": Route.use(Route.filter({ context: { authenticated: true } })),
9
- "/users": Route.get(Route.text("users")),
10
- })
11
-
12
- type TreeRoutes = RouteTree.Routes<typeof tree>
13
-
14
- test
15
- .expectTypeOf<TreeRoutes>()
16
- .toExtend<{
17
- "/users": Route.Route.Tuple
18
- }>()
19
-
20
- // "*" key should not exist in the resulting type
21
- test
22
- .expectTypeOf<TreeRoutes>()
23
- .not
24
- .toHaveProperty("*")
25
-
26
- // layer route should be first in the tuple (method: "*")
27
- test
28
- .expectTypeOf<TreeRoutes["/users"][0]>()
29
- .toExtend<Route.Route.With<{ method: "*" }>>()
30
-
31
- // actual route should be second (method: "GET")
32
- test
33
- .expectTypeOf<TreeRoutes["/users"][1]>()
34
- .toExtend<Route.Route.With<{ method: "GET" }>>()
35
- })
36
-
37
- test.it("prepends LayerRoute to all other routes when walking", () => {
38
- const tree = RouteTree.make({
39
- "*": Route.use(Route.filter({ context: { layer: true } })),
40
- "/users": Route.get(Route.text("users")),
41
- "/admin": Route.post(Route.json({ ok: true })),
42
- })
43
-
44
- test.expect(Route.descriptor(RouteTree.walk(tree))).toEqual([
45
- { path: "/admin", method: "*" },
46
- { path: "/admin", method: "POST", format: "json" },
47
- { path: "/users", method: "*" },
48
- { path: "/users", method: "GET", format: "text" },
49
- ])
50
- })
51
-
52
- test.it("prepends multiple LayerRoutes to all other routes", () => {
53
- const tree = RouteTree.make({
54
- "*": Route
55
- .use(Route.filter({ context: { first: true } }))
56
- .use(Route.filter({ context: { second: true } })),
57
- "/users": Route.get(Route.text("users")),
58
- })
59
-
60
- test.expect(Route.descriptor(RouteTree.walk(tree))).toEqual([
61
- { path: "/users", method: "*" },
62
- { path: "/users", method: "*" },
63
- { path: "/users", method: "GET", format: "text" },
64
- ])
65
- })
66
-
67
- test.it("only allows method '*' routes under '*' key", () => {
68
- const _tree = RouteTree.make({
69
- // @ts-expect-error - LayerRoute must have method "*"
70
- "*": Route.get(Route.text("invalid")),
71
- "/users": Route.get(Route.text("users")),
72
- })
73
- })
74
-
75
- test.it("lookup finds LayerRoute first", () => {
76
- const tree = RouteTree.make({
77
- "*": Route.use(Route.filter({ context: { layer: true } })),
78
- "/users": Route.get(Route.text("users")),
79
- })
80
-
81
- const result = RouteTree.lookup(tree, "GET", "/users")
82
- test.expect(result).not.toBeNull()
83
- test.expect(Route.descriptor(result!.route).method).toBe("*")
84
- })
85
-
86
- test.it("works without LayerRoute (no '*' key)", () => {
87
- const tree = RouteTree.make({
88
- "/users": Route.get(Route.text("users")),
89
- "/admin": Route.post(Route.json({ ok: true })),
90
- })
91
-
92
- test
93
- .expect(Route.descriptor(RouteTree.walk(tree)).map((d) => d.path))
94
- .toEqual(["/admin", "/users"])
95
- })
96
- })
97
-
98
- test.describe(RouteTree.make, () => {
99
- test.it("makes", () => {
100
- const routes = RouteTree.make({
101
- "/admin": Route
102
- .use(
103
- Route.filter({
104
- context: {
105
- isAdmin: true,
106
- },
107
- }),
108
- )
109
- .get(
110
- Route.text("admin home"),
111
- ),
112
-
113
- "/users": Route
114
- .get(
115
- Route.text("users list"),
116
- ),
117
- })
118
-
119
- test
120
- .expectTypeOf<RouteTree.Routes<typeof routes>>()
121
- .toExtend<{
122
- "/admin": unknown
123
- "/users": unknown
124
- }>()
125
- })
126
-
127
- test.it("flattens nested route trees with prefixed paths", () => {
128
- const apiTree = RouteTree.make({
129
- "/users": Route.get(Route.json({ users: [] })),
130
- "/posts": Route.get(Route.json({ posts: [] })),
131
- })
132
-
133
- const tree = RouteTree.make({
134
- "/": Route.get(Route.text("home")),
135
- "/api": apiTree,
136
- })
137
-
138
- type TreeRoutes = RouteTree.Routes<typeof tree>
139
-
140
- test
141
- .expectTypeOf<TreeRoutes["/"]>()
142
- .toExtend<Route.Route.Tuple>()
143
-
144
- test
145
- .expectTypeOf<TreeRoutes["/"][0]>()
146
- .toExtend<
147
- Route.Route.With<
148
- { method: "GET"; format: "text" }
149
- >
150
- >()
151
-
152
- test
153
- .expectTypeOf<TreeRoutes["/api/users"]>()
154
- .toExtend<Route.Route.Tuple>()
155
-
156
- test
157
- .expectTypeOf<TreeRoutes["/api/users"][0]>()
158
- .toExtend<
159
- Route.Route.With<
160
- { method: "GET"; format: "json" }
161
- >
162
- >()
163
-
164
- test
165
- .expectTypeOf<TreeRoutes["/api/posts"]>()
166
- .toExtend<Route.Route.Tuple>()
167
-
168
- test
169
- .expectTypeOf<TreeRoutes["/api/posts"][0]>()
170
- .toExtend<
171
- Route.Route.With<
172
- { method: "GET"; format: "json" }
173
- >
174
- >()
175
- })
176
-
177
- test.it("walks nested route trees with prefixed paths", () => {
178
- const apiTree = RouteTree.make({
179
- "/users": Route.get(Route.json({ users: [] })),
180
- "/posts": Route.post(Route.json({ ok: true })),
181
- })
182
-
183
- const tree = RouteTree.make({
184
- "/": Route.get(Route.text("home")),
185
- "/api": apiTree,
186
- })
187
-
188
- test.expect(Route.descriptor(RouteTree.walk(tree))).toEqual([
189
- { path: "/", method: "GET", format: "text" },
190
- { path: "/api/posts", method: "POST", format: "json" },
191
- { path: "/api/users", method: "GET", format: "json" },
192
- ])
193
- })
194
-
195
- test.it("deeply nested route trees", () => {
196
- const v1Tree = RouteTree.make({
197
- "/health": Route.get(Route.text("ok")),
198
- })
199
-
200
- const apiTree = RouteTree.make({
201
- "/v1": v1Tree,
202
- })
203
-
204
- const tree = RouteTree.make({
205
- "/api": apiTree,
206
- })
207
-
208
- test
209
- .expect(Route.descriptor(RouteTree.walk(tree)).map((d) => d.path))
210
- .toEqual(["/api/v1/health"])
211
- })
212
-
213
- test.it("lookup works with nested trees", () => {
214
- const apiTree = RouteTree.make({
215
- "/users": Route.get(Route.json({ users: [] })),
216
- "/users/:id": Route.get(Route.json({ user: null })),
217
- })
218
-
219
- const tree = RouteTree.make({
220
- "/": Route.get(Route.text("home")),
221
- "/api": apiTree,
222
- })
223
-
224
- const home = RouteTree.lookup(tree, "GET", "/")
225
- test.expect(Route.descriptor(home!.route).path).toBe("/")
226
-
227
- const users = RouteTree.lookup(tree, "GET", "/api/users")
228
- test.expect(Route.descriptor(users!.route).path).toBe("/api/users")
229
-
230
- const user = RouteTree.lookup(tree, "GET", "/api/users/123")
231
- test.expect(Route.descriptor(user!.route).path).toBe("/api/users/:id")
232
- test.expect(user!.params).toEqual({ id: "123" })
233
- })
234
- })
235
-
236
- test.describe(RouteTree.walk, () => {
237
- test.it("walks in sorted order: by depth, static before params", () => {
238
- // routes defined in random order
239
- const routes = RouteTree.make({
240
- "/users/:userId": Route.get(Route.text("user detail")),
241
- "/admin/stats": Route.get(Route.html("admin stats")),
242
- "/": Route.get(Route.text("home")),
243
- "/admin/users": Route.post(Route.json({ ok: true })),
244
- "/users": Route.get(Route.text("users list")),
245
- "/admin": Route.use(Route.filter({ context: { admin: true } })),
246
- })
247
-
248
- // expected order:
249
- // depth 0: /
250
- // depth 1: /admin, /users (alphabetical)
251
- // depth 2: /admin/stats, /admin/users, /users/:userId (static first, param last)
252
- test
253
- .expect(Route.descriptor(RouteTree.walk(routes)).map((d) => d.path))
254
- .toEqual([
255
- "/",
256
- "/admin",
257
- "/users",
258
- "/admin/stats",
259
- "/admin/users",
260
- "/users/:userId",
261
- ])
262
- })
263
-
264
- test.it("static < :param < :param? < :param+ < :param*", () => {
265
- const routes = RouteTree.make({
266
- "/:path*": Route.get(Route.text("catch all")),
267
- "/about": Route.get(Route.text("about")),
268
- "/:path+": Route.get(Route.text("one or more")),
269
- "/:page": Route.get(Route.text("single param")),
270
- "/:page?": Route.get(Route.text("optional param")),
271
- })
272
-
273
- test
274
- .expect(Route.descriptor(RouteTree.walk(routes)).map((d) => d.path))
275
- .toEqual([
276
- "/about",
277
- "/:page",
278
- "/:page?",
279
- "/:path+",
280
- "/:path*",
281
- ])
282
- })
283
-
284
- test.it("greedy routes come after all non-greedy across depth", () => {
285
- const routes = RouteTree.make({
286
- "/:path*": Route.get(Route.text("catch all")),
287
- "/users/:id": Route.get(Route.text("user detail")),
288
- "/users": Route.get(Route.text("users")),
289
- "/users/:id/posts/:postId": Route.get(Route.text("post detail")),
290
- })
291
-
292
- test
293
- .expect(Route.descriptor(RouteTree.walk(routes)).map((d) => d.path))
294
- .toEqual([
295
- "/users",
296
- "/users/:id",
297
- "/users/:id/posts/:postId",
298
- "/:path*",
299
- ])
300
- })
301
-
302
- test.it("greedy routes sorted by greedy position", () => {
303
- const routes = RouteTree.make({
304
- "/:path*": Route.get(Route.text("root catch all")),
305
- "/api/:rest*": Route.get(Route.text("api catch all")),
306
- "/api/v1/:rest*": Route.get(Route.text("api v1 catch all")),
307
- })
308
-
309
- test
310
- .expect(Route.descriptor(RouteTree.walk(routes)).map((d) => d.path))
311
- .toEqual([
312
- "/api/v1/:rest*",
313
- "/api/:rest*",
314
- "/:path*",
315
- ])
316
- })
317
-
318
- test.it("greedy routes with same position sorted by prefix then type", () => {
319
- const routes = RouteTree.make({
320
- "/docs/:path*": Route.get(Route.text("docs catch all")),
321
- "/api/:rest+": Route.get(Route.text("api one or more")),
322
- "/api/:rest*": Route.get(Route.text("api catch all")),
323
- })
324
-
325
- // /api before /docs (alphabetical), then + before * for same prefix
326
- test
327
- .expect(Route.descriptor(RouteTree.walk(routes)).map((d) => d.path))
328
- .toEqual([
329
- "/api/:rest+",
330
- "/api/:rest*",
331
- "/docs/:path*",
332
- ])
333
- })
334
- })
335
-
336
- test.describe(RouteTree.lookup, () => {
337
- test.it("matches static paths", () => {
338
- const tree = RouteTree.make({
339
- "/users": Route.get(Route.text("users list")),
340
- "/admin": Route.get(Route.text("admin")),
341
- })
342
-
343
- const result = RouteTree.lookup(tree, "GET", "/users")
344
- test.expect(result).not.toBeNull()
345
- test.expect(result!.params).toEqual({})
346
- test.expect(Route.descriptor(result!.route).path).toBe("/users")
347
- })
348
-
349
- test.it("extracts path parameters", () => {
350
- const tree = RouteTree.make({
351
- "/users/:id": Route.get(Route.text("user detail")),
352
- })
353
-
354
- const result = RouteTree.lookup(tree, "GET", "/users/123")
355
- test.expect(result).not.toBeNull()
356
- test.expect(result!.params).toEqual({ id: "123" })
357
- })
358
-
359
- test.it("filters by HTTP method", () => {
360
- const tree = RouteTree.make({
361
- "/users": Route.get(Route.text("get users")),
362
- "/admin": Route.post(Route.text("post admin")),
363
- })
364
-
365
- const getResult = RouteTree.lookup(tree, "GET", "/users")
366
- test.expect(getResult).not.toBeNull()
367
-
368
- const postOnGet = RouteTree.lookup(tree, "POST", "/users")
369
- test.expect(postOnGet).toBeNull()
370
-
371
- const postResult = RouteTree.lookup(tree, "POST", "/admin")
372
- test.expect(postResult).not.toBeNull()
373
- })
374
-
375
- test.it("wildcard method matches any method", () => {
376
- const tree = RouteTree.make({
377
- "/api": Route.use(Route.filter({ context: { api: true } })),
378
- })
379
-
380
- const getResult = RouteTree.lookup(tree, "GET", "/api")
381
- test.expect(getResult).not.toBeNull()
382
-
383
- const postResult = RouteTree.lookup(tree, "POST", "/api")
384
- test.expect(postResult).not.toBeNull()
385
-
386
- const deleteResult = RouteTree.lookup(tree, "DELETE", "/api")
387
- test.expect(deleteResult).not.toBeNull()
388
- })
389
-
390
- test.it("static routes take priority over param routes", () => {
391
- const tree = RouteTree.make({
392
- "/users/:id": Route.get(Route.text("user by id")),
393
- "/users/me": Route.get(Route.text("current user")),
394
- })
395
-
396
- const result = RouteTree.lookup(tree, "GET", "/users/me")
397
- test.expect(result).not.toBeNull()
398
- test.expect(Route.descriptor(result!.route).path).toBe("/users/me")
399
- test.expect(result!.params).toEqual({})
400
- })
401
-
402
- test.it("matches greedy params with +", () => {
403
- const tree = RouteTree.make({
404
- "/docs/:path+": Route.get(Route.text("docs")),
405
- })
406
-
407
- const result = RouteTree.lookup(tree, "GET", "/docs/api/v1/users")
408
- test.expect(result).not.toBeNull()
409
- test.expect(result!.params).toEqual({ path: "api/v1/users" })
410
-
411
- const noMatch = RouteTree.lookup(tree, "GET", "/docs")
412
- test.expect(noMatch).toBeNull()
413
- })
414
-
415
- test.it("matches greedy params with *", () => {
416
- const tree = RouteTree.make({
417
- "/files/:path*": Route.get(Route.text("files")),
418
- })
419
-
420
- const withPath = RouteTree.lookup(tree, "GET", "/files/a/b/c")
421
- test.expect(withPath).not.toBeNull()
422
- test.expect(withPath!.params).toEqual({ path: "a/b/c" })
423
-
424
- const withoutPath = RouteTree.lookup(tree, "GET", "/files")
425
- test.expect(withoutPath).not.toBeNull()
426
- test.expect(withoutPath!.params).toEqual({})
427
- })
428
-
429
- test.it("returns null for no match", () => {
430
- const tree = RouteTree.make({
431
- "/users": Route.get(Route.text("users")),
432
- })
433
-
434
- const result = RouteTree.lookup(tree, "GET", "/not-found")
435
- test.expect(result).toBeNull()
436
- })
437
-
438
- test.it("matches optional params with ?", () => {
439
- const tree = RouteTree.make({
440
- "/files/:name?": Route.get(Route.text("files")),
441
- })
442
-
443
- const withParam = RouteTree.lookup(tree, "GET", "/files/readme")
444
- test.expect(withParam).not.toBeNull()
445
- test.expect(withParam!.params).toEqual({ name: "readme" })
446
-
447
- const withoutParam = RouteTree.lookup(tree, "GET", "/files")
448
- test.expect(withoutParam).not.toBeNull()
449
- test.expect(withoutParam!.params).toEqual({})
450
- })
451
-
452
- test.it("respects route priority for complex trees", () => {
453
- const tree = RouteTree.make({
454
- "/:path*": Route.get(Route.text("catch all")),
455
- "/api/:rest+": Route.get(Route.text("api wildcard")),
456
- "/api/users": Route.get(Route.text("api users")),
457
- "/api/users/:id": Route.get(Route.text("api user detail")),
458
- })
459
-
460
- const staticMatch = RouteTree.lookup(tree, "GET", "/api/users")
461
- test.expect(Route.descriptor(staticMatch!.route).path).toBe("/api/users")
462
-
463
- const paramMatch = RouteTree.lookup(tree, "GET", "/api/users/123")
464
- test.expect(Route.descriptor(paramMatch!.route).path).toBe("/api/users/:id")
465
- test.expect(paramMatch!.params).toEqual({ id: "123" })
466
-
467
- const greedyMatch = RouteTree.lookup(tree, "GET", "/api/something/else")
468
- test.expect(Route.descriptor(greedyMatch!.route).path).toBe("/api/:rest+")
469
- test.expect(greedyMatch!.params).toEqual({ rest: "something/else" })
470
-
471
- const catchAll = RouteTree.lookup(tree, "GET", "/random/path")
472
- test.expect(Route.descriptor(catchAll!.route).path).toBe("/:path*")
473
- })
474
-
475
- test.it("static routes take priority over optional param routes", () => {
476
- const tree = RouteTree.make({
477
- "/files/:name?": Route.get(Route.text("files optional")),
478
- "/files/latest": Route.get(Route.text("files latest")),
479
- })
480
-
481
- const staticMatch = RouteTree.lookup(tree, "GET", "/files/latest")
482
- test.expect(Route.descriptor(staticMatch!.route).path).toBe("/files/latest")
483
-
484
- const optionalMatch = RouteTree.lookup(tree, "GET", "/files/other")
485
- test.expect(Route.descriptor(optionalMatch!.route).path).toBe(
486
- "/files/:name?",
487
- )
488
- test.expect(optionalMatch!.params).toEqual({ name: "other" })
489
-
490
- const noParam = RouteTree.lookup(tree, "GET", "/files")
491
- test.expect(Route.descriptor(noParam!.route).path).toBe("/files/:name?")
492
- test.expect(noParam!.params).toEqual({})
493
- })
494
- })