@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1c0bdfad

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 (406) hide show
  1. package/AGENTS.md +17 -0
  2. package/README.md +291 -61
  3. package/dist/bin/rango.js +544 -143
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3744 -1329
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +67 -13
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +312 -0
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +247 -23
  12. package/skills/caching/SKILL.md +322 -19
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/debug-manifest/SKILL.md +4 -2
  16. package/skills/document-cache/SKILL.md +78 -55
  17. package/skills/handler-use/SKILL.md +364 -0
  18. package/skills/hooks/SKILL.md +282 -60
  19. package/skills/host-router/SKILL.md +278 -0
  20. package/skills/i18n/SKILL.md +276 -0
  21. package/skills/intercept/SKILL.md +50 -6
  22. package/skills/layout/SKILL.md +35 -9
  23. package/skills/links/SKILL.md +249 -17
  24. package/skills/loader/SKILL.md +297 -31
  25. package/skills/middleware/SKILL.md +52 -13
  26. package/skills/migrate-nextjs/SKILL.md +584 -0
  27. package/skills/migrate-react-router/SKILL.md +771 -0
  28. package/skills/mime-routes/SKILL.md +28 -1
  29. package/skills/observability/SKILL.md +172 -0
  30. package/skills/parallel/SKILL.md +203 -7
  31. package/skills/prerender/SKILL.md +155 -111
  32. package/skills/rango/SKILL.md +251 -23
  33. package/skills/react-compiler/SKILL.md +168 -0
  34. package/skills/response-routes/SKILL.md +123 -48
  35. package/skills/route/SKILL.md +104 -9
  36. package/skills/router-setup/SKILL.md +124 -11
  37. package/skills/scripts/SKILL.md +179 -0
  38. package/skills/server-actions/SKILL.md +775 -0
  39. package/skills/streams-and-websockets/SKILL.md +283 -0
  40. package/skills/tailwind/SKILL.md +27 -3
  41. package/skills/testing/SKILL.md +125 -222
  42. package/skills/testing/bindings.md +103 -0
  43. package/skills/testing/cache-prerender.md +127 -0
  44. package/skills/testing/client-components.md +124 -0
  45. package/skills/testing/e2e-parity.md +125 -0
  46. package/skills/testing/flight.md +91 -0
  47. package/skills/testing/handles.md +129 -0
  48. package/skills/testing/loader.md +128 -0
  49. package/skills/testing/middleware.md +99 -0
  50. package/skills/testing/render-handler.md +121 -0
  51. package/skills/testing/response-routes.md +95 -0
  52. package/skills/testing/reverse-and-types.md +84 -0
  53. package/skills/testing/server-actions.md +107 -0
  54. package/skills/testing/server-tree.md +128 -0
  55. package/skills/testing/setup.md +123 -0
  56. package/skills/typesafety/SKILL.md +357 -52
  57. package/skills/use-cache/SKILL.md +46 -14
  58. package/skills/view-transitions/SKILL.md +294 -0
  59. package/src/__augment-tests__/augment.ts +81 -0
  60. package/src/__augment-tests__/augmented.check.ts +116 -0
  61. package/src/__internal.ts +67 -40
  62. package/src/bin/rango.ts +18 -0
  63. package/src/browser/action-coordinator.ts +53 -36
  64. package/src/browser/action-fence.ts +47 -0
  65. package/src/browser/app-shell.ts +39 -0
  66. package/src/browser/app-version.ts +14 -0
  67. package/src/browser/connection-warmup.ts +134 -0
  68. package/src/browser/cookie-name.ts +140 -0
  69. package/src/browser/event-controller.ts +197 -150
  70. package/src/browser/history-state.ts +21 -0
  71. package/src/browser/index.ts +3 -3
  72. package/src/browser/invalidate-client-cache.ts +52 -0
  73. package/src/browser/link-interceptor.ts +4 -0
  74. package/src/browser/navigation-bridge.ts +200 -30
  75. package/src/browser/navigation-client.ts +217 -58
  76. package/src/browser/navigation-store-handle.ts +38 -0
  77. package/src/browser/navigation-store.ts +76 -67
  78. package/src/browser/navigation-transaction.ts +18 -66
  79. package/src/browser/network-error-handler.ts +34 -7
  80. package/src/browser/partial-update.ts +187 -112
  81. package/src/browser/prefetch/cache.ts +312 -30
  82. package/src/browser/prefetch/fetch.ts +344 -47
  83. package/src/browser/prefetch/policy.ts +6 -0
  84. package/src/browser/prefetch/queue.ts +126 -20
  85. package/src/browser/prefetch/resource-ready.ts +77 -0
  86. package/src/browser/rango-state.ts +158 -76
  87. package/src/browser/react/Link.tsx +125 -18
  88. package/src/browser/react/NavigationProvider.tsx +135 -120
  89. package/src/browser/react/ScrollRestoration.tsx +10 -6
  90. package/src/browser/react/context.ts +7 -2
  91. package/src/browser/react/filter-segment-order.ts +66 -7
  92. package/src/browser/react/index.ts +0 -48
  93. package/src/browser/react/location-state-shared.ts +178 -8
  94. package/src/browser/react/location-state.ts +39 -14
  95. package/src/browser/react/use-action.ts +6 -15
  96. package/src/browser/react/use-handle.ts +23 -69
  97. package/src/browser/react/use-href.tsx +8 -1
  98. package/src/browser/react/use-link-status.ts +33 -8
  99. package/src/browser/react/use-navigation.ts +32 -7
  100. package/src/browser/react/use-params.ts +20 -10
  101. package/src/browser/react/use-reverse.ts +106 -0
  102. package/src/browser/react/use-router.ts +46 -11
  103. package/src/browser/react/use-search-params.ts +0 -5
  104. package/src/browser/react/use-segments.ts +11 -21
  105. package/src/browser/response-adapter.ts +80 -5
  106. package/src/browser/rsc-router.tsx +226 -75
  107. package/src/browser/scroll-restoration.ts +54 -42
  108. package/src/browser/segment-reconciler.ts +36 -9
  109. package/src/browser/segment-structure-assert.ts +2 -2
  110. package/src/browser/server-action-bridge.ts +619 -442
  111. package/src/browser/types.ts +115 -11
  112. package/src/browser/validate-redirect-origin.ts +43 -16
  113. package/src/build/collect-fallback-refs.ts +107 -0
  114. package/src/build/generate-manifest.ts +65 -40
  115. package/src/build/generate-route-types.ts +7 -1
  116. package/src/build/index.ts +8 -2
  117. package/src/build/prefix-tree-utils.ts +123 -0
  118. package/src/build/route-trie.ts +182 -37
  119. package/src/build/route-types/ast-route-extraction.ts +15 -8
  120. package/src/build/route-types/codegen.ts +16 -5
  121. package/src/build/route-types/include-resolution.ts +125 -24
  122. package/src/build/route-types/param-extraction.ts +6 -3
  123. package/src/build/route-types/per-module-writer.ts +22 -6
  124. package/src/build/route-types/router-processing.ts +392 -106
  125. package/src/build/route-types/scan-filter.ts +9 -2
  126. package/src/build/route-types/source-scan.ts +216 -0
  127. package/src/build/runtime-discovery.ts +9 -20
  128. package/src/cache/cache-error.ts +104 -0
  129. package/src/cache/cache-key-utils.ts +29 -13
  130. package/src/cache/cache-policy.ts +108 -34
  131. package/src/cache/cache-runtime.ts +214 -48
  132. package/src/cache/cache-scope.ts +236 -89
  133. package/src/cache/cache-tag.ts +103 -0
  134. package/src/cache/cf/cf-base64.ts +33 -0
  135. package/src/cache/cf/cf-cache-constants.ts +127 -0
  136. package/src/cache/cf/cf-cache-store.ts +2224 -171
  137. package/src/cache/cf/cf-cache-types.ts +349 -0
  138. package/src/cache/cf/cf-kv-utils.ts +46 -0
  139. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  140. package/src/cache/cf/index.ts +11 -17
  141. package/src/cache/document-cache.ts +89 -27
  142. package/src/cache/handle-snapshot.ts +70 -0
  143. package/src/cache/index.ts +11 -20
  144. package/src/cache/memory-segment-store.ts +136 -37
  145. package/src/cache/profile-registry.ts +31 -31
  146. package/src/cache/read-through-swr.ts +41 -11
  147. package/src/cache/segment-codec.ts +9 -17
  148. package/src/cache/tag-invalidation.ts +230 -0
  149. package/src/cache/taint.ts +55 -0
  150. package/src/cache/types.ts +37 -100
  151. package/src/client.rsc.tsx +45 -21
  152. package/src/client.tsx +120 -336
  153. package/src/cloudflare/index.ts +11 -0
  154. package/src/cloudflare/tracing.ts +109 -0
  155. package/src/component-utils.ts +19 -0
  156. package/src/components/DefaultDocument.tsx +8 -2
  157. package/src/context-var.ts +84 -2
  158. package/src/debug.ts +2 -2
  159. package/src/decode-loader-results.ts +52 -0
  160. package/src/defer.ts +196 -0
  161. package/src/deps/ssr.ts +0 -1
  162. package/src/encode-kv.ts +49 -0
  163. package/src/errors.ts +30 -4
  164. package/src/escape-script.ts +52 -0
  165. package/src/handle.ts +70 -22
  166. package/src/handles/MetaTags.tsx +56 -19
  167. package/src/handles/Scripts.tsx +183 -0
  168. package/src/handles/breadcrumbs.ts +95 -0
  169. package/src/handles/is-thenable.ts +19 -0
  170. package/src/handles/meta.ts +51 -40
  171. package/src/handles/script.ts +244 -0
  172. package/src/host/cookie-handler.ts +9 -60
  173. package/src/host/errors.ts +0 -24
  174. package/src/host/index.ts +8 -5
  175. package/src/host/pattern-matcher.ts +23 -52
  176. package/src/host/router.ts +107 -99
  177. package/src/host/testing.ts +40 -27
  178. package/src/host/types.ts +37 -4
  179. package/src/host/utils.ts +1 -1
  180. package/src/href-client.ts +137 -22
  181. package/src/index.rsc.ts +79 -29
  182. package/src/index.ts +149 -65
  183. package/src/internal-debug.ts +11 -10
  184. package/src/loader-store.ts +500 -0
  185. package/src/loader.rsc.ts +20 -13
  186. package/src/loader.ts +12 -11
  187. package/src/missing-id-error.ts +68 -0
  188. package/src/outlet-context.ts +1 -1
  189. package/src/outlet-provider.tsx +1 -5
  190. package/src/prerender/param-hash.ts +16 -16
  191. package/src/prerender/store.ts +63 -26
  192. package/src/prerender.ts +198 -82
  193. package/src/redirect-origin.ts +100 -0
  194. package/src/regex-escape.ts +8 -0
  195. package/src/render-error-thrower.tsx +20 -0
  196. package/src/response-utils.ts +62 -0
  197. package/src/reverse.ts +65 -15
  198. package/src/root-error-boundary.tsx +1 -19
  199. package/src/route-content-wrapper.tsx +7 -72
  200. package/src/route-definition/dsl-helpers.ts +469 -276
  201. package/src/route-definition/helper-factories.ts +29 -139
  202. package/src/route-definition/helpers-types.ts +113 -37
  203. package/src/route-definition/index.ts +3 -3
  204. package/src/route-definition/redirect.ts +53 -12
  205. package/src/route-definition/resolve-handler-use.ts +161 -0
  206. package/src/route-definition/use-item-types.ts +32 -0
  207. package/src/route-map-builder.ts +7 -17
  208. package/src/route-types.ts +37 -41
  209. package/src/router/basename.ts +14 -0
  210. package/src/router/content-negotiation.ts +164 -17
  211. package/src/router/error-handling.ts +45 -18
  212. package/src/router/find-match.ts +45 -22
  213. package/src/router/handler-context.ts +110 -39
  214. package/src/router/instrument.ts +350 -0
  215. package/src/router/intercept-resolution.ts +50 -24
  216. package/src/router/lazy-includes.ts +19 -53
  217. package/src/router/loader-resolution.ts +274 -56
  218. package/src/router/logging.ts +5 -8
  219. package/src/router/manifest.ts +49 -45
  220. package/src/router/match-api.ts +121 -205
  221. package/src/router/match-context.ts +0 -22
  222. package/src/router/match-handlers.ts +58 -58
  223. package/src/router/match-middleware/background-revalidation.ts +33 -6
  224. package/src/router/match-middleware/cache-lookup.ts +214 -263
  225. package/src/router/match-middleware/cache-store.ts +73 -33
  226. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  227. package/src/router/match-middleware/segment-resolution.ts +52 -18
  228. package/src/router/match-pipelines.ts +1 -42
  229. package/src/router/match-result.ts +104 -49
  230. package/src/router/metrics.ts +217 -26
  231. package/src/router/middleware-types.ts +24 -110
  232. package/src/router/middleware.ts +384 -197
  233. package/src/router/navigation-snapshot.ts +131 -0
  234. package/src/router/params-util.ts +23 -0
  235. package/src/router/pattern-matching.ts +148 -91
  236. package/src/router/prefetch-cache-ttl.ts +51 -0
  237. package/src/router/prerender-match.ts +199 -56
  238. package/src/router/preview-match.ts +32 -102
  239. package/src/router/request-classification.ts +276 -0
  240. package/src/router/revalidation.ts +144 -74
  241. package/src/router/route-snapshot.ts +244 -0
  242. package/src/router/router-context.ts +8 -28
  243. package/src/router/router-interfaces.ts +129 -36
  244. package/src/router/router-options.ts +185 -23
  245. package/src/router/router-registry.ts +2 -5
  246. package/src/router/segment-resolution/fresh.ts +281 -76
  247. package/src/router/segment-resolution/helpers.ts +116 -31
  248. package/src/router/segment-resolution/loader-cache.ts +63 -37
  249. package/src/router/segment-resolution/revalidation.ts +493 -391
  250. package/src/router/segment-resolution/static-store.ts +19 -5
  251. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  252. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  253. package/src/router/segment-resolution.ts +5 -1
  254. package/src/router/segment-wrappers.ts +8 -5
  255. package/src/router/state-cookie-name.ts +33 -0
  256. package/src/router/substitute-pattern-params.ts +56 -0
  257. package/src/router/telemetry-otel.ts +161 -199
  258. package/src/router/telemetry.ts +96 -19
  259. package/src/router/timeout.ts +0 -20
  260. package/src/router/tracing.ts +206 -0
  261. package/src/router/trie-matching.ts +180 -58
  262. package/src/router/types.ts +10 -63
  263. package/src/router/url-params.ts +44 -0
  264. package/src/router.ts +182 -54
  265. package/src/rsc/handler-context.ts +3 -2
  266. package/src/rsc/handler.ts +702 -460
  267. package/src/rsc/helpers.ts +168 -46
  268. package/src/rsc/index.ts +2 -25
  269. package/src/rsc/json-route-result.ts +38 -0
  270. package/src/rsc/loader-fetch.ts +127 -31
  271. package/src/rsc/manifest-init.ts +33 -42
  272. package/src/rsc/origin-guard.ts +39 -25
  273. package/src/rsc/progressive-enhancement.ts +98 -19
  274. package/src/rsc/redirect-guard.ts +99 -0
  275. package/src/rsc/response-cache-serve.ts +238 -0
  276. package/src/rsc/response-error.ts +79 -12
  277. package/src/rsc/response-route-handler.ts +99 -189
  278. package/src/rsc/rsc-rendering.ts +126 -106
  279. package/src/rsc/runtime-warnings.ts +23 -10
  280. package/src/rsc/server-action.ts +269 -114
  281. package/src/rsc/ssr-setup.ts +144 -0
  282. package/src/rsc/types.ts +34 -6
  283. package/src/runtime-env.ts +18 -0
  284. package/src/search-params.ts +49 -41
  285. package/src/segment-content-promise.ts +67 -0
  286. package/src/segment-loader-promise.ts +149 -0
  287. package/src/segment-system.tsx +281 -129
  288. package/src/serialize.ts +243 -0
  289. package/src/server/context.ts +317 -63
  290. package/src/server/cookie-parse.ts +32 -0
  291. package/src/server/cookie-store.ts +80 -5
  292. package/src/server/handle-store.ts +40 -38
  293. package/src/server/loader-registry.ts +26 -46
  294. package/src/server/request-context.ts +425 -177
  295. package/src/server.ts +6 -0
  296. package/src/ssr/index.tsx +25 -16
  297. package/src/static-handler.ts +27 -18
  298. package/src/testing/cache-status.ts +162 -0
  299. package/src/testing/collect-handle.ts +40 -0
  300. package/src/testing/dispatch.ts +701 -0
  301. package/src/testing/dom.entry.ts +22 -0
  302. package/src/testing/e2e/fixture.ts +188 -0
  303. package/src/testing/e2e/index.ts +128 -0
  304. package/src/testing/e2e/matchers.ts +35 -0
  305. package/src/testing/e2e/page-helpers.ts +272 -0
  306. package/src/testing/e2e/parity.ts +387 -0
  307. package/src/testing/e2e/server.ts +195 -0
  308. package/src/testing/flight-matchers.ts +97 -0
  309. package/src/testing/flight-normalize.ts +11 -0
  310. package/src/testing/flight-runtime.d.ts +57 -0
  311. package/src/testing/flight-tree.ts +682 -0
  312. package/src/testing/flight.entry.ts +52 -0
  313. package/src/testing/flight.ts +257 -0
  314. package/src/testing/generated-routes.ts +183 -0
  315. package/src/testing/index.ts +99 -0
  316. package/src/testing/internal/context.ts +371 -0
  317. package/src/testing/internal/flight-client-globals.ts +30 -0
  318. package/src/testing/internal/seed-vars.ts +54 -0
  319. package/src/testing/render-handler.ts +343 -0
  320. package/src/testing/render-route.tsx +581 -0
  321. package/src/testing/run-loader.ts +385 -0
  322. package/src/testing/run-middleware.ts +205 -0
  323. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  324. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  325. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  326. package/src/testing/vitest-stubs/version.ts +5 -0
  327. package/src/testing/vitest.ts +305 -0
  328. package/src/theme/ThemeProvider.tsx +20 -58
  329. package/src/theme/ThemeScript.tsx +7 -9
  330. package/src/theme/constants.ts +52 -13
  331. package/src/theme/index.ts +3 -19
  332. package/src/theme/theme-context.ts +1 -5
  333. package/src/theme/theme-script.ts +22 -21
  334. package/src/theme/use-theme.ts +0 -3
  335. package/src/types/boundaries.ts +0 -35
  336. package/src/types/cache-types.ts +17 -8
  337. package/src/types/error-types.ts +30 -90
  338. package/src/types/global-namespace.ts +54 -41
  339. package/src/types/handler-context.ts +236 -88
  340. package/src/types/index.ts +1 -10
  341. package/src/types/loader-types.ts +44 -15
  342. package/src/types/request-scope.ts +112 -0
  343. package/src/types/route-config.ts +10 -45
  344. package/src/types/route-entry.ts +19 -7
  345. package/src/types/segments.ts +37 -19
  346. package/src/urls/include-helper.ts +33 -70
  347. package/src/urls/index.ts +1 -11
  348. package/src/urls/path-helper-types.ts +58 -11
  349. package/src/urls/path-helper.ts +57 -111
  350. package/src/urls/pattern-types.ts +48 -19
  351. package/src/urls/response-types.ts +25 -22
  352. package/src/urls/type-extraction.ts +58 -139
  353. package/src/urls/urls-function.ts +1 -18
  354. package/src/use-loader.tsx +346 -89
  355. package/src/vite/debug.ts +185 -0
  356. package/src/vite/discovery/bundle-postprocess.ts +64 -91
  357. package/src/vite/discovery/discover-routers.ts +147 -88
  358. package/src/vite/discovery/discovery-errors.ts +194 -0
  359. package/src/vite/discovery/gate-state.ts +171 -0
  360. package/src/vite/discovery/prerender-collection.ts +247 -145
  361. package/src/vite/discovery/route-types-writer.ts +40 -84
  362. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  363. package/src/vite/discovery/state.ts +61 -13
  364. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  365. package/src/vite/index.ts +10 -3
  366. package/src/vite/inject-client-debug.ts +36 -0
  367. package/src/vite/plugin-types.ts +155 -65
  368. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  369. package/src/vite/plugins/client-ref-dedup.ts +120 -0
  370. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  371. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  372. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  373. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  374. package/src/vite/plugins/expose-action-id.ts +49 -98
  375. package/src/vite/plugins/expose-id-utils.ts +96 -51
  376. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  377. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  378. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  379. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  380. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  381. package/src/vite/plugins/performance-tracks.ts +89 -0
  382. package/src/vite/plugins/refresh-cmd.ts +127 -0
  383. package/src/vite/plugins/use-cache-transform.ts +73 -83
  384. package/src/vite/plugins/version-injector.ts +21 -25
  385. package/src/vite/plugins/version-plugin.ts +46 -37
  386. package/src/vite/plugins/virtual-entries.ts +13 -18
  387. package/src/vite/rango.ts +241 -287
  388. package/src/vite/router-discovery.ts +956 -149
  389. package/src/vite/utils/ast-handler-extract.ts +26 -35
  390. package/src/vite/utils/banner.ts +4 -4
  391. package/src/vite/utils/bundle-analysis.ts +10 -15
  392. package/src/vite/utils/client-chunks.ts +184 -0
  393. package/src/vite/utils/directive-prologue.ts +40 -0
  394. package/src/vite/utils/forward-user-plugins.ts +171 -0
  395. package/src/vite/utils/manifest-utils.ts +4 -59
  396. package/src/vite/utils/package-resolution.ts +20 -52
  397. package/src/vite/utils/prerender-utils.ts +141 -34
  398. package/src/vite/utils/shared-utils.ts +92 -42
  399. package/CLAUDE.md +0 -5
  400. package/src/browser/action-response-classifier.ts +0 -99
  401. package/src/browser/react/use-client-cache.ts +0 -58
  402. package/src/browser/shallow.ts +0 -40
  403. package/src/handles/index.ts +0 -6
  404. package/src/network-error-thrower.tsx +0 -23
  405. package/src/route-definition/route-function.ts +0 -119
  406. package/src/router/middleware-cookies.ts +0 -55
