@rangojs/router 0.0.0-experimental.98 → 0.0.0-experimental.98914650

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 (355) hide show
  1. package/README.md +24 -9
  2. package/dist/bin/rango.js +157 -63
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +1584 -639
  5. package/package.json +60 -11
  6. package/skills/api-client/SKILL.md +211 -0
  7. package/skills/breadcrumbs/SKILL.md +60 -0
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +222 -30
  10. package/skills/caching/SKILL.md +263 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/css/SKILL.md +76 -0
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +3 -1
  15. package/skills/hooks/SKILL.md +235 -28
  16. package/skills/host-router/SKILL.md +122 -22
  17. package/skills/intercept/SKILL.md +29 -5
  18. package/skills/layout/SKILL.md +13 -9
  19. package/skills/links/SKILL.md +173 -17
  20. package/skills/loader/SKILL.md +170 -23
  21. package/skills/middleware/SKILL.md +16 -10
  22. package/skills/migrate-nextjs/SKILL.md +38 -16
  23. package/skills/mime-routes/SKILL.md +27 -0
  24. package/skills/observability/SKILL.md +137 -0
  25. package/skills/parallel/SKILL.md +11 -7
  26. package/skills/prerender/SKILL.md +14 -33
  27. package/skills/rango/SKILL.md +250 -26
  28. package/skills/react-compiler/SKILL.md +168 -0
  29. package/skills/response-routes/SKILL.md +114 -47
  30. package/skills/route/SKILL.md +22 -5
  31. package/skills/router-setup/SKILL.md +3 -3
  32. package/skills/server-actions/SKILL.md +78 -42
  33. package/skills/tailwind/SKILL.md +27 -3
  34. package/skills/testing/SKILL.md +129 -0
  35. package/skills/testing/bindings.md +89 -0
  36. package/skills/testing/cache-prerender.md +124 -0
  37. package/skills/testing/client-components.md +122 -0
  38. package/skills/testing/e2e-parity.md +125 -0
  39. package/skills/testing/flight.md +92 -0
  40. package/skills/testing/handles.md +129 -0
  41. package/skills/testing/loader.md +128 -0
  42. package/skills/testing/middleware.md +99 -0
  43. package/skills/testing/render-handler.md +121 -0
  44. package/skills/testing/response-routes.md +95 -0
  45. package/skills/testing/reverse-and-types.md +84 -0
  46. package/skills/testing/server-actions.md +107 -0
  47. package/skills/testing/server-tree.md +128 -0
  48. package/skills/testing/setup.md +120 -0
  49. package/skills/typesafety/SKILL.md +310 -26
  50. package/skills/use-cache/SKILL.md +36 -5
  51. package/skills/vercel/SKILL.md +107 -0
  52. package/skills/view-transitions/SKILL.md +294 -0
  53. package/src/__augment-tests__/augment.ts +81 -0
  54. package/src/__augment-tests__/augmented.check.ts +116 -0
  55. package/src/__internal.ts +0 -65
  56. package/src/browser/action-coordinator.ts +53 -36
  57. package/src/browser/action-fence.ts +47 -0
  58. package/src/browser/app-shell.ts +14 -27
  59. package/src/browser/cookie-name.ts +140 -0
  60. package/src/browser/event-controller.ts +37 -143
  61. package/src/browser/history-state.ts +21 -0
  62. package/src/browser/index.ts +3 -3
  63. package/src/browser/invalidate-client-cache.ts +52 -0
  64. package/src/browser/navigation-bridge.ts +30 -59
  65. package/src/browser/navigation-client.ts +96 -84
  66. package/src/browser/navigation-store-handle.ts +38 -0
  67. package/src/browser/navigation-store.ts +32 -82
  68. package/src/browser/navigation-transaction.ts +9 -59
  69. package/src/browser/partial-update.ts +60 -127
  70. package/src/browser/prefetch/cache.ts +82 -72
  71. package/src/browser/prefetch/fetch.ts +108 -33
  72. package/src/browser/prefetch/queue.ts +6 -3
  73. package/src/browser/rango-state.ts +157 -115
  74. package/src/browser/react/Link.tsx +0 -2
  75. package/src/browser/react/NavigationProvider.tsx +41 -48
  76. package/src/browser/react/ScrollRestoration.tsx +10 -6
  77. package/src/browser/react/filter-segment-order.ts +0 -2
  78. package/src/browser/react/index.ts +0 -48
  79. package/src/browser/react/location-state-shared.ts +166 -8
  80. package/src/browser/react/location-state.ts +39 -14
  81. package/src/browser/react/use-action.ts +6 -15
  82. package/src/browser/react/use-handle.ts +17 -14
  83. package/src/browser/react/use-link-status.ts +0 -4
  84. package/src/browser/react/use-navigation.ts +0 -3
  85. package/src/browser/react/use-params.ts +3 -6
  86. package/src/browser/react/use-reverse.ts +106 -0
  87. package/src/browser/react/use-router.ts +20 -5
  88. package/src/browser/react/use-search-params.ts +0 -5
  89. package/src/browser/react/use-segments.ts +0 -13
  90. package/src/browser/response-adapter.ts +52 -1
  91. package/src/browser/rsc-router.tsx +70 -34
  92. package/src/browser/scroll-restoration.ts +22 -14
  93. package/src/browser/segment-structure-assert.ts +2 -2
  94. package/src/browser/server-action-bridge.ts +168 -44
  95. package/src/browser/types.ts +36 -21
  96. package/src/browser/validate-redirect-origin.ts +43 -16
  97. package/src/build/collect-fallback-refs.ts +107 -0
  98. package/src/build/generate-manifest.ts +60 -35
  99. package/src/build/generate-route-types.ts +3 -0
  100. package/src/build/index.ts +8 -2
  101. package/src/build/prefix-tree-utils.ts +123 -0
  102. package/src/build/route-trie.ts +89 -11
  103. package/src/build/route-types/codegen.ts +4 -4
  104. package/src/build/route-types/include-resolution.ts +1 -1
  105. package/src/build/route-types/param-extraction.ts +6 -3
  106. package/src/build/route-types/per-module-writer.ts +7 -4
  107. package/src/build/route-types/router-processing.ts +122 -22
  108. package/src/build/route-types/scan-filter.ts +1 -1
  109. package/src/build/route-types/source-scan.ts +118 -0
  110. package/src/build/runtime-discovery.ts +9 -20
  111. package/src/cache/cache-error.ts +104 -0
  112. package/src/cache/cache-policy.ts +68 -28
  113. package/src/cache/cache-runtime.ts +134 -32
  114. package/src/cache/cache-scope.ts +100 -74
  115. package/src/cache/cache-tag.ts +98 -0
  116. package/src/cache/cf/cf-cache-store.ts +2255 -238
  117. package/src/cache/cf/index.ts +6 -16
  118. package/src/cache/document-cache.ts +61 -20
  119. package/src/cache/handle-snapshot.ts +63 -0
  120. package/src/cache/index.ts +22 -20
  121. package/src/cache/memory-segment-store.ts +136 -37
  122. package/src/cache/profile-registry.ts +6 -30
  123. package/src/cache/read-through-swr.ts +41 -11
  124. package/src/cache/segment-codec.ts +0 -16
  125. package/src/cache/tag-invalidation.ts +230 -0
  126. package/src/cache/types.ts +33 -100
  127. package/src/cache/vercel/index.ts +11 -0
  128. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  129. package/src/client.rsc.tsx +6 -21
  130. package/src/client.tsx +25 -61
  131. package/src/component-utils.ts +19 -0
  132. package/src/context-var.ts +17 -5
  133. package/src/decode-loader-results.ts +36 -0
  134. package/src/defer.ts +196 -0
  135. package/src/deps/ssr.ts +0 -1
  136. package/src/errors.ts +30 -4
  137. package/src/handle.ts +31 -23
  138. package/src/handles/MetaTags.tsx +0 -14
  139. package/src/handles/breadcrumbs.ts +16 -5
  140. package/src/handles/meta.ts +0 -39
  141. package/src/host/cookie-handler.ts +0 -36
  142. package/src/host/errors.ts +0 -24
  143. package/src/host/index.ts +8 -2
  144. package/src/host/pattern-matcher.ts +7 -50
  145. package/src/host/router.ts +107 -99
  146. package/src/host/testing.ts +40 -27
  147. package/src/host/types.ts +37 -4
  148. package/src/host/utils.ts +1 -1
  149. package/src/href-client.ts +137 -22
  150. package/src/index.rsc.ts +63 -9
  151. package/src/index.ts +64 -9
  152. package/src/internal-debug.ts +2 -4
  153. package/src/loader-store.ts +500 -0
  154. package/src/loader.rsc.ts +20 -13
  155. package/src/loader.ts +12 -11
  156. package/src/missing-id-error.ts +68 -0
  157. package/src/network-error-thrower.tsx +1 -6
  158. package/src/outlet-provider.tsx +1 -5
  159. package/src/prerender/param-hash.ts +10 -11
  160. package/src/prerender/store.ts +32 -37
  161. package/src/prerender.ts +61 -6
  162. package/src/redirect-origin.ts +100 -0
  163. package/src/response-utils.ts +9 -0
  164. package/src/reverse.ts +65 -41
  165. package/src/root-error-boundary.tsx +1 -19
  166. package/src/route-content-wrapper.tsx +7 -72
  167. package/src/route-definition/dsl-helpers.ts +244 -281
  168. package/src/route-definition/helper-factories.ts +29 -139
  169. package/src/route-definition/helpers-types.ts +40 -17
  170. package/src/route-definition/redirect.ts +43 -9
  171. package/src/route-definition/resolve-handler-use.ts +6 -0
  172. package/src/route-definition/use-item-types.ts +32 -0
  173. package/src/route-map-builder.ts +0 -16
  174. package/src/route-types.ts +19 -41
  175. package/src/router/basename.ts +14 -0
  176. package/src/router/content-negotiation.ts +15 -15
  177. package/src/router/error-handling.ts +13 -17
  178. package/src/router/find-match.ts +44 -23
  179. package/src/router/handler-context.ts +4 -42
  180. package/src/router/intercept-resolution.ts +14 -19
  181. package/src/router/lazy-includes.ts +9 -46
  182. package/src/router/loader-resolution.ts +91 -46
  183. package/src/router/logging.ts +0 -6
  184. package/src/router/manifest.ts +18 -29
  185. package/src/router/match-api.ts +0 -20
  186. package/src/router/match-context.ts +0 -22
  187. package/src/router/match-handlers.ts +57 -58
  188. package/src/router/match-middleware/background-revalidation.ts +0 -7
  189. package/src/router/match-middleware/cache-lookup.ts +150 -271
  190. package/src/router/match-middleware/cache-store.ts +3 -33
  191. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  192. package/src/router/match-middleware/segment-resolution.ts +0 -22
  193. package/src/router/match-pipelines.ts +1 -42
  194. package/src/router/match-result.ts +31 -80
  195. package/src/router/metrics.ts +0 -34
  196. package/src/router/middleware-types.ts +0 -116
  197. package/src/router/middleware.ts +118 -133
  198. package/src/router/navigation-snapshot.ts +0 -51
  199. package/src/router/params-util.ts +23 -0
  200. package/src/router/pattern-matching.ts +20 -58
  201. package/src/router/prerender-match.ts +99 -63
  202. package/src/router/preview-match.ts +3 -1
  203. package/src/router/request-classification.ts +28 -62
  204. package/src/router/revalidation.ts +50 -56
  205. package/src/router/route-snapshot.ts +0 -1
  206. package/src/router/router-context.ts +0 -27
  207. package/src/router/router-interfaces.ts +68 -35
  208. package/src/router/router-options.ts +55 -1
  209. package/src/router/router-registry.ts +2 -5
  210. package/src/router/segment-resolution/fresh.ts +44 -63
  211. package/src/router/segment-resolution/helpers.ts +34 -0
  212. package/src/router/segment-resolution/loader-cache.ts +40 -37
  213. package/src/router/segment-resolution/revalidation.ts +203 -285
  214. package/src/router/segment-resolution/static-store.ts +19 -5
  215. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  216. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  217. package/src/router/segment-resolution.ts +4 -1
  218. package/src/router/segment-wrappers.ts +0 -3
  219. package/src/router/state-cookie-name.ts +33 -0
  220. package/src/router/substitute-pattern-params.ts +56 -0
  221. package/src/router/telemetry-otel.ts +0 -20
  222. package/src/router/telemetry.ts +96 -19
  223. package/src/router/timeout.ts +0 -20
  224. package/src/router/trie-matching.ts +87 -47
  225. package/src/router/types.ts +9 -63
  226. package/src/router/url-params.ts +0 -5
  227. package/src/router.ts +80 -41
  228. package/src/rsc/handler-context.ts +3 -2
  229. package/src/rsc/handler.ts +83 -78
  230. package/src/rsc/helpers.ts +93 -5
  231. package/src/rsc/index.ts +1 -1
  232. package/src/rsc/json-route-result.ts +38 -0
  233. package/src/rsc/manifest-init.ts +28 -41
  234. package/src/rsc/origin-guard.ts +39 -25
  235. package/src/rsc/progressive-enhancement.ts +12 -1
  236. package/src/rsc/redirect-guard.ts +99 -0
  237. package/src/rsc/response-error.ts +79 -12
  238. package/src/rsc/response-route-handler.ts +76 -62
  239. package/src/rsc/rsc-rendering.ts +41 -60
  240. package/src/rsc/runtime-warnings.ts +23 -10
  241. package/src/rsc/server-action.ts +62 -67
  242. package/src/rsc/ssr-setup.ts +16 -0
  243. package/src/rsc/types.ts +10 -5
  244. package/src/runtime-env.ts +18 -0
  245. package/src/search-params.ts +4 -20
  246. package/src/segment-loader-promise.ts +14 -2
  247. package/src/segment-system.tsx +199 -142
  248. package/src/serialize.ts +243 -0
  249. package/src/server/context.ts +150 -51
  250. package/src/server/cookie-store.ts +80 -5
  251. package/src/server/handle-store.ts +7 -24
  252. package/src/server/loader-registry.ts +5 -24
  253. package/src/server/request-context.ts +165 -87
  254. package/src/ssr/index.tsx +14 -14
  255. package/src/static-handler.ts +10 -13
  256. package/src/testing/cache-status.ts +162 -0
  257. package/src/testing/collect-handle.ts +40 -0
  258. package/src/testing/dispatch.ts +618 -0
  259. package/src/testing/dom.entry.ts +22 -0
  260. package/src/testing/e2e/fixture.ts +188 -0
  261. package/src/testing/e2e/index.ts +128 -0
  262. package/src/testing/e2e/matchers.ts +35 -0
  263. package/src/testing/e2e/page-helpers.ts +272 -0
  264. package/src/testing/e2e/parity.ts +387 -0
  265. package/src/testing/e2e/server.ts +195 -0
  266. package/src/testing/flight-matchers.ts +97 -0
  267. package/src/testing/flight-normalize.ts +11 -0
  268. package/src/testing/flight-runtime.d.ts +57 -0
  269. package/src/testing/flight-tree.ts +682 -0
  270. package/src/testing/flight.entry.ts +52 -0
  271. package/src/testing/flight.ts +232 -0
  272. package/src/testing/generated-routes.ts +183 -0
  273. package/src/testing/index.ts +99 -0
  274. package/src/testing/internal/context.ts +348 -0
  275. package/src/testing/internal/flight-client-globals.ts +30 -0
  276. package/src/testing/internal/seed-vars.ts +54 -0
  277. package/src/testing/render-handler.ts +330 -0
  278. package/src/testing/render-route.tsx +566 -0
  279. package/src/testing/run-loader.ts +378 -0
  280. package/src/testing/run-middleware.ts +205 -0
  281. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  282. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  283. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  284. package/src/testing/vitest-stubs/version.ts +5 -0
  285. package/src/testing/vitest.ts +305 -0
  286. package/src/theme/ThemeProvider.tsx +0 -52
  287. package/src/theme/ThemeScript.tsx +0 -6
  288. package/src/theme/constants.ts +0 -12
  289. package/src/theme/index.ts +0 -7
  290. package/src/theme/theme-context.ts +1 -5
  291. package/src/theme/theme-script.ts +0 -14
  292. package/src/theme/use-theme.ts +0 -3
  293. package/src/types/boundaries.ts +0 -35
  294. package/src/types/cache-types.ts +13 -4
  295. package/src/types/error-types.ts +30 -90
  296. package/src/types/global-namespace.ts +54 -41
  297. package/src/types/handler-context.ts +97 -22
  298. package/src/types/index.ts +1 -10
  299. package/src/types/loader-types.ts +6 -3
  300. package/src/types/request-scope.ts +0 -19
  301. package/src/types/route-config.ts +6 -50
  302. package/src/types/route-entry.ts +0 -6
  303. package/src/types/segments.ts +18 -14
  304. package/src/urls/include-helper.ts +9 -56
  305. package/src/urls/index.ts +1 -11
  306. package/src/urls/path-helper-types.ts +19 -5
  307. package/src/urls/path-helper.ts +17 -106
  308. package/src/urls/pattern-types.ts +36 -19
  309. package/src/urls/response-types.ts +20 -19
  310. package/src/urls/type-extraction.ts +58 -139
  311. package/src/urls/urls-function.ts +1 -18
  312. package/src/use-loader.tsx +292 -107
  313. package/src/vite/debug.ts +1 -0
  314. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  315. package/src/vite/discovery/discover-routers.ts +95 -82
  316. package/src/vite/discovery/discovery-errors.ts +194 -0
  317. package/src/vite/discovery/prerender-collection.ts +26 -34
  318. package/src/vite/discovery/route-types-writer.ts +40 -84
  319. package/src/vite/discovery/state.ts +39 -1
  320. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  321. package/src/vite/index.ts +4 -0
  322. package/src/vite/plugin-types.ts +185 -10
  323. package/src/vite/plugins/cjs-to-esm.ts +3 -18
  324. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  325. package/src/vite/plugins/client-ref-hashing.ts +12 -11
  326. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  327. package/src/vite/plugins/expose-action-id.ts +4 -75
  328. package/src/vite/plugins/expose-id-utils.ts +3 -54
  329. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  330. package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
  331. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  332. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  333. package/src/vite/plugins/expose-internal-ids.ts +57 -67
  334. package/src/vite/plugins/performance-tracks.ts +9 -16
  335. package/src/vite/plugins/refresh-cmd.ts +1 -1
  336. package/src/vite/plugins/use-cache-transform.ts +26 -49
  337. package/src/vite/plugins/vercel-output.ts +258 -0
  338. package/src/vite/plugins/version-injector.ts +2 -32
  339. package/src/vite/plugins/version-plugin.ts +32 -23
  340. package/src/vite/plugins/virtual-entries.ts +35 -17
  341. package/src/vite/rango.ts +148 -115
  342. package/src/vite/router-discovery.ts +220 -68
  343. package/src/vite/utils/ast-handler-extract.ts +15 -31
  344. package/src/vite/utils/bundle-analysis.ts +10 -15
  345. package/src/vite/utils/client-chunks.ts +184 -0
  346. package/src/vite/utils/forward-user-plugins.ts +171 -0
  347. package/src/vite/utils/manifest-utils.ts +4 -59
  348. package/src/vite/utils/package-resolution.ts +1 -73
  349. package/src/vite/utils/prerender-utils.ts +0 -35
  350. package/src/vite/utils/shared-utils.ts +95 -43
  351. package/src/browser/action-response-classifier.ts +0 -99
  352. package/src/browser/react/use-client-cache.ts +0 -58
  353. package/src/browser/shallow.ts +0 -40
  354. package/src/handles/index.ts +0 -7
  355. package/src/router/middleware-cookies.ts +0 -55
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared route-param comparison helpers.
3
+ */
4
+
5
+ /**
6
+ * Shallow equality for two route-param records. Same-reference is a fast path;
7
+ * otherwise compares key count then each value.
8
+ */
9
+ export function paramsEqual(
10
+ a: Record<string, string>,
11
+ b: Record<string, string>,
12
+ ): boolean {
13
+ if (a === b) return true;
14
+
15
+ const keysA = Object.keys(a);
16
+ if (keysA.length !== Object.keys(b).length) return false;
17
+
18
+ for (const key of keysA) {
19
+ if (a[key] !== b[key]) return false;
20
+ }
21
+
22
+ return true;
23
+ }
@@ -33,13 +33,6 @@ export interface ParsedSegment {
33
33
  */
34
34
  export function parsePattern(pattern: string): ParsedSegment[] {
35
35
  const segments: ParsedSegment[] = [];
36
- // Match: /segment where segment can be:
37
- // - static text
38
- // - :param
39
- // - :param?
40
- // - :param(a|b)
41
- // - :param(a|b)?
42
- // - *
43
36
  const segmentRegex =
44
37
  /\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)?([^/]*)|(\*)|([^/]+))/g;
