effect-start 0.14.0 → 0.15.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 (81) hide show
  1. package/package.json +8 -9
  2. package/src/Commander.test.ts +507 -245
  3. package/src/ContentNegotiation.test.ts +500 -0
  4. package/src/ContentNegotiation.ts +535 -0
  5. package/src/FileRouter.ts +16 -12
  6. package/src/{FileRouterCodegen.test.ts → FileRouterCodegen.todo.ts} +384 -219
  7. package/src/FileRouterCodegen.ts +6 -6
  8. package/src/FileRouterPattern.test.ts +93 -62
  9. package/src/FileRouter_files.test.ts +5 -5
  10. package/src/FileRouter_path.test.ts +121 -69
  11. package/src/FileRouter_tree.test.ts +62 -56
  12. package/src/FileSystemExtra.test.ts +46 -30
  13. package/src/Http.test.ts +24 -0
  14. package/src/Http.ts +25 -0
  15. package/src/HttpAppExtra.test.ts +39 -20
  16. package/src/HttpAppExtra.ts +0 -1
  17. package/src/HttpUtils.test.ts +35 -18
  18. package/src/HttpUtils.ts +2 -0
  19. package/src/PathPattern.test.ts +648 -0
  20. package/src/PathPattern.ts +483 -0
  21. package/src/Route.ts +258 -1073
  22. package/src/RouteBody.test.ts +182 -0
  23. package/src/RouteBody.ts +106 -0
  24. package/src/RouteHook.test.ts +40 -0
  25. package/src/RouteHook.ts +105 -0
  26. package/src/RouteHttp.test.ts +443 -0
  27. package/src/RouteHttp.ts +219 -0
  28. package/src/RouteMount.test.ts +468 -0
  29. package/src/RouteMount.ts +313 -0
  30. package/src/RouteSchema.test.ts +81 -0
  31. package/src/RouteSchema.ts +44 -0
  32. package/src/RouteTree.test.ts +346 -0
  33. package/src/RouteTree.ts +165 -0
  34. package/src/RouteTrie.test.ts +322 -0
  35. package/src/RouteTrie.ts +224 -0
  36. package/src/RouterPattern.test.ts +569 -548
  37. package/src/RouterPattern.ts +7 -7
  38. package/src/Start.ts +3 -3
  39. package/src/TuplePathPattern.ts +64 -0
  40. package/src/Values.ts +16 -0
  41. package/src/bun/BunBundle.test.ts +36 -42
  42. package/src/bun/BunBundle.ts +2 -2
  43. package/src/bun/BunBundle_imports.test.ts +4 -6
  44. package/src/bun/BunHttpServer.test.ts +183 -6
  45. package/src/bun/BunHttpServer.ts +56 -32
  46. package/src/bun/BunHttpServer_web.ts +18 -6
  47. package/src/bun/BunImportTrackerPlugin.test.ts +3 -3
  48. package/src/bun/BunRoute.ts +29 -210
  49. package/src/{BundleHttp.test.ts → bundler/BundleHttp.test.ts} +34 -60
  50. package/src/{BundleHttp.ts → bundler/BundleHttp.ts} +1 -2
  51. package/src/client/index.ts +1 -1
  52. package/src/{Effect_HttpRouter.test.ts → effect/HttpRouter.test.ts} +69 -90
  53. package/src/experimental/EncryptedCookies.test.ts +125 -64
  54. package/src/experimental/SseHttpResponse.ts +0 -1
  55. package/src/hyper/Hyper.ts +89 -0
  56. package/src/{HyperHtml.test.ts → hyper/HyperHtml.test.ts} +13 -13
  57. package/src/{HyperHtml.ts → hyper/HyperHtml.ts} +2 -2
  58. package/src/{jsx.d.ts → hyper/jsx.d.ts} +1 -1
  59. package/src/index.ts +2 -4
  60. package/src/middlewares/BasicAuthMiddleware.test.ts +29 -19
  61. package/src/{NodeFileSystem.ts → node/FileSystem.ts} +6 -2
  62. package/src/testing/TestHttpClient.test.ts +26 -26
  63. package/src/testing/TestLogger.test.ts +27 -11
  64. package/src/x/datastar/Datastar.test.ts +47 -48
  65. package/src/x/datastar/Datastar.ts +1 -1
  66. package/src/x/tailwind/TailwindPlugin.test.ts +56 -58
  67. package/src/x/tailwind/plugin.ts +1 -1
  68. package/src/FileHttpRouter.test.ts +0 -239
  69. package/src/FileHttpRouter.ts +0 -194
  70. package/src/Hyper.ts +0 -194
  71. package/src/Route.test.ts +0 -1370
  72. package/src/RouteRender.ts +0 -40
  73. package/src/Router.test.ts +0 -375
  74. package/src/Router.ts +0 -255
  75. package/src/bun/BunRoute.test.ts +0 -480
  76. package/src/bun/BunRoute_bundles.test.ts +0 -219
  77. /package/src/{Bundle.ts → bundler/Bundle.ts} +0 -0
  78. /package/src/{BundleFiles.ts → bundler/BundleFiles.ts} +0 -0
  79. /package/src/{HyperNode.ts → hyper/HyperNode.ts} +0 -0
  80. /package/src/{jsx-runtime.ts → hyper/jsx-runtime.ts} +0 -0
  81. /package/src/{NodeUtils.ts → node/Utils.ts} +0 -0
