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,873 @@
1
+ import * as t from "bun:test"
2
+ import * as Effect from "effect/Effect"
3
+ import * as Function from "effect/Function"
4
+ import * as Schema from "effect/Schema"
5
+ import * as Route from "./Route.ts"
6
+
7
+ t.it("types default routes", () => {
8
+ const implicit = Route
9
+ .text(
10
+ Effect.succeed("hello"),
11
+ )
12
+ .html(
13
+ Effect.succeed(""),
14
+ )
15
+
16
+ const explicit = Route
17
+ .get(
18
+ Route
19
+ .text(
20
+ Effect.succeed("hello"),
21
+ )
22
+ .html(
23
+ Effect.succeed(""),
24
+ ),
25
+ )
26
+
27
+ type Expected = Route.RouteSet<[
28
+ Route.Route<"GET", "text/plain">,
29
+ Route.Route<"GET", "text/html">,
30
+ ]>
31
+
32
+ Function.satisfies<Expected>()(implicit)
33
+ Function.satisfies<Expected>()(explicit)
34
+ })
35
+
36
+ t.it("types GET & POST routes", () => {
37
+ const implicit = Route
38
+ .text(
39
+ Effect.succeed("hello"),
40
+ )
41
+ .html(
42
+ Effect.succeed(""),
43
+ )
44
+ .post(
45
+ Route.json(
46
+ Effect.succeed({
47
+ message: "created",
48
+ }),
49
+ ),
50
+ )
51
+
52
+ const explicit = Route
53
+ .get(
54
+ Route
55
+ .text(
56
+ Effect.succeed("hello"),
57
+ )
58
+ .html(
59
+ Effect.succeed(""),
60
+ ),
61
+ )
62
+ .post(
63
+ Route.json(
64
+ Effect.succeed({
65
+ message: "created",
66
+ }),
67
+ ),
68
+ )
69
+
70
+ type Expected = Route.RouteSet<[
71
+ Route.Route<"GET", "text/plain">,
72
+ Route.Route<"GET", "text/html">,
73
+ Route.Route<"POST", "application/json">,
74
+ ]>
75
+
76
+ Function.satisfies<Expected>()(implicit)
77
+ Function.satisfies<Expected>()(explicit)
78
+ })
79
+
80
+ t.it("schemaPathParams adds schema to RouteSet", () => {
81
+ const IdSchema = Schema.Struct({
82
+ id: Schema.String,
83
+ })
84
+
85
+ const routes = Route
86
+ .schemaPathParams(IdSchema)
87
+ .text(
88
+ Effect.succeed("hello"),
89
+ )
90
+
91
+ type ExpectedSchemas = {
92
+ readonly PathParams: typeof IdSchema
93
+ }
94
+
95
+ type Expected = Route.RouteSet<
96
+ [Route.Route<"GET", "text/plain", any, ExpectedSchemas>],
97
+ ExpectedSchemas
98
+ >
99
+
100
+ Function.satisfies<Expected>()(routes)
101
+ })
102
+
103
+ t.it("schemaPathParams accepts struct fields directly", () => {
104
+ const routes = Route
105
+ .schemaPathParams({
106
+ id: Schema.String,
107
+ })
108
+ .text(
109
+ Effect.succeed("hello"),
110
+ )
111
+
112
+ type ExpectedSchemas = {
113
+ readonly PathParams: Schema.Struct<{
114
+ id: typeof Schema.String
115
+ }>
116
+ }
117
+
118
+ type Expected = Route.RouteSet<
119
+ [Route.Route<"GET", "text/plain", any, ExpectedSchemas>],
120
+ ExpectedSchemas
121
+ >
122
+
123
+ Function.satisfies<Expected>()(routes)
124
+ })
125
+
126
+ t.it("schemaPathParams with struct fields types context correctly", () => {
127
+ Route
128
+ .schemaPathParams({
129
+ id: Schema.String,
130
+ })
131
+ .text(
132
+ (context) => {
133
+ Function.satisfies<string>()(context.pathParams.id)
134
+
135
+ return Effect.succeed("hello")
136
+ },
137
+ )
138
+ })
139
+
140
+ t.it("schemaPayload propagates to all routes", () => {
141
+ const PayloadSchema = Schema.Struct({
142
+ name: Schema.String,
143
+ age: Schema.Number,
144
+ })
145
+
146
+ const routes = Route
147
+ .schemaPayload(PayloadSchema)
148
+ .get(
149
+ Route.text(
150
+ Effect.succeed("get"),
151
+ ),
152
+ )
153
+ .post(
154
+ Route.text(
155
+ Effect.succeed("post"),
156
+ ),
157
+ )
158
+
159
+ type ExpectedSchemas = {
160
+ readonly Payload: typeof PayloadSchema
161
+ }
162
+
163
+ type Expected = Route.RouteSet<
164
+ [
165
+ Route.Route<"GET", "text/plain", any, ExpectedSchemas>,
166
+ Route.Route<"POST", "text/plain", any, ExpectedSchemas>,
167
+ ],
168
+ ExpectedSchemas
169
+ >
170
+
171
+ Function.satisfies<Expected>()(routes)
172
+ })
173
+
174
+ t.it(
175
+ "context is typed with pathParams when schemaPathParams is provided",
176
+ () => {
177
+ const IdSchema = Schema.Struct({
178
+ id: Schema.String,
179
+ })
180
+
181
+ Route
182
+ .schemaPathParams(IdSchema)
183
+ .text(
184
+ (context) => {
185
+ type ContextType = typeof context
186
+
187
+ type Expected = Route.RouteContext<{
188
+ pathParams: {
189
+ id: string
190
+ }
191
+ }>
192
+
193
+ Function.satisfies<Expected>()(context)
194
+
195
+ Function.satisfies<string>()(context.pathParams.id)
196
+
197
+ return Effect.succeed("hello")
198
+ },
199
+ )
200
+ },
201
+ )
202
+
203
+ t.it("context is typed with urlParams when schemaUrlParams is provided", () => {
204
+ const QuerySchema = Schema.Struct({
205
+ page: Schema.NumberFromString,
206
+ limit: Schema.NumberFromString,
207
+ })
208
+
209
+ Route
210
+ .schemaUrlParams(QuerySchema)
211
+ .text(
212
+ (context) => {
213
+ type Expected = Route.RouteContext<{
214
+ urlParams: {
215
+ page: number
216
+ limit: number
217
+ }
218
+ }>
219
+
220
+ Function.satisfies<Expected>()(context)
221
+
222
+ Function.satisfies<number>()(context.urlParams.page)
223
+
224
+ Function.satisfies<number>()(context.urlParams.limit)
225
+
226
+ return Effect.succeed("hello")
227
+ },
228
+ )
229
+ })
230
+
231
+ t.it("context is typed with payload when schemaPayload is provided", () => {
232
+ const PayloadSchema = Schema.Struct({
233
+ name: Schema.String,
234
+ age: Schema.Number,
235
+ })
236
+
237
+ Route
238
+ .schemaPayload(PayloadSchema)
239
+ .text(
240
+ (context) => {
241
+ type Expected = Route.RouteContext<{
242
+ payload: {
243
+ name: string
244
+ age: number
245
+ }
246
+ }>
247
+
248
+ Function.satisfies<Expected>()(context)
249
+
250
+ Function.satisfies<string>()(context.payload.name)
251
+
252
+ Function.satisfies<number>()(context.payload.age)
253
+
254
+ return Effect.succeed("hello")
255
+ },
256
+ )
257
+ })
258
+
259
+ t.it("context is typed with headers when schemaHeaders is provided", () => {
260
+ const HeadersSchema = Schema.Struct({
261
+ authorization: Schema.String,
262
+ })
263
+
264
+ Route
265
+ .schemaHeaders(HeadersSchema)
266
+ .text(
267
+ (context) => {
268
+ type Expected = Route.RouteContext<{
269
+ headers: {
270
+ authorization: string
271
+ }
272
+ }>
273
+
274
+ Function.satisfies<Expected>()(context)
275
+
276
+ Function.satisfies<string>()(context.headers.authorization)
277
+
278
+ return Effect.succeed("hello")
279
+ },
280
+ )
281
+ })
282
+
283
+ t.it("context is typed with multiple schemas", () => {
284
+ const IdSchema = Schema.Struct({
285
+ id: Schema.String,
286
+ })
287
+
288
+ const QuerySchema = Schema.Struct({
289
+ page: Schema.NumberFromString,
290
+ })
291
+
292
+ const PayloadSchema = Schema.Struct({
293
+ name: Schema.String,
294
+ })
295
+
296
+ Route
297
+ .schemaPathParams(IdSchema)
298
+ .schemaUrlParams(QuerySchema)
299
+ .schemaPayload(PayloadSchema)
300
+ .text(
301
+ (context) => {
302
+ type Expected = Route.RouteContext<{
303
+ pathParams: {
304
+ id: string
305
+ }
306
+ urlParams: {
307
+ page: number
308
+ }
309
+ payload: {
310
+ name: string
311
+ }
312
+ }>
313
+
314
+ Function.satisfies<Expected>()(context)
315
+
316
+ Function.satisfies<string>()(context.pathParams.id)
317
+
318
+ Function.satisfies<number>()(context.urlParams.page)
319
+
320
+ Function.satisfies<string>()(context.payload.name)
321
+
322
+ return Effect.succeed("hello")
323
+ },
324
+ )
325
+ })
326
+
327
+ t.it("schemaSuccess and schemaError are stored in RouteSet", () => {
328
+ const SuccessSchema = Schema.Struct({
329
+ ok: Schema.Boolean,
330
+ })
331
+
332
+ const ErrorSchema = Schema.Struct({
333
+ error: Schema.String,
334
+ })
335
+
336
+ const routes = Route
337
+ .schemaSuccess(SuccessSchema)
338
+ .schemaError(ErrorSchema)
339
+ .text(
340
+ Effect.succeed("hello"),
341
+ )
342
+
343
+ type ExpectedSchemas = {
344
+ readonly Success: typeof SuccessSchema
345
+ readonly Error: typeof ErrorSchema
346
+ }
347
+
348
+ type Expected = Route.RouteSet<
349
+ [Route.Route<"GET", "text/plain", any, ExpectedSchemas>],
350
+ ExpectedSchemas
351
+ >
352
+
353
+ Function.satisfies<Expected>()(routes)
354
+ })
355
+
356
+ t.it("all schema methods work together", () => {
357
+ const PathSchema = Schema.Struct({
358
+ id: Schema.String,
359
+ })
360
+
361
+ const QuerySchema = Schema.Struct({
362
+ page: Schema.NumberFromString,
363
+ })
364
+
365
+ const PayloadSchema = Schema.Struct({
366
+ name: Schema.String,
367
+ })
368
+
369
+ const SuccessSchema = Schema.Struct({
370
+ ok: Schema.Boolean,
371
+ })
372
+
373
+ const ErrorSchema = Schema.Struct({
374
+ error: Schema.String,
375
+ })
376
+
377
+ const HeadersSchema = Schema.Struct({
378
+ authorization: Schema.String,
379
+ })
380
+
381
+ const routes = Route
382
+ .schemaPathParams(PathSchema)
383
+ .schemaUrlParams(QuerySchema)
384
+ .schemaPayload(PayloadSchema)
385
+ .schemaSuccess(SuccessSchema)
386
+ .schemaError(ErrorSchema)
387
+ .schemaHeaders(HeadersSchema)
388
+ .text(
389
+ Effect.succeed("hello"),
390
+ )
391
+
392
+ type ExpectedSchemas = {
393
+ readonly PathParams: typeof PathSchema
394
+ readonly UrlParams: typeof QuerySchema
395
+ readonly Payload: typeof PayloadSchema
396
+ readonly Success: typeof SuccessSchema
397
+ readonly Error: typeof ErrorSchema
398
+ readonly Headers: typeof HeadersSchema
399
+ }
400
+
401
+ type Expected = Route.RouteSet<
402
+ [Route.Route<"GET", "text/plain", any, ExpectedSchemas>],
403
+ ExpectedSchemas
404
+ >
405
+
406
+ Function.satisfies<Expected>()(routes)
407
+ })
408
+
409
+ t.it("schemas merge when RouteSet and Route both define same schema", () => {
410
+ const BaseSchema = Schema.Struct({
411
+ id: Schema.String,
412
+ })
413
+
414
+ const ExtendedSchema = Schema.Struct({
415
+ name: Schema.String,
416
+ })
417
+
418
+ const routes = Route
419
+ .schemaPathParams(BaseSchema)
420
+ .get(
421
+ Route
422
+ .schemaPathParams(ExtendedSchema)
423
+ .text(Effect.succeed("hello")),
424
+ )
425
+
426
+ type Expected = Route.RouteSet<
427
+ [
428
+ Route.Route<
429
+ "GET",
430
+ "text/plain",
431
+ any,
432
+ {
433
+ readonly PathParams: Schema.Struct<
434
+ {
435
+ id: typeof Schema.String
436
+ name: typeof Schema.String
437
+ }
438
+ >
439
+ }
440
+ >,
441
+ ],
442
+ {
443
+ readonly PathParams: typeof BaseSchema
444
+ }
445
+ >
446
+
447
+ Function.satisfies<Expected>()(routes)
448
+ })
449
+
450
+ t.it("context has only request and url when no schemas provided", () => {
451
+ Route
452
+ .text(
453
+ (context) => {
454
+ type Expected = Route.RouteContext<{}>
455
+
456
+ Function.satisfies<Expected>()(context)
457
+
458
+ Function.satisfies<typeof context.request>()(context.request)
459
+
460
+ Function.satisfies<URL>()(context.url)
461
+
462
+ // @ts-expect-error - pathParams should not exist
463
+ context.pathParams
464
+
465
+ return Effect.succeed("hello")
466
+ },
467
+ )
468
+ })
469
+
470
+ t.it("schemas work with all media types", () => {
471
+ const PathSchema = Schema.Struct({
472
+ id: Schema.String,
473
+ })
474
+
475
+ Route
476
+ .schemaPathParams(PathSchema)
477
+ .html(
478
+ (context) => {
479
+ Function.satisfies<string>()(context.pathParams.id)
480
+
481
+ return Effect.succeed("<h1>Hello</h1>")
482
+ },
483
+ )
484
+
485
+ Route
486
+ .schemaPathParams(PathSchema)
487
+ .json(
488
+ (context) => {
489
+ Function.satisfies<string>()(context.pathParams.id)
490
+
491
+ return Effect.succeed({ message: "hello" })
492
+ },
493
+ )
494
+ })
495
+
496
+ t.it("schemas work with generator functions", () => {
497
+ const IdSchema = Schema.Struct({
498
+ id: Schema.String,
499
+ })
500
+
501
+ Route
502
+ .schemaPathParams(IdSchema)
503
+ .text(
504
+ function*(context) {
505
+ Function.satisfies<string>()(context.pathParams.id)
506
+
507
+ return "hello"
508
+ },
509
+ )
510
+ })
511
+
512
+ t.it("schema property is correctly set on RouteSet", () => {
513
+ const PathSchema = Schema.Struct({
514
+ id: Schema.String,
515
+ })
516
+
517
+ const routes = Route
518
+ .schemaPathParams(PathSchema)
519
+ .text(Effect.succeed("hello"))
520
+
521
+ type Expected = {
522
+ readonly PathParams: typeof PathSchema
523
+ }
524
+
525
+ Function.satisfies<Expected>()(routes.schema)
526
+ })
527
+
528
+ t.it("schemas don't leak between independent route chains", () => {
529
+ const Schema1 = Schema.Struct({
530
+ id: Schema.String,
531
+ })
532
+
533
+ const Schema2 = Schema.Struct({
534
+ userId: Schema.String,
535
+ })
536
+
537
+ const route1 = Route
538
+ .schemaPathParams(Schema1)
539
+ .text(Effect.succeed("route1"))
540
+
541
+ const route2 = Route
542
+ .schemaPathParams(Schema2)
543
+ .text(Effect.succeed("route2"))
544
+
545
+ type Expected1 = Route.RouteSet<
546
+ [
547
+ Route.Route<
548
+ "GET",
549
+ "text/plain",
550
+ any,
551
+ { readonly PathParams: typeof Schema1 }
552
+ >,
553
+ ],
554
+ { readonly PathParams: typeof Schema1 }
555
+ >
556
+
557
+ type Expected2 = Route.RouteSet<
558
+ [
559
+ Route.Route<
560
+ "GET",
561
+ "text/plain",
562
+ any,
563
+ { readonly PathParams: typeof Schema2 }
564
+ >,
565
+ ],
566
+ { readonly PathParams: typeof Schema2 }
567
+ >
568
+
569
+ Function.satisfies<Expected1>()(route1)
570
+ Function.satisfies<Expected2>()(route2)
571
+ })
572
+
573
+ t.it("schema order doesn't matter", () => {
574
+ const PathSchema = Schema.Struct({
575
+ id: Schema.String,
576
+ })
577
+
578
+ const PayloadSchema = Schema.Struct({
579
+ name: Schema.String,
580
+ })
581
+
582
+ const routes1 = Route
583
+ .schemaPathParams(PathSchema)
584
+ .schemaPayload(PayloadSchema)
585
+ .text(Effect.succeed("hello"))
586
+
587
+ const routes2 = Route
588
+ .schemaPayload(PayloadSchema)
589
+ .schemaPathParams(PathSchema)
590
+ .text(Effect.succeed("hello"))
591
+
592
+ type Expected = {
593
+ readonly PathParams: typeof PathSchema
594
+ readonly Payload: typeof PayloadSchema
595
+ }
596
+
597
+ Function.satisfies<Expected>()(routes1.schema)
598
+ Function.satisfies<Expected>()(routes2.schema)
599
+ })
600
+
601
+ t.it("multiple routes in RouteSet each get the schema", () => {
602
+ const PathSchema = Schema.Struct({
603
+ id: Schema.String,
604
+ })
605
+
606
+ const routes = Route
607
+ .schemaPathParams(PathSchema)
608
+ .text(Effect.succeed("text"))
609
+ .html(Effect.succeed("<p>html</p>"))
610
+ .json(Effect.succeed({ data: "json" }))
611
+
612
+ type ExpectedSchemas = {
613
+ readonly PathParams: typeof PathSchema
614
+ }
615
+
616
+ type Expected = Route.RouteSet<
617
+ [
618
+ Route.Route<"GET", "text/plain", any, ExpectedSchemas>,
619
+ Route.Route<"GET", "text/html", any, ExpectedSchemas>,
620
+ Route.Route<"GET", "application/json", any, ExpectedSchemas>,
621
+ ],
622
+ ExpectedSchemas
623
+ >
624
+
625
+ Function.satisfies<Expected>()(routes)
626
+ })
627
+
628
+ t.it("schemas merge correctly with struct fields syntax", () => {
629
+ const routes = Route
630
+ .schemaPathParams({ id: Schema.String })
631
+ .get(
632
+ Route
633
+ .schemaPathParams({ userId: Schema.String })
634
+ .text(Effect.succeed("hello")),
635
+ )
636
+
637
+ routes
638
+ .set[0]
639
+ .text(
640
+ (context) => {
641
+ Function.satisfies<string>()(context.pathParams.id)
642
+ Function.satisfies<string>()(context.pathParams.userId)
643
+
644
+ return Effect.succeed("hello")
645
+ },
646
+ )
647
+ })
648
+
649
+ t.it("method modifiers preserve and merge schemas", () => {
650
+ const PathSchema = Schema.Struct({
651
+ id: Schema.String,
652
+ })
653
+
654
+ const PayloadSchema = Schema.Struct({
655
+ name: Schema.String,
656
+ })
657
+
658
+ const routes = Route
659
+ .schemaPathParams(PathSchema)
660
+ .post(
661
+ Route
662
+ .schemaPayload(PayloadSchema)
663
+ .text(Effect.succeed("created")),
664
+ )
665
+
666
+ type Expected = Route.RouteSet<
667
+ [
668
+ Route.Route<
669
+ "POST",
670
+ "text/plain",
671
+ any,
672
+ {
673
+ readonly PathParams: typeof PathSchema
674
+ readonly Payload: typeof PayloadSchema
675
+ }
676
+ >,
677
+ ],
678
+ {
679
+ readonly PathParams: typeof PathSchema
680
+ }
681
+ >
682
+
683
+ Function.satisfies<Expected>()(routes)
684
+ })
685
+
686
+ t.it("method modifiers require routes with handlers", () => {
687
+ const PathSchema = Schema.Struct({
688
+ id: Schema.String,
689
+ })
690
+
691
+ Route
692
+ .schemaPathParams(PathSchema)
693
+ .get(
694
+ Route
695
+ .schemaPathParams({ userId: Schema.String })
696
+ .text(Effect.succeed("hello")),
697
+ )
698
+
699
+ Route
700
+ .schemaPathParams(PathSchema)
701
+ .get(
702
+ // @ts-expect-error - method modifiers should reject empty RouteSet
703
+ Route.schemaPathParams({ userId: Schema.String }),
704
+ )
705
+ })
706
+
707
+ t.it("method modifiers preserve proper types when nesting schemas", () => {
708
+ const PathSchema = Schema.Struct({
709
+ id: Schema.String,
710
+ })
711
+
712
+ const route = Route
713
+ .schemaPathParams(PathSchema)
714
+ .get(
715
+ Route
716
+ .schemaPathParams({ userId: Schema.String })
717
+ .text(Effect.succeed("hello")),
718
+ )
719
+
720
+ type BaseSchemas = {
721
+ readonly PathParams: typeof PathSchema
722
+ }
723
+
724
+ type MergedPathParams = Schema.Struct<{
725
+ id: typeof Schema.String
726
+ userId: typeof Schema.String
727
+ }>
728
+
729
+ type Expected = Route.RouteSet<
730
+ [
731
+ Route.Route<"GET", "text/plain", any, {
732
+ readonly PathParams: MergedPathParams
733
+ }>,
734
+ ],
735
+ BaseSchemas
736
+ >
737
+
738
+ Function.satisfies<Expected>()(route)
739
+ })
740
+
741
+ t.it("schemaUrlParams accepts optional fields", () => {
742
+ const routes = Route
743
+ .schemaUrlParams({
744
+ hello: Function.pipe(
745
+ Schema.String,
746
+ Schema.optional,
747
+ ),
748
+ })
749
+ .html(
750
+ (ctx) => {
751
+ Function.satisfies<string | undefined>()(ctx.urlParams.hello)
752
+
753
+ const page = ctx.urlParams.hello ?? "default"
754
+
755
+ return Effect.succeed(`<div><h1>About ${page}</h1></div>`)
756
+ },
757
+ )
758
+
759
+ type ExpectedSchemas = {
760
+ readonly UrlParams: Schema.Struct<{
761
+ hello: Schema.optional<typeof Schema.String>
762
+ }>
763
+ }
764
+
765
+ type Expected = Route.RouteSet<
766
+ [Route.Route<"GET", "text/html", any, ExpectedSchemas>],
767
+ ExpectedSchemas
768
+ >
769
+
770
+ Function.satisfies<Expected>()(routes)
771
+ })
772
+
773
+ t.it("schemaPathParams only accepts string-encoded schemas", () => {
774
+ Route
775
+ .schemaPathParams({
776
+ id: Schema.String,
777
+ })
778
+ .text(Effect.succeed("ok"))
779
+
780
+ Route
781
+ .schemaPathParams({
782
+ id: Schema.NumberFromString,
783
+ })
784
+ .text(Effect.succeed("ok"))
785
+
786
+ Route
787
+ .schemaPathParams({
788
+ // @ts-expect-error - Schema.Number is not string-encoded
789
+ id: Schema.Number,
790
+ })
791
+ .text(Effect.succeed("ok"))
792
+
793
+ Route
794
+ .schemaPathParams({
795
+ // @ts-expect-error - Schema.Struct is not string-encoded
796
+ nested: Schema.Struct({
797
+ field: Schema.String,
798
+ }),
799
+ })
800
+ .text(Effect.succeed("ok"))
801
+ })
802
+
803
+ t.it("schemaUrlParams accepts string and string array encoded schemas", () => {
804
+ Route
805
+ .schemaUrlParams({
806
+ page: Schema.String,
807
+ })
808
+ .text(Effect.succeed("ok"))
809
+
810
+ Route
811
+ .schemaUrlParams({
812
+ page: Schema.NumberFromString,
813
+ })
814
+ .text(Effect.succeed("ok"))
815
+
816
+ Route
817
+ .schemaUrlParams({
818
+ tags: Schema.Array(Schema.String),
819
+ })
820
+ .text(Effect.succeed("ok"))
821
+
822
+ Route
823
+ .schemaUrlParams({
824
+ // @ts-expect-error - Schema.Number is not string-encoded
825
+ page: Schema.Number,
826
+ })
827
+ .text(Effect.succeed("ok"))
828
+
829
+ Route
830
+ .schemaUrlParams({
831
+ // @ts-expect-error - Schema.Struct is not string-encoded
832
+ nested: Schema.Struct({
833
+ field: Schema.String,
834
+ }),
835
+ })
836
+ .text(Effect.succeed("ok"))
837
+ })
838
+
839
+ t.it("schemaHeaders accepts string and string array encoded schemas", () => {
840
+ Route
841
+ .schemaHeaders({
842
+ authorization: Schema.String,
843
+ })
844
+ .text(Effect.succeed("ok"))
845
+
846
+ Route
847
+ .schemaHeaders({
848
+ "x-custom-header": Schema.NumberFromString,
849
+ })
850
+ .text(Effect.succeed("ok"))
851
+
852
+ Route
853
+ .schemaHeaders({
854
+ "accept-encoding": Schema.Array(Schema.String),
855
+ })
856
+ .text(Effect.succeed("ok"))
857
+
858
+ Route
859
+ .schemaHeaders({
860
+ // @ts-expect-error - Schema.Number is not string-encoded
861
+ "x-count": Schema.Number,
862
+ })
863
+ .text(Effect.succeed("ok"))
864
+
865
+ Route
866
+ .schemaHeaders({
867
+ // @ts-expect-error - Schema.Struct is not string-encoded
868
+ "x-metadata": Schema.Struct({
869
+ field: Schema.String,
870
+ }),
871
+ })
872
+ .text(Effect.succeed("ok"))
873
+ })