effect-start 0.14.0 → 0.16.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 (87) hide show
  1. package/package.json +8 -9
  2. package/src/Commander.test.ts +507 -245
  3. package/src/ContentNegotiation.test.ts +603 -0
  4. package/src/ContentNegotiation.ts +542 -0
  5. package/src/Entity.test.ts +592 -0
  6. package/src/Entity.ts +362 -0
  7. package/src/FileRouter.ts +16 -12
  8. package/src/{FileRouterCodegen.test.ts → FileRouterCodegen.todo.ts} +384 -219
  9. package/src/FileRouterCodegen.ts +6 -6
  10. package/src/FileRouterPattern.test.ts +93 -62
  11. package/src/FileRouter_files.test.ts +5 -5
  12. package/src/FileRouter_path.test.ts +121 -69
  13. package/src/FileRouter_tree.test.ts +62 -56
  14. package/src/FileSystemExtra.test.ts +46 -30
  15. package/src/Http.test.ts +319 -0
  16. package/src/Http.ts +167 -0
  17. package/src/HttpAppExtra.test.ts +39 -20
  18. package/src/HttpAppExtra.ts +0 -1
  19. package/src/HttpUtils.test.ts +35 -18
  20. package/src/HttpUtils.ts +2 -0
  21. package/src/PathPattern.test.ts +648 -0
  22. package/src/PathPattern.ts +485 -0
  23. package/src/Route.ts +266 -1069
  24. package/src/RouteBody.test.ts +234 -0
  25. package/src/RouteBody.ts +193 -0
  26. package/src/RouteHook.test.ts +40 -0
  27. package/src/RouteHook.ts +106 -0
  28. package/src/RouteHttp.test.ts +2906 -0
  29. package/src/RouteHttp.ts +427 -0
  30. package/src/RouteHttpTracer.ts +92 -0
  31. package/src/RouteMount.test.ts +481 -0
  32. package/src/RouteMount.ts +470 -0
  33. package/src/RouteSchema.test.ts +427 -0
  34. package/src/RouteSchema.ts +423 -0
  35. package/src/RouteTree.test.ts +494 -0
  36. package/src/RouteTree.ts +219 -0
  37. package/src/RouteTrie.test.ts +322 -0
  38. package/src/RouteTrie.ts +224 -0
  39. package/src/RouterPattern.test.ts +569 -548
  40. package/src/RouterPattern.ts +7 -7
  41. package/src/Start.ts +3 -3
  42. package/src/StreamExtra.ts +21 -1
  43. package/src/TuplePathPattern.ts +64 -0
  44. package/src/Values.test.ts +263 -0
  45. package/src/Values.ts +76 -0
  46. package/src/bun/BunBundle.test.ts +36 -42
  47. package/src/bun/BunBundle.ts +2 -2
  48. package/src/bun/BunBundle_imports.test.ts +4 -6
  49. package/src/bun/BunHttpServer.test.ts +183 -6
  50. package/src/bun/BunHttpServer.ts +72 -32
  51. package/src/bun/BunHttpServer_web.ts +18 -6
  52. package/src/bun/BunImportTrackerPlugin.test.ts +3 -3
  53. package/src/bun/BunRoute.test.ts +124 -442
  54. package/src/bun/BunRoute.ts +146 -286
  55. package/src/{BundleHttp.test.ts → bundler/BundleHttp.test.ts} +34 -60
  56. package/src/{BundleHttp.ts → bundler/BundleHttp.ts} +1 -2
  57. package/src/client/index.ts +1 -1
  58. package/src/{Effect_HttpRouter.test.ts → effect/HttpRouter.test.ts} +69 -90
  59. package/src/experimental/EncryptedCookies.test.ts +125 -64
  60. package/src/experimental/SseHttpResponse.ts +0 -1
  61. package/src/hyper/Hyper.ts +89 -0
  62. package/src/{HyperHtml.test.ts → hyper/HyperHtml.test.ts} +13 -13
  63. package/src/{HyperHtml.ts → hyper/HyperHtml.ts} +2 -2
  64. package/src/{jsx.d.ts → hyper/jsx.d.ts} +1 -1
  65. package/src/index.ts +3 -4
  66. package/src/middlewares/BasicAuthMiddleware.test.ts +29 -19
  67. package/src/{NodeFileSystem.ts → node/FileSystem.ts} +6 -2
  68. package/src/testing/TestHttpClient.test.ts +26 -26
  69. package/src/testing/TestLogger.test.ts +27 -14
  70. package/src/testing/TestLogger.ts +15 -9
  71. package/src/x/datastar/Datastar.test.ts +47 -48
  72. package/src/x/datastar/Datastar.ts +1 -1
  73. package/src/x/tailwind/TailwindPlugin.test.ts +56 -58
  74. package/src/x/tailwind/plugin.ts +1 -1
  75. package/src/FileHttpRouter.test.ts +0 -239
  76. package/src/FileHttpRouter.ts +0 -194
  77. package/src/Hyper.ts +0 -194
  78. package/src/Route.test.ts +0 -1370
  79. package/src/RouteRender.ts +0 -40
  80. package/src/Router.test.ts +0 -375
  81. package/src/Router.ts +0 -255
  82. package/src/bun/BunRoute_bundles.test.ts +0 -219
  83. /package/src/{Bundle.ts → bundler/Bundle.ts} +0 -0
  84. /package/src/{BundleFiles.ts → bundler/BundleFiles.ts} +0 -0
  85. /package/src/{HyperNode.ts → hyper/HyperNode.ts} +0 -0
  86. /package/src/{jsx-runtime.ts → hyper/jsx-runtime.ts} +0 -0
  87. /package/src/{NodeUtils.ts → node/Utils.ts} +0 -0
