@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2

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 (222) hide show
  1. package/AGENTS.md +6 -10
  2. package/README.md +289 -938
  3. package/dist/bin/rango.js +271 -46
  4. package/dist/vite/index.js +673 -193
  5. package/package.json +10 -8
  6. package/skills/api-client/SKILL.md +1 -1
  7. package/skills/breadcrumbs/SKILL.md +31 -14
  8. package/skills/cache-guide/SKILL.md +5 -2
  9. package/skills/caching/SKILL.md +59 -4
  10. package/skills/catalog.json +271 -0
  11. package/skills/comparison/SKILL.md +50 -0
  12. package/skills/comparison/agents/openai.yaml +4 -0
  13. package/skills/comparison/references/framework-comparison.md +837 -0
  14. package/skills/composability/SKILL.md +83 -2
  15. package/skills/debug-manifest/SKILL.md +1 -1
  16. package/skills/defer-hydration/SKILL.md +235 -0
  17. package/skills/document-cache/SKILL.md +9 -1
  18. package/skills/fonts/SKILL.md +1 -1
  19. package/skills/handler-use/SKILL.md +8 -8
  20. package/skills/hooks/SKILL.md +54 -892
  21. package/skills/hooks/data.md +273 -0
  22. package/skills/hooks/handle-and-actions.md +103 -0
  23. package/skills/hooks/navigation.md +110 -0
  24. package/skills/hooks/outlets.md +41 -0
  25. package/skills/hooks/state.md +228 -0
  26. package/skills/hooks/urls.md +135 -0
  27. package/skills/host-router/SKILL.md +4 -4
  28. package/skills/i18n/SKILL.md +1 -1
  29. package/skills/intercept/SKILL.md +46 -14
  30. package/skills/layout/SKILL.md +27 -10
  31. package/skills/links/SKILL.md +1 -1
  32. package/skills/loader/SKILL.md +23 -1
  33. package/skills/middleware/SKILL.md +7 -3
  34. package/skills/migrate-nextjs/SKILL.md +167 -6
  35. package/skills/migrate-react-router/SKILL.md +59 -677
  36. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  37. package/skills/migrate-react-router/component-migration.md +196 -0
  38. package/skills/migrate-react-router/data-and-actions.md +225 -0
  39. package/skills/migrate-react-router/route-mapping.md +271 -0
  40. package/skills/mime-routes/SKILL.md +1 -1
  41. package/skills/observability/SKILL.md +9 -1
  42. package/skills/parallel/SKILL.md +23 -4
  43. package/skills/ppr/SKILL.md +622 -0
  44. package/skills/prerender/SKILL.md +28 -18
  45. package/skills/rango/SKILL.md +84 -25
  46. package/skills/response-routes/SKILL.md +15 -1
  47. package/skills/route/SKILL.md +71 -4
  48. package/skills/router-setup/SKILL.md +14 -3
  49. package/skills/scripts/SKILL.md +1 -1
  50. package/skills/server-actions/SKILL.md +3 -2
  51. package/skills/shell-manifest/SKILL.md +185 -0
  52. package/skills/streams-and-websockets/SKILL.md +1 -1
  53. package/skills/tailwind/SKILL.md +1 -1
  54. package/skills/testing/SKILL.md +2 -1
  55. package/skills/testing/handles.md +4 -2
  56. package/skills/testing/render-handler.md +15 -14
  57. package/skills/testing/reverse-and-types.md +8 -7
  58. package/skills/theme/SKILL.md +1 -1
  59. package/skills/typesafety/SKILL.md +45 -919
  60. package/skills/typesafety/env-and-bindings.md +254 -0
  61. package/skills/typesafety/generated-files-and-cli.md +335 -0
  62. package/skills/typesafety/params-and-search.md +153 -0
  63. package/skills/typesafety/route-types.md +209 -0
  64. package/skills/use-cache/SKILL.md +30 -3
  65. package/skills/vercel/SKILL.md +1 -1
  66. package/skills/view-transitions/SKILL.md +44 -1
  67. package/src/browser/event-controller.ts +62 -10
  68. package/src/browser/logging.ts +28 -0
  69. package/src/browser/merge-segment-loaders.ts +6 -4
  70. package/src/browser/navigation-bridge.ts +65 -16
  71. package/src/browser/navigation-client.ts +32 -2
  72. package/src/browser/navigation-store.ts +128 -14
  73. package/src/browser/network-error-handler.ts +34 -7
  74. package/src/browser/partial-update.ts +76 -17
  75. package/src/browser/prefetch/cache.ts +51 -11
  76. package/src/browser/prefetch/fetch.ts +59 -21
  77. package/src/browser/prefetch/queue.ts +19 -4
  78. package/src/browser/react/Link.tsx +13 -3
  79. package/src/browser/react/NavigationProvider.tsx +108 -4
  80. package/src/browser/response-adapter.ts +38 -9
  81. package/src/browser/rsc-router.tsx +54 -4
  82. package/src/browser/scroll-restoration.ts +7 -5
  83. package/src/browser/segment-reconciler.ts +31 -21
  84. package/src/browser/server-action-bridge.ts +22 -10
  85. package/src/browser/types.ts +54 -1
  86. package/src/build/generate-manifest.ts +155 -131
  87. package/src/build/index.ts +3 -1
  88. package/src/build/route-trie.ts +35 -7
  89. package/src/build/route-types/include-resolution.ts +347 -47
  90. package/src/build/runtime-discovery.ts +4 -1
  91. package/src/cache/cache-key-utils.ts +29 -0
  92. package/src/cache/cache-runtime.ts +262 -71
  93. package/src/cache/cache-scope.ts +2 -17
  94. package/src/cache/cache-tag.ts +60 -14
  95. package/src/cache/cf/cf-cache-store.ts +243 -20
  96. package/src/cache/document-cache.ts +54 -21
  97. package/src/cache/index.ts +1 -0
  98. package/src/cache/memory-segment-store.ts +110 -3
  99. package/src/cache/profile-registry.ts +15 -0
  100. package/src/cache/read-through-swr.ts +15 -1
  101. package/src/cache/segment-codec.ts +4 -4
  102. package/src/cache/shell-snapshot.ts +417 -0
  103. package/src/cache/types.ts +158 -0
  104. package/src/cache/vercel/vercel-cache-store.ts +401 -124
  105. package/src/client.rsc.tsx +0 -3
  106. package/src/client.tsx +0 -3
  107. package/src/cloudflare/tracing.ts +7 -8
  108. package/src/defer.ts +11 -22
  109. package/src/handle.ts +37 -15
  110. package/src/handles/MetaTags.tsx +16 -82
  111. package/src/handles/breadcrumbs.ts +12 -14
  112. package/src/handles/deferred-resolution.ts +127 -0
  113. package/src/handles/is-thenable.ts +7 -8
  114. package/src/handles/meta.ts +7 -44
  115. package/src/host/errors.ts +15 -0
  116. package/src/host/index.ts +1 -0
  117. package/src/index.rsc.ts +8 -2
  118. package/src/index.ts +19 -13
  119. package/src/internal-debug.ts +11 -8
  120. package/src/prerender.ts +17 -4
  121. package/src/redirect-origin.ts +14 -0
  122. package/src/render-error-thrower.tsx +20 -0
  123. package/src/route-content-wrapper.tsx +12 -5
  124. package/src/route-definition/dsl-helpers.ts +21 -32
  125. package/src/route-definition/helper-factories.ts +0 -2
  126. package/src/route-definition/helpers-types.ts +43 -43
  127. package/src/route-definition/index.ts +1 -2
  128. package/src/route-definition/resolve-handler-use.ts +0 -1
  129. package/src/route-definition/use-item-types.ts +3 -6
  130. package/src/route-map-builder.ts +41 -4
  131. package/src/route-types.ts +0 -5
  132. package/src/router/find-match.ts +86 -8
  133. package/src/router/instrument.ts +9 -4
  134. package/src/router/lazy-includes.ts +72 -12
  135. package/src/router/loader-resolution.ts +14 -2
  136. package/src/router/manifest.ts +56 -11
  137. package/src/router/match-api.ts +76 -32
  138. package/src/router/match-handlers.ts +181 -135
  139. package/src/router/match-middleware/background-revalidation.ts +40 -23
  140. package/src/router/match-middleware/cache-store.ts +39 -24
  141. package/src/router/match-result.ts +35 -15
  142. package/src/router/middleware.ts +64 -38
  143. package/src/router/navigation-snapshot.ts +7 -5
  144. package/src/router/parse-pattern.ts +115 -0
  145. package/src/router/pattern-matching.ts +53 -64
  146. package/src/router/prefetch-limits.ts +37 -0
  147. package/src/router/prerender-match.ts +11 -5
  148. package/src/router/preview-match.ts +3 -1
  149. package/src/router/request-classification.ts +23 -8
  150. package/src/router/route-snapshot.ts +14 -2
  151. package/src/router/router-context.ts +3 -1
  152. package/src/router/router-interfaces.ts +32 -1
  153. package/src/router/router-options.ts +30 -0
  154. package/src/router/segment-resolution/fresh.ts +39 -3
  155. package/src/router/segment-resolution/loader-cache.ts +93 -2
  156. package/src/router/segment-resolution/loader-mask.ts +60 -0
  157. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  158. package/src/router/segment-resolution/mask-nested.ts +83 -0
  159. package/src/router/segment-resolution/revalidation.ts +3 -0
  160. package/src/router/segment-resolution/view-transition-default.ts +35 -15
  161. package/src/router/substitute-pattern-params.ts +54 -35
  162. package/src/router/telemetry-otel.ts +6 -8
  163. package/src/router/telemetry.ts +9 -1
  164. package/src/router/tracing.ts +14 -5
  165. package/src/router/trie-matching.ts +19 -11
  166. package/src/router/url-params.ts +13 -0
  167. package/src/router.ts +47 -16
  168. package/src/rsc/full-payload.ts +70 -0
  169. package/src/rsc/handler.ts +60 -33
  170. package/src/rsc/manifest-init.ts +1 -1
  171. package/src/rsc/nonce.ts +10 -1
  172. package/src/rsc/progressive-enhancement.ts +61 -4
  173. package/src/rsc/redirect-guard.ts +2 -1
  174. package/src/rsc/rsc-rendering.ts +429 -37
  175. package/src/rsc/server-action.ts +25 -2
  176. package/src/rsc/shell-capture.ts +1190 -0
  177. package/src/rsc/shell-serve.ts +181 -0
  178. package/src/rsc/transition-gate.ts +89 -0
  179. package/src/rsc/types.ts +30 -0
  180. package/src/segment-loader-promise.ts +18 -0
  181. package/src/segment-system.tsx +149 -14
  182. package/src/server/context.ts +67 -9
  183. package/src/server/cookie-store.ts +73 -1
  184. package/src/server/loader-registry.ts +13 -1
  185. package/src/server/request-context.ts +169 -10
  186. package/src/ssr/index.tsx +462 -178
  187. package/src/ssr/inject-rsc-eager.ts +167 -0
  188. package/src/ssr/ssr-root.tsx +228 -0
  189. package/src/testing/collect-handle.ts +14 -8
  190. package/src/testing/dispatch.ts +152 -40
  191. package/src/testing/generated-routes.ts +27 -11
  192. package/src/testing/index.ts +6 -0
  193. package/src/testing/render-handler.ts +14 -0
  194. package/src/testing/render-route.tsx +13 -10
  195. package/src/testing/run-transition-when.ts +164 -0
  196. package/src/theme/ThemeProvider.tsx +36 -26
  197. package/src/types/handler-context.ts +1 -1
  198. package/src/types/index.ts +2 -0
  199. package/src/types/route-config.ts +19 -7
  200. package/src/types/segments.ts +100 -0
  201. package/src/urls/include-helper.ts +10 -8
  202. package/src/urls/include-provider.ts +71 -0
  203. package/src/urls/index.ts +1 -0
  204. package/src/urls/path-helper-types.ts +44 -12
  205. package/src/urls/path-helper.ts +5 -0
  206. package/src/urls/pattern-types.ts +36 -0
  207. package/src/urls/type-extraction.ts +43 -18
  208. package/src/urls/urls-function.ts +0 -1
  209. package/src/vercel/tracing.ts +7 -7
  210. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  211. package/src/vite/discovery/discover-routers.ts +1 -1
  212. package/src/vite/discovery/discovery-errors.ts +61 -0
  213. package/src/vite/index.ts +7 -0
  214. package/src/vite/inject-client-debug.ts +88 -0
  215. package/src/vite/plugins/vercel-output.ts +114 -25
  216. package/src/vite/plugins/version-injector.ts +22 -7
  217. package/src/vite/plugins/virtual-entries.ts +80 -22
  218. package/src/vite/rango.ts +29 -19
  219. package/src/vite/router-discovery.ts +171 -43
  220. package/src/vite/utils/prerender-utils.ts +17 -4
  221. package/src/vite/utils/shared-utils.ts +47 -0
  222. package/src/network-error-thrower.tsx +0 -18