package/src/rsc/types.ts CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { ResolvedSegment, SlotState } from "../types.js";
9
9
  import type { HandleData } from "../server/handle-store.js";
10
- import type { RSCRouterInternal } from "../router/router-interfaces.js";
10
+ import type { RangoInternal } from "../router/router-interfaces.js";
11
11
  import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
12
12
 
13
13
  /**
@@ -19,10 +19,19 @@ export interface RscPayload {
19
19
  metadata?: {
20
20
  pathname: string;
21
21
  segments: ResolvedSegment[];
22
+ /** Router instance ID. When this changes between navigations, the client
23
+ * discards cached segments and does a full tree replacement (app switch). */
24
+ routerId?: string;
22
25
  isPartial?: boolean;
23
26
  isError?: boolean;
24
27
  matched?: string[];
25
28
  diff?: string[];
29
+ /**
30
+ * All segment ids re-resolved on the server, including null-component
31
+ * ones excluded from `segments`/`diff`. Drives client-side handle-bucket
32
+ * cleanup. Superset of `diff`. See MatchResult.resolvedIds.
33
+ */
34
+ resolvedIds?: string[];
26
35
  /** Merged route params from the matched route */
27
36
  params?: Record<string, string>;
28
37
  slots?: Record<string, SlotState>;
