@fragno-dev/core 0.0.1

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 (108) hide show
  1. package/.turbo/turbo-build.log +61 -0
  2. package/.turbo/turbo-types$colon$check.log +2 -0
  3. package/dist/api/api.d.ts +2 -0
  4. package/dist/api/api.js +3 -0
  5. package/dist/api-CBDGZiLC.d.ts +278 -0
  6. package/dist/api-CBDGZiLC.d.ts.map +1 -0
  7. package/dist/api-DgHfYjq2.js +54 -0
  8. package/dist/api-DgHfYjq2.js.map +1 -0
  9. package/dist/client/client.d.ts +3 -0
  10. package/dist/client/client.js +6 -0
  11. package/dist/client/client.svelte.d.ts +33 -0
  12. package/dist/client/client.svelte.d.ts.map +1 -0
  13. package/dist/client/client.svelte.js +123 -0
  14. package/dist/client/client.svelte.js.map +1 -0
  15. package/dist/client/react.d.ts +58 -0
  16. package/dist/client/react.d.ts.map +1 -0
  17. package/dist/client/react.js +80 -0
  18. package/dist/client/react.js.map +1 -0
  19. package/dist/client/vanilla.d.ts +61 -0
  20. package/dist/client/vanilla.d.ts.map +1 -0
  21. package/dist/client/vanilla.js +136 -0
  22. package/dist/client/vanilla.js.map +1 -0
  23. package/dist/client/vue.d.ts +39 -0
  24. package/dist/client/vue.d.ts.map +1 -0
  25. package/dist/client/vue.js +108 -0
  26. package/dist/client/vue.js.map +1 -0
  27. package/dist/client-DWjxKDnE.js +703 -0
  28. package/dist/client-DWjxKDnE.js.map +1 -0
  29. package/dist/client-XFdAy-IQ.d.ts +287 -0
  30. package/dist/client-XFdAy-IQ.d.ts.map +1 -0
  31. package/dist/integrations/astro.d.ts +18 -0
  32. package/dist/integrations/astro.d.ts.map +1 -0
  33. package/dist/integrations/astro.js +16 -0
  34. package/dist/integrations/astro.js.map +1 -0
  35. package/dist/integrations/next-js.d.ts +15 -0
  36. package/dist/integrations/next-js.d.ts.map +1 -0
  37. package/dist/integrations/next-js.js +17 -0
  38. package/dist/integrations/next-js.js.map +1 -0
  39. package/dist/integrations/react-ssr.d.ts +19 -0
  40. package/dist/integrations/react-ssr.d.ts.map +1 -0
  41. package/dist/integrations/react-ssr.js +38 -0
  42. package/dist/integrations/react-ssr.js.map +1 -0
  43. package/dist/integrations/svelte-kit.d.ts +21 -0
  44. package/dist/integrations/svelte-kit.d.ts.map +1 -0
  45. package/dist/integrations/svelte-kit.js +18 -0
  46. package/dist/integrations/svelte-kit.js.map +1 -0
  47. package/dist/mod.d.ts +3 -0
  48. package/dist/mod.js +177 -0
  49. package/dist/mod.js.map +1 -0
  50. package/dist/route-Bp6eByhz.js +331 -0
  51. package/dist/route-Bp6eByhz.js.map +1 -0
  52. package/dist/ssr-tJHqcNSw.js +48 -0
  53. package/dist/ssr-tJHqcNSw.js.map +1 -0
  54. package/package.json +127 -0
  55. package/src/api/api.test.ts +140 -0
  56. package/src/api/api.ts +106 -0
  57. package/src/api/error.ts +47 -0
  58. package/src/api/fragment.test.ts +509 -0
  59. package/src/api/fragment.ts +277 -0
  60. package/src/api/internal/path-runtime.test.ts +121 -0
  61. package/src/api/internal/path-type.test.ts +602 -0
  62. package/src/api/internal/path.ts +322 -0
  63. package/src/api/internal/response-stream.ts +118 -0
  64. package/src/api/internal/route.test.ts +56 -0
  65. package/src/api/internal/route.ts +9 -0
  66. package/src/api/request-input-context.test.ts +437 -0
  67. package/src/api/request-input-context.ts +201 -0
  68. package/src/api/request-middleware.test.ts +544 -0
  69. package/src/api/request-middleware.ts +126 -0
  70. package/src/api/request-output-context.test.ts +626 -0
  71. package/src/api/request-output-context.ts +175 -0
  72. package/src/api/route.test.ts +176 -0
  73. package/src/api/route.ts +152 -0
  74. package/src/client/client-builder.test.ts +264 -0
  75. package/src/client/client-error.test.ts +15 -0
  76. package/src/client/client-error.ts +141 -0
  77. package/src/client/client-types.test.ts +493 -0
  78. package/src/client/client.ssr.test.ts +173 -0
  79. package/src/client/client.svelte.test.ts +837 -0
  80. package/src/client/client.svelte.ts +278 -0
  81. package/src/client/client.test.ts +1690 -0
  82. package/src/client/client.ts +1035 -0
  83. package/src/client/component.test.svelte +21 -0
  84. package/src/client/internal/ndjson-streaming.test.ts +457 -0
  85. package/src/client/internal/ndjson-streaming.ts +248 -0
  86. package/src/client/react.test.ts +947 -0
  87. package/src/client/react.ts +241 -0
  88. package/src/client/vanilla.test.ts +867 -0
  89. package/src/client/vanilla.ts +265 -0
  90. package/src/client/vue.test.ts +754 -0
  91. package/src/client/vue.ts +242 -0
  92. package/src/http/http-status.ts +60 -0
  93. package/src/integrations/astro.ts +17 -0
  94. package/src/integrations/next-js.ts +31 -0
  95. package/src/integrations/react-ssr.ts +40 -0
  96. package/src/integrations/svelte-kit.ts +41 -0
  97. package/src/mod.ts +20 -0
  98. package/src/util/async.test.ts +85 -0
  99. package/src/util/async.ts +96 -0
  100. package/src/util/content-type.test.ts +136 -0
  101. package/src/util/content-type.ts +84 -0
  102. package/src/util/nanostores.test.ts +28 -0
  103. package/src/util/nanostores.ts +65 -0
  104. package/src/util/ssr.ts +75 -0
  105. package/src/util/types-util.ts +16 -0
  106. package/tsconfig.json +10 -0
  107. package/tsdown.config.ts +21 -0
  108. package/vitest.config.ts +10 -0
