@rangojs/router 0.0.0-experimental.bd6e11bc → 0.0.0-experimental.bdaf10aa

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 (411) hide show
  1. package/AGENTS.md +8 -4
  2. package/README.md +296 -887
  3. package/dist/bin/rango.js +459 -91
  4. package/dist/testing/vitest.js +36 -2
  5. package/dist/vite/index.js +1708 -414
  6. package/package.json +35 -10
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +82 -5
  9. package/skills/bundle-analysis/SKILL.md +2 -2
  10. package/skills/cache-guide/SKILL.md +14 -9
  11. package/skills/caching/SKILL.md +221 -12
  12. package/skills/catalog.json +271 -0
  13. package/skills/comparison/SKILL.md +50 -0
  14. package/skills/comparison/agents/openai.yaml +4 -0
  15. package/skills/comparison/references/framework-comparison.md +837 -0
  16. package/skills/composability/SKILL.md +83 -2
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +5 -3
  19. package/skills/defer-hydration/SKILL.md +235 -0
  20. package/skills/document-cache/SKILL.md +11 -3
  21. package/skills/fonts/SKILL.md +1 -1
  22. package/skills/handler-use/SKILL.md +9 -9
  23. package/skills/hooks/SKILL.md +73 -900
  24. package/skills/hooks/data.md +273 -0
  25. package/skills/hooks/handle-and-actions.md +103 -0
  26. package/skills/hooks/navigation.md +110 -0
  27. package/skills/hooks/outlets.md +41 -0
  28. package/skills/hooks/state.md +228 -0
  29. package/skills/hooks/urls.md +135 -0
  30. package/skills/host-router/SKILL.md +84 -7
  31. package/skills/i18n/SKILL.md +1 -1
  32. package/skills/intercept/SKILL.md +51 -17
  33. package/skills/layout/SKILL.md +38 -16
  34. package/skills/links/SKILL.md +1 -1
  35. package/skills/loader/SKILL.md +48 -20
  36. package/skills/middleware/SKILL.md +11 -5
  37. package/skills/migrate-nextjs/SKILL.md +203 -20
  38. package/skills/migrate-react-router/SKILL.md +59 -675
  39. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  40. package/skills/migrate-react-router/component-migration.md +196 -0
  41. package/skills/migrate-react-router/data-and-actions.md +225 -0
  42. package/skills/migrate-react-router/route-mapping.md +271 -0
  43. package/skills/mime-routes/SKILL.md +3 -3
  44. package/skills/observability/SKILL.md +70 -5
  45. package/skills/parallel/SKILL.md +32 -8
  46. package/skills/ppr/SKILL.md +622 -0
  47. package/skills/prerender/SKILL.md +59 -28
  48. package/skills/rango/SKILL.md +124 -50
  49. package/skills/response-routes/SKILL.md +78 -46
  50. package/skills/route/SKILL.md +85 -6
  51. package/skills/router-setup/SKILL.md +41 -6
  52. package/skills/scripts/SKILL.md +179 -0
  53. package/skills/server-actions/SKILL.md +28 -3
  54. package/skills/shell-manifest/SKILL.md +185 -0
  55. package/skills/streams-and-websockets/SKILL.md +1 -1
  56. package/skills/tailwind/SKILL.md +28 -4
  57. package/skills/testing/SKILL.md +68 -654
  58. package/skills/testing/bindings.md +103 -0
  59. package/skills/testing/cache-prerender.md +127 -0
  60. package/skills/testing/client-components.md +124 -0
  61. package/skills/testing/e2e-parity.md +125 -0
  62. package/skills/testing/flight.md +91 -0
  63. package/skills/testing/handles.md +131 -0
  64. package/skills/testing/loader.md +128 -0
  65. package/skills/testing/middleware.md +99 -0
  66. package/skills/testing/render-handler.md +122 -0
  67. package/skills/testing/response-routes.md +95 -0
  68. package/skills/testing/reverse-and-types.md +85 -0
  69. package/skills/testing/server-actions.md +107 -0
  70. package/skills/testing/server-tree.md +128 -0
  71. package/skills/testing/setup.md +123 -0
  72. package/skills/theme/SKILL.md +1 -1
  73. package/skills/typesafety/SKILL.md +45 -918
  74. package/skills/typesafety/env-and-bindings.md +254 -0
  75. package/skills/typesafety/generated-files-and-cli.md +335 -0
  76. package/skills/typesafety/params-and-search.md +153 -0
  77. package/skills/typesafety/route-types.md +209 -0
  78. package/skills/use-cache/SKILL.md +47 -17
  79. package/skills/vercel/SKILL.md +128 -0
  80. package/skills/view-transitions/SKILL.md +44 -1
  81. package/src/__augment-tests__/augmented.check.ts +2 -3
  82. package/src/__internal.ts +0 -65
  83. package/src/browser/action-coordinator.ts +1 -1
  84. package/src/browser/action-fence.ts +47 -0
  85. package/src/browser/app-shell.ts +14 -27
  86. package/src/browser/connection-warmup.ts +134 -0
  87. package/src/browser/cookie-name.ts +140 -0
  88. package/src/browser/event-controller.ts +178 -100
  89. package/src/browser/invalidate-client-cache.ts +52 -0
  90. package/src/browser/logging.ts +28 -0
  91. package/src/browser/merge-segment-loaders.ts +6 -4
  92. package/src/browser/navigation-bridge.ts +81 -68
  93. package/src/browser/navigation-client.ts +115 -70
  94. package/src/browser/navigation-store-handle.ts +38 -0
  95. package/src/browser/navigation-store.ts +153 -88
  96. package/src/browser/navigation-transaction.ts +0 -32
  97. package/src/browser/network-error-handler.ts +34 -7
  98. package/src/browser/partial-update.ts +157 -144
  99. package/src/browser/prefetch/cache.ts +148 -81
  100. package/src/browser/prefetch/fetch.ts +231 -51
  101. package/src/browser/prefetch/queue.ts +25 -7
  102. package/src/browser/rango-state.ts +157 -115
  103. package/src/browser/react/Link.tsx +40 -7
  104. package/src/browser/react/NavigationProvider.tsx +140 -99
  105. package/src/browser/react/ScrollRestoration.tsx +10 -6
  106. package/src/browser/react/filter-segment-order.ts +17 -2
  107. package/src/browser/react/index.ts +0 -51
  108. package/src/browser/react/location-state-shared.ts +14 -15
  109. package/src/browser/react/location-state.ts +0 -1
  110. package/src/browser/react/use-action.ts +6 -15
  111. package/src/browser/react/use-handle.ts +0 -5
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +33 -8
  114. package/src/browser/react/use-navigation.ts +10 -5
  115. package/src/browser/react/use-params.ts +0 -2
  116. package/src/browser/react/use-router.ts +6 -4
  117. package/src/browser/react/use-search-params.ts +0 -5
  118. package/src/browser/react/use-segments.ts +0 -13
  119. package/src/browser/response-adapter.ts +74 -8
  120. package/src/browser/rsc-router.tsx +97 -22
  121. package/src/browser/scroll-restoration.ts +15 -8
  122. package/src/browser/segment-reconciler.ts +31 -21
  123. package/src/browser/server-action-bridge.ts +216 -38
  124. package/src/browser/types.ts +94 -22
  125. package/src/browser/validate-redirect-origin.ts +43 -16
  126. package/src/build/generate-manifest.ts +155 -131
  127. package/src/build/generate-route-types.ts +1 -1
  128. package/src/build/index.ts +11 -5
  129. package/src/build/prefix-tree-utils.ts +123 -0
  130. package/src/build/route-trie.ts +152 -22
  131. package/src/build/route-types/ast-route-extraction.ts +15 -8
  132. package/src/build/route-types/codegen.ts +12 -1
  133. package/src/build/route-types/include-resolution.ts +455 -61
  134. package/src/build/route-types/param-extraction.ts +6 -3
  135. package/src/build/route-types/per-module-writer.ts +15 -2
  136. package/src/build/route-types/router-processing.ts +77 -41
  137. package/src/build/route-types/source-scan.ts +105 -7
  138. package/src/build/runtime-discovery.ts +4 -1
  139. package/src/cache/cache-error.ts +104 -0
  140. package/src/cache/cache-key-utils.ts +58 -13
  141. package/src/cache/cache-policy.ts +108 -34
  142. package/src/cache/cache-runtime.ts +454 -101
  143. package/src/cache/cache-scope.ts +159 -54
  144. package/src/cache/cache-tag.ts +149 -0
  145. package/src/cache/cf/cf-base64.ts +33 -0
  146. package/src/cache/cf/cf-cache-constants.ts +127 -0
  147. package/src/cache/cf/cf-cache-store.ts +2170 -377
  148. package/src/cache/cf/cf-cache-types.ts +349 -0
  149. package/src/cache/cf/cf-kv-utils.ts +46 -0
  150. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  151. package/src/cache/cf/index.ts +6 -16
  152. package/src/cache/document-cache.ts +126 -41
  153. package/src/cache/handle-snapshot.ts +70 -0
  154. package/src/cache/index.ts +23 -20
  155. package/src/cache/memory-segment-store.ts +243 -37
  156. package/src/cache/profile-registry.ts +46 -31
  157. package/src/cache/read-through-swr.ts +56 -12
  158. package/src/cache/segment-codec.ts +13 -21
  159. package/src/cache/shell-snapshot.ts +417 -0
  160. package/src/cache/tag-invalidation.ts +230 -0
  161. package/src/cache/types.ts +194 -99
  162. package/src/cache/vercel/index.ts +11 -0
  163. package/src/cache/vercel/vercel-cache-store.ts +1132 -0
  164. package/src/client.rsc.tsx +39 -22
  165. package/src/client.tsx +28 -58
  166. package/src/cloudflare/index.ts +11 -0
  167. package/src/cloudflare/tracing.ts +108 -0
  168. package/src/component-utils.ts +19 -0
  169. package/src/components/DefaultDocument.tsx +8 -2
  170. package/src/context-var.ts +13 -1
  171. package/src/decode-loader-results.ts +18 -2
  172. package/src/defer.ts +185 -0
  173. package/src/deps/ssr.ts +0 -1
  174. package/src/encode-kv.ts +49 -0
  175. package/src/errors.ts +0 -3
  176. package/src/escape-script.ts +52 -0
  177. package/src/handle.ts +57 -40
  178. package/src/handles/MetaTags.tsx +24 -53
  179. package/src/handles/Scripts.tsx +183 -0
  180. package/src/handles/breadcrumbs.ts +35 -8
  181. package/src/handles/deferred-resolution.ts +127 -0
  182. package/src/handles/is-thenable.ts +18 -0
  183. package/src/handles/meta.ts +14 -40
  184. package/src/handles/script.ts +244 -0
  185. package/src/host/cookie-handler.ts +9 -60
  186. package/src/host/errors.ts +13 -22
  187. package/src/host/index.ts +7 -0
  188. package/src/host/pattern-matcher.ts +23 -52
  189. package/src/host/router.ts +1 -65
  190. package/src/host/testing.ts +40 -27
  191. package/src/host/types.ts +6 -2
  192. package/src/href-client.ts +7 -12
  193. package/src/index.rsc.ts +88 -8
  194. package/src/index.ts +90 -16
  195. package/src/internal-debug.ts +11 -10
  196. package/src/loader.rsc.ts +19 -9
  197. package/src/loader.ts +12 -4
  198. package/src/outlet-provider.tsx +1 -5
  199. package/src/prerender/param-hash.ts +16 -16
  200. package/src/prerender/store.ts +32 -37
  201. package/src/prerender.ts +75 -7
  202. package/src/redirect-origin.ts +114 -0
  203. package/src/regex-escape.ts +8 -0
  204. package/src/render-error-thrower.tsx +20 -0
  205. package/src/response-utils.ts +25 -0
  206. package/src/root-error-boundary.tsx +1 -19
  207. package/src/route-content-wrapper.tsx +13 -49
  208. package/src/route-definition/dsl-helpers.ts +60 -53
  209. package/src/route-definition/helper-factories.ts +0 -2
  210. package/src/route-definition/helpers-types.ts +46 -46
  211. package/src/route-definition/index.ts +1 -2
  212. package/src/route-definition/redirect.ts +44 -11
  213. package/src/route-definition/resolve-handler-use.ts +6 -1
  214. package/src/route-definition/use-item-types.ts +3 -6
  215. package/src/route-map-builder.ts +41 -20
  216. package/src/route-types.ts +0 -5
  217. package/src/router/content-negotiation.ts +58 -23
  218. package/src/router/error-handling.ts +44 -17
  219. package/src/router/find-match.ts +129 -30
  220. package/src/router/handler-context.ts +6 -1
  221. package/src/router/instrument.ts +355 -0
  222. package/src/router/intercept-resolution.ts +35 -2
  223. package/src/router/lazy-includes.ts +79 -56
  224. package/src/router/loader-resolution.ts +151 -73
  225. package/src/router/logging.ts +0 -6
  226. package/src/router/manifest.ts +74 -40
  227. package/src/router/match-api.ts +76 -52
  228. package/src/router/match-context.ts +0 -22
  229. package/src/router/match-handlers.ts +181 -178
  230. package/src/router/match-middleware/background-revalidation.ts +40 -24
  231. package/src/router/match-middleware/cache-lookup.ts +115 -194
  232. package/src/router/match-middleware/cache-store.ts +61 -50
  233. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  234. package/src/router/match-middleware/segment-resolution.ts +0 -22
  235. package/src/router/match-pipelines.ts +1 -42
  236. package/src/router/match-result.ts +36 -67
  237. package/src/router/metrics.ts +0 -34
  238. package/src/router/middleware-types.ts +0 -116
  239. package/src/router/middleware.ts +231 -120
  240. package/src/router/navigation-snapshot.ts +7 -56
  241. package/src/router/params-util.ts +23 -0
  242. package/src/router/parse-pattern.ts +115 -0
  243. package/src/router/pattern-matching.ts +99 -152
  244. package/src/router/prefetch-cache-ttl.ts +51 -0
  245. package/src/router/prefetch-limits.ts +37 -0
  246. package/src/router/prerender-match.ts +111 -66
  247. package/src/router/preview-match.ts +3 -1
  248. package/src/router/request-classification.ts +47 -42
  249. package/src/router/revalidation.ts +75 -81
  250. package/src/router/route-snapshot.ts +14 -3
  251. package/src/router/router-context.ts +6 -29
  252. package/src/router/router-interfaces.ts +70 -8
  253. package/src/router/router-options.ts +126 -4
  254. package/src/router/segment-resolution/fresh.ts +104 -80
  255. package/src/router/segment-resolution/helpers.ts +86 -6
  256. package/src/router/segment-resolution/loader-cache.ts +155 -39
  257. package/src/router/segment-resolution/loader-mask.ts +60 -0
  258. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  259. package/src/router/segment-resolution/mask-nested.ts +83 -0
  260. package/src/router/segment-resolution/revalidation.ts +215 -304
  261. package/src/router/segment-resolution/static-store.ts +19 -5
  262. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  263. package/src/router/segment-resolution/view-transition-default.ts +35 -15
  264. package/src/router/segment-resolution.ts +5 -1
  265. package/src/router/segment-wrappers.ts +6 -5
  266. package/src/router/state-cookie-name.ts +33 -0
  267. package/src/router/substitute-pattern-params.ts +54 -35
  268. package/src/router/telemetry-otel.ts +160 -200
  269. package/src/router/telemetry.ts +9 -23
  270. package/src/router/timeout.ts +0 -20
  271. package/src/router/tracing.ts +215 -0
  272. package/src/router/trie-matching.ts +171 -64
  273. package/src/router/types.ts +1 -63
  274. package/src/router/url-params.ts +13 -5
  275. package/src/router.ts +119 -48
  276. package/src/rsc/full-payload.ts +70 -0
  277. package/src/rsc/handler-context.ts +1 -0
  278. package/src/rsc/handler.ts +267 -152
  279. package/src/rsc/helpers.ts +78 -4
  280. package/src/rsc/index.ts +1 -4
  281. package/src/rsc/json-route-result.ts +38 -0
  282. package/src/rsc/loader-fetch.ts +114 -38
  283. package/src/rsc/manifest-init.ts +29 -42
  284. package/src/rsc/nonce.ts +10 -1
  285. package/src/rsc/origin-guard.ts +11 -15
  286. package/src/rsc/progressive-enhancement.ts +120 -13
  287. package/src/rsc/redirect-guard.ts +100 -0
  288. package/src/rsc/response-cache-serve.ts +238 -0
  289. package/src/rsc/response-error.ts +79 -12
  290. package/src/rsc/response-route-handler.ts +58 -141
  291. package/src/rsc/rsc-rendering.ts +492 -49
  292. package/src/rsc/runtime-warnings.ts +14 -0
  293. package/src/rsc/server-action.ts +268 -82
  294. package/src/rsc/shell-capture.ts +1190 -0
  295. package/src/rsc/shell-serve.ts +181 -0
  296. package/src/rsc/transition-gate.ts +89 -0
  297. package/src/rsc/types.ts +45 -3
  298. package/src/runtime-env.ts +18 -0
  299. package/src/search-params.ts +31 -26
  300. package/src/segment-loader-promise.ts +49 -4
  301. package/src/segment-system.tsx +260 -95
  302. package/src/server/context.ts +99 -9
  303. package/src/server/cookie-parse.ts +32 -0
  304. package/src/server/cookie-store.ts +125 -2
  305. package/src/server/handle-store.ts +21 -38
  306. package/src/server/loader-registry.ts +33 -42
  307. package/src/server/request-context.ts +379 -138
  308. package/src/ssr/index.tsx +491 -182
  309. package/src/ssr/inject-rsc-eager.ts +167 -0
  310. package/src/ssr/ssr-root.tsx +228 -0
  311. package/src/static-handler.ts +10 -13
  312. package/src/testing/cache-status.ts +44 -48
  313. package/src/testing/collect-handle.ts +14 -31
  314. package/src/testing/dispatch.ts +533 -160
  315. package/src/testing/e2e/fixture.ts +45 -11
  316. package/src/testing/e2e/index.ts +1 -22
  317. package/src/testing/e2e/matchers.ts +0 -16
  318. package/src/testing/e2e/parity.ts +85 -4
  319. package/src/testing/e2e/server.ts +12 -0
  320. package/src/testing/flight-matchers.ts +7 -14
  321. package/src/testing/flight-normalize.ts +11 -0
  322. package/src/testing/flight-runtime.d.ts +36 -0
  323. package/src/testing/flight-tree.ts +682 -0
  324. package/src/testing/flight.entry.ts +30 -0
  325. package/src/testing/flight.ts +145 -70
  326. package/src/testing/generated-routes.ts +26 -50
  327. package/src/testing/index.ts +18 -19
  328. package/src/testing/internal/context.ts +184 -68
  329. package/src/testing/internal/flight-client-globals.ts +30 -0
  330. package/src/testing/internal/seed-vars.ts +54 -0
  331. package/src/testing/render-handler.ts +357 -0
  332. package/src/testing/render-route.tsx +134 -115
  333. package/src/testing/run-loader.ts +140 -51
  334. package/src/testing/run-middleware.ts +59 -33
  335. package/src/testing/run-transition-when.ts +164 -0
  336. package/src/testing/vitest-stubs/cloudflare-email.ts +1 -1
  337. package/src/testing/vitest-stubs/cloudflare-workers.ts +1 -1
  338. package/src/testing/vitest.ts +138 -16
  339. package/src/theme/ThemeProvider.tsx +56 -84
  340. package/src/theme/ThemeScript.tsx +7 -9
  341. package/src/theme/constants.ts +52 -13
  342. package/src/theme/index.ts +0 -7
  343. package/src/theme/theme-context.ts +1 -5
  344. package/src/theme/theme-script.ts +22 -21
  345. package/src/theme/use-theme.ts +0 -3
  346. package/src/types/boundaries.ts +0 -35
  347. package/src/types/cache-types.ts +13 -4
  348. package/src/types/error-types.ts +30 -90
  349. package/src/types/global-namespace.ts +15 -15
  350. package/src/types/handler-context.ts +45 -15
  351. package/src/types/index.ts +2 -10
  352. package/src/types/loader-types.ts +6 -3
  353. package/src/types/request-scope.ts +8 -22
  354. package/src/types/route-config.ts +20 -52
  355. package/src/types/route-entry.ts +0 -6
  356. package/src/types/segments.ts +100 -13
  357. package/src/urls/include-helper.ts +10 -12
  358. package/src/urls/include-provider.ts +71 -0
  359. package/src/urls/index.ts +2 -8
  360. package/src/urls/path-helper-types.ts +52 -14
  361. package/src/urls/path-helper.ts +5 -54
  362. package/src/urls/pattern-types.ts +36 -0
  363. package/src/urls/type-extraction.ts +76 -42
  364. package/src/urls/urls-function.ts +0 -14
  365. package/src/use-loader.tsx +0 -186
  366. package/src/vercel/index.ts +11 -0
  367. package/src/vercel/tracing.ts +88 -0
  368. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  369. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  370. package/src/vite/discovery/discover-routers.ts +34 -43
  371. package/src/vite/discovery/discovery-errors.ts +61 -0
  372. package/src/vite/discovery/prerender-collection.ts +33 -46
  373. package/src/vite/discovery/state.ts +12 -1
  374. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  375. package/src/vite/index.ts +9 -0
  376. package/src/vite/inject-client-debug.ts +88 -0
  377. package/src/vite/plugin-types.ts +143 -10
  378. package/src/vite/plugins/cjs-to-esm.ts +8 -12
  379. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  380. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  381. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  382. package/src/vite/plugins/expose-action-id.ts +2 -73
  383. package/src/vite/plugins/expose-id-utils.ts +85 -56
  384. package/src/vite/plugins/expose-ids/export-analysis.ts +30 -43
  385. package/src/vite/plugins/expose-ids/handler-transform.ts +5 -31
  386. package/src/vite/plugins/expose-ids/loader-transform.ts +12 -20
  387. package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
  388. package/src/vite/plugins/expose-internal-ids.ts +10 -1
  389. package/src/vite/plugins/performance-tracks.ts +0 -3
  390. package/src/vite/plugins/refresh-cmd.ts +1 -1
  391. package/src/vite/plugins/use-cache-transform.ts +21 -46
  392. package/src/vite/plugins/vercel-output.ts +384 -0
  393. package/src/vite/plugins/version-injector.ts +22 -27
  394. package/src/vite/plugins/version-plugin.ts +6 -66
  395. package/src/vite/plugins/virtual-entries.ts +137 -26
  396. package/src/vite/rango.ts +146 -135
  397. package/src/vite/router-discovery.ts +189 -48
  398. package/src/vite/utils/ast-handler-extract.ts +11 -20
  399. package/src/vite/utils/bundle-analysis.ts +6 -13
  400. package/src/vite/utils/client-chunks.ts +0 -6
  401. package/src/vite/utils/directive-prologue.ts +40 -0
  402. package/src/vite/utils/forward-user-plugins.ts +0 -22
  403. package/src/vite/utils/manifest-utils.ts +4 -75
  404. package/src/vite/utils/package-resolution.ts +1 -73
  405. package/src/vite/utils/prerender-utils.ts +71 -44
  406. package/src/vite/utils/shared-utils.ts +55 -37
  407. package/src/browser/react/use-client-cache.ts +0 -58
  408. package/src/browser/shallow.ts +0 -40
  409. package/src/handles/index.ts +0 -7
  410. package/src/network-error-thrower.tsx +0 -23
  411. package/src/router/middleware-cookies.ts +0 -55
