@rangojs/router 0.16.0 → 0.18.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 (309) hide show
  1. package/README.md +255 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/cookie-name.d.ts +6 -0
  4. package/dist/types/browser/history-state.d.ts +16 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  6. package/dist/types/browser/partial-update.d.ts +11 -0
  7. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  8. package/dist/types/browser/rsc-router.d.ts +1 -1
  9. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  10. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  11. package/dist/types/cache/cache-key-utils.d.ts +17 -2
  12. package/dist/types/cache/cache-policy.d.ts +17 -0
  13. package/dist/types/cache/cache-scope.d.ts +132 -19
  14. package/dist/types/cache/cache-tag.d.ts +92 -7
  15. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  16. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  17. package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
  18. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  19. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  20. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
  21. package/dist/types/cache/handle-capture.d.ts +41 -3
  22. package/dist/types/cache/handle-snapshot.d.ts +54 -13
  23. package/dist/types/cache/index.d.ts +1 -0
  24. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  25. package/dist/types/cache/segment-codec.d.ts +10 -3
  26. package/dist/types/cache/shell-memo.d.ts +178 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +110 -21
  28. package/dist/types/cache/tag-invalidation.d.ts +30 -3
  29. package/dist/types/cache/taint.d.ts +1 -1
  30. package/dist/types/cache/types.d.ts +220 -44
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
  32. package/dist/types/client.d.ts +4 -4
  33. package/dist/types/client.rsc.d.ts +4 -5
  34. package/dist/types/components/DefaultDocument.d.ts +5 -2
  35. package/dist/types/context-var.d.ts +6 -5
  36. package/dist/types/escape-script.d.ts +1 -1
  37. package/dist/types/handles/MetaTags.d.ts +5 -4
  38. package/dist/types/handles/Scripts.d.ts +8 -7
  39. package/dist/types/handles/meta.d.ts +2 -2
  40. package/dist/types/handles/script.d.ts +17 -16
  41. package/dist/types/href-client.d.ts +1 -1
  42. package/dist/types/html.d.ts +12 -0
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +3 -2
  45. package/dist/types/loader-store.d.ts +7 -4
  46. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  47. package/dist/types/reverse.d.ts +36 -4
  48. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  49. package/dist/types/route-definition/helpers-types.d.ts +33 -9
  50. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  51. package/dist/types/route-types.d.ts +1 -1
  52. package/dist/types/router/debug-manifest.d.ts +3 -2
  53. package/dist/types/router/error-handling.d.ts +20 -0
  54. package/dist/types/router/handler-context.d.ts +6 -0
  55. package/dist/types/router/instrument.d.ts +8 -0
  56. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  57. package/dist/types/router/middleware-types.d.ts +9 -3
  58. package/dist/types/router/router-context.d.ts +3 -1
  59. package/dist/types/router/router-interfaces.d.ts +24 -2
  60. package/dist/types/router/router-options.d.ts +8 -5
  61. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  62. package/dist/types/router/segment-resolution/loader-cache.d.ts +66 -1
  63. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  64. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
  65. package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  67. package/dist/types/router/segment-wrappers.d.ts +1 -1
  68. package/dist/types/rsc/full-payload.d.ts +14 -0
  69. package/dist/types/rsc/handler.d.ts +1 -1
  70. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  71. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  72. package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
  73. package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
  74. package/dist/types/rsc/shell-capture.d.ts +151 -19
  75. package/dist/types/rsc/shell-serve.d.ts +140 -17
  76. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  77. package/dist/types/rsc/types.d.ts +17 -0
  78. package/dist/types/server/context.d.ts +139 -15
  79. package/dist/types/server/cookie-store.d.ts +21 -0
  80. package/dist/types/server/handle-store.d.ts +26 -2
  81. package/dist/types/server/request-context.d.ts +182 -53
  82. package/dist/types/ssr/index.d.ts +24 -0
  83. package/dist/types/ssr/ssr-root.d.ts +9 -8
  84. package/dist/types/static-handler.d.ts +4 -1
  85. package/dist/types/testing/flight.entry.d.ts +7 -0
  86. package/dist/types/testing/generated-routes.d.ts +2 -2
  87. package/dist/types/testing/internal/context.d.ts +1 -1
  88. package/dist/types/testing/render-route.d.ts +32 -6
  89. package/dist/types/testing/run-loader.d.ts +18 -3
  90. package/dist/types/testing/serve-shell-request.d.ts +132 -0
  91. package/dist/types/testing/shell-status.d.ts +23 -10
  92. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  93. package/dist/types/testing/vitest.d.ts +35 -1
  94. package/dist/types/theme/ThemeScript.d.ts +3 -3
  95. package/dist/types/types/cache-types.d.ts +14 -10
  96. package/dist/types/types/handler-context.d.ts +28 -1
  97. package/dist/types/types/loader-types.d.ts +8 -6
  98. package/dist/types/urls/pattern-types.d.ts +22 -18
  99. package/dist/types/urls/response-types.d.ts +3 -12
  100. package/dist/types/use-loader.d.ts +12 -3
  101. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  102. package/dist/types/vite/rango.d.ts +1 -1
  103. package/dist/types/vite/router-discovery.d.ts +29 -2
  104. package/dist/vite/index.js +1420 -1348
  105. package/package.json +2 -2
  106. package/skills/api-client/SKILL.md +22 -10
  107. package/skills/breadcrumbs/SKILL.md +75 -22
  108. package/skills/bundle-analysis/SKILL.md +22 -22
  109. package/skills/cache-guide/SKILL.md +306 -124
  110. package/skills/caching/SKILL.md +530 -98
  111. package/skills/catalog.json +16 -16
  112. package/skills/client-urls/SKILL.md +65 -38
  113. package/skills/cloudflare/SKILL.md +87 -20
  114. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  115. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  116. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  117. package/skills/comparison/SKILL.md +5 -0
  118. package/skills/comparison/references/framework-comparison.md +75 -57
  119. package/skills/composability/SKILL.md +38 -29
  120. package/skills/css/SKILL.md +10 -13
  121. package/skills/debug-manifest/SKILL.md +74 -31
  122. package/skills/defer-hydration/SKILL.md +52 -31
  123. package/skills/deployment-caching/SKILL.md +11 -3
  124. package/skills/document-cache/SKILL.md +98 -28
  125. package/skills/fonts/SKILL.md +16 -8
  126. package/skills/handler-use/SKILL.md +55 -17
  127. package/skills/hooks/SKILL.md +28 -20
  128. package/skills/hooks/data.md +23 -3
  129. package/skills/hooks/handle-and-actions.md +34 -17
  130. package/skills/hooks/navigation.md +39 -10
  131. package/skills/hooks/outlets.md +13 -6
  132. package/skills/hooks/state.md +39 -15
  133. package/skills/hooks/urls.md +7 -5
  134. package/skills/host-router/SKILL.md +64 -12
  135. package/skills/i18n/SKILL.md +60 -34
  136. package/skills/intercept/SKILL.md +161 -26
  137. package/skills/layout/SKILL.md +106 -50
  138. package/skills/links/SKILL.md +51 -19
  139. package/skills/loader/SKILL.md +386 -128
  140. package/skills/middleware/SKILL.md +129 -26
  141. package/skills/migrate-nextjs/SKILL.md +156 -70
  142. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  143. package/skills/migrate-react-router/SKILL.md +51 -39
  144. package/skills/migrate-react-router/component-migration.md +28 -7
  145. package/skills/migrate-react-router/data-and-actions.md +21 -6
  146. package/skills/migrate-react-router/route-mapping.md +19 -10
  147. package/skills/mime-routes/SKILL.md +12 -7
  148. package/skills/observability/SKILL.md +92 -14
  149. package/skills/parallel/SKILL.md +88 -47
  150. package/skills/ppr/SKILL.md +866 -379
  151. package/skills/prerender/SKILL.md +210 -108
  152. package/skills/rango/SKILL.md +115 -65
  153. package/skills/react-compiler/SKILL.md +6 -5
  154. package/skills/response-routes/SKILL.md +133 -25
  155. package/skills/route/SKILL.md +170 -60
  156. package/skills/router-setup/SKILL.md +420 -240
  157. package/skills/scripts/SKILL.md +39 -21
  158. package/skills/server-actions/SKILL.md +231 -59
  159. package/skills/shell-manifest/SKILL.md +28 -14
  160. package/skills/streams-and-websockets/SKILL.md +32 -14
  161. package/skills/tailwind/SKILL.md +37 -33
  162. package/skills/testing/SKILL.md +73 -33
  163. package/skills/testing/cache-prerender.md +60 -33
  164. package/skills/testing/client-components.md +59 -6
  165. package/skills/testing/e2e-parity.md +12 -2
  166. package/skills/testing/flight.md +19 -11
  167. package/skills/testing/handles.md +8 -5
  168. package/skills/testing/loader.md +26 -6
  169. package/skills/testing/middleware.md +8 -3
  170. package/skills/testing/predicates.md +120 -0
  171. package/skills/testing/render-handler.md +70 -22
  172. package/skills/testing/response-routes.md +11 -4
  173. package/skills/testing/reverse-and-types.md +20 -14
  174. package/skills/testing/serve-shell-request.md +188 -0
  175. package/skills/testing/server-actions.md +12 -3
  176. package/skills/testing/server-tree.md +9 -0
  177. package/skills/testing/setup.md +9 -5
  178. package/skills/theme/SKILL.md +155 -30
  179. package/skills/typesafety/SKILL.md +20 -9
  180. package/skills/typesafety/env-and-bindings.md +137 -50
  181. package/skills/typesafety/generated-files-and-cli.md +26 -14
  182. package/skills/typesafety/params-and-search.md +44 -19
  183. package/skills/typesafety/route-types.md +50 -14
  184. package/skills/use-cache/SKILL.md +224 -83
  185. package/skills/vercel/SKILL.md +35 -13
  186. package/skills/view-transitions/SKILL.md +42 -21
  187. package/src/__augment-tests__/augmented.check.ts +26 -1
  188. package/src/browser/cookie-name.ts +23 -0
  189. package/src/browser/history-state.ts +23 -0
  190. package/src/browser/index.ts +2 -2
  191. package/src/browser/navigation-bridge.ts +3 -4
  192. package/src/browser/navigation-transaction.ts +22 -2
  193. package/src/browser/partial-update.ts +35 -25
  194. package/src/browser/react/ScrollRestoration.tsx +6 -6
  195. package/src/browser/rsc-router.tsx +6 -1
  196. package/src/browser/scroll-restoration.ts +53 -6
  197. package/src/cache/cache-exec-scope.ts +37 -4
  198. package/src/cache/cache-key-utils.ts +23 -8
  199. package/src/cache/cache-policy.ts +22 -0
  200. package/src/cache/cache-runtime.ts +264 -68
  201. package/src/cache/cache-scope.ts +419 -43
  202. package/src/cache/cache-tag.ts +375 -11
  203. package/src/cache/cf/cf-base64.ts +36 -4
  204. package/src/cache/cf/cf-cache-constants.ts +12 -0
  205. package/src/cache/cf/cf-cache-store.ts +990 -348
  206. package/src/cache/cf/cf-cache-types.ts +32 -4
  207. package/src/cache/cf/cf-shell-frame.ts +258 -0
  208. package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
  209. package/src/cache/document-cache.ts +15 -0
  210. package/src/cache/handle-capture.ts +90 -10
  211. package/src/cache/handle-snapshot.ts +133 -28
  212. package/src/cache/index.ts +2 -0
  213. package/src/cache/isolate-tag-memo.ts +308 -0
  214. package/src/cache/segment-codec.ts +26 -7
  215. package/src/cache/shell-memo.ts +341 -0
  216. package/src/cache/shell-snapshot.ts +217 -27
  217. package/src/cache/tag-invalidation.ts +108 -22
  218. package/src/cache/taint.ts +1 -1
  219. package/src/cache/types.ts +225 -44
  220. package/src/cache/vercel/vercel-cache-store.ts +545 -61
  221. package/src/client.rsc.tsx +10 -9
  222. package/src/client.tsx +5 -4
  223. package/src/components/DefaultDocument.tsx +7 -2
  224. package/src/context-var.ts +6 -5
  225. package/src/escape-script.ts +1 -1
  226. package/src/handles/MetaTags.tsx +13 -11
  227. package/src/handles/Scripts.tsx +11 -10
  228. package/src/handles/meta.ts +2 -2
  229. package/src/handles/script.ts +17 -16
  230. package/src/href-client.ts +1 -1
  231. package/src/html.ts +12 -0
  232. package/src/index.rsc.ts +5 -0
  233. package/src/index.ts +1 -0
  234. package/src/loader-store.ts +7 -4
  235. package/src/prerender/build-shell-capture.ts +73 -22
  236. package/src/reverse.ts +44 -4
  237. package/src/route-definition/dsl-helpers.ts +121 -33
  238. package/src/route-definition/helpers-types.ts +33 -9
  239. package/src/route-definition/resolve-handler-use.ts +121 -6
  240. package/src/route-types.ts +0 -3
  241. package/src/router/debug-manifest.ts +10 -2
  242. package/src/router/error-handling.ts +51 -15
  243. package/src/router/handler-context.ts +71 -28
  244. package/src/router/instrument.ts +54 -2
  245. package/src/router/intercept-resolution.ts +141 -43
  246. package/src/router/loader-resolution.ts +115 -39
  247. package/src/router/match-api.ts +40 -26
  248. package/src/router/match-handlers.ts +5 -0
  249. package/src/router/match-middleware/background-revalidation.ts +161 -96
  250. package/src/router/match-middleware/cache-lookup.ts +110 -10
  251. package/src/router/match-middleware/cache-store.ts +48 -101
  252. package/src/router/middleware-types.ts +9 -6
  253. package/src/router/middleware.ts +75 -13
  254. package/src/router/prerender-match.ts +59 -19
  255. package/src/router/route-snapshot.ts +11 -1
  256. package/src/router/router-context.ts +2 -0
  257. package/src/router/router-interfaces.ts +25 -2
  258. package/src/router/router-options.ts +8 -5
  259. package/src/router/segment-resolution/fresh.ts +62 -21
  260. package/src/router/segment-resolution/helpers.ts +4 -2
  261. package/src/router/segment-resolution/loader-cache.ts +407 -76
  262. package/src/router/segment-resolution/loader-mask.ts +12 -22
  263. package/src/router/segment-resolution/loader-snapshot.ts +208 -56
  264. package/src/router/segment-resolution/mask-nested.ts +138 -44
  265. package/src/router/segment-resolution/revalidation.ts +61 -16
  266. package/src/router/segment-wrappers.ts +4 -0
  267. package/src/rsc/full-payload.ts +25 -1
  268. package/src/rsc/handler.ts +14 -2
  269. package/src/rsc/progressive-enhancement.ts +3 -2
  270. package/src/rsc/render-pipeline.ts +3 -0
  271. package/src/rsc/response-cache-serve.ts +9 -6
  272. package/src/rsc/response-route-handler.ts +11 -3
  273. package/src/rsc/rsc-rendering.ts +608 -225
  274. package/src/rsc/shell-build-manifest.ts +27 -9
  275. package/src/rsc/shell-capture-constants.ts +73 -6
  276. package/src/rsc/shell-capture.ts +1068 -296
  277. package/src/rsc/shell-serve.ts +316 -34
  278. package/src/rsc/ssr-setup.ts +14 -8
  279. package/src/rsc/types.ts +18 -0
  280. package/src/server/context.ts +300 -32
  281. package/src/server/cookie-store.ts +87 -61
  282. package/src/server/handle-store.ts +232 -7
  283. package/src/server/request-context.ts +297 -67
  284. package/src/ssr/index.tsx +136 -44
  285. package/src/ssr/ssr-root.tsx +9 -8
  286. package/src/static-handler.ts +4 -1
  287. package/src/testing/flight-runtime.d.ts +21 -0
  288. package/src/testing/flight.entry.ts +14 -0
  289. package/src/testing/generated-routes.ts +2 -2
  290. package/src/testing/internal/context.ts +1 -1
  291. package/src/testing/internal/flight-client-globals.ts +7 -5
  292. package/src/testing/render-route.tsx +85 -24
  293. package/src/testing/run-loader.ts +68 -6
  294. package/src/testing/serve-shell-request.ts +378 -0
  295. package/src/testing/shell-status.ts +34 -12
  296. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  297. package/src/testing/vitest.ts +62 -1
  298. package/src/theme/ThemeProvider.tsx +7 -6
  299. package/src/theme/ThemeScript.tsx +3 -3
  300. package/src/types/cache-types.ts +14 -10
  301. package/src/types/handler-context.ts +28 -1
  302. package/src/types/loader-types.ts +8 -6
  303. package/src/urls/pattern-types.ts +22 -18
  304. package/src/urls/response-types.ts +3 -23
  305. package/src/use-loader.tsx +12 -3
  306. package/src/vite/discovery/shell-prerender-phase.ts +9 -0
  307. package/src/vite/plugins/use-cache-transform.ts +118 -74
  308. package/src/vite/rango.ts +7 -3
  309. package/src/vite/router-discovery.ts +14 -4