@@ -0,0 +1,602 @@
1
+ import { test, expect, expectTypeOf } from "vitest";
2
+ import type {
3
+ ExtractPathParams,
4
+ ExtractPathParamNames,
5
+ ExtractPathParamNamesAsTuple,
6
+ ExtractPathParamsAsLabeledTuple,
7
+ HasPathParams,
8
+ ExtractPathParamsOrWiden,
9
+ MaybeExtractPathParamsOrWiden,
10
+ QueryParamsHint,
11
+ } from "./path";
12
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
13
+
14
+ // Type-only tests using expectTypeOf from vitest
15
+ test("ExtractPathParams type tests", () => {
16
+ // Simple path without parameters
17
+ expectTypeOf<ExtractPathParams<"/path">>().toEqualTypeOf<Record<string, never>>();
18
+
19
+ // Single named parameter
20
+ expectTypeOf<ExtractPathParams<"/path/:name">>().toEqualTypeOf<Record<"name", string>>();
21
+
22
+ // Parameter with no name
23
+ expectTypeOf<ExtractPathParams<"/path/:">>().toEqualTypeOf<Record<"", string>>();
24
+ expectTypeOf<ExtractPathParams<"/path/:/x">>().toEqualTypeOf<Record<"", string>>();
25
+
26
+ // Duplicate identifiers
27
+ expectTypeOf<ExtractPathParams<"/path/:/x/:/">>().toEqualTypeOf<Record<"", string>>();
28
+ expectTypeOf<ExtractPathParams<"/path/:var/x/:var">>().toEqualTypeOf<Record<"var", string>>();
29
+
30
+ // Multiple named parameters
31
+ expectTypeOf<ExtractPathParams<"/users/:id/posts/:postId">>().toEqualTypeOf<
32
+ Record<"id" | "postId", string>
33
+ >();
34
+
35
+ // Wildcard without name
36
+ expectTypeOf<ExtractPathParams<"/path/foo/**">>().toEqualTypeOf<Record<"**", string>>();
37
+
38
+ // Named wildcard
39
+ expectTypeOf<ExtractPathParams<"/path/foo/**:name">>().toEqualTypeOf<Record<"name", string>>();
40
+
41
+ // Complex path with mixed parameters
42
+ expectTypeOf<
43
+ ExtractPathParams<"/api/:version/users/:userId/posts/:postId/**:remaining">
44
+ >().toEqualTypeOf<Record<"version" | "userId" | "postId" | "remaining", string>>();
45
+
46
+ // Root path
47
+ expectTypeOf<ExtractPathParams<"/">>().toEqualTypeOf<Record<string, never>>();
48
+
49
+ // Empty string
50
+ expectTypeOf<ExtractPathParams<"">>().toEqualTypeOf<Record<string, never>>();
51
+
52
+ // Path with only parameter
53
+ expectTypeOf<ExtractPathParams<":id">>().toEqualTypeOf<Record<"id", string>>();
54
+
55
+ // Path with parameter at root
56
+ expectTypeOf<ExtractPathParams<"/:id">>().toEqualTypeOf<Record<"id", string>>();
57
+ });
58
+
59
+ test("ExtractPathParams configurable value type tests", () => {
60
+ // Test with number type
61
+ expectTypeOf<ExtractPathParams<"/path", number>>().toEqualTypeOf<Record<string, never>>();
62
+ expectTypeOf<ExtractPathParams<"/path/:id", number>>().toEqualTypeOf<Record<"id", number>>();
63
+ expectTypeOf<ExtractPathParams<"/users/:id/posts/:postId", number>>().toEqualTypeOf<
64
+ Record<"id" | "postId", number>
65
+ >();
66
+
67
+ // Test with boolean type
68
+ expectTypeOf<ExtractPathParams<"/path/:enabled", boolean>>().toEqualTypeOf<
69
+ Record<"enabled", boolean>
70
+ >();
71
+ expectTypeOf<ExtractPathParams<"/api/:debug/users/:active", boolean>>().toEqualTypeOf<
72
+ Record<"debug" | "active", boolean>
73
+ >();
74
+
75
+ // Test with custom object type
76
+ type CustomType = { value: string; parsed: boolean };
77
+ expectTypeOf<ExtractPathParams<"/path/:data", CustomType>>().toEqualTypeOf<
78
+ Record<"data", CustomType>
79
+ >();
80
+ expectTypeOf<ExtractPathParams<"/api/:config/**:metadata", CustomType>>().toEqualTypeOf<
81
+ Record<"config" | "metadata", CustomType>
82
+ >();
83
+
84
+ // Test with union type
85
+ type StringOrNumber = string | number;
86
+ expectTypeOf<ExtractPathParams<"/path/:value", StringOrNumber>>().toEqualTypeOf<
87
+ Record<"value", StringOrNumber>
88
+ >();
89
+
90
+ // Test with undefined (should work but not very useful)
91
+ expectTypeOf<ExtractPathParams<"/path/:id", undefined>>().toEqualTypeOf<
92
+ Record<"id", undefined>
93
+ >();
94
+
95
+ // Test backward compatibility - default should be string
96
+ expectTypeOf<ExtractPathParams<"/path/:id">>().toEqualTypeOf<
97
+ ExtractPathParams<"/path/:id", string>
98
+ >();
99
+
100
+ // Complex example with custom type
101
+ type ParsedParam = { raw: string; validated: boolean; converted: number };
102
+ expectTypeOf<
103
+ ExtractPathParams<"/api/:version/users/:userId/posts/:postId", ParsedParam>
104
+ >().toEqualTypeOf<Record<"version" | "userId" | "postId", ParsedParam>>();
105
+
106
+ // Wildcard with custom type
107
+ expectTypeOf<ExtractPathParams<"/files/**:path", File>>().toEqualTypeOf<Record<"path", File>>();
108
+
109
+ // Mixed parameters and wildcards with custom type
110
+ expectTypeOf<
111
+ ExtractPathParams<"/api/:version/users/:userId/**:remaining", ParsedParam>
112
+ >().toEqualTypeOf<Record<"version" | "userId" | "remaining", ParsedParam>>();
113
+
114
+ // No parameters should still return Record<string, never> regardless of ValueType
115
+ expectTypeOf<ExtractPathParams<"/static/assets", number>>().toEqualTypeOf<
116
+ Record<string, never>
117
+ >();
118
+ expectTypeOf<ExtractPathParams<"/static/assets", CustomType>>().toEqualTypeOf<
119
+ Record<string, never>
120
+ >();
121
+ });
122
+
123
+ test("ExtractPathParamNames type tests", () => {
124
+ // Simple path without parameters
125
+ expectTypeOf<ExtractPathParamNames<"/path">>().toEqualTypeOf<never>();
126
+
127
+ // Single named parameter
128
+ expectTypeOf<ExtractPathParamNames<"/path/:name">>().toEqualTypeOf<"name">();
129
+
130
+ // Multiple named parameters
131
+ expectTypeOf<ExtractPathParamNames<"/users/:id/posts/:postId">>().toEqualTypeOf<
132
+ "id" | "postId"
133
+ >();
134
+
135
+ // Wildcard without name
136
+ expectTypeOf<ExtractPathParamNames<"/path/foo/**">>().toEqualTypeOf<"**">();
137
+
138
+ // Named wildcard
139
+ expectTypeOf<ExtractPathParamNames<"/path/foo/**:name">>().toEqualTypeOf<"name">();
140
+
141
+ // Complex path
142
+ expectTypeOf<ExtractPathParamNames<"/api/:version/users/:userId/**:files">>().toEqualTypeOf<
143
+ "version" | "userId" | "files"
144
+ >();
145
+
146
+ // Root and empty
147
+ expectTypeOf<ExtractPathParamNames<"/">>().toEqualTypeOf<never>();
148
+ expectTypeOf<ExtractPathParamNames<"">>().toEqualTypeOf<never>();
149
+ });
150
+
151
+ test("ExtractPathParamNamesAsTuple type tests", () => {
152
+ // Simple path without parameters
153
+ expectTypeOf<ExtractPathParamNamesAsTuple<"/path">>().toEqualTypeOf<[]>();
154
+
155
+ // Single named parameter
156
+ expectTypeOf<ExtractPathParamNamesAsTuple<"/path/:name">>().toEqualTypeOf<["name"]>();
157
+
158
+ // Multiple named parameters (should preserve order)
159
+ expectTypeOf<ExtractPathParamNamesAsTuple<"/users/:id/posts/:postId">>().toEqualTypeOf<
160
+ ["id", "postId"]
161
+ >();
162
+
163
+ // Wildcard without name
164
+ expectTypeOf<ExtractPathParamNamesAsTuple<"/path/foo/**">>().toEqualTypeOf<["**"]>();
165
+
166
+ // Named wildcard
167
+ expectTypeOf<ExtractPathParamNamesAsTuple<"/path/foo/**:name">>().toEqualTypeOf<["name"]>();
168
+
169
+ // Complex path with mixed parameters (should preserve order)
170
+ expectTypeOf<
171
+ ExtractPathParamNamesAsTuple<"/api/:version/users/:userId/**:files">
172
+ >().toEqualTypeOf<["version", "userId", "files"]>();
173
+
174
+ // Root and empty
175
+ expectTypeOf<ExtractPathParamNamesAsTuple<"/">>().toEqualTypeOf<[]>();
176
+ expectTypeOf<ExtractPathParamNamesAsTuple<"">>().toEqualTypeOf<[]>();
177
+
178
+ // Path with only parameter
179
+ expectTypeOf<ExtractPathParamNamesAsTuple<":id">>().toEqualTypeOf<["id"]>();
180
+ expectTypeOf<ExtractPathParamNamesAsTuple<"/:id">>().toEqualTypeOf<["id"]>();
181
+
182
+ // More complex examples
183
+ expectTypeOf<
184
+ ExtractPathParamNamesAsTuple<"/api/:version/users/:userId/posts/:postId/**:remaining">
185
+ >().toEqualTypeOf<["version", "userId", "postId", "remaining"]>();
186
+
187
+ // Edge cases
188
+ expectTypeOf<ExtractPathParamNamesAsTuple<"/path/:user_id">>().toEqualTypeOf<["user_id"]>();
189
+ expectTypeOf<ExtractPathParamNamesAsTuple<"/path/:user-id">>().toEqualTypeOf<["user-id"]>();
190
+
191
+ // Mixed wildcards and parameters
192
+ expectTypeOf<ExtractPathParamNamesAsTuple<"/api/:version/**:rest">>().toEqualTypeOf<
193
+ ["version", "rest"]
194
+ >();
195
+ });
196
+
197
+ test("ExtractPathParamsAsLabeledTuple type tests", () => {
198
+ // Simple path without parameters
199
+ expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path">>().toEqualTypeOf<[]>();
200
+
201
+ // Single named parameter
202
+ expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path/:name">>().toEqualTypeOf<[name: string]>();
203
+
204
+ // Multiple named parameters (should preserve order with labels)
205
+ expectTypeOf<ExtractPathParamsAsLabeledTuple<"/users/:id/posts/:postId">>().toEqualTypeOf<
206
+ [id: string, postId: string]
207
+ >();
208
+
209
+ // Wildcard without name
210
+ expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path/foo/**">>().toEqualTypeOf<[string]>();
211
+
212
+ // Named wildcard
213
+ expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path/foo/**:name">>().toEqualTypeOf<
214
+ [name: string]
215
+ >();
216
+
217
+ // Complex path with mixed parameters (the example from the user)
218
+ expectTypeOf<ExtractPathParamsAsLabeledTuple<"/api/:version/**:rest">>().toEqualTypeOf<
219
+ [version: string, rest: string]
220
+ >();
221
+
222
+ // More complex examples
223
+ expectTypeOf<
224
+ ExtractPathParamsAsLabeledTuple<"/api/:version/users/:userId/posts/:postId/**:remaining">
225
+ >().toEqualTypeOf<[version: string, userId: string, postId: string, remaining: string]>();
226
+
227
+ // Root and empty
228
+ expectTypeOf<ExtractPathParamsAsLabeledTuple<"/">>().toEqualTypeOf<[]>();
229
+ expectTypeOf<ExtractPathParamsAsLabeledTuple<"">>().toEqualTypeOf<[]>();
230
+
231
+ // Path with only parameter
232
+ expectTypeOf<ExtractPathParamsAsLabeledTuple<":id">>().toEqualTypeOf<[id: string]>();
233
+ expectTypeOf<ExtractPathParamsAsLabeledTuple<"/:id">>().toEqualTypeOf<[id: string]>();
234
+
235
+ // Edge cases with special characters
236
+ expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path/:user_id">>().toEqualTypeOf<
237
+ [user_id: string]
238
+ >();
239
+ // "user-id" is not a valid identifier in the tuple, so it doesn't become labeled.
240
+ expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path/:user-id">>().toEqualTypeOf<[string]>();
241
+
242
+ // Real-world examples
243
+ expectTypeOf<ExtractPathParamsAsLabeledTuple<"/api/v1/users/:userId">>().toEqualTypeOf<
244
+ [userId: string]
245
+ >();
246
+ expectTypeOf<ExtractPathParamsAsLabeledTuple<"/uploads/:userId/**:filename">>().toEqualTypeOf<
247
+ [userId: string, filename: string]
248
+ >();
249
+ expectTypeOf<ExtractPathParamsAsLabeledTuple<"/admin/:section/:action">>().toEqualTypeOf<
250
+ [section: string, action: string]
251
+ >();
252
+ });
253
+
254
+ test("ExtractPathParamsAsLabeledTuple configurable element type tests", () => {
255
+ // Test with number type
256
+ expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path", number>>().toEqualTypeOf<[]>();
257
+ expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path/:id", number>>().toEqualTypeOf<
258
+ [id: number]
259
+ >();
260
+ expectTypeOf<ExtractPathParamsAsLabeledTuple<"/users/:id/posts/:postId", number>>().toEqualTypeOf<
261
+ [id: number, postId: number]
262
+ >();
263
+
264
+ // Test with boolean type
265
+ expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path/:enabled", boolean>>().toEqualTypeOf<
266
+ [enabled: boolean]
267
+ >();
268
+ expectTypeOf<
269
+ ExtractPathParamsAsLabeledTuple<"/api/:debug/users/:active", boolean>
270
+ >().toEqualTypeOf<[debug: boolean, active: boolean]>();
271
+
272
+ // Test with custom object type
273
+ type CustomType = { value: string; parsed: boolean };
274
+ expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path/:data", CustomType>>().toEqualTypeOf<
275
+ [data: CustomType]
276
+ >();
277
+ expectTypeOf<
278
+ ExtractPathParamsAsLabeledTuple<"/api/:config/**:metadata", CustomType>
279
+ >().toEqualTypeOf<[config: CustomType, metadata: CustomType]>();
280
+
281
+ // Test with union type
282
+ type StringOrNumber = string | number;
283
+ expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path/:value", StringOrNumber>>().toEqualTypeOf<
284
+ [value: StringOrNumber]
285
+ >();
286
+
287
+ // Test with undefined (should work but not very useful)
288
+ expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path/:id", undefined>>().toEqualTypeOf<
289
+ [id: undefined]
290
+ >();
291
+
292
+ // Test backward compatibility - default should be string
293
+ expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path/:id">>().toEqualTypeOf<
294
+ ExtractPathParamsAsLabeledTuple<"/path/:id", string>
295
+ >();
296
+
297
+ // Complex example with custom type
298
+ type ParsedParam = { raw: string; validated: boolean; converted: number };
299
+ expectTypeOf<
300
+ ExtractPathParamsAsLabeledTuple<"/api/:version/users/:userId/posts/:postId", ParsedParam>
301
+ >().toEqualTypeOf<[version: ParsedParam, userId: ParsedParam, postId: ParsedParam]>();
302
+
303
+ // Wildcard with custom type
304
+ expectTypeOf<ExtractPathParamsAsLabeledTuple<"/files/**:path", File>>().toEqualTypeOf<
305
+ [path: File]
306
+ >();
307
+ });
308
+
309
+ test("HasPathParams type tests", () => {
310
+ // Paths without parameters
311
+ expectTypeOf<HasPathParams<"/path">>().toEqualTypeOf<false>();
312
+ expectTypeOf<HasPathParams<"/path/foo/bar">>().toEqualTypeOf<false>();
313
+ expectTypeOf<HasPathParams<"/path/foo/**">>().toEqualTypeOf<true>();
314
+ expectTypeOf<HasPathParams<"/">>().toEqualTypeOf<false>();
315
+ expectTypeOf<HasPathParams<"">>().toEqualTypeOf<false>();
316
+
317
+ // Paths with parameters
318
+ expectTypeOf<HasPathParams<"/path/:name">>().toEqualTypeOf<true>();
319
+ expectTypeOf<HasPathParams<"/users/:id">>().toEqualTypeOf<true>();
320
+ expectTypeOf<HasPathParams<"/users/:id/posts/:postId">>().toEqualTypeOf<true>();
321
+ expectTypeOf<HasPathParams<"/path/foo/**:name">>().toEqualTypeOf<true>();
322
+ expectTypeOf<HasPathParams<":id">>().toEqualTypeOf<true>();
323
+ expectTypeOf<HasPathParams<"/:id">>().toEqualTypeOf<true>();
324
+
325
+ type _T = HasPathParams<string>;
326
+ type _T2 = StandardSchemaV1.InferOutput<StandardSchemaV1>;
327
+ });
328
+
329
+ // Edge case tests
330
+ test("ExtractPathParams edge cases", () => {
331
+ // Parameter names with special characters (though not recommended in practice)
332
+ expectTypeOf<ExtractPathParams<"/path/:user_id">>().toEqualTypeOf<Record<"user_id", string>>();
333
+ expectTypeOf<ExtractPathParams<"/path/:user-id">>().toEqualTypeOf<Record<"user-id", string>>();
334
+
335
+ // Consecutive slashes (malformed but should handle gracefully)
336
+ expectTypeOf<ExtractPathParams<"//path//:name">>().toEqualTypeOf<Record<"name", string>>();
337
+
338
+ // Mixed wildcards and parameters
339
+ expectTypeOf<ExtractPathParams<"/api/:version/**:rest">>().toEqualTypeOf<
340
+ Record<"version" | "rest", string>
341
+ >();
342
+
343
+ // Multiple wildcards (edge case)
344
+ expectTypeOf<ExtractPathParams<"/api/**:first/**:second">>().toEqualTypeOf<
345
+ Record<"first" | "second", string>
346
+ >();
347
+
348
+ expectTypeOf<ExtractPathParams<string>>().toEqualTypeOf<Record<string, never>>();
349
+ });
350
+
351
+ test("Real-world route examples", () => {
352
+ // Common REST API patterns
353
+ expectTypeOf<ExtractPathParams<"/api/v1/users/:userId">>().toEqualTypeOf<
354
+ Record<"userId", string>
355
+ >();
356
+ expectTypeOf<
357
+ ExtractPathParams<"/api/v1/users/:userId/posts/:postId/comments/:commentId">
358
+ >().toEqualTypeOf<Record<"userId" | "postId" | "commentId", string>>();
359
+
360
+ // File serving patterns
361
+ expectTypeOf<ExtractPathParams<"/static/**:filepath">>().toEqualTypeOf<
362
+ Record<"filepath", string>
363
+ >();
364
+ expectTypeOf<ExtractPathParams<"/uploads/:userId/**:filename">>().toEqualTypeOf<
365
+ Record<"userId" | "filename", string>
366
+ >();
367
+
368
+ // Admin/dashboard patterns
369
+ expectTypeOf<ExtractPathParams<"/admin/:section/:action">>().toEqualTypeOf<
370
+ Record<"section" | "action", string>
371
+ >();
372
+ expectTypeOf<ExtractPathParams<"/dashboard/:org/projects/:projectId/settings">>().toEqualTypeOf<
373
+ Record<"org" | "projectId", string>
374
+ >();
375
+ });
376
+
377
+ // Runtime verification tests (ensuring the types work as expected in practice)
378
+ test("Type compatibility runtime tests", () => {
379
+ // These tests verify that the types actually work as expected at runtime
380
+ // by checking if type assignments would be valid
381
+
382
+ // Function that expects no params
383
+ function _handleNoParams(_params: ExtractPathParams<"/static/assets">) {
384
+ // Should receive Record<string, never> which is essentially {}
385
+ }
386
+
387
+ // Function that expects specific params
388
+ function _handleUserRoute(params: ExtractPathParams<"/users/:id">) {
389
+ // Should have id property
390
+ expect(typeof params).toBe("object");
391
+ // Type system ensures params has 'id' property of type string
392
+ }
393
+
394
+ // Function that expects multiple params
395
+ function _handleComplexRoute(
396
+ params: ExtractPathParams<"/api/:version/users/:userId/posts/:postId">,
397
+ ) {
398
+ // Should have version, userId, and postId properties
399
+ expect(typeof params).toBe("object");
400
+ // Type system ensures all required properties exist
401
+ }
402
+
403
+ // Test type narrowing works
404
+ const _path1 = "/users/:id" as const;
405
+ type Path1Params = ExtractPathParams<typeof _path1>;
406
+ expectTypeOf<Path1Params>().toEqualTypeOf<Record<"id", string>>();
407
+
408
+ const _path2 = "/static/files" as const;
409
+ type Path2Params = ExtractPathParams<typeof _path2>;
410
+ expectTypeOf<Path2Params>().toEqualTypeOf<Record<string, never>>();
411
+
412
+ // Verify HasPathParams utility
413
+ const hasParams1: HasPathParams<"/users/:id"> = true;
414
+ const hasParams2: HasPathParams<"/static"> = false;
415
+
416
+ expect(hasParams1).toBe(true);
417
+ expect(hasParams2).toBe(false);
418
+ });
419
+
420
+ test("ExtractPathParamsOrWiden type tests", () => {
421
+ expectTypeOf<ExtractPathParamsOrWiden<"/path">>().toEqualTypeOf<Record<string, never>>();
422
+ expectTypeOf<ExtractPathParamsOrWiden<"/path/:id">>().toEqualTypeOf<Record<"id", string>>();
423
+ expectTypeOf<ExtractPathParamsOrWiden<"/path/:id", number>>().toEqualTypeOf<
424
+ Record<"id", number>
425
+ >();
426
+ expectTypeOf<ExtractPathParamsOrWiden<"/path/:id", boolean>>().toEqualTypeOf<
427
+ Record<"id", boolean>
428
+ >();
429
+ expectTypeOf<ExtractPathParamsOrWiden<"/path/:id", undefined>>().toEqualTypeOf<
430
+ Record<"id", undefined>
431
+ >();
432
+
433
+ // This is the actual tests
434
+ expectTypeOf<ExtractPathParamsOrWiden<string>>().toEqualTypeOf<Record<string, string>>();
435
+ });
436
+
437
+ test("MaybeExtractPathParamsOrWiden type tests", () => {
438
+ expectTypeOf<MaybeExtractPathParamsOrWiden<"/path">>().toEqualTypeOf<undefined>();
439
+ expectTypeOf<MaybeExtractPathParamsOrWiden<"/path/:id">>().toEqualTypeOf<Record<"id", string>>();
440
+ expectTypeOf<MaybeExtractPathParamsOrWiden<"/path/:id", number>>().toEqualTypeOf<
441
+ Record<"id", number>
442
+ >();
443
+ expectTypeOf<MaybeExtractPathParamsOrWiden<string>>().toEqualTypeOf<undefined>();
444
+ });
445
+
446
+ test("QueryParamsHint type tests", () => {
447
+ // Basic usage with string union
448
+ expectTypeOf<QueryParamsHint<"page" | "limit">>().toEqualTypeOf<
449
+ Partial<Record<"page" | "limit", string>> & Record<string, string>
450
+ >();
451
+
452
+ // Single parameter hint
453
+ expectTypeOf<QueryParamsHint<"search">>().toEqualTypeOf<
454
+ Partial<Record<"search", string>> & Record<string, string>
455
+ >();
456
+
457
+ // Empty hint (never) - should still allow any string keys
458
+ expectTypeOf<QueryParamsHint<never>>().toEqualTypeOf<
459
+ Partial<Record<never, string>> & Record<string, string>
460
+ >();
461
+
462
+ // With custom value type
463
+ expectTypeOf<QueryParamsHint<"page" | "limit", number>>().toEqualTypeOf<
464
+ Partial<Record<"page" | "limit", number>> & Record<string, number>
465
+ >();
466
+
467
+ // With boolean value type
468
+ expectTypeOf<QueryParamsHint<"enabled" | "debug", boolean>>().toEqualTypeOf<
469
+ Partial<Record<"enabled" | "debug", boolean>> & Record<string, boolean>
470
+ >();
471
+
472
+ // With union value type
473
+ type StringOrNumber = string | number;
474
+ expectTypeOf<QueryParamsHint<"value", StringOrNumber>>().toEqualTypeOf<
475
+ Partial<Record<"value", StringOrNumber>> & Record<string, StringOrNumber>
476
+ >();
477
+
478
+ // With custom object type
479
+ type CustomType = { raw: string; parsed: boolean };
480
+ expectTypeOf<QueryParamsHint<"data", CustomType>>().toEqualTypeOf<
481
+ Partial<Record<"data", CustomType>> & Record<string, CustomType>
482
+ >();
483
+ });
484
+
485
+ test("QueryParamsHint assignability tests", () => {
486
+ // Test that the type allows the expected assignments
487
+ type TestQuery = QueryParamsHint<"page" | "limit">;
488
+
489
+ // Empty object should be assignable
490
+ const query1: TestQuery = {};
491
+ expect(query1).toEqual({});
492
+
493
+ // Hinted parameters should be assignable
494
+ const query2: TestQuery = { page: "1" };
495
+ expect(query2.page).toBe("1");
496
+
497
+ const query3: TestQuery = { limit: "10" };
498
+ expect(query3.limit).toBe("10");
499
+
500
+ const query4: TestQuery = { page: "1", limit: "10" };
501
+ expect(query4.page).toBe("1");
502
+ expect(query4.limit).toBe("10");
503
+
504
+ // Additional parameters should be assignable
505
+ const query5: TestQuery = { page: "1", sort: "asc" };
506
+ expect(query5.page).toBe("1");
507
+ expect(query5["sort"]).toBe("asc");
508
+
509
+ const query6: TestQuery = { search: "test", filter: "active" };
510
+ expect(query6["search"]).toBe("test");
511
+ expect(query6["filter"]).toBe("active");
512
+
513
+ // Mixed hinted and additional parameters
514
+ const query7: TestQuery = { page: "1", limit: "10", sort: "desc", filter: "all" };
515
+ expect(query7.page).toBe("1");
516
+ expect(query7.limit).toBe("10");
517
+ expect(query7["sort"]).toBe("desc");
518
+ expect(query7["filter"]).toBe("all");
519
+ });
520
+
521
+ test("QueryParamsHint with different value types", () => {
522
+ // Number value type
523
+ type NumberQuery = QueryParamsHint<"count" | "offset", number>;
524
+
525
+ const numQuery1: NumberQuery = {};
526
+ expect(numQuery1).toEqual({});
527
+
528
+ const numQuery2: NumberQuery = { count: 5 };
529
+ expect(numQuery2.count).toBe(5);
530
+
531
+ const numQuery3: NumberQuery = { count: 5, extra: 10 };
532
+ expect(numQuery3.count).toBe(5);
533
+ expect(numQuery3["extra"]).toBe(10);
534
+
535
+ // Boolean value type
536
+ type BooleanQuery = QueryParamsHint<"enabled" | "debug", boolean>;
537
+
538
+ const boolQuery1: BooleanQuery = {};
539
+ expect(boolQuery1).toEqual({});
540
+
541
+ const boolQuery2: BooleanQuery = { enabled: true };
542
+ expect(boolQuery2.enabled).toBe(true);
543
+
544
+ const boolQuery3: BooleanQuery = { enabled: true, verbose: false };
545
+ expect(boolQuery3.enabled).toBe(true);
546
+ expect(boolQuery3["verbose"]).toBe(false);
547
+
548
+ // Union type
549
+ type MixedQuery = QueryParamsHint<"value", string | number>;
550
+
551
+ const mixedQuery1: MixedQuery = { value: "text" };
552
+ expect(mixedQuery1.value).toBe("text");
553
+
554
+ const mixedQuery2: MixedQuery = { value: 42 };
555
+ expect(mixedQuery2.value).toBe(42);
556
+
557
+ const mixedQuery3: MixedQuery = { value: "text", other: 123 };
558
+ expect(mixedQuery3.value).toBe("text");
559
+ expect(mixedQuery3["other"]).toBe(123);
560
+ });
561
+
562
+ test("QueryParamsHint real-world examples", () => {
563
+ // Pagination query
564
+ type PaginationQuery = QueryParamsHint<"page" | "limit" | "offset">;
565
+
566
+ const paginationQuery: PaginationQuery = {
567
+ page: "1",
568
+ limit: "20",
569
+ sort: "created_at",
570
+ order: "desc",
571
+ };
572
+ expect(paginationQuery.page).toBe("1");
573
+ expect(paginationQuery.limit).toBe("20");
574
+ expect(paginationQuery["sort"]).toBe("created_at");
575
+ expect(paginationQuery["order"]).toBe("desc");
576
+
577
+ // Search and filter query
578
+ type SearchQuery = QueryParamsHint<"q" | "category" | "tags">;
579
+
580
+ const searchQuery: SearchQuery = {
581
+ q: "typescript",
582
+ status: "published",
583
+ author: "john",
584
+ };
585
+ expect(searchQuery.q).toBe("typescript");
586
+ expect(searchQuery["status"]).toBe("published");
587
+ expect(searchQuery["author"]).toBe("john");
588
+
589
+ // API configuration query
590
+ type ApiQuery = QueryParamsHint<"version" | "format">;
591
+
592
+ const apiQuery: ApiQuery = {
593
+ version: "v2",
594
+ format: "json",
595
+ include: "metadata",
596
+ fields: "id,name,created_at",
597
+ };
598
+ expect(apiQuery.version).toBe("v2");
599
+ expect(apiQuery.format).toBe("json");
600
+ expect(apiQuery["include"]).toBe("metadata");
601
+ expect(apiQuery["fields"]).toBe("id,name,created_at");
602
+ });