effect-start 0.9.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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/package.json +57 -0
  4. package/src/Bundle.ts +167 -0
  5. package/src/BundleFiles.ts +174 -0
  6. package/src/BundleHttp.test.ts +160 -0
  7. package/src/BundleHttp.ts +259 -0
  8. package/src/Commander.test.ts +1378 -0
  9. package/src/Commander.ts +672 -0
  10. package/src/Datastar.test.ts +267 -0
  11. package/src/Datastar.ts +68 -0
  12. package/src/Effect_HttpRouter.test.ts +570 -0
  13. package/src/EncryptedCookies.test.ts +427 -0
  14. package/src/EncryptedCookies.ts +451 -0
  15. package/src/FileHttpRouter.test.ts +207 -0
  16. package/src/FileHttpRouter.ts +122 -0
  17. package/src/FileRouter.ts +405 -0
  18. package/src/FileRouterCodegen.test.ts +598 -0
  19. package/src/FileRouterCodegen.ts +251 -0
  20. package/src/FileRouter_files.test.ts +64 -0
  21. package/src/FileRouter_path.test.ts +132 -0
  22. package/src/FileRouter_tree.test.ts +126 -0
  23. package/src/FileSystemExtra.ts +102 -0
  24. package/src/HttpAppExtra.ts +127 -0
  25. package/src/Hyper.ts +194 -0
  26. package/src/HyperHtml.test.ts +90 -0
  27. package/src/HyperHtml.ts +139 -0
  28. package/src/HyperNode.ts +37 -0
  29. package/src/JsModule.test.ts +14 -0
  30. package/src/JsModule.ts +116 -0
  31. package/src/PublicDirectory.test.ts +280 -0
  32. package/src/PublicDirectory.ts +108 -0
  33. package/src/Route.test.ts +873 -0
  34. package/src/Route.ts +992 -0
  35. package/src/Router.ts +80 -0
  36. package/src/SseHttpResponse.ts +55 -0
  37. package/src/Start.ts +133 -0
  38. package/src/StartApp.ts +43 -0
  39. package/src/StartHttp.ts +42 -0
  40. package/src/StreamExtra.ts +146 -0
  41. package/src/TestHttpClient.test.ts +54 -0
  42. package/src/TestHttpClient.ts +100 -0
  43. package/src/bun/BunBundle.test.ts +277 -0
  44. package/src/bun/BunBundle.ts +309 -0
  45. package/src/bun/BunBundle_imports.test.ts +50 -0
  46. package/src/bun/BunFullstackServer.ts +45 -0
  47. package/src/bun/BunFullstackServer_httpServer.ts +541 -0
  48. package/src/bun/BunImportTrackerPlugin.test.ts +77 -0
  49. package/src/bun/BunImportTrackerPlugin.ts +97 -0
  50. package/src/bun/BunTailwindPlugin.test.ts +335 -0
  51. package/src/bun/BunTailwindPlugin.ts +322 -0
  52. package/src/bun/BunVirtualFilesPlugin.ts +59 -0
  53. package/src/bun/index.ts +4 -0
  54. package/src/client/Overlay.ts +34 -0
  55. package/src/client/ScrollState.ts +120 -0
  56. package/src/client/index.ts +101 -0
  57. package/src/index.ts +24 -0
  58. package/src/jsx-datastar.d.ts +63 -0
  59. package/src/jsx-runtime.ts +23 -0
  60. package/src/jsx.d.ts +4402 -0
  61. package/src/testing.ts +55 -0
  62. package/src/x/cloudflare/CloudflareTunnel.ts +110 -0
  63. package/src/x/cloudflare/index.ts +1 -0
  64. package/src/x/datastar/Datastar.test.ts +267 -0
  65. package/src/x/datastar/Datastar.ts +68 -0
  66. package/src/x/datastar/index.ts +4 -0
  67. package/src/x/datastar/jsx-datastar.d.ts +63 -0