@@ -32,19 +41,35 @@ export interface RscPayload {
32
41
  handles?: AsyncGenerator<HandleData, void, unknown>;
33
42
  /** RSC version string for cache invalidation */
34
43
  version?: string;
44
+ /** TTL in milliseconds for the client-side in-memory prefetch cache */
45
+ prefetchCacheTTL?: number;
46
+ /** Server-resolved rango state cookie name; the client reads it verbatim. */
47
+ stateCookieName?: string;
35
48
  /** Theme configuration for FOUC prevention */
36
49
  themeConfig?: ResolvedThemeConfig | null;
37
50
  /** Initial theme from cookie (for SSR hydration) */
38
51
  initialTheme?: Theme;
52
+ /** URL prefix for all routes (from createRouter({ basename })). */
53
+ basename?: string;
39
54
  /** Whether connection warmup is enabled */
40
55
  warmupEnabled?: boolean;
41
- /** Server-side redirect with optional state (for partial requests) */
42
- redirect?: { url: string };
56
+ /**
57
+ * Whether the client should hydrate inside React.StrictMode. Carried on
58
+ * the initial full-render payload only; the browser entry reads it once at
59
+ * hydration. Absent on partial (navigation) payloads. Defaults to true on
60
+ * the client when omitted.
61
+ */
62
+ strictMode?: boolean;
63
+ /**
64
+ * Server-side redirect with optional state (for partial requests).
65
+ * `external: true` (from redirect(url, { external: true })) tells the client
66
+ * to hard-navigate to an off-host target instead of validating same-origin.
67
+ */
68
+ redirect?: { url: string; external?: boolean };
43
69
  /** Server-set location state to include in history.pushState */
44
70
  locationState?: Record<string, unknown>;
45
71
  };
