@orkestrel/router 0.0.12 → 0.0.14
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 +18 -15
- package/dist/src/browser/index.d.ts +83 -66
- package/dist/src/browser/index.js +61 -52
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +214 -131
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +1050 -918
- package/dist/src/core/index.d.ts +1050 -918
- package/dist/src/core/index.js +213 -131
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +39 -26
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +38 -29
- package/dist/src/server/index.d.ts +38 -29
- package/dist/src/server/index.js +39 -26
- package/dist/src/server/index.js.map +1 -1
- package/package.json +13 -14
package/README.md
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
# @orkestrel/router
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
> The typed request router: a path-matching engine (`Router`) that compiles route patterns,
|
|
4
|
+
> extracts URL-decoded params, and resolves the most specific match, with a fetch-standard,
|
|
5
|
+
> method-dimensioned dispatcher (`Dispatcher`), a headless History or hash `Navigator`, and a
|
|
6
|
+
> `node:http` adapter all composing that same engine.
|
|
7
|
+
|
|
8
|
+
Register routes on a `Router`, or hand them to a `Dispatcher` where they are
|
|
9
|
+
method-dimensioned; reach for `createNavigator` in the browser and `createListener` behind
|
|
10
|
+
`node:http`. Part of the `@orkestrel` line, built on `@orkestrel/contract` for validation,
|
|
11
|
+
`@orkestrel/emitter` for the observable surface, and `@orkestrel/abort` for cancellation.
|
|
7
12
|
|
|
8
13
|
## Install
|
|
9
14
|
|
|
@@ -13,8 +18,8 @@ npm install @orkestrel/router
|
|
|
13
18
|
|
|
14
19
|
## Requirements
|
|
15
20
|
|
|
16
|
-
- Node.js >=
|
|
17
|
-
- ESM
|
|
21
|
+
- Node.js >= 22.12.0
|
|
22
|
+
- ESM and CommonJS for the core and `./server` entries; the `./browser` entry is ESM only.
|
|
18
23
|
- Server and browser environments both supported
|
|
19
24
|
|
|
20
25
|
## Usage
|
|
@@ -38,23 +43,21 @@ const dispatcher = createDispatcher<{ readonly userId: string }>({
|
|
|
38
43
|
const response = await dispatcher.handle(new Request('http://x/users/7'), { userId: 'me' })
|
|
39
44
|
```
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
`
|
|
46
|
-
adds `createNavigator` for headless History/hash navigation; the `./server` entry adds
|
|
47
|
-
`buildRequest` / `sendResponse` / `createListener` for `node:http`.
|
|
46
|
+
Path params are inferred at the type level from the literal pattern through the
|
|
47
|
+
`PathParams` type, and the `defineRoute` function pins a `RouteInput`'s path so literal
|
|
48
|
+
inference survives across call sites. The `./browser` entry adds the `createNavigator`
|
|
49
|
+
function for headless History or hash navigation; the `./server` entry adds the
|
|
50
|
+
`buildRequest`, `sendResponse`, and `createListener` functions for `node:http`.
|
|
48
51
|
|
|
49
52
|
## Guide
|
|
50
53
|
|
|
51
54
|
For the full surface — the core `Router`, the `Dispatcher`, the browser `Navigator`, and the
|
|
52
55
|
`node:http` server adapter — see
|
|
53
|
-
[`guides/
|
|
56
|
+
[`guides/router.md`](guides/router.md).
|
|
54
57
|
|
|
55
58
|
## Package
|
|
56
59
|
|
|
57
|
-
Published as
|
|
60
|
+
Published as environment-scoped entry points per the `exports` field in
|
|
58
61
|
`package.json`: a shared core, `./browser`, and `./server`.
|
|
59
62
|
|
|
60
63
|
## License
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import { EmitterErrorHandler } from '@orkestrel/emitter';
|
|
2
|
-
import { EmitterHooks } from '@orkestrel/emitter';
|
|
3
|
-
import { EmitterInterface } from '@orkestrel/emitter';
|
|
4
|
-
import { RouteEntry } from '@orkestrel/router';
|
|
5
|
-
import { RouterInterface } from '@orkestrel/router';
|
|
6
|
-
import { RouterMatch } from '@orkestrel/router';
|
|
1
|
+
import type { EmitterErrorHandler } from '@orkestrel/emitter';
|
|
2
|
+
import type { EmitterHooks } from '@orkestrel/emitter';
|
|
3
|
+
import type { EmitterInterface } from '@orkestrel/emitter';
|
|
4
|
+
import type { RouteEntry } from '@orkestrel/router';
|
|
5
|
+
import type { RouterInterface } from '@orkestrel/router';
|
|
6
|
+
import type { RouterMatch } from '@orkestrel/router';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* Computes the canonical path key a `Navigator` registers a browser navigation
|
|
10
|
+
* route under.
|
|
10
11
|
*
|
|
11
12
|
* @remarks
|
|
12
|
-
* Projects the
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* Projects the route's path through the core engine's canonical trailing-slash
|
|
14
|
+
* identity, so `/users` and `/users/` replace one another in the Navigator's
|
|
15
|
+
* shared Router. The entry's `meta` payload is never read, so any payload type
|
|
16
|
+
* is accepted.
|
|
15
17
|
*
|
|
16
|
-
* @param entry - The
|
|
17
|
-
* @returns The
|
|
18
|
+
* @param entry - The Router entry carrying the Navigator route
|
|
19
|
+
* @returns The route's canonical path
|
|
18
20
|
*
|
|
19
21
|
* @example
|
|
20
22
|
* ```ts
|
|
21
|
-
* computeNavigationKey({ path: '/users/', meta: {
|
|
23
|
+
* computeNavigationKey({ path: '/users/', meta: {} }) // '/users'
|
|
22
24
|
* ```
|
|
23
25
|
*/
|
|
24
|
-
export declare function computeNavigationKey(entry: RouteEntry<
|
|
25
|
-
readonly path: string;
|
|
26
|
-
}>): string;
|
|
26
|
+
export declare function computeNavigationKey(entry: RouteEntry<unknown>): string;
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
30
|
-
* entity composing one core `Router<
|
|
29
|
+
* Creates a {@link NavigatorInterface} — the headless History/hash navigation
|
|
30
|
+
* entity composing one core `Router<Meta>`.
|
|
31
31
|
*
|
|
32
32
|
* @remarks
|
|
33
33
|
* Prefer this over `new Navigator(...)` at call sites that only need the
|
|
@@ -37,7 +37,7 @@ export declare function computeNavigationKey(entry: RouteEntry<{
|
|
|
37
37
|
* @param options - The `routes` to register, the `history` toggle (default
|
|
38
38
|
* `false`, hash mode), an optional `base` (history mode), an optional
|
|
39
39
|
* `fallback` path, an optional `guard` hook, opt-in link `intercept`
|
|
40
|
-
* (history mode), the `sensitive` case toggle, and the
|
|
40
|
+
* (history mode), the `sensitive` case toggle, and the Emitter pattern's
|
|
41
41
|
* `on`/`error` wiring
|
|
42
42
|
* @returns A live {@link NavigatorInterface} handle — call `start()` to begin
|
|
43
43
|
* dispatching
|
|
@@ -59,17 +59,17 @@ export declare function computeNavigationKey(entry: RouteEntry<{
|
|
|
59
59
|
export declare function createNavigator<Meta>(options: NavigatorOptions<Meta>): NavigatorInterface<Meta>;
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
*
|
|
62
|
+
* Extracts the `/`-prefixed pathname from a `location.hash` value — strips the
|
|
63
63
|
* leading `#` (keeping the route's own leading `/`) and any `?query` suffix.
|
|
64
64
|
*
|
|
65
65
|
* @remarks
|
|
66
|
-
* The grammar this package matches everywhere is `/`-prefixed
|
|
67
|
-
*
|
|
66
|
+
* The grammar this package matches everywhere is `/`-prefixed, so a hash-mode
|
|
67
|
+
* location's `'#/users/7?x'` becomes `'/users/7'`
|
|
68
68
|
* — a hash pattern is expected to start `'#/'`; anything else (an empty hash,
|
|
69
69
|
* or one that does not begin `'#/'`) yields `''` (the `Navigator` then falls
|
|
70
70
|
* back). Total — never throws.
|
|
71
71
|
*
|
|
72
|
-
* @param hash - The raw `window.location.hash` value (
|
|
72
|
+
* @param hash - The raw `window.location.hash` value (for example `'#/users/7?x'`)
|
|
73
73
|
* @returns The `/`-prefixed pathname to match, or `''` for an empty / non-`#/` hash
|
|
74
74
|
*
|
|
75
75
|
* @example
|
|
@@ -83,7 +83,7 @@ export declare function createNavigator<Meta>(options: NavigatorOptions<Meta>):
|
|
|
83
83
|
export declare function extractHashPath(hash: string): string;
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
|
-
*
|
|
86
|
+
* Finds the nearest enclosing `<a>` element a DOM event originated from, by
|
|
87
87
|
* walking its composed path — the pure lookup behind history-mode link
|
|
88
88
|
* interception.
|
|
89
89
|
*
|
|
@@ -107,10 +107,10 @@ export declare function extractHashPath(hash: string): string;
|
|
|
107
107
|
export declare function findAnchor(event: Event): HTMLAnchorElement | undefined;
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
|
-
*
|
|
111
|
-
* `Router<
|
|
110
|
+
* Represents the headless History/hash navigation entity — composes one core
|
|
111
|
+
* `Router<Meta>`, resolving the current location on `start()` and
|
|
112
112
|
* every subsequent navigation event, tracking `active`, and emitting
|
|
113
|
-
* `navigate` through the core {@link Emitter}
|
|
113
|
+
* `navigate` through the core {@link Emitter}. No `render` /
|
|
114
114
|
* `outlet` — the consumer owns rendering.
|
|
115
115
|
*
|
|
116
116
|
* @typeParam Meta - The opaque per-route payload a match carries back
|
|
@@ -120,14 +120,13 @@ export declare function findAnchor(event: Event): HTMLAnchorElement | undefined;
|
|
|
120
120
|
* `Router` machine the core `Dispatcher` composes, keyed for dedup by its
|
|
121
121
|
* {@link canonicalizePath} (last write wins, replace-in-place) — literal-
|
|
122
122
|
* over-param precedence, trailing-slash insensitivity, and
|
|
123
|
-
* `:param`/`*wildcard` extraction all come from that one engine
|
|
124
|
-
* §21).
|
|
123
|
+
* `:param`/`*wildcard` extraction all come from that one shared engine.
|
|
125
124
|
* - **Resolve pipeline.** Compute the `/`-prefixed pathname to match
|
|
126
125
|
* ({@link resolveLocationPath}) → {@link match} it → on a miss, match the
|
|
127
126
|
* `fallback` through the SAME engine → a fallback that ALSO matches nothing
|
|
128
127
|
* aborts any pending guarded navigation (a miss SUPERSEDES it, same as a
|
|
129
|
-
* newer navigation) and leaves `active` `undefined`, emitting nothing
|
|
130
|
-
*
|
|
128
|
+
* newer navigation) and leaves `active` `undefined`, emitting nothing —
|
|
129
|
+
* honest to the one-shared-engine rule: no phantom match is fabricated → the optional `guard` may
|
|
131
130
|
* veto → on a verdict, `active` is set and `navigate` emitted.
|
|
132
131
|
* - **Supersede-safe guard.** Every navigation mints an `@orkestrel/abort`
|
|
133
132
|
* handle, aborting the PREVIOUS navigation's handle first; a guard verdict
|
|
@@ -156,7 +155,7 @@ export declare function findAnchor(event: Event): HTMLAnchorElement | undefined;
|
|
|
156
155
|
declare class Navigator_2<Meta> implements NavigatorInterface<Meta> {
|
|
157
156
|
#private;
|
|
158
157
|
constructor(options: NavigatorOptions<Meta>);
|
|
159
|
-
get router(): RouterInterface<
|
|
158
|
+
get router(): RouterInterface<Meta>;
|
|
160
159
|
get emitter(): EmitterInterface<NavigatorEventMap<Meta>>;
|
|
161
160
|
get active(): RouterMatch<Meta> | undefined;
|
|
162
161
|
start(): void;
|
|
@@ -168,7 +167,7 @@ declare class Navigator_2<Meta> implements NavigatorInterface<Meta> {
|
|
|
168
167
|
export { Navigator_2 as Navigator }
|
|
169
168
|
|
|
170
169
|
/**
|
|
171
|
-
*
|
|
170
|
+
* Represents the `Navigator`'s event map — the single `navigate` signal a
|
|
172
171
|
* consumer observes.
|
|
173
172
|
*
|
|
174
173
|
* @typeParam Meta - The opaque per-route payload the resolved match carries
|
|
@@ -177,53 +176,70 @@ export { Navigator_2 as Navigator }
|
|
|
177
176
|
* `navigate` fires once per successful resolution (start, hashchange/popstate,
|
|
178
177
|
* `navigate()`, link interception) — never for a vetoed or superseded navigation
|
|
179
178
|
* ({@link NavigatorOptions.guard}), and never when a miss's fallback also
|
|
180
|
-
* misses
|
|
179
|
+
* misses — honest to the one-shared-engine rule: `active` is left `undefined`, nothing emitted.
|
|
181
180
|
*/
|
|
182
181
|
export declare type NavigatorEventMap<Meta> = {
|
|
183
182
|
readonly navigate: readonly [match: RouterMatch<Meta>];
|
|
184
183
|
};
|
|
185
184
|
|
|
186
185
|
/**
|
|
187
|
-
*
|
|
188
|
-
* interface role for the one-class-per-file `Navigator`). Composes a core
|
|
189
|
-
* `Router<
|
|
190
|
-
*
|
|
191
|
-
*
|
|
186
|
+
* Represents the headless History/hash navigation entity contract (the
|
|
187
|
+
* behavioral-interface role for the one-class-per-file `Navigator`). Composes a core
|
|
188
|
+
* `Router<Meta>`, resolves the current location on `start()` and on every subsequent
|
|
189
|
+
* navigation event, tracks `active`, and emits `navigate` through the
|
|
190
|
+
* {@link EmitterInterface}.
|
|
192
191
|
*
|
|
193
192
|
* @typeParam Meta - The opaque per-route payload a match carries back
|
|
194
193
|
*
|
|
195
194
|
* @remarks
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* - `emitter` — the AGENTS §13 observable surface for {@link NavigatorEventMap}.
|
|
199
|
-
* - `active` — the currently-resolved {@link RouterMatch}, or `undefined`
|
|
200
|
-
* before the first resolve (or when a miss's fallback also misses).
|
|
201
|
-
* - `start()` — begin listening (`hashchange` in hash mode; `popstate` +
|
|
202
|
-
* optional link interception in history mode) and resolve the current
|
|
203
|
-
* location now. Idempotent — a second call is a no-op.
|
|
204
|
-
* - `stop()` — stop listening. Idempotent.
|
|
205
|
-
* - `navigate(path)` — navigate programmatically: sets `location.hash` (hash
|
|
206
|
-
* mode) or calls `history.pushState` (history mode), then resolves. A
|
|
207
|
-
* no-op hash navigation (already the active hash) resolves directly, since
|
|
208
|
-
* no `hashchange` would otherwise fire.
|
|
209
|
-
* - `match(path)` — a PURE lookup through the underlying `Router`: no
|
|
210
|
-
* location read, no fallback, no guard, no emit.
|
|
211
|
-
* - `destroy()` — `stop()` plus tear down the `#emitter` (AGENTS §13).
|
|
195
|
+
* Rendering stays outside this contract: a consumer subscribes to `navigate` and
|
|
196
|
+
* renders whatever the resolved match names.
|
|
212
197
|
*/
|
|
213
198
|
export declare interface NavigatorInterface<Meta> {
|
|
214
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Holds the underlying registry, exposed readonly for introspection — the same
|
|
201
|
+
* object the routes were registered on.
|
|
202
|
+
*/
|
|
203
|
+
readonly router: RouterInterface<Meta>;
|
|
204
|
+
/** Holds the observable surface for {@link NavigatorEventMap}. */
|
|
215
205
|
readonly emitter: EmitterInterface<NavigatorEventMap<Meta>>;
|
|
206
|
+
/**
|
|
207
|
+
* Holds the resolved {@link RouterMatch}, or `undefined` before the first resolve
|
|
208
|
+
* and whenever a miss's fallback also misses.
|
|
209
|
+
*/
|
|
216
210
|
readonly active: RouterMatch<Meta> | undefined;
|
|
211
|
+
/**
|
|
212
|
+
* Begins listening and resolves the current location — idempotent, so a second call
|
|
213
|
+
* is a no-op.
|
|
214
|
+
*
|
|
215
|
+
* @remarks
|
|
216
|
+
* Hash mode binds `hashchange`; history mode binds `popstate` and, where `intercept`
|
|
217
|
+
* is set, same-origin link interception.
|
|
218
|
+
*/
|
|
217
219
|
start(): void;
|
|
220
|
+
/** Stops listening and aborts any pending guard — idempotent. */
|
|
218
221
|
stop(): void;
|
|
222
|
+
/**
|
|
223
|
+
* Navigates programmatically — sets `location.hash` in hash mode or calls
|
|
224
|
+
* `history.pushState` in history mode, then resolves.
|
|
225
|
+
*
|
|
226
|
+
* @remarks
|
|
227
|
+
* A hash navigation to the active hash resolves directly, because no `hashchange`
|
|
228
|
+
* would otherwise fire.
|
|
229
|
+
*/
|
|
219
230
|
navigate(path: string): void;
|
|
231
|
+
/**
|
|
232
|
+
* Looks one path up through the underlying `Router` — a pure lookup with no location
|
|
233
|
+
* read, no fallback, no guard, and no emit.
|
|
234
|
+
*/
|
|
220
235
|
match(path: string): RouterMatch<Meta> | undefined;
|
|
236
|
+
/** Stops listening and tears down the emitter. */
|
|
221
237
|
destroy(): void;
|
|
222
238
|
}
|
|
223
239
|
|
|
224
240
|
/**
|
|
225
|
-
*
|
|
226
|
-
* navigation substrate, the optional guard hook, and the
|
|
241
|
+
* Represents the options for `createNavigator` — the `routes` to dispatch between, the
|
|
242
|
+
* navigation substrate, the optional guard hook, and the Emitter pattern's
|
|
227
243
|
* wiring.
|
|
228
244
|
*
|
|
229
245
|
* @typeParam Meta - The opaque payload each route may carry
|
|
@@ -240,20 +256,21 @@ export declare interface NavigatorInterface<Meta> {
|
|
|
240
256
|
* - `fallback` — the route PATTERN to resolve when the current location
|
|
241
257
|
* matches NOTHING. Omitted ⇒ the first route's path. A `fallback` that
|
|
242
258
|
* itself matches no registered route leaves `active` `undefined` and emits
|
|
243
|
-
* nothing
|
|
259
|
+
* nothing — honest to the one-shared-engine rule: no phantom match is fabricated.
|
|
244
260
|
* - `guard` — `(to, from, signal) => boolean | Promise<boolean>`, called
|
|
245
261
|
* before a navigation commits; a `false`/rejected verdict, or one arriving
|
|
246
262
|
* after the navigation was SUPERSEDED (`signal.aborted`), is discarded —
|
|
247
263
|
* `active` stays unchanged and nothing is emitted. `signal` fires when a
|
|
248
264
|
* NEWER navigation starts (or on `stop`/`destroy`), so a slow async guard
|
|
249
265
|
* can cancel its own work off it. A throw routes to the `error` handler
|
|
250
|
-
*
|
|
266
|
+
* described later in this list and vetoes the navigation.
|
|
251
267
|
* - `intercept` — opt-in same-origin `<a>` click interception (history mode
|
|
252
268
|
* only): a plain left-click on a same-origin link with no modifier keys,
|
|
253
269
|
* no `target`, and no `download` attribute is intercepted into `navigate`.
|
|
254
270
|
* - `sensitive` — forwarded to the underlying `Router` (default `true`).
|
|
255
|
-
* - `on` — initial `NavigatorEventMap` listeners (
|
|
256
|
-
*
|
|
271
|
+
* - `on` — initial `NavigatorEventMap` listeners (the Emitter pattern's
|
|
272
|
+
* similar-surface pin).
|
|
273
|
+
* - `error` — the emitter's listener-error handler; ALSO the
|
|
257
274
|
* handler a thrown {@link guard} routes to (the Navigator's own pipeline,
|
|
258
275
|
* not a listener throw, so it is surfaced through the same channel).
|
|
259
276
|
*/
|
|
@@ -270,7 +287,7 @@ export declare interface NavigatorOptions<Meta> {
|
|
|
270
287
|
}
|
|
271
288
|
|
|
272
289
|
/**
|
|
273
|
-
*
|
|
290
|
+
* Resolves the `/`-prefixed pathname to match for the current location, in
|
|
274
291
|
* either navigation mode — the one seam `extractHashPath` (hash mode) and
|
|
275
292
|
* history-mode base-stripping share.
|
|
276
293
|
*
|
|
@@ -280,12 +297,12 @@ export declare interface NavigatorOptions<Meta> {
|
|
|
280
297
|
* `location.pathname` and strips a leading `base` prefix when one is
|
|
281
298
|
* configured: `base` itself maps to the root `'/'`; a pathname that is not
|
|
282
299
|
* under `base` is returned unchanged (a base mismatch is not this helper's
|
|
283
|
-
* concern — the `Navigator`'s match then
|
|
300
|
+
* concern — the `Navigator`'s match then misses). Total — never throws.
|
|
284
301
|
*
|
|
285
302
|
* @param location - The `hash` + `pathname` pair to resolve from (accepts a
|
|
286
303
|
* real `Location` or any object shaped the same, for pure unit testing)
|
|
287
|
-
* @param history -
|
|
288
|
-
*
|
|
304
|
+
* @param history - If `true`, the pathname is read from `location.pathname`
|
|
305
|
+
* with `base` stripped; if `false`, it is read from `location.hash`
|
|
289
306
|
* @param base - The history-mode path prefix to strip (ignored in hash mode;
|
|
290
307
|
* omit for no prefix)
|
|
291
308
|
* @returns The `/`-prefixed pathname to match
|
|
@@ -1,39 +1,41 @@
|
|
|
1
1
|
import { canonicalizePath, createRouter, joinPaths } from "../core/index.js";
|
|
2
2
|
import { createAbort } from "@orkestrel/abort";
|
|
3
3
|
import { Emitter } from "@orkestrel/emitter";
|
|
4
|
-
import { isFunction, isString } from "@orkestrel/contract";
|
|
4
|
+
import { ContractError, isFunction, isString, preview } from "@orkestrel/contract";
|
|
5
5
|
//#region src/browser/helpers.ts
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Computes the canonical path key a `Navigator` registers a browser navigation
|
|
8
|
+
* route under.
|
|
8
9
|
*
|
|
9
10
|
* @remarks
|
|
10
|
-
* Projects the
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Projects the route's path through the core engine's canonical trailing-slash
|
|
12
|
+
* identity, so `/users` and `/users/` replace one another in the Navigator's
|
|
13
|
+
* shared Router. The entry's `meta` payload is never read, so any payload type
|
|
14
|
+
* is accepted.
|
|
13
15
|
*
|
|
14
|
-
* @param entry - The
|
|
15
|
-
* @returns The
|
|
16
|
+
* @param entry - The Router entry carrying the Navigator route
|
|
17
|
+
* @returns The route's canonical path
|
|
16
18
|
*
|
|
17
19
|
* @example
|
|
18
20
|
* ```ts
|
|
19
|
-
* computeNavigationKey({ path: '/users/', meta: {
|
|
21
|
+
* computeNavigationKey({ path: '/users/', meta: {} }) // '/users'
|
|
20
22
|
* ```
|
|
21
23
|
*/
|
|
22
24
|
function computeNavigationKey(entry) {
|
|
23
|
-
return canonicalizePath(entry.
|
|
25
|
+
return canonicalizePath(entry.path);
|
|
24
26
|
}
|
|
25
27
|
/**
|
|
26
|
-
*
|
|
28
|
+
* Extracts the `/`-prefixed pathname from a `location.hash` value — strips the
|
|
27
29
|
* leading `#` (keeping the route's own leading `/`) and any `?query` suffix.
|
|
28
30
|
*
|
|
29
31
|
* @remarks
|
|
30
|
-
* The grammar this package matches everywhere is `/`-prefixed
|
|
31
|
-
*
|
|
32
|
+
* The grammar this package matches everywhere is `/`-prefixed, so a hash-mode
|
|
33
|
+
* location's `'#/users/7?x'` becomes `'/users/7'`
|
|
32
34
|
* — a hash pattern is expected to start `'#/'`; anything else (an empty hash,
|
|
33
35
|
* or one that does not begin `'#/'`) yields `''` (the `Navigator` then falls
|
|
34
36
|
* back). Total — never throws.
|
|
35
37
|
*
|
|
36
|
-
* @param hash - The raw `window.location.hash` value (
|
|
38
|
+
* @param hash - The raw `window.location.hash` value (for example `'#/users/7?x'`)
|
|
37
39
|
* @returns The `/`-prefixed pathname to match, or `''` for an empty / non-`#/` hash
|
|
38
40
|
*
|
|
39
41
|
* @example
|
|
@@ -51,7 +53,7 @@ function extractHashPath(hash) {
|
|
|
51
53
|
return queryIndex === -1 ? withoutHash : withoutHash.slice(0, queryIndex);
|
|
52
54
|
}
|
|
53
55
|
/**
|
|
54
|
-
*
|
|
56
|
+
* Resolves the `/`-prefixed pathname to match for the current location, in
|
|
55
57
|
* either navigation mode — the one seam `extractHashPath` (hash mode) and
|
|
56
58
|
* history-mode base-stripping share.
|
|
57
59
|
*
|
|
@@ -61,12 +63,12 @@ function extractHashPath(hash) {
|
|
|
61
63
|
* `location.pathname` and strips a leading `base` prefix when one is
|
|
62
64
|
* configured: `base` itself maps to the root `'/'`; a pathname that is not
|
|
63
65
|
* under `base` is returned unchanged (a base mismatch is not this helper's
|
|
64
|
-
* concern — the `Navigator`'s match then
|
|
66
|
+
* concern — the `Navigator`'s match then misses). Total — never throws.
|
|
65
67
|
*
|
|
66
68
|
* @param location - The `hash` + `pathname` pair to resolve from (accepts a
|
|
67
69
|
* real `Location` or any object shaped the same, for pure unit testing)
|
|
68
|
-
* @param history -
|
|
69
|
-
*
|
|
70
|
+
* @param history - If `true`, the pathname is read from `location.pathname`
|
|
71
|
+
* with `base` stripped; if `false`, it is read from `location.hash`
|
|
70
72
|
* @param base - The history-mode path prefix to strip (ignored in hash mode;
|
|
71
73
|
* omit for no prefix)
|
|
72
74
|
* @returns The `/`-prefixed pathname to match
|
|
@@ -89,7 +91,7 @@ function resolveLocationPath(location, history, base) {
|
|
|
89
91
|
return pathname;
|
|
90
92
|
}
|
|
91
93
|
/**
|
|
92
|
-
*
|
|
94
|
+
* Finds the nearest enclosing `<a>` element a DOM event originated from, by
|
|
93
95
|
* walking its composed path — the pure lookup behind history-mode link
|
|
94
96
|
* interception.
|
|
95
97
|
*
|
|
@@ -116,10 +118,10 @@ function findAnchor(event) {
|
|
|
116
118
|
//#endregion
|
|
117
119
|
//#region src/browser/Navigator.ts
|
|
118
120
|
/**
|
|
119
|
-
*
|
|
120
|
-
* `Router<
|
|
121
|
+
* Represents the headless History/hash navigation entity — composes one core
|
|
122
|
+
* `Router<Meta>`, resolving the current location on `start()` and
|
|
121
123
|
* every subsequent navigation event, tracking `active`, and emitting
|
|
122
|
-
* `navigate` through the core {@link Emitter}
|
|
124
|
+
* `navigate` through the core {@link Emitter}. No `render` /
|
|
123
125
|
* `outlet` — the consumer owns rendering.
|
|
124
126
|
*
|
|
125
127
|
* @typeParam Meta - The opaque per-route payload a match carries back
|
|
@@ -129,14 +131,13 @@ function findAnchor(event) {
|
|
|
129
131
|
* `Router` machine the core `Dispatcher` composes, keyed for dedup by its
|
|
130
132
|
* {@link canonicalizePath} (last write wins, replace-in-place) — literal-
|
|
131
133
|
* over-param precedence, trailing-slash insensitivity, and
|
|
132
|
-
* `:param`/`*wildcard` extraction all come from that one engine
|
|
133
|
-
* §21).
|
|
134
|
+
* `:param`/`*wildcard` extraction all come from that one shared engine.
|
|
134
135
|
* - **Resolve pipeline.** Compute the `/`-prefixed pathname to match
|
|
135
136
|
* ({@link resolveLocationPath}) → {@link match} it → on a miss, match the
|
|
136
137
|
* `fallback` through the SAME engine → a fallback that ALSO matches nothing
|
|
137
138
|
* aborts any pending guarded navigation (a miss SUPERSEDES it, same as a
|
|
138
|
-
* newer navigation) and leaves `active` `undefined`, emitting nothing
|
|
139
|
-
*
|
|
139
|
+
* newer navigation) and leaves `active` `undefined`, emitting nothing —
|
|
140
|
+
* honest to the one-shared-engine rule: no phantom match is fabricated → the optional `guard` may
|
|
140
141
|
* veto → on a verdict, `active` is set and `navigate` emitted.
|
|
141
142
|
* - **Supersede-safe guard.** Every navigation mints an `@orkestrel/abort`
|
|
142
143
|
* handle, aborting the PREVIOUS navigation's handle first; a guard verdict
|
|
@@ -171,16 +172,36 @@ var Navigator = class {
|
|
|
171
172
|
#guard;
|
|
172
173
|
#error;
|
|
173
174
|
#intercept;
|
|
174
|
-
#
|
|
175
|
-
#popListener;
|
|
175
|
+
#listener;
|
|
176
176
|
#clickListener;
|
|
177
177
|
#active;
|
|
178
178
|
#started = false;
|
|
179
179
|
#current;
|
|
180
180
|
constructor(options) {
|
|
181
|
-
if (options.guard !== void 0 && !isFunction(options.guard)) throw new
|
|
182
|
-
|
|
183
|
-
|
|
181
|
+
if (options.guard !== void 0 && !isFunction(options.guard)) throw new ContractError("a navigator guard must be a function when defined", {
|
|
182
|
+
code: "literal",
|
|
183
|
+
context: {
|
|
184
|
+
path: ["options", "guard"],
|
|
185
|
+
limit: "function or undefined",
|
|
186
|
+
received: preview(options.guard)
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
if (options.fallback !== void 0 && !isString(options.fallback)) throw new ContractError("a navigator fallback must be a string when defined", {
|
|
190
|
+
code: "literal",
|
|
191
|
+
context: {
|
|
192
|
+
path: ["options", "fallback"],
|
|
193
|
+
limit: "string or undefined",
|
|
194
|
+
received: preview(options.fallback)
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
if (options.base !== void 0 && !isString(options.base)) throw new ContractError("a navigator base must be a string when defined", {
|
|
198
|
+
code: "literal",
|
|
199
|
+
context: {
|
|
200
|
+
path: ["options", "base"],
|
|
201
|
+
limit: "string or undefined",
|
|
202
|
+
received: preview(options.base)
|
|
203
|
+
}
|
|
204
|
+
});
|
|
184
205
|
this.#history = options.history ?? false;
|
|
185
206
|
this.#base = options.base;
|
|
186
207
|
this.#intercept = options.intercept ?? false;
|
|
@@ -191,17 +212,12 @@ var Navigator = class {
|
|
|
191
212
|
...options.error === void 0 ? {} : { error: options.error }
|
|
192
213
|
});
|
|
193
214
|
this.#router = createRouter({
|
|
194
|
-
entries: options.routes
|
|
195
|
-
path: route.path,
|
|
196
|
-
meta: route,
|
|
197
|
-
...route.name === void 0 ? {} : { name: route.name }
|
|
198
|
-
})),
|
|
215
|
+
entries: options.routes,
|
|
199
216
|
...options.sensitive === void 0 ? {} : { sensitive: options.sensitive },
|
|
200
217
|
key: computeNavigationKey
|
|
201
218
|
});
|
|
202
219
|
this.#fallback = options.fallback ?? options.routes[0]?.path;
|
|
203
|
-
this.#
|
|
204
|
-
this.#popListener = this.#resolve.bind(this);
|
|
220
|
+
this.#listener = this.#resolve.bind(this);
|
|
205
221
|
this.#clickListener = this.#intercepted.bind(this);
|
|
206
222
|
}
|
|
207
223
|
get router() {
|
|
@@ -216,9 +232,9 @@ var Navigator = class {
|
|
|
216
232
|
start() {
|
|
217
233
|
if (this.#started) return;
|
|
218
234
|
this.#started = true;
|
|
219
|
-
if (!this.#history) window.addEventListener("hashchange", this.#
|
|
235
|
+
if (!this.#history) window.addEventListener("hashchange", this.#listener);
|
|
220
236
|
else {
|
|
221
|
-
window.addEventListener("popstate", this.#
|
|
237
|
+
window.addEventListener("popstate", this.#listener);
|
|
222
238
|
if (this.#intercept) document.addEventListener("click", this.#clickListener);
|
|
223
239
|
}
|
|
224
240
|
this.#resolve();
|
|
@@ -226,9 +242,9 @@ var Navigator = class {
|
|
|
226
242
|
stop() {
|
|
227
243
|
if (!this.#started) return;
|
|
228
244
|
this.#started = false;
|
|
229
|
-
if (!this.#history) window.removeEventListener("hashchange", this.#
|
|
245
|
+
if (!this.#history) window.removeEventListener("hashchange", this.#listener);
|
|
230
246
|
else {
|
|
231
|
-
window.removeEventListener("popstate", this.#
|
|
247
|
+
window.removeEventListener("popstate", this.#listener);
|
|
232
248
|
if (this.#intercept) document.removeEventListener("click", this.#clickListener);
|
|
233
249
|
}
|
|
234
250
|
this.#current?.abort();
|
|
@@ -245,14 +261,7 @@ var Navigator = class {
|
|
|
245
261
|
this.#resolve();
|
|
246
262
|
}
|
|
247
263
|
match(path) {
|
|
248
|
-
|
|
249
|
-
if (hit === void 0) return void 0;
|
|
250
|
-
return {
|
|
251
|
-
path: hit.path,
|
|
252
|
-
params: hit.params,
|
|
253
|
-
meta: hit.meta.meta,
|
|
254
|
-
...hit.meta.name === void 0 ? {} : { name: hit.meta.name }
|
|
255
|
-
};
|
|
264
|
+
return this.#router.match(path);
|
|
256
265
|
}
|
|
257
266
|
destroy() {
|
|
258
267
|
this.stop();
|
|
@@ -327,8 +336,8 @@ var Navigator = class {
|
|
|
327
336
|
//#endregion
|
|
328
337
|
//#region src/browser/factories.ts
|
|
329
338
|
/**
|
|
330
|
-
*
|
|
331
|
-
* entity composing one core `Router<
|
|
339
|
+
* Creates a {@link NavigatorInterface} — the headless History/hash navigation
|
|
340
|
+
* entity composing one core `Router<Meta>`.
|
|
332
341
|
*
|
|
333
342
|
* @remarks
|
|
334
343
|
* Prefer this over `new Navigator(...)` at call sites that only need the
|
|
@@ -338,7 +347,7 @@ var Navigator = class {
|
|
|
338
347
|
* @param options - The `routes` to register, the `history` toggle (default
|
|
339
348
|
* `false`, hash mode), an optional `base` (history mode), an optional
|
|
340
349
|
* `fallback` path, an optional `guard` hook, opt-in link `intercept`
|
|
341
|
-
* (history mode), the `sensitive` case toggle, and the
|
|
350
|
+
* (history mode), the `sensitive` case toggle, and the Emitter pattern's
|
|
342
351
|
* `on`/`error` wiring
|
|
343
352
|
* @returns A live {@link NavigatorInterface} handle — call `start()` to begin
|
|
344
353
|
* dispatching
|