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.
- package/package.json +15 -14
- package/src/BundleHttp.test.ts +1 -1
- package/src/Commander.test.ts +15 -15
- package/src/Commander.ts +58 -88
- package/src/EncryptedCookies.test.ts +4 -4
- package/src/FileHttpRouter.test.ts +85 -16
- package/src/FileHttpRouter.ts +119 -32
- package/src/FileRouter.ts +62 -166
- package/src/FileRouterCodegen.test.ts +252 -66
- package/src/FileRouterCodegen.ts +13 -56
- package/src/FileRouterPattern.test.ts +116 -0
- package/src/FileRouterPattern.ts +59 -0
- package/src/FileRouter_path.test.ts +63 -102
- package/src/FileSystemExtra.test.ts +226 -0
- package/src/FileSystemExtra.ts +24 -60
- package/src/HttpAppExtra.test.ts +84 -0
- package/src/HttpAppExtra.ts +399 -47
- package/src/HttpUtils.test.ts +68 -0
- package/src/HttpUtils.ts +15 -0
- package/src/HyperHtml.ts +24 -5
- package/src/JsModule.test.ts +1 -1
- package/src/NodeFileSystem.ts +764 -0
- package/src/Random.ts +59 -0
- package/src/Route.test.ts +515 -18
- package/src/Route.ts +321 -166
- package/src/RouteRender.ts +40 -0
- package/src/Router.test.ts +416 -0
- package/src/Router.ts +288 -31
- package/src/RouterPattern.test.ts +655 -0
- package/src/RouterPattern.ts +416 -0
- package/src/Start.ts +14 -52
- package/src/TestHttpClient.test.ts +29 -0
- package/src/TestHttpClient.ts +122 -73
- package/src/assets.d.ts +39 -0
- package/src/bun/BunBundle.test.ts +0 -3
- package/src/bun/BunHttpServer.test.ts +74 -0
- package/src/bun/BunHttpServer.ts +259 -0
- package/src/bun/BunHttpServer_web.ts +384 -0
- package/src/bun/BunRoute.test.ts +514 -0
- package/src/bun/BunRoute.ts +427 -0
- package/src/bun/BunRoute_bundles.test.ts +218 -0
- package/src/bun/BunRuntime.ts +33 -0
- package/src/bun/BunTailwindPlugin.test.ts +1 -1
- package/src/bun/_empty.html +1 -0
- package/src/bun/index.ts +2 -1
- package/src/index.ts +14 -14
- package/src/middlewares/BasicAuthMiddleware.test.ts +74 -0
- package/src/middlewares/BasicAuthMiddleware.ts +36 -0
- package/src/testing.ts +12 -3
- package/src/Datastar.test.ts +0 -267
- package/src/Datastar.ts +0 -68
- package/src/bun/BunFullstackServer.ts +0 -45
- package/src/bun/BunFullstackServer_httpServer.ts +0 -541
- 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
|
|
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
|
|
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
|
|
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("
|
|
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
|
|
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
|
|
261
|
+
.toContain("export const routes = [] as const")
|
|
307
262
|
})
|
|
308
263
|
|
|
309
|
-
t.it("only includes routes
|
|
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
|
|
274
|
+
.toContain("export const routes = [] as const")
|
|
320
275
|
})
|
|
321
276
|
|
|
322
277
|
t.it("complex nested routes with multiple layers", () => {
|
|
@@ -371,16 +326,10 @@ 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", () => {
|
|
383
|
-
const validRoute = Route.text(
|
|
332
|
+
const validRoute = Route.text("Hello")
|
|
384
333
|
|
|
385
334
|
t
|
|
386
335
|
.expect(
|
|
@@ -399,7 +348,7 @@ t.it("validateRouteModule returns true for valid modules", () => {
|
|
|
399
348
|
t
|
|
400
349
|
.expect(
|
|
401
350
|
FileRouterCodegen.validateRouteModule({
|
|
402
|
-
default: Route.json(
|
|
351
|
+
default: Route.json({ message: "Hello" }),
|
|
403
352
|
}),
|
|
404
353
|
)
|
|
405
354
|
.toBe(true)
|
|
@@ -454,10 +403,247 @@ t.it("mixed params and rest in same route", () => {
|
|
|
454
403
|
|
|
455
404
|
t
|
|
456
405
|
.expect(code)
|
|
457
|
-
.toContain("
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
|
667
|
+
.toContain("export const routes =")
|
|
482
668
|
})
|
|
483
669
|
.pipe(
|
|
484
670
|
Effect.provide(MemoryFileSystem.layerWith(update_FilesWithRoutes)),
|
package/src/FileRouterCodegen.ts
CHANGED
|
@@ -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
|
|
100
|
-
const
|
|
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
|
-
//
|
|
108
|
-
const
|
|
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
|
|
112
|
-
if (
|
|
113
|
-
return false
|
|
114
|
-
}
|
|
87
|
+
// Layer at root (empty layerDir) applies to all routes
|
|
88
|
+
if (layerDir === "") return true
|
|
115
89
|
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
127
|
+
const routeCode = ` {
|
|
170
128
|
path: "${path}",
|
|
171
|
-
segments: ${segmentsArray},
|
|
172
129
|
load: () => import("./${route.modulePath}"),${layersCode}
|
|
173
130
|
},`
|
|
174
131
|
|
|
175
|
-
|
|
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
|
|
183
|
-
? `[\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
|
|
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
|
+
})
|