@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.
Files changed (160) hide show
  1. package/{CLAUDE.md → AGENTS.md} +4 -0
  2. package/README.md +122 -30
  3. package/dist/bin/rango.js +245 -63
  4. package/dist/vite/index.js +859 -418
  5. package/package.json +3 -3
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +32 -0
  8. package/skills/caching/SKILL.md +49 -8
  9. package/skills/document-cache/SKILL.md +2 -2
  10. package/skills/hooks/SKILL.md +33 -31
  11. package/skills/host-router/SKILL.md +218 -0
  12. package/skills/links/SKILL.md +3 -1
  13. package/skills/loader/SKILL.md +72 -22
  14. package/skills/middleware/SKILL.md +2 -0
  15. package/skills/parallel/SKILL.md +126 -0
  16. package/skills/prerender/SKILL.md +112 -70
  17. package/skills/rango/SKILL.md +0 -1
  18. package/skills/route/SKILL.md +34 -4
  19. package/skills/router-setup/SKILL.md +95 -5
  20. package/skills/typesafety/SKILL.md +35 -23
  21. package/src/__internal.ts +92 -0
  22. package/src/bin/rango.ts +18 -0
  23. package/src/browser/app-version.ts +14 -0
  24. package/src/browser/event-controller.ts +5 -0
  25. package/src/browser/link-interceptor.ts +4 -0
  26. package/src/browser/navigation-bridge.ts +114 -18
  27. package/src/browser/navigation-client.ts +126 -44
  28. package/src/browser/navigation-store.ts +43 -8
  29. package/src/browser/navigation-transaction.ts +11 -9
  30. package/src/browser/partial-update.ts +80 -15
  31. package/src/browser/prefetch/cache.ts +166 -27
  32. package/src/browser/prefetch/fetch.ts +52 -39
  33. package/src/browser/prefetch/policy.ts +6 -0
  34. package/src/browser/prefetch/queue.ts +92 -20
  35. package/src/browser/prefetch/resource-ready.ts +77 -0
  36. package/src/browser/react/Link.tsx +70 -14
  37. package/src/browser/react/NavigationProvider.tsx +40 -4
  38. package/src/browser/react/context.ts +7 -2
  39. package/src/browser/react/use-handle.ts +9 -58
  40. package/src/browser/react/use-router.ts +21 -8
  41. package/src/browser/rsc-router.tsx +143 -59
  42. package/src/browser/scroll-restoration.ts +41 -42
  43. package/src/browser/segment-reconciler.ts +6 -1
  44. package/src/browser/server-action-bridge.ts +454 -436
  45. package/src/browser/types.ts +60 -5
  46. package/src/build/generate-manifest.ts +6 -6
  47. package/src/build/generate-route-types.ts +5 -0
  48. package/src/build/route-trie.ts +19 -3
  49. package/src/build/route-types/include-resolution.ts +8 -1
  50. package/src/build/route-types/router-processing.ts +346 -87
  51. package/src/build/route-types/scan-filter.ts +8 -1
  52. package/src/cache/cache-runtime.ts +15 -11
  53. package/src/cache/cache-scope.ts +48 -7
  54. package/src/cache/cf/cf-cache-store.ts +453 -11
  55. package/src/cache/cf/index.ts +5 -1
  56. package/src/cache/document-cache.ts +17 -7
  57. package/src/cache/index.ts +1 -0
  58. package/src/cache/taint.ts +55 -0
  59. package/src/client.rsc.tsx +2 -1
  60. package/src/client.tsx +3 -102
  61. package/src/context-var.ts +72 -2
  62. package/src/debug.ts +2 -2
  63. package/src/handle.ts +40 -0
  64. package/src/handles/breadcrumbs.ts +66 -0
  65. package/src/handles/index.ts +1 -0
  66. package/src/host/index.ts +0 -3
  67. package/src/index.rsc.ts +8 -37
  68. package/src/index.ts +40 -66
  69. package/src/prerender/store.ts +57 -15
  70. package/src/prerender.ts +138 -77
  71. package/src/reverse.ts +22 -1
  72. package/src/route-definition/dsl-helpers.ts +73 -25
  73. package/src/route-definition/helpers-types.ts +10 -6
  74. package/src/route-definition/index.ts +3 -3
  75. package/src/route-definition/redirect.ts +11 -3
  76. package/src/route-definition/resolve-handler-use.ts +149 -0
  77. package/src/route-map-builder.ts +7 -1
  78. package/src/route-types.ts +11 -0
  79. package/src/router/content-negotiation.ts +100 -1
  80. package/src/router/find-match.ts +4 -2
  81. package/src/router/handler-context.ts +108 -25
  82. package/src/router/intercept-resolution.ts +11 -4
  83. package/src/router/lazy-includes.ts +4 -1
  84. package/src/router/loader-resolution.ts +123 -11
  85. package/src/router/logging.ts +5 -2
  86. package/src/router/manifest.ts +9 -3
  87. package/src/router/match-api.ts +125 -190
  88. package/src/router/match-middleware/background-revalidation.ts +30 -2
  89. package/src/router/match-middleware/cache-lookup.ts +88 -16
  90. package/src/router/match-middleware/cache-store.ts +53 -10
  91. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  92. package/src/router/match-middleware/segment-resolution.ts +61 -5
  93. package/src/router/match-result.ts +22 -15
  94. package/src/router/metrics.ts +238 -13
  95. package/src/router/middleware-types.ts +53 -12
  96. package/src/router/middleware.ts +172 -85
  97. package/src/router/navigation-snapshot.ts +182 -0
  98. package/src/router/pattern-matching.ts +20 -5
  99. package/src/router/prerender-match.ts +114 -10
  100. package/src/router/preview-match.ts +30 -102
  101. package/src/router/request-classification.ts +310 -0
  102. package/src/router/revalidation.ts +27 -7
  103. package/src/router/route-snapshot.ts +245 -0
  104. package/src/router/router-context.ts +6 -1
  105. package/src/router/router-interfaces.ts +50 -5
  106. package/src/router/router-options.ts +50 -19
  107. package/src/router/segment-resolution/fresh.ts +200 -19
  108. package/src/router/segment-resolution/helpers.ts +30 -25
  109. package/src/router/segment-resolution/loader-cache.ts +1 -0
  110. package/src/router/segment-resolution/revalidation.ts +429 -301
  111. package/src/router/segment-wrappers.ts +2 -0
  112. package/src/router/trie-matching.ts +20 -2
  113. package/src/router/types.ts +1 -0
  114. package/src/router.ts +88 -15
  115. package/src/rsc/handler.ts +546 -359
  116. package/src/rsc/index.ts +0 -20
  117. package/src/rsc/manifest-init.ts +5 -1
  118. package/src/rsc/progressive-enhancement.ts +25 -8
  119. package/src/rsc/rsc-rendering.ts +35 -43
  120. package/src/rsc/server-action.ts +16 -10
  121. package/src/rsc/ssr-setup.ts +128 -0
  122. package/src/rsc/types.ts +10 -1
  123. package/src/search-params.ts +16 -13
  124. package/src/segment-system.tsx +140 -4
  125. package/src/server/context.ts +148 -16
  126. package/src/server/loader-registry.ts +9 -8
  127. package/src/server/request-context.ts +182 -34
  128. package/src/server.ts +6 -0
  129. package/src/ssr/index.tsx +4 -0
  130. package/src/static-handler.ts +18 -6
  131. package/src/theme/index.ts +4 -13
  132. package/src/types/cache-types.ts +4 -4
  133. package/src/types/handler-context.ts +149 -49
  134. package/src/types/loader-types.ts +36 -9
  135. package/src/types/route-config.ts +17 -8
  136. package/src/types/route-entry.ts +8 -1
  137. package/src/types/segments.ts +2 -5
  138. package/src/urls/path-helper-types.ts +9 -2
  139. package/src/urls/path-helper.ts +48 -13
  140. package/src/urls/pattern-types.ts +12 -0
  141. package/src/urls/response-types.ts +16 -6
  142. package/src/use-loader.tsx +73 -4
  143. package/src/vite/discovery/bundle-postprocess.ts +61 -89
  144. package/src/vite/discovery/discover-routers.ts +23 -5
  145. package/src/vite/discovery/prerender-collection.ts +48 -15
  146. package/src/vite/discovery/state.ts +17 -13
  147. package/src/vite/index.ts +8 -3
  148. package/src/vite/plugin-types.ts +51 -79
  149. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  150. package/src/vite/plugins/expose-action-id.ts +1 -3
  151. package/src/vite/plugins/performance-tracks.ts +88 -0
  152. package/src/vite/plugins/refresh-cmd.ts +127 -0
  153. package/src/vite/plugins/version-plugin.ts +13 -1
  154. package/src/vite/rango.ts +174 -211
  155. package/src/vite/router-discovery.ts +169 -42
  156. package/src/vite/utils/banner.ts +3 -3
  157. package/src/vite/utils/prerender-utils.ts +78 -0
  158. package/src/vite/utils/shared-utils.ts +3 -2
  159. package/skills/testing/SKILL.md +0 -226
  160. package/src/route-definition/route-function.ts +0 -119
