@rangojs/router 0.0.0-experimental.10
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/CLAUDE.md +43 -0
- package/README.md +19 -0
- package/dist/bin/rango.js +227 -0
- package/dist/vite/index.js +3039 -0
- package/package.json +171 -0
- package/skills/caching/SKILL.md +191 -0
- package/skills/debug-manifest/SKILL.md +108 -0
- package/skills/document-cache/SKILL.md +180 -0
- package/skills/fonts/SKILL.md +165 -0
- package/skills/hooks/SKILL.md +442 -0
- package/skills/intercept/SKILL.md +190 -0
- package/skills/layout/SKILL.md +213 -0
- package/skills/links/SKILL.md +180 -0
- package/skills/loader/SKILL.md +246 -0
- package/skills/middleware/SKILL.md +202 -0
- package/skills/mime-routes/SKILL.md +124 -0
- package/skills/parallel/SKILL.md +228 -0
- package/skills/prerender/SKILL.md +283 -0
- package/skills/rango/SKILL.md +54 -0
- package/skills/response-routes/SKILL.md +358 -0
- package/skills/route/SKILL.md +173 -0
- package/skills/router-setup/SKILL.md +346 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +78 -0
- package/skills/typesafety/SKILL.md +394 -0
- package/src/__internal.ts +175 -0
- package/src/bin/rango.ts +24 -0
- package/src/browser/event-controller.ts +876 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/link-interceptor.ts +121 -0
- package/src/browser/lru-cache.ts +69 -0
- package/src/browser/merge-segment-loaders.ts +126 -0
- package/src/browser/navigation-bridge.ts +913 -0
- package/src/browser/navigation-client.ts +165 -0
- package/src/browser/navigation-store.ts +823 -0
- package/src/browser/partial-update.ts +600 -0
- package/src/browser/react/Link.tsx +248 -0
- package/src/browser/react/NavigationProvider.tsx +346 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +53 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +120 -0
- package/src/browser/react/location-state.ts +62 -0
- package/src/browser/react/mount-context.ts +32 -0
- package/src/browser/react/use-action.ts +240 -0
- package/src/browser/react/use-client-cache.ts +56 -0
- package/src/browser/react/use-handle.ts +203 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +134 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +140 -0
- package/src/browser/react/use-segments.ts +188 -0
- package/src/browser/request-controller.ts +164 -0
- package/src/browser/rsc-router.tsx +352 -0
- package/src/browser/scroll-restoration.ts +324 -0
- package/src/browser/segment-structure-assert.ts +67 -0
- package/src/browser/server-action-bridge.ts +762 -0
- package/src/browser/shallow.ts +35 -0
- package/src/browser/types.ts +478 -0
- package/src/build/generate-manifest.ts +377 -0
- package/src/build/generate-route-types.ts +828 -0
- package/src/build/index.ts +36 -0
- package/src/build/route-trie.ts +239 -0
- package/src/cache/cache-scope.ts +563 -0
- package/src/cache/cf/cf-cache-store.ts +428 -0
- package/src/cache/cf/index.ts +19 -0
- package/src/cache/document-cache.ts +340 -0
- package/src/cache/index.ts +58 -0
- package/src/cache/memory-segment-store.ts +150 -0
- package/src/cache/memory-store.ts +253 -0
- package/src/cache/types.ts +392 -0
- package/src/client.rsc.tsx +83 -0
- package/src/client.tsx +643 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +23 -0
- package/src/debug.ts +233 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +295 -0
- package/src/handle.ts +130 -0
- package/src/handles/MetaTags.tsx +193 -0
- package/src/handles/index.ts +6 -0
- package/src/handles/meta.ts +247 -0
- package/src/host/cookie-handler.ts +159 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +56 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +330 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +138 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +202 -0
- package/src/href-context.ts +33 -0
- package/src/index.rsc.ts +121 -0
- package/src/index.ts +165 -0
- package/src/loader.rsc.ts +207 -0
- package/src/loader.ts +47 -0
- package/src/network-error-thrower.tsx +21 -0
- package/src/outlet-context.ts +15 -0
- package/src/prerender/param-hash.ts +35 -0
- package/src/prerender/store.ts +40 -0
- package/src/prerender.ts +156 -0
- package/src/reverse.ts +267 -0
- package/src/root-error-boundary.tsx +277 -0
- package/src/route-content-wrapper.tsx +193 -0
- package/src/route-definition.ts +1431 -0
- package/src/route-map-builder.ts +242 -0
- package/src/route-types.ts +220 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/handler-context.ts +158 -0
- package/src/router/intercept-resolution.ts +387 -0
- package/src/router/loader-resolution.ts +327 -0
- package/src/router/manifest.ts +216 -0
- package/src/router/match-api.ts +621 -0
- package/src/router/match-context.ts +264 -0
- package/src/router/match-middleware/background-revalidation.ts +236 -0
- package/src/router/match-middleware/cache-lookup.ts +382 -0
- package/src/router/match-middleware/cache-store.ts +276 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +281 -0
- package/src/router/match-middleware/segment-resolution.ts +184 -0
- package/src/router/match-pipelines.ts +214 -0
- package/src/router/match-result.ts +213 -0
- package/src/router/metrics.ts +62 -0
- package/src/router/middleware.ts +791 -0
- package/src/router/pattern-matching.ts +407 -0
- package/src/router/revalidation.ts +190 -0
- package/src/router/router-context.ts +301 -0
- package/src/router/segment-resolution.ts +1315 -0
- package/src/router/trie-matching.ts +172 -0
- package/src/router/types.ts +163 -0
- package/src/router.gen.ts +6 -0
- package/src/router.ts +2423 -0
- package/src/rsc/handler.ts +1443 -0
- package/src/rsc/helpers.ts +64 -0
- package/src/rsc/index.ts +56 -0
- package/src/rsc/nonce.ts +18 -0
- package/src/rsc/types.ts +236 -0
- package/src/segment-system.tsx +442 -0
- package/src/server/context.ts +466 -0
- package/src/server/handle-store.ts +229 -0
- package/src/server/loader-registry.ts +174 -0
- package/src/server/request-context.ts +554 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +171 -0
- package/src/ssr/index.tsx +296 -0
- package/src/theme/ThemeProvider.tsx +291 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +59 -0
- package/src/theme/index.ts +58 -0
- package/src/theme/theme-context.ts +70 -0
- package/src/theme/theme-script.ts +152 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types.ts +1757 -0
- package/src/urls.gen.ts +8 -0
- package/src/urls.ts +1282 -0
- package/src/use-loader.tsx +346 -0
- package/src/vite/expose-action-id.ts +344 -0
- package/src/vite/expose-handle-id.ts +209 -0
- package/src/vite/expose-loader-id.ts +426 -0
- package/src/vite/expose-location-state-id.ts +177 -0
- package/src/vite/expose-prerender-handler-id.ts +429 -0
- package/src/vite/index.ts +2068 -0
- package/src/vite/package-resolution.ts +125 -0
- package/src/vite/version.d.ts +12 -0
- package/src/vite/virtual-entries.ts +114 -0
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { join, dirname, resolve, relative, basename as pathBasename } from "node:path";
|
|
3
|
+
// @ts-ignore -- picomatch ships no .d.ts; types are trivial
|
|
4
|
+
import picomatch from "picomatch";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extract route definitions from source code by statically parsing path() calls.
|
|
8
|
+
* No code execution needed -- works on raw source text.
|
|
9
|
+
*
|
|
10
|
+
* Handles multi-line handlers with JSX, nested braces, string literals,
|
|
11
|
+
* and comments. Skips unnamed paths (no { name: "..." }).
|
|
12
|
+
*/
|
|
13
|
+
export function extractRoutesFromSource(
|
|
14
|
+
code: string
|
|
15
|
+
): Array<{ name: string; pattern: string }> {
|
|
16
|
+
const routes: Array<{ name: string; pattern: string }> = [];
|
|
17
|
+
const regex = /\bpath(?:\.(?:json|text|html|xml|image|stream|any))?\s*\(/g;
|
|
18
|
+
let match;
|
|
19
|
+
|
|
20
|
+
while ((match = regex.exec(code)) !== null) {
|
|
21
|
+
const result = parsePathCall(code, match.index + match[0].length);
|
|
22
|
+
if (result) routes.push(result);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return routes;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generate a per-module types file from extracted routes.
|
|
30
|
+
* Output has zero imports, preventing circular references.
|
|
31
|
+
*/
|
|
32
|
+
export function generatePerModuleTypesSource(
|
|
33
|
+
routes: Array<{ name: string; pattern: string }>
|
|
34
|
+
): string {
|
|
35
|
+
const valid = routes.filter(({ name }) => {
|
|
36
|
+
if (!name || /["'\\`\n\r]/.test(name)) {
|
|
37
|
+
console.warn(`[rsc-router] Skipping route with invalid name: ${JSON.stringify(name)}`);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Deduplicate by name (last definition wins for same name)
|
|
44
|
+
const deduped = new Map<string, string>();
|
|
45
|
+
for (const { name, pattern } of valid) {
|
|
46
|
+
deduped.set(name, pattern);
|
|
47
|
+
}
|
|
48
|
+
const sorted = [...deduped.entries()]
|
|
49
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
50
|
+
const body = sorted
|
|
51
|
+
.map(([name, pattern]) => {
|
|
52
|
+
// Quote names that aren't valid bare identifiers (dots, dashes, etc.)
|
|
53
|
+
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `"${name}"`;
|
|
54
|
+
return ` ${key}: "${pattern}",`;
|
|
55
|
+
})
|
|
56
|
+
.join("\n");
|
|
57
|
+
return `// Auto-generated by @rangojs/router - do not edit\nexport const routes = {\n${body}\n} as const;\nexport type routes = typeof routes;\n`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Mini-parser internals
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
function isWhitespace(ch: string): boolean {
|
|
65
|
+
return ch === " " || ch === "\t" || ch === "\n" || ch === "\r";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Read a single- or double-quoted string literal starting at pos. */
|
|
69
|
+
function readString(
|
|
70
|
+
code: string,
|
|
71
|
+
pos: number
|
|
72
|
+
): { value: string; end: number } | null {
|
|
73
|
+
const quote = code[pos];
|
|
74
|
+
if (quote !== '"' && quote !== "'") return null;
|
|
75
|
+
|
|
76
|
+
let value = "";
|
|
77
|
+
pos++;
|
|
78
|
+
while (pos < code.length) {
|
|
79
|
+
if (code[pos] === "\\") {
|
|
80
|
+
pos++;
|
|
81
|
+
if (pos < code.length) {
|
|
82
|
+
value += code[pos];
|
|
83
|
+
pos++;
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (code[pos] === quote) {
|
|
88
|
+
return { value, end: pos + 1 };
|
|
89
|
+
}
|
|
90
|
+
value += code[pos];
|
|
91
|
+
pos++;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Skip past any string literal (single, double, or template). */
|
|
97
|
+
function skipStringLiteral(code: string, pos: number): number {
|
|
98
|
+
const quote = code[pos];
|
|
99
|
+
|
|
100
|
+
if (quote === "`") {
|
|
101
|
+
pos++;
|
|
102
|
+
while (pos < code.length) {
|
|
103
|
+
if (code[pos] === "\\") {
|
|
104
|
+
pos += 2;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (code[pos] === "`") return pos + 1;
|
|
108
|
+
if (code[pos] === "$" && pos + 1 < code.length && code[pos + 1] === "{") {
|
|
109
|
+
pos += 2;
|
|
110
|
+
let braceDepth = 1;
|
|
111
|
+
while (pos < code.length && braceDepth > 0) {
|
|
112
|
+
if (code[pos] === "{") braceDepth++;
|
|
113
|
+
else if (code[pos] === "}") braceDepth--;
|
|
114
|
+
else if (code[pos] === "\\") pos++;
|
|
115
|
+
else if (
|
|
116
|
+
code[pos] === '"' ||
|
|
117
|
+
code[pos] === "'" ||
|
|
118
|
+
code[pos] === "`"
|
|
119
|
+
) {
|
|
120
|
+
pos = skipStringLiteral(code, pos);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (braceDepth > 0) pos++;
|
|
124
|
+
}
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
pos++;
|
|
128
|
+
}
|
|
129
|
+
return pos;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Simple single/double quoted string
|
|
133
|
+
pos++;
|
|
134
|
+
while (pos < code.length) {
|
|
135
|
+
if (code[pos] === "\\") {
|
|
136
|
+
pos += 2;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (code[pos] === quote) return pos + 1;
|
|
140
|
+
pos++;
|
|
141
|
+
}
|
|
142
|
+
return pos;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if code at pos starts with `name` as a standalone identifier
|
|
147
|
+
* followed by `:` (an object property).
|
|
148
|
+
*/
|
|
149
|
+
function matchesNameColon(code: string, pos: number): boolean {
|
|
150
|
+
if (code.slice(pos, pos + 4) !== "name") return false;
|
|
151
|
+
if (pos > 0 && /\w/.test(code[pos - 1])) return false;
|
|
152
|
+
const afterName = pos + 4;
|
|
153
|
+
if (afterName < code.length && /\w/.test(code[afterName])) return false;
|
|
154
|
+
let checkPos = afterName;
|
|
155
|
+
while (checkPos < code.length && isWhitespace(code[checkPos])) checkPos++;
|
|
156
|
+
return code[checkPos] === ":";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Extract the string value after `name:` starting at the `n` of `name`. */
|
|
160
|
+
function extractNameValue(
|
|
161
|
+
code: string,
|
|
162
|
+
pos: number
|
|
163
|
+
): { value: string; end: number } | null {
|
|
164
|
+
pos += 4; // skip 'name'
|
|
165
|
+
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
166
|
+
pos++; // skip ':'
|
|
167
|
+
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
168
|
+
return readString(code, pos);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Parse a single path() call starting right after the opening paren.
|
|
173
|
+
* Returns { name, pattern } or null if the call is unnamed.
|
|
174
|
+
*/
|
|
175
|
+
function parsePathCall(
|
|
176
|
+
code: string,
|
|
177
|
+
pos: number
|
|
178
|
+
): { name: string; pattern: string } | null {
|
|
179
|
+
// Skip whitespace to first argument
|
|
180
|
+
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
181
|
+
|
|
182
|
+
// First argument must be a string literal (the pattern)
|
|
183
|
+
const patternStr = readString(code, pos);
|
|
184
|
+
if (!patternStr) return null;
|
|
185
|
+
const pattern = patternStr.value;
|
|
186
|
+
pos = patternStr.end;
|
|
187
|
+
|
|
188
|
+
// Scan the rest of the call tracking depth.
|
|
189
|
+
// depth=1: inside path(), depth=2: inside an object/paren at top level of call.
|
|
190
|
+
// We look for `name: "..."` at depth 2 (options object properties).
|
|
191
|
+
let depth = 1;
|
|
192
|
+
let name: string | null = null;
|
|
193
|
+
|
|
194
|
+
while (pos < code.length && depth > 0) {
|
|
195
|
+
const ch = code[pos];
|
|
196
|
+
|
|
197
|
+
if (isWhitespace(ch)) {
|
|
198
|
+
pos++;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Line comment
|
|
203
|
+
if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "/") {
|
|
204
|
+
pos += 2;
|
|
205
|
+
while (pos < code.length && code[pos] !== "\n") pos++;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Block comment
|
|
210
|
+
if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "*") {
|
|
211
|
+
pos += 2;
|
|
212
|
+
while (
|
|
213
|
+
pos < code.length - 1 &&
|
|
214
|
+
!(code[pos] === "*" && code[pos + 1] === "/")
|
|
215
|
+
)
|
|
216
|
+
pos++;
|
|
217
|
+
pos += 2;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// At depth 2 (inside an object at call top-level), look for name: "..."
|
|
222
|
+
if (depth === 2 && ch === "n" && matchesNameColon(code, pos)) {
|
|
223
|
+
const nameResult = extractNameValue(code, pos);
|
|
224
|
+
if (nameResult) {
|
|
225
|
+
name = nameResult.value;
|
|
226
|
+
pos = nameResult.end;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Skip string literals.
|
|
232
|
+
// Treat ' preceded by a word char as an apostrophe (e.g. "shouldn't"),
|
|
233
|
+
// not a string delimiter. In valid JS/TS, opening ' is never preceded
|
|
234
|
+
// by a word character.
|
|
235
|
+
if (ch === '"' || ch === "`" || (ch === "'" && (pos === 0 || !/\w/.test(code[pos - 1])))) {
|
|
236
|
+
pos = skipStringLiteral(code, pos);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Track depth
|
|
241
|
+
if (ch === "(" || ch === "{" || ch === "[") depth++;
|
|
242
|
+
else if (ch === ")" || ch === "}" || ch === "]") depth--;
|
|
243
|
+
|
|
244
|
+
pos++;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (name === null) return null;
|
|
248
|
+
return { name, pattern };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Generates a .ts file that augments RSCRouter.GeneratedRouteMap
|
|
253
|
+
* with route name -> pattern mappings. This enables Handler<"routeName">
|
|
254
|
+
* without circular references since the file has no imports from the app.
|
|
255
|
+
*/
|
|
256
|
+
export function generateRouteTypesSource(
|
|
257
|
+
routeManifest: Record<string, string>
|
|
258
|
+
): string {
|
|
259
|
+
const entries = Object.entries(routeManifest).sort(([a], [b]) =>
|
|
260
|
+
a.localeCompare(b)
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const interfaceBody = entries
|
|
264
|
+
.map(([name, pattern]) => ` "${name}": "${pattern}";`)
|
|
265
|
+
.join("\n");
|
|
266
|
+
|
|
267
|
+
return `// Auto-generated by @rangojs/router - do not edit
|
|
268
|
+
export {};
|
|
269
|
+
|
|
270
|
+
declare global {
|
|
271
|
+
namespace RSCRouter {
|
|
272
|
+
interface GeneratedRouteMap {
|
|
273
|
+
${interfaceBody}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Default exclude patterns for route type scanning. */
|
|
281
|
+
export const DEFAULT_EXCLUDE_PATTERNS: string[] = [
|
|
282
|
+
"**/__tests__/**",
|
|
283
|
+
"**/__mocks__/**",
|
|
284
|
+
"**/dist/**",
|
|
285
|
+
"**/coverage/**",
|
|
286
|
+
"**/*.test.{ts,tsx}",
|
|
287
|
+
"**/*.spec.{ts,tsx}",
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
export type ScanFilter = (absolutePath: string) => boolean;
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Compile include/exclude glob patterns into a single predicate.
|
|
294
|
+
* Paths are made root-relative before matching.
|
|
295
|
+
* Returns undefined when no filtering is needed (no include, default exclude).
|
|
296
|
+
*/
|
|
297
|
+
export function createScanFilter(
|
|
298
|
+
root: string,
|
|
299
|
+
opts: { include?: string[]; exclude?: string[] },
|
|
300
|
+
): ScanFilter | undefined {
|
|
301
|
+
const { include, exclude } = opts;
|
|
302
|
+
const hasInclude = include && include.length > 0;
|
|
303
|
+
const hasCustomExclude = exclude !== undefined;
|
|
304
|
+
|
|
305
|
+
if (!hasInclude && !hasCustomExclude) return undefined;
|
|
306
|
+
|
|
307
|
+
const effectiveExclude = exclude ?? DEFAULT_EXCLUDE_PATTERNS;
|
|
308
|
+
const includeMatcher = hasInclude ? picomatch(include) : null;
|
|
309
|
+
const excludeMatcher = effectiveExclude.length > 0 ? picomatch(effectiveExclude) : null;
|
|
310
|
+
|
|
311
|
+
return (absolutePath: string) => {
|
|
312
|
+
const rel = relative(root, absolutePath);
|
|
313
|
+
if (excludeMatcher && excludeMatcher(rel)) return false;
|
|
314
|
+
if (includeMatcher) return includeMatcher(rel);
|
|
315
|
+
return true;
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Recursively find .ts/.tsx files under a directory, skipping node_modules
|
|
321
|
+
* and .gen. files.
|
|
322
|
+
*/
|
|
323
|
+
export function findTsFiles(dir: string, filter?: ScanFilter): string[] {
|
|
324
|
+
const results: string[] = [];
|
|
325
|
+
let entries;
|
|
326
|
+
try {
|
|
327
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
328
|
+
} catch (err) {
|
|
329
|
+
console.warn(`[rsc-router] Failed to scan directory ${dir}: ${(err as Error).message}`);
|
|
330
|
+
return results;
|
|
331
|
+
}
|
|
332
|
+
for (const entry of entries) {
|
|
333
|
+
const fullPath = join(dir, entry.name);
|
|
334
|
+
if (entry.isDirectory()) {
|
|
335
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
336
|
+
results.push(...findTsFiles(fullPath, filter));
|
|
337
|
+
} else if (
|
|
338
|
+
(entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) &&
|
|
339
|
+
!entry.name.includes(".gen.")
|
|
340
|
+
) {
|
|
341
|
+
if (filter && !filter(fullPath)) continue;
|
|
342
|
+
results.push(fullPath);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return results;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Generate per-module route type files by statically parsing url module source.
|
|
350
|
+
* Scans for files containing `urls(` and writes a sibling `.gen.ts` with the
|
|
351
|
+
* extracted route name/pattern pairs. Only writes when content has changed.
|
|
352
|
+
*/
|
|
353
|
+
export function writePerModuleRouteTypes(root: string, filter?: ScanFilter): void {
|
|
354
|
+
const files = findTsFiles(root, filter);
|
|
355
|
+
for (const filePath of files) {
|
|
356
|
+
writePerModuleRouteTypesForFile(filePath);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Generate per-module route types for a single url module file.
|
|
362
|
+
* No-ops if the file doesn't contain `urls(` or has no named routes.
|
|
363
|
+
*/
|
|
364
|
+
export function writePerModuleRouteTypesForFile(filePath: string): void {
|
|
365
|
+
try {
|
|
366
|
+
const source = readFileSync(filePath, "utf-8");
|
|
367
|
+
if (!source.includes("urls(")) return;
|
|
368
|
+
|
|
369
|
+
const routes = extractRoutesFromSource(source);
|
|
370
|
+
if (routes.length === 0) return;
|
|
371
|
+
|
|
372
|
+
const genPath = filePath.replace(/\.(tsx?)$/, ".gen.ts");
|
|
373
|
+
const genSource = generatePerModuleTypesSource(routes);
|
|
374
|
+
const existing = existsSync(genPath) ? readFileSync(genPath, "utf-8") : null;
|
|
375
|
+
if (existing !== genSource) {
|
|
376
|
+
writeFileSync(genPath, genSource);
|
|
377
|
+
console.log(`[rsc-router] Generated route types -> ${genPath}`);
|
|
378
|
+
}
|
|
379
|
+
} catch (err) {
|
|
380
|
+
console.warn(`[rsc-router] Failed to generate route types for ${filePath}: ${(err as Error).message}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
// Static include() parsing
|
|
386
|
+
// ---------------------------------------------------------------------------
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Extract include() calls from source code by statically parsing.
|
|
390
|
+
* Returns the path prefix, variable name, and optional name prefix for each.
|
|
391
|
+
*/
|
|
392
|
+
export function extractIncludesFromSource(
|
|
393
|
+
code: string
|
|
394
|
+
): Array<{ pathPrefix: string; variableName: string; namePrefix: string | null }> {
|
|
395
|
+
const results: Array<{
|
|
396
|
+
pathPrefix: string;
|
|
397
|
+
variableName: string;
|
|
398
|
+
namePrefix: string | null;
|
|
399
|
+
}> = [];
|
|
400
|
+
const regex = /\binclude\s*\(/g;
|
|
401
|
+
let match;
|
|
402
|
+
|
|
403
|
+
while ((match = regex.exec(code)) !== null) {
|
|
404
|
+
const result = parseIncludeCall(code, match.index + match[0].length);
|
|
405
|
+
if (result) results.push(result);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return results;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Parse a single include() call starting right after the opening paren.
|
|
413
|
+
* Expects: include("prefix", variableName, { name: "prefix" })
|
|
414
|
+
*/
|
|
415
|
+
function parseIncludeCall(
|
|
416
|
+
code: string,
|
|
417
|
+
pos: number
|
|
418
|
+
): {
|
|
419
|
+
pathPrefix: string;
|
|
420
|
+
variableName: string;
|
|
421
|
+
namePrefix: string | null;
|
|
422
|
+
} | null {
|
|
423
|
+
// Skip whitespace to first argument
|
|
424
|
+
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
425
|
+
|
|
426
|
+
// First arg: string literal (pathPrefix)
|
|
427
|
+
const prefixStr = readString(code, pos);
|
|
428
|
+
if (!prefixStr) return null;
|
|
429
|
+
const pathPrefix = prefixStr.value;
|
|
430
|
+
pos = prefixStr.end;
|
|
431
|
+
|
|
432
|
+
// Comma
|
|
433
|
+
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
434
|
+
if (pos >= code.length || code[pos] !== ",") return null;
|
|
435
|
+
pos++;
|
|
436
|
+
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
437
|
+
|
|
438
|
+
// Second arg: identifier (variableName)
|
|
439
|
+
const varStart = pos;
|
|
440
|
+
while (pos < code.length && /[\w$]/.test(code[pos])) pos++;
|
|
441
|
+
if (pos === varStart) return null;
|
|
442
|
+
const variableName = code.slice(varStart, pos);
|
|
443
|
+
|
|
444
|
+
// Scan rest of call for optional { name: "..." }
|
|
445
|
+
let namePrefix: string | null = null;
|
|
446
|
+
let depth = 1; // inside include()
|
|
447
|
+
|
|
448
|
+
while (pos < code.length && depth > 0) {
|
|
449
|
+
const ch = code[pos];
|
|
450
|
+
|
|
451
|
+
if (isWhitespace(ch)) {
|
|
452
|
+
pos++;
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Line comment
|
|
457
|
+
if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "/") {
|
|
458
|
+
pos += 2;
|
|
459
|
+
while (pos < code.length && code[pos] !== "\n") pos++;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Block comment
|
|
464
|
+
if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "*") {
|
|
465
|
+
pos += 2;
|
|
466
|
+
while (
|
|
467
|
+
pos < code.length - 1 &&
|
|
468
|
+
!(code[pos] === "*" && code[pos + 1] === "/")
|
|
469
|
+
)
|
|
470
|
+
pos++;
|
|
471
|
+
pos += 2;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// At depth 2 (inside options object), look for name: "..."
|
|
476
|
+
if (depth === 2 && ch === "n" && matchesNameColon(code, pos)) {
|
|
477
|
+
const nameResult = extractNameValue(code, pos);
|
|
478
|
+
if (nameResult) {
|
|
479
|
+
namePrefix = nameResult.value;
|
|
480
|
+
pos = nameResult.end;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Skip string literals
|
|
486
|
+
if (
|
|
487
|
+
ch === '"' ||
|
|
488
|
+
ch === "`" ||
|
|
489
|
+
(ch === "'" && (pos === 0 || !/\w/.test(code[pos - 1])))
|
|
490
|
+
) {
|
|
491
|
+
pos = skipStringLiteral(code, pos);
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Track depth
|
|
496
|
+
if (ch === "(" || ch === "{" || ch === "[") depth++;
|
|
497
|
+
else if (ch === ")" || ch === "}" || ch === "]") depth--;
|
|
498
|
+
|
|
499
|
+
pos++;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return { pathPrefix, variableName, namePrefix };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
// Import resolution
|
|
507
|
+
// ---------------------------------------------------------------------------
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Find the import statement for a local variable name.
|
|
511
|
+
* Returns the import specifier and the exported name from the source module.
|
|
512
|
+
*/
|
|
513
|
+
function resolveImportedVariable(
|
|
514
|
+
code: string,
|
|
515
|
+
localName: string
|
|
516
|
+
): { specifier: string; exportedName: string } | null {
|
|
517
|
+
const importRegex = /import\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']/g;
|
|
518
|
+
let match;
|
|
519
|
+
|
|
520
|
+
while ((match = importRegex.exec(code)) !== null) {
|
|
521
|
+
const imports = match[1];
|
|
522
|
+
const specifier = match[2];
|
|
523
|
+
|
|
524
|
+
const parts = imports
|
|
525
|
+
.split(",")
|
|
526
|
+
.map((s) => s.trim())
|
|
527
|
+
.filter(Boolean);
|
|
528
|
+
for (const part of parts) {
|
|
529
|
+
const asMatch = part.match(/^(\w+)\s+as\s+(\w+)$/);
|
|
530
|
+
if (asMatch && asMatch[2] === localName)
|
|
531
|
+
return { specifier, exportedName: asMatch[1] };
|
|
532
|
+
if (part === localName) return { specifier, exportedName: localName };
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Resolve an import specifier relative to the importing file.
|
|
541
|
+
* Strips .js/.mjs extensions and tries .ts/.tsx candidates.
|
|
542
|
+
*/
|
|
543
|
+
function resolveImportPath(
|
|
544
|
+
importSpec: string,
|
|
545
|
+
fromFile: string
|
|
546
|
+
): string | null {
|
|
547
|
+
if (!importSpec.startsWith(".")) return null;
|
|
548
|
+
|
|
549
|
+
const dir = dirname(fromFile);
|
|
550
|
+
let base = importSpec;
|
|
551
|
+
if (base.endsWith(".js")) base = base.slice(0, -3);
|
|
552
|
+
else if (base.endsWith(".mjs")) base = base.slice(0, -4);
|
|
553
|
+
|
|
554
|
+
const candidates = [
|
|
555
|
+
resolve(dir, base + ".ts"),
|
|
556
|
+
resolve(dir, base + ".tsx"),
|
|
557
|
+
resolve(dir, base + "/index.ts"),
|
|
558
|
+
resolve(dir, base + "/index.tsx"),
|
|
559
|
+
];
|
|
560
|
+
|
|
561
|
+
for (const candidate of candidates) {
|
|
562
|
+
if (existsSync(candidate)) return candidate;
|
|
563
|
+
}
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ---------------------------------------------------------------------------
|
|
568
|
+
// urls() block extraction for same-file variables
|
|
569
|
+
// ---------------------------------------------------------------------------
|
|
570
|
+
|
|
571
|
+
function escapeRegExp(s: string): string {
|
|
572
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Extract the source of a specific `const varName = urls(...)` block.
|
|
577
|
+
* Used for same-file variables where include() references a urls() defined
|
|
578
|
+
* in the same module rather than imported.
|
|
579
|
+
*/
|
|
580
|
+
function extractUrlsBlockForVariable(
|
|
581
|
+
code: string,
|
|
582
|
+
varName: string
|
|
583
|
+
): string | null {
|
|
584
|
+
const pattern = new RegExp(
|
|
585
|
+
`(?:export\\s+)?(?:const|let|var)\\s+${escapeRegExp(varName)}\\s*=\\s*urls\\s*\\(`
|
|
586
|
+
);
|
|
587
|
+
const match = pattern.exec(code);
|
|
588
|
+
if (!match) return null;
|
|
589
|
+
|
|
590
|
+
// Start from the opening paren of urls(
|
|
591
|
+
const openParen = match.index + match[0].length - 1;
|
|
592
|
+
let depth = 1;
|
|
593
|
+
let pos = openParen + 1;
|
|
594
|
+
|
|
595
|
+
while (pos < code.length && depth > 0) {
|
|
596
|
+
const ch = code[pos];
|
|
597
|
+
|
|
598
|
+
// Skip strings
|
|
599
|
+
if (
|
|
600
|
+
ch === '"' ||
|
|
601
|
+
ch === "`" ||
|
|
602
|
+
(ch === "'" && (pos === 0 || !/\w/.test(code[pos - 1])))
|
|
603
|
+
) {
|
|
604
|
+
pos = skipStringLiteral(code, pos);
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Line comment
|
|
609
|
+
if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "/") {
|
|
610
|
+
pos += 2;
|
|
611
|
+
while (pos < code.length && code[pos] !== "\n") pos++;
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Block comment
|
|
616
|
+
if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "*") {
|
|
617
|
+
pos += 2;
|
|
618
|
+
while (
|
|
619
|
+
pos < code.length - 1 &&
|
|
620
|
+
!(code[pos] === "*" && code[pos + 1] === "/")
|
|
621
|
+
)
|
|
622
|
+
pos++;
|
|
623
|
+
pos += 2;
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (ch === "(" || ch === "{" || ch === "[") depth++;
|
|
628
|
+
else if (ch === ")" || ch === "}" || ch === "]") depth--;
|
|
629
|
+
|
|
630
|
+
pos++;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return code.slice(openParen, pos);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// ---------------------------------------------------------------------------
|
|
637
|
+
// Combined route map building
|
|
638
|
+
// ---------------------------------------------------------------------------
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Recursively build a route map from a urls module file.
|
|
642
|
+
* Extracts local path() routes and follows include() calls to sub-modules.
|
|
643
|
+
* Handles both imported and same-file variables.
|
|
644
|
+
*/
|
|
645
|
+
export function buildCombinedRouteMap(
|
|
646
|
+
filePath: string,
|
|
647
|
+
variableName?: string,
|
|
648
|
+
visited?: Set<string>
|
|
649
|
+
): Record<string, string> {
|
|
650
|
+
visited = visited ?? new Set();
|
|
651
|
+
const realPath = resolve(filePath);
|
|
652
|
+
const key = variableName ? `${realPath}:${variableName}` : realPath;
|
|
653
|
+
if (visited.has(key)) return {};
|
|
654
|
+
visited.add(key);
|
|
655
|
+
|
|
656
|
+
let source: string;
|
|
657
|
+
try {
|
|
658
|
+
source = readFileSync(realPath, "utf-8");
|
|
659
|
+
} catch {
|
|
660
|
+
return {};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// If a specific variable is requested, extract just its urls() block
|
|
664
|
+
let block: string;
|
|
665
|
+
if (variableName) {
|
|
666
|
+
const extracted = extractUrlsBlockForVariable(source, variableName);
|
|
667
|
+
if (!extracted) return {};
|
|
668
|
+
block = extracted;
|
|
669
|
+
} else {
|
|
670
|
+
block = source;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return buildRouteMapFromBlock(block, source, realPath, visited);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function buildRouteMapFromBlock(
|
|
677
|
+
block: string,
|
|
678
|
+
fullSource: string,
|
|
679
|
+
filePath: string,
|
|
680
|
+
visited: Set<string>
|
|
681
|
+
): Record<string, string> {
|
|
682
|
+
const routeMap: Record<string, string> = {};
|
|
683
|
+
|
|
684
|
+
// Extract local path() routes
|
|
685
|
+
const localRoutes = extractRoutesFromSource(block);
|
|
686
|
+
for (const { name, pattern } of localRoutes) {
|
|
687
|
+
routeMap[name] = pattern;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Extract include() calls
|
|
691
|
+
const includes = extractIncludesFromSource(block);
|
|
692
|
+
for (const { pathPrefix, variableName, namePrefix } of includes) {
|
|
693
|
+
let childRoutes: Record<string, string>;
|
|
694
|
+
|
|
695
|
+
// Try import resolution first
|
|
696
|
+
const imported = resolveImportedVariable(fullSource, variableName);
|
|
697
|
+
if (imported) {
|
|
698
|
+
const targetFile = resolveImportPath(imported.specifier, filePath);
|
|
699
|
+
if (!targetFile) continue;
|
|
700
|
+
childRoutes = buildCombinedRouteMap(
|
|
701
|
+
targetFile,
|
|
702
|
+
imported.exportedName,
|
|
703
|
+
visited
|
|
704
|
+
);
|
|
705
|
+
} else {
|
|
706
|
+
// Same-file variable
|
|
707
|
+
childRoutes = buildCombinedRouteMap(filePath, variableName, visited);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Apply prefixes
|
|
711
|
+
for (const [name, pattern] of Object.entries(childRoutes)) {
|
|
712
|
+
const prefixedName = namePrefix ? `${namePrefix}.${name}` : name;
|
|
713
|
+
const prefixedPattern =
|
|
714
|
+
pattern === "/" ? pathPrefix || "/" : pathPrefix + pattern;
|
|
715
|
+
routeMap[prefixedName] = prefixedPattern;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return routeMap;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// ---------------------------------------------------------------------------
|
|
723
|
+
// Router file URL extraction
|
|
724
|
+
// ---------------------------------------------------------------------------
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Extract the url patterns variable from a router file.
|
|
728
|
+
* Looks for patterns like:
|
|
729
|
+
* .routes(variableName)
|
|
730
|
+
* urls: variableName
|
|
731
|
+
* Returns the local variable name and optional import info.
|
|
732
|
+
*/
|
|
733
|
+
function extractUrlsVariableFromRouter(
|
|
734
|
+
code: string
|
|
735
|
+
): string | null {
|
|
736
|
+
// Pattern 1: .routes(variableName) where variableName is an identifier (not a string)
|
|
737
|
+
const routesCallMatch = code.match(/\.routes\s*\(\s*([a-zA-Z_$][\w$]*)\s*\)/);
|
|
738
|
+
if (routesCallMatch) return routesCallMatch[1];
|
|
739
|
+
|
|
740
|
+
// Pattern 2: urls: variableName in createRouter options
|
|
741
|
+
const urlsOptionMatch = code.match(/urls\s*:\s*([a-zA-Z_$][\w$]*)/);
|
|
742
|
+
if (urlsOptionMatch) return urlsOptionMatch[1];
|
|
743
|
+
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// ---------------------------------------------------------------------------
|
|
748
|
+
// Per-router named-routes.gen.ts writer
|
|
749
|
+
// ---------------------------------------------------------------------------
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Scan for files containing createRouter() and return their paths.
|
|
753
|
+
* Call once at startup; the result can be reused on subsequent watcher triggers.
|
|
754
|
+
*/
|
|
755
|
+
export function findRouterFiles(root: string, filter?: ScanFilter): string[] {
|
|
756
|
+
const files = findTsFiles(root, filter);
|
|
757
|
+
const result: string[] = [];
|
|
758
|
+
for (const filePath of files) {
|
|
759
|
+
if (filePath.includes(".gen.")) continue;
|
|
760
|
+
try {
|
|
761
|
+
const source = readFileSync(filePath, "utf-8");
|
|
762
|
+
if (/\bcreateRouter\s*[<(]/.test(source)) {
|
|
763
|
+
result.push(filePath);
|
|
764
|
+
}
|
|
765
|
+
} catch {
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return result;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Generate per-router named-routes.gen.ts files from known router file paths.
|
|
774
|
+
* Re-reads each router file and resolves url patterns via static source parsing.
|
|
775
|
+
*
|
|
776
|
+
* Pass `knownRouterFiles` from a previous `findRouterFiles()` call to skip the
|
|
777
|
+
* full directory scan. If omitted, falls back to scanning (startup path).
|
|
778
|
+
*/
|
|
779
|
+
export function writeCombinedRouteTypes(root: string, knownRouterFiles?: string[]): void {
|
|
780
|
+
// Delete old combined named-routes.gen.ts if it exists (stale from older versions)
|
|
781
|
+
try {
|
|
782
|
+
const oldCombinedPath = join(root, "src", "named-routes.gen.ts");
|
|
783
|
+
if (existsSync(oldCombinedPath)) {
|
|
784
|
+
unlinkSync(oldCombinedPath);
|
|
785
|
+
console.log(`[rsc-router] Removed stale combined route types: ${oldCombinedPath}`);
|
|
786
|
+
}
|
|
787
|
+
} catch {}
|
|
788
|
+
|
|
789
|
+
const routerFilePaths = knownRouterFiles ?? findRouterFiles(root);
|
|
790
|
+
if (routerFilePaths.length === 0) return;
|
|
791
|
+
|
|
792
|
+
for (const routerFilePath of routerFilePaths) {
|
|
793
|
+
let routerSource: string;
|
|
794
|
+
try {
|
|
795
|
+
routerSource = readFileSync(routerFilePath, "utf-8");
|
|
796
|
+
} catch {
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
// Extract the urls variable name from .routes(varName) or urls: varName
|
|
800
|
+
const urlsVarName = extractUrlsVariableFromRouter(routerSource);
|
|
801
|
+
if (!urlsVarName) continue;
|
|
802
|
+
|
|
803
|
+
// Resolve the variable to its source module
|
|
804
|
+
let routeMap: Record<string, string>;
|
|
805
|
+
|
|
806
|
+
const imported = resolveImportedVariable(routerSource, urlsVarName);
|
|
807
|
+
if (imported) {
|
|
808
|
+
// Variable is imported from another module
|
|
809
|
+
const targetFile = resolveImportPath(imported.specifier, routerFilePath);
|
|
810
|
+
if (!targetFile) continue;
|
|
811
|
+
routeMap = buildCombinedRouteMap(targetFile, imported.exportedName);
|
|
812
|
+
} else {
|
|
813
|
+
// Variable is defined in the same file
|
|
814
|
+
routeMap = buildCombinedRouteMap(routerFilePath, urlsVarName);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (Object.keys(routeMap).length === 0) continue;
|
|
818
|
+
|
|
819
|
+
const routerBasename = pathBasename(routerFilePath).replace(/\.(tsx?|jsx?)$/, "");
|
|
820
|
+
const outPath = join(dirname(routerFilePath), `${routerBasename}.named-routes.gen.ts`);
|
|
821
|
+
const source = generateRouteTypesSource(routeMap);
|
|
822
|
+
const existing = existsSync(outPath) ? readFileSync(outPath, "utf-8") : null;
|
|
823
|
+
if (existing !== source) {
|
|
824
|
+
writeFileSync(outPath, source);
|
|
825
|
+
console.log(`[rsc-router] Generated route types (${Object.keys(routeMap).length} routes) -> ${outPath}`);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|