@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1fa245e2
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 → AGENTS.md} +4 -0
- package/README.md +122 -30
- package/dist/bin/rango.js +245 -63
- package/dist/vite/index.js +859 -418
- package/package.json +3 -3
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +49 -8
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +33 -31
- package/skills/host-router/SKILL.md +218 -0
- package/skills/links/SKILL.md +3 -1
- package/skills/loader/SKILL.md +72 -22
- package/skills/middleware/SKILL.md +2 -0
- package/skills/parallel/SKILL.md +126 -0
- package/skills/prerender/SKILL.md +112 -70
- package/skills/rango/SKILL.md +0 -1
- package/skills/route/SKILL.md +34 -4
- package/skills/router-setup/SKILL.md +95 -5
- package/skills/typesafety/SKILL.md +35 -23
- package/src/__internal.ts +92 -0
- package/src/bin/rango.ts +18 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +114 -18
- package/src/browser/navigation-client.ts +126 -44
- package/src/browser/navigation-store.ts +43 -8
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +80 -15
- package/src/browser/prefetch/cache.ts +166 -27
- package/src/browser/prefetch/fetch.ts +52 -39
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +92 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/react/Link.tsx +70 -14
- package/src/browser/react/NavigationProvider.tsx +40 -4
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-router.ts +21 -8
- package/src/browser/rsc-router.tsx +143 -59
- package/src/browser/scroll-restoration.ts +41 -42
- package/src/browser/segment-reconciler.ts +6 -1
- package/src/browser/server-action-bridge.ts +454 -436
- package/src/browser/types.ts +60 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/route-trie.ts +19 -3
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +346 -87
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +48 -7
- package/src/cache/cf/cf-cache-store.ts +453 -11
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.rsc.tsx +2 -1
- package/src/client.tsx +3 -102
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/handle.ts +40 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/host/index.ts +0 -3
- package/src/index.rsc.ts +8 -37
- package/src/index.ts +40 -66
- package/src/prerender/store.ts +57 -15
- package/src/prerender.ts +138 -77
- package/src/reverse.ts +22 -1
- package/src/route-definition/dsl-helpers.ts +73 -25
- package/src/route-definition/helpers-types.ts +10 -6
- package/src/route-definition/index.ts +3 -3
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +11 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +108 -25
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +4 -1
- package/src/router/loader-resolution.ts +123 -11
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +9 -3
- package/src/router/match-api.ts +125 -190
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +88 -16
- package/src/router/match-middleware/cache-store.ts +53 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +61 -5
- package/src/router/match-result.ts +22 -15
- package/src/router/metrics.ts +238 -13
- package/src/router/middleware-types.ts +53 -12
- package/src/router/middleware.ts +172 -85
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +20 -5
- package/src/router/prerender-match.ts +114 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +27 -7
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +50 -5
- package/src/router/router-options.ts +50 -19
- package/src/router/segment-resolution/fresh.ts +200 -19
- package/src/router/segment-resolution/helpers.ts +30 -25
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +429 -301
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/trie-matching.ts +20 -2
- package/src/router/types.ts +1 -0
- package/src/router.ts +88 -15
- package/src/rsc/handler.ts +546 -359
- package/src/rsc/index.ts +0 -20
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +25 -8
- package/src/rsc/rsc-rendering.ts +35 -43
- package/src/rsc/server-action.ts +16 -10
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +10 -1
- package/src/search-params.ts +16 -13
- package/src/segment-system.tsx +140 -4
- package/src/server/context.ts +148 -16
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +182 -34
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +4 -0
- package/src/static-handler.ts +18 -6
- package/src/theme/index.ts +4 -13
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +149 -49
- package/src/types/loader-types.ts +36 -9
- package/src/types/route-config.ts +17 -8
- package/src/types/route-entry.ts +8 -1
- package/src/types/segments.ts +2 -5
- package/src/urls/path-helper-types.ts +9 -2
- package/src/urls/path-helper.ts +48 -13
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +16 -6
- package/src/use-loader.tsx +73 -4
- package/src/vite/discovery/bundle-postprocess.ts +61 -89
- package/src/vite/discovery/discover-routers.ts +23 -5
- package/src/vite/discovery/prerender-collection.ts +48 -15
- package/src/vite/discovery/state.ts +17 -13
- package/src/vite/index.ts +8 -3
- package/src/vite/plugin-types.ts +51 -79
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/expose-action-id.ts +1 -3
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +174 -211
- package/src/vite/router-discovery.ts +169 -42
- package/src/vite/utils/banner.ts +3 -3
- package/src/vite/utils/prerender-utils.ts +78 -0
- package/src/vite/utils/shared-utils.ts +3 -2
- package/skills/testing/SKILL.md +0 -226
- package/src/route-definition/route-function.ts +0 -119
|
@@ -1,9 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
readFileSync,
|
|
3
|
+
writeFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
unlinkSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import {
|
|
9
|
+
join,
|
|
10
|
+
dirname,
|
|
11
|
+
resolve,
|
|
12
|
+
sep,
|
|
13
|
+
basename as pathBasename,
|
|
14
|
+
} from "node:path";
|
|
3
15
|
import ts from "typescript";
|
|
4
16
|
import { generateRouteTypesSource } from "./codegen.js";
|
|
5
17
|
import type { ScanFilter } from "./scan-filter.js";
|
|
6
|
-
import { findTsFiles } from "./scan-filter.js";
|
|
7
18
|
import {
|
|
8
19
|
resolveImportedVariable,
|
|
9
20
|
resolveImportPath,
|
|
@@ -26,18 +37,146 @@ function countPublicRouteEntries(source: string): number {
|
|
|
26
37
|
return count;
|
|
27
38
|
}
|
|
28
39
|
|
|
40
|
+
const ROUTER_CALL_PATTERN = /\bcreateRouter\s*[<(]/;
|
|
41
|
+
|
|
42
|
+
function isRoutableSourceFile(name: string): boolean {
|
|
43
|
+
return (
|
|
44
|
+
(name.endsWith(".ts") ||
|
|
45
|
+
name.endsWith(".tsx") ||
|
|
46
|
+
name.endsWith(".js") ||
|
|
47
|
+
name.endsWith(".jsx")) &&
|
|
48
|
+
!name.includes(".gen.") &&
|
|
49
|
+
!name.includes(".test.") &&
|
|
50
|
+
!name.includes(".spec.")
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function findRouterFilesRecursive(
|
|
55
|
+
dir: string,
|
|
56
|
+
filter: ScanFilter | undefined,
|
|
57
|
+
results: string[],
|
|
58
|
+
): void {
|
|
59
|
+
let entries;
|
|
60
|
+
try {
|
|
61
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.warn(
|
|
64
|
+
`[rsc-router] Failed to scan directory ${dir}: ${(err as Error).message}`,
|
|
65
|
+
);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const childDirs: string[] = [];
|
|
70
|
+
const routerFilesInDir: string[] = [];
|
|
71
|
+
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
const fullPath = join(dir, entry.name);
|
|
74
|
+
if (entry.isDirectory()) {
|
|
75
|
+
if (
|
|
76
|
+
entry.name === "node_modules" ||
|
|
77
|
+
entry.name === "dist" ||
|
|
78
|
+
entry.name === "coverage" ||
|
|
79
|
+
entry.name === "__tests__" ||
|
|
80
|
+
entry.name === "__mocks__" ||
|
|
81
|
+
entry.name.startsWith(".")
|
|
82
|
+
)
|
|
83
|
+
continue;
|
|
84
|
+
childDirs.push(fullPath);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!isRoutableSourceFile(entry.name)) continue;
|
|
89
|
+
if (filter && !filter(fullPath)) continue;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const source = readFileSync(fullPath, "utf-8");
|
|
93
|
+
if (ROUTER_CALL_PATTERN.test(source)) {
|
|
94
|
+
routerFilesInDir.push(fullPath);
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// A directory that contains a router file is treated as a router root.
|
|
102
|
+
// Once found, deeper directories are skipped to avoid redundant scans.
|
|
103
|
+
if (routerFilesInDir.length > 0) {
|
|
104
|
+
results.push(...routerFilesInDir);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const childDir of childDirs) {
|
|
109
|
+
findRouterFilesRecursive(childDir, filter, results);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function findNestedRouterConflict(
|
|
114
|
+
routerFiles: string[],
|
|
115
|
+
): { ancestor: string; nested: string } | null {
|
|
116
|
+
const routerDirs = [
|
|
117
|
+
...new Set(routerFiles.map((filePath) => dirname(resolve(filePath)))),
|
|
118
|
+
].sort((a, b) => a.length - b.length);
|
|
119
|
+
|
|
120
|
+
for (let i = 0; i < routerDirs.length; i++) {
|
|
121
|
+
const ancestorDir = routerDirs[i];
|
|
122
|
+
const prefix = ancestorDir.endsWith(sep)
|
|
123
|
+
? ancestorDir
|
|
124
|
+
: `${ancestorDir}${sep}`;
|
|
125
|
+
for (let j = i + 1; j < routerDirs.length; j++) {
|
|
126
|
+
const nestedDir = routerDirs[j];
|
|
127
|
+
if (!nestedDir.startsWith(prefix)) continue;
|
|
128
|
+
const ancestorFile = routerFiles.find(
|
|
129
|
+
(filePath) => dirname(resolve(filePath)) === ancestorDir,
|
|
130
|
+
);
|
|
131
|
+
const nestedFile = routerFiles.find(
|
|
132
|
+
(filePath) => dirname(resolve(filePath)) === nestedDir,
|
|
133
|
+
);
|
|
134
|
+
if (ancestorFile && nestedFile) {
|
|
135
|
+
return { ancestor: ancestorFile, nested: nestedFile };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function formatNestedRouterConflictError(
|
|
144
|
+
conflict: { ancestor: string; nested: string },
|
|
145
|
+
prefix = "[rsc-router]",
|
|
146
|
+
): string {
|
|
147
|
+
return (
|
|
148
|
+
`${prefix} Nested router roots are not supported.\n` +
|
|
149
|
+
`Router root: ${conflict.ancestor}\n` +
|
|
150
|
+
`Nested router: ${conflict.nested}\n` +
|
|
151
|
+
`Move the nested router into a sibling directory or configure it as a separate app root.`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
29
155
|
// ---------------------------------------------------------------------------
|
|
30
156
|
// Router file URL extraction
|
|
31
157
|
// ---------------------------------------------------------------------------
|
|
32
158
|
|
|
33
159
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
160
|
+
* Result of extracting URL patterns from a router file.
|
|
161
|
+
* - "variable": a named variable reference (e.g., `.routes(patterns)` or `urls: patterns`)
|
|
162
|
+
* - "inline": an inline builder function (e.g., `.routes(({ path }) => [...])` or `urls: ({ path }) => [...]`)
|
|
163
|
+
*/
|
|
164
|
+
export type UrlsExtractionResult =
|
|
165
|
+
| { kind: "variable"; name: string }
|
|
166
|
+
| { kind: "inline"; block: string };
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Extract the url patterns from a router file using AST.
|
|
170
|
+
* Detects four patterns:
|
|
36
171
|
* 1. createRouter(...).routes(variableName)
|
|
37
172
|
* 2. createRouter({ urls: variableName, ... })
|
|
38
|
-
*
|
|
173
|
+
* 3. createRouter(...).routes(({ path, ... }) => [...])
|
|
174
|
+
* 4. createRouter({ urls: ({ path, ... }) => [...], ... })
|
|
175
|
+
* Returns either a variable name or an inline code block.
|
|
39
176
|
*/
|
|
40
|
-
export function
|
|
177
|
+
export function extractUrlsFromRouter(
|
|
178
|
+
code: string,
|
|
179
|
+
): UrlsExtractionResult | null {
|
|
41
180
|
const sourceFile = ts.createSourceFile(
|
|
42
181
|
"router.tsx",
|
|
43
182
|
code,
|
|
@@ -45,7 +184,7 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
|
|
|
45
184
|
true,
|
|
46
185
|
ts.ScriptKind.TSX,
|
|
47
186
|
);
|
|
48
|
-
let result:
|
|
187
|
+
let result: UrlsExtractionResult | null = null;
|
|
49
188
|
|
|
50
189
|
function isCreateRouterCall(node: ts.Node): boolean {
|
|
51
190
|
if (!ts.isCallExpression(node)) return false;
|
|
@@ -53,44 +192,108 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
|
|
|
53
192
|
return ts.isIdentifier(callee) && callee.text === "createRouter";
|
|
54
193
|
}
|
|
55
194
|
|
|
195
|
+
/** Check if a node is an arrow/function expression (inline builder). */
|
|
196
|
+
function isInlineBuilder(node: ts.Node): boolean {
|
|
197
|
+
return ts.isArrowFunction(node) || ts.isFunctionExpression(node);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Check if a .routes() call chains from createRouter(). */
|
|
201
|
+
function isRoutesOnCreateRouter(node: ts.CallExpression): boolean {
|
|
202
|
+
if (
|
|
203
|
+
!ts.isPropertyAccessExpression(node.expression) ||
|
|
204
|
+
node.expression.name.text !== "routes"
|
|
205
|
+
)
|
|
206
|
+
return false;
|
|
207
|
+
let inner: ts.Expression = node.expression.expression;
|
|
208
|
+
while (
|
|
209
|
+
ts.isCallExpression(inner) &&
|
|
210
|
+
ts.isPropertyAccessExpression(inner.expression)
|
|
211
|
+
) {
|
|
212
|
+
inner = inner.expression.expression;
|
|
213
|
+
}
|
|
214
|
+
return isCreateRouterCall(inner);
|
|
215
|
+
}
|
|
216
|
+
|
|
56
217
|
function visit(node: ts.Node) {
|
|
57
218
|
if (result) return;
|
|
58
219
|
|
|
59
|
-
// Pattern 1: createRouter(...).routes(variableName)
|
|
60
|
-
// The AST shape is CallExpression(.routes) -> PropertyAccessExpression -> CallExpression(createRouter)
|
|
220
|
+
// Pattern 1 & 3: createRouter(...).routes(variableName | builder)
|
|
61
221
|
if (
|
|
62
222
|
ts.isCallExpression(node) &&
|
|
63
|
-
ts.isPropertyAccessExpression(node.expression) &&
|
|
64
|
-
node.expression.name.text === "routes" &&
|
|
65
223
|
node.arguments.length >= 1 &&
|
|
66
|
-
|
|
224
|
+
isRoutesOnCreateRouter(node)
|
|
67
225
|
) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
ts.isPropertyAccessExpression(inner.expression)
|
|
74
|
-
) {
|
|
75
|
-
inner = inner.expression.expression;
|
|
76
|
-
}
|
|
77
|
-
if (isCreateRouterCall(inner)) {
|
|
78
|
-
result = (node.arguments[0] as ts.Identifier).text;
|
|
79
|
-
return;
|
|
226
|
+
const arg = node.arguments[0];
|
|
227
|
+
if (ts.isIdentifier(arg)) {
|
|
228
|
+
result = { kind: "variable", name: arg.text };
|
|
229
|
+
} else if (isInlineBuilder(arg)) {
|
|
230
|
+
result = { kind: "inline", block: arg.getText(sourceFile) };
|
|
80
231
|
}
|
|
232
|
+
return;
|
|
81
233
|
}
|
|
82
234
|
|
|
83
|
-
// Pattern 2: createRouter({ urls: variableName, ... })
|
|
235
|
+
// Pattern 2 & 4: createRouter({ urls: variableName | builder, ... })
|
|
84
236
|
if (isCreateRouterCall(node)) {
|
|
85
237
|
const callExpr = node as ts.CallExpression;
|
|
86
|
-
for (const
|
|
238
|
+
for (const callArg of callExpr.arguments) {
|
|
239
|
+
if (ts.isObjectLiteralExpression(callArg)) {
|
|
240
|
+
for (const prop of callArg.properties) {
|
|
241
|
+
if (
|
|
242
|
+
ts.isPropertyAssignment(prop) &&
|
|
243
|
+
ts.isIdentifier(prop.name) &&
|
|
244
|
+
prop.name.text === "urls"
|
|
245
|
+
) {
|
|
246
|
+
if (ts.isIdentifier(prop.initializer)) {
|
|
247
|
+
result = { kind: "variable", name: prop.initializer.text };
|
|
248
|
+
} else if (isInlineBuilder(prop.initializer)) {
|
|
249
|
+
result = {
|
|
250
|
+
kind: "inline",
|
|
251
|
+
block: prop.initializer.getText(sourceFile),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
ts.forEachChild(node, visit);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
visit(sourceFile);
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Extract the `basename` string literal from createRouter({ basename: "..." }).
|
|
270
|
+
* Returns the basename value or undefined if not present.
|
|
271
|
+
*/
|
|
272
|
+
export function extractBasenameFromRouter(code: string): string | undefined {
|
|
273
|
+
const sourceFile = ts.createSourceFile(
|
|
274
|
+
"router.tsx",
|
|
275
|
+
code,
|
|
276
|
+
ts.ScriptTarget.Latest,
|
|
277
|
+
true,
|
|
278
|
+
ts.ScriptKind.TSX,
|
|
279
|
+
);
|
|
280
|
+
let result: string | undefined;
|
|
281
|
+
|
|
282
|
+
function visit(node: ts.Node) {
|
|
283
|
+
if (result !== undefined) return;
|
|
284
|
+
if (
|
|
285
|
+
ts.isCallExpression(node) &&
|
|
286
|
+
ts.isIdentifier(node.expression) &&
|
|
287
|
+
node.expression.text === "createRouter"
|
|
288
|
+
) {
|
|
289
|
+
for (const arg of node.arguments) {
|
|
87
290
|
if (ts.isObjectLiteralExpression(arg)) {
|
|
88
291
|
for (const prop of arg.properties) {
|
|
89
292
|
if (
|
|
90
293
|
ts.isPropertyAssignment(prop) &&
|
|
91
294
|
ts.isIdentifier(prop.name) &&
|
|
92
|
-
prop.name.text === "
|
|
93
|
-
ts.
|
|
295
|
+
prop.name.text === "basename" &&
|
|
296
|
+
ts.isStringLiteral(prop.initializer)
|
|
94
297
|
) {
|
|
95
298
|
result = prop.initializer.text;
|
|
96
299
|
return;
|
|
@@ -99,7 +302,6 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
|
|
|
99
302
|
}
|
|
100
303
|
}
|
|
101
304
|
}
|
|
102
|
-
|
|
103
305
|
ts.forEachChild(node, visit);
|
|
104
306
|
}
|
|
105
307
|
|
|
@@ -107,9 +309,40 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
|
|
|
107
309
|
return result;
|
|
108
310
|
}
|
|
109
311
|
|
|
312
|
+
/** @deprecated Use extractUrlsFromRouter instead */
|
|
313
|
+
export function extractUrlsVariableFromRouter(code: string): string | null {
|
|
314
|
+
const result = extractUrlsFromRouter(code);
|
|
315
|
+
return result?.kind === "variable" ? result.name : null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Apply a basename prefix to all route patterns in a result set. */
|
|
319
|
+
function applyBasenameToRoutes(
|
|
320
|
+
result: {
|
|
321
|
+
routes: Record<string, string>;
|
|
322
|
+
searchSchemas: Record<string, Record<string, string>>;
|
|
323
|
+
},
|
|
324
|
+
basename: string,
|
|
325
|
+
): {
|
|
326
|
+
routes: Record<string, string>;
|
|
327
|
+
searchSchemas: Record<string, Record<string, string>>;
|
|
328
|
+
} {
|
|
329
|
+
const prefixed: Record<string, string> = {};
|
|
330
|
+
for (const [name, pattern] of Object.entries(result.routes)) {
|
|
331
|
+
if (pattern === "/") {
|
|
332
|
+
prefixed[name] = basename;
|
|
333
|
+
} else if (basename.endsWith("/") && pattern.startsWith("/")) {
|
|
334
|
+
prefixed[name] = basename + pattern.slice(1);
|
|
335
|
+
} else {
|
|
336
|
+
prefixed[name] = basename + pattern;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return { routes: prefixed, searchSchemas: result.searchSchemas };
|
|
340
|
+
}
|
|
341
|
+
|
|
110
342
|
/**
|
|
111
343
|
* Resolve routes and search schemas from a router source file by following the
|
|
112
|
-
* variable passed to `.routes(...)` or `urls: ...` in createRouter options
|
|
344
|
+
* variable passed to `.routes(...)` or `urls: ...` in createRouter options,
|
|
345
|
+
* or by parsing an inline builder function directly.
|
|
113
346
|
*/
|
|
114
347
|
export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
|
|
115
348
|
routes: Record<string, string>;
|
|
@@ -122,21 +355,54 @@ export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
|
|
|
122
355
|
return { routes: {}, searchSchemas: {} };
|
|
123
356
|
}
|
|
124
357
|
|
|
125
|
-
const
|
|
126
|
-
if (!
|
|
358
|
+
const extraction = extractUrlsFromRouter(routerSource);
|
|
359
|
+
if (!extraction) {
|
|
127
360
|
return { routes: {}, searchSchemas: {} };
|
|
128
361
|
}
|
|
129
362
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
363
|
+
// Detect basename from createRouter({ basename: "..." })
|
|
364
|
+
const rawBasename = extractBasenameFromRouter(routerSource);
|
|
365
|
+
const basename = rawBasename
|
|
366
|
+
? ("/" + rawBasename.replace(/^\/+|\/+$/g, "")).replace(/^\/$/, "")
|
|
367
|
+
: undefined;
|
|
368
|
+
|
|
369
|
+
let result: {
|
|
370
|
+
routes: Record<string, string>;
|
|
371
|
+
searchSchemas: Record<string, Record<string, string>>;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// Inline builder: extract routes directly from the function body
|
|
375
|
+
if (extraction.kind === "inline") {
|
|
376
|
+
result = buildCombinedRouteMapWithSearch(
|
|
377
|
+
routerFilePath,
|
|
378
|
+
undefined,
|
|
379
|
+
undefined,
|
|
380
|
+
undefined,
|
|
381
|
+
extraction.block,
|
|
382
|
+
);
|
|
383
|
+
} else {
|
|
384
|
+
// Variable reference: follow imports or same-file declaration
|
|
385
|
+
const imported = resolveImportedVariable(routerSource, extraction.name);
|
|
386
|
+
if (imported) {
|
|
387
|
+
const targetFile = resolveImportPath(imported.specifier, routerFilePath);
|
|
388
|
+
if (!targetFile) {
|
|
389
|
+
return { routes: {}, searchSchemas: {} };
|
|
390
|
+
}
|
|
391
|
+
result = buildCombinedRouteMapWithSearch(
|
|
392
|
+
targetFile,
|
|
393
|
+
imported.exportedName,
|
|
394
|
+
);
|
|
395
|
+
} else {
|
|
396
|
+
result = buildCombinedRouteMapWithSearch(routerFilePath, extraction.name);
|
|
135
397
|
}
|
|
136
|
-
return buildCombinedRouteMapWithSearch(targetFile, imported.exportedName);
|
|
137
398
|
}
|
|
138
399
|
|
|
139
|
-
|
|
400
|
+
// Apply basename prefix to all extracted route patterns
|
|
401
|
+
if (basename) {
|
|
402
|
+
result = applyBasenameToRoutes(result, basename);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return result;
|
|
140
406
|
}
|
|
141
407
|
|
|
142
408
|
// ---------------------------------------------------------------------------
|
|
@@ -159,12 +425,26 @@ export function detectUnresolvableIncludes(
|
|
|
159
425
|
return [];
|
|
160
426
|
}
|
|
161
427
|
|
|
162
|
-
// Extract the urls
|
|
163
|
-
const
|
|
164
|
-
if (!
|
|
428
|
+
// Extract the urls source from the router file
|
|
429
|
+
const extraction = extractUrlsFromRouter(source);
|
|
430
|
+
if (!extraction) return [];
|
|
431
|
+
|
|
432
|
+
const diagnostics: UnresolvableInclude[] = [];
|
|
433
|
+
|
|
434
|
+
if (extraction.kind === "inline") {
|
|
435
|
+
// Inline builder: parse directly
|
|
436
|
+
buildCombinedRouteMapWithSearch(
|
|
437
|
+
realPath,
|
|
438
|
+
undefined,
|
|
439
|
+
new Set(),
|
|
440
|
+
diagnostics,
|
|
441
|
+
extraction.block,
|
|
442
|
+
);
|
|
443
|
+
return diagnostics;
|
|
444
|
+
}
|
|
165
445
|
|
|
166
|
-
//
|
|
167
|
-
const imported = resolveImportedVariable(source,
|
|
446
|
+
// Variable reference: resolve where it comes from
|
|
447
|
+
const imported = resolveImportedVariable(source, extraction.name);
|
|
168
448
|
let targetFile: string;
|
|
169
449
|
let exportedName: string | undefined;
|
|
170
450
|
|
|
@@ -186,10 +466,9 @@ export function detectUnresolvableIncludes(
|
|
|
186
466
|
} else {
|
|
187
467
|
// Same-file urls() definition
|
|
188
468
|
targetFile = realPath;
|
|
189
|
-
exportedName =
|
|
469
|
+
exportedName = extraction.name;
|
|
190
470
|
}
|
|
191
471
|
|
|
192
|
-
const diagnostics: UnresolvableInclude[] = [];
|
|
193
472
|
buildCombinedRouteMapWithSearch(
|
|
194
473
|
targetFile,
|
|
195
474
|
exportedName,
|
|
@@ -235,19 +514,8 @@ export function detectUnresolvableIncludesForUrlsFile(
|
|
|
235
514
|
* Call once at startup; the result can be reused on subsequent watcher triggers.
|
|
236
515
|
*/
|
|
237
516
|
export function findRouterFiles(root: string, filter?: ScanFilter): string[] {
|
|
238
|
-
const files = findTsFiles(root, filter);
|
|
239
517
|
const result: string[] = [];
|
|
240
|
-
|
|
241
|
-
if (filePath.includes(".gen.")) continue;
|
|
242
|
-
try {
|
|
243
|
-
const source = readFileSync(filePath, "utf-8");
|
|
244
|
-
if (/\bcreateRouter\s*[<(]/.test(source)) {
|
|
245
|
-
result.push(filePath);
|
|
246
|
-
}
|
|
247
|
-
} catch {
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
518
|
+
findRouterFilesRecursive(root, filter, result);
|
|
251
519
|
return result;
|
|
252
520
|
}
|
|
253
521
|
|
|
@@ -276,35 +544,26 @@ export function writeCombinedRouteTypes(
|
|
|
276
544
|
const routerFilePaths = knownRouterFiles ?? findRouterFiles(root);
|
|
277
545
|
if (routerFilePaths.length === 0) return;
|
|
278
546
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
} catch {
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
// Extract the urls variable name from .routes(varName) or urls: varName
|
|
287
|
-
const urlsVarName = extractUrlsVariableFromRouter(routerSource);
|
|
288
|
-
if (!urlsVarName) continue;
|
|
289
|
-
|
|
290
|
-
// Resolve the variable to its source module
|
|
291
|
-
let result: {
|
|
292
|
-
routes: Record<string, string>;
|
|
293
|
-
searchSchemas: Record<string, Record<string, string>>;
|
|
294
|
-
};
|
|
547
|
+
const nestedRouterConflict = findNestedRouterConflict(routerFilePaths);
|
|
548
|
+
if (nestedRouterConflict) {
|
|
549
|
+
throw new Error(formatNestedRouterConflictError(nestedRouterConflict));
|
|
550
|
+
}
|
|
295
551
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
552
|
+
for (const routerFilePath of routerFilePaths) {
|
|
553
|
+
const result = buildCombinedRouteMapForRouterFile(routerFilePath);
|
|
554
|
+
if (
|
|
555
|
+
Object.keys(result.routes).length === 0 &&
|
|
556
|
+
Object.keys(result.searchSchemas).length === 0
|
|
557
|
+
) {
|
|
558
|
+
// Check if the file even has a createRouter call — if not, skip entirely.
|
|
559
|
+
// If it does, fall through to write an empty placeholder below.
|
|
560
|
+
let routerSource: string;
|
|
561
|
+
try {
|
|
562
|
+
routerSource = readFileSync(routerFilePath, "utf-8");
|
|
563
|
+
} catch {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (!extractUrlsFromRouter(routerSource)) continue;
|
|
308
567
|
}
|
|
309
568
|
|
|
310
569
|
const routerBasename = pathBasename(routerFilePath).replace(
|
|
@@ -61,7 +61,14 @@ export function findTsFiles(dir: string, filter?: ScanFilter): string[] {
|
|
|
61
61
|
for (const entry of entries) {
|
|
62
62
|
const fullPath = join(dir, entry.name);
|
|
63
63
|
if (entry.isDirectory()) {
|
|
64
|
-
if (
|
|
64
|
+
if (
|
|
65
|
+
entry.name === "node_modules" ||
|
|
66
|
+
entry.name.startsWith(".") ||
|
|
67
|
+
entry.name === "dist" ||
|
|
68
|
+
entry.name === "build" ||
|
|
69
|
+
entry.name === "coverage"
|
|
70
|
+
)
|
|
71
|
+
continue;
|
|
65
72
|
results.push(...findTsFiles(fullPath, filter));
|
|
66
73
|
} else if (
|
|
67
74
|
(entry.name.endsWith(".ts") ||
|
|
@@ -214,11 +214,21 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
214
214
|
bgStopCapture = c.stop;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
// Stamp tainted
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
217
|
+
// Stamp tainted ARGS only — not requestCtx. The args stamp guards
|
|
218
|
+
// direct ctx method calls (ctx.set, ctx.header, ctx.onResponse, etc.)
|
|
219
|
+
// which is sufficient for correctness.
|
|
220
|
+
//
|
|
221
|
+
// We intentionally skip stamping requestCtx here because:
|
|
222
|
+
// 1. runBackground starts the async task synchronously (before the
|
|
223
|
+
// first await), so stampCacheExec would pollute the shared
|
|
224
|
+
// requestCtx while the foreground pipeline is still running.
|
|
225
|
+
// This causes assertNotInsideCacheExec to fire when cache-store
|
|
226
|
+
// later calls requestCtx.onResponse().
|
|
227
|
+
// 2. requestCtx methods are closure-bound to the original ctx, so
|
|
228
|
+
// neither Object.create() nor a proxy can isolate the stamp.
|
|
229
|
+
// 3. The foreground miss path already stamps requestCtx and catches
|
|
230
|
+
// cookies()/headers() misuse on first execution. The background
|
|
231
|
+
// re-runs the same function with the same request.
|
|
222
232
|
const bgTaintedArgs: unknown[] = [];
|
|
223
233
|
for (const arg of args) {
|
|
224
234
|
if (isTainted(arg)) {
|
|
@@ -226,9 +236,6 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
226
236
|
bgTaintedArgs.push(arg);
|
|
227
237
|
}
|
|
228
238
|
}
|
|
229
|
-
if (requestCtx) {
|
|
230
|
-
stampCacheExec(requestCtx as object);
|
|
231
|
-
}
|
|
232
239
|
|
|
233
240
|
try {
|
|
234
241
|
const freshResult = await fn.apply(this, args);
|
|
@@ -249,9 +256,6 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
249
256
|
for (const arg of bgTaintedArgs) {
|
|
250
257
|
unstampCacheExec(arg as object);
|
|
251
258
|
}
|
|
252
|
-
if (requestCtx) {
|
|
253
|
-
unstampCacheExec(requestCtx as object);
|
|
254
|
-
}
|
|
255
259
|
// Restore original handle store
|
|
256
260
|
if (originalHandleStore && requestCtx) {
|
|
257
261
|
requestCtx._handleStore = originalHandleStore;
|