@rangojs/router 0.0.0-experimental.31 → 0.0.0-experimental.3232cd17

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 (376) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +198 -44
  3. package/dist/bin/rango.js +287 -105
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3248 -1117
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +73 -21
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +107 -1
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +245 -21
  12. package/skills/caching/SKILL.md +302 -6
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +364 -0
  17. package/skills/hooks/SKILL.md +270 -30
  18. package/skills/host-router/SKILL.md +82 -22
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +49 -5
  21. package/skills/layout/SKILL.md +35 -9
  22. package/skills/links/SKILL.md +249 -17
  23. package/skills/loader/SKILL.md +294 -30
  24. package/skills/middleware/SKILL.md +52 -13
  25. package/skills/migrate-nextjs/SKILL.md +584 -0
  26. package/skills/migrate-react-router/SKILL.md +769 -0
  27. package/skills/mime-routes/SKILL.md +27 -0
  28. package/skills/observability/SKILL.md +137 -0
  29. package/skills/parallel/SKILL.md +203 -7
  30. package/skills/prerender/SKILL.md +123 -100
  31. package/skills/rango/SKILL.md +250 -22
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +122 -47
  34. package/skills/route/SKILL.md +97 -5
  35. package/skills/router-setup/SKILL.md +90 -5
  36. package/skills/server-actions/SKILL.md +775 -0
  37. package/skills/streams-and-websockets/SKILL.md +283 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +129 -0
  40. package/skills/testing/bindings.md +89 -0
  41. package/skills/testing/cache-prerender.md +124 -0
  42. package/skills/testing/client-components.md +122 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +92 -0
  45. package/skills/testing/handles.md +129 -0
  46. package/skills/testing/loader.md +128 -0
  47. package/skills/testing/middleware.md +99 -0
  48. package/skills/testing/render-handler.md +121 -0
  49. package/skills/testing/response-routes.md +95 -0
  50. package/skills/testing/reverse-and-types.md +84 -0
  51. package/skills/testing/server-actions.md +107 -0
  52. package/skills/testing/server-tree.md +128 -0
  53. package/skills/testing/setup.md +120 -0
  54. package/skills/typesafety/SKILL.md +329 -27
  55. package/skills/use-cache/SKILL.md +36 -5
  56. package/skills/view-transitions/SKILL.md +294 -0
  57. package/src/__augment-tests__/augment.ts +81 -0
  58. package/src/__augment-tests__/augmented.check.ts +116 -0
  59. package/src/__internal.ts +67 -40
  60. package/src/browser/action-coordinator.ts +53 -36
  61. package/src/browser/action-fence.ts +47 -0
  62. package/src/browser/app-shell.ts +39 -0
  63. package/src/browser/app-version.ts +14 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +86 -147
  66. package/src/browser/history-state.ts +21 -0
  67. package/src/browser/index.ts +3 -3
  68. package/src/browser/invalidate-client-cache.ts +52 -0
  69. package/src/browser/link-interceptor.ts +4 -0
  70. package/src/browser/navigation-bridge.ts +148 -19
  71. package/src/browser/navigation-client.ts +187 -67
  72. package/src/browser/navigation-store-handle.ts +38 -0
  73. package/src/browser/navigation-store.ts +76 -67
  74. package/src/browser/navigation-transaction.ts +18 -66
  75. package/src/browser/partial-update.ts +123 -94
  76. package/src/browser/prefetch/cache.ts +214 -36
  77. package/src/browser/prefetch/fetch.ts +260 -38
  78. package/src/browser/prefetch/policy.ts +6 -0
  79. package/src/browser/prefetch/queue.ts +126 -20
  80. package/src/browser/prefetch/resource-ready.ts +77 -0
  81. package/src/browser/rango-state.ts +158 -76
  82. package/src/browser/react/Link.tsx +93 -11
  83. package/src/browser/react/NavigationProvider.tsx +115 -34
  84. package/src/browser/react/ScrollRestoration.tsx +10 -6
  85. package/src/browser/react/context.ts +7 -2
  86. package/src/browser/react/filter-segment-order.ts +49 -7
  87. package/src/browser/react/index.ts +0 -48
  88. package/src/browser/react/location-state-shared.ts +166 -8
  89. package/src/browser/react/location-state.ts +39 -14
  90. package/src/browser/react/use-action.ts +6 -15
  91. package/src/browser/react/use-handle.ts +23 -69
  92. package/src/browser/react/use-link-status.ts +0 -4
  93. package/src/browser/react/use-navigation.ts +22 -5
  94. package/src/browser/react/use-params.ts +20 -10
  95. package/src/browser/react/use-reverse.ts +106 -0
  96. package/src/browser/react/use-router.ts +46 -11
  97. package/src/browser/react/use-search-params.ts +0 -5
  98. package/src/browser/react/use-segments.ts +11 -21
  99. package/src/browser/response-adapter.ts +52 -1
  100. package/src/browser/rsc-router.tsx +215 -76
  101. package/src/browser/scroll-restoration.ts +46 -39
  102. package/src/browser/segment-reconciler.ts +36 -9
  103. package/src/browser/segment-structure-assert.ts +2 -2
  104. package/src/browser/server-action-bridge.ts +176 -50
  105. package/src/browser/types.ts +95 -11
  106. package/src/browser/validate-redirect-origin.ts +43 -16
  107. package/src/build/collect-fallback-refs.ts +107 -0
  108. package/src/build/generate-manifest.ts +65 -40
  109. package/src/build/generate-route-types.ts +5 -0
  110. package/src/build/index.ts +8 -2
  111. package/src/build/prefix-tree-utils.ts +123 -0
  112. package/src/build/route-trie.ts +137 -32
  113. package/src/build/route-types/codegen.ts +4 -4
  114. package/src/build/route-types/include-resolution.ts +9 -2
  115. package/src/build/route-types/param-extraction.ts +6 -3
  116. package/src/build/route-types/per-module-writer.ts +7 -4
  117. package/src/build/route-types/router-processing.ts +278 -96
  118. package/src/build/route-types/scan-filter.ts +9 -2
  119. package/src/build/route-types/source-scan.ts +118 -0
  120. package/src/build/runtime-discovery.ts +9 -20
  121. package/src/cache/cache-error.ts +104 -0
  122. package/src/cache/cache-policy.ts +68 -28
  123. package/src/cache/cache-runtime.ts +149 -43
  124. package/src/cache/cache-scope.ts +148 -81
  125. package/src/cache/cache-tag.ts +98 -0
  126. package/src/cache/cf/cf-cache-store.ts +2550 -93
  127. package/src/cache/cf/index.ts +11 -17
  128. package/src/cache/document-cache.ts +78 -27
  129. package/src/cache/handle-snapshot.ts +63 -0
  130. package/src/cache/index.ts +23 -20
  131. package/src/cache/memory-segment-store.ts +136 -37
  132. package/src/cache/profile-registry.ts +6 -30
  133. package/src/cache/read-through-swr.ts +41 -11
  134. package/src/cache/segment-codec.ts +0 -16
  135. package/src/cache/tag-invalidation.ts +230 -0
  136. package/src/cache/taint.ts +55 -0
  137. package/src/cache/types.ts +33 -100
  138. package/src/cache/vercel/index.ts +11 -0
  139. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  140. package/src/client.rsc.tsx +6 -21
  141. package/src/client.tsx +108 -290
  142. package/src/component-utils.ts +19 -0
  143. package/src/context-var.ts +84 -2
  144. package/src/debug.ts +2 -2
  145. package/src/decode-loader-results.ts +36 -0
  146. package/src/defer.ts +196 -0
  147. package/src/deps/ssr.ts +0 -1
  148. package/src/errors.ts +30 -4
  149. package/src/handle.ts +70 -22
  150. package/src/handles/MetaTags.tsx +0 -14
  151. package/src/handles/breadcrumbs.ts +16 -5
  152. package/src/handles/meta.ts +0 -39
  153. package/src/host/cookie-handler.ts +0 -36
  154. package/src/host/errors.ts +0 -24
  155. package/src/host/index.ts +8 -2
  156. package/src/host/pattern-matcher.ts +7 -50
  157. package/src/host/router.ts +107 -99
  158. package/src/host/testing.ts +40 -27
  159. package/src/host/types.ts +37 -4
  160. package/src/host/utils.ts +1 -1
  161. package/src/href-client.ts +137 -22
  162. package/src/index.rsc.ts +52 -26
  163. package/src/index.ts +100 -38
  164. package/src/internal-debug.ts +2 -4
  165. package/src/loader-store.ts +500 -0
  166. package/src/loader.rsc.ts +20 -13
  167. package/src/loader.ts +12 -11
  168. package/src/missing-id-error.ts +68 -0
  169. package/src/network-error-thrower.tsx +1 -6
  170. package/src/outlet-context.ts +1 -1
  171. package/src/outlet-provider.tsx +1 -5
  172. package/src/prerender/param-hash.ts +10 -11
  173. package/src/prerender/store.ts +37 -41
  174. package/src/prerender.ts +198 -82
  175. package/src/redirect-origin.ts +100 -0
  176. package/src/response-utils.ts +37 -0
  177. package/src/reverse.ts +65 -15
  178. package/src/root-error-boundary.tsx +1 -19
  179. package/src/route-content-wrapper.tsx +7 -72
  180. package/src/route-definition/dsl-helpers.ts +437 -274
  181. package/src/route-definition/helper-factories.ts +29 -139
  182. package/src/route-definition/helpers-types.ts +113 -37
  183. package/src/route-definition/index.ts +3 -0
  184. package/src/route-definition/redirect.ts +52 -10
  185. package/src/route-definition/resolve-handler-use.ts +161 -0
  186. package/src/route-definition/use-item-types.ts +32 -0
  187. package/src/route-map-builder.ts +7 -17
  188. package/src/route-types.ts +37 -41
  189. package/src/router/basename.ts +14 -0
  190. package/src/router/content-negotiation.ts +108 -9
  191. package/src/router/error-handling.ts +13 -17
  192. package/src/router/find-match.ts +45 -22
  193. package/src/router/handler-context.ts +83 -41
  194. package/src/router/intercept-resolution.ts +25 -23
  195. package/src/router/lazy-includes.ts +19 -53
  196. package/src/router/loader-resolution.ts +213 -30
  197. package/src/router/logging.ts +5 -8
  198. package/src/router/manifest.ts +49 -45
  199. package/src/router/match-api.ts +121 -205
  200. package/src/router/match-context.ts +0 -22
  201. package/src/router/match-handlers.ts +58 -58
  202. package/src/router/match-middleware/background-revalidation.ts +27 -6
  203. package/src/router/match-middleware/cache-lookup.ts +205 -249
  204. package/src/router/match-middleware/cache-store.ts +45 -32
  205. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  206. package/src/router/match-middleware/segment-resolution.ts +52 -18
  207. package/src/router/match-pipelines.ts +1 -42
  208. package/src/router/match-result.ts +104 -40
  209. package/src/router/metrics.ts +5 -34
  210. package/src/router/middleware-types.ts +13 -142
  211. package/src/router/middleware.ts +173 -143
  212. package/src/router/navigation-snapshot.ts +131 -0
  213. package/src/router/params-util.ts +23 -0
  214. package/src/router/pattern-matching.ts +109 -63
  215. package/src/router/prerender-match.ts +192 -54
  216. package/src/router/preview-match.ts +32 -102
  217. package/src/router/request-classification.ts +276 -0
  218. package/src/router/revalidation.ts +63 -55
  219. package/src/router/route-snapshot.ts +244 -0
  220. package/src/router/router-context.ts +6 -28
  221. package/src/router/router-interfaces.ts +100 -35
  222. package/src/router/router-options.ts +91 -11
  223. package/src/router/router-registry.ts +2 -5
  224. package/src/router/segment-resolution/fresh.ts +242 -75
  225. package/src/router/segment-resolution/helpers.ts +64 -25
  226. package/src/router/segment-resolution/loader-cache.ts +41 -37
  227. package/src/router/segment-resolution/revalidation.ts +456 -372
  228. package/src/router/segment-resolution/static-store.ts +19 -5
  229. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  230. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  231. package/src/router/segment-resolution.ts +4 -1
  232. package/src/router/segment-wrappers.ts +2 -3
  233. package/src/router/state-cookie-name.ts +33 -0
  234. package/src/router/substitute-pattern-params.ts +56 -0
  235. package/src/router/telemetry-otel.ts +0 -20
  236. package/src/router/telemetry.ts +96 -19
  237. package/src/router/timeout.ts +0 -20
  238. package/src/router/trie-matching.ts +91 -46
  239. package/src/router/types.ts +10 -63
  240. package/src/router/url-params.ts +44 -0
  241. package/src/router.ts +134 -43
  242. package/src/rsc/handler-context.ts +3 -2
  243. package/src/rsc/handler.ts +492 -383
  244. package/src/rsc/helpers.ts +162 -46
  245. package/src/rsc/index.ts +1 -1
  246. package/src/rsc/json-route-result.ts +38 -0
  247. package/src/rsc/loader-fetch.ts +23 -3
  248. package/src/rsc/manifest-init.ts +33 -42
  249. package/src/rsc/origin-guard.ts +39 -25
  250. package/src/rsc/progressive-enhancement.ts +30 -3
  251. package/src/rsc/redirect-guard.ts +99 -0
  252. package/src/rsc/response-error.ts +79 -12
  253. package/src/rsc/response-route-handler.ts +90 -63
  254. package/src/rsc/rsc-rendering.ts +56 -54
  255. package/src/rsc/runtime-warnings.ts +23 -10
  256. package/src/rsc/server-action.ts +74 -67
  257. package/src/rsc/ssr-setup.ts +18 -2
  258. package/src/rsc/types.ts +25 -6
  259. package/src/runtime-env.ts +18 -0
  260. package/src/search-params.ts +4 -20
  261. package/src/segment-content-promise.ts +67 -0
  262. package/src/segment-loader-promise.ts +134 -0
  263. package/src/segment-system.tsx +272 -129
  264. package/src/serialize.ts +243 -0
  265. package/src/server/context.ts +309 -61
  266. package/src/server/cookie-store.ts +80 -5
  267. package/src/server/handle-store.ts +26 -24
  268. package/src/server/loader-registry.ts +10 -28
  269. package/src/server/request-context.ts +348 -128
  270. package/src/ssr/index.tsx +23 -15
  271. package/src/static-handler.ts +27 -18
  272. package/src/testing/cache-status.ts +162 -0
  273. package/src/testing/collect-handle.ts +40 -0
  274. package/src/testing/dispatch.ts +618 -0
  275. package/src/testing/dom.entry.ts +22 -0
  276. package/src/testing/e2e/fixture.ts +188 -0
  277. package/src/testing/e2e/index.ts +128 -0
  278. package/src/testing/e2e/matchers.ts +35 -0
  279. package/src/testing/e2e/page-helpers.ts +272 -0
  280. package/src/testing/e2e/parity.ts +387 -0
  281. package/src/testing/e2e/server.ts +195 -0
  282. package/src/testing/flight-matchers.ts +97 -0
  283. package/src/testing/flight-normalize.ts +11 -0
  284. package/src/testing/flight-runtime.d.ts +57 -0
  285. package/src/testing/flight-tree.ts +682 -0
  286. package/src/testing/flight.entry.ts +52 -0
  287. package/src/testing/flight.ts +232 -0
  288. package/src/testing/generated-routes.ts +183 -0
  289. package/src/testing/index.ts +99 -0
  290. package/src/testing/internal/context.ts +348 -0
  291. package/src/testing/internal/flight-client-globals.ts +30 -0
  292. package/src/testing/internal/seed-vars.ts +54 -0
  293. package/src/testing/render-handler.ts +330 -0
  294. package/src/testing/render-route.tsx +566 -0
  295. package/src/testing/run-loader.ts +378 -0
  296. package/src/testing/run-middleware.ts +205 -0
  297. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  298. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  299. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  300. package/src/testing/vitest-stubs/version.ts +5 -0
  301. package/src/testing/vitest.ts +305 -0
  302. package/src/theme/ThemeProvider.tsx +0 -52
  303. package/src/theme/ThemeScript.tsx +0 -6
  304. package/src/theme/constants.ts +0 -12
  305. package/src/theme/index.ts +0 -7
  306. package/src/theme/theme-context.ts +1 -5
  307. package/src/theme/theme-script.ts +0 -14
  308. package/src/theme/use-theme.ts +0 -3
  309. package/src/types/boundaries.ts +0 -35
  310. package/src/types/cache-types.ts +17 -8
  311. package/src/types/error-types.ts +30 -90
  312. package/src/types/global-namespace.ts +54 -41
  313. package/src/types/handler-context.ts +233 -81
  314. package/src/types/index.ts +1 -10
  315. package/src/types/loader-types.ts +44 -15
  316. package/src/types/request-scope.ts +107 -0
  317. package/src/types/route-config.ts +6 -50
  318. package/src/types/route-entry.ts +19 -7
  319. package/src/types/segments.ts +37 -14
  320. package/src/urls/include-helper.ts +33 -70
  321. package/src/urls/index.ts +1 -11
  322. package/src/urls/path-helper-types.ts +58 -11
  323. package/src/urls/path-helper.ts +57 -111
  324. package/src/urls/pattern-types.ts +48 -19
  325. package/src/urls/response-types.ts +25 -22
  326. package/src/urls/type-extraction.ts +58 -139
  327. package/src/urls/urls-function.ts +1 -18
  328. package/src/use-loader.tsx +346 -89
  329. package/src/vite/debug.ts +185 -0
  330. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  331. package/src/vite/discovery/discover-routers.ts +130 -85
  332. package/src/vite/discovery/discovery-errors.ts +194 -0
  333. package/src/vite/discovery/gate-state.ts +171 -0
  334. package/src/vite/discovery/prerender-collection.ts +192 -99
  335. package/src/vite/discovery/route-types-writer.ts +40 -84
  336. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  337. package/src/vite/discovery/state.ts +51 -6
  338. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  339. package/src/vite/index.ts +8 -0
  340. package/src/vite/plugin-types.ts +187 -69
  341. package/src/vite/plugins/cjs-to-esm.ts +8 -18
  342. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  343. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  344. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  345. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  346. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  347. package/src/vite/plugins/expose-action-id.ts +49 -98
  348. package/src/vite/plugins/expose-id-utils.ts +11 -50
  349. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  350. package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
  351. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  352. package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
  353. package/src/vite/plugins/expose-internal-ids.ts +554 -317
  354. package/src/vite/plugins/performance-tracks.ts +89 -0
  355. package/src/vite/plugins/refresh-cmd.ts +89 -27
  356. package/src/vite/plugins/use-cache-transform.ts +73 -83
  357. package/src/vite/plugins/vercel-output.ts +258 -0
  358. package/src/vite/plugins/version-injector.ts +21 -25
  359. package/src/vite/plugins/version-plugin.ts +41 -20
  360. package/src/vite/plugins/virtual-entries.ts +2 -17
  361. package/src/vite/rango.ts +257 -289
  362. package/src/vite/router-discovery.ts +930 -140
  363. package/src/vite/utils/ast-handler-extract.ts +15 -31
  364. package/src/vite/utils/banner.ts +4 -4
  365. package/src/vite/utils/bundle-analysis.ts +10 -15
  366. package/src/vite/utils/client-chunks.ts +184 -0
  367. package/src/vite/utils/forward-user-plugins.ts +171 -0
  368. package/src/vite/utils/manifest-utils.ts +4 -59
  369. package/src/vite/utils/package-resolution.ts +20 -52
  370. package/src/vite/utils/prerender-utils.ts +27 -29
  371. package/src/vite/utils/shared-utils.ts +92 -42
  372. package/src/browser/action-response-classifier.ts +0 -99
  373. package/src/browser/react/use-client-cache.ts +0 -58
  374. package/src/browser/shallow.ts +0 -40
  375. package/src/handles/index.ts +0 -7
  376. package/src/router/middleware-cookies.ts +0 -55
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Request Classification
3
+ *
4
+ * Replaces the implicit "preview then match again" model with a clean
5
+ * two-stage architecture:
6
+ *
7
+ * 1. Classification — classifyRequest() produces a RequestPlan that answers
8
+ * all routing questions once: target route, request mode, route middleware,
9
+ * response-route info, negotiation state.
10
+ *
11
+ * 2. Execution — executeRequest() dispatches on the plan to the appropriate
12
+ * handler (response route, loader fetch, full render, partial render,
13
+ * action revalidation, PE render).
14
+ *
15
+ * Builds on RouteSnapshot from route-snapshot.ts.
16
+ */
17
+
18
+ import { RouteNotFoundError } from "../errors.js";
19
+ import type { EntryData } from "../server/context.js";
20
+ import type { CollectedMiddleware } from "./middleware-types.js";
21
+ import type { RouteMatchResult } from "./pattern-matching.js";
22
+ import { negotiateRoute } from "./content-negotiation.js";
23
+ import { stripInternalParams } from "./handler-context.js";
24
+ import { resolveRoute, type RouteSnapshot } from "./route-snapshot.js";
25
+
26
+ interface RedirectPlan<TEnv = any> {
27
+ mode: "redirect";
28
+ route: RouteSnapshot<TEnv>;
29
+ redirectUrl: string;
30
+ }
31
+
32
+ interface VersionMismatchPlan<TEnv = any> {
33
+ mode: "version-mismatch";
34
+ /** May be undefined when version mismatch is detected before route resolution */
35
+ route?: RouteSnapshot<TEnv>;
36
+ reloadUrl: string;
37
+ }
38
+
39
+ interface AppSwitchReloadPlan {
40
+ mode: "app-switch";
41
+ /** Clean target URL (internal _rsc_* params stripped) to navigate to. */
42
+ reloadUrl: string;
43
+ }
44
+
45
+ interface ResponseRoutePlan<TEnv = any> {
46
+ mode: "response";
47
+ route: RouteSnapshot<TEnv>;
48
+ handler: Function;
49
+ responseType: string;
50
+ negotiated: boolean;
51
+ manifestEntry: EntryData;
52
+ routeMiddleware: CollectedMiddleware[];
53
+ }
54
+
55
+ interface LoaderFetchPlan<TEnv = any> {
56
+ mode: "loader";
57
+ route: RouteSnapshot<TEnv>;
58
+ }
59
+
60
+ interface PeRenderPlan<TEnv = any> {
61
+ mode: "pe-render";
62
+ route: RouteSnapshot<TEnv>;
63
+ }
64
+
65
+ interface ActionPlan<TEnv = any> {
66
+ mode: "action";
67
+ route: RouteSnapshot<TEnv>;
68
+ actionId: string;
69
+ negotiated: boolean;
70
+ }
71
+
72
+ interface FullRenderPlan<TEnv = any> {
73
+ mode: "full-render";
74
+ route: RouteSnapshot<TEnv>;
75
+ negotiated: boolean;
76
+ }
77
+
78
+ interface PartialRenderPlan<TEnv = any> {
79
+ mode: "partial-render";
80
+ route: RouteSnapshot<TEnv>;
81
+ negotiated: boolean;
82
+ }
83
+
84
+ /**
85
+ * The output of request classification. A discriminated union where each
86
+ * variant carries exactly the fields needed for its execution path.
87
+ */
88
+ export type RequestPlan<TEnv = any> =
89
+ | RedirectPlan<TEnv>
90
+ | VersionMismatchPlan<TEnv>
91
+ | AppSwitchReloadPlan
92
+ | ResponseRoutePlan<TEnv>
93
+ | LoaderFetchPlan<TEnv>
94
+ | PeRenderPlan<TEnv>
95
+ | ActionPlan<TEnv>
96
+ | FullRenderPlan<TEnv>
97
+ | PartialRenderPlan<TEnv>;
98
+
99
+ /**
100
+ * Plans that have passed the terminal-check gate (version-mismatch and
101
+ * app-switch reloads handled) and are ready for execution. Always have a
102
+ * `route` field.
103
+ */
104
+ export type ExecutableRequestPlan<TEnv = any> = Exclude<
105
+ RequestPlan<TEnv>,
106
+ VersionMismatchPlan<TEnv> | AppSwitchReloadPlan
107
+ >;
108
+
109
+ /**
110
+ * Re-export individual plan types for consumers that need to narrow.
111
+ */
112
+ export type {
113
+ RedirectPlan,
114
+ VersionMismatchPlan,
115
+ ResponseRoutePlan,
116
+ LoaderFetchPlan,
117
+ PeRenderPlan,
118
+ ActionPlan,
119
+ FullRenderPlan,
120
+ PartialRenderPlan,
121
+ };
122
+
123
+ export interface ClassifyRequestDeps<TEnv = any> {
124
+ findMatch: (pathname: string) => RouteMatchResult<TEnv> | null;
125
+ routerVersion: string;
126
+ routerId: string;
127
+ }
128
+
129
+ /**
130
+ * Classify an incoming request into a RequestPlan.
131
+ *
132
+ * This is the single source of truth for request mode detection. It replaces
133
+ * the scattered previewMatch + isAction/isLoaderFetch/isPartial checks in
134
+ * handler.ts.
135
+ *
136
+ * Classification order:
137
+ * 1. Route resolution (findMatch + loadManifest via resolveRoute lite)
138
+ * 2. Redirect detection
139
+ * 3. Version mismatch
140
+ * 4. Response route + content negotiation
141
+ * 5. Mode detection from headers/params
142
+ */
143
+ export async function classifyRequest<TEnv = any>(
144
+ request: Request,
145
+ url: URL,
146
+ deps: ClassifyRequestDeps<TEnv>,
147
+ ): Promise<RequestPlan<TEnv>> {
148
+ const pathname = url.pathname;
149
+ const isAction =
150
+ request.headers.has("rsc-action") || url.searchParams.has("_rsc_action");
151
+
152
+ const clientVersion = url.searchParams.get("_rsc_v");
153
+ if (
154
+ deps.routerVersion &&
155
+ clientVersion &&
156
+ clientVersion !== deps.routerVersion
157
+ ) {
158
+ let reloadUrl = stripInternalParams(url).toString();
159
+ if (isAction) {
160
+ const referer = request.headers.get("referer");
161
+ if (referer) {
162
+ try {
163
+ const refererUrl = new URL(referer);
164
+ if (refererUrl.origin === url.origin) {
165
+ reloadUrl = referer;
166
+ }
167
+ } catch {}
168
+ }
169
+ }
170
+
171
+ return {
172
+ mode: "version-mismatch",
173
+ reloadUrl,
174
+ };
175
+ }
176
+
177
+ const clientRouterId = url.searchParams.get("_rsc_rid");
178
+ if (
179
+ clientRouterId &&
180
+ clientRouterId !== deps.routerId &&
181
+ url.searchParams.has("_rsc_partial")
182
+ ) {
183
+ return {
184
+ mode: "app-switch",
185
+ reloadUrl: stripInternalParams(url).toString(),
186
+ };
187
+ }
188
+
189
+ const result = await resolveRoute<TEnv>(pathname, {
190
+ findMatch: deps.findMatch,
191
+ lite: true,
192
+ });
193
+
194
+ if (!result) {
195
+ throw new RouteNotFoundError(`No route matched for ${pathname}`, {
196
+ cause: { pathname, method: request.method },
197
+ });
198
+ }
199
+
200
+ if (result.type === "redirect") {
201
+ const snapshot: RouteSnapshot<TEnv> = {
202
+ matched: result as any,
203
+ manifestEntry: null as any,
204
+ entries: [],
205
+ routeKey: "",
206
+ localRouteName: "",
207
+ params: {},
208
+ routeMiddleware: [],
209
+ cacheScope: null,
210
+ isPassthrough: false,
211
+ };
212
+ return {
213
+ mode: "redirect",
214
+ route: snapshot,
215
+ redirectUrl: result.redirectTo + url.search,
216
+ };
217
+ }
218
+
219
+ const snapshot = result.snapshot;
220
+
221
+ const responseResult = await classifyResponseRoute(
222
+ request,
223
+ pathname,
224
+ snapshot,
225
+ );
226
+ if (responseResult) {
227
+ return responseResult;
228
+ }
229
+
230
+ const actionId =
231
+ request.headers.get("rsc-action") || url.searchParams.get("_rsc_action");
232
+ const isLoaderFetch = url.searchParams.has("_rsc_loader");
233
+
234
+ const hasVariants =
235
+ snapshot.matched.negotiateVariants &&
236
+ snapshot.matched.negotiateVariants.length > 0;
237
+ const negotiated = !!hasVariants;
238
+
239
+ if (isAction && actionId) {
240
+ return { mode: "action", route: snapshot, actionId, negotiated };
241
+ }
242
+
243
+ if (isLoaderFetch) {
244
+ return { mode: "loader", route: snapshot };
245
+ }
246
+
247
+ const contentType = request.headers.get("content-type") || "";
248
+ const isFormSubmission =
249
+ contentType.includes("multipart/form-data") ||
250
+ contentType.includes("application/x-www-form-urlencoded");
251
+ if (request.method === "POST" && !isAction && isFormSubmission) {
252
+ return { mode: "pe-render", route: snapshot };
253
+ }
254
+
255
+ if (url.searchParams.has("_rsc_partial")) {
256
+ return { mode: "partial-render", route: snapshot, negotiated };
257
+ }
258
+
259
+ return { mode: "full-render", route: snapshot, negotiated };
260
+ }
261
+
262
+ /**
263
+ * Check if the route is a response route and perform content negotiation
264
+ * if negotiate variants exist. Returns a ResponseRoutePlan if the route
265
+ * is a response route, null otherwise (RSC route).
266
+ */
267
+ async function classifyResponseRoute<TEnv>(
268
+ request: Request,
269
+ pathname: string,
270
+ snapshot: RouteSnapshot<TEnv>,
271
+ ): Promise<ResponseRoutePlan<TEnv> | null> {
272
+ const negotiation = await negotiateRoute(request, pathname, snapshot);
273
+ return negotiation
274
+ ? { mode: "response", route: snapshot, ...negotiation }
275
+ : null;
276
+ }
@@ -4,7 +4,7 @@
4
4
  * Evaluates whether segments should revalidate based on params, actions, and custom functions.
