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,485 @@
1
+ export type PathPattern = `/${string}`
2
+
3
+ export type Segments<Path extends string> = Path extends `/${infer Rest}`
4
+ ? Segments<Rest>
5
+ : Path extends `${infer Head}/${infer Tail}` ? [Head, ...Segments<Tail>]
6
+ : Path extends "" ? []
7
+ : [Path]
8
+
9
+ export type Params<T extends string> = string extends T ? Record<string, string>
10
+ : T extends `${infer _Start}:${infer Param}?/${infer Rest}`
11
+ ? { [K in Param]?: string } & Params<`/${Rest}`>
12
+ : T extends `${infer _Start}:${infer Param}/${infer Rest}`
13
+ ? { [K in Param]: string } & Params<`/${Rest}`>
14
+ : T extends `${infer _Start}:${infer Param}+` ? { [K in Param]: string }
15
+ : T extends `${infer _Start}:${infer Param}*` ? { [K in Param]?: string }
16
+ : T extends `${infer _Start}:${infer Param}?` ? { [K in Param]?: string }
17
+ : T extends `${infer _Start}:${infer Param}` ? { [K in Param]: string }
18
+ : {}
19
+
20
+ export type ValidateResult =
21
+ | { ok: true; segments: string[] }
22
+ | { ok: false; error: string }
23
+
24
+ function isValidSegment(segment: string): boolean {
25
+ if (segment.startsWith(":")) {
26
+ const rest = segment.slice(1)
27
+ if (rest.endsWith("*") || rest.endsWith("+") || rest.endsWith("?")) {
28
+ const name = rest.slice(0, -1)
29
+ return name !== "" && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)
30
+ }
31
+ return rest !== "" && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(rest)
32
+ }
33
+ return /^[\p{L}\p{N}._~-]+$/u.test(segment)
34
+ }
35
+
36
+ export function validate(path: string): ValidateResult {
37
+ const segments = path.split("/").filter(Boolean)
38
+ for (const segment of segments) {
39
+ if (!isValidSegment(segment)) {
40
+ return {
41
+ ok: false,
42
+ error: `Invalid segment "${segment}" in "${path}"`,
43
+ }
44
+ }
45
+ }
46
+ return { ok: true, segments }
47
+ }
48
+
49
+ export function match(
50
+ pattern: string,
51
+ path: string,
52
+ ): Record<string, string> | null {
53
+ const patternSegments = pattern.split("/").filter(Boolean)
54
+ const pathSegments = path.split("/").filter(Boolean)
55
+ const params: Record<string, string> = {}
56
+ let patternIndex = 0
57
+ let pathIndex = 0
58
+
59
+ while (patternIndex < patternSegments.length) {
60
+ const seg = patternSegments[patternIndex]
61
+
62
+ if (seg.startsWith(":")) {
63
+ const rest = seg.slice(1)
64
+
65
+ if (rest.endsWith("+")) {
66
+ const name = rest.slice(0, -1)
67
+ const remaining = pathSegments.slice(pathIndex)
68
+ if (remaining.length === 0) {
69
+ return null
70
+ }
71
+ params[name] = remaining.join("/")
72
+ return params
73
+ }
74
+
75
+ if (rest.endsWith("*")) {
76
+ const name = rest.slice(0, -1)
77
+ const remaining = pathSegments.slice(pathIndex)
78
+ if (remaining.length > 0) {
79
+ params[name] = remaining.join("/")
80
+ }
81
+ return params
82
+ }
83
+
84
+ if (rest.endsWith("?")) {
85
+ const name = rest.slice(0, -1)
86
+ if (pathIndex < pathSegments.length) {
87
+ params[name] = pathSegments[pathIndex]
88
+ pathIndex++
89
+ }
90
+ patternIndex++
91
+ continue
92
+ }
93
+
94
+ if (pathIndex >= pathSegments.length) {
95
+ return null
96
+ }
97
+
98
+ params[rest] = pathSegments[pathIndex]
99
+ pathIndex++
100
+ patternIndex++
101
+ continue
102
+ }
103
+
104
+ if (pathIndex >= pathSegments.length) {
105
+ return null
106
+ }
107
+
108
+ if (seg !== pathSegments[pathIndex]) {
109
+ return null
110
+ }
111
+
112
+ pathIndex++
113
+ patternIndex++
114
+ }
115
+
116
+ if (pathIndex !== pathSegments.length) {
117
+ return null
118
+ }
119
+
120
+ return params
121
+ }
122
+
123
+ export function toRegex(path: string): RegExp {
124
+ const result = path
125
+ .replace(/\/+(\/|$)/g, "$1")
126
+ .replace(/\./g, "\\.")
127
+ .replace(/(\/?):(\w+)\+/g, "($1(?<$2>*))")
128
+ .replace(/(\/?):(\w+)\*/g, "(?:\\/(?<$2>.*))?")
129
+ .replace(/(\/?):(\w+)/g, "($1(?<$2>[^$1/]+?))")
130
+ .replace(/(\/?)\*/g, "($1.*)?")
131
+
132
+ return new RegExp(`^${result}/*$`)
133
+ }
134
+
135
+ function getModifier(seg: string): "" | "?" | "*" | "+" {
136
+ const last = seg[seg.length - 1]
137
+ if (last === "?" || last === "*" || last === "+") return last
138
+ return ""
139
+ }
140
+
141
+ function getParamName(seg: string): string {
142
+ const modifier = getModifier(seg)
143
+ return modifier ? seg.slice(1, -1) : seg.slice(1)
144
+ }
145
+
146
+ /**
147
+ * Converts to Express path pattern.
148
+ *
149
+ * @see https://expressjs.com/en/guide/routing.html
150
+ *
151
+ * - `:param` → `:param`
152
+ * - `:param?` → `{/:param}`
153
+ * - `:param+` → `/*param`
154
+ * - `:param*` → `/`, `/*param`
155
+ */
156
+ export function toExpress(path: string): string[] {
157
+ const segments = path.split("/").filter(Boolean)
158
+ const optionalWildcardIndex = segments.findIndex(
159
+ (s) => s.startsWith(":") && s.endsWith("*"),
160
+ )
161
+
162
+ if (optionalWildcardIndex !== -1) {
163
+ const before = segments.slice(0, optionalWildcardIndex)
164
+ const rest = segments[optionalWildcardIndex]
165
+ const name = getParamName(rest)
166
+ const beforeJoined = before
167
+ .map((s) => (s.startsWith(":") ? `:${getParamName(s)}` : s))
168
+ .join("/")
169
+ const basePath = beforeJoined ? "/" + beforeJoined : "/"
170
+ const withWildcard = basePath === "/" ? `/*${name}` : basePath + `/*${name}`
171
+ return [basePath, withWildcard]
172
+ }
173
+
174
+ let result = ""
175
+ for (const seg of segments) {
176
+ if (!seg.startsWith(":")) {
177
+ result += "/" + seg
178
+ } else {
179
+ const name = getParamName(seg)
180
+ const modifier = getModifier(seg)
181
+ switch (modifier) {
182
+ case "":
183
+ result += `/:${name}`
184
+ break
185
+ case "?":
186
+ result += `{/:${name}}`
187
+ break
188
+ case "+":
189
+ result += `/*${name}`
190
+ break
191
+ case "*":
192
+ result += `/*${name}`
193
+ break
194
+ }
195
+ }
196
+ }
197
+ return [result || "/"]
198
+ }
199
+
200
+ /**
201
+ * Converts to URLPattern path pattern.
202
+ *
203
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API
204
+ *
205
+ * - `:param` → `:param`
206
+ * - `:param?` → `:param?`
207
+ * - `:param+` → `:param+`
208
+ * - `:param*` → `:param*`
209
+ */
210
+ export function toURLPattern(path: string): string[] {
211
+ const segments = path.split("/").filter(Boolean)
212
+ const joined = segments
213
+ .map((seg) => {
214
+ if (!seg.startsWith(":")) return seg
215
+ const name = getParamName(seg)
216
+ const modifier = getModifier(seg)
217
+ return `:${name}${modifier}`
218
+ })
219
+ .join("/")
220
+ return [joined ? "/" + joined : "/"]
221
+ }
222
+
223
+ /**
224
+ * Converts to React Router path pattern.
225
+ *
226
+ * @see https://reactrouter.com/start/framework/routing
227
+ *
228
+ * - `:param` → `:param`
229
+ * - `:param?` → `:param?`
230
+ * - `:param+` → `*` (splat, required)
231
+ * - `:param*` → `/`, `/*` (splat, optional - two routes)
232
+ */
233
+ export function toReactRouter(path: string): string[] {
234
+ const segments = path.split("/").filter(Boolean)
235
+ const optionalWildcardIndex = segments.findIndex(
236
+ (s) => s.startsWith(":") && s.endsWith("*"),
237
+ )
238
+
239
+ if (optionalWildcardIndex !== -1) {
240
+ const before = segments.slice(0, optionalWildcardIndex)
241
+ const beforeJoined = before
242
+ .map((s) => {
243
+ if (!s.startsWith(":")) return s
244
+ const name = getParamName(s)
245
+ const modifier = getModifier(s)
246
+ return modifier === "?" ? `:${name}?` : `:${name}`
247
+ })
248
+ .join("/")
249
+ const basePath = beforeJoined ? "/" + beforeJoined : "/"
250
+ const withWildcard = basePath === "/" ? "/*" : basePath + "/*"
251
+ return [basePath, withWildcard]
252
+ }
253
+
254
+ const joined = segments
255
+ .map((s) => {
256
+ if (!s.startsWith(":")) return s
257
+ const name = getParamName(s)
258
+ const modifier = getModifier(s)
259
+ switch (modifier) {
260
+ case "":
261
+ return `:${name}`
262
+ case "?":
263
+ return `:${name}?`
264
+ case "+":
265
+ case "*":
266
+ return "*"
267
+ }
268
+ })
269
+ .join("/")
270
+ return [joined ? "/" + joined : "/"]
271
+ }
272
+
273
+ /**
274
+ * Alias for toReactRouter.
275
+ *
276
+ * @see https://reactrouter.com/start/framework/routing
277
+ */
278
+ export const toRemix = toReactRouter
279
+
280
+ /**
281
+ * Converts to Remix file-based route naming convention.
282
+ *
283
+ * Returns a file path segment (without extension) for Remix's
284
+ * flat file routing convention.
285
+ *
286
+ * @see https://remix.run/docs/file-conventions/routes
287
+ *
288
+ * - `:param` → `$param`
289
+ * - `:param?` → `($param)`
290
+ * - `:param+` → `$` (splat)
291
+ * - `:param*` → `($)` (optional splat) - Note: not officially supported
292
+ */
293
+ export function toRemixFile(path: string): string {
294
+ const segments = path.split("/").filter(Boolean)
295
+
296
+ const mapped = segments.map((seg) => {
297
+ if (!seg.startsWith(":")) return seg
298
+ const name = getParamName(seg)
299
+ const modifier = getModifier(seg)
300
+ switch (modifier) {
301
+ case "":
302
+ return `$${name}`
303
+ case "?":
304
+ return `($${name})`
305
+ case "+":
306
+ return "$"
307
+ case "*":
308
+ return "($)"
309
+ }
310
+ })
311
+
312
+ return mapped.join(".")
313
+ }
314
+
315
+ /**
316
+ * Converts to TanStack Router path/file pattern.
317
+ *
318
+ * TanStack uses the same `$param` syntax for both route paths and file names.
319
+ * Returns a dot-separated file name (without extension).
320
+ *
321
+ * @see https://tanstack.com/router/v1/docs/framework/react/guide/path-params
322
+ * @see https://tanstack.com/router/v1/docs/framework/react/routing/file-naming-conventions
323
+ *
324
+ * - `:param` → `$param`
325
+ * - `:param?` → `{-$param}` (optional segment)
326
+ * - `:param+` → `$` (splat)
327
+ * - `:param*` → `$` (splat, optional not supported - treated as required)
328
+ */
329
+ export function toTanStack(path: string): string {
330
+ const segments = path.split("/").filter(Boolean)
331
+
332
+ const mapped = segments.map((seg) => {
333
+ if (!seg.startsWith(":")) return seg
334
+ const name = getParamName(seg)
335
+ const modifier = getModifier(seg)
336
+ switch (modifier) {
337
+ case "":
338
+ return `$${name}`
339
+ case "?":
340
+ return `{-$${name}}`
341
+ case "+":
342
+ return "$"
343
+ case "*":
344
+ return "$"
345
+ }
346
+ })
347
+
348
+ return mapped.join(".")
349
+ }
350
+
351
+ /**
352
+ * Converts to Hono path pattern.
353
+ *
354
+ * Hono uses unnamed wildcards - they are NOT accessible via c.req.param().
355
+ * Use c.req.path to access the matched path for wildcard routes.
356
+ *
357
+ * @see https://hono.dev/docs/api/routing
358
+ *
359
+ * - `:param` → `:param`
360
+ * - `:param?` → `:param?`
361
+ * - `:param+` → `*` (unnamed, required)
362
+ * - `:param*` → `/`, `/*` (unnamed, optional - two routes)
363
+ */
364
+ export function toHono(path: string): string[] {
365
+ const segments = path.split("/").filter(Boolean)
366
+ const optionalWildcardIndex = segments.findIndex(
367
+ (s) => s.startsWith(":") && s.endsWith("*"),
368
+ )
369
+
370
+ if (optionalWildcardIndex !== -1) {
371
+ const before = segments.slice(0, optionalWildcardIndex)
372
+ const beforeJoined = before
373
+ .map((s) => {
374
+ if (!s.startsWith(":")) return s
375
+ const name = getParamName(s)
376
+ const modifier = getModifier(s)
377
+ return modifier === "?" ? `:${name}?` : `:${name}`
378
+ })
379
+ .join("/")
380
+ const basePath = beforeJoined ? "/" + beforeJoined : "/"
381
+ const withWildcard = basePath === "/" ? "/*" : basePath + "/*"
382
+ return [basePath, withWildcard]
383
+ }
384
+
385
+ const joined = segments
386
+ .map((s) => {
387
+ if (!s.startsWith(":")) return s
388
+ const name = getParamName(s)
389
+ const modifier = getModifier(s)
390
+ switch (modifier) {
391
+ case "":
392
+ return `:${name}`
393
+ case "?":
394
+ return `:${name}?`
395
+ case "+":
396
+ case "*":
397
+ return "*"
398
+ }
399
+ })
400
+ .join("/")
401
+ return [joined ? "/" + joined : "/"]
402
+ }
403
+
404
+ /**
405
+ * Converts to Effect HttpRouter / find-my-way path pattern.
406
+ *
407
+ * Effect uses colon-style params with unnamed wildcards.
408
+ *
409
+ * @see https://effect.website/docs/platform/http-router
410
+ *
411
+ * - `:param` → `:param`
412
+ * - `:param?` → `:param?`
413
+ * - `:param+` → `*` (unnamed)
414
+ * - `:param*` → `/`, `/*` (two routes)
415
+ */
416
+ export function toEffect(path: string): string[] {
417
+ return toHono(path)
418
+ }
419
+
420
+ /**
421
+ * Converts to Bun.serve path pattern.
422
+ *
423
+ * Since Bun doesn't support optional params (`:param?`), optional segments
424
+ * are expanded into multiple routes.
425
+ *
426
+ * @see https://bun.sh/docs/api/http#routing
427
+ *
428
+ * - `:param` → `:param`
429
+ * - `:param?` → `/`, `/:param` (two routes)
430
+ * - `:param+` → `/*`
431
+ * - `:param*` → `/`, `/*` (two routes)
432
+ */
433
+ export function toBun(path: string): PathPattern[] {
434
+ const segments = path.split("/").filter(Boolean)
435
+
436
+ const optionalIndex = segments.findIndex(
437
+ (s) => s.startsWith(":") && (s.endsWith("?") || s.endsWith("*")),
438
+ )
439
+
440
+ if (optionalIndex === -1) {
441
+ const joined = segments
442
+ .map((s) => {
443
+ if (!s.startsWith(":")) return s
444
+ const modifier = getModifier(s)
445
+ const name = getParamName(s)
446
+ return modifier === "+" || modifier === "*" ? "*" : `:${name}`
447
+ })
448
+ .join("/")
449
+ return [joined ? `/${joined}` : "/"]
450
+ }
451
+
452
+ const before = segments.slice(0, optionalIndex)
453
+ const optional = segments[optionalIndex]
454
+ const after = segments.slice(optionalIndex + 1)
455
+
456
+ const formatSegment = (s: string): string => {
457
+ if (!s.startsWith(":")) return s
458
+ const modifier = getModifier(s)
459
+ const name = getParamName(s)
460
+ switch (modifier) {
461
+ case "":
462
+ case "?":
463
+ return `:${name}`
464
+ case "+":
465
+ case "*":
466
+ return "*"
467
+ }
468
+ }
469
+
470
+ const beforePath = before.map(formatSegment).join("/")
471
+ const basePath: PathPattern = beforePath ? `/${beforePath}` : "/"
472
+
473
+ const optionalModifier = getModifier(optional)
474
+ const optionalName = getParamName(optional)
475
+ const requiredOptional = optionalModifier === "*"
476
+ ? `:${optionalName}+`
477
+ : `:${optionalName}`
478
+
479
+ const withOptionalSegments = [...before, requiredOptional, ...after]
480
+ const withOptionalPath: PathPattern = `/${
481
+ withOptionalSegments.map(formatSegment).join("/")
482
+ }`
483
+
484
+ return [...toBun(basePath), ...toBun(withOptionalPath)]
485
+ }