effect-start 0.9.0 → 0.10.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 (44) hide show
  1. package/package.json +12 -13
  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 +81 -12
  7. package/src/FileHttpRouter.ts +115 -26
  8. package/src/FileRouter.ts +60 -162
  9. package/src/FileRouterCodegen.test.ts +250 -64
  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/HttpUtils.test.ts +68 -0
  17. package/src/HttpUtils.ts +15 -0
  18. package/src/HyperHtml.ts +24 -5
  19. package/src/JsModule.test.ts +1 -1
  20. package/src/NodeFileSystem.ts +764 -0
  21. package/src/Random.ts +59 -0
  22. package/src/Route.test.ts +471 -0
  23. package/src/Route.ts +298 -153
  24. package/src/RouteRender.ts +38 -0
  25. package/src/Router.ts +11 -33
  26. package/src/RouterPattern.test.ts +629 -0
  27. package/src/RouterPattern.ts +391 -0
  28. package/src/Start.ts +14 -52
  29. package/src/bun/BunBundle.test.ts +0 -3
  30. package/src/bun/BunHttpServer.ts +246 -0
  31. package/src/bun/BunHttpServer_web.ts +384 -0
  32. package/src/bun/BunRoute.test.ts +341 -0
  33. package/src/bun/BunRoute.ts +326 -0
  34. package/src/bun/BunRoute_bundles.test.ts +218 -0
  35. package/src/bun/BunRuntime.ts +33 -0
  36. package/src/bun/BunTailwindPlugin.test.ts +1 -1
  37. package/src/bun/_empty.html +1 -0
  38. package/src/bun/index.ts +2 -1
  39. package/src/testing.ts +12 -3
  40. package/src/Datastar.test.ts +0 -267
  41. package/src/Datastar.ts +0 -68
  42. package/src/bun/BunFullstackServer.ts +0 -45
  43. package/src/bun/BunFullstackServer_httpServer.ts +0 -541
  44. package/src/jsx-datastar.d.ts +0 -63
