@rangojs/router 0.0.0-experimental.78a48627 → 0.0.0-experimental.79

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 (147) hide show
  1. package/README.md +76 -18
  2. package/dist/bin/rango.js +138 -50
  3. package/dist/vite/index.js +853 -435
  4. package/dist/vite/index.js.bak +5448 -0
  5. package/package.json +16 -17
  6. package/skills/cache-guide/SKILL.md +32 -0
  7. package/skills/caching/SKILL.md +45 -4
  8. package/skills/handler-use/SKILL.md +362 -0
  9. package/skills/intercept/SKILL.md +20 -0
  10. package/skills/layout/SKILL.md +22 -0
  11. package/skills/links/SKILL.md +3 -1
  12. package/skills/loader/SKILL.md +53 -43
  13. package/skills/middleware/SKILL.md +34 -3
  14. package/skills/migrate-nextjs/SKILL.md +560 -0
  15. package/skills/migrate-react-router/SKILL.md +764 -0
  16. package/skills/parallel/SKILL.md +185 -0
  17. package/skills/prerender/SKILL.md +110 -68
  18. package/skills/rango/SKILL.md +24 -22
  19. package/skills/route/SKILL.md +55 -0
  20. package/skills/router-setup/SKILL.md +87 -2
  21. package/skills/typesafety/SKILL.md +10 -0
  22. package/src/__internal.ts +1 -1
  23. package/src/browser/app-version.ts +14 -0
  24. package/src/browser/event-controller.ts +5 -0
  25. package/src/browser/navigation-bridge.ts +37 -5
  26. package/src/browser/navigation-client.ts +142 -57
  27. package/src/browser/navigation-store.ts +43 -8
  28. package/src/browser/partial-update.ts +63 -22
  29. package/src/browser/prefetch/cache.ts +73 -11
  30. package/src/browser/prefetch/fetch.ts +98 -27
  31. package/src/browser/prefetch/queue.ts +92 -20
  32. package/src/browser/prefetch/resource-ready.ts +77 -0
  33. package/src/browser/react/Link.tsx +76 -9
  34. package/src/browser/react/NavigationProvider.tsx +16 -7
  35. package/src/browser/react/context.ts +7 -2
  36. package/src/browser/react/use-handle.ts +9 -58
  37. package/src/browser/react/use-router.ts +21 -8
  38. package/src/browser/rsc-router.tsx +134 -59
  39. package/src/browser/scroll-restoration.ts +21 -18
  40. package/src/browser/segment-reconciler.ts +36 -9
  41. package/src/browser/server-action-bridge.ts +8 -6
  42. package/src/browser/types.ts +27 -5
  43. package/src/build/generate-manifest.ts +6 -6
  44. package/src/build/generate-route-types.ts +3 -0
  45. package/src/build/route-trie.ts +50 -24
  46. package/src/build/route-types/include-resolution.ts +8 -1
  47. package/src/build/route-types/router-processing.ts +223 -74
  48. package/src/build/route-types/scan-filter.ts +8 -1
  49. package/src/cache/cache-runtime.ts +15 -11
  50. package/src/cache/cache-scope.ts +48 -7
  51. package/src/cache/cf/cf-cache-store.ts +453 -11
  52. package/src/cache/cf/index.ts +5 -1
  53. package/src/cache/document-cache.ts +17 -7
  54. package/src/cache/index.ts +1 -0
  55. package/src/cache/taint.ts +55 -0
  56. package/src/client.tsx +84 -230
  57. package/src/context-var.ts +72 -2
  58. package/src/debug.ts +2 -2
  59. package/src/handle.ts +40 -0
  60. package/src/index.rsc.ts +3 -1
  61. package/src/index.ts +46 -6
  62. package/src/prerender/store.ts +5 -4
  63. package/src/prerender.ts +138 -77
  64. package/src/reverse.ts +25 -1
  65. package/src/route-definition/dsl-helpers.ts +224 -37
  66. package/src/route-definition/helpers-types.ts +67 -19
  67. package/src/route-definition/index.ts +3 -0
  68. package/src/route-definition/redirect.ts +11 -3
  69. package/src/route-definition/resolve-handler-use.ts +149 -0
  70. package/src/route-types.ts +18 -0
  71. package/src/router/content-negotiation.ts +100 -1
  72. package/src/router/handler-context.ts +82 -23
  73. package/src/router/intercept-resolution.ts +9 -4
  74. package/src/router/lazy-includes.ts +7 -6
  75. package/src/router/loader-resolution.ts +156 -21
  76. package/src/router/logging.ts +1 -1
  77. package/src/router/manifest.ts +28 -15
  78. package/src/router/match-api.ts +124 -189
  79. package/src/router/match-middleware/background-revalidation.ts +30 -2
  80. package/src/router/match-middleware/cache-lookup.ts +94 -17
  81. package/src/router/match-middleware/cache-store.ts +53 -10
  82. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  83. package/src/router/match-middleware/segment-resolution.ts +60 -5
  84. package/src/router/match-result.ts +104 -10
  85. package/src/router/metrics.ts +6 -1
  86. package/src/router/middleware-types.ts +6 -8
  87. package/src/router/middleware.ts +4 -6
  88. package/src/router/navigation-snapshot.ts +182 -0
  89. package/src/router/prerender-match.ts +110 -10
  90. package/src/router/preview-match.ts +30 -102
  91. package/src/router/request-classification.ts +310 -0
  92. package/src/router/route-snapshot.ts +245 -0
  93. package/src/router/router-context.ts +1 -0
  94. package/src/router/router-interfaces.ts +36 -4
  95. package/src/router/router-options.ts +37 -11
  96. package/src/router/segment-resolution/fresh.ts +198 -20
  97. package/src/router/segment-resolution/helpers.ts +29 -24
  98. package/src/router/segment-resolution/loader-cache.ts +1 -0
  99. package/src/router/segment-resolution/revalidation.ts +433 -296
  100. package/src/router/types.ts +1 -0
  101. package/src/router.ts +55 -6
  102. package/src/rsc/handler.ts +472 -372
  103. package/src/rsc/loader-fetch.ts +23 -3
  104. package/src/rsc/manifest-init.ts +5 -1
  105. package/src/rsc/progressive-enhancement.ts +14 -2
  106. package/src/rsc/rsc-rendering.ts +10 -1
  107. package/src/rsc/server-action.ts +8 -0
  108. package/src/rsc/ssr-setup.ts +2 -2
  109. package/src/rsc/types.ts +9 -1
  110. package/src/segment-content-promise.ts +67 -0
  111. package/src/segment-loader-promise.ts +122 -0
  112. package/src/segment-system.tsx +109 -23
  113. package/src/server/context.ts +166 -17
  114. package/src/server/handle-store.ts +19 -0
  115. package/src/server/loader-registry.ts +9 -8
  116. package/src/server/request-context.ts +185 -19
  117. package/src/ssr/index.tsx +4 -0
  118. package/src/static-handler.ts +18 -6
  119. package/src/types/cache-types.ts +4 -4
  120. package/src/types/handler-context.ts +137 -33
  121. package/src/types/loader-types.ts +36 -9
  122. package/src/types/route-entry.ts +12 -1
  123. package/src/types/segments.ts +2 -0
  124. package/src/urls/include-helper.ts +24 -14
  125. package/src/urls/path-helper-types.ts +39 -6
  126. package/src/urls/path-helper.ts +48 -13
  127. package/src/urls/pattern-types.ts +12 -0
  128. package/src/urls/response-types.ts +16 -6
  129. package/src/use-loader.tsx +77 -5
  130. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  131. package/src/vite/discovery/discover-routers.ts +5 -1
  132. package/src/vite/discovery/prerender-collection.ts +128 -74
  133. package/src/vite/discovery/state.ts +13 -6
  134. package/src/vite/index.ts +4 -0
  135. package/src/vite/plugin-types.ts +51 -79
  136. package/src/vite/plugins/expose-action-id.ts +1 -3
  137. package/src/vite/plugins/expose-id-utils.ts +12 -0
  138. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  139. package/src/vite/plugins/expose-internal-ids.ts +257 -40
  140. package/src/vite/plugins/performance-tracks.ts +88 -0
  141. package/src/vite/plugins/refresh-cmd.ts +88 -26
  142. package/src/vite/plugins/version-plugin.ts +13 -1
  143. package/src/vite/rango.ts +163 -211
  144. package/src/vite/router-discovery.ts +178 -45
  145. package/src/vite/utils/banner.ts +3 -3
  146. package/src/vite/utils/prerender-utils.ts +37 -5
  147. package/src/vite/utils/shared-utils.ts +3 -2