46
72
  returnValue?: { ok: boolean; data: unknown };
47
- formState?: unknown;
48
73
  }
49
74
 
50
75
  /**
@@ -61,7 +86,10 @@ export interface RSCDependencies {
61
86
  */
62
87
  renderToReadableStream: <T>(
63
88
  payload: T,
64
- options?: { temporaryReferences?: unknown },
89
+ options?: {
90
+ temporaryReferences?: unknown;
91
+ onError?: (error: unknown) => string | void;
92
+ },
65
93
  ) => ReadableStream<Uint8Array>;
66
94
 
67
95
  /**
@@ -169,7 +197,7 @@ export interface CreateRSCHandlerOptions<
169
197
  /**
170
198
  * The RSC router instance
171
199
  */
172
- router: RSCRouterInternal<TEnv, TRoutes>;
200
+ router: RangoInternal<TEnv, TRoutes>;
173
201
 
174
202
  /**
175
203
  * RSC dependencies from @vitejs/plugin-rsc/rsc.
@@ -0,0 +1,18 @@
1
+ // Runtime-safe detection of a test runner (Vitest), used to decide whether a
2
+ // create*() call with no plugin-injected $$id may fall back to a synthetic id (a
3
+ // bare test) or must fail loud (dev / a real build).
4
+ //
5
+ // `process` is absent in some target runtimes (the browser, certain edge/worker
6
+ // RSC environments), so probe it through `globalThis` with optional chaining —
7
+ // NEVER a bare `process.env.VITEST`, which would ReferenceError before the
8
+ // intended error is thrown. Unlike `process.env.NODE_ENV` (folded by the app's
9
+ // build `define`), `VITEST` is not folded, so this stays a small runtime check;
10
+ // it lives only on the create*() error path (id missing), which never runs in a
11
+ // correct production build.
12
+ //
13
+ // Vitest sets `VITEST` in every test process — the node project and the
14
+ // react-server forks alike (the RSC project forces NODE_ENV=production, so NODE_ENV
15
+ // cannot distinguish it from a real build; `VITEST` can). A real build never sets it.
16
+ export function isUnderTestRunner(): boolean {
17
+ return !!globalThis.process?.env?.VITEST;
18
+ }
@@ -7,9 +7,16 @@
7
7
  * URLSearchParams instance.
8
8
  */
