@mpen/rerouter 0.1.9 → 0.3.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.
Files changed (59) hide show
  1. package/README.md +76 -18
  2. package/dist/bin.d.ts +29 -0
  3. package/dist/bin.js +228 -0
  4. package/dist/hooks-Dlwcb0sV.js +20 -0
  5. package/dist/hooks.d.ts +2 -0
  6. package/dist/hooks.js +2 -0
  7. package/dist/index-BYXpNitc.d.ts +5 -0
  8. package/dist/index.d.ts +265 -0
  9. package/dist/index.js +139 -0
  10. package/dist/routes-Hpf6cwcZ.js +135 -0
  11. package/examples/App.tsx +111 -0
  12. package/examples/index.html +67 -0
  13. package/examples/pages/BlogPost.tsx +17 -0
  14. package/examples/pages/FetchLoading.tsx +53 -0
  15. package/examples/pages/FetchLoadingItem.tsx +45 -0
  16. package/examples/pages/Home.tsx +3 -0
  17. package/examples/pages/KitchenSink.tsx +23 -0
  18. package/examples/pages/Login.tsx +3 -0
  19. package/examples/pages/Match.tsx +5 -0
  20. package/examples/pages/NotFound.tsx +3 -0
  21. package/examples/pages/SlowLoading.tsx +8 -0
  22. package/examples/routes.gen.ts +125 -0
  23. package/examples/routes.ts +40 -0
  24. package/package.json +37 -32
  25. package/src/bin.test.ts +199 -0
  26. package/src/bin.ts +333 -0
  27. package/src/components/Link.test.tsx +139 -0
  28. package/src/components/Link.tsx +87 -0
  29. package/src/components/NavLink.test.tsx +119 -0
  30. package/src/components/NavLink.tsx +71 -0
  31. package/src/components/Router.tsx +75 -0
  32. package/src/fixtures/bin/kitchen-sink.tsx +15 -0
  33. package/src/fixtures/bin/optional.tsx +3 -0
  34. package/src/fixtures/bin/pages/Home.tsx +3 -0
  35. package/src/fixtures/bin/pages/KitchenSink.tsx +3 -0
  36. package/src/fixtures/bin/pages/Login.tsx +3 -0
  37. package/src/fixtures/bin/pages/Match.tsx +3 -0
  38. package/src/fixtures/bin/pages/NotFound.tsx +3 -0
  39. package/src/fixtures/bin/pages/Optional.tsx +3 -0
  40. package/src/fixtures/bin/regexp-groups.tsx +11 -0
  41. package/src/fixtures/bin/simple.tsx +1 -0
  42. package/src/fixtures/bin/unnamed.tsx +4 -0
  43. package/src/hooks/index.ts +1 -0
  44. package/src/hooks/useUrl.ts +22 -0
  45. package/src/index.ts +6 -0
  46. package/src/lib/mergeSearch.test.ts +37 -0
  47. package/src/lib/mergeSearch.ts +21 -0
  48. package/src/lib/routes.test.ts +67 -0
  49. package/src/lib/routes.ts +245 -0
  50. package/src/lib/url.ts +9 -0
  51. package/tsconfig.json +9 -0
  52. package/tsdown.config.ts +22 -0
  53. package/LICENSE +0 -21
  54. package/dist/bundle.cjs +0 -422
  55. package/dist/bundle.d.ts +0 -2
  56. package/dist/bundle.mjs +0 -420
  57. package/dist/dev.d.ts +0 -1
  58. package/dist/log.d.ts +0 -1
  59. package/dist/uri-template.d.ts +0 -56
package/README.md CHANGED
@@ -1,31 +1,89 @@
1
1
  # @mpen/rerouter
2
2
 