@@ -1,9 +1,20 @@
1
- import { readFileSync, writeFileSync, existsSync, unlinkSync } from "node:fs";
2
- import { join, dirname, resolve, basename as pathBasename } from "node:path";
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
- * Extract the url patterns variable from a router file using AST.
35
- * Detects two patterns:
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
- * Returns the local variable name.
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 extractUrlsVariableFromRouter(code: string): string | null {
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: string | null = null;
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
- ts.isIdentifier(node.arguments[0])
224
+ isRoutesOnCreateRouter(node)
67
225
  ) {
68
- // Walk up the chain: createRouter().middleware(...).routes(x) etc.
69
- // The innermost call should be createRouter(...)
70
- let inner: ts.Expression = node.expression.expression;
71
- while (
72
- ts.isCallExpression(inner) &&
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 arg of callExpr.arguments) {
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 === "urls" &&
93
- ts.isIdentifier(prop.initializer)
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 urlsVarName = extractUrlsVariableFromRouter(routerSource);
126
- if (!urlsVarName) {
358
+ const extraction = extractUrlsFromRouter(routerSource);
359
+ if (!extraction) {
127
360
  return { routes: {}, searchSchemas: {} };
128
361
  }
129
362
 
130
- const imported = resolveImportedVariable(routerSource, urlsVarName);
131
- if (imported) {
132
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
133
- if (!targetFile) {
134
- return { routes: {}, searchSchemas: {} };
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
- return buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
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 variable from the router file
163
- const urlsVarName = extractUrlsVariableFromRouter(source);
164
- if (!urlsVarName) return [];
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
- // Resolve where the urls variable comes from
167
- const imported = resolveImportedVariable(source, urlsVarName);
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 = urlsVarName;
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
- for (const filePath of files) {
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
- for (const routerFilePath of routerFilePaths) {
280
- let routerSource: string;
281
- try {
282
- routerSource = readFileSync(routerFilePath, "utf-8");
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
- const imported = resolveImportedVariable(routerSource, urlsVarName);
297
- if (imported) {
298
- // Variable is imported from another module
299
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
300
- if (!targetFile) continue;
301
- result = buildCombinedRouteMapWithSearch(
302
- targetFile,
303
- imported.exportedName,
304
- );
305
- } else {
306
- // Variable is defined in the same file
307
- result = buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
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 (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
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 args and RequestContext so request-scoped
218
- // reads (cookies, headers) and side effects (ctx.set, etc.)
219
- // throw inside background revalidation, same as the miss path.
220
- // Uses ref-counted stamp/unstamp so overlapping executions
221
- // sharing the same ctx don't clear each other's guards.
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;