@@ -0,0 +1,542 @@
1
+ /**
2
+ * RFC 7231 Content Negotiation compatible with Express/Node.js ecosystem.
3
+ * Based on {@link https://github.com/jshttp/negotiator}
4
+ */
5
+
6
+ import type * as Headers from "@effect/platform/Headers"
7
+
8
+ interface ParsedSpec {
9
+ value: string
10
+ q: number
11
+ s: number
12
+ o: number
13
+ i: number
14
+ }
15
+
16
+ const simpleMediaTypeRegExp = /^\s*([^\s\/;]+)\/([^;\s]+)\s*(?:;(.*))?$/
17
+ const simpleLanguageRegExp = /^\s*([^\s\-;]+)(?:-([^\s;]+))?\s*(?:;(.*))?$/
18
+ const simpleEncodingRegExp = /^\s*([^\s;]+)\s*(?:;(.*))?$/
19
+ const simpleCharsetRegExp = /^\s*([^\s;]+)\s*(?:;(.*))?$/
20
+
21
+ function parseQuality(params: string | undefined): number {
22
+ if (!params) return 1
23
+ const match = params.match(/q\s*=\s*([0-9.]+)/)
24
+ if (!match) return 1
25
+ const q = parseFloat(match[1])
26
+ return isNaN(q) ? 1 : Math.min(Math.max(q, 0), 1)
27
+ }
28
+
29
+ function splitMediaTypeParams(
30
+ params: string,
31
+ ): { params: Record<string, string>; q: number } {
32
+ const result: Record<string, string> = {}
33
+ let q = 1
34
+
35
+ const parts = params.split(";")
36
+ for (const part of parts) {
37
+ const trimmed = part.trim()
38
+ const eqIndex = trimmed.indexOf("=")
39
+ if (eqIndex === -1) continue
40
+
41
+ const key = trimmed.slice(0, eqIndex).trim().toLowerCase()
42
+ let value = trimmed.slice(eqIndex + 1).trim()
43
+
44
+ if (value.startsWith("\"") && value.endsWith("\"")) {
45
+ value = value.slice(1, -1)
46
+ }
47
+
48
+ if (key === "q") {
49
+ q = parseFloat(value)
50
+ if (isNaN(q)) q = 1
51
+ q = Math.min(Math.max(q, 0), 1)
52
+ } else {
53
+ result[key] = value
54
+ }
55
+ }
56
+
57
+ return { params: result, q }
58
+ }
59
+
60
+ function parseAccept(
61
+ accept: string,
62
+ ): Array<
63
+ {
64
+ type: string
65
+ subtype: string
66
+ params: Record<string, string>
67
+ q: number
68
+ o: number
69
+ }
70
+ > {
71
+ const specs: Array<{
72
+ type: string
73
+ subtype: string
74
+ params: Record<string, string>
75
+ q: number
76
+ o: number
77
+ }> = []
78
+ const parts = accept.split(",")
79
+
80
+ for (let o = 0; o < parts.length; o++) {
81
+ const part = parts[o].trim()
82
+ if (!part) continue
83
+
84
+ const match = simpleMediaTypeRegExp.exec(part)
85
+ if (!match) continue
86
+
87
+ const type = match[1].toLowerCase()
88
+ const subtype = match[2].toLowerCase()
89
+ const { params, q } = match[3]
90
+ ? splitMediaTypeParams(match[3])
91
+ : { params: {}, q: 1 }
92
+
93
+ if (q > 0) {
94
+ specs.push({ type, subtype, params, q, o })
95
+ }
96
+ }
97
+
98
+ return specs
99
+ }
100
+
101
+ function specifyMediaType(
102
+ type: string,
103
+ subtype: string,
104
+ params: Record<string, string>,
105
+ spec: {
106
+ type: string
107
+ subtype: string
108
+ params: Record<string, string>
109
+ q: number
110
+ o: number
111
+ },
112
+ ): { q: number; s: number; o: number } | null {
113
+ let s = 0
114
+
115
+ if (spec.type === type) {
116
+ s |= 4 // exact match: highest specificity
117
+ } else if (type === "*") {
118
+ s |= 0 // server offers wildcard (e.g. */*): matches any client type
119
+ } else if (spec.type !== "*") {
120
+ // client is NOT requesting wildcard: no match
121
+ return null
122
+ }
123
+
124
+ // client requests wildcard (e.g. Accept: */*)
125
+ if (spec.subtype === subtype) {
126
+ s |= 2 // // exact match: highest specificity
127
+ } else if (subtype === "*") {
128
+ s |= 1 // server offers wildcard (e.g. text/*)
129
+ } else if (spec.subtype !== "*") {
130
+ return null // client is NOT requesting wildcard
131
+ }
132
+
133
+ // client requests wildcard (e.g. Accept: text/*): matches any server subtype
134
+ const specParams = Object.keys(spec.params)
135
+ if (specParams.length > 0) {
136
+ if (
137
+ specParams.every(
138
+ (key) =>
139
+ spec.params[key].toLowerCase() === (params[key] || "").toLowerCase(),
140
+ )
141
+ ) {
142
+ s |= 1
143
+ } else {
144
+ return null
145
+ }
146
+ }
147
+
148
+ return { q: spec.q, s, o: spec.o }
149
+ }
150
+
151
+ function getMediaTypePriority(
152
+ mediaType: string,
153
+ accepted: Array<
154
+ {
155
+ type: string
156
+ subtype: string
157
+ params: Record<string, string>
158
+ q: number
159
+ o: number
160
+ }
161
+ >,
162
+ index: number,
163
+ ): ParsedSpec {
164
+ let best: { q: number; s: number; o: number } | null = null
165
+
166
+ const match = simpleMediaTypeRegExp.exec(mediaType)
167
+ if (!match) {
168
+ return { value: mediaType, q: 0, s: 0, o: -1, i: index }
169
+ }
170
+
171
+ const type = match[1].toLowerCase()
172
+ const subtype = match[2].toLowerCase()
173
+ const { params } = match[3]
174
+ ? splitMediaTypeParams(match[3])
175
+ : { params: {} }
176
+
177
+ for (const spec of accepted) {
178
+ const result = specifyMediaType(type, subtype, params, spec)
179
+ if (
180
+ result
181
+ && (best === null
182
+ || result.s > best.s
183
+ || (result.s === best.s && result.q > best.q)
184
+ || (result.s === best.s && result.q === best.q && result.o < best.o))
185
+ ) {
186
+ best = result
187
+ }
188
+ }
189
+
190
+ return {
191
+ value: mediaType,
192
+ q: best?.q ?? 0,
193
+ s: best?.s ?? 0,
194
+ o: best?.o ?? -1,
195
+ i: index,
196
+ }
197
+ }
198
+
199
+ function parseAcceptLanguage(
200
+ accept: string,
201
+ ): Array<{ prefix: string; suffix: string | undefined; q: number; o: number }> {
202
+ const specs: Array<{
203
+ prefix: string
204
+ suffix: string | undefined
205
+ q: number
206
+ o: number
207
+ }> = []
208
+ const parts = accept.split(",")
209
+
210
+ for (let o = 0; o < parts.length; o++) {
211
+ const part = parts[o].trim()
212
+ if (!part) continue
213
+
214
+ const match = simpleLanguageRegExp.exec(part)
215
+ if (!match) continue
216
+
217
+ const prefix = match[1].toLowerCase()
218
+ const suffix = match[2]?.toLowerCase()
219
+ const q = parseQuality(match[3])
220
+
221
+ if (q > 0) {
222
+ specs.push({ prefix, suffix, q, o })
223
+ }
224
+ }
225
+
226
+ return specs
227
+ }
228
+
229
+ function specifyLanguage(
230
+ language: string,
231
+ spec: { prefix: string; suffix: string | undefined; q: number; o: number },
232
+ ): { q: number; s: number; o: number } | null {
233
+ const match = simpleLanguageRegExp.exec(language)
234
+ if (!match) return null
235
+
236
+ const prefix = match[1].toLowerCase()
237
+ const suffix = match[2]?.toLowerCase()
238
+
239
+ if (spec.prefix === "*") {
240
+ return { q: spec.q, s: 0, o: spec.o }
241
+ }
242
+
243
+ if (spec.prefix !== prefix) {
244
+ return null
245
+ }
246
+
247
+ if (spec.suffix === undefined) {
248
+ return { q: spec.q, s: suffix ? 2 : 4, o: spec.o }
249
+ }
250
+
251
+ if (spec.suffix === suffix) {
252
+ return { q: spec.q, s: 4, o: spec.o }
253
+ }
254
+
255
+ return null
256
+ }
257
+
258
+ function getLanguagePriority(
259
+ language: string,
260
+ accepted: Array<
261
+ { prefix: string; suffix: string | undefined; q: number; o: number }
262
+ >,
263
+ index: number,
264
+ ): ParsedSpec {
265
+ let best: { q: number; s: number; o: number } | null = null
266
+
267
+ for (const spec of accepted) {
268
+ const result = specifyLanguage(language, spec)
269
+ if (
270
+ result
271
+ && (best === null
272
+ || result.s > best.s
273
+ || (result.s === best.s && result.q > best.q)
274
+ || (result.s === best.s && result.q === best.q && result.o < best.o))
275
+ ) {
276
+ best = result
277
+ }
278
+ }
279
+
280
+ return {
281
+ value: language,
282
+ q: best?.q ?? 0,
283
+ s: best?.s ?? 0,
284
+ o: best?.o ?? -1,
285
+ i: index,
286
+ }
287
+ }
288
+
289
+ function parseAcceptEncoding(
290
+ accept: string,
291
+ ): Array<{ encoding: string; q: number; o: number }> {
292
+ const specs: Array<{ encoding: string; q: number; o: number }> = []
293
+ const parts = accept.split(",")
294
+ let hasIdentity = false
295
+
296
+ for (let o = 0; o < parts.length; o++) {
297
+ const part = parts[o].trim()
298
+ if (!part) continue
299
+
300
+ const match = simpleEncodingRegExp.exec(part)
301
+ if (!match) continue
302
+
303
+ const encoding = match[1].toLowerCase()
304
+ const q = parseQuality(match[2])
305
+
306
+ if (encoding === "identity") hasIdentity = true
307
+ if (encoding === "*") hasIdentity = true
308
+
309
+ if (q > 0) {
310
+ specs.push({ encoding, q, o })
311
+ }
312
+ }
313
+
314
+ if (!hasIdentity) {
315
+ specs.push({ encoding: "identity", q: 0.0001, o: specs.length })
316
+ }
317
+
318
+ return specs
319
+ }
320
+
321
+ function specifyEncoding(
322
+ encoding: string,
323
+ spec: { encoding: string; q: number; o: number },
324
+ ): { q: number; s: number; o: number } | null {
325
+ const e = encoding.toLowerCase()
326
+ const s = spec.encoding
327
+
328
+ if (s === "*" || s === e) {
329
+ return { q: spec.q, s: s === e ? 1 : 0, o: spec.o }
330
+ }
331
+
332
+ return null
333
+ }
334
+
335
+ function getEncodingPriority(
336
+ encoding: string,
337
+ accepted: Array<{ encoding: string; q: number; o: number }>,
338
+ index: number,
339
+ ): ParsedSpec {
340
+ let best: { q: number; s: number; o: number } | null = null
341
+
342
+ for (const spec of accepted) {
343
+ const result = specifyEncoding(encoding, spec)
344
+ if (
345
+ result
346
+ && (best === null
347
+ || result.s > best.s
348
+ || (result.s === best.s && result.q > best.q)
349
+ || (result.s === best.s && result.q === best.q && result.o < best.o))
350
+ ) {
351
+ best = result
352
+ }
353
+ }
354
+
355
+ return {
356
+ value: encoding,
357
+ q: best?.q ?? 0,
358
+ s: best?.s ?? 0,
359
+ o: best?.o ?? -1,
360
+ i: index,
361
+ }
362
+ }
363
+
364
+ function parseAcceptCharset(
365
+ accept: string,
366
+ ): Array<{ charset: string; q: number; o: number }> {
367
+ const specs: Array<{ charset: string; q: number; o: number }> = []
368
+ const parts = accept.split(",")
369
+
370
+ for (let o = 0; o < parts.length; o++) {
371
+ const part = parts[o].trim()
372
+ if (!part) continue
373
+
374
+ const match = simpleCharsetRegExp.exec(part)
375
+ if (!match) continue
376
+
377
+ const charset = match[1].toLowerCase()
378
+ const q = parseQuality(match[2])
379
+
380
+ if (q > 0) {
381
+ specs.push({ charset, q, o })
382
+ }
383
+ }
384
+
385
+ return specs
386
+ }
387
+
388
+ function specifyCharset(
389
+ charset: string,
390
+ spec: { charset: string; q: number; o: number },
391
+ ): { q: number; s: number; o: number } | null {
392
+ const c = charset.toLowerCase()
393
+ const s = spec.charset
394
+
395
+ if (s === "*" || s === c) {
396
+ return { q: spec.q, s: s === c ? 1 : 0, o: spec.o }
397
+ }
398
+
399
+ return null
400
+ }
401
+
402
+ function getCharsetPriority(
403
+ charset: string,
404
+ accepted: Array<{ charset: string; q: number; o: number }>,
405
+ index: number,
406
+ ): ParsedSpec {
407
+ let best: { q: number; s: number; o: number } | null = null
408
+
409
+ for (const spec of accepted) {
410
+ const result = specifyCharset(charset, spec)
411
+ if (
412
+ result
413
+ && (best === null
414
+ || result.s > best.s
415
+ || (result.s === best.s && result.q > best.q)
416
+ || (result.s === best.s && result.q === best.q && result.o < best.o))
417
+ ) {
418
+ best = result
419
+ }
420
+ }
421
+
422
+ return {
423
+ value: charset,
424
+ q: best?.q ?? 0,
425
+ s: best?.s ?? 0,
426
+ o: best?.o ?? -1,
427
+ i: index,
428
+ }
429
+ }
430
+
431
+ function compareSpecs(a: ParsedSpec, b: ParsedSpec): number {
432
+ return (
433
+ b.q - a.q
434
+ || b.s - a.s
435
+ || a.o - b.o
436
+ || a.i - b.i
437
+ )
438
+ }
439
+
440
+ export function media(accept: string, available?: string[]): string[] {
441
+ const parsed = parseAccept(accept)
442
+ if (parsed.length === 0) {
443
+ return []
444
+ }
445
+
446
+ if (!available) {
447
+ return parsed.sort((a, b) => b.q - a.q || a.o - b.o).map((p) =>
448
+ `${p.type}/${p.subtype}`
449
+ )
450
+ }
451
+
452
+ const priorities = available.map((t, i) => getMediaTypePriority(t, parsed, i))
453
+ const sorted = priorities.filter((p) => p.q > 0).sort(compareSpecs)
454
+
455
+ return sorted.map((p) => p.value)
456
+ }
457
+
458
+ export function language(accept: string, available?: string[]): string[] {
459
+ const parsed = parseAcceptLanguage(accept)
460
+ if (parsed.length === 0) {
461
+ return []
462
+ }
463
+
464
+ if (!available) {
465
+ return parsed.sort((a, b) => b.q - a.q || a.o - b.o).map((p) =>
466
+ p.suffix ? `${p.prefix}-${p.suffix}` : p.prefix
467
+ )
468
+ }
469
+
470
+ const priorities = available.map((l, i) => getLanguagePriority(l, parsed, i))
471
+ const sorted = priorities.filter((p) => p.q > 0).sort(compareSpecs)
472
+
473
+ return sorted.map((p) => p.value)
474
+ }
475
+
476
+ export function encoding(accept: string, available?: string[]): string[] {
477
+ const parsed = parseAcceptEncoding(accept)
478
+ if (parsed.length === 0) {
479
+ return []
480
+ }
481
+
482
+ if (!available) {
483
+ return parsed.sort((a, b) => b.q - a.q || a.o - b.o).map((p) => p.encoding)
484
+ }
485
+
486
+ const priorities = available.map((e, i) => getEncodingPriority(e, parsed, i))
487
+ const sorted = priorities.filter((p) => p.q > 0).sort(compareSpecs)
488
+
489
+ return sorted.map((p) => p.value)
490
+ }
491
+
492
+ export function charset(accept: string, available?: string[]): string[] {
493
+ const parsed = parseAcceptCharset(accept)
494
+ if (parsed.length === 0) {
495
+ return []
496
+ }
497
+
498
+ if (!available) {
499
+ return parsed.sort((a, b) => b.q - a.q || a.o - b.o).map((p) => p.charset)
500
+ }
501
+
502
+ const priorities = available.map((c, i) => getCharsetPriority(c, parsed, i))
503
+ const sorted = priorities.filter((p) => p.q > 0).sort(compareSpecs)
504
+
505
+ return sorted.map((p) => p.value)
506
+ }
507
+
508
+ export function headerMedia(
509
+ headers: Headers.Headers,
510
+ available?: string[],
511
+ ): string[] {
512
+ const accept = headers["accept"]
513
+ if (!accept) return []
514
+ return media(accept, available)
515
+ }
516
+
517
+ export function headerLanguage(
518
+ headers: Headers.Headers,
519
+ available?: string[],
520
+ ): string[] {
521
+ const accept = headers["accept-language"]
522
+ if (!accept) return []
523
+ return language(accept, available)
524
+ }
525
+
526
+ export function headerEncoding(
527
+ headers: Headers.Headers,
528
+ available?: string[],
529
+ ): string[] {
530
+ const accept = headers["accept-encoding"]
531
+ if (!accept) return []
532
+ return encoding(accept, available)
533
+ }
534
+
535
+ export function headerCharset(
536
+ headers: Headers.Headers,
537
+ available?: string[],
538
+ ): string[] {
539
+ const accept = headers["accept-charset"]
540
+ if (!accept) return []
541
+ return charset(accept, available)
542
+ }