@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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { normalizePathname } from "./paths.mjs";
|
|
3
|
+
import { createContext, startTransition, useEffect, useRef, useState } from "react";
|
|
4
|
+
//#region src/navigation.ts
|
|
5
|
+
/**
|
|
6
|
+
* Whether this navigation is the application's to handle at all.
|
|
7
|
+
*
|
|
8
|
+
* A reload is a request for a fresh document, and a POST submission carries a
|
|
9
|
+
* body only the server can act on: intercepting either means silently doing
|
|
10
|
+
* nothing where the platform would have done the obvious thing. A GET form
|
|
11
|
+
* has no `formData`, so the search-shaped navigations `@k8ordo/state` builds
|
|
12
|
+
* still come through here.
|
|
13
|
+
*/
|
|
14
|
+
const isOurs = (event) => event.canIntercept && !event.hashChange && event.downloadRequest === null && event.navigationType !== "reload" && event.formData === null;
|
|
15
|
+
const scrollPlanFor = (navigationType, hash) => {
|
|
16
|
+
if (navigationType === "traverse") return null;
|
|
17
|
+
if (hash.length > 1) {
|
|
18
|
+
let id = hash.slice(1);
|
|
19
|
+
try {
|
|
20
|
+
id = decodeURIComponent(id);
|
|
21
|
+
} catch {}
|
|
22
|
+
return {
|
|
23
|
+
kind: "fragment",
|
|
24
|
+
id
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return { kind: "top" };
|
|
28
|
+
};
|
|
29
|
+
const applyScroll = (plan) => {
|
|
30
|
+
if (plan.kind === "fragment") {
|
|
31
|
+
const selector = `#${CSS.escape(plan.id)}, [name=${JSON.stringify(plan.id)}]`;
|
|
32
|
+
const target = document.querySelector(selector);
|
|
33
|
+
if (target !== null) {
|
|
34
|
+
target.scrollIntoView();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
window.scrollTo({
|
|
39
|
+
left: 0,
|
|
40
|
+
top: -1
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Which navigation put the tree on screen — a number that changes exactly
|
|
45
|
+
* when a new tree is applied, and not when only the URL moved. It is the
|
|
46
|
+
* identity an error boundary keys on: leaving the page that failed is what
|
|
47
|
+
* should clear the failure, and the URL commits before the tree arrives, so
|
|
48
|
+
* the pathname would clear it one render too early and let the old tree
|
|
49
|
+
* fail again under the new key.
|
|
50
|
+
*/
|
|
51
|
+
const NavigationGeneration = createContext(-1);
|
|
52
|
+
/**
|
|
53
|
+
* The navigation half of the router, on its own: intercept, load, apply in a
|
|
54
|
+
* transition, and resolve the platform's handler only once the new tree is on
|
|
55
|
+
* screen — which is what makes `navigation.navigate().finished` mean "the
|
|
56
|
+
* page is showing", and what `@k8ordo/state`'s `update().finished` inherits.
|
|
57
|
+
*
|
|
58
|
+
* What gets loaded is the caller's business: a route table match for a plain
|
|
59
|
+
* client app, an RSC payload under the framework. Neither has to teach this
|
|
60
|
+
* hook anything about the other.
|
|
61
|
+
*/
|
|
62
|
+
function useInterceptedNavigation(handler) {
|
|
63
|
+
const latest = useRef(handler);
|
|
64
|
+
latest.current = handler;
|
|
65
|
+
const pending = useRef(/* @__PURE__ */ new Map());
|
|
66
|
+
const count = useRef(0);
|
|
67
|
+
const [applied, setApplied] = useState(-1);
|
|
68
|
+
const shown = useRef(null);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
shown.current = normalizePathname(location.pathname);
|
|
71
|
+
const onNavigate = (event) => {
|
|
72
|
+
if (!isOurs(event)) return;
|
|
73
|
+
const url = new URL(event.destination.url);
|
|
74
|
+
const pathname = normalizePathname(url.pathname);
|
|
75
|
+
if (pathname === shown.current) {
|
|
76
|
+
event.intercept({
|
|
77
|
+
scroll: "manual",
|
|
78
|
+
focusReset: "manual"
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (!latest.current.claim(url)) return;
|
|
83
|
+
const id = count.current++;
|
|
84
|
+
const scroll = scrollPlanFor(event.navigationType, url.hash);
|
|
85
|
+
event.intercept({
|
|
86
|
+
scroll: scroll === null ? "after-transition" : "manual",
|
|
87
|
+
handler: async () => {
|
|
88
|
+
const value = await latest.current.load(url, event.signal);
|
|
89
|
+
if (event.signal.aborted) throw event.signal.reason;
|
|
90
|
+
await new Promise((resolve, reject) => {
|
|
91
|
+
pending.current.set(id, {
|
|
92
|
+
resolve,
|
|
93
|
+
scroll
|
|
94
|
+
});
|
|
95
|
+
event.signal.addEventListener("abort", () => {
|
|
96
|
+
pending.current.delete(id);
|
|
97
|
+
reject(event.signal.reason);
|
|
98
|
+
}, { once: true });
|
|
99
|
+
startTransition(() => {
|
|
100
|
+
shown.current = pathname;
|
|
101
|
+
latest.current.apply(value);
|
|
102
|
+
setApplied(id);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
navigation.addEventListener("navigate", onNavigate);
|
|
109
|
+
return () => {
|
|
110
|
+
navigation.removeEventListener("navigate", onNavigate);
|
|
111
|
+
};
|
|
112
|
+
}, []);
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
const entry = pending.current.get(applied);
|
|
115
|
+
if (entry === void 0) return;
|
|
116
|
+
pending.current.delete(applied);
|
|
117
|
+
if (entry.scroll !== null) applyScroll(entry.scroll);
|
|
118
|
+
entry.resolve();
|
|
119
|
+
}, [applied]);
|
|
120
|
+
return { generation: applied };
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
export { NavigationGeneration, isOurs, scrollPlanFor, useInterceptedNavigation };
|
package/dist/paths.d.mts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//#region src/paths.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The type half of the route table: everything here is derived from the
|
|
4
|
+
* pattern strings by inference alone — no code generation. A pattern like
|
|
5
|
+
* `'/:locale/products/:id'` yields its params (`{ locale, id }`), its
|
|
6
|
+
* concrete-path type (`` `/${string}/products/${string}` ``), and its place
|
|
7
|
+
* in the app-wide route union.
|
|
8
|
+
*/
|
|
9
|
+
type SegParam<Segment extends string> = Segment extends `:${infer Name}` ? Record<Name, string> : Record<never, never>;
|
|
10
|
+
type SegListParams<Rest extends string> = Rest extends `${infer Segment}/${infer Tail}` ? SegParam<Segment> & SegListParams<Tail> : SegParam<Rest>;
|
|
11
|
+
/** `'/:locale/products/:id'` → `{ locale: string; id: string }` */
|
|
12
|
+
type ParamsOf<Pattern extends string> = Pattern extends `/${infer Rest}` ? SegListParams<Rest> : Record<never, never>;
|
|
13
|
+
/**
|
|
14
|
+
* The shape every validation library agrees on (Standard Schema): a value
|
|
15
|
+
* under `~standard` whose `types` carry the input and output. Only the
|
|
16
|
+
* output is read here — the router itself never runs a schema; the framework
|
|
17
|
+
* does, and this package only has to type what comes out.
|
|
18
|
+
*/
|
|
19
|
+
type StandardSchemaLike<Output = unknown> = {
|
|
20
|
+
readonly '~standard': {
|
|
21
|
+
readonly types?: {
|
|
22
|
+
readonly output: Output;
|
|
23
|
+
} | undefined;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
/** What a schema produces. */
|
|
27
|
+
type SchemaOutput<Schema> = Schema extends StandardSchemaLike<infer Output> ? Output : never;
|
|
28
|
+
/**
|
|
29
|
+
* A schema a route file may declare for the params its pattern names: its
|
|
30
|
+
* output is an object whose keys are a subset of those params — a key the
|
|
31
|
+
* pattern does not name has nothing to validate.
|
|
32
|
+
*/
|
|
33
|
+
type ParamsSchemaFor<Pattern extends string> = StandardSchemaLike<Partial<Record<keyof ParamsOf<Pattern> & string, unknown>>>;
|
|
34
|
+
type MergedOutput<Schemas extends readonly unknown[]> = Schemas extends readonly [infer Head, ...infer Tail] ? Omit<MergedOutput<Tail>, keyof SchemaOutput<Head>> & SchemaOutput<Head> : Record<never, never>;
|
|
35
|
+
/**
|
|
36
|
+
* A pattern's params after the schemas along its stack have run — outer
|
|
37
|
+
* layouts first, the page last — each replacing the strings it names with
|
|
38
|
+
* what it produced. Params no schema names stay strings.
|
|
39
|
+
*/
|
|
40
|
+
type ParsedParams<Pattern extends string, Schemas extends readonly unknown[]> = Omit<ParamsOf<Pattern>, keyof MergedOutput<Schemas>> & MergedOutput<Schemas>;
|
|
41
|
+
/** `{ '/products/:id': [schema, …] }` → `{ '/products/:id': { id: number } }` */
|
|
42
|
+
type ParsedParamsMap<Schemas extends Record<string, readonly unknown[]>> = { [Pattern in keyof Schemas & string]: ParsedParams<Pattern, Schemas[Pattern]>; };
|
|
43
|
+
type SegPath<Segment extends string> = Segment extends `:${string}` ? string : Segment;
|
|
44
|
+
type SegListPath<Rest extends string> = Rest extends `${infer Segment}/${infer Tail}` ? `${SegPath<Segment>}/${SegListPath<Tail>}` : SegPath<Rest>;
|
|
45
|
+
/** `'/:locale/products/:id'` → `` `/${string}/products/${string}` `` */
|
|
46
|
+
type PathFor<Pattern extends string> = Pattern extends `/${infer Rest}` ? `/${SegListPath<Rest>}` : never;
|
|
47
|
+
/**
|
|
48
|
+
* Keys that address the parent's own place instead of a place below it:
|
|
49
|
+
* `'/'` (the branch's index) and `'/(name)'` (a group, which structures the
|
|
50
|
+
* table — its own layout, its own subtree — without touching the URL).
|
|
51
|
+
*/
|
|
52
|
+
type Transparent<Key extends string> = Key extends '/' ? true : Key extends `/(${string})` ? true : false;
|
|
53
|
+
/** Composes a parent prefix with a child key. */
|
|
54
|
+
type Join<Prefix extends string, Key extends string> = Transparent<Key> extends true ? Prefix extends '' ? '/' : Prefix : `${Prefix}${Key}`;
|
|
55
|
+
declare const isGroupKey: (key: string) => boolean;
|
|
56
|
+
declare const joinPattern: (prefix: string, key: string) => string;
|
|
57
|
+
/**
|
|
58
|
+
* URLPattern treats `/products` and `/products/` as different pathnames; the
|
|
59
|
+
* router does not. Everything is matched against the slashless canonical
|
|
60
|
+
* form, root excepted.
|
|
61
|
+
*/
|
|
62
|
+
declare const normalizePathname: (pathname: string) => string;
|
|
63
|
+
/** What a param value may be on the way into a link: anything with one spelling. */
|
|
64
|
+
type ParamValue = string | number | bigint | boolean;
|
|
65
|
+
declare const buildHref: (pattern: string, params: Readonly<Record<string, unknown>> | undefined) => string;
|
|
66
|
+
//#endregion
|
|
67
|
+
export { Join, ParamValue, ParamsOf, ParamsSchemaFor, ParsedParams, ParsedParamsMap, PathFor, SchemaOutput, StandardSchemaLike, buildHref, isGroupKey, joinPattern, normalizePathname };
|
package/dist/paths.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/paths.ts
|
|
2
|
+
const isGroupKey = (key) => /^\/\([^)]+\)$/u.test(key);
|
|
3
|
+
const joinPattern = (prefix, key) => key === "/" || isGroupKey(key) ? prefix === "" ? "/" : prefix : `${prefix}${key}`;
|
|
4
|
+
/**
|
|
5
|
+
* URLPattern treats `/products` and `/products/` as different pathnames; the
|
|
6
|
+
* router does not. Everything is matched against the slashless canonical
|
|
7
|
+
* form, root excepted.
|
|
8
|
+
*/
|
|
9
|
+
const normalizePathname = (pathname) => {
|
|
10
|
+
let end = pathname.length;
|
|
11
|
+
while (end > 1 && pathname.charAt(end - 1) === "/") end -= 1;
|
|
12
|
+
return pathname.slice(0, end);
|
|
13
|
+
};
|
|
14
|
+
const buildHref = (pattern, params) => {
|
|
15
|
+
if (pattern.includes("*")) throw new TypeError(`"${pattern}" is a wildcard — it has no href`);
|
|
16
|
+
return pattern.split("/").map((segment) => {
|
|
17
|
+
if (!segment.startsWith(":")) return segment;
|
|
18
|
+
const name = segment.slice(1);
|
|
19
|
+
const value = params?.[name];
|
|
20
|
+
if (value === void 0) throw new TypeError(`"${pattern}" needs a value for ":${name}"`);
|
|
21
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "bigint" && typeof value !== "boolean") throw new TypeError(`"${pattern}" got a value for ":${name}" that has no URL spelling`);
|
|
22
|
+
return encodeURIComponent(String(value));
|
|
23
|
+
}).join("/");
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { buildHref, isGroupKey, joinPattern, normalizePathname };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ParamValue, ParamsOf } from "./paths.mjs";
|
|
2
|
+
import { NavigablePatternOf, PatternOf, Routes, RoutesRecord } from "./define-routes.mjs";
|
|
3
|
+
//#region src/register.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The app-side hook for the route table's type. An application augments this
|
|
6
|
+
* once:
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* declare module '@k8ordo/router' {
|
|
10
|
+
* interface Register { routes: typeof routes }
|
|
11
|
+
* }
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* and `href` / `navigateTo` / `useParams` verify their pattern against the
|
|
15
|
+
* table everywhere — without any component importing the table's value.
|
|
16
|
+
* Only `<Router>` touches the value; everything else needs just the string
|
|
17
|
+
* pattern, so the routes-module → pages → routes-module import cycle never
|
|
18
|
+
* forms. Declared as an interface — the one exception to the repository's
|
|
19
|
+
* type-only rule — because declaration merging is the entire mechanism.
|
|
20
|
+
*/
|
|
21
|
+
interface Register {}
|
|
22
|
+
/** Every pattern in the registered table; any `/`-pattern before Register. */
|
|
23
|
+
type RegisteredPattern = Register extends {
|
|
24
|
+
routes: Routes<infer R extends RoutesRecord>;
|
|
25
|
+
} ? PatternOf<R> : `/${string}`;
|
|
26
|
+
/** Linkable patterns of the registered table (wildcards excluded). */
|
|
27
|
+
type RegisteredNavigablePattern = Register extends {
|
|
28
|
+
routes: Routes<infer R extends RoutesRecord>;
|
|
29
|
+
} ? NavigablePatternOf<R> : `/${string}`;
|
|
30
|
+
type RegisteredParamsMap = Register extends {
|
|
31
|
+
params: infer M;
|
|
32
|
+
} ? M : null;
|
|
33
|
+
/** The pattern's params as anything with one spelling, before any schema. */
|
|
34
|
+
type LooseParamsOf<P extends string> = { [K in keyof ParamsOf<P>]: ParamValue; };
|
|
35
|
+
/**
|
|
36
|
+
* A pattern's params as a link takes them: where the registered `params`
|
|
37
|
+
* map — written by the framework from the schemas the route files declared
|
|
38
|
+
* — says a page receives something, a link takes that same value (a number
|
|
39
|
+
* in, a number's one spelling out); a param no schema covers takes anything
|
|
40
|
+
* with one spelling. Before `Register` is augmented — a client application
|
|
41
|
+
* that has not, or the framework's generated file not yet written — the
|
|
42
|
+
* same loose shape applies, so a link written for a schema still compiles.
|
|
43
|
+
*/
|
|
44
|
+
type RegisteredParams<P extends string> = RegisteredParamsMap extends null ? LooseParamsOf<P> : P extends keyof RegisteredParamsMap ? Omit<LooseParamsOf<P>, keyof RegisteredParamsMap[P]> & RegisteredParamsMap[P] : LooseParamsOf<P>;
|
|
45
|
+
//#endregion
|
|
46
|
+
export { Register, RegisteredNavigablePattern, RegisteredParams, RegisteredPattern };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ParamsOf } from "./paths.mjs";
|
|
2
|
+
import { Match, Routes } from "./define-routes.mjs";
|
|
3
|
+
import { RegisteredPattern } from "./register.mjs";
|
|
4
|
+
import { FC, ReactNode } from "react";
|
|
5
|
+
//#region src/router.d.ts
|
|
6
|
+
/** Renders the next element of the matched stack — a layout's hole. */
|
|
7
|
+
declare const Outlet: FC;
|
|
8
|
+
/**
|
|
9
|
+
* Mounts a route table on the Navigation API: every same-origin navigation
|
|
10
|
+
* that the table claims is handled in the client, and the rest is left to the
|
|
11
|
+
* server. Layout components render their children through `<Outlet />`.
|
|
12
|
+
*
|
|
13
|
+
* This is the whole router for an application that renders on the client.
|
|
14
|
+
* Under the framework the tree comes from the server instead, and what is
|
|
15
|
+
* shared is `useInterceptedNavigation` and the `<PathnameProvider>` this
|
|
16
|
+
* mounts.
|
|
17
|
+
*/
|
|
18
|
+
declare function Router({ routes }: {
|
|
19
|
+
routes: Routes;
|
|
20
|
+
}): ReactNode;
|
|
21
|
+
/** The winning pattern and its params, untyped — `useParams` narrows. */
|
|
22
|
+
declare function useRoute(): Pick<Match, 'pattern' | 'params'>;
|
|
23
|
+
/**
|
|
24
|
+
* Typed params for the pattern this component believes it renders under —
|
|
25
|
+
* inferred from the pattern literal, verified against `Register`, and read
|
|
26
|
+
* from context at runtime, so no component imports the route table. The
|
|
27
|
+
* belief is checked: rendering under any other pattern throws instead of
|
|
28
|
+
* silently returning params of the wrong shape.
|
|
29
|
+
*/
|
|
30
|
+
declare function useParams<P extends RegisteredPattern>(pattern: P): ParamsOf<P>;
|
|
31
|
+
//#endregion
|
|
32
|
+
export { Outlet, Router, useParams, useRoute };
|
package/dist/router.mjs
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { NavigationGeneration, useInterceptedNavigation } from "./navigation.mjs";
|
|
3
|
+
import { setBoundaryOutlet } from "./define-routes.mjs";
|
|
4
|
+
import { PathnameProvider } from "./location.mjs";
|
|
5
|
+
import { createContext, use, useMemo, useState } from "react";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
//#region src/router.tsx
|
|
8
|
+
const RouterContext = createContext(null);
|
|
9
|
+
const StackContext = createContext(null);
|
|
10
|
+
const RenderStack = ({ stack, index }) => {
|
|
11
|
+
const value = useMemo(() => ({
|
|
12
|
+
stack,
|
|
13
|
+
index: index + 1
|
|
14
|
+
}), [stack, index]);
|
|
15
|
+
const next = stack[index];
|
|
16
|
+
if (next === void 0) return null;
|
|
17
|
+
return /* @__PURE__ */ jsx(StackContext, {
|
|
18
|
+
value,
|
|
19
|
+
children: /* @__PURE__ */ jsx(next, {})
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
/** Renders the next element of the matched stack — a layout's hole. */
|
|
23
|
+
const Outlet = () => {
|
|
24
|
+
const ctx = use(StackContext);
|
|
25
|
+
if (ctx === null) throw new Error("Outlet must render inside <Router>");
|
|
26
|
+
return /* @__PURE__ */ jsx(RenderStack, {
|
|
27
|
+
stack: ctx.stack,
|
|
28
|
+
index: ctx.index
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
setBoundaryOutlet(() => /* @__PURE__ */ jsx(Outlet, {}));
|
|
32
|
+
/**
|
|
33
|
+
* Mounts a route table on the Navigation API: every same-origin navigation
|
|
34
|
+
* that the table claims is handled in the client, and the rest is left to the
|
|
35
|
+
* server. Layout components render their children through `<Outlet />`.
|
|
36
|
+
*
|
|
37
|
+
* This is the whole router for an application that renders on the client.
|
|
38
|
+
* Under the framework the tree comes from the server instead, and what is
|
|
39
|
+
* shared is `useInterceptedNavigation` and the `<PathnameProvider>` this
|
|
40
|
+
* mounts.
|
|
41
|
+
*/
|
|
42
|
+
function Router({ routes }) {
|
|
43
|
+
const [match, setMatch] = useState(() => routes.match(location.pathname));
|
|
44
|
+
const { generation } = useInterceptedNavigation({
|
|
45
|
+
claim: (url) => routes.match(url.pathname) !== null,
|
|
46
|
+
load: (url) => routes.match(url.pathname),
|
|
47
|
+
apply: setMatch
|
|
48
|
+
});
|
|
49
|
+
const value = useMemo(() => ({
|
|
50
|
+
routes,
|
|
51
|
+
match
|
|
52
|
+
}), [routes, match]);
|
|
53
|
+
return /* @__PURE__ */ jsx(PathnameProvider, {
|
|
54
|
+
pathname: location.pathname,
|
|
55
|
+
children: /* @__PURE__ */ jsx(NavigationGeneration, {
|
|
56
|
+
value: generation,
|
|
57
|
+
children: /* @__PURE__ */ jsx(RouterContext, {
|
|
58
|
+
value,
|
|
59
|
+
children: match === null ? null : /* @__PURE__ */ jsx(RenderStack, {
|
|
60
|
+
stack: match.stack,
|
|
61
|
+
index: 0
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** The winning pattern and its params, untyped — `useParams` narrows. */
|
|
68
|
+
function useRoute() {
|
|
69
|
+
const current = use(RouterContext)?.match ?? null;
|
|
70
|
+
if (current === null) throw new Error("useRoute must render inside a matched <Router>");
|
|
71
|
+
return {
|
|
72
|
+
pattern: current.pattern,
|
|
73
|
+
params: current.params
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Typed params for the pattern this component believes it renders under —
|
|
78
|
+
* inferred from the pattern literal, verified against `Register`, and read
|
|
79
|
+
* from context at runtime, so no component imports the route table. The
|
|
80
|
+
* belief is checked: rendering under any other pattern throws instead of
|
|
81
|
+
* silently returning params of the wrong shape.
|
|
82
|
+
*/
|
|
83
|
+
function useParams(pattern) {
|
|
84
|
+
const { pattern: current, params } = useRoute();
|
|
85
|
+
if (current !== pattern) throw new Error(`useParams("${pattern}") rendered under "${current}"`);
|
|
86
|
+
return params;
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
export { Outlet, Router, useParams, useRoute };
|