@@ -0,0 +1,500 @@
1
+ import * as Headers from "@effect/platform/Headers"
2
+ import * as test from "bun:test"
3
+ import * as ContentNegotiation from "./ContentNegotiation.ts"
4
+
5
+ test.describe("ContentNegotiation.media", () => {
6
+ test.it("returns empty array when no types provided", () => {
7
+ const result = ContentNegotiation.media("text/html", [])
8
+ test
9
+ .expect(result)
10
+ .toEqual([])
11
+ })
12
+
13
+ test.it("returns matching type", () => {
14
+ const result = ContentNegotiation.media(
15
+ "application/json",
16
+ ["text/html", "application/json"],
17
+ )
18
+ test
19
+ .expect(result)
20
+ .toEqual(["application/json"])
21
+ })
22
+
23
+ test.it("returns types sorted by quality", () => {
24
+ const result = ContentNegotiation.media(
25
+ "text/html;q=0.5, application/json;q=0.9",
26
+ ["text/html", "application/json"],
27
+ )
28
+ test
29
+ .expect(result)
30
+ .toEqual(["application/json", "text/html"])
31
+ })
32
+
33
+ test.it("returns empty array when no matching type", () => {
34
+ const result = ContentNegotiation.media(
35
+ "text/plain",
36
+ ["text/html", "application/json"],
37
+ )
38
+ test
39
+ .expect(result)
40
+ .toEqual([])
41
+ })
42
+
43
+ test.it("handles wildcard subtype", () => {
44
+ const result = ContentNegotiation.media(
45
+ "text/*",
46
+ ["application/json", "text/html", "text/plain"],
47
+ )
48
+ test
49
+ .expect(result)
50
+ .toEqual(["text/html", "text/plain"])
51
+ })
52
+
53
+ test.it("prefers exact match over wildcard", () => {
54
+ const result = ContentNegotiation.media(
55
+ "text/*, text/html",
56
+ ["text/plain", "text/html"],
57
+ )
58
+ test
59
+ .expect(result)
60
+ .toEqual(["text/html", "text/plain"])
61
+ })
62
+
63
+ test.it("handles complex accept header", () => {
64
+ const result = ContentNegotiation.media(
65
+ "text/html, application/*;q=0.2, image/jpeg;q=0.8",
66
+ ["image/jpeg", "application/json", "text/html"],
67
+ )
68
+ test
69
+ .expect(result)
70
+ .toEqual(["text/html", "image/jpeg", "application/json"])
71
+ })
72
+
73
+ test.it("returns type as provided (preserves original string)", () => {
74
+ const result = ContentNegotiation.media(
75
+ "application/json",
76
+ ["text/HTML", "Application/JSON"],
77
+ )
78
+ test
79
+ .expect(result)
80
+ .toEqual(["Application/JSON"])
81
+ })
82
+
83
+ test.it("handles */* wildcard", () => {
84
+ const result = ContentNegotiation.media(
85
+ "*/*",
86
+ ["text/html", "application/json"],
87
+ )
88
+ test
89
+ .expect(result)
90
+ .toEqual(["text/html", "application/json"])
91
+ })
92
+
93
+ test.it("returns empty array for invalid accept header", () => {
94
+ const result = ContentNegotiation.media(
95
+ "invalid",
96
+ ["text/html", "application/json"],
97
+ )
98
+ test
99
+ .expect(result)
100
+ .toEqual([])
101
+ })
102
+
103
+ test.it("returns all accepted types when available not provided", () => {
104
+ const result = ContentNegotiation.media(
105
+ "text/html, application/json;q=0.9, text/plain;q=0.5",
106
+ )
107
+ test
108
+ .expect(result)
109
+ .toEqual(["text/html", "application/json", "text/plain"])
110
+ })
111
+
112
+ test.it("returns empty array for empty accept header", () => {
113
+ const result = ContentNegotiation.media("", [
114
+ "text/html",
115
+ "application/json",
116
+ ])
117
+ test
118
+ .expect(result)
119
+ .toEqual([])
120
+ })
121
+
122
+ test.it("excludes types with q=0", () => {
123
+ const result = ContentNegotiation.media(
124
+ "text/html, application/json;q=0",
125
+ ["text/html", "application/json"],
126
+ )
127
+ test
128
+ .expect(result)
129
+ .toEqual(["text/html"])
130
+ })
131
+
132
+ test.it("matches media type with parameters", () => {
133
+ const result = ContentNegotiation.media(
134
+ "text/html;level=1",
135
+ ["text/html;level=1", "text/html;level=2", "text/html"],
136
+ )
137
+ test
138
+ .expect(result)
139
+ .toEqual(["text/html;level=1"])
140
+ })
141
+
142
+ test.it("prefers more specific wildcard match", () => {
143
+ const result = ContentNegotiation.media(
144
+ "text/*;q=0.5, */*;q=0.1",
145
+ ["text/html", "application/json"],
146
+ )
147
+ test
148
+ .expect(result)
149
+ .toEqual(["text/html", "application/json"])
150
+ })
151
+ })
152
+
153
+ test.describe("ContentNegotiation.language", () => {
154
+ test.it("returns empty array when no languages provided", () => {
155
+ const result = ContentNegotiation.language("en", [])
156
+ test
157
+ .expect(result)
158
+ .toEqual([])
159
+ })
160
+
161
+ test.it("returns matching language", () => {
162
+ const result = ContentNegotiation.language("fr", ["en", "fr"])
163
+ test
164
+ .expect(result)
165
+ .toEqual(["fr"])
166
+ })
167
+
168
+ test.it("returns languages sorted by quality", () => {
169
+ const result = ContentNegotiation.language(
170
+ "en;q=0.5, fr;q=0.9",
171
+ ["en", "fr"],
172
+ )
173
+ test
174
+ .expect(result)
175
+ .toEqual(["fr", "en"])
176
+ })
177
+
178
+ test.it("returns empty array when no matching language", () => {
179
+ const result = ContentNegotiation.language("de", ["en", "fr"])
180
+ test
181
+ .expect(result)
182
+ .toEqual([])
183
+ })
184
+
185
+ test.it("handles language prefix match", () => {
186
+ const result = ContentNegotiation.language("en", ["en-US", "en-GB", "fr"])
187
+ test
188
+ .expect(result)
189
+ .toEqual(["en-US", "en-GB"])
190
+ })
191
+
192
+ test.it("handles language with region", () => {
193
+ const result = ContentNegotiation.language("en-US", ["en", "en-US", "fr"])
194
+ test
195
+ .expect(result)
196
+ .toEqual(["en-US"])
197
+ })
198
+
199
+ test.it("prefers exact match over prefix match", () => {
200
+ const result = ContentNegotiation.language(
201
+ "en-US, en;q=0.9",
202
+ ["en", "en-US"],
203
+ )
204
+ test
205
+ .expect(result)
206
+ .toEqual(["en-US", "en"])
207
+ })
208
+
209
+ test.it("handles complex accept-language header", () => {
210
+ const result = ContentNegotiation.language(
211
+ "en;q=0.8, es, pt",
212
+ ["en", "es", "pt"],
213
+ )
214
+ test
215
+ .expect(result)
216
+ .toEqual(["es", "pt", "en"])
217
+ })
218
+
219
+ test.it("handles * wildcard", () => {
220
+ const result = ContentNegotiation.language("*", ["en", "fr"])
221
+ test
222
+ .expect(result)
223
+ .toEqual(["en", "fr"])
224
+ })
225
+
226
+ test.it("returns all accepted languages when available not provided", () => {
227
+ const result = ContentNegotiation.language("en-US, fr;q=0.8, de;q=0.5")
228
+ test
229
+ .expect(result)
230
+ .toEqual(["en-us", "fr", "de"])
231
+ })
232
+
233
+ test.it("returns empty array for empty accept-language header", () => {
234
+ const result = ContentNegotiation.language("", ["en", "fr"])
235
+ test
236
+ .expect(result)
237
+ .toEqual([])
238
+ })
239
+
240
+ test.it("matches case-insensitively", () => {
241
+ const result = ContentNegotiation.language("EN-US", ["en-us", "fr"])
242
+ test
243
+ .expect(result)
244
+ .toEqual(["en-us"])
245
+ })
246
+ })
247
+
248
+ test.describe("ContentNegotiation.encoding", () => {
249
+ test.it("returns empty array when no encodings provided", () => {
250
+ const result = ContentNegotiation.encoding("gzip", [])
251
+ test
252
+ .expect(result)
253
+ .toEqual([])
254
+ })
255
+
256
+ test.it("returns matching encoding", () => {
257
+ const result = ContentNegotiation.encoding("deflate", ["gzip", "deflate"])
258
+ test
259
+ .expect(result)
260
+ .toEqual(["deflate"])
261
+ })
262
+
263
+ test.it("returns encodings sorted by quality", () => {
264
+ const result = ContentNegotiation.encoding(
265
+ "gzip;q=0.5, deflate;q=0.9",
266
+ ["gzip", "deflate"],
267
+ )
268
+ test
269
+ .expect(result)
270
+ .toEqual(["deflate", "gzip"])
271
+ })
272
+
273
+ test.it(
274
+ "returns empty array when no matching encoding (except identity)",
275
+ () => {
276
+ const result = ContentNegotiation.encoding("br", ["gzip", "deflate"])
277
+ test
278
+ .expect(result)
279
+ .toEqual([])
280
+ },
281
+ )
282
+
283
+ test.it("handles wildcard", () => {
284
+ const result = ContentNegotiation.encoding("*", ["gzip", "deflate"])
285
+ test
286
+ .expect(result)
287
+ .toEqual(["gzip", "deflate"])
288
+ })
289
+
290
+ test.it("handles identity encoding as implicit fallback", () => {
291
+ const result = ContentNegotiation.encoding("br", ["identity", "gzip"])
292
+ test
293
+ .expect(result)
294
+ .toEqual(["identity"])
295
+ })
296
+
297
+ test.it("handles complex accept-encoding header", () => {
298
+ const result = ContentNegotiation.encoding(
299
+ "gzip;q=1.0, identity;q=0.5, *;q=0",
300
+ ["deflate", "gzip", "identity"],
301
+ )
302
+ test
303
+ .expect(result)
304
+ .toEqual(["gzip", "identity"])
305
+ })
306
+
307
+ test.it("returns all accepted encodings when available not provided", () => {
308
+ const result = ContentNegotiation.encoding("gzip, deflate;q=0.8, br;q=0.5")
309
+ test
310
+ .expect(result)
311
+ .toEqual(["gzip", "deflate", "br", "identity"])
312
+ })
313
+
314
+ test.it("returns empty array for empty accept-encoding header", () => {
315
+ const result = ContentNegotiation.encoding("", ["gzip", "deflate"])
316
+ test
317
+ .expect(result)
318
+ .toEqual([])
319
+ })
320
+
321
+ test.it("excludes identity when identity;q=0", () => {
322
+ const result = ContentNegotiation.encoding(
323
+ "gzip, identity;q=0",
324
+ ["gzip", "identity"],
325
+ )
326
+ test
327
+ .expect(result)
328
+ .toEqual(["gzip"])
329
+ })
330
+
331
+ test.it("excludes unspecified encodings when *;q=0", () => {
332
+ const result = ContentNegotiation.encoding(
333
+ "gzip, *;q=0",
334
+ ["gzip", "deflate", "br"],
335
+ )
336
+ test
337
+ .expect(result)
338
+ .toEqual(["gzip"])
339
+ })
340
+ })
341
+
342
+ test.describe("ContentNegotiation.charset", () => {
343
+ test.it("returns empty array when no charsets provided", () => {
344
+ const result = ContentNegotiation.charset("utf-8", [])
345
+ test
346
+ .expect(result)
347
+ .toEqual([])
348
+ })
349
+
350
+ test.it("returns matching charset", () => {
351
+ const result = ContentNegotiation.charset(
352
+ "iso-8859-1",
353
+ ["utf-8", "iso-8859-1"],
354
+ )
355
+ test
356
+ .expect(result)
357
+ .toEqual(["iso-8859-1"])
358
+ })
359
+
360
+ test.it("returns charsets sorted by quality", () => {
361
+ const result = ContentNegotiation.charset(
362
+ "utf-8;q=0.5, iso-8859-1;q=0.9",
363
+ ["utf-8", "iso-8859-1"],
364
+ )
365
+ test
366
+ .expect(result)
367
+ .toEqual(["iso-8859-1", "utf-8"])
368
+ })
369
+
370
+ test.it("returns empty array when no matching charset", () => {
371
+ const result = ContentNegotiation.charset(
372
+ "utf-16",
373
+ ["utf-8", "iso-8859-1"],
374
+ )
375
+ test
376
+ .expect(result)
377
+ .toEqual([])
378
+ })
379
+
380
+ test.it("handles wildcard", () => {
381
+ const result = ContentNegotiation.charset("*", ["utf-8", "iso-8859-1"])
382
+ test
383
+ .expect(result)
384
+ .toEqual(["utf-8", "iso-8859-1"])
385
+ })
386
+
387
+ test.it("handles complex accept-charset header", () => {
388
+ const result = ContentNegotiation.charset(
389
+ "utf-8, iso-8859-1;q=0.8, utf-7;q=0.2",
390
+ ["utf-7", "iso-8859-1", "utf-8"],
391
+ )
392
+ test
393
+ .expect(result)
394
+ .toEqual(["utf-8", "iso-8859-1", "utf-7"])
395
+ })
396
+
397
+ test.it("returns all accepted charsets when available not provided", () => {
398
+ const result = ContentNegotiation.charset(
399
+ "utf-8, iso-8859-1;q=0.8, utf-7;q=0.2",
400
+ )
401
+ test
402
+ .expect(result)
403
+ .toEqual(["utf-8", "iso-8859-1", "utf-7"])
404
+ })
405
+
406
+ test.it("returns empty array for empty accept-charset header", () => {
407
+ const result = ContentNegotiation.charset("", ["utf-8", "iso-8859-1"])
408
+ test
409
+ .expect(result)
410
+ .toEqual([])
411
+ })
412
+
413
+ test.it("matches case-insensitively", () => {
414
+ const result = ContentNegotiation.charset("UTF-8", ["utf-8", "iso-8859-1"])
415
+ test
416
+ .expect(result)
417
+ .toEqual(["utf-8"])
418
+ })
419
+ })
420
+
421
+ test.describe("ContentNegotiation.headerMedia", () => {
422
+ test.it("parses Accept header from Headers object", () => {
423
+ const headers = Headers.fromInput({ accept: "text/html, application/json" })
424
+ const result = ContentNegotiation.headerMedia(headers, [
425
+ "text/html",
426
+ "application/json",
427
+ ])
428
+ test
429
+ .expect(result)
430
+ .toEqual(["text/html", "application/json"])
431
+ })
432
+
433
+ test.it("returns empty array when Accept header is missing", () => {
434
+ const headers = Headers.fromInput({})
435
+ const result = ContentNegotiation.headerMedia(headers, ["text/html"])
436
+ test
437
+ .expect(result)
438
+ .toEqual([])
439
+ })
440
+ })
441
+
442
+ test.describe("ContentNegotiation.headerLanguage", () => {
443
+ test.it("parses Accept-Language header from Headers object", () => {
444
+ const headers = Headers.fromInput({ "accept-language": "en, fr;q=0.8" })
445
+ const result = ContentNegotiation.headerLanguage(headers, ["en", "fr"])
446
+ test
447
+ .expect(result)
448
+ .toEqual(["en", "fr"])
449
+ })
450
+
451
+ test.it("returns empty array when Accept-Language header is missing", () => {
452
+ const headers = Headers.fromInput({})
453
+ const result = ContentNegotiation.headerLanguage(headers, ["en"])
454
+ test
455
+ .expect(result)
456
+ .toEqual([])
457
+ })
458
+ })
459
+
460
+ test.describe("ContentNegotiation.headerEncoding", () => {
461
+ test.it("parses Accept-Encoding header from Headers object", () => {
462
+ const headers = Headers.fromInput({ "accept-encoding": "gzip, deflate" })
463
+ const result = ContentNegotiation.headerEncoding(headers, [
464
+ "gzip",
465
+ "deflate",
466
+ ])
467
+ test
468
+ .expect(result)
469
+ .toEqual(["gzip", "deflate"])
470
+ })
471
+
472
+ test.it("returns empty array when Accept-Encoding header is missing", () => {
473
+ const headers = Headers.fromInput({})
474
+ const result = ContentNegotiation.headerEncoding(headers, ["gzip"])
475
+ test
476
+ .expect(result)
477
+ .toEqual([])
478
+ })
479
+ })
480
+
481
+ test.describe("ContentNegotiation.headerCharset", () => {
482
+ test.it("parses Accept-Charset header from Headers object", () => {
483
+ const headers = Headers.fromInput({ "accept-charset": "utf-8, iso-8859-1" })
484
+ const result = ContentNegotiation.headerCharset(headers, [
485
+ "utf-8",
486
+ "iso-8859-1",
487
+ ])
488
+ test
489
+ .expect(result)
490
+ .toEqual(["utf-8", "iso-8859-1"])
491
+ })
492
+
493
+ test.it("returns empty array when Accept-Charset header is missing", () => {
494
+ const headers = Headers.fromInput({})
495
+ const result = ContentNegotiation.headerCharset(headers, ["utf-8"])
496
+ test
497
+ .expect(result)
498
+ .toEqual([])
499
+ })
500
+ })