@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/dist/vite.js ADDED
@@ -0,0 +1,276 @@
1
+ // src/plugin/plugin.ts
2
+ import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2 } from "path";
3
+
4
+ // src/plugin/scan.ts
5
+ import { readdirSync } from "fs";
6
+ import { relative } from "path";
7
+ var ROUTE_DIR_FILE_RE = /^(page|layout|default|template)\.(tsx|jsx|ts|js)$/;
8
+ function toPosix(p) {
9
+ return p.split("\\").join("/");
10
+ }
11
+ function isPrivateSegment(seg) {
12
+ return seg.startsWith("_");
13
+ }
14
+ function isSlotSegment(seg) {
15
+ return seg.startsWith("@") && seg.length > 1;
16
+ }
17
+ function parseInterceptPrefix(seg) {
18
+ if (seg.startsWith("(...)")) return { depth: "root", rest: seg.slice(5) };
19
+ if (seg.startsWith("(..)(..)")) return { depth: 3, rest: seg.slice(8) };
20
+ if (seg.startsWith("(..)")) return { depth: 2, rest: seg.slice(4) };
21
+ if (seg.startsWith("(.)")) return { depth: 1, rest: seg.slice(3) };
22
+ return null;
23
+ }
24
+ function scanAppDir(appDir) {
25
+ let entries;
26
+ try {
27
+ entries = readdirSync(appDir, { recursive: true, withFileTypes: true });
28
+ } catch {
29
+ return { routeDirs: [] };
30
+ }
31
+ const routeDirs = /* @__PURE__ */ new Set();
32
+ for (const entry of entries) {
33
+ if (!entry.isFile()) continue;
34
+ if (!ROUTE_DIR_FILE_RE.test(entry.name)) continue;
35
+ const dir = entry.parentPath ?? entry.path ?? appDir;
36
+ const rel = toPosix(relative(appDir, dir));
37
+ if (rel !== "" && rel.split("/").some(isPrivateSegment)) continue;
38
+ routeDirs.add(dir);
39
+ }
40
+ return { routeDirs: [...routeDirs] };
41
+ }
42
+ function routeKeySegmentsOf(parts) {
43
+ return parts.filter((s) => !isSlotSegment(s) && !isPrivateSegment(s));
44
+ }
45
+ function computeRouteKey(parts) {
46
+ let interceptIdx = -1;
47
+ let intercept = null;
48
+ for (let i = 0; i < parts.length; i++) {
49
+ const p = parseInterceptPrefix(parts[i]);
50
+ if (p) {
51
+ interceptIdx = i;
52
+ intercept = p;
53
+ break;
54
+ }
55
+ }
56
+ if (intercept === null) return routeKeySegmentsOf(parts).join("/");
57
+ const fsPrefix = parts.slice(0, interceptIdx);
58
+ let resolved;
59
+ if (intercept.depth === "root") {
60
+ resolved = [];
61
+ } else {
62
+ const popCount = intercept.depth - 1;
63
+ resolved = fsPrefix.slice(0, Math.max(0, fsPrefix.length - popCount));
64
+ }
65
+ const prefixSegs = routeKeySegmentsOf(resolved);
66
+ const tail = parts.slice(interceptIdx + 1);
67
+ const restSegments = [];
68
+ if (intercept.rest) restSegments.push(intercept.rest);
69
+ restSegments.push(...tail);
70
+ return [...prefixSegs, ...routeKeySegmentsOf(restSegments)].join("/");
71
+ }
72
+ function routeKeyFor(appDir, routeDir) {
73
+ const rel = toPosix(relative(appDir, routeDir));
74
+ if (rel === "") return "";
75
+ return computeRouteKey(rel.split("/"));
76
+ }
77
+ function routeHasParams(routeKey) {
78
+ return routeKey.includes("[");
79
+ }
80
+ var ROUTE_FILE_RE = /[\\/](page|layout|loader|loading|error|default|template|not-found)\.(tsx|jsx|ts|js)$/;
81
+
82
+ // src/plugin/render.ts
83
+ var HEADER = "// AUTO-GENERATED by react-router-next typegen \u2014 do not edit.";
84
+ function renderRuntimeModule(routeKey) {
85
+ if (!routeHasParams(routeKey)) {
86
+ return `${HEADER}
87
+ import { generateUrl } from "@evolonix/react-router-next";
88
+
89
+ const PATH = ${JSON.stringify(routeKey)};
90
+
91
+ export function generate() {
92
+ return generateUrl(PATH, {});
93
+ }
94
+ `;
95
+ }
96
+ return `${HEADER}
97
+ import {
98
+ generateUrl,
99
+ useRouteParams as useRouteParamsBase,
100
+ } from "@evolonix/react-router-next";
101
+
102
+ const PATH = ${JSON.stringify(routeKey)};
103
+
104
+ export function useRouteParams() {
105
+ return useRouteParamsBase(PATH);
106
+ }
107
+
108
+ export function generate(params) {
109
+ return generateUrl(PATH, params);
110
+ }
111
+ `;
112
+ }
113
+ function renderDeclareBody(routeKey) {
114
+ if (!routeHasParams(routeKey)) {
115
+ return ` import type { RouteParams as RouteParamsBase } from "@evolonix/react-router-next";
116
+ const PATH: ${JSON.stringify(routeKey)};
117
+ export type RouteParams = RouteParamsBase<typeof PATH>;
118
+ export function generate(): string;
119
+ `;
120
+ }
121
+ return ` import type { RouteParams as RouteParamsBase } from "@evolonix/react-router-next";
122
+ const PATH: ${JSON.stringify(routeKey)};
123
+ export type RouteParams = RouteParamsBase<typeof PATH>;
124
+ export type RouteProps = { params: RouteParams };
125
+ export function useRouteParams(): RouteParams;
126
+ export function generate(params: RouteParams): string;
127
+ `;
128
+ }
129
+ function virtualIdFor(routeKey) {
130
+ return `virtual:react-router-next/${routeKey === "" ? "_root" : routeKey}`;
131
+ }
132
+ function renderDtsShim(routeKeys) {
133
+ const blocks = routeKeys.map((key) => {
134
+ const id = virtualIdFor(key);
135
+ return `declare module ${JSON.stringify(id)} {
136
+ ${renderDeclareBody(key)}}
137
+ `;
138
+ });
139
+ return `${HEADER}
140
+ // Ambient declarations for react-router-next virtual route modules.
141
+ // Generated by \`react-router-next typegen\` and the routeTypegen Vite plugin.
142
+
143
+ declare module "virtual:react-router-next/app-tree" {
144
+ import type { ComponentType } from "react";
145
+ import type { LoaderFunction } from "react-router";
146
+
147
+ export type RouteModule = {
148
+ default?: ComponentType<{
149
+ params?: Record<string, string | string[] | undefined>;
150
+ }>;
151
+ loader?: LoaderFunction;
152
+ };
153
+
154
+ export const modules: Record<string, RouteModule>;
155
+ export const appDir: string;
156
+ }
157
+
158
+ ${blocks.join("\n")}`;
159
+ }
160
+
161
+ // src/plugin/typegen.ts
162
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
163
+ import { dirname, isAbsolute, join, resolve } from "path";
164
+ function resolveAgainst(root, p) {
165
+ return isAbsolute(p) ? p : resolve(root, p);
166
+ }
167
+ function writeIfChanged(path, contents) {
168
+ try {
169
+ const existing = readFileSync(path, "utf8");
170
+ if (existing === contents) return false;
171
+ } catch {
172
+ }
173
+ mkdirSync(dirname(path), { recursive: true });
174
+ writeFileSync(path, contents);
175
+ return true;
176
+ }
177
+ function generateRouteTypes(opts = {}) {
178
+ const root = opts.root ?? process.cwd();
179
+ const appDir = resolveAgainst(root, opts.appDir ?? "src/app");
180
+ const outDir = resolveAgainst(
181
+ root,
182
+ opts.outDir ?? "node_modules/.react-router-next"
183
+ );
184
+ const { routeDirs } = scanAppDir(appDir);
185
+ const routeKeys = [
186
+ ...new Set(routeDirs.map((dir) => routeKeyFor(appDir, dir)))
187
+ ].sort((a, b) => a.localeCompare(b));
188
+ const shimPath = join(outDir, "routes.d.ts");
189
+ const written = writeIfChanged(shimPath, renderDtsShim(routeKeys));
190
+ return { appDir, outDir, routeKeys, shimPath, written };
191
+ }
192
+
193
+ // src/plugin/plugin.ts
194
+ var VIRTUAL_PREFIX = "virtual:react-router-next/";
195
+ var APP_TREE_ID = `${VIRTUAL_PREFIX}app-tree`;
196
+ var RESOLVED_PREFIX = "\0";
197
+ function isOurVirtual(id) {
198
+ return id.startsWith(VIRTUAL_PREFIX);
199
+ }
200
+ function resolveOpt(root, p) {
201
+ return isAbsolute2(p) ? p : resolve2(root, p);
202
+ }
203
+ function routeTypegen(options = {}) {
204
+ let root = process.cwd();
205
+ let appDir = "";
206
+ let outDir = "";
207
+ let routeKeys = /* @__PURE__ */ new Set();
208
+ function resolvePaths(viteRoot) {
209
+ root = viteRoot;
210
+ appDir = resolveOpt(root, options.appDir ?? "src/app");
211
+ outDir = resolveOpt(
212
+ root,
213
+ options.outDir ?? "node_modules/.react-router-next"
214
+ );
215
+ }
216
+ function regenerate() {
217
+ const result = generateRouteTypes({ root, appDir, outDir });
218
+ routeKeys = new Set(result.routeKeys);
219
+ }
220
+ function refreshKnownKeys() {
221
+ const { routeDirs } = scanAppDir(appDir);
222
+ routeKeys = new Set(routeDirs.map((d) => routeKeyFor(appDir, d)));
223
+ }
224
+ return {
225
+ name: "react-router-next:typegen",
226
+ enforce: "pre",
227
+ configResolved(config) {
228
+ resolvePaths(config.root);
229
+ refreshKnownKeys();
230
+ },
231
+ buildStart() {
232
+ regenerate();
233
+ },
234
+ configureServer(server) {
235
+ const onChange = (file) => {
236
+ if (ROUTE_FILE_RE.test(file)) regenerate();
237
+ };
238
+ server.watcher.on("add", onChange);
239
+ server.watcher.on("unlink", onChange);
240
+ },
241
+ resolveId(id) {
242
+ if (id === APP_TREE_ID) return RESOLVED_PREFIX + APP_TREE_ID;
243
+ if (isOurVirtual(id)) return RESOLVED_PREFIX + id;
244
+ return null;
245
+ },
246
+ load(id) {
247
+ if (!id.startsWith(RESOLVED_PREFIX)) return null;
248
+ const realId = id.slice(RESOLVED_PREFIX.length);
249
+ if (realId === APP_TREE_ID) {
250
+ const rootRelative = "/" + toPosix(relative2(root, appDir)).replace(/^\/+/, "");
251
+ const pattern = `${rootRelative}/**/{page,layout,loader,loading,error,default,template,not-found}.{tsx,jsx,ts,js}`;
252
+ return `const modules = import.meta.glob(${JSON.stringify(pattern)}, { eager: true });
253
+ const appDir = ${JSON.stringify(rootRelative)};
254
+ export { modules, appDir };
255
+ `;
256
+ }
257
+ if (!isOurVirtual(realId)) return null;
258
+ const slug = realId.slice(VIRTUAL_PREFIX.length);
259
+ const routeKey = slug === "_root" ? "" : slug;
260
+ if (!routeKeys.has(routeKey)) {
261
+ refreshKnownKeys();
262
+ if (!routeKeys.has(routeKey)) {
263
+ this.error(
264
+ `[react-router-next] Unknown route "${routeKey}". Expected a page.tsx or layout.tsx under ${appDir}.`
265
+ );
266
+ }
267
+ }
268
+ return renderRuntimeModule(routeKey);
269
+ }
270
+ };
271
+ }
272
+ export {
273
+ generateRouteTypes,
274
+ routeTypegen
275
+ };
276
+ //# sourceMappingURL=vite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/plugin/plugin.ts","../src/plugin/scan.ts","../src/plugin/render.ts","../src/plugin/typegen.ts"],"sourcesContent":["import { isAbsolute, relative, resolve } from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport { renderRuntimeModule } from \"./render\";\nimport { ROUTE_FILE_RE, routeKeyFor, scanAppDir, toPosix } from \"./scan\";\nimport { generateRouteTypes } from \"./typegen\";\n\nexport type RouteTypegenOptions = {\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\nconst VIRTUAL_PREFIX = \"virtual:react-router-next/\";\nconst APP_TREE_ID = `${VIRTUAL_PREFIX}app-tree`;\n\nconst RESOLVED_PREFIX = \"\\0\";\n\nfunction isOurVirtual(id: string): boolean {\n return id.startsWith(VIRTUAL_PREFIX);\n}\n\nfunction resolveOpt(root: string, p: string): string {\n return isAbsolute(p) ? p : resolve(root, p);\n}\n\nexport function routeTypegen(options: RouteTypegenOptions = {}): Plugin {\n let root = process.cwd();\n let appDir = \"\";\n let outDir = \"\";\n let routeKeys = new Set<string>();\n\n function resolvePaths(viteRoot: string): void {\n root = viteRoot;\n appDir = resolveOpt(root, options.appDir ?? \"src/app\");\n outDir = resolveOpt(\n root,\n options.outDir ?? \"node_modules/.react-router-next\",\n );\n }\n\n function regenerate(): void {\n const result = generateRouteTypes({ root, appDir, outDir });\n routeKeys = new Set(result.routeKeys);\n }\n\n function refreshKnownKeys(): void {\n const { routeDirs } = scanAppDir(appDir);\n routeKeys = new Set(routeDirs.map((d) => routeKeyFor(appDir, d)));\n }\n\n return {\n name: \"react-router-next:typegen\",\n enforce: \"pre\",\n\n configResolved(config) {\n resolvePaths(config.root);\n refreshKnownKeys();\n },\n\n buildStart() {\n regenerate();\n },\n\n configureServer(server) {\n const onChange = (file: string): void => {\n if (ROUTE_FILE_RE.test(file)) regenerate();\n };\n server.watcher.on(\"add\", onChange);\n server.watcher.on(\"unlink\", onChange);\n },\n\n resolveId(id) {\n if (id === APP_TREE_ID) return RESOLVED_PREFIX + APP_TREE_ID;\n if (isOurVirtual(id)) return RESOLVED_PREFIX + id;\n return null;\n },\n\n load(id) {\n if (!id.startsWith(RESOLVED_PREFIX)) return null;\n const realId = id.slice(RESOLVED_PREFIX.length);\n\n if (realId === APP_TREE_ID) {\n // Vite resolves `import.meta.glob` patterns relative to the project\n // root when they begin with `/`. Virtual modules have no importer\n // path, so absolute filesystem paths or `./` patterns won't match —\n // root-relative is the only form that works here. The keys Vite\n // returns are also root-relative (e.g. \"/src/app/page.tsx\"), so we\n // export a matching `appDir` for the tree builder to strip.\n const rootRelative =\n \"/\" + toPosix(relative(root, appDir)).replace(/^\\/+/, \"\");\n const pattern = `${rootRelative}/**/{page,layout,loader,loading,error,default,template,not-found}.{tsx,jsx,ts,js}`;\n return `\\\nconst modules = import.meta.glob(${JSON.stringify(pattern)}, { eager: true });\nconst appDir = ${JSON.stringify(rootRelative)};\nexport { modules, appDir };\n`;\n }\n\n if (!isOurVirtual(realId)) return null;\n const slug = realId.slice(VIRTUAL_PREFIX.length);\n const routeKey = slug === \"_root\" ? \"\" : slug;\n if (!routeKeys.has(routeKey)) {\n // Refresh in case a new page was just added before the watcher fired.\n refreshKnownKeys();\n if (!routeKeys.has(routeKey)) {\n this.error(\n `[react-router-next] Unknown route \"${routeKey}\". ` +\n `Expected a page.tsx or layout.tsx under ${appDir}.`,\n );\n }\n }\n return renderRuntimeModule(routeKey);\n },\n };\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","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"],"mappings":"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","names":["isAbsolute","relative","resolve","isAbsolute","resolve","relative"]}
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@evolonix/react-router-next",
3
+ "version": "0.2.0",
4
+ "description": "Next.js-style filesystem routing for React Router 7, with a Vite plugin that generates per-route typed params.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "bin": {
9
+ "react-router-next": "./dist/cli.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ },
16
+ "./vite": {
17
+ "types": "./dist/vite.d.ts",
18
+ "import": "./dist/vite.js"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "dev": "tsup --watch",
29
+ "typecheck": "tsc --noEmit",
30
+ "prepare": "npm run build",
31
+ "prepublishOnly": "npm run build"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public",
35
+ "provenance": true
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/evolonix/react-router-next.git",
40
+ "directory": "packages/react-router-next"
41
+ },
42
+ "bugs": {
43
+ "url": "https://github.com/evolonix/react-router-next/issues"
44
+ },
45
+ "homepage": "https://github.com/evolonix/react-router-next#readme",
46
+ "peerDependencies": {
47
+ "react": ">=19",
48
+ "react-dom": ">=19",
49
+ "react-router": ">=7",
50
+ "vite": ">=5"
51
+ },
52
+ "peerDependenciesMeta": {
53
+ "vite": {
54
+ "optional": true
55
+ }
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "^24.12.2",
59
+ "@types/react": "^19.2.14",
60
+ "@types/react-dom": "^19.2.3",
61
+ "react": "^19.2.5",
62
+ "react-dom": "^19.2.5",
63
+ "react-router": "^7.14.2",
64
+ "tsup": "^8.3.0",
65
+ "typescript": "~6.0.2",
66
+ "vite": "^8.0.10"
67
+ },
68
+ "engines": {
69
+ "node": ">=20"
70
+ },
71
+ "keywords": [
72
+ "react",
73
+ "react-router",
74
+ "router",
75
+ "filesystem-routing",
76
+ "app-router",
77
+ "nextjs",
78
+ "vite",
79
+ "vite-plugin",
80
+ "typegen"
81
+ ]
82
+ }