@@ -105,31 +105,67 @@ export function invokeOnError<TEnv = any>(
105
105
  }
106
106
  }
107
107
 
108
+ /**
109
+ * First non-undefined `pick(orphan)` over the routeless entries in
110
+ * `entry.layout` and, recursively, in theirs: depth-first in render order
111
+ * (resolveOrphanLayout in segment-resolution/fresh.ts). Since #922 a routeless
112
+ * entry nested in another renders (a layout() after a bare cache() marker, a
113
+ * layout() in a routeless wrapper); a one-level scan missed the boundaries and
114
+ * intercepts it declares (#926).
115
+ *
116
+ * `skip` is the entry an upward walk came from. Only a bare cache() marker is
117
+ * both in its layout's layout[] and in the chain, and its subtree was already
118
+ * scanned at its chain position.
119
+ */
120
+ export function findInOrphans<T>(
121
+ entry: EntryData,
122
+ pick: (orphan: EntryData) => T | undefined,
123
+ skip?: EntryData | null,
124
+ ): T | undefined {
125
+ for (const orphan of entry.layout ?? []) {
126
+ if (orphan === skip) continue;
127
+ const found = pick(orphan) ?? findInOrphans(orphan, pick);
128
+ if (found !== undefined) return found;
129
+ }
130
+ return undefined;
131
+ }
132
+
133
+ /** `findInOrphans` pick: the orphan when it declares an errorBoundary(). */
134
+ export const withErrorBoundary = (entry: EntryData): EntryData | undefined =>
135
+ entry.errorBoundary && entry.errorBoundary.length > 0 ? entry : undefined;
136
+
137
+ const withNotFoundBoundary = (entry: EntryData): EntryData | undefined =>
138
+ entry.notFoundBoundary && entry.notFoundBoundary.length > 0
139
+ ? entry
140
+ : undefined;
141
+
108
142
  /**
109
143
  * Find the nearest error boundary by walking up the entry chain
110
144
  * Also checks sibling layouts (orphan layouts) for error boundaries
111
145
  * Returns the first fallback found, or the default error boundary if configured
146
+ *
147
+ * An orphan's parent is null, so from an orphan the walk continues at its
148
+ * orphanOwner (EntryData). Without that step, errors from an intercept declared
149
+ * in a routeless layout, or from that layout's loaders, stopped at the orphan
150
+ * and skipped every ancestor boundary (#898).
112
151
  */
