@k8ordo/router 0.1.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.
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/dist/boundary.d.mts +24 -0
- package/dist/boundary.mjs +44 -0
- package/dist/define-routes.d.mts +67 -0
- package/dist/define-routes.mjs +84 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.mjs +8 -0
- package/dist/links.d.mts +31 -0
- package/dist/links.mjs +27 -0
- package/dist/location.d.mts +31 -0
- package/dist/location.mjs +51 -0
- package/dist/match.d.mts +26 -0
- package/dist/match.mjs +35 -0
- package/dist/navigation.d.mts +69 -0
- package/dist/navigation.mjs +123 -0
- package/dist/paths.d.mts +67 -0
- package/dist/paths.mjs +26 -0
- package/dist/register.d.mts +46 -0
- package/dist/register.mjs +1 -0
- package/dist/router.d.mts +32 -0
- package/dist/router.mjs +89 -0
- package/docs/GUIDE.md +372 -0
- package/docs/llms.txt +54 -0
- package/package.json +76 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Koki Sakano
|
|
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,113 @@
|
|
|
1
|
+
# @k8ordo/router
|
|
2
|
+
|
|
3
|
+
The URL's pathname axis, owned. One route table is the application's pathname
|
|
4
|
+
schema, and from it come the types, the matching, the links, and the
|
|
5
|
+
navigation — over the Navigation API and URLPattern. Search params and
|
|
6
|
+
history-entry state are not its business; those belong to `@k8ordo/state`,
|
|
7
|
+
and the division is the URL's own `?`.
|
|
8
|
+
|
|
9
|
+
Like every [k8ordo](https://ordo.k8o.me) package it assumes React 19 and Server
|
|
10
|
+
Components, uses only what has reached Baseline newly available, and ships no
|
|
11
|
+
polyfills or legacy fallbacks.
|
|
12
|
+
|
|
13
|
+
- **Documentation**: https://ordo.k8o.me/router
|
|
14
|
+
- **Design guide**: [docs/GUIDE.md](docs/GUIDE.md) — shipped inside this package
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @k8ordo/router
|
|
20
|
+
# or
|
|
21
|
+
pnpm add @k8ordo/router
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Under `@k8ordo/static` or `@k8ordo/server` this package is what the framework
|
|
25
|
+
builds on: the table is generated from `src/routes/`, and what an application
|
|
26
|
+
uses of it is `href`, `usePathname` and `useMatch`. On its own it is the whole
|
|
27
|
+
router for an application that renders in the browser.
|
|
28
|
+
|
|
29
|
+
## Peer Dependencies
|
|
30
|
+
|
|
31
|
+
| Package | Version | Needed for |
|
|
32
|
+
| -------------- | -------- | ----------------------------- |
|
|
33
|
+
| `react` | ≥19.2.6 | `<Router>` and the hooks |
|
|
34
|
+
| `typescript` | ≥7.0.2 | the shipped type declarations |
|
|
35
|
+
| `@types/react` | ≥19.2.18 | the shipped type declarations |
|
|
36
|
+
|
|
37
|
+
No runtime dependencies. The Navigation API and URLPattern are the platform's.
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// routes.ts — the pathname schema, in one place
|
|
43
|
+
import { defineRoutes } from '@k8ordo/router';
|
|
44
|
+
|
|
45
|
+
export const routes = defineRoutes({
|
|
46
|
+
'/': Home,
|
|
47
|
+
'/products': {
|
|
48
|
+
layout: Shell,
|
|
49
|
+
error: ProductsError, // shown inside Shell when what is below throws
|
|
50
|
+
children: { '/': ProductList, '/:id': ProductPage },
|
|
51
|
+
},
|
|
52
|
+
'/*': NotFound,
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
// app.tsx — mounted once; layouts render what they wrap through <Outlet />
|
|
58
|
+
import { Outlet, Router } from '@k8ordo/router';
|
|
59
|
+
|
|
60
|
+
export const App = () => <Router routes={routes} />;
|
|
61
|
+
export const Shell = () => (
|
|
62
|
+
<section>
|
|
63
|
+
<nav>…</nav>
|
|
64
|
+
<Outlet />
|
|
65
|
+
</section>
|
|
66
|
+
);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
// anywhere — from the pattern string alone, never importing the table
|
|
71
|
+
import { href, navigateTo, useMatch, useParams } from '@k8ordo/router';
|
|
72
|
+
|
|
73
|
+
<a href={href('/products/:id', { id })}>…</a>; // a plain <a> is a client navigation
|
|
74
|
+
navigateTo('/products'); // push; the back button undoes it
|
|
75
|
+
const { id } = useParams('/products/:id'); // typed from the pattern
|
|
76
|
+
const inProducts = useMatch('/products/*'); // which section is showing
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Augment `Register` once (`interface Register { routes: typeof routes }`) and
|
|
80
|
+
every pattern is checked against the table. The [design guide](docs/GUIDE.md)
|
|
81
|
+
covers the grammar (params, wildcards, route groups, declaration order), what
|
|
82
|
+
navigation guarantees (`finished` after the tree is on screen, scroll to top
|
|
83
|
+
or fragment on a new page, superseded navigations abort), error boundaries in
|
|
84
|
+
the table, and how the framework reuses the navigation half.
|
|
85
|
+
|
|
86
|
+
## AI Agent Documentation
|
|
87
|
+
|
|
88
|
+
The docs ship **inside the package**, so an agent always reads the exact
|
|
89
|
+
version you installed — there is no snapshot to copy or re-sync on upgrade.
|
|
90
|
+
|
|
91
|
+
Point your agent at them once by pasting this into your project's `CLAUDE.md` /
|
|
92
|
+
`AGENTS.md`:
|
|
93
|
+
|
|
94
|
+
```markdown
|
|
95
|
+
Use `@k8ordo/router` for routing. Before touching routes or links, read
|
|
96
|
+
`node_modules/@k8ordo/router/docs/GUIDE.md`. The route table is the one
|
|
97
|
+
place the pathname space is declared; pages build links with
|
|
98
|
+
`href(pattern, params)` from the pattern string and never import the table.
|
|
99
|
+
There is no `<Link>` — a plain `<a>` is a client navigation. Search params
|
|
100
|
+
are `@k8ordo/state`'s, never the router's.
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
What each surface gives an agent:
|
|
104
|
+
|
|
105
|
+
| Surface | Where |
|
|
106
|
+
| -------------------------- | ---------------------------------------------- |
|
|
107
|
+
| Design guide (entry point) | `node_modules/@k8ordo/router/docs/GUIDE.md` |
|
|
108
|
+
| Docs index for LLMs | `docs/llms.txt` · https://ordo.k8o.me/llms.txt |
|
|
109
|
+
| Markdown twin on the web | https://ordo.k8o.me/router/docs/GUIDE.md |
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT License - see [LICENSE](https://github.com/k35o/k8ordo/blob/main/LICENSE) for details.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ComponentType, ReactNode } from "react";
|
|
2
|
+
//#region src/boundary.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* What a route table's `error` component receives: what was thrown, and a
|
|
5
|
+
* way to try the subtree again in place.
|
|
6
|
+
*/
|
|
7
|
+
type ErrorProps = {
|
|
8
|
+
readonly error: unknown;
|
|
9
|
+
readonly reset: () => void;
|
|
10
|
+
};
|
|
11
|
+
type ErrorComponent = ComponentType<ErrorProps>;
|
|
12
|
+
type CatchProps = {
|
|
13
|
+
readonly fallback: ErrorComponent;
|
|
14
|
+
readonly children?: ReactNode;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* The boundary a table's `error` component renders inside. Keyed by the
|
|
18
|
+
* navigation that put the tree on screen, so leaving the page that failed
|
|
19
|
+
* leaves the failure behind — and not by the pathname, which commits before
|
|
20
|
+
* the tree arrives and would remount the boundary onto the old tree.
|
|
21
|
+
*/
|
|
22
|
+
declare const RouteErrorBoundary: ({ fallback, children }: CatchProps) => ReactNode;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { ErrorComponent, ErrorProps, RouteErrorBoundary };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { NavigationGeneration } from "./navigation.mjs";
|
|
3
|
+
import { Component, Suspense, createElement, use } from "react";
|
|
4
|
+
//#region src/boundary.tsx
|
|
5
|
+
var Catch = class extends Component {
|
|
6
|
+
state = {
|
|
7
|
+
error: void 0,
|
|
8
|
+
failed: false
|
|
9
|
+
};
|
|
10
|
+
static getDerivedStateFromError(error) {
|
|
11
|
+
return {
|
|
12
|
+
error,
|
|
13
|
+
failed: true
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
reset = () => {
|
|
17
|
+
this.setState({
|
|
18
|
+
error: void 0,
|
|
19
|
+
failed: false
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
render() {
|
|
23
|
+
if (!this.state.failed) return this.props.children;
|
|
24
|
+
return createElement(this.props.fallback, {
|
|
25
|
+
error: this.state.error,
|
|
26
|
+
reset: this.reset
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* The boundary a table's `error` component renders inside. Keyed by the
|
|
32
|
+
* navigation that put the tree on screen, so leaving the page that failed
|
|
33
|
+
* leaves the failure behind — and not by the pathname, which commits before
|
|
34
|
+
* the tree arrives and would remount the boundary onto the old tree.
|
|
35
|
+
*/
|
|
36
|
+
const RouteErrorBoundary = ({ fallback, children }) => {
|
|
37
|
+
const generation = use(NavigationGeneration);
|
|
38
|
+
return createElement(Suspense, { fallback: null }, createElement(Catch, {
|
|
39
|
+
key: generation,
|
|
40
|
+
fallback
|
|
41
|
+
}, children));
|
|
42
|
+
};
|
|
43
|
+
//#endregion
|
|
44
|
+
export { RouteErrorBoundary };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ErrorComponent } from "./boundary.mjs";
|
|
2
|
+
import { Join, PathFor } from "./paths.mjs";
|
|
3
|
+
import { ComponentType, ReactNode } from "react";
|
|
4
|
+
//#region src/define-routes.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Any component, whatever props it declares. The table is written once and
|
|
7
|
+
* rendered by different renderers — a client app renders leaves with no
|
|
8
|
+
* props, the framework hands server components their `params` and layouts
|
|
9
|
+
* their `children` — so the stored type has to admit them all. `never` props
|
|
10
|
+
* is the type-safe spelling of that; each renderer states what it passes.
|
|
11
|
+
*/
|
|
12
|
+
type RouteComponent = ComponentType<never>;
|
|
13
|
+
/**
|
|
14
|
+
* A leaf renders; a branch wraps its children in an optional layout, and
|
|
15
|
+
* may name an `error` component to show in place of what is below when it
|
|
16
|
+
* throws — inside the layout, so the frame survives the failure. Lazy
|
|
17
|
+
* components (`React.lazy`) are objects, not functions, so a branch is
|
|
18
|
+
* recognized by its `children` key rather than by `typeof`.
|
|
19
|
+
*/
|
|
20
|
+
type RouteNode = RouteComponent | {
|
|
21
|
+
layout?: RouteComponent;
|
|
22
|
+
error?: ErrorComponent;
|
|
23
|
+
children: RoutesRecord;
|
|
24
|
+
};
|
|
25
|
+
type RoutesRecord = Record<`/${string}`, RouteNode>;
|
|
26
|
+
type Below<Prefix extends string, Key extends string> = Join<Prefix, Key> extends '/' ? '' : Join<Prefix, Key>;
|
|
27
|
+
type PatternsIn<Record_, Prefix extends string> = { [K in keyof Record_ & string]: Record_[K] extends {
|
|
28
|
+
children: infer C;
|
|
29
|
+
} ? PatternsIn<C, Below<Prefix, K>> : Join<Prefix, K>; }[keyof Record_ & string];
|
|
30
|
+
/** Every full leaf pattern in the table, as written. */
|
|
31
|
+
type PatternOf<R extends RoutesRecord> = PatternsIn<R, ''> & string;
|
|
32
|
+
/** Patterns a link can point at — wildcards are matched, never linked. */
|
|
33
|
+
type NavigablePatternOf<R extends RoutesRecord> = Exclude<PatternOf<R>, `${string}*${string}`>;
|
|
34
|
+
/**
|
|
35
|
+
* The app's pathname space as a type: the union state's `Register` (and any
|
|
36
|
+
* other typed-path consumer) is fed with.
|
|
37
|
+
*/
|
|
38
|
+
type RouteOf<D> = D extends Routes<infer R> ? PathFor<NavigablePatternOf<R>> : never;
|
|
39
|
+
type Match = {
|
|
40
|
+
/** The full pattern that won, as written in the table. */
|
|
41
|
+
pattern: string;
|
|
42
|
+
params: Readonly<Record<string, string>>;
|
|
43
|
+
/** Layouts outer-first, the leaf last. */
|
|
44
|
+
stack: readonly RouteComponent[];
|
|
45
|
+
};
|
|
46
|
+
type Routes<R extends RoutesRecord = RoutesRecord> = {
|
|
47
|
+
kind: 'routes';
|
|
48
|
+
/** The table as passed. */
|
|
49
|
+
record: R;
|
|
50
|
+
/**
|
|
51
|
+
* Matches a pathname in declaration order, first match wins — precedence
|
|
52
|
+
* is what you wrote, not a specificity ranking to second-guess. The one
|
|
53
|
+
* operation that needs the table's value: links and navigation build from
|
|
54
|
+
* the pattern string alone (`href` / `navigateTo`), so only `<Router>`
|
|
55
|
+
* ever holds this object.
|
|
56
|
+
*
|
|
57
|
+
* `accept` lets the caller decline a match, in which case the walk goes
|
|
58
|
+
* on to the next pattern as if this one had not fit — a param a schema
|
|
59
|
+
* refuses is a pathname the pattern does not answer, and what answers it
|
|
60
|
+
* instead is whatever the table declares next, the catch-all included.
|
|
61
|
+
*/
|
|
62
|
+
match: (pathname: string, accept?: (match: Match) => boolean) => Match | null;
|
|
63
|
+
};
|
|
64
|
+
declare const setBoundaryOutlet: (render: () => ReactNode) => void;
|
|
65
|
+
declare function defineRoutes<R extends RoutesRecord>(record: R): Routes<R>;
|
|
66
|
+
//#endregion
|
|
67
|
+
export { Match, NavigablePatternOf, PatternOf, RouteComponent, RouteNode, RouteOf, Routes, RoutesRecord, defineRoutes, setBoundaryOutlet };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { isGroupKey, joinPattern, normalizePathname } from "./paths.mjs";
|
|
2
|
+
import { RouteErrorBoundary } from "./boundary.mjs";
|
|
3
|
+
import { createElement } from "react";
|
|
4
|
+
//#region src/define-routes.ts
|
|
5
|
+
const isBranch = (node) => typeof node === "object" && "children" in node;
|
|
6
|
+
/**
|
|
7
|
+
* A branch's `error` becomes an element of the stack — after the layout, so
|
|
8
|
+
* it wraps what the layout wraps. It takes what any renderer passes and
|
|
9
|
+
* renders what it is given; a renderer that passes nothing (the client
|
|
10
|
+
* router, which nests through `<Outlet />`) is told so by the absence of
|
|
11
|
+
* `children`, and the boundary leaves the hole to the next element.
|
|
12
|
+
*/
|
|
13
|
+
const boundaryFor = (fallback) => {
|
|
14
|
+
const Boundary = ({ children }) => createElement(RouteErrorBoundary, { fallback }, children === void 0 ? outlet() : children);
|
|
15
|
+
return Boundary;
|
|
16
|
+
};
|
|
17
|
+
const decode = (value) => {
|
|
18
|
+
try {
|
|
19
|
+
return decodeURIComponent(value);
|
|
20
|
+
} catch {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* What a boundary renders when a renderer hands it no children. Set once by
|
|
26
|
+
* the client router, which is the one renderer that nests by context rather
|
|
27
|
+
* than by prop; the framework always passes `children`.
|
|
28
|
+
*/
|
|
29
|
+
let outlet = () => null;
|
|
30
|
+
const setBoundaryOutlet = (render) => {
|
|
31
|
+
outlet = render;
|
|
32
|
+
};
|
|
33
|
+
function defineRoutes(record) {
|
|
34
|
+
const entries = [];
|
|
35
|
+
const seen = /* @__PURE__ */ new Set();
|
|
36
|
+
const walk = (current, prefix, stack) => {
|
|
37
|
+
for (const [key, node] of Object.entries(current)) {
|
|
38
|
+
if (!key.startsWith("/")) throw new TypeError(`route pattern "${key}" must start with "/"`);
|
|
39
|
+
if (key.includes("(") && !isGroupKey(key)) throw new TypeError(`route group "${key}" must be "/(name)" and nothing else — a regular expression is not part of the grammar`);
|
|
40
|
+
if (isGroupKey(key) && !isBranch(node)) throw new TypeError(`route group "${key}" must have children`);
|
|
41
|
+
const pattern = joinPattern(prefix, key);
|
|
42
|
+
if (isBranch(node)) {
|
|
43
|
+
const below = [
|
|
44
|
+
...stack,
|
|
45
|
+
...node.layout === void 0 ? [] : [node.layout],
|
|
46
|
+
...node.error === void 0 ? [] : [boundaryFor(node.error)]
|
|
47
|
+
];
|
|
48
|
+
walk(node.children, pattern === "/" ? "" : pattern, below);
|
|
49
|
+
} else {
|
|
50
|
+
if (seen.has(pattern)) throw new TypeError(`route pattern "${pattern}" is declared twice`);
|
|
51
|
+
seen.add(pattern);
|
|
52
|
+
entries.push({
|
|
53
|
+
pattern,
|
|
54
|
+
matcher: new URLPattern({ pathname: pattern }),
|
|
55
|
+
stack: [...stack, node]
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
walk(record, "", []);
|
|
61
|
+
const match = (pathname, accept) => {
|
|
62
|
+
const normalized = normalizePathname(pathname);
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
const result = entry.matcher.exec({ pathname: normalized });
|
|
65
|
+
if (result === null) continue;
|
|
66
|
+
const params = {};
|
|
67
|
+
for (const [name, value] of Object.entries(result.pathname.groups)) if (!/^\d+$/u.test(name) && value !== void 0) params[name] = decode(value);
|
|
68
|
+
const found = {
|
|
69
|
+
pattern: entry.pattern,
|
|
70
|
+
params,
|
|
71
|
+
stack: entry.stack
|
|
72
|
+
};
|
|
73
|
+
if (accept === void 0 || accept(found)) return found;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
kind: "routes",
|
|
79
|
+
record,
|
|
80
|
+
match
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
export { defineRoutes, setBoundaryOutlet };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ErrorComponent, ErrorProps } from "./boundary.mjs";
|
|
2
|
+
import { ParamValue, ParamsOf, ParamsSchemaFor, ParsedParams, ParsedParamsMap, PathFor, SchemaOutput, StandardSchemaLike, normalizePathname } from "./paths.mjs";
|
|
3
|
+
import { Match, NavigablePatternOf, PatternOf, RouteComponent, RouteNode, RouteOf, Routes, RoutesRecord, defineRoutes } from "./define-routes.mjs";
|
|
4
|
+
import { Register, RegisteredNavigablePattern, RegisteredParams, RegisteredPattern } from "./register.mjs";
|
|
5
|
+
import { NavigateToOptions, href, navigateTo } from "./links.mjs";
|
|
6
|
+
import { PathnameProvider, usePathname } from "./location.mjs";
|
|
7
|
+
import { MatchablePattern, matchPath, useMatch } from "./match.mjs";
|
|
8
|
+
import { NavigationGeneration, NavigationHandler, useInterceptedNavigation } from "./navigation.mjs";
|
|
9
|
+
import { Outlet, Router, useParams, useRoute } from "./router.mjs";
|
|
10
|
+
export { type ErrorComponent, type ErrorProps, type Match, type MatchablePattern, type NavigablePatternOf, type NavigateToOptions, NavigationGeneration, type NavigationHandler, Outlet, type ParamValue, type ParamsOf, type ParamsSchemaFor, type ParsedParams, type ParsedParamsMap, type PathFor, PathnameProvider, type PatternOf, type Register, type RegisteredNavigablePattern, type RegisteredParams, type RegisteredPattern, type RouteComponent, type RouteNode, type RouteOf, Router, type Routes, type RoutesRecord, type SchemaOutput, type StandardSchemaLike, defineRoutes, href, matchPath, navigateTo, normalizePathname, useInterceptedNavigation, useMatch, useParams, usePathname, useRoute };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { normalizePathname } from "./paths.mjs";
|
|
2
|
+
import { NavigationGeneration, useInterceptedNavigation } from "./navigation.mjs";
|
|
3
|
+
import { defineRoutes } from "./define-routes.mjs";
|
|
4
|
+
import { href, navigateTo } from "./links.mjs";
|
|
5
|
+
import { PathnameProvider, usePathname } from "./location.mjs";
|
|
6
|
+
import { matchPath, useMatch } from "./match.mjs";
|
|
7
|
+
import { Outlet, Router, useParams, useRoute } from "./router.mjs";
|
|
8
|
+
export { NavigationGeneration, Outlet, PathnameProvider, Router, defineRoutes, href, matchPath, navigateTo, normalizePathname, useInterceptedNavigation, useMatch, useParams, usePathname, useRoute };
|
package/dist/links.d.mts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { PathFor } from "./paths.mjs";
|
|
2
|
+
import { RegisteredNavigablePattern, RegisteredParams } from "./register.mjs";
|
|
3
|
+
//#region src/links.d.ts
|
|
4
|
+
type HrefArgs<Params> = keyof Params extends never ? [] : [params: Readonly<Params & Record<never, never>>];
|
|
5
|
+
type NavigateToOptions = {
|
|
6
|
+
/**
|
|
7
|
+
* `push` by default: going to a page is what the back button undoes —
|
|
8
|
+
* the opposite default from a state refinement.
|
|
9
|
+
*/
|
|
10
|
+
history?: 'push' | 'replace';
|
|
11
|
+
};
|
|
12
|
+
type NavigateToArgs<Params> = keyof Params extends never ? [options?: NavigateToOptions] : [params: Readonly<Params & Record<never, never>>, options?: NavigateToOptions];
|
|
13
|
+
/**
|
|
14
|
+
* Builds a concrete path from a pattern and its params — no route table
|
|
15
|
+
* involved, which is why pages never import one. Params are inferred from
|
|
16
|
+
* the pattern literal itself; `Register` adds the check that the pattern
|
|
17
|
+
* exists in the app's table, and — under the framework — the type a page's
|
|
18
|
+
* schema gave each param, so a link takes what the page received. The path
|
|
19
|
+
* shape survives in the return type for typed-path consumers
|
|
20
|
+
* (`@k8ordo/state`'s `Register` among them).
|
|
21
|
+
*/
|
|
22
|
+
declare const href: <P extends RegisteredNavigablePattern>(pattern: P, ...params: HrefArgs<RegisteredParams<P>>) => PathFor<P>;
|
|
23
|
+
/**
|
|
24
|
+
* Typed imperative navigation: `href` composed with
|
|
25
|
+
* `navigation.navigate()`, returning the platform's own
|
|
26
|
+
* `{ committed, finished }`. Changing pages goes through here; changing
|
|
27
|
+
* state goes through `@k8ordo/state`'s `update()`.
|
|
28
|
+
*/
|
|
29
|
+
declare const navigateTo: <P extends RegisteredNavigablePattern>(pattern: P, ...rest: NavigateToArgs<RegisteredParams<P>>) => ReturnType<Navigation['navigate']>;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { NavigateToOptions, href, navigateTo };
|
package/dist/links.mjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { buildHref } from "./paths.mjs";
|
|
2
|
+
//#region src/links.ts
|
|
3
|
+
/**
|
|
4
|
+
* Builds a concrete path from a pattern and its params — no route table
|
|
5
|
+
* involved, which is why pages never import one. Params are inferred from
|
|
6
|
+
* the pattern literal itself; `Register` adds the check that the pattern
|
|
7
|
+
* exists in the app's table, and — under the framework — the type a page's
|
|
8
|
+
* schema gave each param, so a link takes what the page received. The path
|
|
9
|
+
* shape survives in the return type for typed-path consumers
|
|
10
|
+
* (`@k8ordo/state`'s `Register` among them).
|
|
11
|
+
*/
|
|
12
|
+
const href = (pattern, ...params) => buildHref(pattern, params[0]);
|
|
13
|
+
/**
|
|
14
|
+
* Typed imperative navigation: `href` composed with
|
|
15
|
+
* `navigation.navigate()`, returning the platform's own
|
|
16
|
+
* `{ committed, finished }`. Changing pages goes through here; changing
|
|
17
|
+
* state goes through `@k8ordo/state`'s `update()`.
|
|
18
|
+
*/
|
|
19
|
+
const navigateTo = (pattern, ...rest) => {
|
|
20
|
+
const wantsParams = pattern.includes(":");
|
|
21
|
+
const args = rest;
|
|
22
|
+
const params = wantsParams ? args[0] : void 0;
|
|
23
|
+
const options = wantsParams ? args[1] : args[0];
|
|
24
|
+
return navigation.navigate(buildHref(pattern, params), { history: options?.history ?? "push" });
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
export { href, navigateTo };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
//#region src/location.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The pathname the render started from. A client component's first render can
|
|
5
|
+
* happen where there is no Navigation API — on the server, and again during
|
|
6
|
+
* hydration — so the value has to arrive from the renderer that did know it.
|
|
7
|
+
*
|
|
8
|
+
* `<Router>` supplies this on its own. Under the framework the mode's runtime
|
|
9
|
+
* supplies it; an application never writes this itself.
|
|
10
|
+
*/
|
|
11
|
+
declare const PathnameProvider: ({ pathname, children }: {
|
|
12
|
+
pathname: string;
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
}) => ReactNode;
|
|
15
|
+
/**
|
|
16
|
+
* Where the browser currently is, as a pathname — the one axis this package
|
|
17
|
+
* owns. It reads the platform rather than a table, so it works the same in a
|
|
18
|
+
* client application and under the framework, where there is no route table
|
|
19
|
+
* in the browser at all.
|
|
20
|
+
*
|
|
21
|
+
* It is not `useRoute`: this answers "what is the URL", which a navigation
|
|
22
|
+
* link needs, while `useRoute` answers "which route won", which only a table
|
|
23
|
+
* can say.
|
|
24
|
+
*
|
|
25
|
+
* The search string is deliberately absent. A component that re-rendered on
|
|
26
|
+
* every search change would defeat `@k8ordo/state`'s keyed subscriptions, and
|
|
27
|
+
* the split at the `?` is the whole boundary between the two packages.
|
|
28
|
+
*/
|
|
29
|
+
declare function usePathname(): string;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { PathnameProvider, usePathname };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { normalizePathname } from "./paths.mjs";
|
|
3
|
+
import { createContext, use, useMemo, useSyncExternalStore } from "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
//#region src/location.tsx
|
|
6
|
+
const ServerPathname = createContext(null);
|
|
7
|
+
/**
|
|
8
|
+
* The pathname the render started from. A client component's first render can
|
|
9
|
+
* happen where there is no Navigation API — on the server, and again during
|
|
10
|
+
* hydration — so the value has to arrive from the renderer that did know it.
|
|
11
|
+
*
|
|
12
|
+
* `<Router>` supplies this on its own. Under the framework the mode's runtime
|
|
13
|
+
* supplies it; an application never writes this itself.
|
|
14
|
+
*/
|
|
15
|
+
const PathnameProvider = ({ pathname, children }) => {
|
|
16
|
+
const value = useMemo(() => normalizePathname(pathname), [pathname]);
|
|
17
|
+
return /* @__PURE__ */ jsx(ServerPathname, {
|
|
18
|
+
value,
|
|
19
|
+
children
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
const subscribe = (onChange) => {
|
|
23
|
+
navigation.addEventListener("currententrychange", onChange);
|
|
24
|
+
return () => {
|
|
25
|
+
navigation.removeEventListener("currententrychange", onChange);
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
const readPathname = () => normalizePathname(location.pathname);
|
|
29
|
+
/**
|
|
30
|
+
* Where the browser currently is, as a pathname — the one axis this package
|
|
31
|
+
* owns. It reads the platform rather than a table, so it works the same in a
|
|
32
|
+
* client application and under the framework, where there is no route table
|
|
33
|
+
* in the browser at all.
|
|
34
|
+
*
|
|
35
|
+
* It is not `useRoute`: this answers "what is the URL", which a navigation
|
|
36
|
+
* link needs, while `useRoute` answers "which route won", which only a table
|
|
37
|
+
* can say.
|
|
38
|
+
*
|
|
39
|
+
* The search string is deliberately absent. A component that re-rendered on
|
|
40
|
+
* every search change would defeat `@k8ordo/state`'s keyed subscriptions, and
|
|
41
|
+
* the split at the `?` is the whole boundary between the two packages.
|
|
42
|
+
*/
|
|
43
|
+
function usePathname() {
|
|
44
|
+
const fromServer = use(ServerPathname);
|
|
45
|
+
return useSyncExternalStore(subscribe, readPathname, () => {
|
|
46
|
+
if (fromServer === null) throw new Error("usePathname needs <Router> above it, or a page rendered by @k8ordo/static or @k8ordo/server");
|
|
47
|
+
return fromServer;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { PathnameProvider, usePathname };
|
package/dist/match.d.mts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ParamsOf } from "./paths.mjs";
|
|
2
|
+
import { RegisteredNavigablePattern, RegisteredPattern } from "./register.mjs";
|
|
3
|
+
//#region src/match.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* A pattern from the table, or a table pattern followed by `/*` to mean
|
|
6
|
+
* "everything below it" — the shape a navigation asks about when it wants
|
|
7
|
+
* to know which section of the site is showing. The pattern's own page is
|
|
8
|
+
* not below it: `/x/*` matches `/x/y` and not `/x`.
|
|
9
|
+
*/
|
|
10
|
+
type MatchablePattern = RegisteredPattern | `${RegisteredNavigablePattern}/*`;
|
|
11
|
+
/**
|
|
12
|
+
* Whether a pathname is the one a pattern names, and with which params. Pure
|
|
13
|
+
* and table-free — the pattern is checked against `Register`, the pathname
|
|
14
|
+
* is whatever the caller holds — which is why it works where no table is
|
|
15
|
+
* mounted, under the framework included.
|
|
16
|
+
*/
|
|
17
|
+
declare const matchPath: <P extends MatchablePattern>(pattern: P, pathname: string) => ParamsOf<P> | null;
|
|
18
|
+
/**
|
|
19
|
+
* `matchPath` against where the browser is. It re-renders when the pathname
|
|
20
|
+
* changes and never on the search, like `usePathname` it is built on — so a
|
|
21
|
+
* sidebar can ask "is a page under `/docs/*` showing" without a table in the
|
|
22
|
+
* browser, which is the question `useRoute` cannot answer under the framework.
|
|
23
|
+
*/
|
|
24
|
+
declare function useMatch<P extends MatchablePattern>(pattern: P): ParamsOf<P> | null;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { MatchablePattern, matchPath, useMatch };
|
package/dist/match.mjs
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { normalizePathname } from "./paths.mjs";
|
|
3
|
+
import { usePathname } from "./location.mjs";
|
|
4
|
+
//#region src/match.ts
|
|
5
|
+
const decode = (value) => {
|
|
6
|
+
try {
|
|
7
|
+
return decodeURIComponent(value);
|
|
8
|
+
} catch {
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Whether a pathname is the one a pattern names, and with which params. Pure
|
|
14
|
+
* and table-free — the pattern is checked against `Register`, the pathname
|
|
15
|
+
* is whatever the caller holds — which is why it works where no table is
|
|
16
|
+
* mounted, under the framework included.
|
|
17
|
+
*/
|
|
18
|
+
const matchPath = (pattern, pathname) => {
|
|
19
|
+
const result = new URLPattern({ pathname: pattern }).exec({ pathname: normalizePathname(pathname) });
|
|
20
|
+
if (result === null) return null;
|
|
21
|
+
const params = {};
|
|
22
|
+
for (const [name, value] of Object.entries(result.pathname.groups)) if (!/^\d+$/u.test(name) && value !== void 0) params[name] = decode(value);
|
|
23
|
+
return params;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* `matchPath` against where the browser is. It re-renders when the pathname
|
|
27
|
+
* changes and never on the search, like `usePathname` it is built on — so a
|
|
28
|
+
* sidebar can ask "is a page under `/docs/*` showing" without a table in the
|
|
29
|
+
* browser, which is the question `useRoute` cannot answer under the framework.
|
|
30
|
+
*/
|
|
31
|
+
function useMatch(pattern) {
|
|
32
|
+
return matchPath(pattern, usePathname());
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { matchPath, useMatch };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
//#region src/navigation.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* What the decision below reads. Spelled out rather than taking the event,
|
|
4
|
+
* because a `NavigateEvent` cannot be constructed in a test — its destination
|
|
5
|
+
* is not constructible — and this decision is the whole contract with the
|
|
6
|
+
* platform.
|
|
7
|
+
*/
|
|
8
|
+
type NavigationFacts = Pick<NavigateEvent, 'canIntercept' | 'hashChange' | 'downloadRequest' | 'navigationType'> & {
|
|
9
|
+
readonly formData: FormData | null;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Whether this navigation is the application's to handle at all.
|
|
13
|
+
*
|
|
14
|
+
* A reload is a request for a fresh document, and a POST submission carries a
|
|
15
|
+
* body only the server can act on: intercepting either means silently doing
|
|
16
|
+
* nothing where the platform would have done the obvious thing. A GET form
|
|
17
|
+
* has no `formData`, so the search-shaped navigations `@k8ordo/state` builds
|
|
18
|
+
* still come through here.
|
|
19
|
+
*/
|
|
20
|
+
declare const isOurs: (event: NavigationFacts) => boolean;
|
|
21
|
+
type NavigationHandler<T> = {
|
|
22
|
+
/**
|
|
23
|
+
* Whether this navigation is the application's to handle. Answered
|
|
24
|
+
* synchronously, because that is the only moment the platform lets anyone
|
|
25
|
+
* intercept. Saying no leaves a real document load — and a real 404 — to
|
|
26
|
+
* the server.
|
|
27
|
+
*/
|
|
28
|
+
claim: (url: URL) => boolean;
|
|
29
|
+
/** Produces whatever the application renders for this URL. */
|
|
30
|
+
load: (url: URL, signal: AbortSignal) => T | Promise<T>;
|
|
31
|
+
/** Applies it. Called inside a transition. */
|
|
32
|
+
apply: (value: T) => void;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Where the viewport goes once the new page is on screen. A traversal is the
|
|
36
|
+
* browser's to restore, so it carries no instruction; a fragment scrolls to
|
|
37
|
+
* its target; anything else starts at the top, the way a document load does.
|
|
38
|
+
*/
|
|
39
|
+
type ScrollPlan = {
|
|
40
|
+
kind: 'top';
|
|
41
|
+
} | {
|
|
42
|
+
kind: 'fragment';
|
|
43
|
+
id: string;
|
|
44
|
+
};
|
|
45
|
+
declare const scrollPlanFor: (navigationType: NavigationType, hash: string) => ScrollPlan | null;
|
|
46
|
+
/**
|
|
47
|
+
* Which navigation put the tree on screen — a number that changes exactly
|
|
48
|
+
* when a new tree is applied, and not when only the URL moved. It is the
|
|
49
|
+
* identity an error boundary keys on: leaving the page that failed is what
|
|
50
|
+
* should clear the failure, and the URL commits before the tree arrives, so
|
|
51
|
+
* the pathname would clear it one render too early and let the old tree
|
|
52
|
+
* fail again under the new key.
|
|
53
|
+
*/
|
|
54
|
+
declare const NavigationGeneration: import("react").Context<number>;
|
|
55
|
+
/**
|
|
56
|
+
* The navigation half of the router, on its own: intercept, load, apply in a
|
|
57
|
+
* transition, and resolve the platform's handler only once the new tree is on
|
|
58
|
+
* screen — which is what makes `navigation.navigate().finished` mean "the
|
|
59
|
+
* page is showing", and what `@k8ordo/state`'s `update().finished` inherits.
|
|
60
|
+
*
|
|
61
|
+
* What gets loaded is the caller's business: a route table match for a plain
|
|
62
|
+
* client app, an RSC payload under the framework. Neither has to teach this
|
|
63
|
+
* hook anything about the other.
|
|
64
|
+
*/
|
|
65
|
+
declare function useInterceptedNavigation<T>(handler: NavigationHandler<T>): {
|
|
66
|
+
readonly generation: number;
|
|
67
|
+
};
|
|
68
|
+
//#endregion
|
|
69
|
+
export { NavigationFacts, NavigationGeneration, NavigationHandler, ScrollPlan, isOurs, scrollPlanFor, useInterceptedNavigation };
|