@@ -14,14 +14,19 @@
14
14
  * (de)serialization. Consequences:
15
15
  * - It will NOT catch server/client boundary reference-identity remount bugs
16
16
  * (a server-serialized component reference differing from the client
17
- * reference). Use renderServer / e2e for those.
17
+ * reference). Use renderServerTree / e2e for those.
18
18
  * - It will NOT catch real Flight serialization errors (non-serializable
19
19
  * props crossing the RSC boundary), loader execution on the server,
20
- * middleware, or handler ordering. Those are renderServer / e2e territory.
20
+ * middleware, or handler ordering. Those are renderServerTree / renderHandler
21
+ * / e2e territory.
21
22
  * - Loader data, location state, and handle output are SEEDED directly into
22
23
  * client context (see the `loaders` / `locationState` / `handles` options) —
23
24
  * nothing is executed on the server. This exercises the read path
24
25
  * (useLoader / useLocationState / useHandle from context), not the run path.
26
+ * - navigate() commits synchronously, so it does NOT drive the navigation
27
+ * lifecycle: useNavigation().state, useLinkStatus().pending, and
28
+ * useAction().state stay "idle". Assert pending/loading/submitting transition
29
+ * states with renderServerTree / e2e instead (navigate() warns once if used).
25
30
  * What it DOES cover: client hooks that read NavigationProvider /
