@k8ordo/router 0.1.0 → 1.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/README.md +18 -9
- package/dist/base.d.mts +16 -0
- package/dist/base.mjs +45 -0
- package/dist/boundary.d.mts +13 -9
- package/dist/boundary.mjs +19 -6
- package/dist/define-routes.d.mts +22 -17
- package/dist/define-routes.mjs +15 -2
- package/dist/index.d.mts +9 -7
- package/dist/index.mjs +6 -4
- package/dist/links.d.mts +42 -12
- package/dist/links.mjs +44 -10
- package/dist/location.d.mts +22 -6
- package/dist/location.mjs +29 -6
- package/dist/match.d.mts +13 -5
- package/dist/match.mjs +8 -3
- package/dist/navigation.d.mts +61 -19
- package/dist/navigation.mjs +107 -29
- package/dist/not-found.d.mts +11 -0
- package/dist/not-found.mjs +28 -0
- package/dist/paths.d.mts +35 -18
- package/dist/register.d.mts +83 -12
- package/dist/router.d.mts +14 -8
- package/dist/router.mjs +29 -14
- package/docs/GUIDE.md +300 -58
- package/docs/llms.txt +74 -18
- package/package.json +17 -14
package/README.md
CHANGED
|
@@ -21,18 +21,27 @@ npm install @k8ordo/router
|
|
|
21
21
|
pnpm add @k8ordo/router
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Under `@k8ordo/
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
Under `@k8ordo/framework` this package is what the framework builds on: the
|
|
25
|
+
table is generated from `src/routes/`, and what an application uses of it is
|
|
26
|
+
the half that needs no table — `href`, `navigateTo` and `bindParams`;
|
|
27
|
+
`usePathname`, `useMatch` and `matchPath` — plus the route files' props types,
|
|
28
|
+
`PageProps` and `LayoutProps`. There it is installed as the framework's peer,
|
|
29
|
+
and the application imports that half from `@k8ordo/framework`, which
|
|
30
|
+
re-exports it. On its own it is the whole router for an application that
|
|
31
|
+
renders in the browser.
|
|
28
32
|
|
|
29
33
|
## Peer Dependencies
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
|
34
|
-
|
|
|
35
|
-
|
|
|
35
|
+
<!-- peers -->
|
|
36
|
+
|
|
37
|
+
| Package | Version | Required | Needed for |
|
|
38
|
+
| -------------- | ------- | -------- | ------------------------------- |
|
|
39
|
+
| `react` | ≥19.3.0 | yes | `<Router>` and the hooks |
|
|
40
|
+
| `react-dom` | ≥19.3.0 | yes | `BrowserPathname` (`browser()`) |
|
|
41
|
+
| `typescript` | ≥7.0.0 | optional | the shipped type declarations |
|
|
42
|
+
| `@types/react` | ≥19.3.0 | optional | the shipped type declarations |
|
|
43
|
+
|
|
44
|
+
<!-- /peers -->
|
|
36
45
|
|
|
37
46
|
No runtime dependencies. The Navigation API and URLPattern are the platform's.
|
|
38
47
|
|
package/dist/base.d.mts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/base.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A pathname in the table's terms, as the URL spells it: the base in front.
|
|
4
|
+
* Under `base: '/docs/'`, `/products` → `/docs/products` and `/` → `/docs/`.
|
|
5
|
+
* `href` and `navigateTo` build every link through it, so a table written
|
|
6
|
+
* from the root works wherever the application is served.
|
|
7
|
+
*/
|
|
8
|
+
export declare const withBase: (pathname: string, base?: string) => string;
|
|
9
|
+
/**
|
|
10
|
+
* The inverse: a URL's pathname in the table's terms, or `null` when it is
|
|
11
|
+
* not under the base — a URL the application does not serve. Under
|
|
12
|
+
* `base: '/docs/'`, `/docs/products` → `/products`, and `/docs` and `/docs/`
|
|
13
|
+
* → `/`.
|
|
14
|
+
*/
|
|
15
|
+
export declare const withoutBase: (pathname: string, base?: string) => string | null;
|
|
16
|
+
//#endregion
|
package/dist/base.mjs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { normalizePathname } from "./paths.mjs";
|
|
2
|
+
//#region src/base.ts
|
|
3
|
+
/**
|
|
4
|
+
* Where Vite serves the application — its `base`, as `import.meta.env.BASE_URL`
|
|
5
|
+
* spells it (`/`, `/docs/`). Read when a caller does not pass a base: code
|
|
6
|
+
* Node loads without Vite (the static build, `serve`) has no `import.meta.env`
|
|
7
|
+
* and passes the configured base instead.
|
|
8
|
+
*/
|
|
9
|
+
const viteBase = () => import.meta.env.BASE_URL;
|
|
10
|
+
/**
|
|
11
|
+
* The base as a prefix of the URL's pathname: `''` at the root, `/docs` under
|
|
12
|
+
* `/docs/`. Only a path names one — Vite's relative `./` does not say where
|
|
13
|
+
* the pages are, so it adds nothing.
|
|
14
|
+
*/
|
|
15
|
+
const prefixOf = (base) => {
|
|
16
|
+
if (!base.startsWith("/")) return "";
|
|
17
|
+
const prefix = normalizePathname(base);
|
|
18
|
+
return prefix === "/" ? "" : prefix;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* A pathname in the table's terms, as the URL spells it: the base in front.
|
|
22
|
+
* Under `base: '/docs/'`, `/products` → `/docs/products` and `/` → `/docs/`.
|
|
23
|
+
* `href` and `navigateTo` build every link through it, so a table written
|
|
24
|
+
* from the root works wherever the application is served.
|
|
25
|
+
*/
|
|
26
|
+
const withBase = (pathname, base = viteBase()) => {
|
|
27
|
+
const prefix = prefixOf(base);
|
|
28
|
+
if (prefix === "") return pathname;
|
|
29
|
+
return pathname === "/" ? `${prefix}/` : `${prefix}${pathname}`;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The inverse: a URL's pathname in the table's terms, or `null` when it is
|
|
33
|
+
* not under the base — a URL the application does not serve. Under
|
|
34
|
+
* `base: '/docs/'`, `/docs/products` → `/products`, and `/docs` and `/docs/`
|
|
35
|
+
* → `/`.
|
|
36
|
+
*/
|
|
37
|
+
const withoutBase = (pathname, base = viteBase()) => {
|
|
38
|
+
const prefix = prefixOf(base);
|
|
39
|
+
if (prefix === "") return pathname;
|
|
40
|
+
if (pathname === prefix) return "/";
|
|
41
|
+
if (!pathname.startsWith(`${prefix}/`)) return null;
|
|
42
|
+
return pathname.slice(prefix.length);
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
45
|
+
export { withBase, withoutBase };
|
package/dist/boundary.d.mts
CHANGED
|
@@ -4,21 +4,25 @@ import { ComponentType, ReactNode } from "react";
|
|
|
4
4
|
* What a route table's `error` component receives: what was thrown, and a
|
|
5
5
|
* way to try the subtree again in place.
|
|
6
6
|
*/
|
|
7
|
-
type ErrorProps = {
|
|
7
|
+
export type ErrorProps = {
|
|
8
8
|
readonly error: unknown;
|
|
9
9
|
readonly reset: () => void;
|
|
10
10
|
};
|
|
11
|
-
type ErrorComponent = ComponentType<ErrorProps>;
|
|
12
|
-
type
|
|
11
|
+
export type ErrorComponent = ComponentType<ErrorProps>;
|
|
12
|
+
type BoundaryProps = {
|
|
13
13
|
readonly fallback: ErrorComponent;
|
|
14
14
|
readonly children?: ReactNode;
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
17
|
-
* The boundary a table's `error` component renders inside.
|
|
18
|
-
*
|
|
19
|
-
* leaves
|
|
20
|
-
* the tree arrives and would
|
|
17
|
+
* The boundary a table's `error` component renders inside. The navigation
|
|
18
|
+
* that puts the next tree on screen clears the failure, so leaving the page
|
|
19
|
+
* that failed leaves it behind — and not the pathname, which commits before
|
|
20
|
+
* the tree arrives and would clear it onto the old tree.
|
|
21
|
+
*
|
|
22
|
+
* Cleared, not keyed: a key would remount everything below the boundary on
|
|
23
|
+
* every page change, failed or not, and a root boundary would take the whole
|
|
24
|
+
* application's layouts with it.
|
|
21
25
|
*/
|
|
22
|
-
declare const RouteErrorBoundary: ({ fallback, children }:
|
|
26
|
+
export declare const RouteErrorBoundary: ({ fallback, children }: BoundaryProps) => ReactNode;
|
|
23
27
|
//#endregion
|
|
24
|
-
export {
|
|
28
|
+
export {};
|
package/dist/boundary.mjs
CHANGED
|
@@ -5,7 +5,8 @@ import { Component, Suspense, createElement, use } from "react";
|
|
|
5
5
|
var Catch = class extends Component {
|
|
6
6
|
state = {
|
|
7
7
|
error: void 0,
|
|
8
|
-
failed: false
|
|
8
|
+
failed: false,
|
|
9
|
+
generation: this.props.generation
|
|
9
10
|
};
|
|
10
11
|
static getDerivedStateFromError(error) {
|
|
11
12
|
return {
|
|
@@ -13,6 +14,14 @@ var Catch = class extends Component {
|
|
|
13
14
|
failed: true
|
|
14
15
|
};
|
|
15
16
|
}
|
|
17
|
+
static getDerivedStateFromProps(props, state) {
|
|
18
|
+
if (props.generation === state.generation) return null;
|
|
19
|
+
return {
|
|
20
|
+
error: void 0,
|
|
21
|
+
failed: false,
|
|
22
|
+
generation: props.generation
|
|
23
|
+
};
|
|
24
|
+
}
|
|
16
25
|
reset = () => {
|
|
17
26
|
this.setState({
|
|
18
27
|
error: void 0,
|
|
@@ -28,15 +37,19 @@ var Catch = class extends Component {
|
|
|
28
37
|
}
|
|
29
38
|
};
|
|
30
39
|
/**
|
|
31
|
-
* The boundary a table's `error` component renders inside.
|
|
32
|
-
*
|
|
33
|
-
* leaves
|
|
34
|
-
* the tree arrives and would
|
|
40
|
+
* The boundary a table's `error` component renders inside. The navigation
|
|
41
|
+
* that puts the next tree on screen clears the failure, so leaving the page
|
|
42
|
+
* that failed leaves it behind — and not the pathname, which commits before
|
|
43
|
+
* the tree arrives and would clear it onto the old tree.
|
|
44
|
+
*
|
|
45
|
+
* Cleared, not keyed: a key would remount everything below the boundary on
|
|
46
|
+
* every page change, failed or not, and a root boundary would take the whole
|
|
47
|
+
* application's layouts with it.
|
|
35
48
|
*/
|
|
36
49
|
const RouteErrorBoundary = ({ fallback, children }) => {
|
|
37
50
|
const generation = use(NavigationGeneration);
|
|
38
51
|
return createElement(Suspense, { fallback: null }, createElement(Catch, {
|
|
39
|
-
|
|
52
|
+
generation,
|
|
40
53
|
fallback
|
|
41
54
|
}, children));
|
|
42
55
|
};
|
package/dist/define-routes.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ErrorComponent } from "./boundary.mjs";
|
|
2
|
-
import { Join,
|
|
2
|
+
import { Join, PathMatching } from "./paths.mjs";
|
|
3
3
|
import { ComponentType, ReactNode } from "react";
|
|
4
4
|
//#region src/define-routes.d.ts
|
|
5
5
|
/**
|
|
@@ -9,41 +9,46 @@ import { ComponentType, ReactNode } from "react";
|
|
|
9
9
|
* their `children` — so the stored type has to admit them all. `never` props
|
|
10
10
|
* is the type-safe spelling of that; each renderer states what it passes.
|
|
11
11
|
*/
|
|
12
|
-
type RouteComponent = ComponentType<never>;
|
|
12
|
+
export type RouteComponent = ComponentType<never>;
|
|
13
13
|
/**
|
|
14
14
|
* A leaf renders; a branch wraps its children in an optional layout, and
|
|
15
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
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* throws — inside the layout, so the frame survives the failure — and a
|
|
17
|
+
* `loading` component to show while what is below suspends. Lazy components
|
|
18
|
+
* (`React.lazy`) are objects, not functions, so a branch is recognized by
|
|
19
|
+
* its `children` key rather than by `typeof`.
|
|
19
20
|
*/
|
|
20
|
-
type RouteNode = RouteComponent | {
|
|
21
|
+
export type RouteNode = RouteComponent | {
|
|
21
22
|
layout?: RouteComponent;
|
|
22
23
|
error?: ErrorComponent;
|
|
24
|
+
loading?: ComponentType;
|
|
23
25
|
children: RoutesRecord;
|
|
24
26
|
};
|
|
25
|
-
type RoutesRecord = Record<`/${string}`, RouteNode>;
|
|
27
|
+
export type RoutesRecord = Record<`/${string}`, RouteNode>;
|
|
26
28
|
type Below<Prefix extends string, Key extends string> = Join<Prefix, Key> extends '/' ? '' : Join<Prefix, Key>;
|
|
27
29
|
type PatternsIn<Record_, Prefix extends string> = { [K in keyof Record_ & string]: Record_[K] extends {
|
|
28
30
|
children: infer C;
|
|
29
31
|
} ? PatternsIn<C, Below<Prefix, K>> : Join<Prefix, K>; }[keyof Record_ & string];
|
|
30
32
|
/** Every full leaf pattern in the table, as written. */
|
|
31
|
-
type PatternOf<R extends RoutesRecord> = PatternsIn<R, ''> & string;
|
|
33
|
+
export type PatternOf<R extends RoutesRecord> = PatternsIn<R, ''> & string;
|
|
32
34
|
/** Patterns a link can point at — wildcards are matched, never linked. */
|
|
33
|
-
type NavigablePatternOf<R extends RoutesRecord> = Exclude<PatternOf<R>, `${string}*${string}`>;
|
|
35
|
+
export type NavigablePatternOf<R extends RoutesRecord> = Exclude<PatternOf<R>, `${string}*${string}`>;
|
|
34
36
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
+
* `Path` when one of the table's navigable patterns matches it, segment by
|
|
38
|
+
* segment — a literal spelled as the pattern spells it, a `:param` taking any
|
|
39
|
+
* one non-empty segment — and `never` when none does. What `@k8ordo/state`
|
|
40
|
+
* checks a path against with the `routes` its `Register` is given, and what
|
|
41
|
+
* any other typed-path consumer takes.
|
|
37
42
|
*/
|
|
38
|
-
type
|
|
39
|
-
type Match = {
|
|
43
|
+
export type NavigablePath<D, Path extends string> = D extends Routes<infer R extends RoutesRecord> ? PathMatching<Path, NavigablePatternOf<R>> : never;
|
|
44
|
+
export type Match = {
|
|
40
45
|
/** The full pattern that won, as written in the table. */
|
|
41
46
|
pattern: string;
|
|
42
47
|
params: Readonly<Record<string, string>>;
|
|
43
48
|
/** Layouts outer-first, the leaf last. */
|
|
44
49
|
stack: readonly RouteComponent[];
|
|
45
50
|
};
|
|
46
|
-
type Routes<R extends RoutesRecord = RoutesRecord> = {
|
|
51
|
+
export type Routes<R extends RoutesRecord = RoutesRecord> = {
|
|
47
52
|
kind: 'routes';
|
|
48
53
|
/** The table as passed. */
|
|
49
54
|
record: R;
|
|
@@ -61,7 +66,7 @@ type Routes<R extends RoutesRecord = RoutesRecord> = {
|
|
|
61
66
|
*/
|
|
62
67
|
match: (pathname: string, accept?: (match: Match) => boolean) => Match | null;
|
|
63
68
|
};
|
|
64
|
-
declare const setBoundaryOutlet: (render: () => ReactNode) => void;
|
|
65
|
-
declare function defineRoutes<R extends RoutesRecord>(record: R): Routes<R>;
|
|
69
|
+
export declare const setBoundaryOutlet: (render: () => ReactNode) => void;
|
|
70
|
+
export declare function defineRoutes<R extends RoutesRecord>(record: R): Routes<R>;
|
|
66
71
|
//#endregion
|
|
67
|
-
export {
|
|
72
|
+
export {};
|
package/dist/define-routes.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isGroupKey, joinPattern, normalizePathname } from "./paths.mjs";
|
|
2
2
|
import { RouteErrorBoundary } from "./boundary.mjs";
|
|
3
|
-
import { createElement } from "react";
|
|
3
|
+
import { Suspense, createElement } from "react";
|
|
4
4
|
//#region src/define-routes.ts
|
|
5
5
|
const isBranch = (node) => typeof node === "object" && "children" in node;
|
|
6
6
|
/**
|
|
@@ -14,6 +14,18 @@ const boundaryFor = (fallback) => {
|
|
|
14
14
|
const Boundary = ({ children }) => createElement(RouteErrorBoundary, { fallback }, children === void 0 ? outlet() : children);
|
|
15
15
|
return Boundary;
|
|
16
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* A branch's `loading` becomes an element of the stack too — after the
|
|
19
|
+
* layout and inside the error boundary, so what the boundary catches is what
|
|
20
|
+
* the fallback stood in for. A plain `<Suspense>`: a page change under one
|
|
21
|
+
* already showing keeps the page on screen while the next one loads, as
|
|
22
|
+
* every page change does, and the fallback shows while it first streams in
|
|
23
|
+
* and when a navigation mounts it anew.
|
|
24
|
+
*/
|
|
25
|
+
const loadingFor = (Fallback) => {
|
|
26
|
+
const Loading = ({ children }) => createElement(Suspense, { fallback: createElement(Fallback) }, children === void 0 ? outlet() : children);
|
|
27
|
+
return Loading;
|
|
28
|
+
};
|
|
17
29
|
const decode = (value) => {
|
|
18
30
|
try {
|
|
19
31
|
return decodeURIComponent(value);
|
|
@@ -43,7 +55,8 @@ function defineRoutes(record) {
|
|
|
43
55
|
const below = [
|
|
44
56
|
...stack,
|
|
45
57
|
...node.layout === void 0 ? [] : [node.layout],
|
|
46
|
-
...node.error === void 0 ? [] : [boundaryFor(node.error)]
|
|
58
|
+
...node.error === void 0 ? [] : [boundaryFor(node.error)],
|
|
59
|
+
...node.loading === void 0 ? [] : [loadingFor(node.loading)]
|
|
47
60
|
];
|
|
48
61
|
walk(node.children, pattern === "/" ? "" : pattern, below);
|
|
49
62
|
} else {
|
package/dist/index.d.mts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { withBase, withoutBase } from "./base.mjs";
|
|
1
2
|
import { ErrorComponent, ErrorProps } from "./boundary.mjs";
|
|
2
3
|
import { ParamValue, ParamsOf, ParamsSchemaFor, ParsedParams, ParsedParamsMap, PathFor, SchemaOutput, StandardSchemaLike, normalizePathname } from "./paths.mjs";
|
|
3
|
-
import { Match, NavigablePatternOf, PatternOf, RouteComponent, RouteNode,
|
|
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 {
|
|
4
|
+
import { Match, NavigablePath, NavigablePatternOf, PatternOf, RouteComponent, RouteNode, Routes, RoutesRecord, defineRoutes } from "./define-routes.mjs";
|
|
5
|
+
import { LayoutProps, PageProps, Register, RegisteredNavigablePattern, RegisteredPageParams, RegisteredParams, RegisteredPattern, RouteContext } from "./register.mjs";
|
|
6
|
+
import { BoundLinks, BoundParams, NavigateToOptions, bindParams, href, navigateTo } from "./links.mjs";
|
|
7
|
+
import { BrowserPathname, PathnameProvider, usePathname } from "./location.mjs";
|
|
8
|
+
import { MatchOptions, MatchablePattern, matchPath, useMatch } from "./match.mjs";
|
|
9
|
+
import { isNotFound, notFound } from "./not-found.mjs";
|
|
10
|
+
import { NavigationGeneration, NavigationHandler, useInterceptedNavigation, usePendingPathname } from "./navigation.mjs";
|
|
9
11
|
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
|
|
12
|
+
export { type BoundLinks, type BoundParams, BrowserPathname, type ErrorComponent, type ErrorProps, type LayoutProps, type Match, type MatchOptions, type MatchablePattern, type NavigablePath, type NavigablePatternOf, type NavigateToOptions, NavigationGeneration, type NavigationHandler, Outlet, type PageProps, type ParamValue, type ParamsOf, type ParamsSchemaFor, type ParsedParams, type ParsedParamsMap, type PathFor, PathnameProvider, type PatternOf, type Register, type RegisteredNavigablePattern, type RegisteredPageParams, type RegisteredParams, type RegisteredPattern, type RouteComponent, type RouteContext, type RouteNode, Router, type Routes, type RoutesRecord, type SchemaOutput, type StandardSchemaLike, bindParams, defineRoutes, href, isNotFound, matchPath, navigateTo, normalizePathname, notFound, useInterceptedNavigation, useMatch, useParams, usePathname, usePendingPathname, useRoute, withBase, withoutBase };
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { normalizePathname } from "./paths.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { withBase, withoutBase } from "./base.mjs";
|
|
3
|
+
import { NavigationGeneration, useInterceptedNavigation, usePendingPathname } from "./navigation.mjs";
|
|
3
4
|
import { defineRoutes } from "./define-routes.mjs";
|
|
4
|
-
import { href, navigateTo } from "./links.mjs";
|
|
5
|
-
import { PathnameProvider, usePathname } from "./location.mjs";
|
|
5
|
+
import { bindParams, href, navigateTo } from "./links.mjs";
|
|
6
|
+
import { BrowserPathname, PathnameProvider, usePathname } from "./location.mjs";
|
|
6
7
|
import { matchPath, useMatch } from "./match.mjs";
|
|
8
|
+
import { isNotFound, notFound } from "./not-found.mjs";
|
|
7
9
|
import { Outlet, Router, useParams, useRoute } from "./router.mjs";
|
|
8
|
-
export { NavigationGeneration, Outlet, PathnameProvider, Router, defineRoutes, href, matchPath, navigateTo, normalizePathname, useInterceptedNavigation, useMatch, useParams, usePathname, useRoute };
|
|
10
|
+
export { BrowserPathname, NavigationGeneration, Outlet, PathnameProvider, Router, bindParams, defineRoutes, href, isNotFound, matchPath, navigateTo, normalizePathname, notFound, useInterceptedNavigation, useMatch, useParams, usePathname, usePendingPathname, useRoute, withBase, withoutBase };
|
package/dist/links.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ParamValue } from "./paths.mjs";
|
|
2
2
|
import { RegisteredNavigablePattern, RegisteredParams } from "./register.mjs";
|
|
3
3
|
//#region src/links.d.ts
|
|
4
4
|
type HrefArgs<Params> = keyof Params extends never ? [] : [params: Readonly<Params & Record<never, never>>];
|
|
5
|
-
type NavigateToOptions = {
|
|
5
|
+
export type NavigateToOptions = {
|
|
6
6
|
/**
|
|
7
7
|
* `push` by default: going to a page is what the back button undoes —
|
|
8
8
|
* the opposite default from a state refinement.
|
|
@@ -11,21 +11,51 @@ type NavigateToOptions = {
|
|
|
11
11
|
};
|
|
12
12
|
type NavigateToArgs<Params> = keyof Params extends never ? [options?: NavigateToOptions] : [params: Readonly<Params & Record<never, never>>, options?: NavigateToOptions];
|
|
13
13
|
/**
|
|
14
|
-
* Builds a
|
|
15
|
-
* involved, which is why pages never import one. Params are inferred
|
|
16
|
-
* the pattern literal itself; `Register` adds the check that the
|
|
17
|
-
* exists in the app's table, and — under the framework — the type a
|
|
18
|
-
* schema gave each param, so a link takes what the page received.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
14
|
+
* Builds the URL a link points at from a pattern and its params — no route
|
|
15
|
+
* table involved, which is why pages never import one. Params are inferred
|
|
16
|
+
* from the pattern literal itself; `Register` adds the check that the
|
|
17
|
+
* pattern exists in the app's table, and — under the framework — the type a
|
|
18
|
+
* page's schema gave each param, so a link takes what the page received.
|
|
19
|
+
*
|
|
20
|
+
* What comes back is a URL, with Vite's `base` in front (`withBase`), not a
|
|
21
|
+
* pathname in the table's terms — which is why it is typed `string`: handed
|
|
22
|
+
* to something that adds the base itself, it would carry it twice.
|
|
21
23
|
*/
|
|
22
|
-
declare const href: <P extends RegisteredNavigablePattern>(pattern: P, ...params: HrefArgs<RegisteredParams<P>>) =>
|
|
24
|
+
export declare const href: <P extends RegisteredNavigablePattern>(pattern: P, ...params: HrefArgs<RegisteredParams<P>>) => string;
|
|
23
25
|
/**
|
|
24
26
|
* Typed imperative navigation: `href` composed with
|
|
25
27
|
* `navigation.navigate()`, returning the platform's own
|
|
26
28
|
* `{ committed, finished }`. Changing pages goes through here; changing
|
|
27
29
|
* state goes through `@k8ordo/state`'s `update()`.
|
|
28
30
|
*/
|
|
29
|
-
declare const navigateTo: <P extends RegisteredNavigablePattern>(pattern: P, ...rest: NavigateToArgs<RegisteredParams<P>>) => ReturnType<Navigation['navigate']>;
|
|
31
|
+
export declare const navigateTo: <P extends RegisteredNavigablePattern>(pattern: P, ...rest: NavigateToArgs<RegisteredParams<P>>) => ReturnType<Navigation['navigate']>;
|
|
32
|
+
/** What a params source supplies: values with one URL spelling, by name. */
|
|
33
|
+
export type BoundParams = Readonly<Record<string, ParamValue>>;
|
|
34
|
+
/**
|
|
35
|
+
* The params a bound link still takes: what the source does not supply is
|
|
36
|
+
* required as before, and what it does supply may be given to override it.
|
|
37
|
+
*/
|
|
38
|
+
type Unbound<Params, Bound> = Omit<Params, keyof Bound> & Partial<Pick<Params, Extract<keyof Bound, keyof Params>>>;
|
|
39
|
+
type BoundHrefArgs<Params, Bound> = Exclude<keyof Params, keyof Bound> extends never ? [params?: Readonly<Partial<Params> & Record<never, never>>] : [params: Readonly<Unbound<Params, Bound> & Record<never, never>>];
|
|
40
|
+
type BoundNavigateToArgs<Params, Bound> = keyof Params extends never ? [options?: NavigateToOptions] : [...BoundHrefArgs<Params, Bound>, options?: NavigateToOptions];
|
|
41
|
+
export type BoundLinks<Bound extends BoundParams> = {
|
|
42
|
+
readonly href: <P extends RegisteredNavigablePattern>(pattern: P, ...rest: BoundHrefArgs<RegisteredParams<P>, Bound>) => string;
|
|
43
|
+
readonly navigateTo: <P extends RegisteredNavigablePattern>(pattern: P, ...rest: BoundNavigateToArgs<RegisteredParams<P>, Bound>) => ReturnType<Navigation['navigate']>;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* `href` and `navigateTo` with some params supplied by a function instead of
|
|
47
|
+
* by every call — a segment the whole application shares, such as a locale
|
|
48
|
+
* or a tenant. The source is read at each call, so a value that differs per
|
|
49
|
+
* request or per URL is read where it is current. Patterns keep their full
|
|
50
|
+
* spelling (`'/:locale/products'`), so the table's types apply unchanged and
|
|
51
|
+
* a bound param can still be given to point at another value.
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* const { href, navigateTo } = bindParams(() => ({ locale: locales.getLocale() }));
|
|
55
|
+
* href('/:locale/products/:id', { id }); // locale comes from the source
|
|
56
|
+
* navigateTo('/:locale', { locale: 'en' }, { history: 'replace' }); // or is overridden
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export declare const bindParams: <const Bound extends BoundParams>(source: () => Bound) => BoundLinks<Bound>;
|
|
30
60
|
//#endregion
|
|
31
|
-
export {
|
|
61
|
+
export {};
|
package/dist/links.mjs
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { buildHref } from "./paths.mjs";
|
|
2
|
+
import { withBase } from "./base.mjs";
|
|
2
3
|
//#region src/links.ts
|
|
4
|
+
const navigate = (pattern, params, options) => navigation.navigate(withBase(buildHref(pattern, params)), { history: options?.history ?? "push" });
|
|
3
5
|
/**
|
|
4
|
-
* Builds a
|
|
5
|
-
* involved, which is why pages never import one. Params are inferred
|
|
6
|
-
* the pattern literal itself; `Register` adds the check that the
|
|
7
|
-
* exists in the app's table, and — under the framework — the type a
|
|
8
|
-
* schema gave each param, so a link takes what the page received.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* Builds the URL a link points at from a pattern and its params — no route
|
|
7
|
+
* table involved, which is why pages never import one. Params are inferred
|
|
8
|
+
* from the pattern literal itself; `Register` adds the check that the
|
|
9
|
+
* pattern exists in the app's table, and — under the framework — the type a
|
|
10
|
+
* page's schema gave each param, so a link takes what the page received.
|
|
11
|
+
*
|
|
12
|
+
* What comes back is a URL, with Vite's `base` in front (`withBase`), not a
|
|
13
|
+
* pathname in the table's terms — which is why it is typed `string`: handed
|
|
14
|
+
* to something that adds the base itself, it would carry it twice.
|
|
11
15
|
*/
|
|
12
|
-
const href = (pattern, ...params) => buildHref(pattern, params[0]);
|
|
16
|
+
const href = (pattern, ...params) => withBase(buildHref(pattern, params[0]));
|
|
13
17
|
/**
|
|
14
18
|
* Typed imperative navigation: `href` composed with
|
|
15
19
|
* `navigation.navigate()`, returning the platform's own
|
|
@@ -21,7 +25,37 @@ const navigateTo = (pattern, ...rest) => {
|
|
|
21
25
|
const args = rest;
|
|
22
26
|
const params = wantsParams ? args[0] : void 0;
|
|
23
27
|
const options = wantsParams ? args[1] : args[0];
|
|
24
|
-
return
|
|
28
|
+
return navigate(pattern, params, options);
|
|
25
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* `href` and `navigateTo` with some params supplied by a function instead of
|
|
32
|
+
* by every call — a segment the whole application shares, such as a locale
|
|
33
|
+
* or a tenant. The source is read at each call, so a value that differs per
|
|
34
|
+
* request or per URL is read where it is current. Patterns keep their full
|
|
35
|
+
* spelling (`'/:locale/products'`), so the table's types apply unchanged and
|
|
36
|
+
* a bound param can still be given to point at another value.
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* const { href, navigateTo } = bindParams(() => ({ locale: locales.getLocale() }));
|
|
40
|
+
* href('/:locale/products/:id', { id }); // locale comes from the source
|
|
41
|
+
* navigateTo('/:locale', { locale: 'en' }, { history: 'replace' }); // or is overridden
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
const bindParams = (source) => ({
|
|
45
|
+
href: (pattern, ...rest) => withBase(buildHref(pattern, {
|
|
46
|
+
...source(),
|
|
47
|
+
...rest[0]
|
|
48
|
+
})),
|
|
49
|
+
navigateTo: (pattern, ...rest) => {
|
|
50
|
+
const wantsParams = pattern.includes(":");
|
|
51
|
+
const args = rest;
|
|
52
|
+
const params = wantsParams ? args[0] : void 0;
|
|
53
|
+
const options = wantsParams ? args[1] : args[0];
|
|
54
|
+
return navigate(pattern, {
|
|
55
|
+
...source(),
|
|
56
|
+
...params
|
|
57
|
+
}, options);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
26
60
|
//#endregion
|
|
27
|
-
export { href, navigateTo };
|
|
61
|
+
export { bindParams, href, navigateTo };
|
package/dist/location.d.mts
CHANGED
|
@@ -8,13 +8,30 @@ import { ReactNode } from "react";
|
|
|
8
8
|
* `<Router>` supplies this on its own. Under the framework the mode's runtime
|
|
9
9
|
* supplies it; an application never writes this itself.
|
|
10
10
|
*/
|
|
11
|
-
declare const PathnameProvider: ({ pathname, children }: {
|
|
11
|
+
export declare const PathnameProvider: ({ pathname, children }: {
|
|
12
12
|
pathname: string;
|
|
13
13
|
children: ReactNode;
|
|
14
14
|
}) => ReactNode;
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Below this, a server render has no pathname to give: `usePathname` — and
|
|
17
|
+
* `useMatch`, which reads it — asks for the browser with `use(browser())`, so
|
|
18
|
+
* the nearest `<Suspense>` is left for the browser to render. Under
|
|
19
|
+
* `@k8ordo/framework` a fallback.tsx renders inside it; an application never
|
|
20
|
+
* writes this itself.
|
|
21
|
+
*/
|
|
22
|
+
export declare const BrowserPathname: ({ children }: {
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
}) => ReactNode;
|
|
25
|
+
/**
|
|
26
|
+
* The document's pathname in the table's terms — Vite's `base` removed. A
|
|
27
|
+
* document outside its own base is a deployment that went wrong, and the
|
|
28
|
+
* pathname is shown as it is rather than failing the render over it.
|
|
29
|
+
*/
|
|
30
|
+
export declare const appPathname: () => string;
|
|
31
|
+
/**
|
|
32
|
+
* Where the browser currently is, as a pathname in the table's terms — the
|
|
33
|
+
* one axis this package owns, with Vite's `base` removed so it compares with
|
|
34
|
+
* the patterns. It reads the platform rather than a table, so it works the same in a
|
|
18
35
|
* client application and under the framework, where there is no route table
|
|
19
36
|
* in the browser at all.
|
|
20
37
|
*
|
|
@@ -26,6 +43,5 @@ declare const PathnameProvider: ({ pathname, children }: {
|
|
|
26
43
|
* every search change would defeat `@k8ordo/state`'s keyed subscriptions, and
|
|
27
44
|
* the split at the `?` is the whole boundary between the two packages.
|
|
28
45
|
*/
|
|
29
|
-
declare function usePathname(): string;
|
|
30
|
-
//#endregion
|
|
31
|
-
export { PathnameProvider, usePathname };
|
|
46
|
+
export declare function usePathname(): string;
|
|
47
|
+
//#endregion
|
package/dist/location.mjs
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { normalizePathname } from "./paths.mjs";
|
|
3
|
+
import { withoutBase } from "./base.mjs";
|
|
3
4
|
import { createContext, use, useMemo, useSyncExternalStore } from "react";
|
|
5
|
+
import { browser } from "react-dom";
|
|
4
6
|
import { jsx } from "react/jsx-runtime";
|
|
5
7
|
//#region src/location.tsx
|
|
8
|
+
/** Below `BrowserPathname`: the render has no pathname, the browser does. */
|
|
9
|
+
const IN_BROWSER = Symbol("k8ordo.browser-pathname");
|
|
6
10
|
const ServerPathname = createContext(null);
|
|
7
11
|
/**
|
|
8
12
|
* The pathname the render started from. A client component's first render can
|
|
@@ -19,16 +23,33 @@ const PathnameProvider = ({ pathname, children }) => {
|
|
|
19
23
|
children
|
|
20
24
|
});
|
|
21
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* Below this, a server render has no pathname to give: `usePathname` — and
|
|
28
|
+
* `useMatch`, which reads it — asks for the browser with `use(browser())`, so
|
|
29
|
+
* the nearest `<Suspense>` is left for the browser to render. Under
|
|
30
|
+
* `@k8ordo/framework` a fallback.tsx renders inside it; an application never
|
|
31
|
+
* writes this itself.
|
|
32
|
+
*/
|
|
33
|
+
const BrowserPathname = ({ children }) => /* @__PURE__ */ jsx(ServerPathname, {
|
|
34
|
+
value: IN_BROWSER,
|
|
35
|
+
children
|
|
36
|
+
});
|
|
22
37
|
const subscribe = (onChange) => {
|
|
23
38
|
navigation.addEventListener("currententrychange", onChange);
|
|
24
39
|
return () => {
|
|
25
40
|
navigation.removeEventListener("currententrychange", onChange);
|
|
26
41
|
};
|
|
27
42
|
};
|
|
28
|
-
const readPathname = () => normalizePathname(location.pathname);
|
|
29
43
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
44
|
+
* The document's pathname in the table's terms — Vite's `base` removed. A
|
|
45
|
+
* document outside its own base is a deployment that went wrong, and the
|
|
46
|
+
* pathname is shown as it is rather than failing the render over it.
|
|
47
|
+
*/
|
|
48
|
+
const appPathname = () => normalizePathname(withoutBase(location.pathname) ?? location.pathname);
|
|
49
|
+
/**
|
|
50
|
+
* Where the browser currently is, as a pathname in the table's terms — the
|
|
51
|
+
* one axis this package owns, with Vite's `base` removed so it compares with
|
|
52
|
+
* the patterns. It reads the platform rather than a table, so it works the same in a
|
|
32
53
|
* client application and under the framework, where there is no route table
|
|
33
54
|
* in the browser at all.
|
|
34
55
|
*
|
|
@@ -42,10 +63,12 @@ const readPathname = () => normalizePathname(location.pathname);
|
|
|
42
63
|
*/
|
|
43
64
|
function usePathname() {
|
|
44
65
|
const fromServer = use(ServerPathname);
|
|
45
|
-
|
|
46
|
-
|
|
66
|
+
if (fromServer === IN_BROWSER) use(browser("usePathname reads the URL in the browser here: the server render was for a pathname no visitor is at"));
|
|
67
|
+
return useSyncExternalStore(subscribe, appPathname, () => {
|
|
68
|
+
if (fromServer === IN_BROWSER) return appPathname();
|
|
69
|
+
if (fromServer === null) throw new Error("usePathname needs <Router> above it, or a page rendered by @k8ordo/framework");
|
|
47
70
|
return fromServer;
|
|
48
71
|
});
|
|
49
72
|
}
|
|
50
73
|
//#endregion
|
|
51
|
-
export { PathnameProvider, usePathname };
|
|
74
|
+
export { BrowserPathname, PathnameProvider, appPathname, usePathname };
|
package/dist/match.d.mts
CHANGED
|
@@ -7,20 +7,28 @@ import { RegisteredNavigablePattern, RegisteredPattern } from "./register.mjs";
|
|
|
7
7
|
* to know which section of the site is showing. The pattern's own page is
|
|
8
8
|
* not below it: `/x/*` matches `/x/y` and not `/x`.
|
|
9
9
|
*/
|
|
10
|
-
type MatchablePattern = RegisteredPattern | `${RegisteredNavigablePattern}/*`;
|
|
10
|
+
export type MatchablePattern = RegisteredPattern | `${RegisteredNavigablePattern}/*`;
|
|
11
|
+
export type MatchOptions = {
|
|
12
|
+
/**
|
|
13
|
+
* For a `/*` pattern: count the pattern's own page as well, so `/x/*`
|
|
14
|
+
* answers `/x` too — what a section link asks when it wants to be marked
|
|
15
|
+
* on the index as much as below it. Off by default, where `/x/*` is
|
|
16
|
+
* strictly below.
|
|
17
|
+
*/
|
|
18
|
+
readonly inclusive?: boolean;
|
|
19
|
+
};
|
|
11
20
|
/**
|
|
12
21
|
* Whether a pathname is the one a pattern names, and with which params. Pure
|
|
13
22
|
* and table-free — the pattern is checked against `Register`, the pathname
|
|
14
23
|
* is whatever the caller holds — which is why it works where no table is
|
|
15
24
|
* mounted, under the framework included.
|
|
16
25
|
*/
|
|
17
|
-
declare const matchPath: <P extends MatchablePattern>(pattern: P, pathname: string) => ParamsOf<P> | null;
|
|
26
|
+
export declare const matchPath: <P extends MatchablePattern>(pattern: P, pathname: string, options?: MatchOptions) => ParamsOf<P> | null;
|
|
18
27
|
/**
|
|
19
28
|
* `matchPath` against where the browser is. It re-renders when the pathname
|
|
20
29
|
* changes and never on the search, like `usePathname` it is built on — so a
|
|
21
30
|
* sidebar can ask "is a page under `/docs/*` showing" without a table in the
|
|
22
31
|
* browser, which is the question `useRoute` cannot answer under the framework.
|
|
23
32
|
*/
|
|
24
|
-
declare function useMatch<P extends MatchablePattern>(pattern: P): ParamsOf<P> | null;
|
|
25
|
-
//#endregion
|
|
26
|
-
export { MatchablePattern, matchPath, useMatch };
|
|
33
|
+
export declare function useMatch<P extends MatchablePattern>(pattern: P, options?: MatchOptions): ParamsOf<P> | null;
|
|
34
|
+
//#endregion
|