3
- An implementation of [RFC 6570: URI Template](https://tools.ietf.org/html/rfc6570).
4
-
3
+ A lightweight, type-safe router for React with a CLI for generating URL helpers.
4
+
5
+ ## Features
6
+
7
+ - **Small footprint**: Focuses only on the essentials of routing.
8
+ - **Type-safe URL generation**: CLI tool imports your route file and generates helper functions, ensuring you never have broken links.
9
+ - **Support for `path-to-regexp`**: Familiar syntax for route patterns.
10
+ - **Native `URLPattern` support**: Can use the browser's native `URLPattern` API.
11
+ - **Hooks-based**: Easy access to current path and search parameters.
12
+
5
13
  ## Installation
6
14
 
7
- ```sh
15
+ ```bash
8
16
  bun add @mpen/rerouter
9
17
  ```
10
18
 
11
- ## API
19
+ ## CLI: `rerouter`
20
+
21
+ The package includes a CLI tool to generate type-safe route helpers from your route definitions.
22
+
23
+ ### Usage
24
+
25
+ 1. Define your routes in a dedicated `.ts` file:
12
26
 
13
27
  ```ts
14
- const templ = new UriTemplate('/query{?firstName,lastName}')
15
- console.log(templ.match('/query?firstName=Bj%c3%b6rk&lastName=Gu%c3%b0mundsd%c3%b3ttir'))
16
- // {
17
- // score: 7,
18
- // params: {
19
- // firstName: "Björk",
20
- // lastName: "Guðmundsdóttir"
21
- // }
22
- // }
23
- console.log(templ.expand({firstName: 'Mark', lastName: 'Penner'}))
24
- // /query?firstName=Mark&lastName=Penner
28
+ // routes.ts
29
+ export default [
30
+ { name: 'home', pattern: '/', component: () => import('./pages/Home') },
31
+ { name: 'userProfile', pattern: '/user/:id', component: () => import('./pages/UserProfile') },
32
+ { pattern: '/user/:id/settings', component: () => import('./pages/UserProfile') },
33
+ ]
25
34
  ```
26
35
 
27
- That's it. That's the whole API. 2 methods.
36
+ Keep this file side-effect-free. The CLI imports and evaluates the route file to extract route names and patterns, so avoid top-level browser access, data fetching, app bootstrapping, or eager page component imports. Put route components behind `() => import('./pages/...')` loaders so generation does not pull page modules into the CLI process.
37
+
38
+ The `name` field is optional. Named string-pattern routes are included in generated URL helpers; unnamed routes still match at runtime but are skipped by the generator.
28
39
 
29
- It's been tested against https://github.com/uri-templates/uritemplate-test and all 157 tests pass.
40
+ 2. Run the generator:
41
+
42
+ ```bash
43
+ bunx @mpen/rerouter routes.ts -o src/routes.gen.ts
44
+ ```
45
+
46
+ 3. Use the generated helpers:
47
+
48
+ ```tsx
49
+ import { userProfile } from './routes.gen'
50
+
51
+ // Returns "/user/123"
52
+ const url = userProfile({ id: 123 })
53
+ ```
30
54
 
31
- The `score` is still experimental. The idea is that you can run a URL against multiple templates and if multiple match, you should use the one with the highest score. Higher score means more matching parts, i.e. more specific match.
55
+ ## Library Usage
56
+
57
+ ### Router
58
+
59
+ ```tsx
60
+ import { Router } from '@mpen/rerouter'
61
+ import ROUTES from './routes'
62
+
63
+ function App() {
64
+ return <Router routes={ROUTES} loading={<div>Loading...</div>} />
65
+ }
66
+ ```
67
+
68
+ ### Link
69
+
70
+ ```tsx
71
+ import { Link } from '@mpen/rerouter'
72
+ import { userProfile } from './routes.gen'
73
+
74
+ function Navigation() {
75
+ return <Link href={userProfile({ id: 'me' })}>My Profile</Link>
76
+ }
77
+ ```
78
+
79
+ ### Hooks
80
+
81
+ ```tsx
82
+ import { useUrlPath, useUrlSearchParams } from '@mpen/rerouter'
83
+
84
+ function MyComponent() {
85
+ const path = useUrlPath()
86
+ const searchParams = useUrlSearchParams()
87
+ // ...
88
+ }
89
+ ```
package/dist/bin.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ //#region src/bin.d.ts
2
+ type RunOptions = {
3
+ cwd?: string;
4
+ commandName?: string;
5
+ commandArgs?: readonly string[];
6
+ };
7
+ type RunResult = {
8
+ exitCode?: number;
9
+ stdout: string;
10
+ stderr: string;
11
+ };
12
+ /**
13
+ * Runs the rerouter CLI implementation without spawning a separate process.
14
+ *
15
+ * @param args - Command line arguments, excluding the binary name.
16
+ * @param options - Runtime options used to resolve paths and render the command comment.
17
+ * @returns Captured stdout, stderr, and an optional process exit code.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * const result = await runRerouterBin(['./routes.ts'])
22
+ * process.stdout.write(result.stdout)
23
+ * ```
24
+ *
25
+ * @internal
26
+ */
27
+ declare function runRerouterBin(args: readonly string[], options?: RunOptions): Promise<RunResult>;
28
+ //#endregion
29
+ export { runRerouterBin };
package/dist/bin.js ADDED
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env node
2
+ import { n as normalizeRoutes, t as normalizeLegacyPathToRegexpSyntax } from "./routes-Hpf6cwcZ.js";
3
+ import { parse } from "path-to-regexp";
4
+ import fs from "node:fs/promises";
5
+ import path from "node:path";
6
+ import process from "node:process";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
+ import { parseArgs } from "node:util";
9
+ //#region src/bin.ts
10
+ const PARSE_CONFIG = {
11
+ options: {
12
+ output: {
13
+ type: "string",
14
+ short: "o"
15
+ },
16
+ write: {
17
+ type: "boolean",
18
+ short: "w"
19
+ },
20
+ "wildcard-delimiter": { type: "string" },
21
+ "encode-function": { type: "string" }
22
+ },
23
+ allowPositionals: true,
24
+ strict: true
25
+ };
26
+ function escapeString(value) {
27
+ return JSON.stringify(value);
28
+ }
29
+ function shellEscape(arg) {
30
+ if (/^[a-z0-9/_.-]+$/i.test(arg)) return arg;
31
+ return `'${arg.replace(/'/g, "'\\''")}'`;
32
+ }
33
+ function compilePathGenerator(pattern, { delimiter = "/", encode = "encodeURIComponent", functionName = "generate" } = {}) {
34
+ const { tokens } = parse(normalizeLegacyPathToRegexpSyntax(pattern));
35
+ const baseProps = [];
36
+ const groupTypes = [];
37
+ const typeOfParam = (t) => t.type === "wildcard" ? "WildcardType" : "ParamType";
38
+ const makeProp = (name, t) => ({
39
+ name,
40
+ type: typeOfParam(t)
41
+ });
42
+ function collectGroupProps(ts2) {
43
+ const props = [];
44
+ for (const t of ts2) if (t.type === "param" || t.type === "wildcard") props.push(makeProp(t.name, t));
45
+ else if (t.type === "group") props.push(...collectGroupProps(t.tokens));
46
+ return props;
47
+ }
48
+ function collectTypes(ts2, intoBase = true) {
49
+ for (const t of ts2) if ((t.type === "param" || t.type === "wildcard") && intoBase) baseProps.push(makeProp(t.name, t));
50
+ else if (t.type === "group") {
51
+ const groupProps = collectGroupProps(t.tokens);
52
+ if (groupProps.length) {
53
+ const some = [
54
+ "{",
55
+ ...groupProps.map((p) => ` ${escapeString(p.name)}: ${p.type}`),
56
+ "}"
57
+ ].join("\n");
58
+ groupTypes.push(`AllOrNone<${some}>`);
59
+ }
60
+ collectTypes(t.tokens, false);
61
+ }
62
+ }
63
+ collectTypes(tokens);
64
+ const baseParamsType = [
65
+ "{",
66
+ ...baseProps.map((p) => ` ${escapeString(p.name)}: ${p.type}`),
67
+ "}"
68
+ ].join("\n");
69
+ const paramsType = groupTypes.length ? `${baseParamsType} & ${groupTypes.join(" & ")}` : baseParamsType;
70
+ const lines = [];
71
+ const indentUnit = " ";
72
+ const line = (indentLevel, text = "") => {
73
+ if (text === "") lines.push("");
74
+ else lines.push(indentUnit.repeat(indentLevel) + text);
75
+ };
76
+ const hasAnyParams = baseProps.length > 0 || groupTypes.length > 0;
77
+ if (hasAnyParams) {
78
+ lines.push(`export function ${functionName}(`);
79
+ lines.push(` params: ${paramsType}`);
80
+ lines.push(`): string {`);
81
+ } else lines.push(`export function ${functionName}(): string {`);
82
+ line(1, `let sb = ""`);
83
+ line(0);
84
+ const delim = escapeString(delimiter);
85
+ function collectNames(ts2) {
86
+ const names = [];
87
+ for (const t of ts2) if (t.type === "param" || t.type === "wildcard") names.push(t.name);
88
+ else if (t.type === "group") names.push(...collectNames(t.tokens));
89
+ return names;
90
+ }
91
+ function emitTokens(ts2, indentLevel, optional = false) {
92
+ if (!optional && hasAnyParams) {
93
+ for (const t of ts2) if (t.type === "param" || t.type === "wildcard") line(indentLevel, `if (params[${escapeString(t.name)}] == null) throw new Error(${escapeString(`Missing param: ${t.name}`)})`);
94
+ }
95
+ for (const t of ts2) if (t.type === "text") line(indentLevel, `sb += ${escapeString(t.value)}`);
96
+ else if (t.type === "param") line(indentLevel, `sb += (${encode})(String(params[${escapeString(t.name)}]))`);
97
+ else if (t.type === "wildcard") line(indentLevel, `sb += Array.from(params[${escapeString(t.name)}], v => (${encode})(String(v))).join(${delim})`);
98
+ else if (t.type === "group") {
99
+ const names = collectNames(t.tokens).map((name) => escapeString(name));
100
+ if (!names.length) continue;
101
+ const all = names.map((n) => `params[${n}] != null`).join(" && ");
102
+ const none = names.map((n) => `params[${n}] == null`).join(" && ");
103
+ const list = names.join(", ");
104
+ line(indentLevel, `if (${all}) {`);
105
+ emitTokens(t.tokens, indentLevel + 1, true);
106
+ line(indentLevel, `} else if (!(${none})) {`);
107
+ line(indentLevel + 1, `throw new Error(${escapeString(`Group requires all-or-none: ${list}`)})`);
108
+ line(indentLevel, `}`);
109
+ }
110
+ }
111
+ emitTokens(tokens, 1);
112
+ line(0);
113
+ line(1, `return sb`);
114
+ lines.push(`}`);
115
+ return lines.join("\n");
116
+ }
117
+ function toRouteFunctionName(routeName) {
118
+ return routeName.trim().replace(/[^a-zA-Z0-9_]/g, "_").replace(/^[^a-zA-Z_]+/, "") || "route";
119
+ }
120
+ async function importRoutes(routesPath) {
121
+ const mod = await import(pathToFileURL(routesPath).href);
122
+ if (!Array.isArray(mod.default)) throw new Error("Routes file must default export an array of routes.");
123
+ return mod.default;
124
+ }
125
+ function extractRoutes(routes) {
126
+ return normalizeRoutes(routes).flatMap((route) => {
127
+ if (!route.name || typeof route.pattern !== "string") return [];
128
+ return [{
129
+ name: route.name,
130
+ pattern: route.pattern
131
+ }];
132
+ });
133
+ }
134
+ async function main(options, positionals, { cwd = process.cwd(), commandName = "rerouter", commandArgs = process.argv.slice(2) } = {}) {
135
+ const [routesPathArg] = positionals;
136
+ if (!routesPathArg) return {
137
+ exitCode: 1,
138
+ stdout: "",
139
+ stderr: "Usage: rerouter <routes-file> [-o <output-file>] [-w] [--wildcard-delimiter <string>] [--encode-function <identifier>]\n"
140
+ };
141
+ const routesPath = path.resolve(cwd, routesPathArg);
142
+ let outputPath;
143
+ if (options.output) outputPath = path.resolve(cwd, options.output);
144
+ else if (options.write) outputPath = path.join(path.dirname(routesPath), path.basename(routesPath, path.extname(routesPath)) + ".gen.ts");
145
+ const routes = extractRoutes(await importRoutes(routesPath)).map((r) => ({
146
+ ...r,
147
+ pattern: r.pattern.trim()
148
+ })).filter((r) => r.pattern.startsWith("/") && r.pattern !== "*");
149
+ const wildcardDelimiter = options["wildcard-delimiter"] ?? "/";
150
+ const encodeFunction = options["encode-function"] ?? "encodeURIComponent";
151
+ const commandText = [commandName, ...commandArgs.map(shellEscape)].join(" ");
152
+ const out = [];
153
+ out.push(`// Do not modify this file. It was auto-generated with the following command:`);
154
+ out.push(`// $ ${commandText}`);
155
+ out.push(``);
156
+ out.push(`type AllOrNone<T> =`);
157
+ out.push(` | Required<T>`);
158
+ out.push(` | { [K in keyof T]?: never }`);
159
+ out.push(``);
160
+ out.push(`type ParamType = string | number | boolean`);
161
+ out.push(`type WildcardType = Iterable<ParamType>`);
162
+ out.push(``);
163
+ if (!routes.length) {
164
+ out.push(`// No string route patterns found in the default export.`);
165
+ out.push(``);
166
+ } else {
167
+ const usedNames = /* @__PURE__ */ new Set();
168
+ for (const route of routes) {
169
+ const base = toRouteFunctionName(route.name);
170
+ let name = base;
171
+ let i = 2;
172
+ while (usedNames.has(name)) name = `${base}_${i++}`;
173
+ usedNames.add(name);
174
+ out.push(compilePathGenerator(route.pattern, {
175
+ functionName: name,
176
+ delimiter: wildcardDelimiter,
177
+ encode: encodeFunction
178
+ }));
179
+ out.push(``);
180
+ }
181
+ }
182
+ const finalOutput = out.join("\n");
183
+ if (outputPath) {
184
+ await fs.writeFile(outputPath, finalOutput, "utf8");
185
+ return {
186
+ stdout: "",
187
+ stderr: `Wrote ${outputPath}\n`
188
+ };
189
+ } else return {
190
+ stdout: finalOutput,
191
+ stderr: ""
192
+ };
193
+ }
194
+ /**
195
+ * Runs the rerouter CLI implementation without spawning a separate process.
196
+ *
197
+ * @param args - Command line arguments, excluding the binary name.
198
+ * @param options - Runtime options used to resolve paths and render the command comment.
199
+ * @returns Captured stdout, stderr, and an optional process exit code.
200
+ *
201
+ * @example
202
+ * ```ts
203
+ * const result = await runRerouterBin(['./routes.ts'])
204
+ * process.stdout.write(result.stdout)
205
+ * ```
206
+ *
207
+ * @internal
208
+ */
209
+ async function runRerouterBin(args, options = {}) {
210
+ const { values, positionals } = parseArgs({
211
+ ...PARSE_CONFIG,
212
+ args: [...args]
213
+ });
214
+ return main(values, positionals, {
215
+ ...options,
216
+ commandArgs: args
217
+ });
218
+ }
219
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) runRerouterBin(process.argv.slice(2)).then((result) => {
220
+ if (result.stdout) process.stdout.write(result.stdout);
221
+ if (result.stderr) process.stderr.write(result.stderr);
222
+ if (typeof result.exitCode === "number") process.exitCode = result.exitCode;
223
+ }, (err) => {
224
+ console.error(err ?? "An unknown error occurred");
225
+ process.exitCode = 1;
226
+ });
227
+ //#endregion
228
+ export { runRerouterBin };
@@ -0,0 +1,20 @@
1
+ import { useMemo, useSyncExternalStore } from "react";
2
+ //#region src/hooks/useUrl.ts
3
+ const getPathname = () => window.location.pathname;
4
+ const getSearch = () => window.location.search;
5
+ function subscribe(cb) {
6
+ const handler = () => cb();
7
+ window.addEventListener("popstate", handler);
8
+ return () => {
9
+ window.removeEventListener("popstate", handler);
10
+ };
11
+ }
12
+ function useUrlPath() {
13
+ return useSyncExternalStore(subscribe, getPathname, getPathname);
14
+ }
15
+ function useUrlSearchParams() {
16
+ const search = useSyncExternalStore(subscribe, getSearch, getSearch);
17
+ return useMemo(() => new URLSearchParams(search), [search]);
18
+ }
19
+ //#endregion
20
+ export { useUrlSearchParams as n, useUrlPath as t };
@@ -0,0 +1,2 @@
1
+ import { n as useUrlSearchParams, t as useUrlPath } from "./index-BYXpNitc.js";
2
+ export { useUrlPath, useUrlSearchParams };
package/dist/hooks.js ADDED
@@ -0,0 +1,2 @@
1
+ import { n as useUrlSearchParams, t as useUrlPath } from "./hooks-Dlwcb0sV.js";
2
+ export { useUrlPath, useUrlSearchParams };
@@ -0,0 +1,5 @@
1
+ //#region src/hooks/useUrl.d.ts
2
+ declare function useUrlPath(): string;
3
+ declare function useUrlSearchParams(): URLSearchParams;
4
+ //#endregion
5
+ export { useUrlSearchParams as n, useUrlPath as t };
@@ -0,0 +1,265 @@
1
+ import { n as useUrlSearchParams, t as useUrlPath } from "./index-BYXpNitc.js";
2
+ import { ClassValue } from "@mpen/classcat";
3
+ import * as _$react_jsx_runtime0 from "react/jsx-runtime";
4
+ import { ComponentType, ReactNode } from "react";
5
+ import { OverrideProps } from "@mpen/ts-types/react";
6
+ import { Override } from "@mpen/ts-types";
7
+
8
+ //#region src/components/Link.d.ts
9
+ /**
10
+ * Values accepted by [`Link`]{@link Link} for building query strings.
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * <Link to="/matches" search={{ page: 2, sort: 'desc' }}>
15
+ * Matches
16
+ * </Link>
17
+ * ```
18
+ */
19
+ type SearchParamsInit = string | string[][] | Record<string, string | number | boolean | undefined | null> | URLSearchParams;
20
+ /**
21
+ * Props for [`Link`]{@link Link}.
22
+ */
23
+ type LinkProps = OverrideProps<'a', {
24
+ /**
25
+ * Classes to apply to the rendered anchor.
26
+ */
27
+ className?: ClassValue;
28
+ /**
29
+ * Destination URL passed to the rendered anchor's `href` attribute.
30
+ */
31
+ to: string;
32
+ /**
33
+ * Query parameters to merge into [`LinkProps.to`]{@link LinkProps#to}.
34
+ */
35
+ search?: SearchParamsInit;
36
+ /**
37
+ * Whether navigation should replace the current history entry instead of pushing a new one.
38
+ */
39
+ replace?: boolean;
40
+ href: never;
41
+ onClick: never;
42
+ }>;
43
+ /**
44
+ * Renders an anchor that navigates with rerouter history updates on ordinary clicks.
45
+ *
46
+ * @example
47
+ * ```tsx
48
+ * <Link to="/matches/42" search={{ tab: 'details' }}>
49
+ * View match
50
+ * </Link>
51
+ * ```
52
+ *
53
+ * @param props - Anchor props plus rerouter navigation options.
54
+ * @returns An anchor element that pushes or replaces the browser URL.
55
+ */
56
+ declare function Link({
57
+ to,
58
+ search,
59
+ children,
60
+ className,
61
+ replace,
62
+ ...rest
63
+ }: LinkProps): _$react_jsx_runtime0.JSX.Element;
64
+ //#endregion
65
+ //#region src/components/NavLink.d.ts
66
+ type NavLinkMatch = 'exact' | 'prefix';
67
+ /**
68
+ * Props for [`NavLink`]{@link NavLink}.
69
+ */
70
+ type NavLinkProps = Override<LinkProps, {
71
+ /**
72
+ * Classes to apply when the link target matches the current path.
73
+ */
74
+ activeClass?: ClassValue;
75
+ /**
76
+ * Classes to apply when the link target does not match the current path.
77
+ */
78
+ inactiveClass?: ClassValue;
79
+ /**
80
+ * How to compare the link target to the current path.
81
+ *
82
+ * @defaultValue `'exact'`
83
+ */
84
+ match?: NavLinkMatch;
85
+ }>;
86
+ /**
87
+ * Renders a [`Link`]{@link Link} with classes selected from the current route.
88
+ *
89
+ * @example
90
+ * ```tsx
91
+ * <NavLink
92
+ * activeClass={['pill', 'active']}
93
+ * inactiveClass={['pill', { muted: true }]}
94
+ * to="/settings"
95
+ * >
96
+ * Settings
97
+ * </NavLink>
98
+ * ```
99
+ *
100
+ * @param props - Link props plus active and inactive class values.
101
+ * @returns An anchor element that navigates through rerouter.
102
+ */
103
+ declare function NavLink({
104
+ activeClass,
105
+ className,
106
+ inactiveClass,
107
+ match,
108
+ to,
109
+ ...props
110
+ }: NavLinkProps): _$react_jsx_runtime0.JSX.Element;
111
+ //#endregion
112
+ //#region src/lib/routes.d.ts
113
+ /**
114
+ * Route params captured from the current URL pathname.
115
+ */
116
+ type RouteParams = Record<string, string | undefined>;
117
+ /**
118
+ * A React component that receives URL params captured for its route.
119
+ *
120
+ * @example
121
+ * ```tsx
122
+ * const UserPage: RouteComponent<{ id: string }> = ({ id }) => <div>{id}</div>
123
+ * ```
124
+ */
125
+ type RouteComponent<TParams extends RouteParams = RouteParams> = ComponentType<TParams>;
126
+ /**
127
+ * A dynamically imported route component module.
128
+ *
129
+ * @example
130
+ * ```tsx
131
+ * export default function UserPage({ id }: { id: string }) {
132
+ * return <div>{id}</div>
133
+ * }
134
+ * ```
135
+ */
136
+ type RouteComponentModule<TParams extends RouteParams = RouteParams> = {
137
+ default: RouteComponent<TParams>;
138
+ };
139
+ /**
140
+ * Loads a route component on demand.
141
+ *
142
+ * @example
143
+ * ```tsx
144
+ * const loadUserPage: RouteComponentLoader<{ id: string }> = () => import('./pages/UserPage')
145
+ * ```
146
+ */
147
+ type RouteComponentLoader<TParams extends RouteParams = RouteParams> = () => Promise<RouteComponentModule<TParams>>;
148
+ /**
149
+ * Object route definition consumed by [`Router`]{@link Router}.
150
+ *
151
+ * @example
152
+ * ```tsx
153
+ * const route: RouteObject = {
154
+ * name: 'userProfile',
155
+ * pattern: '/users/:id',
156
+ * component: () => import('./pages/UserProfile'),
157
+ * }
158
+ * ```
159
+ */
160
+ type RouteObject = {
161
+ /**
162
+ * Optional route name used by the CLI to generate a URL helper.
163
+ *
164
+ * Routes without a name still participate in runtime matching, but are skipped by the
165
+ * helper generator.
166
+ */
167
+ name?: string;
168
+ pattern: string | URLPattern;
169
+ component: RouteComponentLoader<any>;
170
+ };
171
+ /**
172
+ * Route definition consumed by [`Router`]{@link Router}.
173
+ *
174
+ * @example
175
+ * ```tsx
176
+ * const routes: readonly Route[] = [
177
+ * { name: 'home', pattern: '/', component: () => import('./pages/Home') },
178
+ * { pattern: '/users/:id', component: () => import('./pages/UserLayout') },
179
+ * ]
180
+ * ```
181
+ */
182
+ type Route = RouteObject;
183
+ /**
184
+ * Route definition normalized into a single object shape with a pathname matcher.
185
+ */
186
+ type NormalizedRoute = {
187
+ name?: string;
188
+ pattern: string | URLPattern;
189
+ component: RouteComponentLoader<any>;
190
+ matches(pathname: string): RouteParams | null;
191
+ };
192
+ /**
193
+ * Converts legacy `path-to-regexp` syntax that is ignored by URL generation into syntax accepted
194
+ * by the current parser.
195
+ *
196
+ * @param pattern - The route pattern to normalize.
197
+ * @returns The pattern with custom regexp constraints stripped and optional group suffixes removed.
198
+ *
199
+ * @example
200
+ * ```ts
201
+ * normalizeLegacyPathToRegexpSyntax('/blog/:id(\\d+){-:title}?')
202
+ * // '/blog/:id{-:title}'
203
+ * ```
204
+ *
205
+ * @internal
206
+ */
207
+ declare function normalizeLegacyPathToRegexpSyntax(pattern: string): string;
208
+ /**
209
+ * Normalizes routes into objects with a shared matcher implementation.
210
+ *
211
+ * @param routes - The route definitions to normalize.
212
+ * @returns Routes with stable `name`, `pattern`, `component`, and `matches` fields.
213
+ *
214
+ * @example
215
+ * ```tsx
216
+ * const normalized = normalizeRoutes(routes)
217
+ * const match = normalized[0]?.matches('/users/123')
218
+ * ```
219
+ */
220
+ declare function normalizeRoutes(routes: readonly Route[]): NormalizedRoute[];
221
+ //#endregion
222
+ //#region src/components/Router.d.ts
223
+ /**
224
+ * Props for [`Router`]{@link Router}.
225
+ *
226
+ * @example
227
+ * ```tsx
228
+ * <Router routes={routes} loading={<div>Loading...</div>} />
229
+ * ```
230
+ */
231
+ interface RouterProps {
232
+ /**
233
+ * Route definitions to match against the current URL pathname.
234
+ */
235
+ routes: readonly Route[];
236
+ /**
237
+ * Optional fallback rendered while a matched route component module is loading.
238
+ */
239
+ loading?: ReactNode;
240
+ }
241
+ /**
242
+ * Renders the first route that matches the current URL pathname.
243
+ *
244
+ * @param props - The router props.
245
+ * @returns The matched lazy route component, the loading fallback, or `null`.
246
+ *
247
+ * @example
248
+ * ```tsx
249
+ * import routes from './routes'
250
+ *
251
+ * function App() {
252
+ * return <Router routes={routes} loading={<div>Loading...</div>} />
253
+ * }
254
+ * ```
255
+ */
256
+ declare function Router({
257
+ routes,
258
+ loading
259
+ }: RouterProps): _$react_jsx_runtime0.JSX.Element | null;
260
+ //#endregion
261
+ //#region src/lib/url.d.ts
262
+ declare function pushUrl(next: string, state?: unknown): void;
263
+ declare function replaceUrl(next: string, state?: unknown): void;
264
+ //#endregion
265
+ export { Link, LinkProps, NavLink, NavLinkMatch, NavLinkProps, NormalizedRoute, Route, RouteComponent, RouteComponentLoader, RouteComponentModule, RouteObject, RouteParams, Router, RouterProps, SearchParamsInit, normalizeLegacyPathToRegexpSyntax, normalizeRoutes, pushUrl, replaceUrl, useUrlPath, useUrlSearchParams };