5
5
  */
6
6
 
7
- import type { ResolvedSegment, HandlerContext } from "../types";
7
+ import type { ResolvedSegment, HandlerContext, ActionRef } from "../types";
8
8
  import type { ActionContext } from "./types";
9
9
  import {
10
10
  debugLog,
@@ -14,21 +14,52 @@ import {
14
14
  import type { RevalidationTraceEntry } from "./logging.js";
15
15
  import { _getRequestContext } from "../server/request-context.js";
16
16
  import { isAutoGeneratedRouteName } from "../route-name.js";
17
+ import { paramsEqual } from "./params-util.js";
17
18
 
18
- function paramsEqual(
19
- a: Record<string, string>,
20
- b: Record<string, string>,
21
- ): boolean {
22
- if (a === b) return true;
23
-
24
- const keysA = Object.keys(a);
25
- if (keysA.length !== Object.keys(b).length) return false;
26
-
27
- for (const key of keysA) {
28
- if (a[key] !== b[key]) return false;
29
- }
19
+ /**
20
+ * Resolve a server-action reference's stable id, mirroring how the action
21
+ * boundary derives `actionContext.actionId` in `rsc/server-action.ts`
22
+ * (`$id ?? $$id`): the file-path `$id` set by the expose-action-id plugin in a
23
+ * production RSC build when present, otherwise React's `$$id`. Resolving both
24
+ * the incoming `actionId` and the reference with the same precedence makes
25
+ * `isAction()` form-agnostic across dev and production.
26
+ */
27
+ function resolveActionRefId(ref: unknown): string | undefined {
28
+ if (ref == null) return undefined;
29
+ const r = ref as { $id?: unknown; $$id?: unknown };
30
+ if (typeof r.$id === "string") return r.$id;
31
+ if (typeof r.$$id === "string") return r.$$id;
32
+ return undefined;
33
+ }
30
34
 
31
- return true;
35
+ /**
36
+ * Build the `isAction()` helper bound to the current action's id. Called with no
37
+ * arguments it answers "is this request an action at all?" (any action) — `true`
38
+ * during action handling, `false` on plain navigation. Called with one or more
39
+ * action references it narrows to those: a single imported action, several
40
+ * (variadic), or any export of a namespace import (`import * as Mod`). Returns
41
+ * `false` when there is no action (plain navigation) or nothing matches.
42
+ */
43
+ function makeIsAction(
44
+ currentActionId: string | undefined,
45
+ ): (...actions: ActionRef[]) => boolean {
46
+ return (...actions: ActionRef[]): boolean => {
47
+ if (!currentActionId) return false;
48
+ // Bare isAction(): an action is in flight (currentActionId is set) and the
49
+ // caller did not narrow to a specific action, so this is "any action".
50
+ if (actions.length === 0) return true;
51
+ for (const action of actions) {
52
+ if (typeof action === "function") {
53
+ if (resolveActionRefId(action) === currentActionId) return true;
54
+ } else if (action && typeof action === "object") {
55
+ // Namespace import: match any export of the module.
56
+ for (const value of Object.values(action)) {
57
+ if (resolveActionRefId(value) === currentActionId) return true;
58
+ }
59
+ }
60
+ }
61
+ return false;
62
+ };
32
63
  }
33
64
 
34
65
  /**
@@ -59,6 +90,14 @@ interface EvaluateRevalidationOptions<TEnv> {
59
90
  stale?: boolean;
60
91
  /** Trace source hint for the revalidation trace */
61
92
  traceSource?: RevalidationTraceEntry["source"];
93
+ /**
94
+ * Override the segment-type-derived default. When set, the value is used as
95
+ * the seed `defaultShouldRevalidate` passed to user revalidate fns and the
96
+ * reason flows into the trace. Callers use this when client-knowledge
97
+ * (e.g. parallel slot not in clientSegmentIds) should dictate the seed
98
+ * instead of the params/method-based heuristic.
99
+ */
100
+ defaultOverride?: { value: boolean; reason: string };
62
101
  }
63
102
 
64
103
  /**
@@ -81,13 +120,12 @@ export async function evaluateRevalidation<TEnv>(
81
120
  actionContext,
82
121
  stale,
83
122
  traceSource,
123
+ defaultOverride,
84
124
  } = options;
85
125
  const nextParams = segment.params || {};
86
126
  const paramsChanged = !paramsEqual(nextParams, prevParams);
87
127
  const searchChanged = prevUrl.search !== nextUrl.search;
88
128
 
89
- // Trace helper: push a structured entry to the request-scoped trace buffer.
90
- // Guarded by isTraceActive() so object construction is skipped in production.
91
129
  function pushTrace(
92
130
  defaultVal: boolean,
93
131
  finalVal: boolean,
@@ -106,38 +144,28 @@ export async function evaluateRevalidation<TEnv>(
106
144
  });
107
145
  }
108
146
 
109
- // Calculate default revalidation based on segment type and request method
110
147
  let defaultShouldRevalidate: boolean;
111
148
  let defaultReason: string;
112
149
 
113
- if (request.method === "POST") {
114
- // Actions: revalidate segments that belong to the route, skip parent chain
150
+ if (defaultOverride) {
151
+ defaultShouldRevalidate = defaultOverride.value;
152
+ defaultReason = defaultOverride.reason;
153
+ } else if (request.method === "POST") {
115
154
  if (segment.type === "route") {
116
- // Route segment always revalidates on actions
117
155
  defaultShouldRevalidate = true;
118
156
  defaultReason = "action:route-segment";
119
157
  } else if (segment.type === "loader") {
120
- // Loaders always revalidate on actions - they often contain action-sensitive data
121
- // (e.g., cart count after add-to-cart action)
122
158
  defaultShouldRevalidate = true;
123
159
  defaultReason = "action:loader-segment";
124
160
  } else if (segment.belongsToRoute) {
125
- // Segment belongs to route (orphan layouts/parallels) - revalidate
126
161
  defaultShouldRevalidate = true;
127
162
  defaultReason = "action:belongs-to-route";
128
163
  } else {
129
- // Parent chain segment (shared layouts/parallels) - don't revalidate
130
164
  defaultShouldRevalidate = false;
131
165
  defaultReason = "action:parent-chain-skip";
132
166
  }
133
167
  } else {
134
- // Navigation (GET): Conservative defaults to minimize unnecessary revalidations
135
- // Only the route segment revalidates by default - all others require explicit opt-in
136
-
137
168
  if (segment.type === "route") {
138
- // Route segments revalidate when path params OR search params change.
139
- // Search params (e.g., ?page=2&sort=price) are server-parsed via ctx.search,
140
- // so the handler must re-execute to produce updated content.
141
169
  const routeChanged = paramsChanged || searchChanged;
142
170
  defaultShouldRevalidate = routeChanged;
143
171
  defaultReason = paramsChanged
@@ -153,8 +181,6 @@ export async function evaluateRevalidation<TEnv>(
153
181
  });
154
182
  }
155
183
  } else if (segment.belongsToRoute && (paramsChanged || searchChanged)) {
156
- // Children of the route path (loaders, orphan layouts/parallels)
157
- // revalidate when path params or search params change
158
184
  defaultShouldRevalidate = true;
159
185
  defaultReason = paramsChanged
160
186
  ? "nav:route-child-params-changed"
@@ -166,9 +192,6 @@ export async function evaluateRevalidation<TEnv>(
166
192
  searchChanged,
167
193
  });
168
194
  } else {
169
- // Parent layouts and parallels default to no revalidation
170
- // Cannot assume these segments depend on params without explicit declaration
171
- // Use custom revalidation functions to opt-in when needed
172
195
  defaultShouldRevalidate = false;
173
196
  defaultReason = "nav:non-route-skip";
174
197
  debugLog("revalidation", "non-route segment skipped by default", {
@@ -178,7 +201,6 @@ export async function evaluateRevalidation<TEnv>(
178
201
  }
179
202
  }
180
203
 
181
- // No custom revalidations defined - return default behavior without prev segment
182
204
  if (revalidations.length === 0) {
183
205
  if (defaultShouldRevalidate) {
184
206
  debugLog("revalidation", "default revalidate=true", {
@@ -195,14 +217,10 @@ export async function evaluateRevalidation<TEnv>(
195
217
  return defaultShouldRevalidate;
196
218
  }
197
219
 
198
- // Custom revalidations exist - may need full prev segment
199
- // Lazy load prev segment only if getPrevSegment provided
200
220
  const prevSegment = getPrevSegment ? await getPrevSegment() : null;
201
221
 
202
- // Execute revalidation functions with soft/hard decision pattern
203
222
  let currentSuggestion = defaultShouldRevalidate;
204
223
 
205
- // Compute public route names (filtered: undefined for auto-generated routes)
206
224
  const toRouteName =
207
225
  routeKey && !isAutoGeneratedRouteName(routeKey) ? routeKey : undefined;
208
226
  const reqCtx = _getRequestContext();
@@ -226,23 +244,18 @@ export async function evaluateRevalidation<TEnv>(
226
244
  slotName: segment.slot,
227
245
  // Action context (only populated when triggered by server action)
228
246
  actionId: actionContext?.actionId,
247
+ isAction: makeIsAction(actionContext?.actionId),
229
248
  actionUrl: actionContext?.actionUrl,
230
249
  actionResult: actionContext?.actionResult,
231
250
  formData: actionContext?.formData,
232
- method: request.method, // GET for navigation, POST for actions
233
- routeName: toRouteName, // Navigation target route name (filtered)
234
- fromRouteName, // Navigation source route name (filtered)
235
- toRouteName, // Navigation target route name (filtered)
236
- // Stale cache context (only true for background revalidation after stale cache render)
251
+ method: request.method,
252
+ routeName: toRouteName,
253
+ fromRouteName,
254
+ toRouteName,
237
255
  stale,
238
256
  });
239
257
 
240
- // Check return type:
241
- // - boolean: hard decision, short-circuit immediately
242
- // - { defaultShouldRevalidate: boolean }: soft decision, update suggestion and continue
243
- // - null/undefined: use default behavior (equivalent to returning { defaultShouldRevalidate })
244
258
  if (typeof result === "boolean") {
245
- // Hard decision - short-circuit
246
259
  debugLog("revalidation", "hard decision", {
247
260
  segmentId: segment.id,
248
261
  revalidator: name,
@@ -255,7 +268,6 @@ export async function evaluateRevalidation<TEnv>(
255
268
  typeof result === "object" &&
256
269
  "defaultShouldRevalidate" in result
257
270
  ) {
258
- // Soft decision - update suggestion and continue
259
271
  currentSuggestion = result.defaultShouldRevalidate;
260
272
  debugLog("revalidation", "soft decision", {
261
273
  segmentId: segment.id,
@@ -263,18 +275,14 @@ export async function evaluateRevalidation<TEnv>(
263
275
  revalidate: currentSuggestion,
264
276
  });
265
277
  } else if (result === null || result === undefined) {
266
- // Defer to default - equivalent to { defaultShouldRevalidate: currentSuggestion }
267
- // This means "I don't care, use whatever the default is"
268
278
  debugLog("revalidation", "deferred to current default", {
269
279
  segmentId: segment.id,
270
280
  revalidator: name,
271
281
  revalidate: currentSuggestion,
272
282
  });
273
- // currentSuggestion stays the same, continue to next function
274
283
  }
275
284
  }
276
285
 
277
- // All revalidators completed - use final suggestion
278
286
  debugLog("revalidation", "final decision", {
279
287
  segmentId: segment.id,
280
288
  revalidate: currentSuggestion,