@native-router/core 1.2.0 → 1.4.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.
@@ -1,5 +1,22 @@
1
+ import type { StandardSchemaV1 } from './types';
1
2
  export declare class NativeRouterError extends Error {
2
3
  }
3
4
  export declare class NotFoundError extends NativeRouterError {
4
5
  constructor(pathname: string);
5
6
  }
7
+ export declare class RedirectLoopError extends NativeRouterError {
8
+ constructor(target?: string);
9
+ }
10
+ /**
11
+ * Thrown when a route {@link BaseRoute.search search schema} rejects the
12
+ * location search. Issues are formatted as `path: message` pairs joined
13
+ * with `; `, e.g.
14
+ * `Invalid search params "?page=abc": page: Expected a positive integer`.
15
+ */
16
+ export declare class SearchError extends NativeRouterError {
17
+ /** The raw search string that failed validation. */
18
+ readonly search: string;
19
+ /** The issues reported by the schema. */
20
+ readonly issues: ReadonlyArray<StandardSchemaV1.Issue>;
21
+ constructor(search: string, issues: ReadonlyArray<StandardSchemaV1.Issue>);
22
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './router';
2
2
  export * from './errors';
3
+ export * from './search';
3
4
  export type * from './types';
@@ -1,5 +1,14 @@
1
1
  import { History } from 'history';
2
2
  import type { Location, Matched, Options, BaseRoute, RouterInstance, ResolveView } from './types';
3
+ /**
4
+ * A location resolved through the route guards, together with the view task
5
+ * of its final target. When guards redirected, `location` is the terminal
6
+ * location and `task` resolves the view of the target route.
7
+ */
8
+ export type ResolvedEntry<V> = {
9
+ location: Location;
10
+ task: Promise<V>;
11
+ };
3
12
  /**
4
13
  * Create a router instance.
5
14
  * @group Methods
@@ -70,6 +79,51 @@ export declare function resolve<R extends BaseRoute = BaseRoute, V = any>(router
70
79
  * @returns resolve task(a promise)
71
80
  */
72
81
  export declare function resolveTo<R extends BaseRoute = BaseRoute, V = any>(router: RouterInstance<R, V>, to: string, state?: any): Promise<V>;
82
+ /**
83
+ * Resolve a location through the route guards(`redirect`/`beforeLoad`).
84
+ *
85
+ * Guards run per matched level from the shallowest to the deepest. A guard
86
+ * returning a path string(redirect) restarts the resolution at the new
87
+ * location — from the shallowest level again, so guards of shallower
88
+ * levels re-run on every hop(keep side-effectful guards idempotent) —
89
+ * carrying the original user state; at most
90
+ * {@link MAX_REDIRECTS 10} redirects are followed before a
91
+ * {@link RedirectLoopError} is thrown. An unmatched pathname keeps the
92
+ * {@link resolve resolve} behavior: the task rejects with a
93
+ * {@link NotFoundError} and is routed through `router.errorHandler`.
94
+ *
95
+ * @group Methods
96
+ * @category Router
97
+ * @param router router instance
98
+ * @param location the location to resolve; the object itself is never
99
+ * mutated — a redirect rebinds the resolution to a new location
100
+ * @returns the terminal location and its resolve task
101
+ */
102
+ export declare function resolveEntry<R extends BaseRoute = BaseRoute, V = any>(router: RouterInstance<R, V>, location: Location): Promise<ResolvedEntry<V>>;
103
+ /**
104
+ * Resolve a target through the route guards(`redirect`/`beforeLoad`) and
105
+ * cache the result at the router level, keyed by `pathname + search`.
106
+ *
107
+ * Within its TTL(`opts.ttl`, default 30s) repeated and concurrent calls
108
+ * return the very same entry promise, so concurrent callers share one
109
+ * resolution(in-flight dedup) and repeated prefetches reuse the resolved
110
+ * view task instead of re-running guards and `resolveView`. A rejected
111
+ * resolution(guard error, redirect loop) is evicted from the cache, so
112
+ * the next call retries it. Committing a navigation({@link commit} or
113
+ * {@link commitReplace}) consumes the entry and evicts its cache slot —
114
+ * a later preload re-resolves fresh state, while callers still holding
115
+ * the old entry keep their references.
116
+ *
117
+ * @group Methods
118
+ * @category Router
119
+ * @param router router instance
120
+ * @param to path string
121
+ * @param opts options; `ttl` is the cache lifetime in milliseconds
122
+ * @returns the terminal location and its resolve task
123
+ */
124
+ export declare function preload<R extends BaseRoute = BaseRoute, V = any>(router: RouterInstance<R, V>, to: string, opts?: {
125
+ ttl?: number;
126
+ }): Promise<ResolvedEntry<V>>;
73
127
  /**
74
128
  * Commit the resolve task and push history.
75
129
  * @group Methods
@@ -89,7 +143,11 @@ export declare function commit<R extends BaseRoute = BaseRoute, V = any>(router:
89
143
  */
90
144
  export declare function commitReplace<R extends BaseRoute = BaseRoute, V = any>(router: RouterInstance<R, V>, resolvePromise: Promise<V>, location: Location): Promise<void>;
91
145
  /**
92
- * Navigate to a new path.
146
+ * Navigate to a new path. Route guards(`redirect`/`beforeLoad`) run before
147
+ * the view resolves; the history entry is committed on the terminal
148
+ * location when guards redirected. The guard phase is part of the
149
+ * cancelable navigation: a superseding navigate or a `cancel()` while
150
+ * guards are still running discards this navigation.
93
151
  * @group Methods
94
152
  * @category Router
95
153
  * @param router router instance
@@ -98,7 +156,8 @@ export declare function commitReplace<R extends BaseRoute = BaseRoute, V = any>(
98
156
  */
99
157
  export declare function navigate<R extends BaseRoute = BaseRoute, V = any>(router: RouterInstance<R, V>, to: string, state?: any): Promise<void>;
100
158
  /**
101
- * Refresh the page.
159
+ * Refresh the page. Route guards run before the view resolves; a redirect
160
+ * replaces the current entry with the terminal location.
102
161
  * @group Methods
103
162
  * @category Router
104
163
  * @param router router instance
@@ -141,7 +200,7 @@ export declare function createHref<R extends BaseRoute = BaseRoute, V = any>({ b
141
200
  * @category Router
142
201
  * @param router router instance
143
202
  */
144
- export declare function cancel<R extends BaseRoute = BaseRoute, V = any>({ cancelAll, onLoadingChange }: RouterInstance<R, V>): void;
203
+ export declare function cancel<R extends BaseRoute = BaseRoute, V = any>(router: RouterInstance<R, V>): void;
145
204
  /**
146
205
  * Restore/warm up the view stack by re-resolving every reachable entry
147
206
  * of the in-memory location stack. Call it after a refresh: in-window
@@ -164,16 +223,25 @@ export declare function initHistoryStack<R extends BaseRoute = BaseRoute, V = an
164
223
  */
165
224
  export declare function listen<R extends BaseRoute = BaseRoute, V = any>(router: RouterInstance<R, V>, onViewChange: (v: V) => void): () => void;
166
225
  /**
167
- * Merge params of all matched levels. Params of deeper levels override
226
+ * Merge params of the matched levels. Params of deeper levels override
168
227
  * the same keys of shallower ones.
228
+ *
229
+ * When `end` is given, only the levels up to and including `end` are
230
+ * merged — the accumulated params a level at index `end` sees(shallow →
231
+ * current level). Omitting `end` merges every level.
169
232
  * @group Methods
170
233
  * @category Router
171
234
  * @param matched matched route levels, see {@link match}
235
+ * @param end the index of the last level to merge, defaults to the deepest
172
236
  * @returns the merged params object
173
237
  */
174
- export declare function mergeMatchedParams<R extends BaseRoute = BaseRoute>(matched: Matched<R>[]): Record<string, string>;
238
+ export declare function mergeMatchedParams<R extends BaseRoute = BaseRoute>(matched: Matched<R>[], end?: number): Record<string, string>;
175
239
  /**
176
- * Get current route params from router. Merges params of all matched levels.
240
+ * Get current route params from router. The params are re-derived by
241
+ * matching the current entry of {@link RouterInstance.locationStack} so
242
+ * they stay correct even when the view stack holds resolved views
243
+ * (e.g. React elements) instead of match results. Merges params of all
244
+ * matched levels; deeper levels override shallower ones.
177
245
  * @group Methods
178
246
  * @category Router
179
247
  * @param router router instance
@@ -0,0 +1,45 @@
1
+ import type { SearchInput, SearchOutputOf, StandardSchemaV1 } from './types';
2
+ /**
3
+ * Parse a raw search string(e.g. `?page=2&tag=a&tag=b`) into the plain
4
+ * input object consumed by {@link StandardSchemaV1 search schemas}:
5
+ * single-valued keys are strings, keys repeated in the query string are
6
+ * arrays of their values. An empty search is `{}`.
7
+ *
8
+ * This is also the degraded shape every search API falls back to when no
9
+ * schema is given.
10
+ * @group Methods
11
+ * @category Route
12
+ * @param search the raw `location.search` string, with or without `?`
13
+ * @returns the input object for schema validation
14
+ */
15
+ export declare function parseSearchInput(search: string): SearchInput;
16
+ /**
17
+ * Validate a search string with a {@link StandardSchemaV1} schema — any
18
+ * zod/valibot/arktype schema works, no hard dependency. The string is
19
+ * first degraded via {@link parseSearchInput}, then parsed by the schema,
20
+ * so schemas can coerce(`'2'` → `2`) and normalize along the way.
21
+ *
22
+ * Async schemas(`validate` returning a promise) are awaited.
23
+ *
24
+ * @group Methods
25
+ * @category Route
26
+ * @param schema the search schema
27
+ * @param search the raw `location.search` string
28
+ * @returns the parsed(and possibly coerced) output of the schema
29
+ * @throws {SearchError} when the schema reports issues
30
+ */
31
+ export declare function parseSearch<S extends StandardSchemaV1>(schema: S, search: string): Promise<SearchOutputOf<S>>;
32
+ /**
33
+ * Synchronous flavor of {@link parseSearch}, for render-time reads(see
34
+ * `useSearch` of `@native-router/react`) and route guards.
35
+ *
36
+ * @group Methods
37
+ * @category Route
38
+ * @param schema the search schema — must validate synchronously
39
+ * @param search the raw `location.search` string
40
+ * @returns the parsed(and possibly coerced) output of the schema
41
+ * @throws {SearchError} when the schema reports issues
42
+ * @throws when the schema validates asynchronously; use {@link parseSearch}
43
+ * for async schemas instead
44
+ */
45
+ export declare function parseSearchSync<S extends StandardSchemaV1>(schema: S, search: string): SearchOutputOf<S>;
@@ -22,9 +22,124 @@ export type HistoryState = {
22
22
  };
23
23
  export type WrappedLocation = Location<HistoryState>;
24
24
  export type Awaitable<T> = T | Promise<T>;
25
+ /**
26
+ * The [Standard Schema](https://standardschema.dev) interface, version 1 —
27
+ * the common validation interface implemented by zod, valibot and arktype.
28
+ *
29
+ * Inlined type-only from `@standard-schema/spec` so the core keeps zero
30
+ * extra runtime dependencies: any schema exposing `~standard` works.
31
+ * @group Types
32
+ * @category Route
33
+ */
34
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
35
+ readonly '~standard': StandardSchemaV1.Props<Input, Output>;
36
+ }
37
+ export declare namespace StandardSchemaV1 {
38
+ interface Props<Input = unknown, Output = Input> {
39
+ /** The version number of the standard. */
40
+ readonly version: 1;
41
+ /** The vendor name of the schema library. */
42
+ readonly vendor: string;
43
+ /** Validates unknown input values. */
44
+ readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
45
+ }
46
+ type Result<Output> = SuccessResult<Output> | FailureResult;
47
+ interface SuccessResult<Output> {
48
+ /** The typed output value. */
49
+ readonly value: Output;
50
+ /** The issues of the input value. */
51
+ readonly issues?: undefined;
52
+ }
53
+ interface FailureResult {
54
+ /** The issues of the input value. */
55
+ readonly issues: ReadonlyArray<Issue>;
56
+ }
57
+ interface Issue {
58
+ /** The issue message. */
59
+ readonly message: string;
60
+ /** The issue path. */
61
+ readonly path?: ReadonlyArray<PropertyKey | PathSegment>;
62
+ }
63
+ interface PathSegment {
64
+ /** The key of the path segment. */
65
+ readonly key: PropertyKey;
66
+ }
67
+ }
68
+ /**
69
+ * The plain input object a search string degrades into before schema
70
+ * validation: single-valued keys are strings, keys repeated in the query
71
+ * string are arrays of their values.
72
+ * @group Types
73
+ * @category Route
74
+ */
75
+ export type SearchInput = Record<string, string | string[]>;
76
+ /**
77
+ * Parsed output type of a {@link StandardSchemaV1 search schema}.
78
+ * @group Types
79
+ * @category Route
80
+ */
81
+ export type SearchOutputOf<S> = S extends StandardSchemaV1<any, infer Output> ? Output : never;
82
+ /**
83
+ * Params contributed by a single path segment: `:name` is required,
84
+ * `:name?` is optional, anything else(static or wildcard) contributes
85
+ * nothing.
86
+ *
87
+ * Only the segment-exact forms of the path-to-regexp 6 syntax are
88
+ * modeled. Prefix/suffix params(`/page-:id`), repetitions(`:id*`,
89
+ * `:id+`) and custom regexes(`:id(\\d+)`) are matched at runtime but
90
+ * not modeled here — they simply contribute no keys.
91
+ * @group Types
92
+ * @category Route
93
+ */
94
+ export type PathParamsOf<Seg extends string> = Seg extends `:${infer Name}?` ? {
95
+ [K in Name & string]?: string;
96
+ } : Seg extends `:${infer Name}` ? {
97
+ [K in Name & string]: string;
98
+ } : {};
99
+ /**
100
+ * Extract the params shape of a route path pattern. Splits the pattern
101
+ * into `/`-separated segments and intersects the params of each, e.g.
102
+ * `ExtractPathParams<'/users/:id/posts/:postId?'>` is
103
+ * `{id: string} & {postId?: string}`.
104
+ *
105
+ * Within the modeled path-to-regexp 6 syntax scope(see
106
+ * {@link PathParamsOf}); wildcards(`*`) and static segments are
107
+ * ignored. Distributes over unions of patterns.
108
+ * @group Types
109
+ * @category Route
110
+ */
111
+ export type ExtractPathParams<P extends string> = P extends `${infer Head}/${infer Rest}` ? PathParamsOf<Head> & ExtractPathParams<Rest> : PathParamsOf<P>;
112
+ /**
113
+ * Context passed to a route guard({@link BaseRoute.beforeLoad beforeLoad}).
114
+ * `params` are accumulated from the root level down to the level that
115
+ * owns the guard, so a guard only sees params of itself and its parents.
116
+ */
117
+ export type GuardContext<R extends BaseRoute = BaseRoute> = {
118
+ router: RouterInstance<R>;
119
+ location: Location;
120
+ params: Record<string, string>;
121
+ };
25
122
  export type BaseRoute<T = any> = {
26
123
  path?: Path;
27
124
  children?: BaseRoute<T>[];
125
+ /**
126
+ * Static redirect target. When set, navigating to this route is
127
+ * redirected to the target path before the view resolves.
128
+ */
129
+ redirect?: string;
130
+ /**
131
+ * Optional Standard Schema(zod/valibot/arktype, ...) validator of the
132
+ * route search. Frameworks parse `location.search` with it at resolve
133
+ * time(see `parseSearch`) and inject the parsed output into their data
134
+ * contexts; a validation failure fails the resolve like any other
135
+ * navigation error.
136
+ */
137
+ search?: StandardSchemaV1;
138
+ /**
139
+ * Route guard invoked before the view resolves. Return a path string
140
+ * to redirect, or nothing(`undefined`) to continue.
141
+ */
142
+ beforeLoad?(ctx: GuardContext<BaseRoute<T>>): Awaitable<string | void>;
28
143
  } & Omit<T, 'path' | 'children'>;
29
144
  export type Matched<R extends BaseRoute = BaseRoute> = {
30
145
  route: R;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@native-router/core",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "exports": {
5
5
  ".": {
6
6
  "types": "./dist/types/index.d.ts",