@evolonix/react-router-next 0.2.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jason Ruesch
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,268 @@
1
+ # react-router-next
2
+
3
+ Next.js-style filesystem routing for React Router 7, delivered as a Vite plugin plus a tiny runtime. Drop a `page.tsx` into a folder, get a typed route — including typed params, typed `generate(...)` URL builders, nested layouts/loaders/loading/error boundaries, parallel routes (`@slot`), intercepting routes (`(.)`/`(..)`/`(...)`), `template.tsx` remount-on-navigation, and `_private` colocation folders.
4
+
5
+ > Peer dependencies: `react ≥ 19`, `react-dom ≥ 19`, `react-router ≥ 7`, `vite ≥ 5`.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm i @evolonix/react-router-next react-router
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ### 1. Add the Vite plugin
16
+
17
+ ```ts
18
+ // vite.config.ts
19
+ import react from "@vitejs/plugin-react";
20
+ import { routeTypegen } from "@evolonix/react-router-next/vite";
21
+ import { defineConfig } from "vite";
22
+
23
+ export default defineConfig({
24
+ plugins: [routeTypegen(), react()],
25
+ });
26
+ ```
27
+
28
+ ### 2. Mount the router
29
+
30
+ ```tsx
31
+ // src/main.tsx
32
+ import { StrictMode } from "react";
33
+ import { createRoot } from "react-dom/client";
34
+ import { AppRouter } from "@evolonix/react-router-next";
35
+
36
+ createRoot(document.getElementById("root")!).render(
37
+ <StrictMode>
38
+ <AppRouter />
39
+ </StrictMode>,
40
+ );
41
+ ```
42
+
43
+ ### 3. Drop pages into `src/app/`
44
+
45
+ ```
46
+ src/app/
47
+ ├── layout.tsx # wraps everything below
48
+ ├── page.tsx # /
49
+ ├── not-found.tsx # not-found boundary (root only)
50
+ ├── (marketing)/ # route group — folder name in (parens) is stripped
51
+ │ ├── about/page.tsx # /about
52
+ │ └── pricing/page.tsx # /pricing
53
+ ├── posts/
54
+ │ ├── layout.tsx # /posts/*
55
+ │ ├── loader.ts # parent loader
56
+ │ ├── loading.tsx # skeleton during nav
57
+ │ ├── page.tsx # /posts (index)
58
+ │ └── [postId]/
59
+ │ ├── loader.ts # leaf loader
60
+ │ ├── error.tsx # error boundary
61
+ │ └── page.tsx # /posts/:postId
62
+ ├── dashboard/ # parallel-route slots
63
+ │ ├── layout.tsx # function ({ children, analytics })
64
+ │ ├── page.tsx # /dashboard main panel
65
+ │ ├── settings/page.tsx # /dashboard/settings main panel
66
+ │ └── @analytics/ # parallel slot — invisible in URL
67
+ │ ├── page.tsx # rendered for /dashboard
68
+ │ ├── settings/page.tsx # rendered for /dashboard/settings
69
+ │ └── default.tsx # fallback when slot has no match
70
+ └── photos/
71
+ ├── page.tsx # /photos
72
+ ├── [id]/
73
+ │ ├── page.tsx # full-page detail
74
+ │ └── template.tsx # remounts on every navigation
75
+ ├── (.)[id]/page.tsx # modal interceptor — rendered on PUSH/REPLACE
76
+ └── _components/ # private folder — never routed, importable
77
+ └── dialog.tsx
78
+ ```
79
+
80
+ Folder-name conventions:
81
+
82
+ | Pattern | URL effect | TypeScript shape |
83
+ | ------------- | -------------------------------------------------------------------- | ----------------------- |
84
+ | `foo` | `/foo` | — |
85
+ | `(group)` | (segment removed) | — |
86
+ | `[id]` | `:id` | `{ id: string }` |
87
+ | `[[id]]` | `:id?` (optional) | `{ id?: string }` |
88
+ | `[...slug]` | catch-all | `{ slug: string[] }` |
89
+ | `[[...slug]]` | optional catch-all | `{ slug?: string[] }` |
90
+ | `@slot` | (segment removed) — contents become a slot prop on the parent layout | — |
91
+ | `_private` | folder is skipped by routing (still importable from siblings) | — |
92
+ | `(.)x` | intercepts URL `<parent>/x` — same level as containing folder | inherits `x`'s shape |
93
+ | `(..)x` | intercepts one filesystem level up | inherits target's shape |
94
+ | `(..)(..)x` | intercepts two filesystem levels up | inherits target's shape |
95
+ | `(...)x` | intercepts `/x` from the app root | inherits target's shape |
96
+
97
+ File-name conventions inside a route folder:
98
+
99
+ | File | Role |
100
+ | --------------- | ------------------------------------------------------------------------------------------------------- |
101
+ | `page.tsx` | Leaf component for the route |
102
+ | `layout.tsx` | Wraps children via `<Outlet/>`. With sibling `@slot/` folders, also receives each slot as a named prop. |
103
+ | `template.tsx` | Like `layout.tsx` but remounts on every navigation (keyed on `pathname`). |
104
+ | `default.tsx` | Fallback inside a `@slot/` directory when the URL doesn't match any of the slot's pages. |
105
+ | `loader.ts` | React Router data loader |
106
+ | `loading.tsx` | Rendered while a parent loader is pending |
107
+ | `error.tsx` | `errorElement` for the route |
108
+ | `not-found.tsx` | App-wide not-found boundary (root only) |
109
+
110
+ ### 4. Use the typed helpers
111
+
112
+ For each route folder the plugin exposes a virtual module — `virtual:react-router-next/<route-key>` — that mirrors the folder layout, with the root represented as `_root`:
113
+
114
+ ```tsx
115
+ // src/app/posts/[postId]/page.tsx
116
+ import type { RouteProps } from "virtual:react-router-next/posts/[postId]";
117
+ import { useLoaderData } from "react-router";
118
+ import type { Post } from "../loader";
119
+
120
+ export default function PostPage({ params }: RouteProps) {
121
+ const post = useLoaderData<Post>();
122
+ return (
123
+ <article>
124
+ {post.title} (id: {params.postId})
125
+ </article>
126
+ );
127
+ }
128
+ ```
129
+
130
+ ```tsx
131
+ // any other component
132
+ import { generate as generatePost } from "virtual:react-router-next/posts/[postId]";
133
+
134
+ <NavLink to={generatePost({ postId: "1" })}>First post</NavLink>;
135
+ ```
136
+
137
+ The runtime hook `useRouteParams` is also re-exported from the package itself if you'd rather not pin the route literal:
138
+
139
+ ```tsx
140
+ import { useRouteParams } from "@evolonix/react-router-next";
141
+ const { postId } = useRouteParams("posts/[postId]");
142
+ ```
143
+
144
+ ## Build your own router
145
+
146
+ `<AppRouter />` is a thin wrapper around `createBrowserRouter` plus the package's filesystem-to-`RouteObject[]` builder. If you need a different router (memory router for tests, hash router, SSR via `createStaticRouter`, custom `RouterProvider` props, route post-processing, etc.) import the builder directly and feed it the same virtual module the default router uses:
147
+
148
+ ```tsx
149
+ // src/main.tsx
150
+ import { StrictMode } from "react";
151
+ import { createRoot } from "react-dom/client";
152
+ import { createBrowserRouter, RouterProvider } from "react-router";
153
+ import {
154
+ buildRoutesFromModules,
155
+ type RouteModuleMap,
156
+ } from "@evolonix/react-router-next";
157
+ // @ts-expect-error virtual module is provided by the routeTypegen Vite plugin
158
+ import { modules, appDir } from "virtual:react-router-next/app-tree";
159
+
160
+ const routes = buildRoutesFromModules(modules as RouteModuleMap, appDir);
161
+ const router = createBrowserRouter(routes);
162
+
163
+ createRoot(document.getElementById("root")!).render(
164
+ <StrictMode>
165
+ <RouterProvider router={router} />
166
+ </StrictMode>,
167
+ );
168
+ ```
169
+
170
+ The returned `RouteObject[]` is plain React Router 7 — pass it to any router factory, splice in extra routes, or wrap the elements before mounting.
171
+
172
+ ## Parallel routes (`@slot`)
173
+
174
+ A folder prefixed with `@` doesn't contribute a URL segment — instead, its contents are matched independently against the current URL and rendered in the parent layout as a named prop. The parent layout's signature gains one prop per slot:
175
+
176
+ ```tsx
177
+ // src/app/dashboard/layout.tsx
178
+ export default function DashboardLayout({
179
+ children,
180
+ analytics,
181
+ }: {
182
+ children: ReactNode;
183
+ analytics: ReactNode;
184
+ }) {
185
+ return (
186
+ <div className="grid grid-cols-[2fr_1fr]">
187
+ <main>{children}</main>
188
+ <aside>{analytics}</aside>
189
+ </div>
190
+ );
191
+ }
192
+ ```
193
+
194
+ Each slot subtree can have its own `page.tsx` files (matching the parent's URL space) and a `default.tsx` fallback rendered when the URL doesn't match any of the slot's explicit pages.
195
+
196
+ > **V1 caveat:** slot subtrees are matched via `useRoutes()` outside React Router's data router, so a `loader.ts` under a `@slot/` directory is dropped with a build-time warning. Use ordinary children for data-driven UI.
197
+
198
+ ## Intercepting routes (`(.)`/`(..)`/`(...)`)
199
+
200
+ A folder whose name starts with `(.)`, `(..)`, `(..)(..)`, or `(...)` is an **interceptor**: its `page.tsx` is rendered when the user soft-navigates (PUSH/REPLACE) to a target URL elsewhere in the tree. On reload, back/forward, or direct visit, the original target page renders instead. The interceptor and the target share the same routeKey and `useRouteParams`/`generate` virtual module.
201
+
202
+ ```
203
+ photos/
204
+ ├── page.tsx # /photos
205
+ ├── [id]/page.tsx # /photos/:id — full-page detail (POP / refresh)
206
+ └── (.)[id]/page.tsx # /photos/:id — modal (PUSH / REPLACE from in-app Link)
207
+ ```
208
+
209
+ Prefix semantics (counted in **filesystem** levels — slots count, group folders count, but the prefix itself does not):
210
+
211
+ - `(.)x` — same level as the interceptor's containing folder; appends `x`.
212
+ - `(..)x` — pops one filesystem level above, then appends `x`.
213
+ - `(..)(..)x` — pops two levels.
214
+ - `(...)x` — anchors at the app root.
215
+
216
+ > **V1 caveats:** the interceptor folder may only contain `page.tsx`. A `loader.ts` or `layout.tsx` inside an interceptor is dropped with a build-time warning. The intercept target route must exist — otherwise the build fails (a refresh on the URL has to render _something_).
217
+
218
+ ## `template.tsx` and `_private` folders
219
+
220
+ - `template.tsx` works like `layout.tsx`, but the wrapper is keyed on `useLocation().pathname` so it remounts on every navigation. Useful for entry transitions or `useEffect`-based instrumentation that should fire per-navigation.
221
+ - A folder whose name starts with `_` is skipped by the router entirely. Use it to colocate components, helpers, or fixtures alongside your routes without producing a URL.
222
+
223
+ ## How types work without running Vite
224
+
225
+ The plugin (and the bundled `react-router-next` CLI) emit a single ambient `routes.d.ts` shim into `node_modules/.react-router-next/`. The shim contains one `declare module 'virtual:react-router-next/<route-key>' { … }` block per discovered route, so `tsc` and editors resolve the imports and infer per-route param shapes — even when Vite isn't running.
226
+
227
+ Add the shim to your tsconfig:
228
+
229
+ ```jsonc
230
+ // tsconfig.app.json
231
+ {
232
+ "include": ["src", "node_modules/.react-router-next/routes.d.ts"],
233
+ }
234
+ ```
235
+
236
+ In CI, run typegen before `tsc`:
237
+
238
+ ```jsonc
239
+ // package.json
240
+ {
241
+ "scripts": {
242
+ "typegen": "react-router-next typegen",
243
+ "prebuild": "npm run typegen",
244
+ "build": "tsc -b && vite build",
245
+ },
246
+ }
247
+ ```
248
+
249
+ ## Plugin options
250
+
251
+ ```ts
252
+ routeTypegen({
253
+ appDir: "src/app", // default
254
+ outDir: "node_modules/.react-router-next", // default
255
+ });
256
+ ```
257
+
258
+ The CLI mirrors these:
259
+
260
+ ```sh
261
+ react-router-next typegen \
262
+ --app-dir=src/app \
263
+ --out-dir=node_modules/.react-router-next
264
+ ```
265
+
266
+ ## License
267
+
268
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/plugin/typegen.ts
4
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
5
+ import { dirname, isAbsolute, join, resolve } from "path";
6
+
7
+ // src/plugin/scan.ts
8
+ import { readdirSync } from "fs";
9
+ import { relative } from "path";
10
+ var ROUTE_DIR_FILE_RE = /^(page|layout|default|template)\.(tsx|jsx|ts|js)$/;
11
+ function toPosix(p) {
12
+ return p.split("\\").join("/");
13
+ }
14
+ function isPrivateSegment(seg) {
15
+ return seg.startsWith("_");
16
+ }
17
+ function isSlotSegment(seg) {
18
+ return seg.startsWith("@") && seg.length > 1;
19
+ }
20
+ function parseInterceptPrefix(seg) {
21
+ if (seg.startsWith("(...)")) return { depth: "root", rest: seg.slice(5) };
22
+ if (seg.startsWith("(..)(..)")) return { depth: 3, rest: seg.slice(8) };
23
+ if (seg.startsWith("(..)")) return { depth: 2, rest: seg.slice(4) };
24
+ if (seg.startsWith("(.)")) return { depth: 1, rest: seg.slice(3) };
25
+ return null;
26
+ }
27
+ function scanAppDir(appDir) {
28
+ let entries;
29
+ try {
30
+ entries = readdirSync(appDir, { recursive: true, withFileTypes: true });
31
+ } catch {
32
+ return { routeDirs: [] };
33
+ }
34
+ const routeDirs = /* @__PURE__ */ new Set();
35
+ for (const entry of entries) {
36
+ if (!entry.isFile()) continue;
37
+ if (!ROUTE_DIR_FILE_RE.test(entry.name)) continue;
38
+ const dir = entry.parentPath ?? entry.path ?? appDir;
39
+ const rel = toPosix(relative(appDir, dir));
40
+ if (rel !== "" && rel.split("/").some(isPrivateSegment)) continue;
41
+ routeDirs.add(dir);
42
+ }
43
+ return { routeDirs: [...routeDirs] };
44
+ }
45
+ function routeKeySegmentsOf(parts) {
46
+ return parts.filter((s) => !isSlotSegment(s) && !isPrivateSegment(s));
47
+ }
48
+ function computeRouteKey(parts) {
49
+ let interceptIdx = -1;
50
+ let intercept = null;
51
+ for (let i = 0; i < parts.length; i++) {
52
+ const p = parseInterceptPrefix(parts[i]);
53
+ if (p) {
54
+ interceptIdx = i;
55
+ intercept = p;
56
+ break;
57
+ }
58
+ }
59
+ if (intercept === null) return routeKeySegmentsOf(parts).join("/");
60
+ const fsPrefix = parts.slice(0, interceptIdx);
61
+ let resolved;
62
+ if (intercept.depth === "root") {
63
+ resolved = [];
64
+ } else {
65
+ const popCount = intercept.depth - 1;
66
+ resolved = fsPrefix.slice(0, Math.max(0, fsPrefix.length - popCount));
67
+ }
68
+ const prefixSegs = routeKeySegmentsOf(resolved);
69
+ const tail = parts.slice(interceptIdx + 1);
70
+ const restSegments = [];
71
+ if (intercept.rest) restSegments.push(intercept.rest);
72
+ restSegments.push(...tail);
73
+ return [...prefixSegs, ...routeKeySegmentsOf(restSegments)].join("/");
74
+ }
75
+ function routeKeyFor(appDir, routeDir) {
76
+ const rel = toPosix(relative(appDir, routeDir));
77
+ if (rel === "") return "";
78
+ return computeRouteKey(rel.split("/"));
79
+ }
80
+ function routeHasParams(routeKey) {
81
+ return routeKey.includes("[");
82
+ }
83
+
84
+ // src/plugin/render.ts
85
+ var HEADER = "// AUTO-GENERATED by react-router-next typegen \u2014 do not edit.";
86
+ function renderDeclareBody(routeKey) {
87
+ if (!routeHasParams(routeKey)) {
88
+ return ` import type { RouteParams as RouteParamsBase } from "@evolonix/react-router-next";
89
+ const PATH: ${JSON.stringify(routeKey)};
90
+ export type RouteParams = RouteParamsBase<typeof PATH>;
91
+ export function generate(): string;
92
+ `;
93
+ }
94
+ return ` import type { RouteParams as RouteParamsBase } from "@evolonix/react-router-next";
95
+ const PATH: ${JSON.stringify(routeKey)};
96
+ export type RouteParams = RouteParamsBase<typeof PATH>;
97
+ export type RouteProps = { params: RouteParams };
98
+ export function useRouteParams(): RouteParams;
99
+ export function generate(params: RouteParams): string;
100
+ `;
101
+ }
102
+ function virtualIdFor(routeKey) {
103
+ return `virtual:react-router-next/${routeKey === "" ? "_root" : routeKey}`;
104
+ }
105
+ function renderDtsShim(routeKeys) {
106
+ const blocks = routeKeys.map((key) => {
107
+ const id = virtualIdFor(key);
108
+ return `declare module ${JSON.stringify(id)} {
109
+ ${renderDeclareBody(key)}}
110
+ `;
111
+ });
112
+ return `${HEADER}
113
+ // Ambient declarations for react-router-next virtual route modules.
114
+ // Generated by \`react-router-next typegen\` and the routeTypegen Vite plugin.
115
+
116
+ declare module "virtual:react-router-next/app-tree" {
117
+ import type { ComponentType } from "react";
118
+ import type { LoaderFunction } from "react-router";
119
+
120
+ export type RouteModule = {
121
+ default?: ComponentType<{
122
+ params?: Record<string, string | string[] | undefined>;
123
+ }>;
124
+ loader?: LoaderFunction;
125
+ };
126
+
127
+ export const modules: Record<string, RouteModule>;
128
+ export const appDir: string;
129
+ }
130
+
131
+ ${blocks.join("\n")}`;
132
+ }
133
+
134
+ // src/plugin/typegen.ts
135
+ function resolveAgainst(root, p) {
136
+ return isAbsolute(p) ? p : resolve(root, p);
137
+ }
138
+ function writeIfChanged(path, contents) {
139
+ try {
140
+ const existing = readFileSync(path, "utf8");
141
+ if (existing === contents) return false;
142
+ } catch {
143
+ }
144
+ mkdirSync(dirname(path), { recursive: true });
145
+ writeFileSync(path, contents);
146
+ return true;
147
+ }
148
+ function generateRouteTypes(opts = {}) {
149
+ const root = opts.root ?? process.cwd();
150
+ const appDir = resolveAgainst(root, opts.appDir ?? "src/app");
151
+ const outDir = resolveAgainst(
152
+ root,
153
+ opts.outDir ?? "node_modules/.react-router-next"
154
+ );
155
+ const { routeDirs } = scanAppDir(appDir);
156
+ const routeKeys = [
157
+ ...new Set(routeDirs.map((dir) => routeKeyFor(appDir, dir)))
158
+ ].sort((a, b) => a.localeCompare(b));
159
+ const shimPath = join(outDir, "routes.d.ts");
160
+ const written = writeIfChanged(shimPath, renderDtsShim(routeKeys));
161
+ return { appDir, outDir, routeKeys, shimPath, written };
162
+ }
163
+
164
+ // src/cli.ts
165
+ function parseArgs(argv) {
166
+ const flags = {};
167
+ const rest = [];
168
+ let command;
169
+ for (const arg of argv) {
170
+ if (arg.startsWith("--")) {
171
+ const eq = arg.indexOf("=");
172
+ if (eq === -1) {
173
+ flags[arg.slice(2)] = true;
174
+ } else {
175
+ flags[arg.slice(2, eq)] = arg.slice(eq + 1);
176
+ }
177
+ } else if (!command) {
178
+ command = arg;
179
+ } else {
180
+ rest.push(arg);
181
+ }
182
+ }
183
+ return { command, flags, rest };
184
+ }
185
+ function printHelp() {
186
+ console.log(`react-router-next \u2014 typed filesystem routing for React Router 7
187
+
188
+ Usage:
189
+ react-router-next typegen [options]
190
+
191
+ Options:
192
+ --app-dir <path> Source directory of pages/layouts (default: src/app)
193
+ --out-dir <path> Where the routes.d.ts shim is written
194
+ (default: node_modules/.react-router-next)
195
+ --help, -h Show this message
196
+ `);
197
+ }
198
+ async function main() {
199
+ const { command, flags } = parseArgs(process.argv.slice(2));
200
+ if (flags.help || flags.h || command === "help") {
201
+ printHelp();
202
+ return;
203
+ }
204
+ if (command !== "typegen") {
205
+ if (command) {
206
+ console.error(`[react-router-next] Unknown command: ${command}`);
207
+ }
208
+ printHelp();
209
+ process.exit(command ? 1 : 0);
210
+ }
211
+ const result = generateRouteTypes({
212
+ appDir: typeof flags["app-dir"] === "string" ? flags["app-dir"] : void 0,
213
+ outDir: typeof flags["out-dir"] === "string" ? flags["out-dir"] : void 0
214
+ });
215
+ console.log(
216
+ `[react-router-next] ${result.routeKeys.length} route(s); shim ${result.written ? "updated" : "unchanged"} at ${result.shimPath}`
217
+ );
218
+ }
219
+ main().catch((err) => {
220
+ console.error(err);
221
+ process.exit(1);
222
+ });
223
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/plugin/typegen.ts","../src/plugin/scan.ts","../src/plugin/render.ts","../src/cli.ts"],"sourcesContent":["import { mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, isAbsolute, join, resolve } from \"node:path\";\nimport { renderDtsShim } from \"./render\";\nimport { routeKeyFor, scanAppDir } from \"./scan\";\n\nexport type GenerateOptions = {\n /** Project root used to resolve relative paths. Defaults to `process.cwd()`. */\n root?: string;\n /** Source-of-truth directory containing `page.tsx`/`layout.tsx`. Defaults to `src/app`. */\n appDir?: string;\n /** Where the ambient `routes.d.ts` shim is written. Defaults to `<root>/node_modules/.react-router-next`. */\n outDir?: string;\n};\n\nexport type GenerateResult = {\n appDir: string;\n outDir: string;\n routeKeys: string[];\n shimPath: string;\n written: boolean;\n};\n\nfunction resolveAgainst(root: string, p: string): string {\n return isAbsolute(p) ? p : resolve(root, p);\n}\n\nfunction writeIfChanged(path: string, contents: string): boolean {\n try {\n const existing = readFileSync(path, \"utf8\");\n if (existing === contents) return false;\n } catch {\n // file missing — fall through to write\n }\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, contents);\n return true;\n}\n\nexport function generateRouteTypes(opts: GenerateOptions = {}): GenerateResult {\n const root = opts.root ?? process.cwd();\n const appDir = resolveAgainst(root, opts.appDir ?? \"src/app\");\n const outDir = resolveAgainst(\n root,\n opts.outDir ?? \"node_modules/.react-router-next\",\n );\n\n const { routeDirs } = scanAppDir(appDir);\n const routeKeys = [\n ...new Set(routeDirs.map((dir) => routeKeyFor(appDir, dir))),\n ].sort((a, b) => a.localeCompare(b));\n\n const shimPath = join(outDir, \"routes.d.ts\");\n const written = writeIfChanged(shimPath, renderDtsShim(routeKeys));\n\n return { appDir, outDir, routeKeys, shimPath, written };\n}\n","import { readdirSync } from \"node:fs\";\nimport { relative } from \"node:path\";\n\nconst ROUTE_DIR_FILE_RE = /^(page|layout|default|template)\\.(tsx|jsx|ts|js)$/;\n\nexport function toPosix(p: string): string {\n return p.split(\"\\\\\").join(\"/\");\n}\n\nexport type ScanResult = {\n /** Absolute paths of directories that contain a page/layout/default/template file. */\n routeDirs: string[];\n};\n\nexport function isPrivateSegment(seg: string): boolean {\n return seg.startsWith(\"_\");\n}\n\nexport function isSlotSegment(seg: string): boolean {\n return seg.startsWith(\"@\") && seg.length > 1;\n}\n\nexport type InterceptDepth = 1 | 2 | 3 | \"root\";\n\nexport type InterceptParse = { depth: InterceptDepth; rest: string };\n\nexport function parseInterceptPrefix(seg: string): InterceptParse | null {\n // Check from most specific to least so e.g. \"(...)x\" doesn't get caught by \"(.)\".\n if (seg.startsWith(\"(...)\")) return { depth: \"root\", rest: seg.slice(5) };\n if (seg.startsWith(\"(..)(..)\")) return { depth: 3, rest: seg.slice(8) };\n if (seg.startsWith(\"(..)\")) return { depth: 2, rest: seg.slice(4) };\n if (seg.startsWith(\"(.)\")) return { depth: 1, rest: seg.slice(3) };\n return null;\n}\n\nexport function isRouteGroupSegment(seg: string): boolean {\n return (\n seg.startsWith(\"(\") &&\n seg.endsWith(\")\") &&\n parseInterceptPrefix(seg) === null\n );\n}\n\nexport function scanAppDir(appDir: string): ScanResult {\n let entries;\n try {\n entries = readdirSync(appDir, { recursive: true, withFileTypes: true });\n } catch {\n return { routeDirs: [] };\n }\n const routeDirs = new Set<string>();\n for (const entry of entries) {\n if (!entry.isFile()) continue;\n if (!ROUTE_DIR_FILE_RE.test(entry.name)) continue;\n const dir =\n (entry as unknown as { parentPath?: string; path?: string }).parentPath ??\n (entry as unknown as { path?: string }).path ??\n appDir;\n const rel = toPosix(relative(appDir, dir));\n if (rel !== \"\" && rel.split(\"/\").some(isPrivateSegment)) continue;\n routeDirs.add(dir);\n }\n return { routeDirs: [...routeDirs] };\n}\n\nfunction routeKeySegmentsOf(parts: readonly string[]): string[] {\n // Keep route groups in the routeKey (they're literal directory components).\n // Strip @slots and `_private` because they don't appear in any URL.\n return parts.filter((s) => !isSlotSegment(s) && !isPrivateSegment(s));\n}\n\n/**\n * Compute the route key for a sequence of filesystem segments.\n *\n * - `_private` folders and `@slot` folders are stripped (they don't appear in URLs).\n * - Route groups `(group)` are preserved literally, matching the existing\n * `(marketing)/about` shape.\n * - An intercept-prefixed segment (`(.)x`, `(..)x`, `(..)(..)x`, `(...)x`)\n * collapses preceding filesystem segments by `depth - 1` levels (or all the\n * way to the root for `(...)`), then appends the stripped name.\n *\n * For an interceptor folder, the returned key is the resolved target route key\n * — e.g. `photos/(.)[id]` → `photos/[id]`, the same key as the interceptor's\n * target page. That keeps `useRouteParams<...>()` aligned with the URL the\n * user sees regardless of which file rendered.\n */\nexport function computeRouteKey(parts: readonly string[]): string {\n let interceptIdx = -1;\n let intercept: InterceptParse | null = null;\n for (let i = 0; i < parts.length; i++) {\n const p = parseInterceptPrefix(parts[i]);\n if (p) {\n interceptIdx = i;\n intercept = p;\n break;\n }\n }\n\n if (intercept === null) return routeKeySegmentsOf(parts).join(\"/\");\n\n const fsPrefix = parts.slice(0, interceptIdx);\n let resolved: string[];\n if (intercept.depth === \"root\") {\n resolved = [];\n } else {\n const popCount = intercept.depth - 1;\n resolved = fsPrefix.slice(0, Math.max(0, fsPrefix.length - popCount));\n }\n const prefixSegs = routeKeySegmentsOf(resolved);\n const tail = parts.slice(interceptIdx + 1);\n const restSegments: string[] = [];\n if (intercept.rest) restSegments.push(intercept.rest);\n restSegments.push(...tail);\n return [...prefixSegs, ...routeKeySegmentsOf(restSegments)].join(\"/\");\n}\n\nexport function routeKeyFor(appDir: string, routeDir: string): string {\n const rel = toPosix(relative(appDir, routeDir));\n if (rel === \"\") return \"\";\n return computeRouteKey(rel.split(\"/\"));\n}\n\nexport function routeHasParams(routeKey: string): boolean {\n return routeKey.includes(\"[\");\n}\n\n/** Match `(page|layout|loader|loading|error|default|template|not-found).{tsx,jsx,ts,js}`. */\nexport const ROUTE_FILE_RE =\n /[\\\\/](page|layout|loader|loading|error|default|template|not-found)\\.(tsx|jsx|ts|js)$/;\n","import { routeHasParams } from \"./scan\";\n\nconst HEADER = \"// AUTO-GENERATED by react-router-next typegen — do not edit.\";\n\n/**\n * Source emitted into the **runtime** virtual module for a given route key.\n * Plain JavaScript — type info lives in the ambient `.d.ts` shim emitted by\n * `renderDtsShim`. Virtual modules aren't transformed as TypeScript by Vite,\n * so the runtime form must not contain type-only syntax.\n */\nexport function renderRuntimeModule(routeKey: string): string {\n if (!routeHasParams(routeKey)) {\n return `${HEADER}\nimport { generateUrl } from \"@evolonix/react-router-next\";\n\nconst PATH = ${JSON.stringify(routeKey)};\n\nexport function generate() {\n return generateUrl(PATH, {});\n}\n`;\n }\n\n return `${HEADER}\nimport {\n generateUrl,\n useRouteParams as useRouteParamsBase,\n} from \"@evolonix/react-router-next\";\n\nconst PATH = ${JSON.stringify(routeKey)};\n\nexport function useRouteParams() {\n return useRouteParamsBase(PATH);\n}\n\nexport function generate(params) {\n return generateUrl(PATH, params);\n}\n`;\n}\n\n/**\n * Body of a `declare module 'virtual:react-router-next/<route-key>' { ... }`\n * block. Same shape as the runtime module, but with type-only declarations so\n * `tsc` and editors resolve consumer imports without spinning up Vite.\n */\nfunction renderDeclareBody(routeKey: string): string {\n if (!routeHasParams(routeKey)) {\n return ` import type { RouteParams as RouteParamsBase } from \"@evolonix/react-router-next\";\n const PATH: ${JSON.stringify(routeKey)};\n export type RouteParams = RouteParamsBase<typeof PATH>;\n export function generate(): string;\n`;\n }\n return ` import type { RouteParams as RouteParamsBase } from \"@evolonix/react-router-next\";\n const PATH: ${JSON.stringify(routeKey)};\n export type RouteParams = RouteParamsBase<typeof PATH>;\n export type RouteProps = { params: RouteParams };\n export function useRouteParams(): RouteParams;\n export function generate(params: RouteParams): string;\n`;\n}\n\nexport function virtualIdFor(routeKey: string): string {\n return `virtual:react-router-next/${routeKey === \"\" ? \"_root\" : routeKey}`;\n}\n\n/**\n * Emit a single ambient `.d.ts` shim covering every discovered route. Import\n * specifiers that consumers write (e.g.\n * `virtual:react-router-next/posts/[postId]`) resolve to a `declare module`\n * block in this file at type-check time.\n */\nexport function renderDtsShim(routeKeys: readonly string[]): string {\n const blocks = routeKeys.map((key) => {\n const id = virtualIdFor(key);\n return `declare module ${JSON.stringify(id)} {\n${renderDeclareBody(key)}}\n`;\n });\n return `${HEADER}\n// Ambient declarations for react-router-next virtual route modules.\n// Generated by \\`react-router-next typegen\\` and the routeTypegen Vite plugin.\n\ndeclare module \"virtual:react-router-next/app-tree\" {\n import type { ComponentType } from \"react\";\n import type { LoaderFunction } from \"react-router\";\n\n export type RouteModule = {\n default?: ComponentType<{\n params?: Record<string, string | string[] | undefined>;\n }>;\n loader?: LoaderFunction;\n };\n\n export const modules: Record<string, RouteModule>;\n export const appDir: string;\n}\n\n${blocks.join(\"\\n\")}`;\n}\n","#!/usr/bin/env node\nimport { generateRouteTypes } from \"./plugin/typegen\";\n\ntype ParsedArgs = {\n command: string | undefined;\n flags: Record<string, string | true>;\n rest: string[];\n};\n\nfunction parseArgs(argv: string[]): ParsedArgs {\n const flags: Record<string, string | true> = {};\n const rest: string[] = [];\n let command: string | undefined;\n for (const arg of argv) {\n if (arg.startsWith(\"--\")) {\n const eq = arg.indexOf(\"=\");\n if (eq === -1) {\n flags[arg.slice(2)] = true;\n } else {\n flags[arg.slice(2, eq)] = arg.slice(eq + 1);\n }\n } else if (!command) {\n command = arg;\n } else {\n rest.push(arg);\n }\n }\n return { command, flags, rest };\n}\n\nfunction printHelp(): void {\n console.log(`react-router-next — typed filesystem routing for React Router 7\n\nUsage:\n react-router-next typegen [options]\n\nOptions:\n --app-dir <path> Source directory of pages/layouts (default: src/app)\n --out-dir <path> Where the routes.d.ts shim is written\n (default: node_modules/.react-router-next)\n --help, -h Show this message\n`);\n}\n\nasync function main(): Promise<void> {\n const { command, flags } = parseArgs(process.argv.slice(2));\n\n if (flags.help || flags.h || command === \"help\") {\n printHelp();\n return;\n }\n\n if (command !== \"typegen\") {\n if (command) {\n console.error(`[react-router-next] Unknown command: ${command}`);\n }\n printHelp();\n process.exit(command ? 1 : 0);\n }\n\n const result = generateRouteTypes({\n appDir: typeof flags[\"app-dir\"] === \"string\" ? flags[\"app-dir\"] : undefined,\n outDir: typeof flags[\"out-dir\"] === \"string\" ? flags[\"out-dir\"] : undefined,\n });\n\n console.log(\n `[react-router-next] ${result.routeKeys.length} route(s); shim ${\n result.written ? \"updated\" : \"unchanged\"\n } at ${result.shimPath}`,\n );\n}\n\nmain().catch((err: unknown) => {\n console.error(err);\n process.exit(1);\n});\n"],"mappings":";;;AAAA,SAAS,WAAW,cAAc,qBAAqB;AACvD,SAAS,SAAS,YAAY,MAAM,eAAe;;;ACDnD,SAAS,mBAAmB;AAC5B,SAAS,gBAAgB;AAEzB,IAAM,oBAAoB;AAEnB,SAAS,QAAQ,GAAmB;AACzC,SAAO,EAAE,MAAM,IAAI,EAAE,KAAK,GAAG;AAC/B;AAOO,SAAS,iBAAiB,KAAsB;AACrD,SAAO,IAAI,WAAW,GAAG;AAC3B;AAEO,SAAS,cAAc,KAAsB;AAClD,SAAO,IAAI,WAAW,GAAG,KAAK,IAAI,SAAS;AAC7C;AAMO,SAAS,qBAAqB,KAAoC;AAEvE,MAAI,IAAI,WAAW,OAAO,EAAG,QAAO,EAAE,OAAO,QAAQ,MAAM,IAAI,MAAM,CAAC,EAAE;AACxE,MAAI,IAAI,WAAW,UAAU,EAAG,QAAO,EAAE,OAAO,GAAG,MAAM,IAAI,MAAM,CAAC,EAAE;AACtE,MAAI,IAAI,WAAW,MAAM,EAAG,QAAO,EAAE,OAAO,GAAG,MAAM,IAAI,MAAM,CAAC,EAAE;AAClE,MAAI,IAAI,WAAW,KAAK,EAAG,QAAO,EAAE,OAAO,GAAG,MAAM,IAAI,MAAM,CAAC,EAAE;AACjE,SAAO;AACT;AAUO,SAAS,WAAW,QAA4B;AACrD,MAAI;AACJ,MAAI;AACF,cAAU,YAAY,QAAQ,EAAE,WAAW,MAAM,eAAe,KAAK,CAAC;AAAA,EACxE,QAAQ;AACN,WAAO,EAAE,WAAW,CAAC,EAAE;AAAA,EACzB;AACA,QAAM,YAAY,oBAAI,IAAY;AAClC,aAAW,SAAS,SAAS;AAC3B,QAAI,CAAC,MAAM,OAAO,EAAG;AACrB,QAAI,CAAC,kBAAkB,KAAK,MAAM,IAAI,EAAG;AACzC,UAAM,MACH,MAA4D,cAC5D,MAAuC,QACxC;AACF,UAAM,MAAM,QAAQ,SAAS,QAAQ,GAAG,CAAC;AACzC,QAAI,QAAQ,MAAM,IAAI,MAAM,GAAG,EAAE,KAAK,gBAAgB,EAAG;AACzD,cAAU,IAAI,GAAG;AAAA,EACnB;AACA,SAAO,EAAE,WAAW,CAAC,GAAG,SAAS,EAAE;AACrC;AAEA,SAAS,mBAAmB,OAAoC;AAG9D,SAAO,MAAM,OAAO,CAAC,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;AACtE;AAiBO,SAAS,gBAAgB,OAAkC;AAChE,MAAI,eAAe;AACnB,MAAI,YAAmC;AACvC,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAM,IAAI,qBAAqB,MAAM,CAAC,CAAC;AACvC,QAAI,GAAG;AACL,qBAAe;AACf,kBAAY;AACZ;AAAA,IACF;AAAA,EACF;AAEA,MAAI,cAAc,KAAM,QAAO,mBAAmB,KAAK,EAAE,KAAK,GAAG;AAEjE,QAAM,WAAW,MAAM,MAAM,GAAG,YAAY;AAC5C,MAAI;AACJ,MAAI,UAAU,UAAU,QAAQ;AAC9B,eAAW,CAAC;AAAA,EACd,OAAO;AACL,UAAM,WAAW,UAAU,QAAQ;AACnC,eAAW,SAAS,MAAM,GAAG,KAAK,IAAI,GAAG,SAAS,SAAS,QAAQ,CAAC;AAAA,EACtE;AACA,QAAM,aAAa,mBAAmB,QAAQ;AAC9C,QAAM,OAAO,MAAM,MAAM,eAAe,CAAC;AACzC,QAAM,eAAyB,CAAC;AAChC,MAAI,UAAU,KAAM,cAAa,KAAK,UAAU,IAAI;AACpD,eAAa,KAAK,GAAG,IAAI;AACzB,SAAO,CAAC,GAAG,YAAY,GAAG,mBAAmB,YAAY,CAAC,EAAE,KAAK,GAAG;AACtE;AAEO,SAAS,YAAY,QAAgB,UAA0B;AACpE,QAAM,MAAM,QAAQ,SAAS,QAAQ,QAAQ,CAAC;AAC9C,MAAI,QAAQ,GAAI,QAAO;AACvB,SAAO,gBAAgB,IAAI,MAAM,GAAG,CAAC;AACvC;AAEO,SAAS,eAAe,UAA2B;AACxD,SAAO,SAAS,SAAS,GAAG;AAC9B;;;AC1HA,IAAM,SAAS;AA4Cf,SAAS,kBAAkB,UAA0B;AACnD,MAAI,CAAC,eAAe,QAAQ,GAAG;AAC7B,WAAO;AAAA,gBACK,KAAK,UAAU,QAAQ,CAAC;AAAA;AAAA;AAAA;AAAA,EAItC;AACA,SAAO;AAAA,gBACO,KAAK,UAAU,QAAQ,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAMxC;AAEO,SAAS,aAAa,UAA0B;AACrD,SAAO,6BAA6B,aAAa,KAAK,UAAU,QAAQ;AAC1E;AAQO,SAAS,cAAc,WAAsC;AAClE,QAAM,SAAS,UAAU,IAAI,CAAC,QAAQ;AACpC,UAAM,KAAK,aAAa,GAAG;AAC3B,WAAO,kBAAkB,KAAK,UAAU,EAAE,CAAC;AAAA,EAC7C,kBAAkB,GAAG,CAAC;AAAA;AAAA,EAEtB,CAAC;AACD,SAAO,GAAG,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBhB,OAAO,KAAK,IAAI,CAAC;AACnB;;;AF9EA,SAAS,eAAe,MAAc,GAAmB;AACvD,SAAO,WAAW,CAAC,IAAI,IAAI,QAAQ,MAAM,CAAC;AAC5C;AAEA,SAAS,eAAe,MAAc,UAA2B;AAC/D,MAAI;AACF,UAAM,WAAW,aAAa,MAAM,MAAM;AAC1C,QAAI,aAAa,SAAU,QAAO;AAAA,EACpC,QAAQ;AAAA,EAER;AACA,YAAU,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC5C,gBAAc,MAAM,QAAQ;AAC5B,SAAO;AACT;AAEO,SAAS,mBAAmB,OAAwB,CAAC,GAAmB;AAC7E,QAAM,OAAO,KAAK,QAAQ,QAAQ,IAAI;AACtC,QAAM,SAAS,eAAe,MAAM,KAAK,UAAU,SAAS;AAC5D,QAAM,SAAS;AAAA,IACb;AAAA,IACA,KAAK,UAAU;AAAA,EACjB;AAEA,QAAM,EAAE,UAAU,IAAI,WAAW,MAAM;AACvC,QAAM,YAAY;AAAA,IAChB,GAAG,IAAI,IAAI,UAAU,IAAI,CAAC,QAAQ,YAAY,QAAQ,GAAG,CAAC,CAAC;AAAA,EAC7D,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC;AAEnC,QAAM,WAAW,KAAK,QAAQ,aAAa;AAC3C,QAAM,UAAU,eAAe,UAAU,cAAc,SAAS,CAAC;AAEjE,SAAO,EAAE,QAAQ,QAAQ,WAAW,UAAU,QAAQ;AACxD;;;AG9CA,SAAS,UAAU,MAA4B;AAC7C,QAAM,QAAuC,CAAC;AAC9C,QAAM,OAAiB,CAAC;AACxB,MAAI;AACJ,aAAW,OAAO,MAAM;AACtB,QAAI,IAAI,WAAW,IAAI,GAAG;AACxB,YAAM,KAAK,IAAI,QAAQ,GAAG;AAC1B,UAAI,OAAO,IAAI;AACb,cAAM,IAAI,MAAM,CAAC,CAAC,IAAI;AAAA,MACxB,OAAO;AACL,cAAM,IAAI,MAAM,GAAG,EAAE,CAAC,IAAI,IAAI,MAAM,KAAK,CAAC;AAAA,MAC5C;AAAA,IACF,WAAW,CAAC,SAAS;AACnB,gBAAU;AAAA,IACZ,OAAO;AACL,WAAK,KAAK,GAAG;AAAA,IACf;AAAA,EACF;AACA,SAAO,EAAE,SAAS,OAAO,KAAK;AAChC;AAEA,SAAS,YAAkB;AACzB,UAAQ,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAUb;AACD;AAEA,eAAe,OAAsB;AACnC,QAAM,EAAE,SAAS,MAAM,IAAI,UAAU,QAAQ,KAAK,MAAM,CAAC,CAAC;AAE1D,MAAI,MAAM,QAAQ,MAAM,KAAK,YAAY,QAAQ;AAC/C,cAAU;AACV;AAAA,EACF;AAEA,MAAI,YAAY,WAAW;AACzB,QAAI,SAAS;AACX,cAAQ,MAAM,wCAAwC,OAAO,EAAE;AAAA,IACjE;AACA,cAAU;AACV,YAAQ,KAAK,UAAU,IAAI,CAAC;AAAA,EAC9B;AAEA,QAAM,SAAS,mBAAmB;AAAA,IAChC,QAAQ,OAAO,MAAM,SAAS,MAAM,WAAW,MAAM,SAAS,IAAI;AAAA,IAClE,QAAQ,OAAO,MAAM,SAAS,MAAM,WAAW,MAAM,SAAS,IAAI;AAAA,EACpE,CAAC;AAED,UAAQ;AAAA,IACN,uBAAuB,OAAO,UAAU,MAAM,mBAC5C,OAAO,UAAU,YAAY,WAC/B,OAAO,OAAO,QAAQ;AAAA,EACxB;AACF;AAEA,KAAK,EAAE,MAAM,CAAC,QAAiB;AAC7B,UAAQ,MAAM,GAAG;AACjB,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":[]}
@@ -0,0 +1,39 @@
1
+ import { JSX, ComponentType, ReactNode } from 'react';
2
+ import { LoaderFunction, RouteObject } from 'react-router';
3
+
4
+ declare function AppRouter(): JSX.Element;
5
+
6
+ type RouteParamsRecord = Record<string, string | string[] | undefined>;
7
+ type RouteModule = {
8
+ default?: ComponentType<{
9
+ params?: RouteParamsRecord;
10
+ children?: ReactNode;
11
+ [slot: string]: unknown;
12
+ }>;
13
+ loader?: LoaderFunction;
14
+ };
15
+ type RouteModuleMap = Record<string, RouteModule>;
16
+ declare function buildRoutesFromModules(modules: RouteModuleMap, appDir: string): RouteObject[];
17
+
18
+ type ParseSegment<S extends string> = S extends `[[...${infer Name}]]` ? {
19
+ [K in Name]?: string[];
20
+ } : S extends `[...${infer Name}]` ? {
21
+ [K in Name]: string[];
22
+ } : S extends `[[${infer Name}]]` ? {
23
+ [K in Name]?: string;
24
+ } : S extends `[${infer Name}]` ? {
25
+ [K in Name]: string;
26
+ } : S extends `(${string})` ? Record<never, never> : S extends `@${string}` ? Record<never, never> : Record<never, never>;
27
+ type ParseRoute<S extends string> = S extends `${infer Head}/${infer Tail}` ? ParseSegment<Head> & ParseRoute<Tail> : ParseSegment<S>;
28
+ type RouteParams<S extends string> = string extends S ? Record<string, string | string[] | undefined> : {
29
+ [K in keyof ParseRoute<S>]: ParseRoute<S>[K];
30
+ };
31
+ type RouteProps<S extends string> = {
32
+ params: RouteParams<S>;
33
+ };
34
+ declare function parseRouteParams<S extends string>(route: S, rrParams: Readonly<Record<string, string | undefined>>): RouteParams<S>;
35
+ declare function useRouteParams<S extends string>(route: S): RouteParams<S>;
36
+
37
+ declare function generateUrl<S extends string>(route: S, params: RouteParams<S>): string;
38
+
39
+ export { AppRouter, type RouteModule, type RouteModuleMap, type RouteParams, type RouteProps, buildRoutesFromModules, generateUrl, parseRouteParams, useRouteParams };