@rangojs/router 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/dist/bin/rango.js +343 -125
  2. package/dist/types/browser/react/use-router.d.ts +10 -3
  3. package/dist/types/browser/react/use-search-params.d.ts +57 -10
  4. package/dist/types/browser/types.d.ts +22 -0
  5. package/dist/types/build/merge-full-manifests.d.ts +3 -0
  6. package/dist/types/build/route-trie.d.ts +4 -73
  7. package/dist/types/build/route-types/per-module-writer.d.ts +6 -4
  8. package/dist/types/build/route-types/router-processing.d.ts +2 -3
  9. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  10. package/dist/types/cache/taint.d.ts +12 -6
  11. package/dist/types/client-urls/client-root.d.ts +38 -0
  12. package/dist/types/client-urls/client-urls.d.ts +5 -0
  13. package/dist/types/client-urls/navigation.d.ts +38 -0
  14. package/dist/types/client-urls/revalidation-protocol.d.ts +25 -0
  15. package/dist/types/client-urls/server-projection.d.ts +62 -0
  16. package/dist/types/client-urls/types.d.ts +144 -0
  17. package/dist/types/client.d.ts +12 -4
  18. package/dist/types/client.rsc.d.ts +4 -1
  19. package/dist/types/decode-loader-results.d.ts +37 -0
  20. package/dist/types/errors.d.ts +1 -0
  21. package/dist/types/index.d.ts +1 -1
  22. package/dist/types/loader-redirect.d.ts +27 -0
  23. package/dist/types/outlet-context.d.ts +12 -0
  24. package/dist/types/outlet-provider.d.ts +3 -1
  25. package/dist/types/redirect-origin.d.ts +4 -0
  26. package/dist/types/route-content-wrapper.d.ts +42 -1
  27. package/dist/types/route-definition/helpers-types.d.ts +13 -2
  28. package/dist/types/router/error-handling.d.ts +35 -1
  29. package/dist/types/router/intercept-resolution.d.ts +12 -0
  30. package/dist/types/router/loader-resolution.d.ts +24 -2
  31. package/dist/types/router/revalidation.d.ts +7 -0
  32. package/dist/types/router/route-trie-builder.d.ts +77 -0
  33. package/dist/types/router/router-interfaces.d.ts +20 -0
  34. package/dist/types/router/segment-resolution/helpers.d.ts +1 -1
  35. package/dist/types/router/trie-matching.d.ts +1 -1
  36. package/dist/types/rsc/manifest-init.d.ts +5 -5
  37. package/dist/types/rsc/shell-capture.d.ts +9 -0
  38. package/dist/types/rsc/shell-serve.d.ts +11 -0
  39. package/dist/types/rsc/types.d.ts +30 -0
  40. package/dist/types/segment-system.d.ts +2 -0
  41. package/dist/types/server/context.d.ts +10 -0
  42. package/dist/types/server/handle-store.d.ts +34 -3
  43. package/dist/types/server/request-context.d.ts +11 -1
  44. package/dist/types/server.d.ts +1 -0
  45. package/dist/types/ssr/index.d.ts +22 -0
  46. package/dist/types/ssr/ssr-root.d.ts +10 -0
  47. package/dist/types/testing/dom.entry.d.ts +1 -1
  48. package/dist/types/testing/render-route.d.ts +16 -6
  49. package/dist/types/testing/run-loader.d.ts +9 -0
  50. package/dist/types/types/boundaries.d.ts +22 -0
  51. package/dist/types/types/index.d.ts +1 -1
  52. package/dist/types/types/loader-types.d.ts +57 -5
  53. package/dist/types/types/segments.d.ts +7 -0
  54. package/dist/types/urls/path-helper-types.d.ts +10 -4
  55. package/dist/types/vite/discovery/client-urls-projection.d.ts +53 -0
  56. package/dist/types/vite/discovery/discover-routers.d.ts +1 -1
  57. package/dist/types/vite/discovery/state.d.ts +8 -1
  58. package/dist/vite/index.js +5313 -2365
  59. package/package.json +1 -1
  60. package/skills/breadcrumbs/SKILL.md +39 -9
  61. package/skills/catalog.json +7 -1
  62. package/skills/client-urls/SKILL.md +338 -0
  63. package/skills/comparison/references/framework-comparison.md +23 -9
  64. package/skills/hooks/SKILL.md +2 -2
  65. package/skills/hooks/data.md +11 -2
  66. package/skills/hooks/handle-and-actions.md +7 -0
  67. package/skills/hooks/outlets.md +26 -5
  68. package/skills/hooks/urls.md +40 -3
  69. package/skills/loader/SKILL.md +132 -20
  70. package/skills/migrate-nextjs/SKILL.md +70 -10
  71. package/skills/migrate-react-router/SKILL.md +49 -13
  72. package/skills/migrate-react-router/component-migration.md +18 -13
  73. package/skills/migrate-react-router/data-and-actions.md +14 -3
  74. package/skills/migrate-react-router/route-mapping.md +15 -2
  75. package/skills/parallel/SKILL.md +32 -1
  76. package/skills/ppr/SKILL.md +16 -6
  77. package/skills/prerender/SKILL.md +8 -4
  78. package/skills/rango/SKILL.md +21 -17
  79. package/skills/react-compiler/SKILL.md +3 -3
  80. package/skills/route/SKILL.md +5 -2
  81. package/skills/router-setup/SKILL.md +16 -2
  82. package/skills/scripts/SKILL.md +16 -6
  83. package/skills/shell-manifest/SKILL.md +16 -7
  84. package/skills/testing/SKILL.md +2 -2
  85. package/skills/testing/client-components.md +6 -0
  86. package/skills/testing/handles.md +30 -8
  87. package/skills/testing/loader.md +51 -49
  88. package/skills/testing/middleware.md +1 -1
  89. package/skills/theme/SKILL.md +8 -5
  90. package/src/bin/rango.ts +7 -3
  91. package/src/browser/navigation-bridge.ts +6 -0
  92. package/src/browser/navigation-client.ts +5 -0
  93. package/src/browser/partial-update.ts +65 -13
  94. package/src/browser/react/use-router.ts +40 -11
  95. package/src/browser/react/use-search-params.ts +140 -17
  96. package/src/browser/rsc-router.tsx +59 -0
  97. package/src/browser/server-action-bridge.ts +26 -0
  98. package/src/browser/types.ts +22 -0
  99. package/src/build/merge-full-manifests.ts +161 -0
  100. package/src/build/route-trie.ts +9 -332
  101. package/src/build/route-types/include-resolution.ts +66 -11
  102. package/src/build/route-types/per-module-writer.ts +11 -6
  103. package/src/build/route-types/router-processing.ts +184 -153
  104. package/src/build/runtime-discovery.ts +23 -12
  105. package/src/cache/cache-exec-scope.ts +47 -0
  106. package/src/cache/cache-runtime.ts +24 -25
  107. package/src/cache/taint.ts +28 -9
  108. package/src/client-urls/client-root.tsx +168 -0
  109. package/src/client-urls/client-urls.ts +698 -0
  110. package/src/client-urls/navigation.ts +237 -0
  111. package/src/client-urls/revalidation-protocol.ts +56 -0
  112. package/src/client-urls/server-projection.ts +579 -0
  113. package/src/client-urls/types.ts +195 -0
  114. package/src/client.rsc.tsx +12 -0
  115. package/src/client.tsx +49 -6
  116. package/src/decode-loader-results.ts +113 -0
  117. package/src/errors.ts +14 -0
  118. package/src/handles/deferred-resolution.ts +14 -7
  119. package/src/index.ts +1 -0
  120. package/src/loader-redirect.tsx +64 -0
  121. package/src/outlet-context.ts +12 -0
  122. package/src/outlet-provider.tsx +15 -1
  123. package/src/redirect-origin.ts +29 -0
  124. package/src/route-content-wrapper.tsx +96 -3
  125. package/src/route-definition/dsl-helpers.ts +28 -3
  126. package/src/route-definition/helpers-types.ts +13 -0
  127. package/src/route-definition/redirect.ts +17 -18
  128. package/src/router/error-handling.ts +65 -11
  129. package/src/router/intercept-resolution.ts +29 -0
  130. package/src/router/loader-resolution.ts +261 -28
  131. package/src/router/match-result.ts +7 -0
  132. package/src/router/revalidation.ts +24 -11
  133. package/src/router/route-trie-builder.ts +334 -0
  134. package/src/router/router-interfaces.ts +38 -0
  135. package/src/router/segment-resolution/fresh.ts +47 -0
  136. package/src/router/segment-resolution/helpers.ts +9 -11
  137. package/src/router/segment-resolution/loader-cache.ts +14 -24
  138. package/src/router/segment-resolution/revalidation.ts +20 -1
  139. package/src/router/trie-matching.ts +3 -3
  140. package/src/router.ts +46 -1
  141. package/src/rsc/full-payload.ts +6 -0
  142. package/src/rsc/handler.ts +10 -7
  143. package/src/rsc/loader-fetch.ts +2 -2
  144. package/src/rsc/manifest-init.ts +28 -9
  145. package/src/rsc/rsc-rendering.ts +15 -1
  146. package/src/rsc/shell-capture.ts +12 -0
  147. package/src/rsc/shell-serve.ts +15 -2
  148. package/src/rsc/ssr-setup.ts +10 -1
  149. package/src/rsc/types.ts +31 -2
  150. package/src/segment-system.tsx +83 -26
  151. package/src/server/context.ts +10 -0
  152. package/src/server/cookie-store.ts +19 -19
  153. package/src/server/handle-store.ts +185 -48
  154. package/src/server/request-context.ts +30 -6
  155. package/src/server.ts +7 -0
  156. package/src/ssr/index.tsx +37 -2
  157. package/src/ssr/ssr-root.tsx +29 -2
  158. package/src/testing/dom.entry.ts +1 -1
  159. package/src/testing/render-route.tsx +22 -8
  160. package/src/testing/run-loader.ts +51 -13
  161. package/src/types/boundaries.ts +19 -0
  162. package/src/types/index.ts +1 -0
  163. package/src/types/loader-types.ts +60 -5
  164. package/src/types/segments.ts +7 -0
  165. package/src/urls/include-helper.ts +22 -4
  166. package/src/urls/path-helper-types.ts +14 -1
  167. package/src/use-loader.tsx +67 -6
  168. package/src/vite/discovery/client-urls-projection.ts +322 -0
  169. package/src/vite/discovery/discover-routers.ts +43 -17
  170. package/src/vite/discovery/state.ts +11 -1
  171. package/src/vite/discovery/virtual-module-codegen.ts +20 -0
  172. package/src/vite/plugins/virtual-entries.ts +12 -3
  173. package/src/vite/router-discovery.ts +163 -12
