breaking: remove param files in folder in favor of params.js/ts file#16189
breaking: remove param files in folder in favor of params.js/ts file#16189dummdidumm wants to merge 25 commits into
params.js/ts file#16189Conversation
basically implements #10407 (comment): - new `parse` function can be exported from `params/someMatcher.js` - can be either alongside `match` or standalone - if it throws it counts as invalid and the route will not be matched closes #10407
match can now also be a standard schema. If it parses you get the transformed value. If it throws it counts as invalid and the route will not be matched. closes #10407
…`'~standard'` property, so callable Standard Schemas (e.g. ArkType) are misclassified as plain predicate functions, breaking validation and transformation.
This commit fixes the issue reported at packages/kit/src/utils/routing.js:139
## Bug
`ParamMatcher` is typed as `((param: string) => boolean) | StandardSchemaV1<string, any>`. Several headline Standard Schema implementations expose their schema as a **callable function that also carries a `'~standard'` property**. ArkType is the canonical example:
```js
const t = type('string.numeric.parse');
t('42'); // → 42 (parsed value, or an ArkErrors object on failure)
t['~standard'].validate('42'); // also available
```
In the original `run_matcher`, the `typeof matcher === 'function'` branch comes first:
```js
if (typeof matcher === 'function') {
return matcher(value) ? { success: true, value } : { success: false };
}
if ('~standard' in matcher) { ... }
```
Because an ArkType schema is a function, it never reaches the `'~standard'` branch. Instead `matcher(value)` is called and its return value treated as a boolean:
- **Invalid input**: ArkType returns a truthy `ArkErrors` object → treated as a *successful* match, and the raw, untransformed string is stored as the param value. The route matches when it should not.
- **Valid input that transforms to a falsy value** (e.g. `0`, empty string): the truthiness check fails → the route is incorrectly rejected.
So both validation and transformation are broken for callable Standard Schemas.
## Fix
Reorder the checks so `'~standard'` is tested first. `'~standard' in matcher` works on functions too (functions are objects), so a callable Standard Schema is now routed through the proper `validate` path. Plain predicate functions lack the `'~standard'` property and correctly fall through to the function branch.
```js
function run_matcher(matcher, value) {
if ('~standard' in matcher) {
const result = matcher['~standard'].validate(value);
if (result instanceof Promise || result.issues) {
return { success: false };
}
return { success: true, value: result.value };
}
if (typeof matcher === 'function') {
return matcher(value) ? { success: true, value } : { success: false };
}
return { success: false };
}
```
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: dummdidumm <sholthausen@web.de>
Instead of having to declare one param matcher per file, you now declare all of them within one file. A `defineParams` function both helps with declaring them in a type-safe way and also normalizes the property values: You can either parse a standard schema object, or a function that either throws or returns the parsed value (if you don't want to bring in a schema library). closes #10407 Kudos to @phi-bre for suggesting this in #16148 (comment)
🦋 Changeset detectedLatest commit: 6067b3a The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
…cing the invalid path `src/params/.js` instead of `src/params.js`.
This commit fixes the issue reported at packages/kit/src/core/sync/write_types/index.js:605
## Bug
In `generate_params_type` (`write_types/index.js:606`) and the identical logic in `write_non_ambient.js:101`, the fallback used when `resolve_entry` returns `null` was:
```js
path.join(config.kit.files.params.replace(/.(js|ts)$/, ''), '.js');
```
`config.kit.files.params` defaults to `path.join(files.src, 'params')` → `src/params` (no extension). The `.replace(/.(js|ts)$/, '')` is a no-op there, and `path.join('src/params', '.js')` inserts a path separator, yielding `src/params/.js`. Even when the config value has an extension (e.g. `src/params.ts`), the replace strips it to `src/params` and `path.join` again yields `src/params/.js`.
Verified with Node:
```
$ node -e "console.log(require('path').join('src/params', '.js'))"
src/params/.js
```
### Trigger / impact
When a route references a matcher but `resolve_entry(config.kit.files.params)` returns `null` (params file not found at resolution time), the generated `$types`/app-types contain an import from the broken path `src/params/.js` instead of `src/params.js`, producing invalid type imports.
## Fix
Replaced `path.join(base, '.js')` with string concatenation `base + '.js'` in both locations:
```js
config.kit.files.params.replace(/.(js|ts)$/, '') + '.js';
```
Verified output is now `src/params.js` for both `src/params` and `src/params.ts` inputs.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: dummdidumm <sholthausen@web.de>
|
Hello, Two remarks.
For example if I have this params : export const params = defineParams({
details: v.picklist(['min', 'standard', 'max']);
});I think this should be typed accordingly : resolve('/info/[details=details]', { details: 'min' }); // OK
resolve('/info/[details=details]', { details: 'minimum' }); // Type Error
For example, if I have bigint as uid, and I want to use it as hexadecimal on the URL : export const params = defineParams({
uid: {
// parse the uid as a hexa BigInt :
parse: v.pipe(v.string(),v.transform((value) => BigInt(`0x${value}`)),
// format the BigInt in hexa
format: (value) => value.toString(16)
}
}); |
params.js/ts fileparams.js/ts file
Co-authored-by: Conduitry <git@chor.date>
|
Food for thought, Though prior art is mixed:
|
|
this is better than what we currently got. one question though, will this feature be backported to kit v2 behind an experimental flag? docs dont mention anything about that |
|
no there are no plans to backport this at the moment |
|
Install the latest version of pnpm add https://pkg.svelte.dev/@sveltejs/kit/c/6067b3a96161b9ceec13693f9c91aeaed6f3ff6fOpen in |
that wouldn't really make sense. |
…try-catch on server.
There was a problem hiding this comment.
Additional Suggestion:
A throwing param matcher breaks client-side navigation (silent no-op / unhandled promise rejection) even though the server handles the same throw gracefully, creating a server/client asymmetry introduced by commit f5479de.
|
Vercel is right, but I don't see a good way how we can sensibly handle this. We could fall back to a full page reload to get the better error message eventually that way. // in function navigate(...)
try {
intent ??= await get_navigation_intent(url, false);
} catch {
// Likely a matcher threw unexpectedly. Fall back to the server to get a proper error page.
return await native_navigation(url, replace_state);
}Anything more would mean polluting the whole |
Instead of having to declare one param matcher per file, you now declare all of them within one file. A
defineParamsfunction both helps with declaring them in a type-safe way and also normalizes the property values: You can either parse a standard schema object, or a function that either throws or returns the parsed value (if you don't want to bring in a schema library).closes #10407
Kudos to @phi-bre for suggesting this in #16148 (comment)