@@ -98,8 +98,14 @@ export function buildRouteTrie(
98
98
  }
99
99
 
100
100
  /**
101
- * Insert a route into the trie, handling optional params by forking
102
- * the insertion path (one terminal without the param, one with).
101
+ * Insert a route into the trie. Optional params expand into two branches at
102
+ * registration time (skip-first, then present), so each terminal lives at the
103
+ * correct depth for its number of bound params and carries a branch-local
104
+ * `pa` listing only those names. The trie's single-slot `node.p` is reused
105
+ * across branches because matching ignores `node.p.n` — the leaf's `pa` is
106
+ * the source of truth for naming. Skip-first ordering lets `mergeLeaf`'s
107
+ * last-wins rule produce greedy-leftmost semantics for free at any shared
108
+ * terminal depth.
103
109
  */
104
110
  function insertRoute(
105
111
  node: TrieNode,
@@ -107,14 +113,13 @@ function insertRoute(
107
113
  index: number,
108
114
  leaf: Omit<TrieLeaf, "op" | "cv" | "pa">,
109
115
  ): void {
110
- // Collect param names, optional param names, and constraints across all segments
111
- const paramNames: string[] = [];
116
+ // op (full optional list) and cv (full constraint map) are route-level and
117
+ // identical on every terminal, so compute them once on the shared base.
112
118
  const optionalParams: string[] = [];
113
119
  const constraints: Record<string, string[]> = {};
114
120
 
115
121
  for (const seg of segments) {
116
122
  if (seg.type === "param") {
117
- paramNames.push(seg.value);
118
123
  if (seg.optional) {
119
124
  optionalParams.push(seg.value);
120
125
  }
@@ -124,21 +129,15 @@ function insertRoute(
124
129
  }
125
130
  }
126
131
 
127
- const fullLeaf: TrieLeaf = {
132
+ const leafBase: Omit<TrieLeaf, "pa"> = {
128
133
  ...leaf,
129
- ...(paramNames.length > 0 ? { pa: paramNames } : {}),
130
134
  ...(optionalParams.length > 0 ? { op: optionalParams } : {}),
131
135
  ...(Object.keys(constraints).length > 0 ? { cv: constraints } : {}),
132
136
  };
133
137
 
134
- insertSegments(node, segments, index, fullLeaf);
138
+ insertSegments(node, segments, index, leafBase, []);
135
139
  }
136
140
 
137
- /**
138
- * Recursively insert segments into the trie.
139
- * For optional params, we add a terminal at the current node (param absent)
140
- * AND continue inserting into the param child (param present).
141
- */
142
141
  /**
143
142
  * Extract ancestry map from a built trie by visiting all leaf nodes.
144
143
  * Returns { routeName: ancestryShortCodes[] } for every route in the trie.
@@ -218,15 +217,25 @@ function mergeLeaf(node: TrieNode, leaf: TrieLeaf): void {
218
217
  node.r = mergeLeaves(node.r, leaf);
219
218
  }
220
219
 
220
+ function buildLeaf(
221
+ leafBase: Omit<TrieLeaf, "pa">,
222
+ paramNames: string[],
223
+ ): TrieLeaf {
224
+ return paramNames.length > 0
225
+ ? { ...leafBase, pa: [...paramNames] }
226
+ : { ...leafBase };
227
+ }
228
+
221
229
  function insertSegments(
222
230
  node: TrieNode,
223
231
  segments: ParsedSegment[],
224
232
  index: number,
225
- leaf: TrieLeaf,
233
+ leafBase: Omit<TrieLeaf, "pa">,
234
+ paramNames: string[],
226
235
  ): void {
227
- // Base case: all segments consumed, add terminal
236
+ // Base case: all segments consumed, add terminal with branch-local pa
228
237
  if (index >= segments.length) {
229
- mergeLeaf(node, leaf);
238
+ mergeLeaf(node, buildLeaf(leafBase, paramNames));
230
239
  return;
231
240
  }
232
241
 
@@ -235,12 +244,19 @@ function insertSegments(
235
244
  if (segment.type === "static") {
236
245
  if (!node.s) node.s = {};
237
246
  if (!node.s[segment.value]) node.s[segment.value] = {};
238
- insertSegments(node.s[segment.value], segments, index + 1, leaf);
247
+ insertSegments(
248
+ node.s[segment.value],
249
+ segments,
250
+ index + 1,
251
+ leafBase,
252
+ paramNames,
253
+ );
239
254
  } else if (segment.type === "param") {
240
255
  if (segment.optional) {
241
- // Optional param: add terminal at current node (param absent)
242
- mergeLeaf(node, leaf);
243
- // AND continue with param child (param present)
256
+ // SKIP first: continue at the same node without binding this name.
257
+ // Skip-first ordering means the present-branch's TAKE overwrites any
258
+ // shared terminal later, giving greedy-leftmost semantics.
259
+ insertSegments(node, segments, index + 1, leafBase, paramNames);
244
260
  }
245
261
  if (segment.suffix) {
246
262
  // Suffix param: keyed by suffix string (e.g., ".html")
@@ -248,16 +264,26 @@ function insertSegments(
248
264
  if (!node.xp[segment.suffix]) {
249
265
  node.xp[segment.suffix] = { n: segment.value, c: {} };
250
266
  }
251
- insertSegments(node.xp[segment.suffix].c, segments, index + 1, leaf);
267
+ insertSegments(node.xp[segment.suffix].c, segments, index + 1, leafBase, [
268
+ ...paramNames,
269
+ segment.value,
270
+ ]);
252
271
  } else {
253
272
  if (!node.p) {
254
273
  node.p = { n: segment.value, c: {} };
255
274
  }
256
- insertSegments(node.p.c, segments, index + 1, leaf);
275
+ insertSegments(node.p.c, segments, index + 1, leafBase, [
276
+ ...paramNames,
277
+ segment.value,
278
+ ]);
257
279
  }
258
280
  } else if (segment.type === "wildcard") {
259
- // Wildcard consumes all remaining segments
260
- const wildLeaf = { ...leaf, pn: "*" };
281
+ // Wildcard consumes all remaining segments. Carry any params bound before
282
+ // the wildcard in pa so they zip correctly against paramValues at match.
283
+ const wildLeaf: TrieLeaf & { pn: string } = {
284
+ ...buildLeaf(leafBase, paramNames),
285
+ pn: "*",
286
+ };
261
287
  const existing = node.w ? ({ ...node.w } as TrieLeaf) : undefined;
262
288
  const merged = mergeLeaves(existing, wildLeaf);
263
289
  node.w = merged as TrieLeaf & { pn: string };
@@ -357,12 +357,17 @@ function buildRouteMapFromBlock(
357
357
  /**
358
358
  * Build route map and search schemas together.
359
359
  * Internal helper used by the include resolution path.
360
+ *
361
+ * @param inlineBlock - Optional pre-extracted code block (e.g. from an inline
362
+ * builder function). When provided, variableName is ignored and the block
363
+ * is parsed directly for path()/include() calls.
360
364
  */
361
365
  export function buildCombinedRouteMapWithSearch(
362
366
  filePath: string,
363
367
  variableName?: string,
364
368
  visited?: Set<string>,
365
369
  diagnosticsOut?: UnresolvableInclude[],
370
+ inlineBlock?: string,
366
371
  ): {
367
372
  routes: Record<string, string>;
368
373
  searchSchemas: Record<string, Record<string, string>>;
@@ -384,7 +389,9 @@ export function buildCombinedRouteMapWithSearch(
384
389
  }
385
390
 
386
391
  let block: string;
387
- if (variableName) {
392
+ if (inlineBlock) {
393
+ block = inlineBlock;
394
+ } else if (variableName) {
388
395
  const extracted = extractUrlsBlockForVariable(source, variableName);
389
396
  if (!extracted) return { routes: {}, searchSchemas: {} };
390
397
  block = extracted;
@@ -45,7 +45,9 @@ function isRoutableSourceFile(name: string): boolean {
45
45
  name.endsWith(".tsx") ||
46
46
  name.endsWith(".js") ||
47
47
  name.endsWith(".jsx")) &&
48
- !name.includes(".gen.")
48
+ !name.includes(".gen.") &&
49
+ !name.includes(".test.") &&
50
+ !name.includes(".spec.")
49
51
  );
50
52
  }
51
53
 
@@ -70,7 +72,15 @@ function findRouterFilesRecursive(
70
72
  for (const entry of entries) {
71
73
  const fullPath = join(dir, entry.name);
72
74
  if (entry.isDirectory()) {
73
- if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
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;
74
84
  childDirs.push(fullPath);
75
85
  continue;
76
86
  }
@@ -147,13 +157,26 @@ export function formatNestedRouterConflictError(
147
157
  // ---------------------------------------------------------------------------
148
158
 
149
159
  /**
150
- * Extract the url patterns variable from a router file using AST.
151
- * 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:
152
171
  * 1. createRouter(...).routes(variableName)
153
172
  * 2. createRouter({ urls: variableName, ... })
154
- * 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.
155
176
  */
156
- export function extractUrlsVariableFromRouter(code: string): string | null {
177
+ export function extractUrlsFromRouter(
178
+ code: string,
179
+ ): UrlsExtractionResult | null {
157
180
  const sourceFile = ts.createSourceFile(
158
181
  "router.tsx",
159
182
  code,
@@ -161,7 +184,7 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
161
184
  true,
162
185
  ts.ScriptKind.TSX,
163
186
  );
164
- let result: string | null = null;
187
+ let result: UrlsExtractionResult | null = null;
165
188
 
166
189
  function isCreateRouterCall(node: ts.Node): boolean {
167
190
  if (!ts.isCallExpression(node)) return false;
@@ -169,44 +192,108 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
169
192
  return ts.isIdentifier(callee) && callee.text === "createRouter";
170
193
  }
171
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
+
172
217
  function visit(node: ts.Node) {
173
218
  if (result) return;
174
219
 
175
- // Pattern 1: createRouter(...).routes(variableName)
176
- // The AST shape is CallExpression(.routes) -> PropertyAccessExpression -> CallExpression(createRouter)
220
+ // Pattern 1 & 3: createRouter(...).routes(variableName | builder)
177
221
  if (
178
222
  ts.isCallExpression(node) &&
179
- ts.isPropertyAccessExpression(node.expression) &&
180
- node.expression.name.text === "routes" &&
181
223
  node.arguments.length >= 1 &&
182
- ts.isIdentifier(node.arguments[0])
224
+ isRoutesOnCreateRouter(node)
183
225
  ) {
184
- // Walk up the chain: createRouter().middleware(...).routes(x) etc.
185
- // The innermost call should be createRouter(...)
186
- let inner: ts.Expression = node.expression.expression;
187
- while (
188
- ts.isCallExpression(inner) &&
189
- ts.isPropertyAccessExpression(inner.expression)
190
- ) {
191
- inner = inner.expression.expression;
192
- }
193
- if (isCreateRouterCall(inner)) {
194
- result = (node.arguments[0] as ts.Identifier).text;
195
- 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) };
196
231
  }
232
+ return;
197
233
  }
198
234
 
199
- // Pattern 2: createRouter({ urls: variableName, ... })
235
+ // Pattern 2 & 4: createRouter({ urls: variableName | builder, ... })
200
236
  if (isCreateRouterCall(node)) {
201
237
  const callExpr = node as ts.CallExpression;
202
- 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) {
203
290
  if (ts.isObjectLiteralExpression(arg)) {
204
291
  for (const prop of arg.properties) {
205
292
  if (
206
293
  ts.isPropertyAssignment(prop) &&
207
294
  ts.isIdentifier(prop.name) &&
208
- prop.name.text === "urls" &&
209
- ts.isIdentifier(prop.initializer)
295
+ prop.name.text === "basename" &&
296
+ ts.isStringLiteral(prop.initializer)
210
297
  ) {
211
298
  result = prop.initializer.text;
212
299
  return;
@@ -215,7 +302,6 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
215
302
  }
216
303
  }
217
304
  }
218
-
219
305
  ts.forEachChild(node, visit);
220
306
  }
221
307
 
@@ -223,9 +309,40 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
223
309
  return result;
224
310
  }
225
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
+
226
342
  /**
227
343
  * Resolve routes and search schemas from a router source file by following the
228
- * 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.
229
346
  */
230
347
  export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
231
348
  routes: Record<string, string>;
@@ -238,21 +355,54 @@ export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
238
355
  return { routes: {}, searchSchemas: {} };
239
356
  }
240
357
 
241
- const urlsVarName = extractUrlsVariableFromRouter(routerSource);
242
- if (!urlsVarName) {
358
+ const extraction = extractUrlsFromRouter(routerSource);
359
+ if (!extraction) {
243
360
  return { routes: {}, searchSchemas: {} };
244
361
  }
245
362
 
246
- const imported = resolveImportedVariable(routerSource, urlsVarName);
247
- if (imported) {
248
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
249
- if (!targetFile) {
250
- 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);
251
397
  }
252
- return buildCombinedRouteMapWithSearch(targetFile, imported.exportedName);
253
398
  }
254
399
 
255
- 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;
256
406
  }
257
407
 
258
408
  // ---------------------------------------------------------------------------
@@ -275,12 +425,26 @@ export function detectUnresolvableIncludes(
275
425
  return [];
276
426
  }
277
427
 
278
- // Extract the urls variable from the router file
279
- const urlsVarName = extractUrlsVariableFromRouter(source);
280
- 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
+ }
281
445
 
282
- // Resolve where the urls variable comes from
283
- const imported = resolveImportedVariable(source, urlsVarName);
446
+ // Variable reference: resolve where it comes from
447
+ const imported = resolveImportedVariable(source, extraction.name);
284
448
  let targetFile: string;
285
449
  let exportedName: string | undefined;
286
450
 
@@ -302,10 +466,9 @@ export function detectUnresolvableIncludes(
302
466
  } else {
303
467
  // Same-file urls() definition
304
468
  targetFile = realPath;
305
- exportedName = urlsVarName;
469
+ exportedName = extraction.name;
306
470
  }
307
471
 
308
- const diagnostics: UnresolvableInclude[] = [];
309
472
  buildCombinedRouteMapWithSearch(
310
473
  targetFile,
311
474
  exportedName,
@@ -387,34 +550,20 @@ export function writeCombinedRouteTypes(
387
550
  }
388
551
 
389
552
  for (const routerFilePath of routerFilePaths) {
390
- let routerSource: string;
391
- try {
392
- routerSource = readFileSync(routerFilePath, "utf-8");
393
- } catch {
394
- continue;
395
- }
396
- // Extract the urls variable name from .routes(varName) or urls: varName
397
- const urlsVarName = extractUrlsVariableFromRouter(routerSource);
398
- if (!urlsVarName) continue;
399
-
400
- // Resolve the variable to its source module
401
- let result: {
402
- routes: Record<string, string>;
403
- searchSchemas: Record<string, Record<string, string>>;
404
- };
405
-
406
- const imported = resolveImportedVariable(routerSource, urlsVarName);
407
- if (imported) {
408
- // Variable is imported from another module
409
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
410
- if (!targetFile) continue;
411
- result = buildCombinedRouteMapWithSearch(
412
- targetFile,
413
- imported.exportedName,
414
- );
415
- } else {
416
- // Variable is defined in the same file
417
- result = buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
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;
418
567
  }
419
568
 
420
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;