@@ -97,6 +97,147 @@ function extractNamePrefixFromInclude(node: ts.CallExpression): string | null {
97
97
  return null;
98
98
  }
99
99
 
100
+ /**
101
+ * True when the thunk transforms its dynamic import via a `.then(cb)` whose
102
+ * callback selects a NAMED export other than `default` (e.g.
103
+ * `import("./x").then((m) => m.routes)`). The static resolver walks the module's
104
+ * `export default`, so such a selector would resolve the WRONG export — the
105
+ * caller must treat it as unresolvable instead of silently mis-resolving. Returns
106
+ * false for the supported shapes (no `.then`, `.then((m) => m)`,
107
+ * `.then((m) => m.default)`) and for any shape it cannot positively identify as a
108
+ * non-default member selection (those keep the existing resolve-via-default path).
109
+ */
110
+ function thenSelectsNonDefaultMember(expr: ts.Expression): boolean {
111
+ const findThenCall = (e: ts.Expression): ts.CallExpression | null => {
112
+ if (
113
+ ts.isCallExpression(e) &&
114
+ ts.isPropertyAccessExpression(e.expression) &&
115
+ e.expression.name.text === "then"
116
+ ) {
117
+ return e;
118
+ }
119
+ if (ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression)) {
120
+ return findThenCall(e.expression.expression);
121
+ }
122
+ if (ts.isPropertyAccessExpression(e)) return findThenCall(e.expression);
123
+ if (ts.isParenthesizedExpression(e)) return findThenCall(e.expression);
124
+ if (ts.isAwaitExpression(e)) return findThenCall(e.expression);
125
+ return null;
126
+ };
127
+
128
+ const thenCall = findThenCall(expr);
129
+ if (!thenCall || thenCall.arguments.length === 0) return false;
130
+ const cb = thenCall.arguments[0];
131
+ if (!ts.isArrowFunction(cb) && !ts.isFunctionExpression(cb)) return false;
132
+
133
+ let ret: ts.Expression | undefined;
134
+ if (ts.isBlock(cb.body)) {
135
+ for (const stmt of cb.body.statements) {
136
+ if (ts.isReturnStatement(stmt) && stmt.expression) {
137
+ ret = stmt.expression;
138
+ break;
139
+ }
140
+ }
141
+ } else {
142
+ ret = cb.body;
143
+ }
144
+ if (!ret) return false;
145
+
146
+ if (ts.isPropertyAccessExpression(ret)) {
147
+ return ret.name.text !== "default";
148
+ }
149
+ return false;
150
+ }
151
+
152
+ /**
153
+ * Extract the module specifier from an async include thunk
154
+ * (`() => import("./mod")`). Handles arrow and function-expression thunks,
155
+ * concise or block bodies, and `import("./mod").then(...)` chains. Returns the
156
+ * specifier string, or null when the second arg is not a dynamic-import thunk
157
+ * (so the caller can fall through to identifier / factory-call / dynamic-expr
158
+ * classification).
159
+ */
160
+ function extractDynamicImportSpecifier(node: ts.Expression): string | null {
161
+ let body: ts.ConciseBody | undefined;
162
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
163
+ body = node.body;
164
+ }
165
+ if (!body) return null;
166
+
167
+ // Concise body is the expression itself; block body: first `return <expr>`.
168
+ let expr: ts.Expression | undefined;
169
+ if (ts.isBlock(body)) {
170
+ for (const stmt of body.statements) {
171
+ if (ts.isReturnStatement(stmt) && stmt.expression) {
172
+ expr = stmt.expression;
173
+ break;
174
+ }
175
+ }
176
+ } else {
177
+ expr = body;
178
+ }
179
+ if (!expr) return null;
180
+
181
+ // Descend `import("./mod").then(...)` / parenthesized / await wrappers down to
182
+ // the `import(...)` call itself.
183
+ const findImportCall = (e: ts.Expression): ts.CallExpression | null => {
184
+ if (
185
+ ts.isCallExpression(e) &&
186
+ e.expression.kind === ts.SyntaxKind.ImportKeyword
187
+ ) {
188
+ return e;
189
+ }
190
+ if (ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression)) {
191
+ return findImportCall(e.expression.expression);
192
+ }
193
+ if (ts.isPropertyAccessExpression(e)) return findImportCall(e.expression);
194
+ if (ts.isParenthesizedExpression(e)) return findImportCall(e.expression);
195
+ if (ts.isAwaitExpression(e)) return findImportCall(e.expression);
196
+ return null;
197
+ };
198
+
199
+ const importCall = findImportCall(expr);
200
+ if (!importCall || importCall.arguments.length === 0) return null;
201
+ // A `.then()` selecting a non-default named export can't be statically
202
+ // resolved to `export default`; return null so the caller emits a diagnostic
203
+ // rather than silently generating types for the wrong export.
204
+ if (thenSelectsNonDefaultMember(expr)) return null;
205
+ return getStringValue(importCall.arguments[0]);
206
+ }
207
+
208
+ /**
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(...)`.
213
+ */
214
+ function resolveDefaultExportTarget(
215
+ code: string,
216
+ sourceFile: ts.SourceFile,
217
+ ): { variableName?: string; inlineBlock?: string } | null {
218
+ let result: { variableName?: string; inlineBlock?: string } | null = null;
219
+
220
+ function visit(node: ts.Node) {
221
+ if (result) return;
222
+ if (ts.isExportAssignment(node) && !node.isExportEquals) {
223
+ const expr = node.expression;
224
+ if (ts.isIdentifier(expr)) {
225
+ 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
+ }
231
+ }
232
+ return;
233
+ }
234
+ ts.forEachChild(node, visit);
235
+ }
236
+
237
+ visit(sourceFile);
238
+ return result;
239
+ }
240
+
100
241
  /**
101
242
  * Extract include() calls with diagnostics for unresolvable ones.
102
243
  * Returns both resolved includes (identifier second args) and unresolvable
@@ -108,7 +249,12 @@ export function extractIncludesWithDiagnostics(
108
249
  ): {
109
250
  resolved: Array<{
110
251
  pathPrefix: string;
111
- variableName: string;
252
+ // Exactly one of variableName / moduleSpecifier is set. variableName is the
253
+ // classic `include("/api", apiUrls)` identifier form; moduleSpecifier is the
254
+ // async `include("/api", () => import("./api"))` form, carrying the imported
255
+ // module path so resolution can walk its `export default`.
256
+ variableName?: string;
257
+ moduleSpecifier?: string;
112
258
  namePrefix: string | null;
113
259
  }>;
114
260
  unresolvable: Array<{
@@ -130,7 +276,8 @@ export function extractIncludesWithDiagnostics(
130
276
  );
131
277
  const resolved: Array<{
132
278
  pathPrefix: string;
133
- variableName: string;
279
+ variableName?: string;
280
+ moduleSpecifier?: string;
134
281
  namePrefix: string | null;
135
282
  }> = [];
136
283
  const unresolvable: Array<{
@@ -158,12 +305,22 @@ export function extractIncludesWithDiagnostics(
158
305
  const secondArg = node.arguments[1];
159
306
  const namePrefix = extractNamePrefixFromInclude(node);
160
307
 
308
+ const dynamicImportSpec = extractDynamicImportSpecifier(secondArg);
309
+
161
310
  if (ts.isIdentifier(secondArg)) {
162
311
  resolved.push({
163
312
  pathPrefix,
164
313
  variableName: secondArg.text,
165
314
  namePrefix,
166
315
  });
316
+ } else if (dynamicImportSpec !== null) {
317
+ // Async include: `include("/api", () => import("./api"))`. Resolvable
318
+ // statically by walking the imported module's `export default`.
319
+ resolved.push({
320
+ pathPrefix,
321
+ moduleSpecifier: dynamicImportSpec,
322
+ namePrefix,
323
+ });
167
324
  } else if (ts.isCallExpression(secondArg)) {
168
325
  const callText = secondArg.expression.getText(sourceFile);
169
326
  unresolvable.push({
@@ -348,65 +505,120 @@ function buildRouteMapFromBlock(
348
505
  }
349
506
  }
350
507
 
351
- for (const { pathPrefix, variableName, namePrefix } of includes) {
508
+ for (const inc of includes) {
509
+ const { pathPrefix, namePrefix } = inc;
352
510
  let childResult: {
353
511
  routes: Record<string, string>;
354
512
  searchSchemas: Record<string, Record<string, string>>;
355
513
  };
356
514
 
357
- // Try import resolution first
358
- const imported = resolveImportedVariable(fullSource, variableName);
359
- if (imported) {
360
- const targetFile = resolveImportPath(imported.specifier, filePath);
515
+ if (inc.moduleSpecifier) {
516
+ // Async include `() => import("./mod")`: resolve the imported module's
517
+ // `export default` (convention: `export default urls(...)`), then recurse
518
+ // into it exactly like an identifier include. Nested include()s inside the
519
+ // async module — eager or async — resolve through the same recursion.
520
+ const targetFile = resolveImportPath(inc.moduleSpecifier, filePath);
361
521
  if (!targetFile) {
362
- if (diagnosticsOut) {
363
- diagnosticsOut.push({
364
- pathPrefix,
365
- namePrefix,
366
- reason: "file-not-found",
367
- sourceFile: filePath,
368
- detail: `import "${imported.specifier}" resolved to no file`,
369
- });
370
- }
522
+ diagnosticsOut?.push({
523
+ pathPrefix,
524
+ namePrefix,
525
+ reason: "file-not-found",
526
+ sourceFile: filePath,
527
+ detail: `import("${inc.moduleSpecifier}") resolved to no file`,
528
+ });
371
529
  continue;
372
530
  }
373
- childResult = buildCombinedRouteMapWithSearch(
374
- targetFile,
375
- imported.exportedName,
376
- visited,
377
- diagnosticsOut,
378
- undefined,
379
- memo,
380
- );
381
- } else {
382
- // Check if variable exists as a same-file urls() definition. Share the
383
- // full-source SourceFile parse via the memo (block === fullSource hits the
384
- // same cache entry already used above).
385
- const sameFileBlock = extractUrlsBlockForVariable(
386
- fullSource,
387
- variableName,
388
- parseBlock(memo, fullSource),
531
+ let targetSource: string;
532
+ try {
533
+ targetSource = readSourceMemoized(memo, resolve(targetFile));
534
+ } catch (err) {
535
+ // Every other failure path in this function emits a diagnostic; a bare
536
+ // `continue` here would drop the group from generated types with no
537
+ // signal at all. Emit one like the siblings do.
538
+ diagnosticsOut?.push({
539
+ pathPrefix,
540
+ namePrefix,
541
+ reason: "file-not-found",
542
+ sourceFile: filePath,
543
+ detail: `import("${inc.moduleSpecifier}") resolved to "${targetFile}" but reading it failed: ${(err as Error)?.message ?? String(err)}`,
544
+ });
545
+ continue;
546
+ }
547
+ const def = resolveDefaultExportTarget(
548
+ targetSource,
549
+ parseBlock(memo, targetSource),
389
550
  );
390
- if (!sameFileBlock) {
391
- if (diagnosticsOut) {
392
- diagnosticsOut.push({
393
- pathPrefix,
394
- namePrefix,
395
- reason: "unresolvable-import",
396
- sourceFile: filePath,
397
- detail: `variable "${variableName}" not found in imports or same-file scope`,
398
- });
399
- }
551
+ if (!def) {
552
+ diagnosticsOut?.push({
553
+ pathPrefix,
554
+ namePrefix,
555
+ reason: "unresolvable-import",
556
+ sourceFile: filePath,
557
+ detail: `import("${inc.moduleSpecifier}") has no resolvable \`export default urls(...)\``,
558
+ });
400
559
  continue;
401
560
  }
402
- childResult = buildCombinedRouteMapWithSearch(
403
- filePath,
561
+ if (def.inlineBlock) {
562
+ // `export default urls(...)` — recurse into the inline call block.
563
+ childResult = buildCombinedRouteMapWithSearch(
564
+ targetFile,
565
+ undefined,
566
+ visited,
567
+ diagnosticsOut,
568
+ def.inlineBlock,
569
+ memo,
570
+ );
571
+ } else {
572
+ // `export default <name>`. That name may be a same-file
573
+ // `const name = urls(...)` OR itself re-exported from an import
574
+ // (`import { name } from "./x"; export default name`). The shared helper
575
+ // resolves the import chain first, then falls back to same-file
576
+ // extraction — without this a re-exported default silently yields empty
577
+ // routes. Resolution scope is the imported module (targetFile); the
578
+ // diagnostic is still attributed to the file with the async include.
579
+ const variableName = def.variableName!;
580
+ const resolved = resolveIncludedVariable({
581
+ variableName,
582
+ resolutionFile: targetFile,
583
+ resolutionSource: targetSource,
584
+ reportFile: filePath,
585
+ memo,
586
+ visited,
587
+ diagnosticsOut,
588
+ pathPrefix,
589
+ namePrefix,
590
+ fileNotFoundDetail: (specifier) =>
591
+ `import("${inc.moduleSpecifier}") default re-exports "${variableName}" from "${specifier}", which resolved to no file`,
592
+ notFoundDetail: () =>
593
+ `import("${inc.moduleSpecifier}") default "${variableName}" not found in its imports or same-file scope`,
594
+ });
595
+ if (!resolved) continue;
596
+ childResult = resolved;
597
+ }
598
+ } else if (inc.variableName) {
599
+ // Identifier include `include(prefix, patterns)`: resolve `patterns`
600
+ // (imported or same-file) through the same shared helper as the async
601
+ // `export default <name>` form above.
602
+ const variableName = inc.variableName;
603
+ const resolved = resolveIncludedVariable({
404
604
  variableName,
605
+ resolutionFile: filePath,
606
+ resolutionSource: fullSource,
607
+ reportFile: filePath,
608
+ memo,
405
609
  visited,
406
610
  diagnosticsOut,
407
- undefined,
408
- memo,
409
- );
611
+ pathPrefix,
612
+ namePrefix,
613
+ fileNotFoundDetail: (specifier) =>
614
+ `import "${specifier}" resolved to no file`,
615
+ notFoundDetail: () =>
616
+ `variable "${variableName}" not found in imports or same-file scope`,
617
+ });
618
+ if (!resolved) continue;
619
+ childResult = resolved;
620
+ } else {
621
+ continue;
410
622
  }
411
623
 
412
624
  // Includes without a name keep their child names private to the mounted
@@ -438,6 +650,94 @@ function buildRouteMapFromBlock(
438
650
  return routeMap;
439
651
  }
440
652
 
653
+ /**
654
+ * Resolve an include's target VARIABLE to its child route map. The variable may
655
+ * be imported (`import { name } from "./x"`) or defined in the same file
656
+ * (`const name = urls(...)`). Shared by the identifier include branch and the
657
+ * async-include `export default <name>` branch so a future import-resolution fix
658
+ * cannot silently miss one form. Returns null (with a diagnostic pushed) when
659
+ * the variable is unresolvable — the caller should skip the include.
660
+ *
661
+ * `resolutionFile`/`resolutionSource` is the module whose import + same-file
662
+ * scope is searched; `reportFile` is the file the diagnostic is attributed to
663
+ * (the one containing the include() call), which differs for async includes.
664
+ */
665
+ function resolveIncludedVariable(opts: {
666
+ variableName: string;
667
+ resolutionFile: string;
668
+ resolutionSource: string;
669
+ reportFile: string;
670
+ memo: ScanMemo | undefined;
671
+ visited: Set<string> | undefined;
672
+ diagnosticsOut: UnresolvableInclude[] | undefined;
673
+ pathPrefix: string;
674
+ namePrefix: string | null;
675
+ fileNotFoundDetail: (specifier: string) => string;
676
+ notFoundDetail: () => string;
677
+ }): {
678
+ routes: Record<string, string>;
679
+ searchSchemas: Record<string, Record<string, string>>;
680
+ } | null {
681
+ const {
682
+ variableName,
683
+ resolutionFile,
684
+ resolutionSource,
685
+ reportFile,
686
+ memo,
687
+ visited,
688
+ diagnosticsOut,
689
+ pathPrefix,
690
+ namePrefix,
691
+ } = opts;
692
+ const imported = resolveImportedVariable(resolutionSource, variableName);
693
+ if (imported) {
694
+ const targetFile = resolveImportPath(imported.specifier, resolutionFile);
695
+ if (!targetFile) {
696
+ diagnosticsOut?.push({
697
+ pathPrefix,
698
+ namePrefix,
699
+ reason: "file-not-found",
700
+ sourceFile: reportFile,
701
+ detail: opts.fileNotFoundDetail(imported.specifier),
702
+ });
703
+ return null;
704
+ }
705
+ return buildCombinedRouteMapWithSearch(
706
+ targetFile,
707
+ imported.exportedName,
708
+ visited,
709
+ diagnosticsOut,
710
+ undefined,
711
+ memo,
712
+ );
713
+ }
714
+ // Not imported: it must be a same-file `const name = urls(...)`. Confirm it
715
+ // exists so an undefined name emits a diagnostic instead of empty routes.
716
+ const sameFileBlock = extractUrlsBlockForVariable(
717
+ resolutionSource,
718
+ variableName,
719
+ parseBlock(memo, resolutionSource),
720
+ );
721
+ if (!sameFileBlock) {
722
+ diagnosticsOut?.push({
723
+ pathPrefix,
724
+ namePrefix,
725
+ reason: "unresolvable-import",
726
+ sourceFile: reportFile,
727
+ detail: opts.notFoundDetail(),
728
+ });
729
+ return null;
730
+ }
731
+ return buildCombinedRouteMapWithSearch(
732
+ resolutionFile,
733
+ variableName,
734
+ visited,
735
+ diagnosticsOut,
736
+ undefined,
737
+ memo,
738
+ );
739
+ }
740
+
441
741
  /**
442
742
  * Build route map and search schemas together.
443
743
  * Internal helper used by the include resolution path.
@@ -139,7 +139,10 @@ export async function discoverAndWriteRouteTypes(
139
139
  for (const [id, router] of registry) {
140
140
  if (!router.urlpatterns) continue;
141
141
 
142
- const manifest = generateManifest(router.urlpatterns, routerMountIndex);
142
+ const manifest = await generateManifest(
143
+ router.urlpatterns,
144
+ routerMountIndex,
145
+ );
143
146
  routerMountIndex++;
144
147
 
145
148
  // Filter out auto-generated route names that the runtime creates for
@@ -58,3 +58,32 @@ export function sortedRouteParams(
58
58
  if (!params) return "";
59
59
  return encodeKV(Object.entries(params), { sort: true });
60
60
  }
61
+
62
+ /**
63
+ * Host-namespaced cache key base: `${host}${pathname}[:params][?search]`.
64
+ *
65
+ * The ONE composition of the host-namespacing rule, shared by the segment tier
66
+ * (cache-scope.ts) and the document tier (document-cache.ts) so the rule cannot
67
+ * drift between them. Host prefixing matters because VercelCacheStore /
68
+ * MemorySegmentCacheStore key by the raw string (only CFCacheStore adds host
69
+ * internally) -- on a single function serving multiple domains an
70
+ * un-namespaced key bleeds tenant A's cached response to tenant B.
71
+ *
72
+ * Output is BYTE-STABLE by contract: changing the composition silently
73
+ * invalidates every persisted cache entry on upgrade. Callers append their own
74
+ * tier-specific suffixes (`:rsc`/`:html`, segment hash) after this base.
75
+ */
76
+ export function cacheKeyBase(
77
+ host: string,
78
+ pathname: string,
79
+ searchParams?: URLSearchParams,
80
+ params?: Record<string, string>,
81
+ ): string {
82
+ const paramStr = sortedRouteParams(params);
83
+ const searchStr = searchParams ? sortedSearchString(searchParams) : "";
84
+
85
+ let key = `${host}${pathname}`;
86
+ if (paramStr) key += `:${paramStr}`;
87
+ if (searchStr) key += `?${searchStr}`;
88
+ return key;
89
+ }