26
31
  * OutletContext — useParams, useReverse, useHref, useMount, useNavigation,
27
32
  * useRouter, usePathname, useSearchParams, Outlet nesting, useLoader /
@@ -41,17 +46,22 @@ import {
41
46
  generateHistoryKey,
42
47
  } from "../browser/navigation-store.js";
43
48
  import { createEventController } from "../browser/event-controller.js";
49
+ import { resolveDeferredHandleValues } from "../handles/deferred-resolution.js";
44
50
  import type { NavigationStore, NavigationBridge } from "../browser/types.js";
45
51
  import type { EventController } from "../browser/event-controller.js";
46
52
  import type { ResolvedSegment, RscMetadata } from "../browser/types.js";
47
53
  import { NavigationProvider } from "../browser/react/NavigationProvider.js";
48
- import { compilePattern } from "../router/pattern-matching.js";
54
+ import {
55
+ compilePattern,
56
+ buildParamsFromMatch,
57
+ } from "../router/pattern-matching.js";
49
58
  import { normalizeBasename } from "../router/basename.js";
50
59
  import type { LoaderDefinition } from "../types.js";
51
60
  import type { LocationStateDefinition } from "../browser/react/location-state-shared.js";
52
61
  import type { Handle } from "../handle.js";
53
62
  import type { ThemeConfig } from "../theme/types.js";