@@ -77,6 +77,14 @@ function readSourceMemoized(
77
77
  return source;
78
78
  }
79
79
 
80
+ function isUrlsDefinitionCall(node: ts.CallExpression): boolean {
81
+ const callee = node.expression;
82
+ return (
83
+ ts.isIdentifier(callee) &&
84
+ (callee.text === "urls" || callee.text === "clientUrls")
85
+ );
86
+ }
87
+
80
88
  // ---------------------------------------------------------------------------
81
89
  // AST-based include() parsing
82
90
  // ---------------------------------------------------------------------------
@@ -206,10 +214,9 @@ function extractDynamicImportSpecifier(node: ts.Expression): string | null {
206
214
  }
207
215
 
208
216
  /**
209
- * Resolve a module's `export default` to either a same-file `urls()` variable
210
- * name (`export default shopPatterns`) or the inline call block
211
- * (`export default urls(...)`). Used to walk async include modules
212
- * (`() => import("./mod")`), whose convention is `export default urls(...)`.
217
+ * Resolve a module's `export default` to either a same-file route variable name
218
+ * (`export default shopPatterns`) or an inline urls()/clientUrls() call block.
219
+ * Used for default imports and to walk async include modules.
213
220
  */
214
221
  function resolveDefaultExportTarget(
215
222
  code: string,
@@ -223,11 +230,8 @@ function resolveDefaultExportTarget(
223
230
  const expr = node.expression;
224
231
  if (ts.isIdentifier(expr)) {
225
232
  result = { variableName: expr.text };
226
- } else if (ts.isCallExpression(expr)) {
227
- const callee = expr.expression;
228
- if (ts.isIdentifier(callee) && callee.text === "urls") {
229
- result = { inlineBlock: expr.getText(sourceFile) };
230
- }
233
+ } else if (ts.isCallExpression(expr) && isUrlsDefinitionCall(expr)) {
234
+ result = { inlineBlock: expr.getText(sourceFile) };
231
235
  }
232
236
  return;
233
237
  }
@@ -358,6 +362,16 @@ export function resolveImportedVariable(
358
362
  code: string,
359
363
  localName: string,
360
364
  ): { specifier: string; exportedName: string } | null {
365
+ const defaultImportRegex =
366
+ /import\s+([\w$]+)\s*(?:,\s*\{[^}]*\})?\s+from\s*["']([^"']+)["']/g;
367
+ let defaultMatch;
368
+
369
+ while ((defaultMatch = defaultImportRegex.exec(code)) !== null) {
370
+ if (defaultMatch[1] === localName) {
371
+ return { specifier: defaultMatch[2], exportedName: "default" };
372
+ }
373
+ }
374
+
361
375
  // Allow an optional leading default binding before the named-import brace so
362
376
  // a combined `import Foo, { bar } from "..."` is matched (the named members
363
377
  // are the only part we resolve; the default binding is skipped). Without the
@@ -453,8 +467,7 @@ function extractUrlsBlockForVariable(
453
467
  node.initializer &&
454
468
  ts.isCallExpression(node.initializer)
455
469
  ) {
456
- const callee = node.initializer.expression;
457
- if (ts.isIdentifier(callee) && callee.text === "urls") {
470
+ if (isUrlsDefinitionCall(node.initializer)) {
458
471
  result = node.initializer.getText(sourceFile);
459
472
  return;
460
473
  }
@@ -780,6 +793,48 @@ export function buildCombinedRouteMapWithSearch(
780
793
  let block: string;
781
794
  if (inlineBlock) {
782
795
  block = inlineBlock;
796
+ } else if (variableName === "default") {
797
+ const defaultTarget = resolveDefaultExportTarget(
798
+ source,
799
+ parseBlock(memo, source),
800
+ );
801
+ if (!defaultTarget) {
802
+ visited.delete(key);
803
+ return { routes: {}, searchSchemas: {} };
804
+ }
805
+ if (defaultTarget.inlineBlock) {
806
+ block = defaultTarget.inlineBlock;
807
+ } else {
808
+ const targetName = defaultTarget.variableName!;
809
+ const imported = resolveImportedVariable(source, targetName);
810
+ if (imported) {
811
+ const targetFile = resolveImportPath(imported.specifier, realPath);
812
+ if (!targetFile) {
813
+ visited.delete(key);
814
+ return { routes: {}, searchSchemas: {} };
815
+ }
816
+ const result = buildCombinedRouteMapWithSearch(
817
+ targetFile,
818
+ imported.exportedName,
819
+ visited,
820
+ diagnosticsOut,
821
+ undefined,
822
+ memo,
823
+ );
824
+ visited.delete(key);
825
+ return result;
826
+ }
827
+ const extracted = extractUrlsBlockForVariable(
828
+ source,
829
+ targetName,
830
+ parseBlock(memo, source),
831
+ );
832
+ if (!extracted) {
833
+ visited.delete(key);
834
+ return { routes: {}, searchSchemas: {} };
835
+ }
836
+ block = extracted;
837
+ }
783
838
  } else if (variableName) {
784
839
  const extracted = extractUrlsBlockForVariable(
785
840
  source,
@@ -12,8 +12,9 @@ import { findTsFiles } from "./scan-filter.js";
12
12
 
13
13
  /**
14
14
  * Generate per-module route type files by statically parsing url module source.
15
- * Scans for files containing `urls(` and writes a sibling `.gen.ts` with the
16
- * extracted route name/pattern pairs. Only writes when content has changed.
15
+ * Scans for files containing `urls(` or `clientUrls(` and writes a sibling
16
+ * `.gen.ts` with the extracted route name/pattern pairs. Only writes when
17
+ * content has changed.
17
18
  */
18
19
  export function writePerModuleRouteTypes(
19
20
  root: string,
@@ -26,7 +27,7 @@ export function writePerModuleRouteTypes(
26
27
  }
27
28
 
28
29
  /**
29
- * Find all variable names assigned to urls() calls in source code.
30
+ * Find all variable names assigned to urls() or clientUrls() calls in source.
30
31
  * e.g. `export const patterns = urls(...)` -> ["patterns"]
31
32
  */
32
33
  export function findUrlsVariableNames(code: string): string[] {
@@ -47,7 +48,10 @@ export function findUrlsVariableNames(code: string): string[] {
47
48
  ts.isCallExpression(node.initializer)
48
49
  ) {
49
50
  const callee = node.initializer.expression;
50
- if (ts.isIdentifier(callee) && callee.text === "urls") {
51
+ if (
52
+ ts.isIdentifier(callee) &&
53
+ (callee.text === "urls" || callee.text === "clientUrls")
54
+ ) {
51
55
  names.push(node.name.text);
52
56
  }
53
57
  }
@@ -61,12 +65,13 @@ export function findUrlsVariableNames(code: string): string[] {
61
65
  /**
62
66
  * Generate per-module route types for a single url module file.
63
67
  * Follows include() calls recursively to produce the full route tree.
64
- * No-ops if the file doesn't contain `urls(` or has no named routes.
68
+ * No-ops if the file doesn't contain `urls(`/`clientUrls(` or has no named
69
+ * routes.
65
70
  */
66
71
  export function writePerModuleRouteTypesForFile(filePath: string): void {
67
72
  try {
68
73
  const source = readFileSync(filePath, "utf-8");
69
- if (!source.includes("urls(")) return;
74
+ if (!source.includes("urls(") && !source.includes("clientUrls(")) return;
70
75
 
71
76
  const varNames = findUrlsVariableNames(source);
72
77
 
@@ -20,6 +20,8 @@ import {
20
20
  resolveImportedVariable,
21
21
  resolveImportPath,
22
22
  buildCombinedRouteMapWithSearch,
23
+ createScanMemo,
24
+ type ScanMemo,
23
25
  type UnresolvableInclude,
24
26
  } from "./include-resolution.js";
25
27
  import { findUrlsVariableNames } from "./per-module-writer.js";
@@ -197,18 +199,43 @@ export type UrlsExtractionResult =
197
199
  | { kind: "variable"; name: string }
198
200
  | { kind: "inline"; block: string };
199
201
 
200
- /**
201
- * Extract the url patterns from a router file using AST.
202
- * Detects four patterns:
203
- * 1. createRouter(...).routes(variableName)
204
- * 2. createRouter({ urls: variableName, ... })
205
- * 3. createRouter(...).routes(({ path, ... }) => [...])
206
- * 4. createRouter({ urls: ({ path, ... }) => [...], ... })
207
- * Returns either a variable name or an inline code block.
208
- */
209
- export function extractUrlsFromRouter(
210
- code: string,
202
+ function isCreateRouterCall(node: ts.Node): boolean {
203
+ if (!ts.isCallExpression(node)) return false;
204
+ const callee = node.expression;
205
+ return ts.isIdentifier(callee) && callee.text === "createRouter";
206
+ }
207
+
208
+ function extractUrlsArgument(
209
+ node: ts.Expression,
210
+ sourceFile: ts.SourceFile,
211
211
  ): UrlsExtractionResult | null {
212
+ if (ts.isIdentifier(node)) {
213
+ return { kind: "variable", name: node.text };
214
+ }
215
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
216
+ return { kind: "inline", block: node.getText(sourceFile) };
217
+ }
218
+ return null;
219
+ }
220
+
221
+ /** Return a createRouter call chain in runtime registration order. */
222
+ function collectCreateRouterCallChain(
223
+ endpoint: ts.CallExpression,
224
+ ): ts.CallExpression[] | null {
225
+ const calls: ts.CallExpression[] = [];
226
+ let current = endpoint;
227
+
228
+ while (true) {
229
+ calls.push(current);
230
+ if (isCreateRouterCall(current)) return calls.reverse();
231
+ if (!ts.isPropertyAccessExpression(current.expression)) return null;
232
+ const receiver = current.expression.expression;
233
+ if (!ts.isCallExpression(receiver)) return null;
234
+ current = receiver;
235
+ }
236
+ }
237
+
238
+ function extractAllUrlsFromRouter(code: string): UrlsExtractionResult[] {
212
239
  const sourceFile = ts.createSourceFile(
213
240
  "router.tsx",
214
241
  code,
@@ -216,85 +243,71 @@ export function extractUrlsFromRouter(
216
243
  true,
217
244
  ts.ScriptKind.TSX,
218
245
  );
219
- let result: UrlsExtractionResult | null = null;
220
-
221
- function isCreateRouterCall(node: ts.Node): boolean {
222
- if (!ts.isCallExpression(node)) return false;
223
- const callee = node.expression;
224
- return ts.isIdentifier(callee) && callee.text === "createRouter";
225
- }
226
-
227
- /** Check if a node is an arrow/function expression (inline builder). */
228
- function isInlineBuilder(node: ts.Node): boolean {
229
- return ts.isArrowFunction(node) || ts.isFunctionExpression(node);
230
- }
231
-
232
- /** Check if a .routes() call chains from createRouter(). */
233
- function isRoutesOnCreateRouter(node: ts.CallExpression): boolean {
234
- if (
235
- !ts.isPropertyAccessExpression(node.expression) ||
236
- node.expression.name.text !== "routes"
237
- )
238
- return false;
239
- let inner: ts.Expression = node.expression.expression;
240
- while (
241
- ts.isCallExpression(inner) &&
242
- ts.isPropertyAccessExpression(inner.expression)
243
- ) {
244
- inner = inner.expression.expression;
245
- }
246
- return isCreateRouterCall(inner);
247
- }
246
+ let result: UrlsExtractionResult[] | null = null;
248
247
 
249
248
  function visit(node: ts.Node) {
250
249
  if (result) return;
251
-
252
- // Pattern 1 & 3: createRouter(...).routes(variableName | builder)
253
- if (
254
- ts.isCallExpression(node) &&
255
- node.arguments.length >= 1 &&
256
- isRoutesOnCreateRouter(node)
257
- ) {
258
- const arg = node.arguments[0];
259
- if (ts.isIdentifier(arg)) {
260
- result = { kind: "variable", name: arg.text };
261
- } else if (isInlineBuilder(arg)) {
262
- result = { kind: "inline", block: arg.getText(sourceFile) };
263
- }
264
- return;
265
- }
266
-
267
- // Pattern 2 & 4: createRouter({ urls: variableName | builder, ... })
268
- if (isCreateRouterCall(node)) {
269
- const callExpr = node as ts.CallExpression;
270
- for (const callArg of callExpr.arguments) {
271
- if (ts.isObjectLiteralExpression(callArg)) {
250
+ if (ts.isCallExpression(node)) {
251
+ const chain = collectCreateRouterCallChain(node);
252
+ if (chain) {
253
+ const extracted: UrlsExtractionResult[] = [];
254
+ const createRouterCall = chain[0];
255
+
256
+ for (const callArg of createRouterCall.arguments) {
257
+ if (!ts.isObjectLiteralExpression(callArg)) continue;
272
258
  for (const prop of callArg.properties) {
273
259
  if (
274
- ts.isPropertyAssignment(prop) &&
275
- ts.isIdentifier(prop.name) &&
276
- prop.name.text === "urls"
260
+ !ts.isPropertyAssignment(prop) ||
261
+ !ts.isIdentifier(prop.name) ||
262
+ prop.name.text !== "urls"
277
263
  ) {
278
- if (ts.isIdentifier(prop.initializer)) {
279
- result = { kind: "variable", name: prop.initializer.text };
280
- } else if (isInlineBuilder(prop.initializer)) {
281
- result = {
282
- kind: "inline",
283
- block: prop.initializer.getText(sourceFile),
284
- };
285
- }
286
- return;
264
+ continue;
287
265
  }
266
+ const urls = extractUrlsArgument(prop.initializer, sourceFile);
267
+ if (urls) extracted.push(urls);
268
+ break;
288
269
  }
289
270
  }
271
+
272
+ for (const call of chain.slice(1)) {
273
+ if (
274
+ !ts.isPropertyAccessExpression(call.expression) ||
275
+ call.expression.name.text !== "routes" ||
276
+ call.arguments.length === 0
277
+ ) {
278
+ continue;
279
+ }
280
+ const urls = extractUrlsArgument(call.arguments[0], sourceFile);
281
+ if (urls) extracted.push(urls);
282
+ }
283
+
284
+ if (extracted.length > 0) {
285
+ result = extracted;
286
+ return;
287
+ }
290
288
  }
291
289
  }
292
-
293
290
  ts.forEachChild(node, visit);
294
291
  }
295
292
 
296
293
  visit(sourceFile);
297
- return result;
294
+ return result ?? [];
295
+ }
296
+
297
+ /**
298
+ * Extract the url patterns from a router file using AST.
299
+ * Detects four patterns:
300
+ * 1. createRouter(...).routes(variableName)
301
+ * 2. createRouter({ urls: variableName, ... })
302
+ * 3. createRouter(...).routes(({ path, ... }) => [...])
303
+ * 4. createRouter({ urls: ({ path, ... }) => [...], ... })
304
+ * Returns either a variable name or an inline code block.
305
+ */
306
+ export function extractUrlsFromRouter(
307
+ code: string,
308
+ ): UrlsExtractionResult | null {
309
+ const extractions = extractAllUrlsFromRouter(code);
310
+ return extractions[extractions.length - 1] ?? null;
298
311
  }
299
312
 
300
313
  /**
@@ -365,6 +378,77 @@ function applyBasenameToRoutes(
365
378
  return { routes: prefixed, searchSchemas: result.searchSchemas };
366
379
  }
367
380
 
381
+ type RouteMapWithSearch = {
382
+ routes: Record<string, string>;
383
+ searchSchemas: Record<string, Record<string, string>>;
384
+ };
385
+
386
+ function resolveRouterUrlsExtraction(
387
+ routerFilePath: string,
388
+ routerSource: string,
389
+ extraction: UrlsExtractionResult,
390
+ diagnosticsOut: UnresolvableInclude[] | undefined,
391
+ memo: ScanMemo,
392
+ ): RouteMapWithSearch {
393
+ if (extraction.kind === "inline") {
394
+ return buildCombinedRouteMapWithSearch(
395
+ routerFilePath,
396
+ undefined,
397
+ new Set(),
398
+ diagnosticsOut,
399
+ extraction.block,
400
+ memo,
401
+ );
402
+ }
403
+
404
+ const imported = resolveImportedVariable(routerSource, extraction.name);
405
+ if (imported) {
406
+ const targetFile = resolveImportPath(imported.specifier, routerFilePath);
407
+ if (!targetFile) {
408
+ diagnosticsOut?.push({
409
+ pathPrefix: "/",
410
+ namePrefix: null,
411
+ reason: "file-not-found",
412
+ sourceFile: routerFilePath,
413
+ detail: `import "${imported.specifier}" resolved to no file`,
414
+ });
415
+ return { routes: {}, searchSchemas: {} };
416
+ }
417
+ return buildCombinedRouteMapWithSearch(
418
+ targetFile,
419
+ imported.exportedName,
420
+ new Set(),
421
+ diagnosticsOut,
422
+ undefined,
423
+ memo,
424
+ );
425
+ }
426
+
427
+ return buildCombinedRouteMapWithSearch(
428
+ routerFilePath,
429
+ extraction.name,
430
+ new Set(),
431
+ diagnosticsOut,
432
+ undefined,
433
+ memo,
434
+ );
435
+ }
436
+
437
+ function mergeRouteMaps(
438
+ target: RouteMapWithSearch,
439
+ source: RouteMapWithSearch,
440
+ ): void {
441
+ for (const [name, pattern] of Object.entries(source.routes)) {
442
+ target.routes[name] = pattern;
443
+ const search = source.searchSchemas[name];
444
+ if (search) {
445
+ target.searchSchemas[name] = search;
446
+ } else {
447
+ delete target.searchSchemas[name];
448
+ }
449
+ }
450
+ }
451
+
368
452
  // Filesystem path of the generated route-types file for a router source file.
369
453
  // Native separators — matches the self-gen-tracking Map key the watcher compares.
370
454
  export function genFileTsPath(sourceFile: string): string {
@@ -396,9 +480,8 @@ export function resolveSearchSchemas(
396
480
  }
397
481
 
398
482
  /**
399
- * Resolve routes and search schemas from a router source file by following the
400
- * variable passed to `.routes(...)` or `urls: ...` in createRouter options,
401
- * or by parsing an inline builder function directly.
483
+ * Resolve routes and search schemas from every direct mount in a createRouter
484
+ * chain. Registrations are merged in runtime order, with later mounts winning.
402
485
  */
403
486
  export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
404
487
  routes: Record<string, string>;
@@ -411,8 +494,8 @@ export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
411
494
  return { routes: {}, searchSchemas: {} };
412
495
  }
413
496
 
414
- const extraction = extractUrlsFromRouter(routerSource);
415
- if (!extraction) {
497
+ const extractions = extractAllUrlsFromRouter(routerSource);
498
+ if (extractions.length === 0) {
416
499
  return { routes: {}, searchSchemas: {} };
417
500
  }
418
501
 
@@ -422,40 +505,24 @@ export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
422
505
  ? ("/" + rawBasename.replace(/^\/+|\/+$/g, "")).replace(/^\/$/, "")
423
506
  : undefined;
424
507
 
425
- let result: {
426
- routes: Record<string, string>;
427
- searchSchemas: Record<string, Record<string, string>>;
428
- };
429
-
430
- // Inline builder: extract routes directly from the function body
431
- if (extraction.kind === "inline") {
432
- result = buildCombinedRouteMapWithSearch(
433
- routerFilePath,
434
- undefined,
435
- undefined,
436
- undefined,
437
- extraction.block,
508
+ const result: RouteMapWithSearch = { routes: {}, searchSchemas: {} };
509
+ const memo = createScanMemo();
510
+ for (const extraction of extractions) {
511
+ mergeRouteMaps(
512
+ result,
513
+ resolveRouterUrlsExtraction(
514
+ routerFilePath,
515
+ routerSource,
516
+ extraction,
517
+ undefined,
518
+ memo,
519
+ ),
438
520
  );
439
- } else {
440
- // Variable reference: follow imports or same-file declaration
441
- const imported = resolveImportedVariable(routerSource, extraction.name);
442
- if (imported) {
443
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
444
- if (!targetFile) {
445
- return { routes: {}, searchSchemas: {} };
446
- }
447
- result = buildCombinedRouteMapWithSearch(
448
- targetFile,
449
- imported.exportedName,
450
- );
451
- } else {
452
- result = buildCombinedRouteMapWithSearch(routerFilePath, extraction.name);
453
- }
454
521
  }
455
522
 
456
523
  // Apply basename prefix to all extracted route patterns
457
524
  if (basename) {
458
- result = applyBasenameToRoutes(result, basename);
525
+ return applyBasenameToRoutes(result, basename);
459
526
  }
460
527
 
461
528
  return result;
@@ -481,56 +548,20 @@ export function detectUnresolvableIncludes(
481
548
  return [];
482
549
  }
483
550
 
484
- // Extract the urls source from the router file
485
- const extraction = extractUrlsFromRouter(source);
486
- if (!extraction) return [];
551
+ const extractions = extractAllUrlsFromRouter(source);
552
+ if (extractions.length === 0) return [];
487
553
 
488
554
  const diagnostics: UnresolvableInclude[] = [];
489
-
490
- if (extraction.kind === "inline") {
491
- // Inline builder: parse directly
492
- buildCombinedRouteMapWithSearch(
555
+ const memo = createScanMemo();
556
+ for (const extraction of extractions) {
557
+ resolveRouterUrlsExtraction(
493
558
  realPath,
494
- undefined,
495
- new Set(),
559
+ source,
560
+ extraction,
496
561
  diagnostics,
497
- extraction.block,
562
+ memo,
498
563
  );
499
- return diagnostics;
500
- }
501
-
502
- // Variable reference: resolve where it comes from
503
- const imported = resolveImportedVariable(source, extraction.name);
504
- let targetFile: string;
505
- let exportedName: string | undefined;
506
-
507
- if (imported) {
508
- const resolved = resolveImportPath(imported.specifier, realPath);
509
- if (!resolved) {
510
- return [
511
- {
512
- pathPrefix: "/",
513
- namePrefix: null,
514
- reason: "file-not-found",
515
- sourceFile: realPath,
516
- detail: `import "${imported.specifier}" resolved to no file`,
517
- },
518
- ];
519
- }
520
- targetFile = resolved;
521
- exportedName = imported.exportedName;
522
- } else {
523
- // Same-file urls() definition
524
- targetFile = realPath;
525
- exportedName = extraction.name;
526
564
  }
527
-
528
- buildCombinedRouteMapWithSearch(
529
- targetFile,
530
- exportedName,
531
- new Set(),
532
- diagnostics,
533
- );
534
565
  return diagnostics;
535
566
  }
536
567
 
@@ -5,6 +5,7 @@ import {
5
5
  genFileTsPath,
6
6
  resolveSearchSchemas,
7
7
  } from "./generate-route-types.ts";
8
+ import { mergeFullManifests } from "./merge-full-manifests.js";
8
9
  import { isAutoGeneratedRouteName } from "../route-name.js";
9
10
 
10
11
  export interface RuntimeDiscoveryOptions {
@@ -126,24 +127,34 @@ export async function discoverAndWriteRouteTypes(
126
127
 
127
128
  // Import build utilities for manifest generation
128
129
  const buildMod = await rscEnv.runner.import("@rangojs/router/build");
129
- const generateManifest = buildMod.generateManifest;
130
+ const generateManifestFull = buildMod.generateManifestFull;
130
131
 
131
- if (!generateManifest) {
132
- throw new Error("generateManifest not found in @rangojs/router/build");
132
+ if (!generateManifestFull) {
133
+ throw new Error(
134
+ "generateManifestFull not found in @rangojs/router/build",
135
+ );
133
136
  }
134
137
 
135
138
  const outputFiles: string[] = [];
136
139
  let totalRouteCount = 0;
137
- let routerMountIndex = 0;
140
+ let globalMountIndex = 0;
141
+ let routerCount = 0;
138
142
 
139
143
  for (const [id, router] of registry) {
140
- if (!router.urlpatterns) continue;
141
-
142
- const manifest = await generateManifest(
143
- router.urlpatterns,
144
- routerMountIndex,
145
- );
146
- routerMountIndex++;
144
+ const mounts =
145
+ router.__urlpatternMounts ??
146
+ (router.urlpatterns ? [{ patterns: router.urlpatterns }] : []);
147
+ if (mounts.length === 0) continue;
148
+
149
+ const mountManifests = [];
150
+ for (const mount of mounts) {
151
+ mountManifests.push(
152
+ await generateManifestFull(mount.patterns, globalMountIndex),
153
+ );
154
+ globalMountIndex++;
155
+ }
156
+ const manifest = mergeFullManifests(mountManifests);
157
+ routerCount++;
147
158
 
148
159
  // Filter out auto-generated route names that the runtime creates for
149
160
  // unnamed routes (path() with no name option). These get names like
@@ -211,7 +222,7 @@ export async function discoverAndWriteRouteTypes(
211
222
  }
212
223
 
213
224
  return {
214
- routerCount: routerMountIndex,
225
+ routerCount,
215
226
  routeCount: totalRouteCount,
216
227
  outputFiles,
217
228
  };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Execution-chain scope for "use cache" function bodies.
3
+ *
4
+ * The ambient guards (cookies(), headers(), ctx.set() reached via
5
+ * getRequestContext()) must fire for code running INSIDE a cached body and
6
+ * stay silent for everything else on the request. A property stamped on the
7
+ * shared RequestContext cannot express that: while a slow "use cache" body is
8
+ * in flight, every parallel read on the same request sees the stamp. Scar: a
9
+ * 2s cached product fetch running beside a sibling loader made the loader's
10
+ * cookies() read throw `cannot be called inside a "use cache" function` —
11
+ * same shared-object hazard as issue #684 plan 010, which fixed only the
12
+ * background-revalidation path. AsyncLocalStorage follows the cached body's
13
+ * own async chain, so parallel work is invisible to the guard while genuine
14
+ * in-body reads still throw.
15
+ *
16
+ * Deliberately NOT entered on the store-less bypass paths in cache-runtime.ts:
17
+ * there the body executes for real on every call, side effects take effect,
18
+ * and request-scoped reads are safe (see the bypass comments there).
19
+ *
20
+ * Kept separate from taint.ts so `node:async_hooks` stays out of that module —
21
+ * taint.ts is reachable from browser-bundled DSL helpers. The probe
22
+ * registration below wires assertNotInsideCacheExec to this scope without
23
+ * taint.ts importing it.
24
+ */
25
+
26
+ import { AsyncLocalStorage } from "node:async_hooks";
27
+ import { _setCacheExecScopeProbe } from "./taint.js";
28
+
29
+ const cacheExecStorage = new AsyncLocalStorage<true>();
30
+
31
+ /**
32
+ * Run fn with the "use cache" execution scope active. Continuations spawned
33
+ * from fn's synchronous kickoff inherit the scope; parallel chains do not.
34
+ */
35
+ export function runWithCacheExecScope<T>(fn: () => T): T {
36
+ return cacheExecStorage.run(true, fn);
37
+ }
38
+
39
+ /** True when the calling async chain is inside a "use cache" body. */
40
+ export function isInsideCacheExecScope(): boolean {
41
+ return cacheExecStorage.getStore() === true;
42
+ }
43
+
44
+ // Wire the ambient ctx-method guards (assertNotInsideCacheExec in taint.ts)
45
+ // to this scope. Module-init registration cannot be missed: exec scopes only
46
+ // exist once cache-runtime.ts (which imports this module) is loaded.
47
+ _setCacheExecScopeProbe(isInsideCacheExecScope);