113
152
  export function findNearestErrorBoundary(
114
153
  entry: EntryData | null,
115
154
  defaultErrorBoundary?: ReactNode | ErrorBoundaryHandler,
116
155
  ): ReactNode | ErrorBoundaryHandler | null {
117
156
  let current: EntryData | null = entry;
157
+ let prev: EntryData | null = null;
118
158
 
119
159
  while (current) {
120
160
  if (current.errorBoundary && current.errorBoundary.length > 0) {
121
161
  return current.errorBoundary[current.errorBoundary.length - 1];
122
162
  }
123
163
 
124
- if (current.layout && current.layout.length > 0) {
125
- for (const orphan of current.layout) {
126
- if (orphan.errorBoundary && orphan.errorBoundary.length > 0) {
127
- return orphan.errorBoundary[orphan.errorBoundary.length - 1];
128
- }
129
- }
130
- }
164
+ const orphan = findInOrphans(current, withErrorBoundary, prev);
165
+ if (orphan) return orphan.errorBoundary[orphan.errorBoundary.length - 1];
131
166
 
132
- current = current.parent;
167
+ prev = current;
168
+ current = current.parent ?? current.orphanOwner ?? null;
133
169
  }
134
170
 
135
171
  // Return default error boundary if configured
@@ -145,6 +181,7 @@ export function findNearestNotFoundBoundary(
145
181
  defaultNotFoundBoundary?: ReactNode | NotFoundBoundaryHandler,
146
182
  ): ReactNode | NotFoundBoundaryHandler | null {
147
183
  let current: EntryData | null = entry;
184
+ let prev: EntryData | null = null;
148
185
 
149
186
  while (current) {
150
187
  if (current.notFoundBoundary && current.notFoundBoundary.length > 0) {
@@ -154,15 +191,14 @@ export function findNearestNotFoundBoundary(
154
191
  // Check orphan layouts mirroring findNearestErrorBoundary: notFoundBoundary
155
192
  // attaches identically (onto parent.notFoundBoundary), and an orphan layout
156
193
  // (parent=null) is reachable only via this scan. First sibling is "outer".
157
- if (current.layout && current.layout.length > 0) {
158
- for (const orphan of current.layout) {
159
- if (orphan.notFoundBoundary && orphan.notFoundBoundary.length > 0) {
160
- return orphan.notFoundBoundary[orphan.notFoundBoundary.length - 1];
161
- }
162
- }
194
+ const orphan = findInOrphans(current, withNotFoundBoundary, prev);
195
+ if (orphan) {
196
+ return orphan.notFoundBoundary[orphan.notFoundBoundary.length - 1];
163
197
  }
164
198
 
165
- current = current.parent;
199
+ // Orphan: continue at its owner, as findNearestErrorBoundary does.
200
+ prev = current;
201
+ current = current.parent ?? current.orphanOwner ?? null;
166
202
  }
167
203
 
168
204
  // Return default notFound boundary if configured
@@ -5,7 +5,10 @@
5
5
  */
6
6
 
7
7
  import type { HandlerContext, InternalHandlerContext } from "../types";
8
- import { _getRequestContext } from "../server/request-context.js";
8
+ import {
9
+ _getRequestContext,
10
+ type RequestContext,
11
+ } from "../server/request-context.js";
9
12
  import { getSearchSchema, isRouteRootScoped } from "../route-map-builder.js";
10
13
  import { parseSearchParams, serializeSearchParams } from "../search-params.js";
11
14
  import {
@@ -16,8 +19,9 @@ import {
16
19
  } from "../context-var.js";
17
20
  import {
18
21
  assertCachedHeaderWriteAllowed,
19
- isInsideCacheScope,
22
+ assertNonCacheableReadAllowed,
20
23
  } from "../server/context.js";
24
+ import { assertThemeReadAllowed } from "../server/cookie-store.js";
21
25
  import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
22
26
  import { isAutoGeneratedRouteName } from "../route-name.js";
23
27
  import { PRERENDER_PASSTHROUGH } from "../prerender.js";
@@ -29,6 +33,60 @@ import { fireAndForgetWaitUntil } from "../types/request-scope.js";
29
33
  // once at module load instead of per createHandlerContext (per-request) call.
30
34
  const MUTATING_HEADERS_METHODS = new Set(["set", "append", "delete"]);
31
35
 
36
+ /** The request context a handler ctx's `theme` getter reads (THEME_DESCRIPTOR). */
37
+ const THEME_REQUEST_CONTEXT = Symbol("rango:theme-request-context");
38
+
39
+ /**
40
+ * ctx.theme is the visitor's theme cookie, guarded like cookies() (#971,
41
+ * assertThemeReadAllowed): a getter, so creating the ctx reads nothing and
42
+ * only a handler that reads the theme refuses a ppr capture or throws on a
43
+ * cache() miss. Non-enumerable, so serializing the ctx does not read it:
44
+ * React's dev Flight debug info JSON-stringifies server component props and
45
+ * replayed console.log args, and an enumerable getter read there refused the
46
+ * capture of a route that never read the theme. The trade: a spread or
47
+ * Object.assign copy of the ctx does not carry `theme`. One shared getter
48
+ * with the request context on a symbol key: a getter closure per ctx gives
49
+ * every ctx its own property shape (V8 dictionary mode). The guard reads the
50
+ * ambient context, as cookies() does, and falls back to the ctx's own when
51
+ * there is none.
52
+ */
53
+ const THEME_DESCRIPTOR: PropertyDescriptor = {
54
+ get(this: { [THEME_REQUEST_CONTEXT]?: RequestContext }) {
55
+ const requestContext = this[THEME_REQUEST_CONTEXT];
56
+ if (!requestContext?._themeConfig) return undefined;
57
+ assertThemeReadAllowed(_getRequestContext() ?? requestContext, "ctx.theme");
58
+ return requestContext._readTheme();
59
+ },
60
+ enumerable: false,
61
+ configurable: true,
62
+ };
63
+
64
+ /**
65
+ * `headers` whose mutating methods run `assertWrite(method)` first. Shared by
66
+ * the handler ctx (createHandlerContext) and the middleware ctx
67
+ * (createMiddlewareContext).
68
+ */
69
+ export function guardHeaderWrites(
70
+ headers: Headers,
71
+ assertWrite: (method: string | symbol) => void,
72
+ ): Headers {
73
+ return new Proxy(headers, {
74
+ get(target, prop, receiver) {
75
+ const value = Reflect.get(target, prop, receiver);
76
+ if (typeof value === "function") {
77
+ if (MUTATING_HEADERS_METHODS.has(prop as string)) {
78
+ return (...args: any[]) => {
79
+ assertWrite(prop);
80
+ return value.apply(target, args);
81
+ };
82
+ }
83
+ return value.bind(target);
84
+ }
85
+ return value;
86
+ },
87
+ });
88
+ }
89
+
32
90
  /**
33
91
  * Strip internal _rsc* query params from a URL.
34
92
  * Returns a new URL with only user-facing params.
@@ -224,23 +282,10 @@ export function createHandlerContext<TEnv>(
224
282
  // assertCachedHeaderWriteAllowed, the unified #713 guard).
225
283
  // Uses lazy `ctx` reference (assigned below) — only the specific handler ctx
226
284
  // is stamped by cache-runtime, not the shared request context.
227
- // MUTATING_HEADERS_METHODS is hoisted to module scope (constant, read-only).
228
285
  let ctx: InternalHandlerContext<any, TEnv>;
229
- const guardedHeaders = new Proxy(stubResponse.headers, {
230
- get(target, prop, receiver) {
231
- const value = Reflect.get(target, prop, receiver);
232
- if (typeof value === "function") {
233
- if (MUTATING_HEADERS_METHODS.has(prop as string)) {
234
- return (...args: any[]) => {
235
- assertNotInsideCacheExec(ctx, "headers");
236
- assertCachedHeaderWriteAllowed("ctx.headers", prop);
237
- return value.apply(target, args);
238
- };
239
- }
240
- return value.bind(target);
241
- }
242
- return value;
243
- },
286
+ const guardedHeaders = guardHeaderWrites(stubResponse.headers, (prop) => {
287
+ assertNotInsideCacheExec(ctx, "headers");
288
+ assertCachedHeaderWriteAllowed("ctx.headers", prop);
244
289
  });
245
290
 
246
291
  ctx = {
@@ -263,14 +308,10 @@ export function createHandlerContext<TEnv>(
263
308
  executionContext: requestContext?.executionContext,
264
309
  _variables: variables,
265
310
  get: ((keyOrVar: any) => {
266
- // Read-time guard: non-cacheable var inside cache() → throw.
267
- // Works for both ContextVar tokens and string keys.
268
- if (isNonCacheable(variables, keyOrVar) && isInsideCacheScope()) {
269
- throw new Error(
270
- `ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
271
- `The variable was created with { cache: false } or set with { cache: false }, ` +
272
- `and its value would be stale on cache hit. Move the read outside the cached scope.`,
273
- );
311
+ // Read-time guard: non-cacheable var inside "use cache" or cache() →
312
+ // throw. Works for both ContextVar tokens and string keys.
313
+ if (isNonCacheable(variables, keyOrVar)) {
314
+ assertNonCacheableReadAllowed(keyOrVar, _getRequestContext());
274
315
  }
275
316
  return contextGet(variables, keyOrVar);
276
317
  }) as HandlerContext<any, TEnv>["get"],
@@ -286,8 +327,7 @@ export function createHandlerContext<TEnv>(
286
327
  use: () => {
287
328
  throw new Error("ctx.use() called before loaders were initialized");
288
329
  },
289
- // Theme support (when enabled via router config)
290
- theme: requestContext?.theme,
330
+ // theme: THEME_DESCRIPTOR, defined after this literal.
291
331
  setTheme: requestContext?.setTheme,
292
332
  // Location state support (delegates to request context)
293
333
  setLocationState(entries) {
@@ -316,6 +356,9 @@ export function createHandlerContext<TEnv>(
316
356
  _responseType: responseType,
317
357
  _routeName: routeName,
318
358
  };
359
+ (ctx as { [THEME_REQUEST_CONTEXT]?: unknown })[THEME_REQUEST_CONTEXT] =
360
+ requestContext;
361
+ Object.defineProperty(ctx, "theme", THEME_DESCRIPTOR);
319
362
  // Brand with taint symbol so "use cache" excludes ctx from cache keys
320
363
  (ctx as any)[NOCACHE_SYMBOL] = true;
321
364
  return ctx;
@@ -28,7 +28,14 @@
28
28
  * is a direct call — no wrapper, no timestamp, no allocation.
29
29
  */
30
30
 
31
- import { _getRequestContext } from "../server/request-context.js";
31
+ import {
32
+ _getRequestContext,
33
+ type RequestContext,
34
+ } from "../server/request-context.js";
35
+ import {
36
+ recordTagOwnersArmed,
37
+ runInSegmentTagScope,
38
+ } from "../cache/cache-tag.js";
32
39
  import { isAutoGeneratedRouteName } from "../route-name.js";
33
40
  import { getRouterContext } from "./router-context.js";
34
41
  import { resolveSink, safeEmit, type TelemetryEvent } from "./telemetry.js";
@@ -352,11 +359,56 @@ export function observeHandler<C, R>(
352
359
  handler: (ctx: C) => R,
353
360
  ctx: C,
354
361
  ): R {
355
- const tracing = _getRequestContext()?._tracing;
362
+ return observeHandlerIn(_getRequestContext(), id, handler, ctx);
363
+ }
364
+
365
+ function observeHandlerIn<C, R>(
366
+ reqCtx: RequestContext | undefined,
367
+ id: string,
368
+ handler: (ctx: C) => R,
369
+ ctx: C,
370
+ ): R {
371
+ const tracing = reqCtx?._tracing;
356
372
  if (!tracing || tracing.phases.handler === false) return handler(ctx);
357
373
  return observePhase(PHASES.handler(id), () => handler(ctx));
358
374
  }
359
375
 
376
+ /**
377
+ * observeHandler, inside segment `segmentId`'s tag scope, so the tags the
378
+ * handler records reach the route's cache() record (#957, cache-tag.ts
379
+ * runInSegmentTagScope). `spanId` is observeHandler's `id`. A request that
380
+ * cannot write a record takes observeHandler's path as is, with the same one
381
+ * request-context read: no scope, no closure.
382
+ */
383
+ export function observeSegmentHandler<C, R>(
384
+ segmentId: string,
385
+ spanId: string,
386
+ handler: (ctx: C) => R,
387
+ ctx: C,
388
+ ): R {
389
+ const reqCtx = _getRequestContext();
390
+ if (!recordTagOwnersArmed(reqCtx)) {
391
+ return observeHandlerIn(reqCtx, spanId, handler, ctx);
392
+ }
393
+ return observeInSegmentScope(segmentId, reqCtx, spanId, handler, ctx);
394
+ }
395
+
396
+ // Separate from observeSegmentHandler: a closure there would make V8
397
+ // allocate its captured-variable context on every call, armed or not.
398
+ function observeInSegmentScope<C, R>(
399
+ segmentId: string,
400
+ reqCtx: RequestContext | undefined,
401
+ spanId: string,
402
+ handler: (ctx: C) => R,
403
+ ctx: C,
404
+ ): R {
405
+ return runInSegmentTagScope(
406
+ segmentId,
407
+ (c) => observeHandlerIn(reqCtx, spanId, handler, c),
408
+ ctx,
409
+ );
410
+ }
411
+
360
412
  /**
361
413
  * Emit one discrete telemetry event (the event-shaped counterpart to
362
414
  * observePhase). Resolves the sink from the active router context and stamps the
@@ -26,8 +26,13 @@ import {
26
26
  warnOnStreamedResponse,
27
27
  buildLoaderErrorContext,
28
28
  } from "./segment-resolution.js";
29
+ import { catchSegmentError } from "./segment-resolution/helpers.js";
30
+ import { findInOrphans } from "./error-handling.js";
31
+ import { resolveLoaderData } from "./segment-resolution/loader-cache.js";
29
32
  import type { SegmentResolutionDeps } from "./types.js";
30
33
  import { debugLog } from "./logging.js";
34
+ import { getRouterContext } from "./router-context.js";
35
+ import { runInSegmentTagScope } from "../cache/cache-tag.js";
31
36
  import {
32
37
  RangoContext,
33
38
  latchPprHeaderScopeForEntries,
@@ -101,15 +106,20 @@ export function collectInterceptTargetNames(
101
106
  fromEntry: EntryData | null,
102
107
  ): string[] {
103
108
  const names = new Set<string>();
109
+ // Tolerate partial entries (unit-test mocks omit the arrays); real
110
+ // registration always populates layout/intercept.
111
+ const add = (source: EntryData): undefined => {
112
+ for (const intercept of source.intercept ?? []) {
113
+ names.add(intercept.routeName);
114
+ }
115
+ return undefined;
116
+ };
104
117
  let current: EntryData | null = fromEntry;
118
+ let prev: EntryData | null = null;
105
119
  while (current) {
106
- // Tolerate partial entries (unit-test mocks omit the arrays); real
107
- // registration always populates layout/intercept.
108
- for (const source of [current, ...(current.layout ?? [])]) {
109
- for (const intercept of source.intercept ?? []) {
110
- names.add(intercept.routeName);
111
- }
112
- }
120
+ add(current);
121
+ findInOrphans(current, add, prev);
122
+ prev = current;
113
123
  current = current.parent;
114
124
  }
115
125
  return [...names];
@@ -121,27 +131,62 @@ export function findInterceptForRoute(
121
131
  selectorContext: InterceptSelectorContext | null = null,
122
132
  isAction: boolean = false,
123
133
  ): { intercept: InterceptEntry; entry: EntryData } | null {
134
+ const match = (source: EntryData) => {
135
+ for (const intercept of source.intercept) {
136
+ if (
137
+ intercept.routeName === targetRouteKey &&
138
+ evaluateInterceptWhen(intercept, selectorContext, isAction)
139
+ ) {
140
+ return { intercept, entry: source };
141
+ }
142
+ }
143
+ return undefined;
144
+ };
124
145
  let current: EntryData | null = fromEntry;
146
+ let prev: EntryData | null = null;
125
147
 
126
148
  while (current) {
127
- // current first, then its sibling layouts — same order as before.
128
- for (const source of [current, ...current.layout]) {
129
- for (const intercept of source.intercept) {
130
- if (
131
- intercept.routeName === targetRouteKey &&
132
- evaluateInterceptWhen(intercept, selectorContext, isAction)
133
- ) {
134
- return { intercept, entry: source };
135
- }
136
- }
137
- }
149
+ // current first, then its orphans (nested ones included) in render order.
150
+ const found = match(current) ?? findInOrphans(current, match, prev);
151
+ if (found) return found;
138
152
 
153
+ prev = current;
139
154
  current = current.parent;
140
155
  }
141
156
 
142
157
  return null;
143
158
  }
144
159
 
160
+ /**
161
+ * An intercept handler's throw or notFound() renders the declaring entry's
162
+ * boundary as the slot component (catchSegmentError, as for route handlers on
163
+ * partial navigations: 500/404 status, onError "handler"). Responses and every
164
+ * error on the background re-render (skipMiddleware) rethrow, so a failed
165
+ * re-render aborts its cache write instead of storing the fallback.
166
+ */
167
+ function renderInterceptHandlerError<TEnv>(
168
+ error: unknown,
169
+ parentEntry: EntryData,
170
+ params: Record<string, string>,
171
+ context: HandlerContext<any, TEnv>,
172
+ deps: SegmentResolutionDeps<TEnv>,
173
+ skipMiddleware: boolean | undefined,
174
+ ): ReactNode {
175
+ if (error instanceof Response || skipMiddleware) throw error;
176
+ return catchSegmentError(
177
+ error,
178
+ parentEntry,
179
+ params,
180
+ deps,
181
+ {
182
+ ...buildLoaderErrorContext(context),
183
+ isPartial: true,
184
+ telemetry: getRouterContext()?.telemetry,
185
+ },
186
+ context.pathname,
187
+ ).component;
188
+ }
189
+
145
190
  /**
146
191
  * Resolve an intercept entry and emit segment with the slot name.
147
192
  */
@@ -210,8 +255,8 @@ export async function resolveInterceptEntry<TEnv>(
210
255
  const loaderIds: string[] = [];
211
256
 
212
257
  for (let i = 0; i < interceptEntry.loader.length; i++) {
213
- const { loader, revalidate: loaderRevalidateFns } =
214
- interceptEntry.loader[i];
258
+ const loaderEntry = interceptEntry.loader[i];
259
+ const { loader, revalidate: loaderRevalidateFns } = loaderEntry;
215
260
  const segmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}D${i}.${loader.$$id}`;
216
261
 
217
262
  if (revalidationContext) {
@@ -273,7 +318,11 @@ export async function resolveInterceptEntry<TEnv>(
273
318
  loaderIds.push(loader.$$id);
274
319
  loaderPromises.push(
275
320
  deps.wrapLoaderPromise(
276
- runInsideLoaderScope(() => context.use(loader)),
321
+ // The DSL binding's funnel, as for route loaders: a loader's own
322
+ // cache() applies here too.
323
+ runInsideLoaderScope(() =>
324
+ resolveLoaderData(loaderEntry, context, context.pathname),
325
+ ),
277
326
  parentEntry,
278
327
  segmentId,
279
328
  context.pathname,
@@ -284,26 +333,61 @@ export async function resolveInterceptEntry<TEnv>(
284
333
  );
285
334
  }
286
335
 
287
- const handlerResult =
288
- typeof interceptEntry.handler === "function"
289
- ? handleHandlerResult(interceptEntry.handler(context))
290
- : interceptEntry.handler;
336
+ // The intercept segment's id, and the tag scope its handler and layout
337
+ // record in (#957, cache-tag.ts runInSegmentTagScope).
338
+ const slotSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
339
+ let handlerResult: ReactNode;
340
+ try {
341
+ const handler = interceptEntry.handler;
342
+ handlerResult =
343
+ typeof handler === "function"
344
+ ? handleHandlerResult(
345
+ runInSegmentTagScope(slotSegmentId, handler, context),
346
+ )
347
+ : handler;
348
+ } catch (error) {
349
+ handlerResult = renderInterceptHandlerError(
350
+ error,
351
+ parentEntry,
352
+ params,
353
+ context,
354
+ deps,
355
+ options?.skipMiddleware,
356
+ );
357
+ }
358
+ // Consumed only after the layout and loader awaits below; a rejection in that
359
+ // window would be an unhandledRejection (process crash on Node's default
360
+ // mode). The later await / trackHandler still observes it.
361
+ if (handlerResult instanceof Promise) handlerResult.catch(() => {});
291
362
 
292
363
  let layoutElement: ReactNode | undefined;
293
364
  if (interceptEntry.layout) {
294
- if (typeof interceptEntry.layout === "function") {
295
- const layoutResult = await interceptEntry.layout(context);
365
+ const layout = interceptEntry.layout;
366
+ if (typeof layout === "function") {
367
+ const layoutResult = await runInSegmentTagScope(
368
+ slotSegmentId,
369
+ layout,
370
+ context,
371
+ );
296
372
  if (layoutResult instanceof Response) {
297
373
  throw layoutResult;
298
374
  }
299
375
  layoutElement = layoutResult;
300
376
  } else {
301
- layoutElement = interceptEntry.layout;
377
+ layoutElement = layout;
302
378
  }
303
379
  }
304
380
 
305
381
  let component: ReactNode;
306
382
  let loaderDataPromise: Promise<any[]> | any[] | undefined;
383
+ // A streamed (loading()) handler resolves after the 200 has started, so its
384
+ // rejection stays with the client; track it like route handlers do
385
+ // (onError report, handle-store completion).
386
+ const trackStreamedHandler = (promise: Promise<unknown>): ReactNode =>
387
+ deps.trackHandler(promise, {
388
+ segmentId: `${parentEntry.shortCode}.${interceptEntry.slotName}`,
389
+ segmentType: "parallel",
390
+ }) as ReactNode;
307
391
 
308
392
  if (interceptEntry.loading && loaderPromises.length > 0) {
309
393
  if (handlerResult instanceof Promise) {
@@ -314,24 +398,34 @@ export async function resolveInterceptEntry<TEnv>(
314
398
  }
315
399
  component =
316
400
  handlerResult instanceof Promise
317
- ? handlerResult
401
+ ? trackStreamedHandler(handlerResult)
318
402
  : (Promise.resolve(handlerResult) as ReactNode);
319
403
  loaderDataPromise = Promise.all(loaderPromises);
320
- } else if (loaderPromises.length > 0) {
321
- loaderDataPromise = await Promise.all(loaderPromises);
322
- component =
323
- handlerResult instanceof Promise ? await handlerResult : handlerResult;
404
+ } else if (interceptEntry.loading && handlerResult instanceof Promise) {
405
+ component = trackStreamedHandler(handlerResult);
324
406
  } else {
325
- component =
326
- interceptEntry.loading && handlerResult instanceof Promise
327
- ? handlerResult
328
- : handlerResult instanceof Promise
329
- ? await handlerResult
330
- : handlerResult;
407
+ if (loaderPromises.length > 0) {
408
+ loaderDataPromise = await Promise.all(loaderPromises);
409
+ }
410
+ component = handlerResult;
411
+ if (handlerResult instanceof Promise) {
412
+ try {
413
+ component = await handlerResult;
414
+ } catch (error) {
415
+ component = renderInterceptHandlerError(
416
+ error,
417
+ parentEntry,
418
+ params,
419
+ context,
420
+ deps,
421
+ options?.skipMiddleware,
422
+ );
423
+ }
424
+ }
331
425
  }
332
426
 
333
427
  const interceptSegment = {
334
- id: `${parentEntry.shortCode}.${interceptEntry.slotName}`,
428
+ id: slotSegmentId,
335
429
  namespace: `intercept:${interceptEntry.routeName}`,
336
430
  type: "parallel" as const,
337
431
  index: 0,
@@ -401,8 +495,8 @@ export async function resolveInterceptLoadersOnly<TEnv>(
401
495
  } = revalidationContext;
402
496
 
403
497
  for (let i = 0; i < interceptEntry.loader.length; i++) {
404
- const { loader, revalidate: loaderRevalidateFns } =
405
- interceptEntry.loader[i];
498
+ const loaderEntry = interceptEntry.loader[i];
499
+ const { loader, revalidate: loaderRevalidateFns } = loaderEntry;
406
500
  const segmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}D${i}.${loader.$$id}`;
407
501
 
408
502
  const interceptSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
@@ -451,7 +545,11 @@ export async function resolveInterceptLoadersOnly<TEnv>(
451
545
  loaderIds.push(loader.$$id);
452
546
  loaderPromises.push(
453
547
  deps.wrapLoaderPromise(
454
- runInsideLoaderScope(() => context.use(loader)),
548
+ // The DSL binding's funnel, as for route loaders: a loader's own
549
+ // cache() applies here too.
550
+ runInsideLoaderScope(() =>
551
+ resolveLoaderData(loaderEntry, context, context.pathname),
552
+ ),
455
553
  parentEntry,
456
554
  segmentId,
457
555
  context.pathname,