@mpen/rerouter 0.1.7 → 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.
- package/README.md +76 -18
- package/dist/bin.d.ts +29 -0
- package/dist/bin.js +228 -0
- package/dist/hooks-Dlwcb0sV.js +20 -0
- package/dist/hooks.d.ts +2 -0
- package/dist/hooks.js +2 -0
- package/dist/index-BYXpNitc.d.ts +5 -0
- package/dist/index.d.ts +265 -0
- package/dist/index.js +139 -0
- package/dist/routes-Hpf6cwcZ.js +135 -0
- package/examples/App.tsx +111 -0
- package/examples/index.html +67 -0
- package/examples/pages/BlogPost.tsx +17 -0
- package/examples/pages/FetchLoading.tsx +53 -0
- package/examples/pages/FetchLoadingItem.tsx +45 -0
- package/examples/pages/Home.tsx +3 -0
- package/examples/pages/KitchenSink.tsx +23 -0
- package/examples/pages/Login.tsx +3 -0
- package/examples/pages/Match.tsx +5 -0
- package/examples/pages/NotFound.tsx +3 -0
- package/examples/pages/SlowLoading.tsx +8 -0
- package/examples/routes.gen.ts +125 -0
- package/examples/routes.ts +40 -0
- package/package.json +37 -32
- package/src/bin.test.ts +199 -0
- package/src/bin.ts +333 -0
- package/src/components/Link.test.tsx +139 -0
- package/src/components/Link.tsx +87 -0
- package/src/components/NavLink.test.tsx +119 -0
- package/src/components/NavLink.tsx +71 -0
- package/src/components/Router.tsx +75 -0
- package/src/fixtures/bin/kitchen-sink.tsx +15 -0
- package/src/fixtures/bin/optional.tsx +3 -0
- package/src/fixtures/bin/pages/Home.tsx +3 -0
- package/src/fixtures/bin/pages/KitchenSink.tsx +3 -0
- package/src/fixtures/bin/pages/Login.tsx +3 -0
- package/src/fixtures/bin/pages/Match.tsx +3 -0
- package/src/fixtures/bin/pages/NotFound.tsx +3 -0
- package/src/fixtures/bin/pages/Optional.tsx +3 -0
- package/src/fixtures/bin/regexp-groups.tsx +11 -0
- package/src/fixtures/bin/simple.tsx +1 -0
- package/src/fixtures/bin/unnamed.tsx +4 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useUrl.ts +22 -0
- package/src/index.ts +6 -0
- package/src/lib/mergeSearch.test.ts +37 -0
- package/src/lib/mergeSearch.ts +21 -0
- package/src/lib/routes.test.ts +67 -0
- package/src/lib/routes.ts +245 -0
- package/src/lib/url.ts +9 -0
- package/tsconfig.json +9 -0
- package/tsdown.config.ts +22 -0
- package/LICENSE +0 -21
- package/dist/bundle.cjs +0 -406
- package/dist/bundle.d.ts +0 -2
- package/dist/bundle.mjs +0 -404
- package/dist/dev.d.ts +0 -1
- package/dist/log.d.ts +0 -1
- package/dist/uri-template.d.ts +0 -50
package/README.md
CHANGED
|
@@ -1,31 +1,89 @@
|
|
|
1
1
|
# @mpen/rerouter
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
```
|
|
15
|
+
```bash
|
|
8
16
|
bun add @mpen/rerouter
|
|
9
17
|
```
|
|
10
18
|
|
|
11
|
-
##
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|
package/dist/hooks.d.ts
ADDED
package/dist/hooks.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -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 };
|