@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/docs/GUIDE.md
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
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.
|
|
6
|
+
|
|
7
|
+
Like every k8ordo package it assumes React 19 and Server Components, uses only
|
|
8
|
+
what has reached Baseline newly available, and ships no polyfills or legacy
|
|
9
|
+
fallbacks. Both the Navigation API and URLPattern reached Baseline in 2025–26;
|
|
10
|
+
this package treats them as simply present.
|
|
11
|
+
|
|
12
|
+
## What it does not own
|
|
13
|
+
|
|
14
|
+
Search params and history-entry state belong to `@k8ordo/state`. This package
|
|
15
|
+
has no `useSearchParams`, and it never hands out the raw search string: a
|
|
16
|
+
component that reads one field of the search should re-render when that field
|
|
17
|
+
changes and not otherwise, which is a job for keyed subscriptions, not for a
|
|
18
|
+
router. The division is the URL itself — pathname here, everything after it
|
|
19
|
+
there.
|
|
20
|
+
|
|
21
|
+
It also does not fetch. There is no loader, no route-level data API, and no
|
|
22
|
+
cache. Data belongs to the component that needs it — `use()` and `<Suspense>`
|
|
23
|
+
in a client app, the server in a framework one — and a router that owned
|
|
24
|
+
fetching would be a second, competing answer to a question React already
|
|
25
|
+
answers.
|
|
26
|
+
|
|
27
|
+
## The shape of it
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
routes.ts defineRoutes({ … }) the pathname schema, one place
|
|
31
|
+
↓ mounted once
|
|
32
|
+
<Router routes> match → stack → render layouts wrap through <Outlet />
|
|
33
|
+
↑ never imported by pages
|
|
34
|
+
pages href / navigateTo / useParams
|
|
35
|
+
take the pattern string; Register supplies the check
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The table's value is held by `<Router>` and nothing else. Everything a page
|
|
39
|
+
needs — building a link, going somewhere, reading its own params — works from
|
|
40
|
+
the pattern **string**, so the module that imports the pages is never imported
|
|
41
|
+
back by them.
|
|
42
|
+
|
|
43
|
+
## The route table
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// routes.ts
|
|
47
|
+
import { defineRoutes } from '@k8ordo/router';
|
|
48
|
+
|
|
49
|
+
import { DocsLayout } from './docs-layout';
|
|
50
|
+
import { Guide } from './guide';
|
|
51
|
+
import { Home } from './home';
|
|
52
|
+
import { NotFound } from './not-found';
|
|
53
|
+
import { ProductList } from './product-list';
|
|
54
|
+
import { ProductPage } from './product-page';
|
|
55
|
+
|
|
56
|
+
export const routes = defineRoutes({
|
|
57
|
+
'/': Home,
|
|
58
|
+
'/products': {
|
|
59
|
+
children: {
|
|
60
|
+
'/': ProductList,
|
|
61
|
+
'/:id': ProductPage,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
'/(docs)': {
|
|
65
|
+
layout: DocsLayout,
|
|
66
|
+
children: { '/guide': Guide },
|
|
67
|
+
},
|
|
68
|
+
'/*': NotFound,
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- A **leaf** is the component to render. A **branch** is `{ layout?, children }`,
|
|
73
|
+
and a child key of `'/'` is the branch's own index page.
|
|
74
|
+
- **`/:name`** captures a segment, decoded. **`/*`** matches whatever nothing
|
|
75
|
+
before it did.
|
|
76
|
+
- **`/(name)`** is a route group: it structures the table — its own layout, its
|
|
77
|
+
own subtree — without contributing a URL segment. It exists because `'/'` can
|
|
78
|
+
only appear once in an object, so two sections at the same depth could not
|
|
79
|
+
otherwise have different layouts.
|
|
80
|
+
- **A trailing slash is the same pathname.** `/products/` matches `/products`.
|
|
81
|
+
- A branch may name an **`error`** component beside its layout:
|
|
82
|
+
`{ layout, error, children }`. When anything below throws, it renders in
|
|
83
|
+
the layout's hole instead — with `{ error, reset }` as props — and the
|
|
84
|
+
frame around it survives. Leaving the page that failed clears the failure.
|
|
85
|
+
|
|
86
|
+
### Order is the rule
|
|
87
|
+
|
|
88
|
+
Matching walks the table top to bottom and takes the first pattern that fits.
|
|
89
|
+
Precedence is what you wrote — there is no specificity ranking to reason
|
|
90
|
+
backwards from, which means a table can be read like the code it is.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
defineRoutes({ '/:slug': Article, '/about': About }); // /about → Article
|
|
94
|
+
defineRoutes({ '/about': About, '/:slug': Article }); // /about → About
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Put `/*` last, for the same reason.
|
|
98
|
+
|
|
99
|
+
### What fails at definition time
|
|
100
|
+
|
|
101
|
+
A table is checked when the module loads, not when someone first navigates:
|
|
102
|
+
|
|
103
|
+
| written | error |
|
|
104
|
+
| --------------------------------------------------------- | --------------------------------------------------------------- |
|
|
105
|
+
| the same full pattern twice, wherever the copies nest | `route pattern "/x" is declared twice` |
|
|
106
|
+
| a group with no children (it would redeclare the index) | `route group "/(oops)" must have children` |
|
|
107
|
+
| a key not starting with `/` | `route pattern "x" must start with "/"` |
|
|
108
|
+
| parentheses that are not exactly a group (`/(admin)/new`) | `route group "/(admin)/new" must be "/(name)" and nothing else` |
|
|
109
|
+
| a pattern URLPattern cannot parse | URLPattern's own `TypeError` |
|
|
110
|
+
|
|
111
|
+
## Mounting it
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
import { Outlet, Router } from '@k8ordo/router';
|
|
115
|
+
|
|
116
|
+
import { routes } from './routes';
|
|
117
|
+
|
|
118
|
+
export function App() {
|
|
119
|
+
return <Router routes={routes} />;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// a layout renders what it wraps through Outlet
|
|
123
|
+
export const DocsLayout = () => (
|
|
124
|
+
<section>
|
|
125
|
+
<nav>…</nav>
|
|
126
|
+
<Outlet />
|
|
127
|
+
</section>
|
|
128
|
+
);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Every same-origin link or programmatic navigation the table claims is handled
|
|
132
|
+
in the browser; the rest is left alone, so a real document load — and a real
|
|
133
|
+
404 — stays the server's answer. A pathname the table does not match renders
|
|
134
|
+
nothing rather than guessing.
|
|
135
|
+
|
|
136
|
+
Four kinds of navigation are never the application's, whatever the table says:
|
|
137
|
+
a reload, a form submitted with a body (POST), a download, and a fragment-only
|
|
138
|
+
change. Intercepting any of them would silently do nothing where the platform
|
|
139
|
+
would have done the obvious thing — a POST body only the server can act on, an
|
|
140
|
+
`F5` that stops reloading. A GET form carries no body, so the search-shaped
|
|
141
|
+
submissions `@k8ordo/state` builds still come through. Mark a link to a file
|
|
142
|
+
the host serves as an attachment with `download`, so the browser tells the
|
|
143
|
+
router before the click rather than after the answer.
|
|
144
|
+
|
|
145
|
+
A leaf can be `React.lazy(...)`, which the table stores as any other component;
|
|
146
|
+
put a `<Suspense>` in the layout above it so there is somewhere to fall back
|
|
147
|
+
to while the chunk arrives.
|
|
148
|
+
|
|
149
|
+
## Links and navigation
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
import {
|
|
153
|
+
href,
|
|
154
|
+
navigateTo,
|
|
155
|
+
useParams,
|
|
156
|
+
usePathname,
|
|
157
|
+
useRoute,
|
|
158
|
+
} from '@k8ordo/router';
|
|
159
|
+
|
|
160
|
+
<a href={href('/products/:id', { id })}>…</a>;
|
|
161
|
+
<a href={href('/products')}>…</a>; // no params, no second argument
|
|
162
|
+
|
|
163
|
+
navigateTo('/products/:id', { id }); // pushes: the back button undoes it
|
|
164
|
+
navigateTo('/products', { history: 'replace' });
|
|
165
|
+
|
|
166
|
+
const { id } = useParams('/products/:id');
|
|
167
|
+
const { pattern } = useRoute(); // untyped: the winning pattern and its params
|
|
168
|
+
const pathname = usePathname(); // where the browser is, table or no table
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**There is no `<Link>`.** Under the Navigation API a plain `<a>` is already a
|
|
172
|
+
client navigation — the router intercepts the event the browser was going to
|
|
173
|
+
send anyway. A component wrapping it would add a second way to write the same
|
|
174
|
+
thing and nothing else. An active link is a question you ask, not a prop:
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
import { matchPath, useMatch } from '@k8ordo/router';
|
|
178
|
+
|
|
179
|
+
useMatch('/products/:id'); // { id } when that page is showing, else null
|
|
180
|
+
useMatch('/products/*'); // {} anywhere under /products, else null
|
|
181
|
+
matchPath('/products/:id', pathname); // the same, pure, for a pathname in hand
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`useMatch` takes a pattern from the table, or a table pattern followed by
|
|
185
|
+
`/*` to mean "everything below it" — what a sidebar asks when it wants to
|
|
186
|
+
know which section of the site is open. The pattern's own page is not below
|
|
187
|
+
it: `/products/*` matches `/products/42` and not `/products`, which is
|
|
188
|
+
`useMatch('/products')`; ask both when a section includes its index. It is built on `usePathname`, so it
|
|
189
|
+
re-renders on the pathname and never on the search, and it needs no table in
|
|
190
|
+
the browser — which is what makes it the one of these that also works under
|
|
191
|
+
the framework, where `useRoute` has no match to read.
|
|
192
|
+
|
|
193
|
+
**`usePathname` changes when the URL changes, not when the new page appears.**
|
|
194
|
+
Interception commits the URL first and the tree arrives when it has loaded, so
|
|
195
|
+
on a slow navigation a link marks itself active while the previous page is
|
|
196
|
+
still on screen — the same order the browser's own address bar follows. Pair it
|
|
197
|
+
with `useTransition` if the wait needs showing — `navigateTo`'s `finished`
|
|
198
|
+
resolves when the tree is on screen (below).
|
|
199
|
+
|
|
200
|
+
`usePathname` reads the platform rather than the table, which is why it is the
|
|
201
|
+
one that also works under the framework, where the browser holds no table at
|
|
202
|
+
all. It answers _where the URL is_; `useRoute` answers _which route won_, and
|
|
203
|
+
only something holding the table can say that. The search string is
|
|
204
|
+
deliberately not on offer here: a component re-rendering on every search change
|
|
205
|
+
would defeat `@k8ordo/state`'s keyed subscriptions, and the split at the `?` is
|
|
206
|
+
the boundary between the two packages.
|
|
207
|
+
|
|
208
|
+
`href` refuses a wildcard: `/*` is something to match, never something to link
|
|
209
|
+
to. Param values are URL-encoded on the way in and decoded on the way out.
|
|
210
|
+
`normalizePathname` is the router's own reading of a pathname — a trailing
|
|
211
|
+
slash dropped, root excepted — for code that compares pathnames the way the
|
|
212
|
+
table does.
|
|
213
|
+
|
|
214
|
+
`navigateTo` returns the platform's own `{ committed, finished }`, so it
|
|
215
|
+
composes with React 19's async transitions:
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
const [isPending, startTransition] = useTransition();
|
|
219
|
+
|
|
220
|
+
startTransition(async () => {
|
|
221
|
+
await navigateTo('/products/:id', { id }).finished;
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Its default is `push`, the opposite of `@k8ordo/state`'s `update()`, and for
|
|
226
|
+
the same reason: going to a page is what the back button should undo, while
|
|
227
|
+
refining what is on the page is not. **Changing pages goes through
|
|
228
|
+
`navigateTo`; changing state goes through `update`.**
|
|
229
|
+
|
|
230
|
+
## Checking patterns against the real table
|
|
231
|
+
|
|
232
|
+
Params are inferred from the pattern literal on their own, so a missing or
|
|
233
|
+
misspelled one fails to compile with no setup at all. To have the **pattern**
|
|
234
|
+
checked against the app's actual table as well, augment `Register` once:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
// types/k8ordo-router.d.ts
|
|
238
|
+
import type { routes } from '../src/routes';
|
|
239
|
+
|
|
240
|
+
declare module '@k8ordo/router' {
|
|
241
|
+
interface Register {
|
|
242
|
+
routes: typeof routes;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
href('/products/:id', { id }); // ok
|
|
249
|
+
href('/prodcuts/:id', { id }); // ✗ not a pattern in the table
|
|
250
|
+
href('/products/:id'); // ✗ ":id" is missing
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Without the augmentation the constraint is any `/`-prefixed string. Augment
|
|
254
|
+
only in an application — a library doing it would impose its table on every
|
|
255
|
+
consumer.
|
|
256
|
+
|
|
257
|
+
Under `@k8ordo/static` or `@k8ordo/server` this file is generated into
|
|
258
|
+
`.k8ordo/register.gen.ts` from `routes/` — for `@k8ordo/state` too, when the
|
|
259
|
+
application depends on it — so hand-writing it there is writing a second
|
|
260
|
+
answer to a question already answered. Hand-write it in a client application
|
|
261
|
+
that mounts `<Router>` itself.
|
|
262
|
+
|
|
263
|
+
The framework's generated `Register` also carries `params`: per pattern, the
|
|
264
|
+
type the route file's `paramsSchema` produces. With it, `href` and
|
|
265
|
+
`navigateTo` take a param as the page receives it — `{ id: 42 }` for a
|
|
266
|
+
schema that said number — and spell it the one way the schema reads back. A
|
|
267
|
+
value with no URL spelling (an object) is refused. Before `Register` is
|
|
268
|
+
augmented — or where no schema covers a param — a link takes any value with
|
|
269
|
+
one spelling (a string, a number, a boolean), so a link written for a schema
|
|
270
|
+
compiles before the generated file exists. `useParams` is unaffected: what a
|
|
271
|
+
hand-written table matches is always a string.
|
|
272
|
+
|
|
273
|
+
## Typed paths for @k8ordo/state
|
|
274
|
+
|
|
275
|
+
`@k8ordo/state`'s `Register` takes the same line this one does:
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
declare module '@k8ordo/state' {
|
|
279
|
+
interface Register {
|
|
280
|
+
routes: typeof routes;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
With that, `listState.href('/products', { q })` is checked against the same
|
|
286
|
+
table this router matches against, and the two packages agree on what a path
|
|
287
|
+
is. `RouteOf<typeof routes>` is that pathname space as a union, for any other
|
|
288
|
+
typed-path consumer.
|
|
289
|
+
|
|
290
|
+
## What navigation guarantees
|
|
291
|
+
|
|
292
|
+
**`finished` means the page is on screen.** The intercept handler resolves in
|
|
293
|
+
an effect after React commits the new tree, so anything awaiting the platform's
|
|
294
|
+
promise — including `@k8ordo/state`'s `update().finished` — is awaiting the
|
|
295
|
+
render, not the URL write.
|
|
296
|
+
|
|
297
|
+
**A state change is not a page change.** When only the search or the entry
|
|
298
|
+
state moved, the pathname is the one whose tree is on screen: the route tree
|
|
299
|
+
is left alone, nothing remounts, and scroll and focus are not disturbed. This
|
|
300
|
+
is why a search update never scrolls the page back to the top. The comparison
|
|
301
|
+
is against the page showing, not against the address bar — interception
|
|
302
|
+
commits the URL first, so a state update issued while another page is still
|
|
303
|
+
loading is a page change and lets that page finish arriving.
|
|
304
|
+
|
|
305
|
+
**Route changes run in a transition.** The new tree is applied inside
|
|
306
|
+
`startTransition`, so React can keep the old page interactive while the new
|
|
307
|
+
one prepares.
|
|
308
|
+
|
|
309
|
+
**A new page starts at the top.** Once the new tree is on screen, the
|
|
310
|
+
viewport goes where a document load would have put it: the top, or the
|
|
311
|
+
element a `#fragment` names. Back and forward are left to the browser, which
|
|
312
|
+
restores the position it saved. A state change never moves the viewport —
|
|
313
|
+
same pathname, same place — so a filter update does not scroll the reader
|
|
314
|
+
back to the top.
|
|
315
|
+
|
|
316
|
+
**Superseded navigations abort.** A second navigation aborts the first through
|
|
317
|
+
the platform's own signal: the overtaken `finished` rejects with the abort
|
|
318
|
+
reason, and its tree never reaches the screen even if its load had already
|
|
319
|
+
come back. `load` receives that signal, so a payload fetch under the framework
|
|
320
|
+
is cancelled outright. A `React.lazy` chunk cannot be — a dynamic import takes
|
|
321
|
+
no signal — so in a client application it finishes in the background and is
|
|
322
|
+
kept for the next visit, while the page it belonged to is never shown.
|
|
323
|
+
|
|
324
|
+
## Testing
|
|
325
|
+
|
|
326
|
+
Nothing here is mocked, so a test needs a browser environment (this package's
|
|
327
|
+
own suite uses Vitest's Chromium browser mode). The one thing to know: a test
|
|
328
|
+
that navigates must intercept, or `navigation.navigate()` is a cross-document
|
|
329
|
+
load that takes the test runner with it.
|
|
330
|
+
|
|
331
|
+
```tsx
|
|
332
|
+
// a test that mounts <Router> is already intercepting; one that sets up a URL
|
|
333
|
+
// outside the table has to play the router itself
|
|
334
|
+
const interceptEverything = (event: NavigateEvent) => {
|
|
335
|
+
if (event.canIntercept) event.intercept();
|
|
336
|
+
};
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
`match` is a pure function and needs no browser: a table's shape, its params
|
|
340
|
+
and its precedence can all be asserted directly.
|
|
341
|
+
|
|
342
|
+
## Under the framework
|
|
343
|
+
|
|
344
|
+
`@k8ordo/static` and `@k8ordo/server` render pages on the server, so the
|
|
345
|
+
browser receives a tree rather than building one from a table — there is no
|
|
346
|
+
route table in the client bundle at all, and layouts nest through `children`
|
|
347
|
+
instead of `<Outlet />`. What stays is navigation: both build on
|
|
348
|
+
`useInterceptedNavigation`, the primitive `<Router>` itself uses.
|
|
349
|
+
|
|
350
|
+
```tsx
|
|
351
|
+
const { generation } = useInterceptedNavigation<Value>({
|
|
352
|
+
claim: (url) => boolean, // synchronous: the only moment interception is possible
|
|
353
|
+
load: (url, signal) => Value | Promise<Value>,
|
|
354
|
+
apply: (value) => void, // called inside a transition
|
|
355
|
+
});
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
`generation` changes exactly when a new tree is applied — not when the URL
|
|
359
|
+
moved — and a host provides it through `<NavigationGeneration value>` so the
|
|
360
|
+
table's `error` boundaries know when to let a failure go. `<Router>` does
|
|
361
|
+
this itself; the framework's runtime does too.
|
|
362
|
+
|
|
363
|
+
What carries across unchanged is everything that needs no table: `href`,
|
|
364
|
+
`navigateTo`, `usePathname` and `useMatch`. `usePathname` needs one thing on the server,
|
|
365
|
+
where there is no Navigation API to read: the pathname the render is for,
|
|
366
|
+
supplied by `<PathnameProvider pathname>`. `<Router>` mounts one itself and
|
|
367
|
+
both mode runtimes supply it, so an application never writes it — only a host
|
|
368
|
+
building its own seam out of `useInterceptedNavigation` has to. What does not
|
|
369
|
+
carry across is `useRoute` and `useParams` —
|
|
370
|
+
both read the match from context, and under the framework there is no match in
|
|
371
|
+
the browser to read. A framework page receives its `params` as a prop from the
|
|
372
|
+
server instead, which is the only form Server Components can take them in.
|
package/docs/llms.txt
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# @k8ordo/router
|
|
2
|
+
|
|
3
|
+
> The URL's pathname axis, owned. One `defineRoutes` table is the application's
|
|
4
|
+
> pathname schema: types, matching, links and navigation all come from it, over
|
|
5
|
+
> the Navigation API and URLPattern. Search params and history-entry state are
|
|
6
|
+
> not its business — those belong to `@k8ordo/state`.
|
|
7
|
+
>
|
|
8
|
+
> Like every k8ordo package it assumes React 19 and Server Components, uses
|
|
9
|
+
> only what has reached Baseline newly available, and ships no polyfills or
|
|
10
|
+
> legacy fallbacks.
|
|
11
|
+
|
|
12
|
+
When installed via npm, these docs live under `node_modules/@k8ordo/router/docs/`.
|
|
13
|
+
|
|
14
|
+
## Guide
|
|
15
|
+
|
|
16
|
+
- [Design Guide](GUIDE.md): the table's grammar (leaves, branches, params,
|
|
17
|
+
wildcards, route groups, declaration order), mounting `<Router>` and
|
|
18
|
+
`<Outlet>`, links without a `<Link>` component, `useMatch` for active
|
|
19
|
+
sections, `Register` for checked patterns, what navigation guarantees
|
|
20
|
+
(`finished` after commit, scroll to top or fragment on a new page), and
|
|
21
|
+
how the framework reuses the navigation half.
|
|
22
|
+
|
|
23
|
+
## Entry point
|
|
24
|
+
|
|
25
|
+
- `@k8ordo/router` — `defineRoutes(record)` returns a table whose one operation
|
|
26
|
+
is `match(pathname)`; `<Router routes>` mounts it on the Navigation API and
|
|
27
|
+
`<Outlet />` renders the rest of the matched stack; a branch's `error`
|
|
28
|
+
component (`{ error, reset }` props, type `ErrorComponent`) renders in the
|
|
29
|
+
layout's hole when what is below throws, keyed by `NavigationGeneration`
|
|
30
|
+
so leaving the page clears it. `href(pattern, params?)`
|
|
31
|
+
and `navigateTo(pattern, params?, options?)` build and perform navigations
|
|
32
|
+
from the pattern **string** alone — no table import, so no cycle — with
|
|
33
|
+
params inferred from the literal and the pattern itself checked once
|
|
34
|
+
`interface Register { routes }` is augmented. `useParams(pattern)` returns
|
|
35
|
+
typed params and throws if rendered under a different pattern; under the
|
|
36
|
+
framework `Register` also carries `params` — the type each pattern's
|
|
37
|
+
`paramsSchema` produces — so `href` takes a param as the page receives it
|
|
38
|
+
(`ParsedParams` / `ParsedParamsMap` / `ParamsSchemaFor` are the types the
|
|
39
|
+
generated code uses); `useRoute()`
|
|
40
|
+
is the untyped form. `RouteOf<typeof routes>` is the pathname union to feed
|
|
41
|
+
`@k8ordo/state`'s `Register`. `usePathname()` reads the platform's
|
|
42
|
+
pathname — never the search — so it works with or without a table, which
|
|
43
|
+
makes it the location hook that also works under the framework;
|
|
44
|
+
`useMatch(pattern)` / `matchPath(pattern, pathname)` answer whether that
|
|
45
|
+
pattern — or, with a trailing `/*`, anything strictly below it — is the pathname
|
|
46
|
+
showing, with its params, table-free, so a sidebar can ask which section is
|
|
47
|
+
open under the framework too; `normalizePathname(pathname)` is the router's
|
|
48
|
+
own trailing-slash rule;
|
|
49
|
+
`<PathnameProvider pathname>` seeds it for a server render and hydration,
|
|
50
|
+
and is mounted by `<Router>` and by both mode runtimes, never by an
|
|
51
|
+
application. `useInterceptedNavigation({claim, load, apply})` returns
|
|
52
|
+
`{ generation }` and is the
|
|
53
|
+
navigation primitive `<Router>` is built on, and the seam the framework
|
|
54
|
+
uses to render server-produced trees instead.
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@k8ordo/router",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The pathname axis of the URL, owned. One route table derives the types, the matching, the links, and the navigation, over the Navigation API and URLPattern.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"navigation-api",
|
|
7
|
+
"react",
|
|
8
|
+
"router",
|
|
9
|
+
"routing",
|
|
10
|
+
"server-components",
|
|
11
|
+
"typescript",
|
|
12
|
+
"urlpattern"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://ordo.k8o.me",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/k35o/k8ordo/issues"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "k8o <kosakanoki@gmail.com>",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/k35o/k8ordo.git"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist/**",
|
|
26
|
+
"docs/**"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.mts",
|
|
33
|
+
"default": "./dist/index.mjs"
|
|
34
|
+
},
|
|
35
|
+
"./package.json": "./package.json"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public",
|
|
39
|
+
"registry": "https://registry.npmjs.org"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@arethetypeswrong/cli": "0.18.5",
|
|
43
|
+
"@types/react": "19.2.18",
|
|
44
|
+
"@vitejs/plugin-react": "6.0.5",
|
|
45
|
+
"@vitest/browser-playwright": "4.1.10",
|
|
46
|
+
"publint": "0.3.23",
|
|
47
|
+
"react": "19.2.8",
|
|
48
|
+
"react-dom": "19.2.8",
|
|
49
|
+
"typescript": "7.0.2",
|
|
50
|
+
"vite": "8.2.1",
|
|
51
|
+
"vite-plus": "0.2.9",
|
|
52
|
+
"vitest": "4.1.10",
|
|
53
|
+
"vitest-browser-react": "2.2.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@types/react": ">=19.2.18",
|
|
57
|
+
"react": ">=19.2.6",
|
|
58
|
+
"typescript": ">=7.0.2"
|
|
59
|
+
},
|
|
60
|
+
"peerDependenciesMeta": {
|
|
61
|
+
"@types/react": {
|
|
62
|
+
"optional": true
|
|
63
|
+
},
|
|
64
|
+
"typescript": {
|
|
65
|
+
"optional": true
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"scripts": {
|
|
69
|
+
"build": "vp pack",
|
|
70
|
+
"test": "vp test",
|
|
71
|
+
"typecheck": "tsc --noEmit",
|
|
72
|
+
"check": "vp check",
|
|
73
|
+
"check:write": "vp check --fix",
|
|
74
|
+
"check:package": "publint && attw --pack . --profile esm-only"
|
|
75
|
+
}
|
|
76
|
+
}
|