9
9
 
10
- // ============================================================================
11
- // Schema Types
12
- // ============================================================================
10
+ import { encodeKV } from "./encode-kv.js";
11
+
12
+ /**
13
+ * Decimal-number grammar for `"number"` search params: optional sign, digits
14
+ * with optional fraction, optional exponent. Deliberately excludes hex (`0x`),
15
+ * `Infinity`, and empty/whitespace so `Number()`'s lenient coercions
16
+ * (`Number("")===0`, `Number("0x10")===16`, `Number("Infinity")===Infinity`)
17
+ * do not slip non-decimal values into typed search.
18
+ */
19
+ const DECIMAL_NUMBER_RE = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
13
20
 
14
21
  /** Supported scalar types for search params (append ? for optional). */
15
22
  export type SearchSchemaValue =
@@ -23,10 +30,6 @@ export type SearchSchemaValue =
23
30
  /** A search schema maps param names to their type descriptors. */
24
31
  export type SearchSchema = Record<string, SearchSchemaValue>;
25
32
 
26
- // ============================================================================
27
- // Type-Level Schema Resolution
28
- // ============================================================================
29
-
30
33
  /** Strip trailing `?` from a schema value to get the base type. */
31
34
  type BaseType<T extends string> = T extends `${infer B}?` ? B : T;