54
63
  import { resolveThemeConfig } from "../theme/constants.js";
64
+ import { isUnderTestRunner } from "../runtime-env.js";
55
65
 
56
66
  const TEST_ORIGIN = "http://localhost";
57
67
 
@@ -62,11 +72,6 @@ const TEST_ORIGIN = "http://localhost";
62
72
  */
63
73
  export type HandleDataSeed = Record<string, Record<string, unknown[]>>;
64
74
 
65
- // Loaders and location-state defs carry an id (`$$id` / `__rsc_ls_key`) that the
66
- // Vite plugin injects at build time; in a bare test it is "". These helpers
67
- // assign a synthetic stable id (mutating the handle, tracked per-object) so that
68
- // seeding by reference lines up with the read path (useLoader / useLocationState
69
- // both read the id off the handle at call time).
70
75
  const syntheticIds = new WeakMap<object, string>();
71
76
  let syntheticIdCounter = 0;
72
77
 
@@ -85,7 +90,6 @@ function ensureSyntheticId(
85
90
  return id;
86
91
  }
87
92
 
88
- /** One-level clone of a raw handle seed so we don't mutate the caller's object. */
89
93
  function cloneHandleSeed(seed?: HandleDataSeed): HandleDataSeed {
90
94
  const out: HandleDataSeed = {};
91
95
  for (const [name, segMap] of Object.entries(seed ?? {})) {
@@ -94,17 +98,11 @@ function cloneHandleSeed(seed?: HandleDataSeed): HandleDataSeed {
94
98
  return out;
95
99
  }
96
100
 
97
- /**
98
- * One node of the route definition passed to renderRoute. The array models a
99
- * single matched route plus its optional layout chain — element order is
100
- * outermost layout first, the leaf route last (the same root-to-leaf order the
101
- * real matcher produces).
102
- */
103
101
  export interface RenderRouteSpec {
104
102
  /**
105
103
  * The route pattern this node matches, e.g. "/products/:productId". The LAST
106
104
  * spec in the array is treated as the leaf route; earlier specs are layouts
107
- * wrapping it. Only the leaf pattern is matched against `initialUrl` to
105
+ * wrapping it. Only the leaf pattern is matched against the `request` URL to
108
106
  * extract params; layout patterns are informational.
109
107
  */
110
108
  path: string;
@@ -131,8 +129,13 @@ export interface RenderRouteSpec {
131
129
  * Options for renderRoute.
132
130
  */
133
131
  export interface RenderRouteOptions {
134
- /** Initial URL to render at. Defaults to the leaf spec's static prefix or "/". */
135
- initialUrl?: string;
132
+ /**
133
+ * The initial location to render at: a `Request`, or a URL string (absolute or
134
+ * path). Only the URL is read (this is a client render — headers/method are
135
+ * ignored); named `request` for parity with the other primitives. Defaults to
136
+ * the leaf spec's static prefix or "/".
137
+ */
138
+ request?: Request | string;
136
139
  /**
137
140
  * Loader data to seed into client context, keyed by loader id ($$id). A
138
141
  * component calling useLoader(SomeLoader) reads `loaderData[SomeLoader.$$id]`.
@@ -148,16 +151,28 @@ export interface RenderRouteOptions {
148
151
  * Passing `[loader, data]` pairs lets renderRoute assign a synthetic stable id
149
152
  * and wire `useLoader` to it. Prefer this over `loaderData` for real handles.
150
153
  *
154
+ * NOTE: when a real handle has no `$$id`, renderRoute MUTATES it to assign a
155
+ * synthetic stable id (so repeat renders key consistently). This is a side
156
+ * effect on your input object; a handle reused across tests keeps that id.
157
+ *
151
158
  * @example
159
+ * // useLoader returns an ENVELOPE — destructure `data`, it is not the bare value.
160
+ * function CartBadge() {
161
+ * const { data } = useLoader(CartLoader); // NOT `useLoader(CartLoader).itemCount`
162
+ * return <span>{data.itemCount}</span>;
163
+ * }
152
164
  * renderRoute([{ path: "/cart", Component: CartBadge }], {
153
165
  * loaders: [[CartLoader, { itemCount: 3, total: 89.97 }]],
154
166
  * });
155
167
  */
156
168
  loaders?: ReadonlyArray<readonly [LoaderDefinition<any>, unknown]>;
157
169
  /**
158
- * Explicit params. Merged over (and overriding) params extracted from
159
- * `initialUrl`. Use this when the URL alone cannot express the params, or to
160
- * avoid relying on URL parsing.
170
+ * Explicit params. Merged over (and overriding) params extracted from the
171
+ * `request` URL. Use this when the URL alone cannot express the params, or to
172
+ * avoid relying on URL parsing. Supplying params also OPTS OUT of the
173
+ * request/leaf match check: a `request` whose pathname does not resolve the
174
+ * leaf is normally rejected under the test runner, but passing params here
175
+ * tells renderRoute the request is intentionally not the param source.
161
176
  */
162
177
  params?: Record<string, string>;
163
178
  /**
@@ -221,6 +236,10 @@ export interface RenderRouteOptions {
221
236
  * exactly as `renderSegments` does in production (a segment whose `mountPath`
222
237
  * is set is wrapped in a MountContextProvider). Normalized like a path prefix
223
238
  * (leading slash forced, trailing stripped, bare "/" -> root). Defaults to "/".
239
+ * An explicitly-passed `request` must match the leaf `path` directly (paths are
240
+ * include-RELATIVE; the mount does NOT rewrite the request) — pass the relative
241
+ * path, not the mount-prefixed one, or renderRoute throws rather than silently
242
+ * rendering empty params.
224
243
  *
225
244
  * @example
226
245
  * renderRoute([{ path: "/c/wine", Component: ProductPage }], { mount: "/shop" });
@@ -235,6 +254,20 @@ export interface RenderRouteOptions {
235
254
  * component.
236
255
  */
237
256
  theme?: ThemeConfig | true;
257
+ /**
258
+ * CSP nonce to seed via NonceContext, so a component calling `useNonce()`
259
+ * (e.g. an analytics/GTM head-script component) sees this value — mirroring
260
+ * what the SSR renderer provides per request. Defaults to undefined (the
261
+ * browser default), matching production client behavior.
262
+ *
263
+ * @example
264
+ * const { getByTestId } = await renderRoute(
265
+ * [{ path: "/", Component: NonceProbe }],
266
+ * { nonce: "test-nonce" },
267
+ * );
268
+ * expect(getByTestId("nonce").textContent).toBe("test-nonce");
269
+ */
270
+ nonce?: string;
238
271
  }
239
272
 
240
273
  /**
@@ -266,11 +299,6 @@ interface ResolvedMatch {
266
299
  pathname: string;
267
300
  }
268
301
 
269
- /**
270
- * Match a pathname against the leaf spec's pattern and extract params.
271
- * Returns null when the pattern does not match (params then fall back to the
272
- * caller-provided `options.params`).
273
- */
274
302
  function matchLeaf(
275
303
  pattern: string,
276
304
  pathname: string,
@@ -278,17 +306,11 @@ function matchLeaf(
278
306
  const compiled = compilePattern(pattern);
279
307
  const match = compiled.regex.exec(pathname);
280
308
  if (!match) return null;
281
- const params: Record<string, string> = {};
282
- compiled.paramNames.forEach((name, index) => {
283
- const value = match[index + 1];
284
- if (value !== undefined) {
285
- params[name] = decodeURIComponent(value);
286
- }
287
- });
288
- return params;
309
+ // Reuse the production param builder so the harness matches the real matcher
310
+ // exactly (named catch-all "" binding, decoding) instead of forking it.
311
+ return buildParamsFromMatch(match, compiled.paramNames, compiled.catchAll);
289
312
  }
290
313
 
291
- /** Derive a usable initial pathname from a leaf pattern when none is given. */
292
314
  function staticPrefix(pattern: string): string {
293
315
  const out: string[] = [];
294
316
  for (const part of pattern.split("/")) {
@@ -299,13 +321,6 @@ function staticPrefix(pattern: string): string {
299
321
  return "/" + out.join("/");
300
322
  }
301
323
 
302
- /**
303
- * Build the synthetic ResolvedSegment[] for a matched route. Produces, in
304
- * root-to-leaf order: one layout segment per non-leaf spec, then the leaf route
305
- * segment, plus a loader segment for each seeded loader id attached to the
306
- * owning spec. Segment ids follow the real convention (L0, L0L1, ..., the leaf
307
- * route as L0...R{n}; loaders as {parentId}D{i}.{loaderId}).
308
- */
309
324
  function buildSegments(
310
325
  routes: RenderRouteSpec[],
311
326
  params: Record<string, string>,
@@ -338,20 +353,13 @@ function buildSegments(
338
353
  params,
339
354
  belongsToRoute: true,
340
355
  };
341
- // Model an include() mount: every component segment in the chain shares the
342
- // same prefix, so renderSegments wraps each in a MountContextProvider and
343
- // useMount() resolves the mounted prefix (production sets mountPath on every
344
- // segment of an included subtree). Must be applied identically at both
345
- // buildSegments call sites or segment-structure-assert flags a remount.
346
356
  if (mount) node.mountPath = mount;
347
- // A leaf-owned layout component wraps the route via its own layout element.
348
357
  if (isLeaf && spec.layout) {
349
358
  const Layout = spec.layout;
350
359
  node.layout = <Layout />;
351
360
  }
352
361
  segments.push(node);
353
362
 
354
- // Determine which seeded loader ids this spec owns.
355
363
  const ownedIds = spec.loaderIds
356
364
  ? spec.loaderIds.filter((id) => id in loaderData)
357
365
  : isLeaf
@@ -375,30 +383,6 @@ function buildSegments(
375
383
  return segments;
376
384
  }
377
385
 
378
- /**
379
- * Render a CLIENT component (and its layout chain) inside the router's
380
- * NavigationProvider for unit testing. Exported from `@rangojs/router/testing/dom`
381
- * (its own entry, kept out of the main `@rangojs/router/testing` barrel so that
382
- * barrel never references React/@testing-library/react). Async so the heavy
383
- * @testing-library/react dependency is loaded only at call time.
384
- *
385
- * @example
386
- * ```tsx
387
- * // @vitest-environment happy-dom
388
- * import { renderRoute } from "@rangojs/router/testing/dom";
389
- *
390
- * function Product() {
391
- * const { productId } = useParams<{ productId: string }>();
392
- * const reverse = useReverse({ product: "/products/:productId" });
393
- * return <a href={reverse("product", { productId: "2" })}>{productId}</a>;
394
- * }
395
- *
396
- * const { getByText, router } = await renderRoute(
397
- * [{ path: "/products/:productId", Component: Product }],
398
- * { initialUrl: "/products/1" },
399
- * );
400
- * ```
401
- */
402
386
  export async function renderRoute(
403
387
  routes: RenderRouteSpec[],
404
388
  options: RenderRouteOptions = {},
@@ -406,37 +390,34 @@ export async function renderRoute(
406
390
  if (routes.length === 0) {
407
391
  throw new Error("renderRoute: `routes` must contain at least one entry");
408
392
  }
393
+ if ("initialUrl" in options) {
394
+ throw new Error(
395
+ "renderRoute: the `initialUrl` option was renamed to `request`. " +
396
+ "Pass { request: <Request | url> } instead.",
397
+ );
398
+ }
409
399
 
410
400
  const { render, act } = await import("@testing-library/react");
411
401
 
412
402
  const leaf = routes[routes.length - 1];
413
- const initialUrl = options.initialUrl ?? staticPrefix(leaf.path) ?? "/";
403
+ const requestUrl =
404
+ options.request instanceof Request ? options.request.url : options.request;
405
+ const initialUrl = requestUrl ?? staticPrefix(leaf.path) ?? "/";
414
406
  const url = new URL(initialUrl, TEST_ORIGIN);
415
407
 
416
- // Seed loader data: explicit-id entries from `loaderData`, plus by-reference
417
- // entries from `loaders` (assigning synthetic ids to real handles whose `$$id`
418
- // is empty in a bare test).
419
408
  const loaderData: Record<string, unknown> = { ...(options.loaderData ?? {}) };
420
409
  for (const [loader, data] of options.loaders ?? []) {
421
410
  loaderData[ensureSyntheticId(loader as object, "$$id")] = data;
422
411
  }
423
412
 
424
- // Seed location state into history.state so useLocationState(def) resolves.
425
- // Keyed defs read history.state[def.__rsc_ls_key]; assign a synthetic key when
426
- // the injected one is empty (bare test). RESET history.state to only this
427
- // call's seeds (not a merge) so a previous render's seeded state does not leak
428
- // into a later render in the same DOM environment.
429
413
  if (typeof window !== "undefined") {
430
414
  const stateObj: Record<string, unknown> = {};
431
415
  for (const [def, value] of options.locationState ?? []) {
432
416
  stateObj[ensureSyntheticId(def as object, "__rsc_ls_key")] = value;
433
417
  }
434
- // No URL arg: useLocationState reads history.state (not the URL), and passing
435
- // a TEST_ORIGIN URL would trip the DOM env's same-origin check.
436
418
  window.history.replaceState(stateObj, "");
437
419
  }
438
420
 
439
- // Resolve params: URL-extracted params first, explicit params override.
440
421
  const resolve = (pathname: string): ResolvedMatch => {
441
422
  const matched = matchLeaf(leaf.path, pathname) ?? {};
442
423
  return {
@@ -446,11 +427,34 @@ export async function renderRoute(
446
427
  };
447
428
  const initialMatch = resolve(url.pathname);
448
429
 
449
- // Reuse the real browser primitives so context shape matches production.
450
430
  const historyKey = generateHistoryKey(url.href);
451
- // Normalize the include() mount prefix once and apply it at BOTH buildSegments
452
- // call sites (initial + navigate) so mountPath is consistent across renders.
453
431
  const mount = normalizeBasename(options.mount);
432
+ // Fail loud on a request that cannot resolve the leaf route (a typo, or the
433
+ // mount-prefixed-vs-relative confusion) instead of silently rendering empty
434
+ // params (matchLeaf -> null -> {}). renderRoute paths are include-RELATIVE and
435
+ // resolve() matches the request against the leaf as-is, so the request must be
436
+ // the relative form — a mount does NOT rewrite it. Only checked when `request`
437
+ // was passed explicitly (a defaulted request is staticPrefix of the leaf and
438
+ // always matches). Skipped when explicit `params` are supplied: those are
439
+ // merged over the URL-extracted params in resolve(), so the request is
440
+ // intentionally not the param source and an empty matchLeaf is not the trap.
441
+ // Gated on the test runner so it can never affect production.
442
+ if (
443
+ options.request !== undefined &&
444
+ Object.keys(options.params ?? {}).length === 0 &&
445
+ isUnderTestRunner() &&
446
+ matchLeaf(leaf.path, url.pathname) === null
447
+ ) {
448
+ throw new Error(
449
+ `renderRoute: request "${url.pathname}" does not match the leaf route ` +
450
+ `"${leaf.path}"${mount ? ` (mount "${mount}")` : ""}. renderRoute paths ` +
451
+ `are include-RELATIVE: pass a request that matches "${leaf.path}" ` +
452
+ `(e.g. "${staticPrefix(leaf.path)}"). A mount does NOT auto-rewrite the ` +
453
+ `request — pass the relative path, not the mount-prefixed one. If the ` +
454
+ `request URL intentionally does not carry the params, pass them ` +
455
+ `explicitly via the \`params\` option to bypass this check.`,
456
+ );
457
+ }
454
458
  const initialSegments = buildSegments(
455
459
  routes,
456
460
  initialMatch.params,
@@ -464,36 +468,44 @@ export async function renderRoute(
464
468
  initialSegments,
465
469
  crossTabSync: false,
466
470
  });
467
- // Seed handle data: raw `handle` entries plus by-reference `handles` attached
468
- // to the leaf route segment under each handle's id (so useHandle(handle)
469
- // resolves the pushed values).
470
471
  const leafRouteSegmentId =
471
472
  [...initialSegments].reverse().find((s) => s.type === "route")?.id ??
472
473
  initialSegments[initialSegments.length - 1]?.id;
473
474
  const handleSeed: HandleDataSeed = cloneHandleSeed(options.handle);
474
475
  for (const [handle, values] of options.handles ?? []) {
475
476
  if (leafRouteSegmentId === undefined) continue;
476
- // createHandle always has a non-empty $$id (the Vite plugin injects one, and
477
- // createHandle assigns a runtime fallback otherwise) with its REAL collect
478
- // registered — so seeding under handle.$$id makes useHandle(handle) run the
479
- // handle's actual collect/accumulator (custom collects included), not just a
480
- // default flatten.
481
477
  const id = (handle as unknown as { $$id: string }).$$id;
482
478
  (handleSeed[id] ??= {})[leafRouteSegmentId] = values;
483
479
  }
484
480
 
485
481
  const eventController = createEventController({ initialLocation: url });
486
482
  eventController.setParams(initialMatch.params);
483
+ // Resolve-by-default: resolve any deferred (Promise) seeded handle values
484
+ // before applying, so the seeded handles reach collect/useHandle resolved —
485
+ // matching what the server/client do in a real app.
486
+ const resolvedSeed = await resolveDeferredHandleValues(handleSeed);
487
487
  eventController.setHandleData(
488
- handleSeed,
488
+ resolvedSeed,
489
489
  initialSegments.map((s) => s.id),
490
490
  );
491
491
 
492
- // Client-only navigation: re-resolve against the in-memory routes and emit a
493
- // re-render. No server fetch — only routes passed to renderRoute exist. The
494
- // store update is flushed inside act() so React commits before callers
495
- // assert, mirroring how a real navigation lands a single payload swap.
492
+ let warnedNavLifecycle = false;
496
493
  const navigate = async (target: string): Promise<void> => {
494
+ // renderRoute commits navigations synchronously (no server fetch, no Flight
495
+ // stream), so it never drives the navigation lifecycle. The transition state
496
+ // useNavigation()/useLinkStatus()/useAction() read stays "idle" — asserting a
497
+ // pending/loading/submitting state here proves nothing. Warn once (per render)
498
+ // under the test runner so that false-confidence trap is loud, not silent.
499
+ if (isUnderTestRunner() && !warnedNavLifecycle) {
500
+ warnedNavLifecycle = true;
501
+ console.warn(
502
+ "renderRoute: navigate()/useRouter().push commit synchronously and do " +
503
+ "NOT drive the navigation lifecycle. useNavigation().state, " +
504
+ 'useLinkStatus().pending, and useAction().state stay "idle" here. ' +
505
+ "Assert params/pathname/content after navigate(); use renderServerTree " +
506
+ "or e2e to assert pending/loading/submitting transition states.",
507
+ );
508
+ }
497
509
  const nextUrl = new URL(target, TEST_ORIGIN);
498
510
  const match = resolve(nextUrl.pathname);
499
511
  const segments = buildSegments(routes, match.params, loaderData, mount);
@@ -515,7 +527,6 @@ export async function renderRoute(
515
527
  registerLinkInterception: () => () => {},
516
528
  getVersion: () => undefined,
517
529
  updateVersion: () => {},
518
- updateAppShell: () => {},
519
530
  };
520
531
 
521
532
  const initialMetadata = makeMetadata(
@@ -525,18 +536,27 @@ export async function renderRoute(
525
536
  );
526
537
  const initialTree = await renderSegments(initialSegments);
527
538
 
528
- const result = render(
529
- <NavigationProvider
530
- store={store}
531
- eventController={eventController}
532
- initialPayload={{ root: initialTree, metadata: initialMetadata }}
533
- bridge={bridge}
534
- basename={normalizeBasename(options.basename)}
535
- themeConfig={
536
- options.theme === undefined ? null : resolveThemeConfig(options.theme)
537
- }
538
- />,
539
- );
539
+ // Wrap render in an awaited async act so a tree that suspends (async loaders,
540
+ // loading states, deferred handle entries that arrive as a Promise) settles its
541
+ // Suspense within act — otherwise React orphans the resolution ("a component
542
+ // suspended inside an act scope, but the act call was not awaited") and the
543
+ // resolved content never reaches the asserted DOM.
544
+ let result!: Awaited<ReturnType<typeof render>>;
545
+ await act(async () => {
546
+ result = render(
547
+ <NavigationProvider
548
+ store={store}
549
+ eventController={eventController}
550
+ initialPayload={{ root: initialTree, metadata: initialMetadata }}
551
+ bridge={bridge}
552
+ basename={normalizeBasename(options.basename)}
553
+ themeConfig={
554
+ options.theme === undefined ? null : resolveThemeConfig(options.theme)
555
+ }
556
+ nonce={options.nonce}
557
+ />,
558
+ );
559
+ });
540
560
 
541
561
  const router: TestRouterHandle = {
542
562
  navigate,
@@ -549,7 +569,6 @@ export async function renderRoute(
549
569
  return Object.assign(result, { router });
550
570
  }
551
571
 
552
- /** Minimal RscMetadata for client-side re-renders (no server-only fields). */
553
572
  function makeMetadata(
554
573
  pathname: string,
555
574
  segments: ResolvedSegment[],