@orkestrel/router 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.
- package/LICENSE +21 -0
- package/README.md +33 -0
- package/dist/src/browser/Navigator.d.ts +62 -0
- package/dist/src/browser/factories.d.ts +33 -0
- package/dist/src/browser/helpers.d.ts +76 -0
- package/dist/src/browser/index.d.ts +4 -0
- package/dist/src/browser/index.js +643 -0
- package/dist/src/browser/index.js.map +1 -0
- package/dist/src/browser/types.d.ts +101 -0
- package/dist/src/core/DispatchGroup.d.ts +32 -0
- package/dist/src/core/Dispatcher.d.ts +51 -0
- package/dist/src/core/Group.d.ts +30 -0
- package/dist/src/core/Router.d.ts +42 -0
- package/dist/src/core/constants.d.ts +60 -0
- package/dist/src/core/factories.d.ts +53 -0
- package/dist/src/core/helpers.d.ts +274 -0
- package/dist/src/core/index.d.ts +8 -0
- package/dist/src/core/index.js +1014 -0
- package/dist/src/core/index.js.map +1 -0
- package/dist/src/core/types.d.ts +445 -0
- package/dist/src/server/helpers.d.ts +127 -0
- package/dist/src/server/index.cjs +417 -0
- package/dist/src/server/index.cjs.map +1 -0
- package/dist/src/server/index.d.ts +2 -0
- package/dist/src/server/types.d.ts +31 -0
- package/package.json +93 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Orkestrel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @orkestrel/router
|
|
2
|
+
|
|
3
|
+
A typed request router for the `@orkestrel` line — the first `@orkestrel`
|
|
4
|
+
package to ship both server and browser environments alongside its shared
|
|
5
|
+
core. Built to sit beside `@orkestrel/contract` (validation) and
|
|
6
|
+
`@orkestrel/emitter` (observable lifecycle), reusing both as it takes shape.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install @orkestrel/router
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
- Node.js >= 24
|
|
17
|
+
- ESM-only (no CommonJS build)
|
|
18
|
+
- Server and browser environments both supported
|
|
19
|
+
|
|
20
|
+
## Status
|
|
21
|
+
|
|
22
|
+
The public API is under design and not yet implemented — this package
|
|
23
|
+
currently ships no runtime code. This README will gain an install snippet,
|
|
24
|
+
usage examples, and a guide link once the design lands.
|
|
25
|
+
|
|
26
|
+
## Package
|
|
27
|
+
|
|
28
|
+
Published as three environment-scoped entry points per the `exports` field in
|
|
29
|
+
`package.json`: a shared core, `./browser`, and `./server`.
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
MIT © [Orkestrel](https://github.com/orkestrel) — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { NavigatorEventMap, NavigatorInterface, NavigatorOptions } from './types.js';
|
|
2
|
+
import type { EmitterInterface } from '@orkestrel/emitter';
|
|
3
|
+
import type { RouteEntry, RouterInterface, RouterMatch } from '@src/core';
|
|
4
|
+
/**
|
|
5
|
+
* The headless History/hash navigation entity — composes one core
|
|
6
|
+
* `Router<RouteEntry<Meta>>`, resolving the current location on `start()` and
|
|
7
|
+
* every subsequent navigation event, tracking `active`, and emitting
|
|
8
|
+
* `navigate` through the core {@link Emitter} (AGENTS §13). No `render` /
|
|
9
|
+
* `outlet` — the consumer owns rendering.
|
|
10
|
+
*
|
|
11
|
+
* @typeParam Meta - The opaque per-route payload a match carries back
|
|
12
|
+
*
|
|
13
|
+
* @remarks
|
|
14
|
+
* - **One shared engine.** Each `route.path` is registered on the SAME
|
|
15
|
+
* `Router` machine the core `Dispatcher` composes, keyed for dedup by its
|
|
16
|
+
* {@link canonicalizePath} (last write wins, replace-in-place) — literal-
|
|
17
|
+
* over-param precedence, trailing-slash insensitivity, and
|
|
18
|
+
* `:param`/`*wildcard` extraction all come from that one engine (AGENTS
|
|
19
|
+
* §21).
|
|
20
|
+
* - **Resolve pipeline.** Compute the `/`-prefixed pathname to match
|
|
21
|
+
* ({@link resolveLocationPath}) → {@link match} it → on a miss, match the
|
|
22
|
+
* `fallback` through the SAME engine → a fallback that ALSO matches nothing
|
|
23
|
+
* aborts any pending guarded navigation (a miss SUPERSEDES it, same as a
|
|
24
|
+
* newer navigation) and leaves `active` `undefined`, emitting nothing
|
|
25
|
+
* (§21-honest: no phantom match is fabricated) → the optional `guard` may
|
|
26
|
+
* veto → on a verdict, `active` is set and `navigate` emitted.
|
|
27
|
+
* - **Supersede-safe guard.** Every navigation mints an `@orkestrel/abort`
|
|
28
|
+
* handle, aborting the PREVIOUS navigation's handle first; a guard verdict
|
|
29
|
+
* that resolves after its navigation was superseded (`signal.aborted`) is
|
|
30
|
+
* discarded, same as a `false`/rejected verdict. A guard throw routes to
|
|
31
|
+
* the `error` handler and vetoes. `stop()`/`destroy()` also abort the
|
|
32
|
+
* pending handle.
|
|
33
|
+
* - **Hash vs history mode.** Hash mode (`history: false`, the default) binds
|
|
34
|
+
* `hashchange`; history mode (`history: true`) binds `popstate` and, when
|
|
35
|
+
* `intercept` is set, same-origin `<a>` click interception (a plain
|
|
36
|
+
* left-click with no modifier keys, `target`, or `download` attribute).
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* const navigator = new Navigator<{ readonly title: string }>({
|
|
41
|
+
* routes: [
|
|
42
|
+
* { path: '/users/:id', meta: { title: 'User' } },
|
|
43
|
+
* { path: '/tokens', meta: { title: 'Tokens' } },
|
|
44
|
+
* ],
|
|
45
|
+
* })
|
|
46
|
+
* navigator.emitter.on('navigate', (match) => (document.title = match.meta.title))
|
|
47
|
+
* navigator.start() // resolves the current hash now, and on every hashchange
|
|
48
|
+
* navigator.navigate('/tokens')
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare class Navigator<Meta> implements NavigatorInterface<Meta> {
|
|
52
|
+
#private;
|
|
53
|
+
constructor(options: NavigatorOptions<Meta>);
|
|
54
|
+
get router(): RouterInterface<RouteEntry<Meta>>;
|
|
55
|
+
get emitter(): EmitterInterface<NavigatorEventMap<Meta>>;
|
|
56
|
+
get active(): RouterMatch<Meta> | undefined;
|
|
57
|
+
start(): void;
|
|
58
|
+
stop(): void;
|
|
59
|
+
navigate(path: string): void;
|
|
60
|
+
match(path: string): RouterMatch<Meta> | undefined;
|
|
61
|
+
destroy(): void;
|
|
62
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { NavigatorInterface, NavigatorOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create a {@link NavigatorInterface} — the headless History/hash navigation
|
|
4
|
+
* entity composing one core `Router<RouteEntry<Meta>>`.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* Prefer this over `new Navigator(...)` at call sites that only need the
|
|
8
|
+
* interface.
|
|
9
|
+
*
|
|
10
|
+
* @typeParam Meta - The opaque per-route payload a match carries back
|
|
11
|
+
* @param options - The `routes` to register, the `history` toggle (default
|
|
12
|
+
* `false`, hash mode), an optional `base` (history mode), an optional
|
|
13
|
+
* `fallback` path, an optional `guard` hook, opt-in link `intercept`
|
|
14
|
+
* (history mode), the `sensitive` case toggle, and the AGENTS §13 emitter
|
|
15
|
+
* `on`/`error` wiring
|
|
16
|
+
* @returns A live {@link NavigatorInterface} handle — call `start()` to begin
|
|
17
|
+
* dispatching
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* import { createNavigator } from '@src/browser'
|
|
22
|
+
*
|
|
23
|
+
* const navigator = createNavigator({
|
|
24
|
+
* routes: [
|
|
25
|
+
* { path: '/users/:id', meta: { title: 'User' } },
|
|
26
|
+
* { path: '/tokens', meta: { title: 'Tokens' } },
|
|
27
|
+
* ],
|
|
28
|
+
* on: { navigate: (match) => (document.title = match.meta.title) },
|
|
29
|
+
* })
|
|
30
|
+
* navigator.start()
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function createNavigator<Meta>(options: NavigatorOptions<Meta>): NavigatorInterface<Meta>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract the `/`-prefixed pathname from a `location.hash` value — strip the
|
|
3
|
+
* leading `#` (keeping the route's own leading `/`) and any `?query` suffix.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* The grammar this package matches everywhere is `/`-prefixed (§4 path
|
|
7
|
+
* grammar), so a hash-mode location's `'#/users/7?x'` becomes `'/users/7'`
|
|
8
|
+
* — a hash pattern is expected to start `'#/'`; anything else (an empty hash,
|
|
9
|
+
* or one that does not begin `'#/'`) yields `''` (the `Navigator` then falls
|
|
10
|
+
* back). Total — never throws.
|
|
11
|
+
*
|
|
12
|
+
* @param hash - The raw `window.location.hash` value (e.g. `'#/users/7?x'`)
|
|
13
|
+
* @returns The `/`-prefixed pathname to match, or `''` for an empty / non-`#/` hash
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* extractHashPath('#/users/7?x') // '/users/7'
|
|
18
|
+
* extractHashPath('#/tokens') // '/tokens'
|
|
19
|
+
* extractHashPath('') // '' — the Navigator falls back
|
|
20
|
+
* extractHashPath('#other') // '' — not a `#/` route hash
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function extractHashPath(hash: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the `/`-prefixed pathname to match for the CURRENT location, in
|
|
26
|
+
* either navigation mode — the one seam `extractHashPath` (hash mode) and
|
|
27
|
+
* history-mode base-stripping share.
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* Hash mode (`history: false`) reads `location.hash` through
|
|
31
|
+
* {@link extractHashPath}. History mode (`history: true`) reads
|
|
32
|
+
* `location.pathname` and strips a leading `base` prefix when one is
|
|
33
|
+
* configured: `base` itself maps to the root `'/'`; a pathname that is not
|
|
34
|
+
* under `base` is returned unchanged (a base mismatch is not this helper's
|
|
35
|
+
* concern — the `Navigator`'s match then simply misses). Total — never throws.
|
|
36
|
+
*
|
|
37
|
+
* @param location - The `hash` + `pathname` pair to resolve from (accepts a
|
|
38
|
+
* real `Location` or any object shaped the same, for pure unit testing)
|
|
39
|
+
* @param history - The navigation substrate: `false` for hash mode, `true`
|
|
40
|
+
* for history mode
|
|
41
|
+
* @param base - The history-mode path prefix to strip (ignored in hash mode;
|
|
42
|
+
* omit for no prefix)
|
|
43
|
+
* @returns The `/`-prefixed pathname to match
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* resolveLocationPath({ hash: '#/users/7', pathname: '/' }, false) // '/users/7'
|
|
48
|
+
* resolveLocationPath({ hash: '', pathname: '/app/users/7' }, true, '/app') // '/users/7'
|
|
49
|
+
* resolveLocationPath({ hash: '', pathname: '/app' }, true, '/app') // '/'
|
|
50
|
+
* resolveLocationPath({ hash: '', pathname: '/other/users' }, true, '/app') // '/other/users'
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveLocationPath(location: Pick<Location, 'hash' | 'pathname'>, history: boolean, base?: string): string;
|
|
54
|
+
/**
|
|
55
|
+
* Find the nearest enclosing `<a>` element a DOM event originated from, by
|
|
56
|
+
* walking its composed path — the pure lookup behind history-mode link
|
|
57
|
+
* interception.
|
|
58
|
+
*
|
|
59
|
+
* @remarks
|
|
60
|
+
* Uses `event.composedPath()` (not `event.target`) so a click on a styled
|
|
61
|
+
* child INSIDE an anchor (an icon, a span) still resolves to the anchor.
|
|
62
|
+
* Total — never throws; returns `undefined` when no anchor is found on the
|
|
63
|
+
* path.
|
|
64
|
+
*
|
|
65
|
+
* @param event - The DOM event to search (typically a `click`)
|
|
66
|
+
* @returns The nearest enclosing `HTMLAnchorElement`, or `undefined`
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* document.addEventListener('click', (event) => {
|
|
71
|
+
* const anchor = findAnchor(event)
|
|
72
|
+
* if (anchor !== undefined) console.log(anchor.href)
|
|
73
|
+
* })
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export declare function findAnchor(event: Event): HTMLAnchorElement | undefined;
|