@native-router/core 1.8.0 → 1.9.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.
- package/README.md +33 -0
- package/dist/index.cjs +108 -2
- package/dist/index.mjs +106 -3
- package/dist/types/errors.d.ts +13 -0
- package/dist/types/router.d.ts +6 -1
- package/dist/types/search.d.ts +31 -0
- package/dist/types/types.d.ts +35 -2
- package/package.json +22 -19
package/README.md
CHANGED
|
@@ -113,6 +113,39 @@ const router = create(
|
|
|
113
113
|
- Guards: `beforeLoad` receives the level's parsed search as `ctx.search` — the schema output (parsed with `parseSearch`, so async validators work), or the degraded input on schema-less levels; an invalid search fails the resolution through the `errorHandler` channel like a data-phase search error
|
|
114
114
|
- A rejected validation throws `SearchError` (a `NativeRouterError`) carrying the raw `search` and the reported `issues` — route it through your `errorHandler` like any other resolve failure
|
|
115
115
|
|
|
116
|
+
## Params validation
|
|
117
|
+
|
|
118
|
+
Params are always strings (wildcards: string arrays) — the URL has no types. Declare a `params` schema on a route level and the core validates/coerces the merged params of that level before its `beforeLoad` runs, so guards see numbers instead of `Number(id)` everywhere.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import {create} from '@native-router/core';
|
|
122
|
+
import {z} from 'zod';
|
|
123
|
+
|
|
124
|
+
const router = create(
|
|
125
|
+
{
|
|
126
|
+
path: '',
|
|
127
|
+
children: [
|
|
128
|
+
{
|
|
129
|
+
path: '/users/:id',
|
|
130
|
+
params: z.object({id: z.coerce.number().int().positive()}),
|
|
131
|
+
beforeLoad: ({params}) => {
|
|
132
|
+
params.id; // number — coerced, or the navigation failed
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
},
|
|
137
|
+
createBrowserHistory(),
|
|
138
|
+
(matched) => renderUser(matched)
|
|
139
|
+
);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
- No `params` schema → behavior unchanged: the raw string map flows through
|
|
143
|
+
- The parse runs per level (shallow → deep): a level's schema validates the params merged up to it; a deeper schema sees the (possibly coerced) output of the shallower ones
|
|
144
|
+
- A `redirect` level skips its params schema entirely — the level's guard never runs, so there is nothing to hand coerced params to; the same asymmetry the search schema has (`redirect` wins over `beforeLoad`). Hanging a params schema on a redirect level is inert, it cannot fail the navigation
|
|
145
|
+
- A same-name param on both a parent and a child segment (`/users/:id/files/:id`): the deep-over-shallow merge operates on the **raw** strings, so the child segment's value overwrites the parent's coerced one — a child guard sees the raw string again. Declare the coercing schema on (or below) the deepest level that reads the param — a deeper schema validates the whole merged map anyway — or avoid reusing a param name across levels
|
|
146
|
+
- A rejected validation fails the resolution through the `errorHandler` channel with a `ParamsError` (a `NativeRouterError`) carrying the raw `params` and the reported `issues` — the same route a search-schema failure takes
|
|
147
|
+
- `parseParams`/`parseParamsSync` are exported for custom `resolveView` implementations (the async/sync flavors mirror `parseSearch`/`parseSearchSync`)
|
|
148
|
+
|
|
116
149
|
## Design principles
|
|
117
150
|
|
|
118
151
|
**Navigation semantics follow the browser** — native-router aligns with browser-native navigation semantics, not with what other SPA routers happen to do. Every navigation API decision is measured against that yardstick; "a popular router has it" is not, by itself, a reason to follow. These are deliberate choices, not bugs to fix.
|
package/dist/index.cjs
CHANGED
|
@@ -39,6 +39,28 @@ class SearchError extends NativeRouterError {
|
|
|
39
39
|
this.issues = issues;
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Thrown when a route {@link BaseRoute.params params schema} rejects the
|
|
45
|
+
* merged path params. Issues are formatted like {@link SearchError}'s —
|
|
46
|
+
* `path: message` pairs joined with `; `, e.g.
|
|
47
|
+
* `Invalid path params "/users/abc": id: expected a number`.
|
|
48
|
+
*/
|
|
49
|
+
class ParamsError extends NativeRouterError {
|
|
50
|
+
/** The raw params object that failed validation. */
|
|
51
|
+
params;
|
|
52
|
+
|
|
53
|
+
/** The issues reported by the schema. */
|
|
54
|
+
issues;
|
|
55
|
+
constructor(params, issues) {
|
|
56
|
+
super(`Invalid path params "${JSON.stringify(params)}": ${issues.map(({
|
|
57
|
+
message,
|
|
58
|
+
path
|
|
59
|
+
}) => `${formatIssuePath(path)}${message}`).join('; ')}`);
|
|
60
|
+
this.params = params;
|
|
61
|
+
this.issues = issues;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
42
64
|
function formatIssuePath(path) {
|
|
43
65
|
if (!path?.length) return '';
|
|
44
66
|
const keys = path.map(segment => typeof segment === 'object' ? String(segment.key) : String(segment));
|
|
@@ -123,6 +145,53 @@ function isThenable(value) {
|
|
|
123
145
|
return typeof value?.then === 'function';
|
|
124
146
|
}
|
|
125
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Validate the merged path params of a route level with a
|
|
150
|
+
* {@link StandardSchemaV1} schema — any zod/valibot/arktype schema
|
|
151
|
+
* works, no hard dependency. Params arrive as the plain string map the
|
|
152
|
+
* matcher extracted(see `mergeMatchedParams`), so schemas can coerce
|
|
153
|
+
* (`'7'` → `7`) and normalize along the way.
|
|
154
|
+
*
|
|
155
|
+
* Async schemas(`validate` returning a promise) are awaited.
|
|
156
|
+
*
|
|
157
|
+
* @group Methods
|
|
158
|
+
* @category Route
|
|
159
|
+
* @param schema the params schema
|
|
160
|
+
* @param params the merged raw params of the level
|
|
161
|
+
* @returns the parsed(and possibly coerced) output of the schema
|
|
162
|
+
* @throws {ParamsError} when the schema reports issues
|
|
163
|
+
*/
|
|
164
|
+
async function parseParams(schema, params) {
|
|
165
|
+
const result = await schema['~standard'].validate(params);
|
|
166
|
+
if (result.issues) throw new ParamsError(params, result.issues);
|
|
167
|
+
// The schema's declared output; the loose `StandardSchemaV1` default
|
|
168
|
+
// degrades to `unknown`.
|
|
169
|
+
return result.value;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Synchronous flavor of {@link parseParams}, for render-time reads and
|
|
174
|
+
* custom `resolveView` implementations.
|
|
175
|
+
*
|
|
176
|
+
* @group Methods
|
|
177
|
+
* @category Route
|
|
178
|
+
* @param schema the params schema — must validate synchronously
|
|
179
|
+
* @param params the merged raw params of the level
|
|
180
|
+
* @returns the parsed(and possibly coerced) output of the schema
|
|
181
|
+
* @throws {ParamsError} when the schema reports issues
|
|
182
|
+
* @throws when the schema validates asynchronously; use {@link parseParams}
|
|
183
|
+
* for async schemas instead
|
|
184
|
+
*/
|
|
185
|
+
function parseParamsSync(schema, params) {
|
|
186
|
+
const result = schema['~standard'].validate(params);
|
|
187
|
+
if (isThenable(result)) {
|
|
188
|
+
throw new Error('The params schema validates asynchronously; parse it during resolve ' + '(parseParams) instead of synchronously');
|
|
189
|
+
}
|
|
190
|
+
if (result.issues) throw new ParamsError(params, result.issues);
|
|
191
|
+
// See parseParams for the cast rationale.
|
|
192
|
+
return result.value;
|
|
193
|
+
}
|
|
194
|
+
|
|
126
195
|
const DEFAULT_MAX_STACK_DEPTH = 100;
|
|
127
196
|
|
|
128
197
|
/** Max redirects followed by {@link resolveEntry} before giving up. */
|
|
@@ -408,7 +477,12 @@ function resolveTo(router, to, state) {
|
|
|
408
477
|
* A guard's context also carries the level's parsed
|
|
409
478
|
* {@link GuardContext.search search}: the {@link BaseRoute.search schema}
|
|
410
479
|
* output(its validation failure rejects this resolution with a
|
|
411
|
-
* `SearchError`), or the degraded input without a schema.
|
|
480
|
+
* `SearchError`), or the degraded input without a schema. Its
|
|
481
|
+
* {@link GuardContext.params params} are likewise the merged raw string
|
|
482
|
+
* map unless some level declares a {@link BaseRoute.params params
|
|
483
|
+
* schema} — the deepest schema seen so far has already upgraded them to
|
|
484
|
+
* its output(its validation failure rides the same channel with a
|
|
485
|
+
* `ParamsError`).
|
|
412
486
|
*
|
|
413
487
|
* @group Methods
|
|
414
488
|
* @category Router
|
|
@@ -441,10 +515,39 @@ async function resolveEntry(router, location, opts) {
|
|
|
441
515
|
};
|
|
442
516
|
}
|
|
443
517
|
let redirected = false;
|
|
518
|
+
// Raw params merged level-by-level; a level with a `params` schema
|
|
519
|
+
// upgrades them to the schema output before its guard runs, so
|
|
520
|
+
// guards of deeper levels see coerced params of the whole prefix.
|
|
521
|
+
let params = {};
|
|
444
522
|
for (let i = 0; i < matched.length; i++) {
|
|
445
523
|
const {
|
|
446
524
|
route
|
|
447
525
|
} = matched[i];
|
|
526
|
+
params = {
|
|
527
|
+
...params,
|
|
528
|
+
...matched[i].params
|
|
529
|
+
};
|
|
530
|
+
// The level's params schema runs before its guard, so the guard
|
|
531
|
+
// sees the coerced output. A redirect level never runs its guard,
|
|
532
|
+
// so its schema is skipped — the same asymmetry the level's
|
|
533
|
+
// search schema already has(`redirect` wins over `beforeLoad`):
|
|
534
|
+
// hanging a params schema on a redirect level must not be able to
|
|
535
|
+
// fail the navigation, its only observable effect would be the
|
|
536
|
+
// failure. A validation failure fails the resolution through the
|
|
537
|
+
// task's errorHandler channel — the same route a search-schema
|
|
538
|
+
// failure takes — instead of rejecting this entry, which preload
|
|
539
|
+
// consumers share.
|
|
540
|
+
if (route.params && !route.redirect) {
|
|
541
|
+
try {
|
|
542
|
+
// eslint-disable-next-line no-await-in-loop -- guards must run in declaration order, sequentially
|
|
543
|
+
params = await parseParams(route.params, params);
|
|
544
|
+
} catch (e) {
|
|
545
|
+
return {
|
|
546
|
+
location,
|
|
547
|
+
task: Promise.reject(e).catch(errorHandler)
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
}
|
|
448
551
|
// `redirect` wins over `beforeLoad`; a non-empty string target
|
|
449
552
|
// restarts the resolution at the redirected location.
|
|
450
553
|
let target = route.redirect;
|
|
@@ -473,7 +576,7 @@ async function resolveEntry(router, location, opts) {
|
|
|
473
576
|
target = await route.beforeLoad({
|
|
474
577
|
router,
|
|
475
578
|
location,
|
|
476
|
-
params
|
|
579
|
+
params,
|
|
477
580
|
signal,
|
|
478
581
|
search
|
|
479
582
|
});
|
|
@@ -1184,6 +1287,7 @@ function getParams(router) {
|
|
|
1184
1287
|
|
|
1185
1288
|
exports.NativeRouterError = NativeRouterError;
|
|
1186
1289
|
exports.NotFoundError = NotFoundError;
|
|
1290
|
+
exports.ParamsError = ParamsError;
|
|
1187
1291
|
exports.RedirectLoopError = RedirectLoopError;
|
|
1188
1292
|
exports.SearchError = SearchError;
|
|
1189
1293
|
exports.back = back;
|
|
@@ -1203,6 +1307,8 @@ exports.listen = listen;
|
|
|
1203
1307
|
exports.match = match;
|
|
1204
1308
|
exports.mergeMatchedParams = mergeMatchedParams;
|
|
1205
1309
|
exports.navigate = navigate;
|
|
1310
|
+
exports.parseParams = parseParams;
|
|
1311
|
+
exports.parseParamsSync = parseParamsSync;
|
|
1206
1312
|
exports.parseSearch = parseSearch;
|
|
1207
1313
|
exports.parseSearchInput = parseSearchInput;
|
|
1208
1314
|
exports.parseSearchSync = parseSearchSync;
|
package/dist/index.mjs
CHANGED
|
@@ -37,6 +37,28 @@ class SearchError extends NativeRouterError {
|
|
|
37
37
|
this.issues = issues;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Thrown when a route {@link BaseRoute.params params schema} rejects the
|
|
43
|
+
* merged path params. Issues are formatted like {@link SearchError}'s —
|
|
44
|
+
* `path: message` pairs joined with `; `, e.g.
|
|
45
|
+
* `Invalid path params "/users/abc": id: expected a number`.
|
|
46
|
+
*/
|
|
47
|
+
class ParamsError extends NativeRouterError {
|
|
48
|
+
/** The raw params object that failed validation. */
|
|
49
|
+
params;
|
|
50
|
+
|
|
51
|
+
/** The issues reported by the schema. */
|
|
52
|
+
issues;
|
|
53
|
+
constructor(params, issues) {
|
|
54
|
+
super(`Invalid path params "${JSON.stringify(params)}": ${issues.map(({
|
|
55
|
+
message,
|
|
56
|
+
path
|
|
57
|
+
}) => `${formatIssuePath(path)}${message}`).join('; ')}`);
|
|
58
|
+
this.params = params;
|
|
59
|
+
this.issues = issues;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
40
62
|
function formatIssuePath(path) {
|
|
41
63
|
if (!path?.length) return '';
|
|
42
64
|
const keys = path.map(segment => typeof segment === 'object' ? String(segment.key) : String(segment));
|
|
@@ -121,6 +143,53 @@ function isThenable(value) {
|
|
|
121
143
|
return typeof value?.then === 'function';
|
|
122
144
|
}
|
|
123
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Validate the merged path params of a route level with a
|
|
148
|
+
* {@link StandardSchemaV1} schema — any zod/valibot/arktype schema
|
|
149
|
+
* works, no hard dependency. Params arrive as the plain string map the
|
|
150
|
+
* matcher extracted(see `mergeMatchedParams`), so schemas can coerce
|
|
151
|
+
* (`'7'` → `7`) and normalize along the way.
|
|
152
|
+
*
|
|
153
|
+
* Async schemas(`validate` returning a promise) are awaited.
|
|
154
|
+
*
|
|
155
|
+
* @group Methods
|
|
156
|
+
* @category Route
|
|
157
|
+
* @param schema the params schema
|
|
158
|
+
* @param params the merged raw params of the level
|
|
159
|
+
* @returns the parsed(and possibly coerced) output of the schema
|
|
160
|
+
* @throws {ParamsError} when the schema reports issues
|
|
161
|
+
*/
|
|
162
|
+
async function parseParams(schema, params) {
|
|
163
|
+
const result = await schema['~standard'].validate(params);
|
|
164
|
+
if (result.issues) throw new ParamsError(params, result.issues);
|
|
165
|
+
// The schema's declared output; the loose `StandardSchemaV1` default
|
|
166
|
+
// degrades to `unknown`.
|
|
167
|
+
return result.value;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Synchronous flavor of {@link parseParams}, for render-time reads and
|
|
172
|
+
* custom `resolveView` implementations.
|
|
173
|
+
*
|
|
174
|
+
* @group Methods
|
|
175
|
+
* @category Route
|
|
176
|
+
* @param schema the params schema — must validate synchronously
|
|
177
|
+
* @param params the merged raw params of the level
|
|
178
|
+
* @returns the parsed(and possibly coerced) output of the schema
|
|
179
|
+
* @throws {ParamsError} when the schema reports issues
|
|
180
|
+
* @throws when the schema validates asynchronously; use {@link parseParams}
|
|
181
|
+
* for async schemas instead
|
|
182
|
+
*/
|
|
183
|
+
function parseParamsSync(schema, params) {
|
|
184
|
+
const result = schema['~standard'].validate(params);
|
|
185
|
+
if (isThenable(result)) {
|
|
186
|
+
throw new Error('The params schema validates asynchronously; parse it during resolve ' + '(parseParams) instead of synchronously');
|
|
187
|
+
}
|
|
188
|
+
if (result.issues) throw new ParamsError(params, result.issues);
|
|
189
|
+
// See parseParams for the cast rationale.
|
|
190
|
+
return result.value;
|
|
191
|
+
}
|
|
192
|
+
|
|
124
193
|
const DEFAULT_MAX_STACK_DEPTH = 100;
|
|
125
194
|
|
|
126
195
|
/** Max redirects followed by {@link resolveEntry} before giving up. */
|
|
@@ -406,7 +475,12 @@ function resolveTo(router, to, state) {
|
|
|
406
475
|
* A guard's context also carries the level's parsed
|
|
407
476
|
* {@link GuardContext.search search}: the {@link BaseRoute.search schema}
|
|
408
477
|
* output(its validation failure rejects this resolution with a
|
|
409
|
-
* `SearchError`), or the degraded input without a schema.
|
|
478
|
+
* `SearchError`), or the degraded input without a schema. Its
|
|
479
|
+
* {@link GuardContext.params params} are likewise the merged raw string
|
|
480
|
+
* map unless some level declares a {@link BaseRoute.params params
|
|
481
|
+
* schema} — the deepest schema seen so far has already upgraded them to
|
|
482
|
+
* its output(its validation failure rides the same channel with a
|
|
483
|
+
* `ParamsError`).
|
|
410
484
|
*
|
|
411
485
|
* @group Methods
|
|
412
486
|
* @category Router
|
|
@@ -439,10 +513,39 @@ async function resolveEntry(router, location, opts) {
|
|
|
439
513
|
};
|
|
440
514
|
}
|
|
441
515
|
let redirected = false;
|
|
516
|
+
// Raw params merged level-by-level; a level with a `params` schema
|
|
517
|
+
// upgrades them to the schema output before its guard runs, so
|
|
518
|
+
// guards of deeper levels see coerced params of the whole prefix.
|
|
519
|
+
let params = {};
|
|
442
520
|
for (let i = 0; i < matched.length; i++) {
|
|
443
521
|
const {
|
|
444
522
|
route
|
|
445
523
|
} = matched[i];
|
|
524
|
+
params = {
|
|
525
|
+
...params,
|
|
526
|
+
...matched[i].params
|
|
527
|
+
};
|
|
528
|
+
// The level's params schema runs before its guard, so the guard
|
|
529
|
+
// sees the coerced output. A redirect level never runs its guard,
|
|
530
|
+
// so its schema is skipped — the same asymmetry the level's
|
|
531
|
+
// search schema already has(`redirect` wins over `beforeLoad`):
|
|
532
|
+
// hanging a params schema on a redirect level must not be able to
|
|
533
|
+
// fail the navigation, its only observable effect would be the
|
|
534
|
+
// failure. A validation failure fails the resolution through the
|
|
535
|
+
// task's errorHandler channel — the same route a search-schema
|
|
536
|
+
// failure takes — instead of rejecting this entry, which preload
|
|
537
|
+
// consumers share.
|
|
538
|
+
if (route.params && !route.redirect) {
|
|
539
|
+
try {
|
|
540
|
+
// eslint-disable-next-line no-await-in-loop -- guards must run in declaration order, sequentially
|
|
541
|
+
params = await parseParams(route.params, params);
|
|
542
|
+
} catch (e) {
|
|
543
|
+
return {
|
|
544
|
+
location,
|
|
545
|
+
task: Promise.reject(e).catch(errorHandler)
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
}
|
|
446
549
|
// `redirect` wins over `beforeLoad`; a non-empty string target
|
|
447
550
|
// restarts the resolution at the redirected location.
|
|
448
551
|
let target = route.redirect;
|
|
@@ -471,7 +574,7 @@ async function resolveEntry(router, location, opts) {
|
|
|
471
574
|
target = await route.beforeLoad({
|
|
472
575
|
router,
|
|
473
576
|
location,
|
|
474
|
-
params
|
|
577
|
+
params,
|
|
475
578
|
signal,
|
|
476
579
|
search
|
|
477
580
|
});
|
|
@@ -1180,4 +1283,4 @@ function getParams(router) {
|
|
|
1180
1283
|
return mergeMatchedParams(match(router, location.pathname) ?? []);
|
|
1181
1284
|
}
|
|
1182
1285
|
|
|
1183
|
-
export { NativeRouterError, NotFoundError, RedirectLoopError, SearchError, back, cancel, commit, commitReplace, create, createHref, forward, getCurrentView, getLocation, getParams, go, initHistoryStack, invalidate, listen, match, mergeMatchedParams, navigate, parseSearch, parseSearchInput, parseSearchSync, preload, refresh, resolve, resolveEntry, resolveTo, setBlocker, setOptions, toLocation };
|
|
1286
|
+
export { NativeRouterError, NotFoundError, ParamsError, RedirectLoopError, SearchError, back, cancel, commit, commitReplace, create, createHref, forward, getCurrentView, getLocation, getParams, go, initHistoryStack, invalidate, listen, match, mergeMatchedParams, navigate, parseParams, parseParamsSync, parseSearch, parseSearchInput, parseSearchSync, preload, refresh, resolve, resolveEntry, resolveTo, setBlocker, setOptions, toLocation };
|
package/dist/types/errors.d.ts
CHANGED
|
@@ -20,3 +20,16 @@ export declare class SearchError extends NativeRouterError {
|
|
|
20
20
|
readonly issues: ReadonlyArray<StandardSchemaV1.Issue>;
|
|
21
21
|
constructor(search: string, issues: ReadonlyArray<StandardSchemaV1.Issue>);
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Thrown when a route {@link BaseRoute.params params schema} rejects the
|
|
25
|
+
* merged path params. Issues are formatted like {@link SearchError}'s —
|
|
26
|
+
* `path: message` pairs joined with `; `, e.g.
|
|
27
|
+
* `Invalid path params "/users/abc": id: expected a number`.
|
|
28
|
+
*/
|
|
29
|
+
export declare class ParamsError extends NativeRouterError {
|
|
30
|
+
/** The raw params object that failed validation. */
|
|
31
|
+
readonly params: Record<string, string>;
|
|
32
|
+
/** The issues reported by the schema. */
|
|
33
|
+
readonly issues: ReadonlyArray<StandardSchemaV1.Issue>;
|
|
34
|
+
constructor(params: Record<string, string>, issues: ReadonlyArray<StandardSchemaV1.Issue>);
|
|
35
|
+
}
|
package/dist/types/router.d.ts
CHANGED
|
@@ -103,7 +103,12 @@ export declare function resolveTo<R extends BaseRoute = BaseRoute, V = any>(rout
|
|
|
103
103
|
* A guard's context also carries the level's parsed
|
|
104
104
|
* {@link GuardContext.search search}: the {@link BaseRoute.search schema}
|
|
105
105
|
* output(its validation failure rejects this resolution with a
|
|
106
|
-
* `SearchError`), or the degraded input without a schema.
|
|
106
|
+
* `SearchError`), or the degraded input without a schema. Its
|
|
107
|
+
* {@link GuardContext.params params} are likewise the merged raw string
|
|
108
|
+
* map unless some level declares a {@link BaseRoute.params params
|
|
109
|
+
* schema} — the deepest schema seen so far has already upgraded them to
|
|
110
|
+
* its output(its validation failure rides the same channel with a
|
|
111
|
+
* `ParamsError`).
|
|
107
112
|
*
|
|
108
113
|
* @group Methods
|
|
109
114
|
* @category Router
|
package/dist/types/search.d.ts
CHANGED
|
@@ -43,3 +43,34 @@ export declare function parseSearch<S extends StandardSchemaV1>(schema: S, searc
|
|
|
43
43
|
* for async schemas instead
|
|
44
44
|
*/
|
|
45
45
|
export declare function parseSearchSync<S extends StandardSchemaV1>(schema: S, search: string): SearchOutputOf<S>;
|
|
46
|
+
/**
|
|
47
|
+
* Validate the merged path params of a route level with a
|
|
48
|
+
* {@link StandardSchemaV1} schema — any zod/valibot/arktype schema
|
|
49
|
+
* works, no hard dependency. Params arrive as the plain string map the
|
|
50
|
+
* matcher extracted(see `mergeMatchedParams`), so schemas can coerce
|
|
51
|
+
* (`'7'` → `7`) and normalize along the way.
|
|
52
|
+
*
|
|
53
|
+
* Async schemas(`validate` returning a promise) are awaited.
|
|
54
|
+
*
|
|
55
|
+
* @group Methods
|
|
56
|
+
* @category Route
|
|
57
|
+
* @param schema the params schema
|
|
58
|
+
* @param params the merged raw params of the level
|
|
59
|
+
* @returns the parsed(and possibly coerced) output of the schema
|
|
60
|
+
* @throws {ParamsError} when the schema reports issues
|
|
61
|
+
*/
|
|
62
|
+
export declare function parseParams<S extends StandardSchemaV1>(schema: S, params: Record<string, string>): Promise<SearchOutputOf<S>>;
|
|
63
|
+
/**
|
|
64
|
+
* Synchronous flavor of {@link parseParams}, for render-time reads and
|
|
65
|
+
* custom `resolveView` implementations.
|
|
66
|
+
*
|
|
67
|
+
* @group Methods
|
|
68
|
+
* @category Route
|
|
69
|
+
* @param schema the params schema — must validate synchronously
|
|
70
|
+
* @param params the merged raw params of the level
|
|
71
|
+
* @returns the parsed(and possibly coerced) output of the schema
|
|
72
|
+
* @throws {ParamsError} when the schema reports issues
|
|
73
|
+
* @throws when the schema validates asynchronously; use {@link parseParams}
|
|
74
|
+
* for async schemas instead
|
|
75
|
+
*/
|
|
76
|
+
export declare function parseParamsSync<S extends StandardSchemaV1>(schema: S, params: Record<string, string>): SearchOutputOf<S>;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -142,11 +142,30 @@ export type ExtractPathParams<P extends string> = P extends `${infer Head}/${inf
|
|
|
142
142
|
* Context passed to a route guard({@link BaseRoute.beforeLoad beforeLoad}).
|
|
143
143
|
* `params` are accumulated from the root level down to the level that
|
|
144
144
|
* owns the guard, so a guard only sees params of itself and its parents.
|
|
145
|
+
*
|
|
146
|
+
* Type arguments: `S` types {@link GuardContext.search search}(schema
|
|
147
|
+
* output, or the degraded input without a schema), `P` types
|
|
148
|
+
* {@link GuardContext.params params}. Both default to what a
|
|
149
|
+
* schema-less route produces — `search: unknown`, `params: the raw
|
|
150
|
+
* string map` — so plain guards keep compiling unchanged; thread a
|
|
151
|
+
* params schema's coerced output through `P` to type what the guard
|
|
152
|
+
* actually receives at runtime.
|
|
145
153
|
*/
|
|
146
|
-
export type GuardContext<R extends BaseRoute = BaseRoute, S = unknown
|
|
154
|
+
export type GuardContext<R extends BaseRoute = BaseRoute, S = unknown, P = Record<string, string>> = {
|
|
147
155
|
router: RouterInstance<R>;
|
|
148
156
|
location: Location;
|
|
149
|
-
|
|
157
|
+
/**
|
|
158
|
+
* The merged params of this level and its parents: when any level
|
|
159
|
+
* declares a {@link BaseRoute.params params schema}, the merged raw
|
|
160
|
+
* params are parsed through the deepest matching schema before the
|
|
161
|
+
* guard runs; without schemas the raw string map the matcher
|
|
162
|
+
* extracted. The loose default models the raw map; give the third
|
|
163
|
+
* type argument the schema's output(`GuardContext<R, S, {id: number}>`
|
|
164
|
+
* for a `z.coerce.number()` id) — the runtime value is the parse
|
|
165
|
+
* result, which a coercing schema makes anything but
|
|
166
|
+
* `Record<string, string>`.
|
|
167
|
+
*/
|
|
168
|
+
params: P;
|
|
150
169
|
/**
|
|
151
170
|
* The search the guard sees: the route's {@link BaseRoute.search search
|
|
152
171
|
* schema} output(parsed and validated before the guard runs), or the
|
|
@@ -180,6 +199,20 @@ export type BaseRoute<T = any> = {
|
|
|
180
199
|
* navigation error.
|
|
181
200
|
*/
|
|
182
201
|
search?: StandardSchemaV1;
|
|
202
|
+
/**
|
|
203
|
+
* Optional Standard Schema validator of the merged path params this
|
|
204
|
+
* level and its parents contribute(see `mergeMatchedParams`). The core
|
|
205
|
+
* runs it in {@link resolveEntry} after matching and before the level's
|
|
206
|
+
* `beforeLoad`, so guards and loaders see coerced params(e.g. `:id`
|
|
207
|
+
* as a number) instead of raw strings; a validation failure fails the
|
|
208
|
+
* resolve like any other navigation error(via `ParamsError`). A level
|
|
209
|
+
* with a {@link BaseRoute.redirect redirect} skips the schema — the
|
|
210
|
+
* guard never runs there, so the schema would have no consumer.
|
|
211
|
+
*
|
|
212
|
+
* Omit it and the params stay the raw `Record<string, string>` the
|
|
213
|
+
* matcher extracted — behavior is unchanged.
|
|
214
|
+
*/
|
|
215
|
+
params?: StandardSchemaV1;
|
|
183
216
|
/**
|
|
184
217
|
* Route guard invoked before the view resolves. Return a path string
|
|
185
218
|
* to redirect, or nothing(`undefined`) to continue. The guard's
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@native-router/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.1",
|
|
4
4
|
"exports": {
|
|
5
5
|
".": {
|
|
6
6
|
"types": "./dist/types/index.d.ts",
|
|
@@ -31,10 +31,11 @@
|
|
|
31
31
|
"build": "rm -rf dist && rollup -c && tsc -p tsconfig.production.json && tsc-alias -p tsconfig.production.json",
|
|
32
32
|
"commit": "lint-staged && git-cz -n",
|
|
33
33
|
"coverage": "vitest run --coverage",
|
|
34
|
-
"lint": "eslint --fix src test *.js
|
|
34
|
+
"lint": "eslint --fix src test *.js",
|
|
35
35
|
"doc:gen": "typedoc",
|
|
36
36
|
"deploy": "npm run doc:gen && gh-pages -d dist",
|
|
37
|
-
"test": "vitest run"
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"typecheck": "tsc -p tsconfig.test.json --noEmit"
|
|
38
39
|
},
|
|
39
40
|
"repository": {
|
|
40
41
|
"type": "git",
|
|
@@ -58,44 +59,46 @@
|
|
|
58
59
|
"@babel/core": "^8.0.1",
|
|
59
60
|
"@babel/preset-env": "^8.0.2",
|
|
60
61
|
"@babel/preset-typescript": "^8.0.1",
|
|
62
|
+
"@eslint/eslintrc": "^3.3.6",
|
|
61
63
|
"@rollup/plugin-babel": "^7.1.0",
|
|
62
64
|
"@rollup/plugin-commonjs": "^29.0.3",
|
|
63
65
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
64
66
|
"@rollup/plugin-replace": "^6.0.3",
|
|
65
|
-
"@types/node": "^26.
|
|
67
|
+
"@types/node": "^26.4.0",
|
|
66
68
|
"@types/sinon": "^22.0.0",
|
|
67
|
-
"@typescript-eslint/eslint-plugin": "^
|
|
68
|
-
"@typescript-eslint/parser": "^
|
|
69
|
+
"@typescript-eslint/eslint-plugin": "^8.68.0",
|
|
70
|
+
"@typescript-eslint/parser": "^8.68.0",
|
|
69
71
|
"@vitest/coverage-v8": "^4.1.11",
|
|
70
72
|
"commitizen": "^4.3.2",
|
|
71
73
|
"core-js": "^3.50.0",
|
|
72
74
|
"cross-env": "^10.1.0",
|
|
73
|
-
"eslint": "^
|
|
75
|
+
"eslint": "^10.9.1",
|
|
74
76
|
"eslint-config-airbnb": "^19.0.4",
|
|
75
|
-
"eslint-config-airbnb-typescript": "^
|
|
76
|
-
"eslint-config-prettier": "^
|
|
77
|
-
"eslint-import-resolver-typescript": "^
|
|
78
|
-
"eslint-plugin-compat": "^
|
|
79
|
-
"eslint-plugin-import": "^2.
|
|
80
|
-
"eslint-plugin-jsx-a11y": "^6.
|
|
81
|
-
"eslint-plugin-prettier": "^5.
|
|
82
|
-
"eslint-plugin-react": "^7.
|
|
83
|
-
"eslint-plugin-react-hooks": "^
|
|
77
|
+
"eslint-config-airbnb-typescript": "^18.0.0",
|
|
78
|
+
"eslint-config-prettier": "^10.1.8",
|
|
79
|
+
"eslint-import-resolver-typescript": "^4.4.5",
|
|
80
|
+
"eslint-plugin-compat": "^7.0.2",
|
|
81
|
+
"eslint-plugin-import": "^2.32.0",
|
|
82
|
+
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
83
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
84
|
+
"eslint-plugin-react": "^7.37.5",
|
|
85
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
84
86
|
"gh-pages": "^6.3.0",
|
|
87
|
+
"globals": "^17.11.0",
|
|
85
88
|
"husky": "^9.1.7",
|
|
86
89
|
"lint-staged": "^17.3.0",
|
|
87
90
|
"prettier": "^3.9.6",
|
|
88
|
-
"rollup": "^4.
|
|
91
|
+
"rollup": "^4.63.0",
|
|
89
92
|
"semantic-release": "^25.0.9",
|
|
90
93
|
"should": "^13.2.3",
|
|
91
94
|
"should-sinon": "0.0.6",
|
|
92
95
|
"sinon": "^22.1.0",
|
|
93
|
-
"terser": "^5.
|
|
96
|
+
"terser": "^5.51.0",
|
|
94
97
|
"tsc-alias": "^1.9.2",
|
|
95
98
|
"typedoc": "^0.28.20",
|
|
96
99
|
"typedoc-plugin-mark-react-functional-components": "^0.2.2",
|
|
97
100
|
"typedoc-plugin-missing-exports": "^4.1.4",
|
|
98
|
-
"typescript": "~
|
|
101
|
+
"typescript": "~6.0.3",
|
|
99
102
|
"vitest": "^4.1.11"
|
|
100
103
|
}
|
|
101
104
|
}
|