@@ -22,17 +22,13 @@ t.it("generates code for routes only", () => {
22
22
 
23
23
  import type { Router } from "effect-start"
24
24
 
25
- export const modules = [
25
+ export const routes = [
26
26
  {
27
27
  path: "/",
28
- segments: [],
29
28
  load: () => import("./route.tsx"),
30
29
  },
31
30
  {
32
31
  path: "/about",
33
- segments: [
34
- { literal: "about" },
35
- ],
36
32
  load: () => import("./about/route.tsx"),
37
33
  },
38
34
  ] as const
@@ -58,10 +54,9 @@ t.it("generates code with layers", () => {
58
54
 
59
55
  import type { Router } from "effect-start"
60
56
 
61
- export const modules = [
57
+ export const routes = [
62
58
  {
63
59
  path: "/",
64
- segments: [],
65
60
  load: () => import("./route.tsx"),
66
61
  layers: [
67
62
  () => import("./layer.tsx"),
@@ -69,9 +64,6 @@ export const modules = [
69
64
  },
70
65
  {
71
66
  path: "/about",
72
- segments: [
73
- { literal: "about" },
74
- ],
75
67
  load: () => import("./about/route.tsx"),
76
68
  layers: [
77
69
  () => import("./layer.tsx"),
@@ -101,12 +93,9 @@ t.it("generates code with nested layers", () => {
101
93
 
102
94
  import type { Router } from "effect-start"
103
95
 
104
- export const modules = [
96
+ export const routes = [
105
97
  {
106
98
  path: "/dashboard",
107
- segments: [
108
- { literal: "dashboard" },
109
- ],
110
99
  load: () => import("./dashboard/route.tsx"),
111
100
  layers: [
112
101
  () => import("./layer.tsx"),
@@ -115,10 +104,6 @@ export const modules = [
115
104
  },
116
105
  {
117
106
  path: "/dashboard/settings",
118
- segments: [
119
- { literal: "dashboard" },
120
- { literal: "settings" },
121
- ],
122
107
  load: () => import("./dashboard/settings/route.tsx"),
123
108
  layers: [
124
109
  () => import("./layer.tsx"),
@@ -163,9 +148,6 @@ t.it("only includes group layers for routes in that group", () => {
163
148
  // /movies should only have root layer, not (admin) layer
164
149
  const expectedMovies = ` {
165
150
  path: "/movies",
166
- segments: [
167
- { literal: "movies" },
168
- ],
169
151
  load: () => import("./movies/route.tsx"),
170
152
  layers: [
171
153
  () => import("./layer.tsx"),
@@ -194,16 +176,7 @@ t.it("handles dynamic routes with params", () => {
194
176
  .toContain("path: \"/users/[userId]\"")
195
177
  t
196
178
  .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\" }")
179
+ .toContain("path: \"/posts/[postId]/comments/[commentId]\"")
207
180
  })
208
181
 
209
182
  t.it("handles rest parameters", () => {
@@ -220,12 +193,6 @@ t.it("handles rest parameters", () => {
220
193
  t
221
194
  .expect(code)
222
195
  .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
196
  })
230
197
 
231
198
  t.it("handles groups in path", () => {
@@ -239,9 +206,6 @@ t.it("handles groups in path", () => {
239
206
  t
240
207
  .expect(code)
241
208
  .toContain("path: \"/users\"") // groups stripped from URL
242
- t
243
- .expect(code)
244
- .toContain("{ group: \"admin\" }")
245
209
  t
246
210
  .expect(code)
247
211
  .toContain("layers: [\n () => import(\"./(admin)/layer.tsx\"),\n ]")
@@ -257,9 +221,6 @@ t.it("generates correct variable names for root routes", () => {
257
221
  t
258
222
  .expect(code)
259
223
  .toContain("path: \"/\"")
260
- t
261
- .expect(code)
262
- .toContain("segments: []")
263
224
  })
264
225
 
265
226
  t.it("handles routes with dots in path segments", () => {
@@ -276,12 +237,6 @@ t.it("handles routes with dots in path segments", () => {
276
237
  t
277
238
  .expect(code)
278
239
  .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
240
  })
286
241
 
287
242
  t.it("uses default module identifier", () => {
@@ -296,17 +251,17 @@ t.it("uses default module identifier", () => {
296
251
  .toContain("import type { Router } from \"effect-start\"")
297
252
  })
298
253
 
299
- t.it("generates empty modules array when no handles provided", () => {
254
+ t.it("generates empty routes array when no handles provided", () => {
300
255
  const handles: RouteHandle[] = []
301
256
 
302
257
  const code = FileRouterCodegen.generateCode(handles)
303
258
 
304
259
  t
305
260
  .expect(code)
306
- .toContain("export const modules = [] as const")
261
+ .toContain("export const routes = [] as const")
307
262
  })
308
263
 
309
- t.it("only includes routes in modules, not layers", () => {
264
+ t.it("only includes routes, not layers", () => {
310
265
  const handles: RouteHandle[] = [
311
266
  parseRoute("layer.tsx"),
312
267
  parseRoute("users/layer.tsx"),
@@ -316,7 +271,7 @@ t.it("only includes routes in modules, not layers", () => {
316
271
 
317
272
  t
318
273
  .expect(code)
319
- .toContain("export const modules = [] as const")
274
+ .toContain("export const routes = [] as const")
320
275
  })
321
276
 
322
277
  t.it("complex nested routes with multiple layers", () => {
@@ -371,12 +326,6 @@ t.it("handles routes with hyphens and underscores in path segments", () => {
371
326
  t
372
327
  .expect(code)
373
328
  .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
329
  })
381
330
 
382
331
  t.it("validateRouteModule returns true for valid modules", () => {
@@ -454,10 +403,247 @@ t.it("mixed params and rest in same route", () => {
454
403
 
455
404
  t
456
405
  .expect(code)
457
- .toContain("{ param: \"userId\" }")
458
- t
459
- .expect(code)
460
- .toContain("{ rest: \"path\" }")
406
+ .toContain("path: \"/users/[userId]/files/[...path]\"")
407
+ })
408
+
409
+ t.describe("layerMatchesRoute", () => {
410
+ t.it("layer in dynamic param dir only applies to routes in that dir", () => {
411
+ const handles: RouteHandle[] = [
412
+ parseRoute("[userId]/layer.tsx"),
413
+ parseRoute("[userId]/posts/route.tsx"),
414
+ parseRoute("[otherId]/route.tsx"),
415
+ ]
416
+
417
+ const code = FileRouterCodegen.generateCode(handles)
418
+
419
+ const expectedUserIdPosts = ` {
420
+ path: "/[userId]/posts",
421
+ load: () => import("./[userId]/posts/route.tsx"),
422
+ layers: [
423
+ () => import("./[userId]/layer.tsx"),
424
+ ],
425
+ },`
426
+
427
+ t
428
+ .expect(code)
429
+ .toContain(expectedUserIdPosts)
430
+
431
+ const expectedOtherId = ` {
432
+ path: "/[otherId]",
433
+ load: () => import("./[otherId]/route.tsx"),
434
+ },`
435
+
436
+ t
437
+ .expect(code)
438
+ .toContain(expectedOtherId)
439
+ })
440
+
441
+ t.it("nested groups only apply to routes in those groups", () => {
442
+ const handles: RouteHandle[] = [
443
+ parseRoute("layer.tsx"),
444
+ parseRoute("(admin)/(dashboard)/layer.tsx"),
445
+ parseRoute("(admin)/(dashboard)/users/route.tsx"),
446
+ parseRoute("(admin)/settings/route.tsx"),
447
+ parseRoute("(other)/(dashboard)/route.tsx"),
448
+ ]
449
+
450
+ const code = FileRouterCodegen.generateCode(handles)
451
+
452
+ const expectedAdminDashboardUsers = ` {
453
+ path: "/users",
454
+ load: () => import("./(admin)/(dashboard)/users/route.tsx"),
455
+ layers: [
456
+ () => import("./layer.tsx"),
457
+ () => import("./(admin)/(dashboard)/layer.tsx"),
458
+ ],
459
+ },`
460
+
461
+ t
462
+ .expect(code)
463
+ .toContain(expectedAdminDashboardUsers)
464
+
465
+ const expectedAdminSettings = ` {
466
+ path: "/settings",
467
+ load: () => import("./(admin)/settings/route.tsx"),
468
+ layers: [
469
+ () => import("./layer.tsx"),
470
+ ],
471
+ },`
472
+
473
+ t
474
+ .expect(code)
475
+ .toContain(expectedAdminSettings)
476
+
477
+ const expectedOtherDashboard = ` {
478
+ path: "/",
479
+ load: () => import("./(other)/(dashboard)/route.tsx"),
480
+ layers: [
481
+ () => import("./layer.tsx"),
482
+ ],
483
+ },`
484
+
485
+ t
486
+ .expect(code)
487
+ .toContain(expectedOtherDashboard)
488
+ })
489
+
490
+ t.it("similar directory names do not match (user vs users)", () => {
491
+ const handles: RouteHandle[] = [
492
+ parseRoute("user/layer.tsx"),
493
+ parseRoute("user/route.tsx"),
494
+ parseRoute("users/route.tsx"),
495
+ ]
496
+
497
+ const code = FileRouterCodegen.generateCode(handles)
498
+
499
+ const expectedUser = ` {
500
+ path: "/user",
501
+ load: () => import("./user/route.tsx"),
502
+ layers: [
503
+ () => import("./user/layer.tsx"),
504
+ ],
505
+ },`
506
+
507
+ t
508
+ .expect(code)
509
+ .toContain(expectedUser)
510
+
511
+ const expectedUsers = ` {
512
+ path: "/users",
513
+ load: () => import("./users/route.tsx"),
514
+ },`
515
+
516
+ t
517
+ .expect(code)
518
+ .toContain(expectedUsers)
519
+ })
520
+
521
+ t.it("mixed groups and literals layer matching", () => {
522
+ const handles: RouteHandle[] = [
523
+ parseRoute("(admin)/users/layer.tsx"),
524
+ parseRoute("(admin)/users/[userId]/route.tsx"),
525
+ parseRoute("users/route.tsx"),
526
+ parseRoute("(admin)/posts/route.tsx"),
527
+ ]
528
+
529
+ const code = FileRouterCodegen.generateCode(handles)
530
+
531
+ const expectedAdminUsersId = ` {
532
+ path: "/users/[userId]",
533
+ load: () => import("./(admin)/users/[userId]/route.tsx"),
534
+ layers: [
535
+ () => import("./(admin)/users/layer.tsx"),
536
+ ],
537
+ },`
538
+
539
+ t
540
+ .expect(code)
541
+ .toContain(expectedAdminUsersId)
542
+
543
+ const expectedUsers = ` {
544
+ path: "/users",
545
+ load: () => import("./users/route.tsx"),
546
+ },`
547
+
548
+ t
549
+ .expect(code)
550
+ .toContain(expectedUsers)
551
+
552
+ const expectedAdminPosts = ` {
553
+ path: "/posts",
554
+ load: () => import("./(admin)/posts/route.tsx"),
555
+ },`
556
+
557
+ t
558
+ .expect(code)
559
+ .toContain(expectedAdminPosts)
560
+ })
561
+
562
+ t.it("param directory layer only applies to routes in that dir", () => {
563
+ const handles: RouteHandle[] = [
564
+ parseRoute("[tenantId]/layer.tsx"),
565
+ parseRoute("[tenantId]/settings/route.tsx"),
566
+ parseRoute("other/route.tsx"),
567
+ ]
568
+
569
+ const code = FileRouterCodegen.generateCode(handles)
570
+
571
+ const expectedTenantSettings = ` {
572
+ path: "/[tenantId]/settings",
573
+ load: () => import("./[tenantId]/settings/route.tsx"),
574
+ layers: [
575
+ () => import("./[tenantId]/layer.tsx"),
576
+ ],
577
+ },`
578
+
579
+ t
580
+ .expect(code)
581
+ .toContain(expectedTenantSettings)
582
+
583
+ const expectedOther = ` {
584
+ path: "/other",
585
+ load: () => import("./other/route.tsx"),
586
+ },`
587
+
588
+ t
589
+ .expect(code)
590
+ .toContain(expectedOther)
591
+ })
592
+
593
+ t.it(
594
+ "optional param directory layer only applies to routes in that dir",
595
+ () => {
596
+ const handles: RouteHandle[] = [
597
+ parseRoute("[[id]]/layer.tsx"),
598
+ parseRoute("[[id]]/settings/route.tsx"),
599
+ parseRoute("other/route.tsx"),
600
+ ]
601
+
602
+ const code = FileRouterCodegen.generateCode(handles)
603
+
604
+ const expectedIdSettings = ` {
605
+ path: "/[[id]]/settings",
606
+ load: () => import("./[[id]]/settings/route.tsx"),
607
+ layers: [
608
+ () => import("./[[id]]/layer.tsx"),
609
+ ],
610
+ },`
611
+
612
+ t
613
+ .expect(code)
614
+ .toContain(expectedIdSettings)
615
+
616
+ const expectedOther = ` {
617
+ path: "/other",
618
+ load: () => import("./other/route.tsx"),
619
+ },`
620
+
621
+ t
622
+ .expect(code)
623
+ .toContain(expectedOther)
624
+ },
625
+ )
626
+
627
+ t.it("layer and route at same directory level", () => {
628
+ const handles: RouteHandle[] = [
629
+ parseRoute("users/layer.tsx"),
630
+ parseRoute("users/route.tsx"),
631
+ ]
632
+
633
+ const code = FileRouterCodegen.generateCode(handles)
634
+
635
+ const expected = ` {
636
+ path: "/users",
637
+ load: () => import("./users/route.tsx"),
638
+ layers: [
639
+ () => import("./users/layer.tsx"),
640
+ ],
641
+ },`
642
+
643
+ t
644
+ .expect(code)
645
+ .toContain(expected)
646
+ })
461
647
  })
462
648
 
463
649
  const effect = effectFn()
@@ -478,7 +664,7 @@ t.it("update() > writes file", () =>
478
664
 
479
665
  t
480
666
  .expect(content)
481
- .toContain("export const modules =")
667
+ .toContain("export const routes =")
482
668
  })
483
669
  .pipe(
484
670
  Effect.provide(MemoryFileSystem.layerWith(update_FilesWithRoutes)),
@@ -52,29 +52,6 @@ export function validateRouteModules(
52
52
  })
53
53
  }
54
54
 
55
- /**
56
- * Converts a segment to RouteModuleSegment format
57
- */
58
- function segmentToModuleSegment(segment: FileRouter.Segment): string | null {
59
- if ("literal" in segment) {
60
- return `{ literal: "${segment.literal}" }`
61
- }
62
- if ("group" in segment) {
63
- return `{ group: "${segment.group}" }`
64
- }
65
- if ("param" in segment) {
66
- return segment.optional
67
- ? `{ param: "${segment.param}", optional: true }`
68
- : `{ param: "${segment.param}" }`
69
- }
70
- if ("rest" in segment) {
71
- return segment.optional
72
- ? `{ rest: "${segment.rest}", optional: true }`
73
- : `{ rest: "${segment.rest}" }`
74
- }
75
- return null
76
- }
77
-
78
55
  export function generateCode(
79
56
  handles: FileRouter.OrderedRouteHandles,
80
57
  ): string {
@@ -96,30 +73,22 @@ export function generateCode(
96
73
  routesByPath.set(handle.routePath, existing)
97
74
  }
98
75
 
99
- // Generate module definitions
100
- const modules: string[] = []
76
+ // Generate route definitions
77
+ const routes: string[] = []
101
78
 
102
79
  // Helper to check if layer's path is an ancestor of route's path
103
80
  const layerMatchesRoute = (
104
81
  layer: FileRouter.RouteHandle,
105
82
  route: FileRouter.RouteHandle,
106
83
  ): boolean => {
107
- // Exclude handle segment (last segment) from comparison
108
- const layerLength = layer.segments.length - 1
109
- const routeLength = route.segments.length - 1
84
+ // Get the directory of the layer (strip the filename like layer.tsx)
85
+ const layerDir = layer.modulePath.replace(/\/?(layer)\.(tsx?|jsx?)$/, "")
110
86
 
111
- // Layer's segments must be a prefix of route's segments
112
- if (layerLength > routeLength) {
113
- return false
114
- }
87
+ // Layer at root (empty layerDir) applies to all routes
88
+ if (layerDir === "") return true
115
89
 
116
- for (let i = 0; i < layerLength; i++) {
117
- if (!FileRouter.isSegmentEqual(layer.segments[i], route.segments[i])) {
118
- return false
119
- }
120
- }
121
-
122
- return true
90
+ // Route's modulePath must start with the layer's directory
91
+ return route.modulePath.startsWith(layerDir + "/")
123
92
  }
124
93
 
125
94
  // Find layers for each route by walking up the path hierarchy
@@ -146,17 +115,6 @@ export function generateCode(
146
115
  currentPath = parentPath || "/"
147
116
  }
148
117
 
149
- // Generate segments array
150
- const pathSegments = route.segments.filter(seg => !("handle" in seg))
151
- const segmentsCode = pathSegments
152
- .map(segmentToModuleSegment)
153
- .filter(Boolean)
154
- .join(",\n ") + (pathSegments.length > 0 ? "," : "")
155
-
156
- const segmentsArray = segmentsCode
157
- ? `[\n ${segmentsCode}\n ]`
158
- : "[]"
159
-
160
118
  // Generate layers array
161
119
  const layersCode = allLayers.length > 0
162
120
  ? `\n layers: [\n ${
@@ -166,28 +124,27 @@ export function generateCode(
166
124
  },\n ],`
167
125
  : ""
168
126
 
169
- const moduleCode = ` {
127
+ const routeCode = ` {
170
128
  path: "${path}",
171
- segments: ${segmentsArray},
172
129
  load: () => import("./${route.modulePath}"),${layersCode}
173
130
  },`
174
131
 
175
- modules.push(moduleCode)
132
+ routes.push(routeCode)
176
133
  }
177
134
 
178
135
  const header = `/**
179
136
  * Auto-generated by effect-start.
180
137
  */`
181
138
 
182
- const modulesArray = modules.length > 0
183
- ? `[\n${modules.join("\n")}\n]`
139
+ const routesArray = routes.length > 0
140
+ ? `[\n${routes.join("\n")}\n]`
184
141
  : "[]"
185
142
 
186
143
  return `${header}
187
144
 
188
145
  import type { Router } from "${routerModuleId}"
189
146
 
190
- export const modules = ${modulesArray} as const
147
+ export const routes = ${routesArray} as const
191
148
  `
192
149
  }
193
150
 
@@ -0,0 +1,116 @@
1
+ import * as t from "bun:test"
2
+ import * as FileRouterPattern from "./FileRouterPattern.ts"
3
+
4
+ t.it("empty path", () => {
5
+ t.expect(FileRouterPattern.parse("")).toEqual([])
6
+ t.expect(FileRouterPattern.parse("/")).toEqual([])
7
+ })
8
+
9
+ t.it("groups", () => {
10
+ t.expect(FileRouterPattern.parse("(admin)")).toEqual([
11
+ { _tag: "GroupSegment", name: "admin" },
12
+ ])
13
+ t.expect(FileRouterPattern.parse("/(admin)/users")).toEqual([
14
+ { _tag: "GroupSegment", name: "admin" },
15
+ { _tag: "LiteralSegment", value: "users" },
16
+ ])
17
+ t.expect(FileRouterPattern.parse("(auth)/login/(step1)")).toEqual([
18
+ { _tag: "GroupSegment", name: "auth" },
19
+ { _tag: "LiteralSegment", value: "login" },
20
+ { _tag: "GroupSegment", name: "step1" },
21
+ ])
22
+ })
23
+
24
+ t.it("handle files parsed as Literal", () => {
25
+ t.expect(FileRouterPattern.parse("route.ts")).toEqual([
26
+ { _tag: "LiteralSegment", value: "route.ts" },
27
+ ])
28
+ t.expect(FileRouterPattern.parse("/api/route.js")).toEqual([
29
+ { _tag: "LiteralSegment", value: "api" },
30
+ { _tag: "LiteralSegment", value: "route.js" },
31
+ ])
32
+ t.expect(FileRouterPattern.parse("layer.tsx")).toEqual([
33
+ { _tag: "LiteralSegment", value: "layer.tsx" },
34
+ ])
35
+ t.expect(FileRouterPattern.parse("/blog/layer.jsx")).toEqual([
36
+ { _tag: "LiteralSegment", value: "blog" },
37
+ { _tag: "LiteralSegment", value: "layer.jsx" },
38
+ ])
39
+ })
40
+
41
+ t.it("params and rest", () => {
42
+ t.expect(FileRouterPattern.parse("users/[userId]/posts")).toEqual([
43
+ { _tag: "LiteralSegment", value: "users" },
44
+ { _tag: "ParamSegment", name: "userId" },
45
+ { _tag: "LiteralSegment", value: "posts" },
46
+ ])
47
+
48
+ t.expect(FileRouterPattern.parse("api/[[...path]]")).toEqual([
49
+ { _tag: "LiteralSegment", value: "api" },
50
+ { _tag: "RestSegment", name: "path", optional: true },
51
+ ])
52
+ })
53
+
54
+ t.it("invalid paths", () => {
55
+ t.expect(() => FileRouterPattern.parse("$...")).toThrow()
56
+ t.expect(() => FileRouterPattern.parse("invalid%char")).toThrow()
57
+ t.expect(() => FileRouterPattern.parse("path with spaces")).toThrow()
58
+ })
59
+
60
+ t.it("segments with extensions (literal with dots)", () => {
61
+ t.expect(FileRouterPattern.parse("events.json/route.ts")).toEqual([
62
+ { _tag: "LiteralSegment", value: "events.json" },
63
+ { _tag: "LiteralSegment", value: "route.ts" },
64
+ ])
65
+ })
66
+
67
+ t.it("formatSegment", () => {
68
+ t
69
+ .expect(
70
+ FileRouterPattern.formatSegment({
71
+ _tag: "LiteralSegment",
72
+ value: "users",
73
+ }),
74
+ )
75
+ .toBe("users")
76
+ t
77
+ .expect(
78
+ FileRouterPattern.formatSegment({ _tag: "ParamSegment", name: "id" }),
79
+ )
80
+ .toBe("[id]")
81
+ t
82
+ .expect(
83
+ FileRouterPattern.formatSegment({ _tag: "GroupSegment", name: "admin" }),
84
+ )
85
+ .toBe("(admin)")
86
+ t
87
+ .expect(
88
+ FileRouterPattern.formatSegment({ _tag: "RestSegment", name: "path" }),
89
+ )
90
+ .toBe("[...path]")
91
+ })
92
+
93
+ t.it("format", () => {
94
+ t.expect(FileRouterPattern.format([])).toBe("/")
95
+ t
96
+ .expect(
97
+ FileRouterPattern.format([{ _tag: "LiteralSegment", value: "users" }]),
98
+ )
99
+ .toBe("/users")
100
+ t
101
+ .expect(
102
+ FileRouterPattern.format([
103
+ { _tag: "GroupSegment", name: "admin" },
104
+ { _tag: "LiteralSegment", value: "users" },
105
+ ]),
106
+ )
107
+ .toBe("/(admin)/users")
108
+ t
109
+ .expect(
110
+ FileRouterPattern.format([
111
+ { _tag: "LiteralSegment", value: "users" },
112
+ { _tag: "ParamSegment", name: "id" },
113
+ ]),
114
+ )
115
+ .toBe("/users/[id]")
116
+ })