32
35
 
@@ -55,14 +58,22 @@ type Simplify<T> = { [K in keyof T]: T[K] };
55
58
  /**
56
59
  * Resolve a SearchSchema to its typed object.
57
60
  *
61
+ * Both required and optional params resolve to `T | undefined` at the handler
62
+ * level. The required/optional distinction is a consumer-facing contract
63
+ * (e.g., for href() and reverse() autocomplete) — it tells callers which
64
+ * params the route expects, but the handler must still check for undefined
65
+ * since the framework cannot trust the client to send all required params.
66
+ *
58
67
  * @example
59
68
  * type S = { q: "string"; page: "number?"; sort: "string?" };
60
69
  * type R = ResolveSearchSchema<S>;
61
- * // { q: string; page?: number; sort?: string }
70
+ * // { q: string | undefined; page?: number; sort?: string }
62
71
  */
63
72
  export type ResolveSearchSchema<T extends SearchSchema> = Simplify<
64
73
  {
65
- [K in RequiredKeys<T> & string]: ResolveBaseType<BaseType<T[K]>>;
74
+ [K in RequiredKeys<T> & string]:
75
+ | ResolveBaseType<BaseType<T[K]>>
76
+ | undefined;
66
77
  } & {
67
78
  [K in OptionalKeys<T> & string]?: ResolveBaseType<BaseType<T[K]>>;
68
79
  }
@@ -73,11 +84,11 @@ export type ResolveSearchSchema<T extends SearchSchema> = Simplify<
73
84
  // ============================================================================
74
85
 
75
86
  /** Resolve the global route map from RegisteredRoutes or GeneratedRouteMap. */
76
- type GlobalRouteMap = keyof RSCRouter.RegisteredRoutes extends never
77
- ? keyof RSCRouter.GeneratedRouteMap extends never
87
+ type GlobalRouteMap = keyof Rango.RegisteredRoutes extends never
88
+ ? keyof Rango.GeneratedRouteMap extends never
78
89
  ? Record<string, string>
79
- : RSCRouter.GeneratedRouteMap
80
- : RSCRouter.RegisteredRoutes;
90
+ : Rango.GeneratedRouteMap
91
+ : Rango.RegisteredRoutes;
81
92
 
82
93
  /**
83
94
  * Extract the resolved search params type for a named route.
@@ -155,18 +166,20 @@ type ExtractParamsFromPattern<T extends string> =
155
166
  : { [K in Param]: string }
156
167
  : {};
157
168
 
158
- // ============================================================================
159
- // Runtime Parser
160
- // ============================================================================
161
-
162
169
  /**
163
170
  * Parse URLSearchParams into a typed object using the given schema.
164
171
  *
165
172
  * - `"string"` / `"string?"` - kept as-is
166
- * - `"number"` / `"number?"` - coerced via `Number()`; NaN treated as missing
173
+ * - `"number"` / `"number?"` - parsed as a finite decimal number. Accepts an
174
+ * optional sign, digits with optional fraction, and optional exponent
175
+ * (e.g. `42`, `-3.5`, `1e3`). Empty/whitespace-only, non-decimal forms
176
+ * (`0x10`), and non-finite (`Infinity`) are treated as missing (omitted),
177
+ * NOT coerced to `0`/`16`/`Infinity`.
167
178
  * - `"boolean"` / `"boolean?"` - `"true"` / `"1"` -> true, `"false"` / `"0"` / `""` -> false
168
179
  *
169
- * Missing required params are set to their zero value (empty string / 0 / false).
180
+ * Missing params (both required and optional) are omitted from the result
181
+ * (undefined). The required/optional distinction is a consumer-facing contract
182
+ * only — the handler must check for undefined.
170
183
  */