@@ -0,0 +1,598 @@
1
+ import * as FileSystem from "@effect/platform/FileSystem"
2
+ import * as t from "bun:test"
3
+ import { MemoryFileSystem } from "effect-memfs"
4
+ import * as Effect from "effect/Effect"
5
+ import { parseRoute } from "./FileRouter.ts"
6
+ import type { RouteHandle } from "./FileRouter.ts"
7
+ import * as FileRouterCodegen from "./FileRouterCodegen.ts"
8
+ import * as Route from "./Route.ts"
9
+ import { effectFn } from "./testing.ts"
10
+
11
+ t.it("generates code for routes only", () => {
12
+ const handles: RouteHandle[] = [
13
+ parseRoute("route.tsx"),
14
+ parseRoute("about/route.tsx"),
15
+ ]
16
+
17
+ const code = FileRouterCodegen.generateCode(handles)
18
+
19
+ const expected = `/**
20
+ * Auto-generated by effect-start.
21
+ */
22
+
23
+ import type { Router } from "effect-start"
24
+
25
+ export const modules = [
26
+ {
27
+ path: "/",
28
+ segments: [],
29
+ load: () => import("./route.tsx"),
30
+ },
31
+ {
32
+ path: "/about",
33
+ segments: [
34
+ { literal: "about" },
35
+ ],
36
+ load: () => import("./about/route.tsx"),
37
+ },
38
+ ] as const
39
+ `
40
+
41
+ t
42
+ .expect(code)
43
+ .toBe(expected)
44
+ })
45
+
46
+ t.it("generates code with layers", () => {
47
+ const handles: RouteHandle[] = [
48
+ parseRoute("layer.tsx"),
49
+ parseRoute("route.tsx"),
50
+ parseRoute("about/route.tsx"),
51
+ ]
52
+
53
+ const code = FileRouterCodegen.generateCode(handles)
54
+
55
+ const expected = `/**
56
+ * Auto-generated by effect-start.
57
+ */
58
+
59
+ import type { Router } from "effect-start"
60
+
61
+ export const modules = [
62
+ {
63
+ path: "/",
64
+ segments: [],
65
+ load: () => import("./route.tsx"),
66
+ layers: [
67
+ () => import("./layer.tsx"),
68
+ ],
69
+ },
70
+ {
71
+ path: "/about",
72
+ segments: [
73
+ { literal: "about" },
74
+ ],
75
+ load: () => import("./about/route.tsx"),
76
+ layers: [
77
+ () => import("./layer.tsx"),
78
+ ],
79
+ },
80
+ ] as const
81
+ `
82
+
83
+ t
84
+ .expect(code)
85
+ .toBe(expected)
86
+ })
87
+
88
+ t.it("generates code with nested layers", () => {
89
+ const handles: RouteHandle[] = [
90
+ parseRoute("layer.tsx"),
91
+ parseRoute("dashboard/layer.tsx"),
92
+ parseRoute("dashboard/route.tsx"),
93
+ parseRoute("dashboard/settings/route.tsx"),
94
+ ]
95
+
96
+ const code = FileRouterCodegen.generateCode(handles)
97
+
98
+ const expected = `/**
99
+ * Auto-generated by effect-start.
100
+ */
101
+
102
+ import type { Router } from "effect-start"
103
+
104
+ export const modules = [
105
+ {
106
+ path: "/dashboard",
107
+ segments: [
108
+ { literal: "dashboard" },
109
+ ],
110
+ load: () => import("./dashboard/route.tsx"),
111
+ layers: [
112
+ () => import("./layer.tsx"),
113
+ () => import("./dashboard/layer.tsx"),
114
+ ],
115
+ },
116
+ {
117
+ path: "/dashboard/settings",
118
+ segments: [
119
+ { literal: "dashboard" },
120
+ { literal: "settings" },
121
+ ],
122
+ load: () => import("./dashboard/settings/route.tsx"),
123
+ layers: [
124
+ () => import("./layer.tsx"),
125
+ () => import("./dashboard/layer.tsx"),
126
+ ],
127
+ },
128
+ ] as const
129
+ `
130
+
131
+ t
132
+ .expect(code)
133
+ .toBe(expected)
134
+ })
135
+
136
+ t.it("only includes group layers for routes in that group", () => {
137
+ const handles: RouteHandle[] = [
138
+ parseRoute("layer.tsx"),
139
+ parseRoute("(admin)/layer.ts"),
140
+ parseRoute("(admin)/users/route.tsx"),
141
+ parseRoute("movies/route.tsx"),
142
+ ]
143
+
144
+ const code = FileRouterCodegen.generateCode(handles)
145
+
146
+ t
147
+ .expect(code)
148
+ .toContain("path: \"/users\"")
149
+
150
+ t
151
+ .expect(code)
152
+ .toContain("path: \"/movies\"")
153
+
154
+ // /users should have both root layer and (admin) layer
155
+ t
156
+ .expect(code)
157
+ .toContain("() => import(\"./layer.tsx\")")
158
+
159
+ t
160
+ .expect(code)
161
+ .toContain("() => import(\"./(admin)/layer.ts\")")
162
+
163
+ // /movies should only have root layer, not (admin) layer
164
+ const expectedMovies = ` {
165
+ path: "/movies",
166
+ segments: [
167
+ { literal: "movies" },
168
+ ],
169
+ load: () => import("./movies/route.tsx"),
170
+ layers: [
171
+ () => import("./layer.tsx"),
172
+ ],
173
+ },`
174
+
175
+ t
176
+ .expect(code)
177
+ .toContain(expectedMovies)
178
+ })
179
+
180
+ t.it("handles dynamic routes with params", () => {
181
+ const handles: RouteHandle[] = [
182
+ parseRoute("users/route.tsx"),
183
+ parseRoute("users/[userId]/route.tsx"),
184
+ parseRoute("posts/[postId]/comments/[commentId]/route.tsx"),
185
+ ]
186
+
187
+ const code = FileRouterCodegen.generateCode(handles)
188
+
189
+ t
190
+ .expect(code)
191
+ .toContain("path: \"/users\"")
192
+ t
193
+ .expect(code)
194
+ .toContain("path: \"/users/[userId]\"")
195
+ t
196
+ .expect(code)
197
+ .toContain("{ literal: \"users\" }")
198
+ t
199
+ .expect(code)
200
+ .toContain("{ param: \"userId\" }")
201
+ t
202
+ .expect(code)
203
+ .toContain("{ param: \"postId\" }")
204
+ t
205
+ .expect(code)
206
+ .toContain("{ param: \"commentId\" }")
207
+ })
208
+
209
+ t.it("handles rest parameters", () => {
210
+ const handles: RouteHandle[] = [
211
+ parseRoute("docs/[[...slug]]/route.tsx"),
212
+ parseRoute("api/[...path]/route.tsx"),
213
+ ]
214
+
215
+ const code = FileRouterCodegen.generateCode(handles)
216
+
217
+ t
218
+ .expect(code)
219
+ .toContain("path: \"/docs/[[...slug]]\"")
220
+ t
221
+ .expect(code)
222
+ .toContain("path: \"/api/[...path]\"")
223
+ t
224
+ .expect(code)
225
+ .toContain("{ rest: \"slug\", optional: true }")
226
+ t
227
+ .expect(code)
228
+ .toContain("{ rest: \"path\" }")
229
+ })
230
+
231
+ t.it("handles groups in path", () => {
232
+ const handles: RouteHandle[] = [
233
+ parseRoute("(admin)/users/route.tsx"),
234
+ parseRoute("(admin)/layer.tsx"),
235
+ ]
236
+
237
+ const code = FileRouterCodegen.generateCode(handles)
238
+
239
+ t
240
+ .expect(code)
241
+ .toContain("path: \"/users\"") // groups stripped from URL
242
+ t
243
+ .expect(code)
244
+ .toContain("{ group: \"admin\" }")
245
+ t
246
+ .expect(code)
247
+ .toContain("layers: [\n () => import(\"./(admin)/layer.tsx\"),\n ]")
248
+ })
249
+
250
+ t.it("generates correct variable names for root routes", () => {
251
+ const handles: RouteHandle[] = [
252
+ parseRoute("route.tsx"),
253
+ ]
254
+
255
+ const code = FileRouterCodegen.generateCode(handles)
256
+
257
+ t
258
+ .expect(code)
259
+ .toContain("path: \"/\"")
260
+ t
261
+ .expect(code)
262
+ .toContain("segments: []")
263
+ })
264
+
265
+ t.it("handles routes with dots in path segments", () => {
266
+ const handles: RouteHandle[] = [
267
+ parseRoute("events.json/route.ts"),
268
+ parseRoute("config.yaml.backup/route.ts"),
269
+ ]
270
+
271
+ const code = FileRouterCodegen.generateCode(handles)
272
+
273
+ t
274
+ .expect(code)
275
+ .toContain("path: \"/events.json\"")
276
+ t
277
+ .expect(code)
278
+ .toContain("path: \"/config.yaml.backup\"")
279
+ t
280
+ .expect(code)
281
+ .toContain("{ literal: \"events.json\" }")
282
+ t
283
+ .expect(code)
284
+ .toContain("{ literal: \"config.yaml.backup\" }")
285
+ })
286
+
287
+ t.it("uses default module identifier", () => {
288
+ const handles: RouteHandle[] = [
289
+ parseRoute("route.tsx"),
290
+ ]
291
+
292
+ const code = FileRouterCodegen.generateCode(handles)
293
+
294
+ t
295
+ .expect(code)
296
+ .toContain("import type { Router } from \"effect-start\"")
297
+ })
298
+
299
+ t.it("generates empty modules array when no handles provided", () => {
300
+ const handles: RouteHandle[] = []
301
+
302
+ const code = FileRouterCodegen.generateCode(handles)
303
+
304
+ t
305
+ .expect(code)
306
+ .toContain("export const modules = [] as const")
307
+ })
308
+
309
+ t.it("only includes routes in modules, not layers", () => {
310
+ const handles: RouteHandle[] = [
311
+ parseRoute("layer.tsx"),
312
+ parseRoute("users/layer.tsx"),
313
+ ]
314
+
315
+ const code = FileRouterCodegen.generateCode(handles)
316
+
317
+ t
318
+ .expect(code)
319
+ .toContain("export const modules = [] as const")
320
+ })
321
+
322
+ t.it("complex nested routes with multiple layers", () => {
323
+ const handles: RouteHandle[] = [
324
+ parseRoute("layer.tsx"),
325
+ parseRoute("(auth)/layer.tsx"),
326
+ parseRoute("(auth)/login/route.tsx"),
327
+ parseRoute("(auth)/signup/route.tsx"),
328
+ parseRoute("dashboard/layer.tsx"),
329
+ parseRoute("dashboard/route.tsx"),
330
+ parseRoute("dashboard/settings/route.tsx"),
331
+ ]
332
+
333
+ const code = FileRouterCodegen.generateCode(handles)
334
+
335
+ t
336
+ .expect(code)
337
+ .toContain("path: \"/login\"") // group stripped
338
+ t
339
+ .expect(code)
340
+ .toContain("path: \"/signup\"") // group stripped
341
+ t
342
+ .expect(code)
343
+ .toContain("path: \"/dashboard\"")
344
+ t
345
+ .expect(code)
346
+ .toContain("path: \"/dashboard/settings\"")
347
+
348
+ // Check layers are properly inherited
349
+ t
350
+ .expect(code)
351
+ .toContain("() => import(\"./layer.tsx\")")
352
+ t
353
+ .expect(code)
354
+ .toContain("() => import(\"./(auth)/layer.tsx\")")
355
+ t
356
+ .expect(code)
357
+ .toContain("() => import(\"./dashboard/layer.tsx\")")
358
+ })
359
+
360
+ t.it("handles routes with hyphens and underscores in path segments", () => {
361
+ const handles: RouteHandle[] = [
362
+ parseRoute("api-v1/route.ts"),
363
+ parseRoute("my_resource/route.ts"),
364
+ ]
365
+
366
+ const code = FileRouterCodegen.generateCode(handles)
367
+
368
+ t
369
+ .expect(code)
370
+ .toContain("path: \"/api-v1\"")
371
+ t
372
+ .expect(code)
373
+ .toContain("path: \"/my_resource\"")
374
+ t
375
+ .expect(code)
376
+ .toContain("{ literal: \"api-v1\" }")
377
+ t
378
+ .expect(code)
379
+ .toContain("{ literal: \"my_resource\" }")
380
+ })
381
+
382
+ t.it("validateRouteModule returns true for valid modules", () => {
383
+ const validRoute = Route.text(Effect.succeed("Hello"))
384
+
385
+ t
386
+ .expect(
387
+ FileRouterCodegen.validateRouteModule({ default: validRoute }),
388
+ )
389
+ .toBe(true)
390
+
391
+ t
392
+ .expect(
393
+ FileRouterCodegen.validateRouteModule({
394
+ default: Route.html(Effect.succeed("<div>Hello</div>")),
395
+ }),
396
+ )
397
+ .toBe(true)
398
+
399
+ t
400
+ .expect(
401
+ FileRouterCodegen.validateRouteModule({
402
+ default: Route.json(Effect.succeed({ message: "Hello" })),
403
+ }),
404
+ )
405
+ .toBe(true)
406
+ })
407
+
408
+ t.it("validateRouteModule returns false for invalid modules", () => {
409
+ t
410
+ .expect(FileRouterCodegen.validateRouteModule({}))
411
+ .toBe(false)
412
+
413
+ t
414
+ .expect(
415
+ FileRouterCodegen.validateRouteModule({ default: {} }),
416
+ )
417
+ .toBe(false)
418
+
419
+ t
420
+ .expect(
421
+ FileRouterCodegen.validateRouteModule({ default: "not a route" }),
422
+ )
423
+ .toBe(false)
424
+
425
+ t
426
+ .expect(
427
+ FileRouterCodegen.validateRouteModule({ foo: "bar" }),
428
+ )
429
+ .toBe(false)
430
+
431
+ t
432
+ .expect(FileRouterCodegen.validateRouteModule(null))
433
+ .toBe(false)
434
+
435
+ t
436
+ .expect(FileRouterCodegen.validateRouteModule(undefined))
437
+ .toBe(false)
438
+
439
+ t
440
+ .expect(FileRouterCodegen.validateRouteModule("string"))
441
+ .toBe(false)
442
+
443
+ t
444
+ .expect(FileRouterCodegen.validateRouteModule(42))
445
+ .toBe(false)
446
+ })
447
+
448
+ t.it("mixed params and rest in same route", () => {
449
+ const handles: RouteHandle[] = [
450
+ parseRoute("users/[userId]/files/[...path]/route.tsx"),
451
+ ]
452
+
453
+ const code = FileRouterCodegen.generateCode(handles)
454
+
455
+ t
456
+ .expect(code)
457
+ .toContain("{ param: \"userId\" }")
458
+ t
459
+ .expect(code)
460
+ .toContain("{ rest: \"path\" }")
461
+ })
462
+
463
+ const effect = effectFn()
464
+
465
+ const update_FilesWithRoutes = {
466
+ "/routes/route.tsx": "",
467
+ "/routes/about/route.tsx": "",
468
+ "/routes/_manifest.ts": "",
469
+ }
470
+
471
+ t.it("update() > writes file", () =>
472
+ Effect
473
+ .gen(function*() {
474
+ yield* FileRouterCodegen.update("/routes")
475
+
476
+ const fs = yield* FileSystem.FileSystem
477
+ const content = yield* fs.readFileString("/routes/_manifest.ts")
478
+
479
+ t
480
+ .expect(content)
481
+ .toContain("export const modules =")
482
+ })
483
+ .pipe(
484
+ Effect.provide(MemoryFileSystem.layerWith(update_FilesWithRoutes)),
485
+ Effect.runPromise,
486
+ ))
487
+
488
+ t.it("update() > writes only when it changes", () =>
489
+ Effect
490
+ .gen(function*() {
491
+ yield* FileRouterCodegen.update("/routes")
492
+
493
+ const fs = yield* FileSystem.FileSystem
494
+ const content = yield* fs.readFileString("/routes/_manifest.ts")
495
+
496
+ yield* FileRouterCodegen.update("/routes")
497
+
498
+ const content2 = yield* fs.readFileString("/routes/_manifest.ts")
499
+
500
+ t
501
+ .expect(content2)
502
+ .not
503
+ .toBe("")
504
+
505
+ t
506
+ .expect(content2)
507
+ .toBe(content)
508
+ })
509
+ .pipe(
510
+ Effect.provide(MemoryFileSystem.layerWith(update_FilesWithRoutes)),
511
+ Effect.runPromise,
512
+ ))
513
+
514
+ t.it("update() > removes deleted routes from manifest", () =>
515
+ Effect
516
+ .gen(function*() {
517
+ const fs = yield* FileSystem.FileSystem
518
+
519
+ yield* FileRouterCodegen.update("/routes")
520
+
521
+ const content = yield* fs.readFileString("/routes/_manifest.ts")
522
+
523
+ t
524
+ .expect(content)
525
+ .toContain("path: \"/\"")
526
+
527
+ t
528
+ .expect(content)
529
+ .toContain("path: \"/about\"")
530
+
531
+ yield* fs.remove("/routes/about/route.tsx")
532
+
533
+ yield* FileRouterCodegen.update("/routes")
534
+
535
+ const content2 = yield* fs.readFileString("/routes/_manifest.ts")
536
+
537
+ t
538
+ .expect(content2)
539
+ .toContain("path: \"/\"")
540
+
541
+ t
542
+ .expect(content2)
543
+ .not
544
+ .toContain("path: \"/about\"")
545
+ })
546
+ .pipe(
547
+ Effect.provide(MemoryFileSystem.layerWith(update_FilesWithRoutes)),
548
+ Effect.runPromise,
549
+ ))
550
+
551
+ t.it("update() > removes routes when entire directory is deleted", () =>
552
+ Effect
553
+ .gen(function*() {
554
+ const fs = yield* FileSystem.FileSystem
555
+
556
+ yield* fs.makeDirectory("/routes/users", { recursive: true })
557
+
558
+ yield* fs.writeFileString("/routes/users/route.tsx", "")
559
+
560
+ yield* FileRouterCodegen.update("/routes")
561
+
562
+ const content = yield* fs.readFileString("/routes/_manifest.ts")
563
+
564
+ t
565
+ .expect(content)
566
+ .toContain("path: \"/\"")
567
+
568
+ t
569
+ .expect(content)
570
+ .toContain("path: \"/about\"")
571
+
572
+ t
573
+ .expect(content)
574
+ .toContain("path: \"/users\"")
575
+
576
+ yield* fs.remove("/routes/users", { recursive: true })
577
+
578
+ yield* FileRouterCodegen.update("/routes")
579
+
580
+ const content2 = yield* fs.readFileString("/routes/_manifest.ts")
581
+
582
+ t
583
+ .expect(content2)
584
+ .toContain("path: \"/\"")
585
+
586
+ t
587
+ .expect(content2)
588
+ .toContain("path: \"/about\"")
589
+
590
+ t
591
+ .expect(content2)
592
+ .not
593
+ .toContain("path: \"/users\"")
594
+ })
595
+ .pipe(
596
+ Effect.provide(MemoryFileSystem.layerWith(update_FilesWithRoutes)),
597
+ Effect.runPromise,
598
+ ))