45
38
 
@@ -81,7 +74,6 @@ export function parsePattern(pattern: string): ParsedSegment[] {
81
74
  export interface CompiledPattern {
82
75
  regex: RegExp;
83
76
  paramNames: string[];
84
- optionalParams: Set<string>;
85
77
  hasTrailingSlash: boolean;
86
78
  /**
87
79
  * Param-name → allowed values for constrained params (e.g. `:lang(en|gb)`).
@@ -149,7 +141,6 @@ export function compilePattern(pattern: string): CompiledPattern {
149
141
 
150
142
  const segments = parsePattern(normalizedPattern);
151
143
  const paramNames: string[] = [];
152
- const optionalParams = new Set<string>();
153
144
  let constraints: Record<string, string[]> | undefined;
154
145
 
155
146
  let regexPattern = "";
@@ -171,7 +162,6 @@ export function compilePattern(pattern: string): CompiledPattern {
171
162
  }
172
163
 
173
164
  if (segment.optional) {
174
- optionalParams.add(segment.value);
175
165
  // Optional: make the whole /segment optional
176
166
  regexPattern += `(?:/${valuePattern}${suffixPattern})?`;
177
167
  } else {
@@ -183,7 +173,6 @@ export function compilePattern(pattern: string): CompiledPattern {
183
173
  }
184
174
  }
185
175
 
186
- // Handle root path
187
176
  if (regexPattern === "") {
188
177
  regexPattern = "/";
189
178
  }
@@ -210,7 +199,6 @@ export function compilePattern(pattern: string): CompiledPattern {
210
199
  return {
211
200
  regex: new RegExp(`^${regexPattern}$`),
212
201
  paramNames,
213
- optionalParams,
214
202
  hasTrailingSlash,
215
203
  ...(constraints ? { constraints } : {}),
216
204
  };
@@ -285,7 +273,6 @@ function buildParamsFromMatch(
285
273
  export function extractStaticPrefix(pattern: string): string {
286
274
  if (!pattern || pattern === "/") return "";
287
275
 
288
- // Find the first occurrence of : or *
289
276
  const paramIndex = pattern.indexOf(":");
290
277
  const wildcardIndex = pattern.indexOf("*");
291
278
 
@@ -299,16 +286,13 @@ export function extractStaticPrefix(pattern: string): string {
299
286
  }
300
287
 
301
288
  if (cutIndex === -1) {
302
- // No params or wildcards - entire pattern is static
303
289
  return pattern;
304
290
  }
305
291
 
306
292
  if (cutIndex === 0) {
307
- // Pattern starts with : or * - no static prefix
308
293
  return "";
309
294
  }
310
295
 
311
- // Find the last / before the param
312
296
  const lastSlash = pattern.lastIndexOf("/", cutIndex - 1);
313
297
  if (lastSlash === -1 || lastSlash === 0) {
314
298
  return "";
@@ -317,13 +301,28 @@ export function extractStaticPrefix(pattern: string): string {
317
301
  return pattern.slice(0, lastSlash);
318
302
  }
319
303
 
304
+ /**
305
+ * Join a URL prefix to a sub-prefix, collapsing the duplicate slash when the
306
+ * base ends with "/" and the sub-prefix starts with "/". This mirrors the
307
+ * canonical join in `include()` (urls/include-helper.ts) and `runWithPrefixes`
308
+ * (server/context.ts) so a nested lazy include's runtime staticPrefix matches
309
+ * the build-time trie's `sp` (e.g. `include("/parent/", …)` containing
310
+ * `include("/child", …)` resolves to `/parent/child`, not `/parent//child`).
311
+ */
312
+ export function joinPrefix(base: string | undefined, prefix: string): string {
313
+ if (!base) return prefix;
314
+ return base.endsWith("/") && prefix.startsWith("/")
315
+ ? base + prefix.slice(1)
316
+ : base + prefix;
317
+ }
318
+
320
319
  /**
321
320
  * Match a pathname against registered routes
322
321
  *
323
322
  * Note: Optional params that are absent in the path are omitted from the
324
323
  * returned `params` (read as `undefined`), matching the trie matcher and
325
- * the `ExtractParams<"/:locale?/...">` type. Use the pattern definition or
326
- * `optionalParams` to determine which keys are optional.
324
+ * the `ExtractParams<"/:locale?/...">` type. Use the pattern definition to
325
+ * determine which keys are optional.
327
326
  *
328
327
  * Trailing slash handling (priority order):
329
328
  * 1. Per-route `trailingSlash` config from route()
@@ -341,10 +340,7 @@ export interface RouteMatchResult<TEnv = any> {
341
340
  entry: RouteEntry<TEnv>;
342
341
  routeKey: string;
343
342
  params: Record<string, string>;
344
- optionalParams: Set<string>;
345
343
  redirectTo?: string;
346
- /** Ancestry shortCodes for layout pruning (from trie match) */
347
- ancestry?: string[];
348
344
  /** Route has pre-rendered data available (from trie) */
349
345
  pr?: true;
350
346
  /** Passthrough: handler kept for live fallback on unknown params (from trie) */
@@ -422,8 +418,6 @@ export function findMatch<TEnv>(
422
418
  : pathname + "/";
423
419
 
424
420
  for (const entry of routesEntries) {
425
- // Short-circuit: skip entry if pathname doesn't start with static prefix
426
- // staticPrefix is pre-computed at registration time, so this is O(1)
427
421
  if (entry.staticPrefix && !pathname.startsWith(entry.staticPrefix)) {
428
422
  if (effectiveDebug) {
429
423
  debugStats.entriesSkipped++;
@@ -435,8 +429,6 @@ export function findMatch<TEnv>(
435
429
  continue;
436
430
  }
437
431
 
438
- // Check if this is a lazy entry that needs evaluation
439
- // When staticPrefix matches but routes are not yet populated, signal caller to evaluate
440
432
  if (entry.lazy && !entry.lazyEvaluated) {
441
433
  if (effectiveDebug) {
442
434
  debugLog("findMatch", "lazy entry requires evaluation", {
@@ -457,7 +449,6 @@ export function findMatch<TEnv>(
457
449
  debugStats.routesChecked++;
458
450
  }
459
451
 
460
- // Join prefix and pattern, handling edge cases
461
452
  let fullPattern: string;
462
453
  if (entry.prefix === "" || entry.prefix === "/") {
463
454
  fullPattern = pattern;
@@ -467,19 +458,12 @@ export function findMatch<TEnv>(
467
458
  fullPattern = entry.prefix + pattern;
468
459
  }
469
460
 
470
- const {
471
- regex,
472
- paramNames,
473
- optionalParams,
474
- hasTrailingSlash,
475
- constraints,
476
- } = getCompiledPattern(fullPattern);
461
+ const { regex, paramNames, hasTrailingSlash, constraints } =
462
+ getCompiledPattern(fullPattern);
477
463
 
478
- // Get trailing slash mode for this route (per-route config or pattern-based)
479
464
  const trailingSlashMode: TrailingSlashMode | undefined =
480
465
  entry.trailingSlash?.[routeKey];
481
466
 
482
- // Prerender flag from entry metadata (set by urls() for prerender handlers)
483
467
  const prFlag = entry.prerenderRouteKeys?.has(routeKey)
484
468
  ? { pr: true as const }
485
469
  : {};
@@ -487,13 +471,10 @@ export function findMatch<TEnv>(
487
471
  ? { pt: true as const }
488
472
  : {};
489
473
 
490
- // Try exact match first
491
474
  const match = regex.exec(pathname);
492
475
  if (match) {
493
476
  const params = buildParamsFromMatch(match, paramNames);
494
477
 
495
- // Validate constraints against decoded values; a failure falls
496
- // through to the next route so other patterns can still match.
497
478
  if (!satisfiesConstraints(params, constraints)) {
498
479
  continue;
499
480
  }
@@ -506,29 +487,24 @@ export function findMatch<TEnv>(
506
487
  });
507
488
  }
508
489
 
509
- // Check if trailing slash mode requires redirect even on exact match
510
490
  if (
511
491
  trailingSlashMode === "always" &&
512
492
  !pathnameHasTrailingSlash &&
513
493
  pathname !== "/"
514
494
  ) {
515
- // Mode says always have trailing slash, but pathname doesn't have it
516
495
  return {
517
496
  entry,
518
497
  routeKey,
519
498
  params,
520
- optionalParams,
521
499
  redirectTo: pathname + "/",
522
500
  ...prFlag,
523
501
  ...ptFlag,
524
502
  };
525
503
  } else if (trailingSlashMode === "never" && pathnameHasTrailingSlash) {
526
- // Mode says never have trailing slash, but pathname has it
527
504
  return {
528
505
  entry,
529
506
  routeKey,
530
507
  params,
531
- optionalParams,
532
508
  redirectTo: pathname.slice(0, -1),
533
509
  ...prFlag,
534
510
  ...ptFlag,
@@ -539,13 +515,11 @@ export function findMatch<TEnv>(
539
515
  entry,
540
516
  routeKey,
541
517
  params,
542
- optionalParams,
543
518
  ...prFlag,
544
519
  ...ptFlag,
545
520
  };
546
521
  }
547
522
 
548
- // Try alternate pathname (opposite trailing slash)
549
523
  const altMatch = regex.exec(alternatePathname);
550
524
  if (altMatch) {
551
525
  const params = buildParamsFromMatch(altMatch, paramNames);
@@ -554,25 +528,20 @@ export function findMatch<TEnv>(
554
528
  continue;
555
529
  }
556
530
 
557
- // Determine redirect behavior based on mode
558
531
  if (trailingSlashMode === "ignore") {
559
- // Match without redirect
560
532
  return {
561
533
  entry,
562
534
  routeKey,
563
535
  params,
564
- optionalParams,
565
536
  ...prFlag,
566
537
  ...ptFlag,
567
538
  };
568
539
  } else if (trailingSlashMode === "never") {
569
- // Redirect to no trailing slash
570
540
  if (pathnameHasTrailingSlash) {
571
541
  return {
572
542
  entry,
573
543
  routeKey,
574
544
  params,
575
- optionalParams,
576
545
  redirectTo: alternatePathname,
577
546
  ...prFlag,
578
547
  ...ptFlag,
@@ -582,18 +551,15 @@ export function findMatch<TEnv>(
582
551
  entry,
583
552
  routeKey,
584
553
  params,
585
- optionalParams,
586
554
  ...prFlag,
587
555
  ...ptFlag,
588
556
  };
589
557
  } else if (trailingSlashMode === "always") {
590
- // Redirect to with trailing slash
591
558
  if (!pathnameHasTrailingSlash) {
592
559
  return {
593
560
  entry,
594
561
  routeKey,
595
562
  params,
596
- optionalParams,
597
563
  redirectTo: alternatePathname,
598
564
  ...prFlag,
599
565
  ...ptFlag,
@@ -603,13 +569,10 @@ export function findMatch<TEnv>(
603
569
  entry,
604
570
  routeKey,
605
571
  params,
606
- optionalParams,
607
572
  ...prFlag,
608
573
  ...ptFlag,
609
574
  };
610
575
  } else {
611
- // No explicit mode - use pattern-based detection
612
- // Redirect to canonical form (what the pattern defines)
613
576
  const canonicalPath = hasTrailingSlash
614
577
  ? alternatePathname
615
578
  : pathname.slice(0, -1);
@@ -617,7 +580,6 @@ export function findMatch<TEnv>(
617
580
  entry,
618
581
  routeKey,
619
582
  params,
620
- optionalParams,
621
583
  redirectTo: canonicalPath,
622
584
  ...prFlag,
623
585
  ...ptFlag,
@@ -638,7 +600,7 @@ export function* traverseBack(entry: EntryData): Generator<EntryData> {
638
600
  let current: EntryData | null = entry;
639
601
  const items = [] as EntryData[];
640
602
  while (current !== null) {
641
- items.push(current); // Move up to next parent
603
+ items.push(current);
642
604
  current = current.parent;
643
605
  }
644
606
  for (let i = items.length - 1; i >= 0; i--) {
@@ -5,13 +5,13 @@ import {
5
5
  runWithRequestContext,
6
6
  type RequestContext,
7
7
  } from "../server/request-context.js";
8
- import { contextGet, contextSet } from "../context-var.js";
8
+ import { contextGet, contextSet, hasContextVars } from "../context-var.js";
9
9
  import {
10
10
  createPrerenderContext,
11
11
  createStaticContext,
12
12
  createReverseFunction,
13
13
  } from "./handler-context.js";
14
- import { isPrerenderPassthrough } from "../prerender.js";
14
+ import { detectPrerenderPassthrough } from "../prerender.js";
15
15
  import { isRouteRootScoped } from "../route-map-builder.js";
16
16
  import { setupBuildUse } from "./loader-resolution.js";
17
17
  import { loadManifest } from "./manifest.js";
@@ -59,11 +59,16 @@ export async function matchForPrerender<TEnv = any>(
59
59
  devMode?: boolean,
60
60
  ): Promise<{
61
61
  segments: SerializedSegmentData[];
62
- handles: Record<string, SegmentHandleData>;
62
+ /** RSC-encoded handle map ("" when none) — see handle-snapshot.ts. Encoded in
63
+ * the producer (where the Flight codec resolves) so the node-side build/dev
64
+ * sinks can persist it without touching the codec. */
65
+ handles: string;
63
66
  routeName: string;
64
67
  params: Record<string, string>;
65
68
  interceptSegments?: SerializedSegmentData[];
66
- interceptHandles?: Record<string, SegmentHandleData>;
69
+ /** RSC-encoded MERGED (main + intercept) handle map for the intercept artifact;
70
+ * the sinks store it as-is (no longer merge raw records). */
71
+ interceptHandles?: string;
67
72
  passthrough?: true;
68
73
  } | null> {
69
74
  // 1. Find the matching route entry
@@ -77,6 +82,17 @@ export async function matchForPrerender<TEnv = any>(
77
82
  // Build RouterContext for loadManifest/traverseBack
78
83
  const routerCtx = deps.buildRouterContext();
79
84
 
85
+ // Passthrough sentinel result: an unknown-param Passthrough route falls
86
+ // through to the live handler at runtime, so no artifact is baked. A fresh
87
+ // object is returned per call (no site mutates or identity-compares it).
88
+ const passthroughResult = () => ({
89
+ segments: [],
90
+ handles: "",
91
+ routeName: matched.routeKey,
92
+ params: matchedParams,
93
+ passthrough: true as const,
94
+ });
95
+
80
96
  return runWithRouterContext(routerCtx, async () => {
81
97
  // 2. Load the manifest entry tree
82
98
  const manifestEntry = await loadManifest(
@@ -126,7 +142,7 @@ export async function matchForPrerender<TEnv = any>(
126
142
  get env() {
127
143
  if (buildEnv !== undefined) return buildEnv;
128
144
  throw new Error(
129
- "[rsc-router] ctx.env is not available during dev-mode getParams(). " +
145
+ "[rango] ctx.env is not available during dev-mode getParams(). " +
130
146
  "Configure buildEnv in your rango() plugin options to enable build-time env access.",
131
147
  );
132
148
  },
@@ -140,19 +156,10 @@ export async function matchForPrerender<TEnv = any>(
140
156
  );
141
157
  });
142
158
  if (!isKnown) {
143
- return {
144
- segments: [],
145
- handles: {},
146
- routeName: matched.routeKey,
147
- params: matchedParams,
148
- passthrough: true as const,
149
- };
159
+ return passthroughResult();
150
160
  }
151
161
  // Preserve vars set by getParams() for the render context
152
- if (
153
- Object.keys(probeBuildVars).length > 0 ||
154
- Object.getOwnPropertySymbols(probeBuildVars).length > 0
155
- ) {
162
+ if (hasContextVars(probeBuildVars)) {
156
163
  devProbeBuildVars = probeBuildVars;
157
164
  }
158
165
  } catch (err: any) {
@@ -160,13 +167,7 @@ export async function matchForPrerender<TEnv = any>(
160
167
  // Skip errors are intentional — treat as passthrough.
161
168
  // All other errors propagate so dev surfaces them.
162
169
  if (err?.name === "Skip") {
163
- return {
164
- segments: [],
165
- handles: {},
166
- routeName: matched.routeKey,
167
- params: matchedParams,
168
- passthrough: true as const,
169
- };
170
+ return passthroughResult();
170
171
  }
171
172
  throw err;
172
173
  }
@@ -206,11 +207,14 @@ export async function matchForPrerender<TEnv = any>(
206
207
  header: () => {},
207
208
  setStatus: () => {},
208
209
  _setStatus: () => {},
210
+ _rotateStateCookie: () => {},
211
+ _setKeepCacheDirective: () => {},
209
212
  use: (() => {
210
213
  throw new Error("use() not available during pre-rendering");
211
214
  }) as any,
212
215
  method: "GET",
213
216
  _handleStore: handleStore,
217
+ _requestTags: new Set<string>(),
214
218
  waitUntil: () => {},
215
219
  onResponse: () => {},
216
220
  _onResponseCallbacks: [],
@@ -256,17 +260,13 @@ export async function matchForPrerender<TEnv = any>(
256
260
  { skipLoaders: true },
257
261
  );
258
262
 
259
- // 9. Detect passthrough sentinel: handler returned ctx.passthrough()
260
- for (const seg of allSegments) {
261
- if (isPrerenderPassthrough(seg.component)) {
262
- return {
263
- segments: [],
264
- handles: {},
265
- routeName: matched.routeKey,
266
- params: matchedParams,
267
- passthrough: true as const,
268
- };
269
- }
263
+ // 9. Detect passthrough sentinel: handler returned ctx.passthrough().
264
+ // When the route declares loading(), the handler result is deferred so the
265
+ // component is a thenable resolving to the sentinel — detectPrerenderPassthrough
266
+ // resolves thenables before testing (a sync check would miss it and bake a
267
+ // corrupt artifact).
268
+ if (await detectPrerenderPassthrough(allSegments)) {
269
+ return passthroughResult();
270
270
  }
271
271
 
272
272
  // 10. Filter out any loader segments (belt-and-suspenders)
@@ -278,16 +278,22 @@ export async function matchForPrerender<TEnv = any>(
278
278
 
279
279
  // 12. Serialize segments using the cache serializer
280
280
  const { serializeSegments } = await import("../cache/segment-codec.js");
281
+ const { encodeHandles } = await import("../cache/handle-snapshot.js");
281
282
  const serializedSegments = await serializeSegments(nonLoaderSegments);
282
283
 
283
- // 13. Collect handle data per segment (skip segments with no handle data)
284
- const handles: Record<string, SegmentHandleData> = {};
284
+ // 13. Collect handle data per segment (skip segments with no handle data).
285
+ // Encoded through the Flight codec (not stored raw) so Promise/ReactNode
286
+ // handle values survive the JSON-serialized build artifact / dev wire —
287
+ // the same fix the runtime cache uses. Encode happens here, in the RSC
288
+ // environment where the codec resolves; the node-side sinks only persist.
289
+ const handlesRecord: Record<string, SegmentHandleData> = {};
285
290
  for (const seg of nonLoaderSegments) {
286
291
  const segHandles = handleStore.getDataForSegment(seg.id);
287
292
  if (Object.keys(segHandles).length > 0) {
288
- handles[seg.id] = segHandles;
293
+ handlesRecord[seg.id] = segHandles;
289
294
  }
290
295
  }
296
+ const handles = await encodeHandles(handlesRecord);
291
297
 
292
298
  // Use the trie-level route key (e.g., "docs", "docs.article")
293
299
  const routeName = matched.routeKey;
@@ -297,7 +303,7 @@ export async function matchForPrerender<TEnv = any>(
297
303
  // evaluation -- we pre-render all intercepts unconditionally and let
298
304
  // runtime matching decide which to serve.
299
305
  let interceptSegments: SerializedSegmentData[] | undefined;
300
- let interceptHandles: Record<string, SegmentHandleData> | undefined;
306
+ let interceptHandles: string | undefined;
301
307
 
302
308
  const foundIntercepts: {
303
309
  intercept: InterceptEntry;
@@ -305,24 +311,14 @@ export async function matchForPrerender<TEnv = any>(
305
311
  }[] = [];
306
312
  let current: EntryData | null = manifestEntry;
307
313
  while (current) {
308
- if (current.intercept && current.intercept.length > 0) {
309
- for (const ic of current.intercept) {
314
+ // Flatten the entry and its sibling layouts into one source list, the
315
+ // same traversal findInterceptForRoute uses; the build keeps ALL matches
316
+ // (not just the innermost) and skips when(). intercept/layout are
317
+ // non-optional arrays, so empty ones are a no-op here.
318
+ for (const source of [current, ...current.layout]) {
319
+ for (const ic of source.intercept) {
310
320
  if (ic.routeName === matched.routeKey) {
311
- foundIntercepts.push({ intercept: ic, entry: current });
312
- }
313
- }
314
- }
315
- if (current.layout && current.layout.length > 0) {
316
- for (const siblingLayout of current.layout) {
317
- if (siblingLayout.intercept && siblingLayout.intercept.length > 0) {
318
- for (const ic of siblingLayout.intercept) {
319
- if (ic.routeName === matched.routeKey) {
320
- foundIntercepts.push({
321
- intercept: ic,
322
- entry: siblingLayout,
323
- });
324
- }
325
- }
321
+ foundIntercepts.push({ intercept: ic, entry: source });
326
322
  }
327
323
  }
328
324
  }
@@ -333,6 +329,18 @@ export async function matchForPrerender<TEnv = any>(
333
329
  const interceptResolvedSegments: typeof nonLoaderSegments = [];
334
330
 
335
331
  for (const { intercept, entry: parentEntry } of foundIntercepts) {
332
+ // setupBuildUse keys handle pushes by ctx._currentSegmentId. The main
333
+ // resolveAllSegments pass left it on the route's segment id, so pin it
334
+ // to THIS intercept's slot id before resolving the handler/layout --
335
+ // otherwise the intercept's ctx.use() handle pushes land in the wrong
336
+ // bucket and getDataForSegment(seg.id) below drops them from the baked
337
+ // artifact. Re-pinned per iteration so multiple intercepts targeting
338
+ // the same route each get their own id (mirrors fresh.ts parallel-slot
339
+ // pinning).
340
+ const interceptSegmentId = `${parentEntry.shortCode}.${intercept.slotName}`;
341
+ (buildCtx as InternalHandlerContext<any, TEnv>)._currentSegmentId =
342
+ interceptSegmentId;
343
+
336
344
  // Resolve handler
337
345
  const handlerRaw =
338
346
  typeof intercept.handler === "function"
@@ -359,7 +367,7 @@ export async function matchForPrerender<TEnv = any>(
359
367
  }
360
368
 
361
369
  interceptResolvedSegments.push({
362
- id: `${parentEntry.shortCode}.${intercept.slotName}`,
370
+ id: interceptSegmentId,
363
371
  namespace: `intercept:${intercept.routeName}`,
364
372
  type: "parallel" as const,
365
373
  index: 0,
@@ -379,13 +387,20 @@ export async function matchForPrerender<TEnv = any>(
379
387
  interceptSegments = await serializeSegments(
380
388
  interceptResolvedSegments,
381
389
  );
382
- interceptHandles = {};
390
+ const interceptHandlesRecord: Record<string, SegmentHandleData> = {};
383
391
  for (const seg of interceptResolvedSegments) {
384
392
  const segHandles = handleStore.getDataForSegment(seg.id);
385
393
  if (Object.keys(segHandles).length > 0) {
386
- interceptHandles[seg.id] = segHandles;
394
+ interceptHandlesRecord[seg.id] = segHandles;
387
395
  }
388
396
  }
397
+ // The intercept artifact serves main + intercept segments together, so
398
+ // encode the MERGED handle map here (the sinks no longer merge raw
399
+ // records — they store this pre-encoded string as-is).
400
+ interceptHandles = await encodeHandles({
401
+ ...handlesRecord,
402
+ ...interceptHandlesRecord,
403
+ });
389
404
  }
390
405
  }
391
406
 
@@ -414,7 +429,7 @@ export async function renderStaticSegment<TEnv = any>(
414
429
  routeName?: string,
415
430
  buildEnv?: TEnv,
416
431
  devMode?: boolean,
417
- ): Promise<{ encoded: string; handles: Record<string, unknown[]> } | null> {
432
+ ): Promise<{ encoded: string; handles: string } | null> {
418
433
  const syntheticUrl = new URL("http://prerender/");
419
434
  const syntheticRequest = new Request(syntheticUrl);
420
435
 
@@ -442,11 +457,14 @@ export async function renderStaticSegment<TEnv = any>(
442
457
  header: () => {},
443
458
  setStatus: () => {},
444
459
  _setStatus: () => {},
460
+ _rotateStateCookie: () => {},
461
+ _setKeepCacheDirective: () => {},
445
462
  use: (() => {
446
463
  throw new Error("use() not available during static pre-rendering");
447
464
  }) as any,
448
465
  method: "GET",
449
466
  _handleStore: handleStore,
467
+ _requestTags: new Set<string>(),
450
468
  waitUntil: () => {},
451
469
  onResponse: () => {},
452
470
  _onResponseCallbacks: [],
@@ -479,23 +497,41 @@ export async function renderStaticSegment<TEnv = any>(
479
497
  setupBuildUse(buildCtx);
480
498
 
481
499
  const raw = await handler(buildCtx);
482
- const component = raw?.type ? raw : raw;
500
+
501
+ // Static handlers must return a ReactNode. A returned Response (e.g. an
502
+ // accidental redirect()) would otherwise be serialized as a corrupt build
503
+ // artifact; fail loudly instead. The fresh/revalidation paths route handler
504
+ // results through handleHandlerResult, which throws Responses; the static
505
+ // build path bypasses that, so guard here.
506
+ if (raw instanceof Response) {
507
+ throw new TypeError(
508
+ `Static handler "${routeName}" returned a Response. Static handlers must return a ReactNode; ` +
509
+ `Responses (redirects, file responses) are not supported during static pre-rendering.`,
510
+ );
511
+ }
483
512
 
484
513
  const segment: ResolvedSegment = {
485
514
  id: handlerId,
486
515
  namespace: handlerId,
487
516
  type: "layout",
488
517
  index: 0,
489
- component,
518
+ component: raw,
490
519
  params: {},
491
520
  belongsToRoute: false,
492
521
  };
493
522
 
494
523
  const { serializeSegments } = await import("../cache/segment-codec.js");
524
+ const { encodeHandleValue } = await import("../cache/handle-snapshot.js");
495
525
  const [serialized] = await serializeSegments([segment]);
496
526
 
497
- // Collect handle data pushed during rendering
498
- const handles = handleStore.getDataForSegment(handlerId);
527
+ // Collect handle data pushed during rendering and Flight-encode it (so
528
+ // Promise/ReactNode handle values survive the JSON build artifact). "" when
529
+ // nothing was pushed.
530
+ const segHandles = handleStore.getDataForSegment(handlerId);
531
+ const handles =
532
+ Object.keys(segHandles).length > 0
533
+ ? await encodeHandleValue(segHandles)
534
+ : "";
499
535
 
500
536
  return { encoded: serialized.encoded, handles };
501
537
  });
@@ -67,9 +67,11 @@ export async function previewMatch<TEnv = any>(
67
67
  responseType: negotiation.responseType,
68
68
  handler: negotiation.handler,
69
69
  params: matched.params,
70
- negotiated: true,
71
70
  manifestEntry: negotiation.manifestEntry,
72
71
  routeKey: matched.routeKey,
72
+ // omitted unless a variant negotiated, preserving the prior public
73
+ // shape (absent for plain response routes, not negotiated:false)
74
+ ...(negotiation.negotiated ? { negotiated: true } : {}),
73
75
  };
74
76
  }
75
77