171
184
  export function parseSearchParams<T extends SearchSchema>(
172
185
  searchParams: URLSearchParams,
@@ -180,25 +193,24 @@ export function parseSearchParams<T extends SearchSchema>(
180
193
  const raw = searchParams.get(key);
181
194
 
182
195
  if (raw === null) {
183
- if (!isOptional) {
184
- // Required param missing: use zero value
185
- if (baseType === "string") result[key] = "";
186
- else if (baseType === "number") result[key] = 0;
187
- else if (baseType === "boolean") result[key] = false;
188
- }
189
- // Optional params are omitted (undefined)
196
+ // Missing params are omitted (undefined) regardless of required/optional
190
197
  continue;
191
198
  }
192
199
 
193
200
  if (baseType === "string") {
194
201
  result[key] = raw;
195
202
  } else if (baseType === "number") {
196
- const num = Number(raw);
197
- if (Number.isNaN(num)) {
198
- if (!isOptional) result[key] = 0;
199
- } else {
200
- result[key] = num;
203
+ // Trim, then require a valid decimal numeral and a finite result.
204
+ // Empty/whitespace, hex (0x10), and Infinity are treated as missing
205
+ // (omitted) — not coerced to 0/16/Infinity by Number()'s lenient rules.
206
+ const trimmed = raw.trim();
207
+ if (trimmed !== "" && DECIMAL_NUMBER_RE.test(trimmed)) {
208
+ const num = Number(trimmed);
209
+ if (Number.isFinite(num)) {
210
+ result[key] = num;
211
+ }
201
212
  }
213
+ // Anything else treated as missing (undefined)
202
214
  } else if (baseType === "boolean") {
203
215
  result[key] = raw === "true" || raw === "1";
204
216
  }
@@ -207,21 +219,17 @@ export function parseSearchParams<T extends SearchSchema>(
207
219
  return result as ResolveSearchSchema<T>;
208
220
  }
209
221
 
210
- // ============================================================================
211
- // Runtime Serializer
212
- // ============================================================================
213
-
214
222
  /**
215
223
  * Serialize a typed search params object to a query string (without leading `?`).
216
- * Skips `undefined` and `null` values.
224
+ * Skips `undefined` and `null` values. Preserves insertion order (no sort).
217
225
  */
218
226
  export function serializeSearchParams(params: Record<string, unknown>): string {
219
- const parts: string[] = [];
227
+ // Pre-filter null/undefined and coerce values to strings here so encodeKV
228
+ // (which never inspects values) reproduces this call site's exact output.
229
+ const pairs: [string, string][] = [];
220
230
  for (const [key, value] of Object.entries(params)) {
221
231
  if (value === undefined || value === null) continue;
222
- parts.push(
223
- `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`,
224
- );
232
+ pairs.push([key, String(value)]);
225
233
  }
226
- return parts.join("&");
234
+ return encodeKV(pairs);
227
235
  }
@@ -0,0 +1,67 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ /**
4
+ * Stable Promise wrappers keyed on the component itself. Objects (React
5
+ * elements, functions, lazy payloads) land in a WeakMap so entries GC when
6
+ * the underlying component is released; primitives (string, number, boolean,
7
+ * null) land in a Map so memoization still applies to text-/null-backed
8
+ * segments like those in partial-update flows. Keeping this cache outside
9
+ * the segment eliminates preservation fields on ResolvedSegment — it survives
10
+ * reconciliation naturally because the component ref is what's stable.
11
+ *
12
+ * Browser-only. On the server each SSR render needs a fresh pending promise
13
+ * so Suspense can emit the loading fallback HTML before content streams. A
14
+ * shared already-resolved promise has `.status === "fulfilled"` attached by
15
+ * React on its first observation — subsequent `use()` calls return
16
+ * synchronously without suspending, so the Suspense fallback never makes it
17
+ * into the initial HTML. Route-definition components share refs across
18
+ * requests, so a global cache would leak tracked state between renders.
19
+ */
20
+ const IS_BROWSER = typeof window !== "undefined";
21
+ const objectContentCache = IS_BROWSER
22
+ ? new WeakMap<object, Promise<ReactNode>>()
23
+ : null;
24
+ const primitiveContentCache = IS_BROWSER
25
+ ? new Map<unknown, Promise<ReactNode>>()
26
+ : null;
27
+
28
+ /**
29
+ * Return a stable Promise wrapping `component`, memoized on the component ref.
30
+ *
31
+ * A fresh `Promise.resolve(component)` each render would suspend for one
32
+ * microtask and briefly commit the loading fallback inside Suspender — the
33
+ * intercept / parallel-slot flicker this indirection prevents. Reusing the
34
+ * same Promise ref keeps React's `use()` in "known fulfilled" state after
35
+ * the first observation.
36
+ *
37
+ * @internal
38
+ */
39
+ export function getMemoizedContentPromise(
40
+ component: ReactNode,
41
+ ): Promise<ReactNode> {
42
+ if (component instanceof Promise) {
43
+ return component as Promise<ReactNode>;
44
+ }
45
+
46
+ if (!objectContentCache || !primitiveContentCache) {
47
+ return Promise.resolve(component);
48
+ }
49
+
50
+ if (component !== null && typeof component === "object") {
51
+ const cached = objectContentCache.get(component);
52
+ if (cached) {
53
+ return cached;
54
+ }
55
+ const promise = Promise.resolve(component);
56
+ objectContentCache.set(component, promise);
57
+ return promise;
58
+ }
59
+
60
+ const cached = primitiveContentCache.get(component);
61
+ if (cached) {
62
+ return cached;
63
+ }
64
+ const promise = Promise.resolve(component);
65
+ primitiveContentCache.set(component, promise);
66
+ return promise;
67
+ }
@@ -0,0 +1,149 @@
1
+ import type { ResolvedSegment } from "./types.js";
2
+
3
+ /**
4
+ * Cache of aggregate Promise.all results keyed on the first loader's
5
+ * `loaderData` reference. Each entry holds the source refs it was built from
6
+ * plus the resulting Promise/array; lookup scans entries for the matching
7
+ * source array (typically a single entry, since distinct loader groups rarely
8
+ * share a first source). Object first-refs live in a WeakMap (auto-GC);
9
+ * primitive first-refs (strings/numbers/booleans/null) live in a Map so
10
+ * loaders that resolve to primitive data are memoized too. The per-key array
11
+ * is capped (MAX_ENTRIES_PER_KEY, oldest evicted) so a long session under a
12
+ * stable first-ref does not grow it without bound.
13
+ *
14
+ * Keying externally means reconciliation's fresh segment objects no longer
15
+ * drop memoization — the cache survives as long as the underlying loader
16
+ * segments do, and GC collects entries when those loaders are released
17
+ * (object keys only).
18
+ *
19
+ * Browser-only. On the server each SSR render needs a fresh Promise so
20
+ * Suspense can actually suspend and emit the loading fallback HTML before
21
+ * content streams. A shared already-resolved promise has `.status` attached
22
+ * by React on first `use()`; subsequent observations return synchronously
23
+ * and skip the fallback. The zero-loader case is especially prone because
24
+ * every empty-loader site would otherwise share one promise across requests.
25
+ */
26
+ const IS_BROWSER = typeof window !== "undefined";
27
+
28
+ interface LoaderCacheEntry {
29
+ sources: any[];
30
+ // buildLoaderPromise always returns a Promise, so the cached value is never a
31
+ // bare array. The public getMemoizedLoaderPromise return type stays broader
32
+ // (Promise<any[]> | any[]) to mirror its siblings.
33
+ promise: Promise<any[]>;
34
+ }
35
+
36
+ // Cap the per-key entries array. A stable first-ref (e.g. a layout loader whose
37
+ // loaderData object survives reconciliation across navigations) keeps its
38
+ // WeakMap/Map key alive, while a per-route loader whose ref changes each
39
+ // navigation appends a brand-new sources array under that same live key on
40
+ // every navigation. Nothing was ever removed, so the array grew linearly with
41
+ // navigation count, pinning each stale Promise + sources array from GC — a
42
+ // steady client-side leak over a long session. Only the current render's combo
43
+ // needs to stay warm; evict the oldest beyond the cap.
44
+ const MAX_ENTRIES_PER_KEY = 8;
45
+
46
+ const objectLoaderCache = IS_BROWSER
47
+ ? new WeakMap<object, LoaderCacheEntry[]>()
48
+ : null;
49
+ const primitiveLoaderCache = IS_BROWSER
50
+ ? new Map<unknown, LoaderCacheEntry[]>()
51
+ : null;
52
+
53
+ // In the browser, a single shared empty aggregate is safe (and desirable) —
54
+ // reusing the same resolved promise keeps React's `use()` in a known-fulfilled
55
+ // state across renders. On the server it would leak `.status = "fulfilled"`
56
+ // across requests and skip the Suspense fallback, so we rebuild on each call.
57
+ const SHARED_EMPTY_LOADER_PROMISE: Promise<any[]> | null = IS_BROWSER
58
+ ? Promise.resolve([])
59
+ : null;
60
+
61
+ function hasSameReferences(a: any[], b: any[]): boolean {
62
+ if (a.length !== b.length) {
63
+ return false;
64
+ }
65
+ for (let i = 0; i < a.length; i++) {
66
+ if (a[i] !== b[i]) {
67
+ return false;
68
+ }
69
+ }
70
+ return true;
71
+ }
72
+
73
+ /**
74
+ * Build a fresh aggregate Promise.all over the loaders' resolved data refs.
75
+ * Unlike getMemoizedLoaderPromise this never caches, so each call yields a new
76
+ * Promise — correct for sites that await the result immediately (a shared,
77
+ * already-resolved promise would leak React's `.status` across server requests
78
+ * and skip the Suspense fallback).
79
+ *
80
+ * @internal
81
+ */
82
+ export function buildLoaderPromise(loaders: ResolvedSegment[]): Promise<any[]> {
83
+ if (loaders.length === 0) {
84
+ return Promise.resolve([]);
85
+ }
86
+ return Promise.all(
87
+ loaders.map((loader) =>
88
+ loader.loaderData instanceof Promise
89
+ ? loader.loaderData
90
+ : Promise.resolve(loader.loaderData),
91
+ ),
92
+ );
93
+ }
94
+
95
+ function isObjectLike(value: unknown): value is object {
96
+ return (
97
+ value !== null && (typeof value === "object" || typeof value === "function")
98
+ );
99
+ }
100
+
101
+ /**
102
+ * Memoize an aggregate Promise.all for a set of loader segments. Reusing the
103
+ * same aggregate across renders — invalidated only when any underlying
104
+ * loader.loaderData ref changes — keeps React's `use()` in "known fulfilled"
105
+ * state and prevents a fresh Promise.all from suspending (and briefly
106
+ * committing the Suspense fallback) on every partial update that doesn't
107
+ * actually change loader data.
108
+ *
109
+ * @internal
110
+ */
111
+ export function getMemoizedLoaderPromise(
112
+ loaders: ResolvedSegment[],
113
+ ): Promise<any[]> | any[] {
114
+ if (loaders.length === 0) {
115
+ return SHARED_EMPTY_LOADER_PROMISE ?? buildLoaderPromise(loaders);
116
+ }
117
+ if (!objectLoaderCache || !primitiveLoaderCache) {
118
+ return buildLoaderPromise(loaders);
119
+ }
120
+
121
+ const sources = loaders.map((loader) => loader.loaderData);
122
+ const first = sources[0];
123
+ const entries = isObjectLike(first)
124
+ ? objectLoaderCache.get(first)
125
+ : primitiveLoaderCache.get(first);
126
+
127
+ if (entries) {
128
+ for (const entry of entries) {
129
+ if (hasSameReferences(entry.sources, sources)) {
130
+ return entry.promise;
131
+ }
132
+ }
133
+ }
134
+
135
+ const promise = buildLoaderPromise(loaders);
136
+ const newEntry: LoaderCacheEntry = { sources, promise };
137
+ if (entries) {
138
+ // Bound the array: drop the oldest entry before appending when at the cap.
139
+ if (entries.length >= MAX_ENTRIES_PER_KEY) {
140
+ entries.shift();
141
+ }
142
+ entries.push(newEntry);
143
+ } else if (isObjectLike(first)) {
144
+ objectLoaderCache.set(first, [newEntry]);
145
+ } else {
146
+ primitiveLoaderCache.set(first, [newEntry]);
147
+ }
148
+ return promise;
149
+ }