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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (356) hide show
  1. package/README.md +24 -9
  2. package/dist/bin/rango.js +157 -63
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +1584 -639
  5. package/package.json +71 -21
  6. package/skills/api-client/SKILL.md +211 -0
  7. package/skills/breadcrumbs/SKILL.md +60 -0
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +222 -30
  10. package/skills/caching/SKILL.md +263 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/css/SKILL.md +76 -0
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +3 -1
  15. package/skills/hooks/SKILL.md +235 -28
  16. package/skills/host-router/SKILL.md +122 -22
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +29 -5
  19. package/skills/layout/SKILL.md +13 -9
  20. package/skills/links/SKILL.md +173 -17
  21. package/skills/loader/SKILL.md +170 -23
  22. package/skills/middleware/SKILL.md +16 -10
  23. package/skills/migrate-nextjs/SKILL.md +38 -16
  24. package/skills/mime-routes/SKILL.md +27 -0
  25. package/skills/observability/SKILL.md +137 -0
  26. package/skills/parallel/SKILL.md +11 -7
  27. package/skills/prerender/SKILL.md +14 -33
  28. package/skills/rango/SKILL.md +250 -25
  29. package/skills/react-compiler/SKILL.md +168 -0
  30. package/skills/response-routes/SKILL.md +114 -47
  31. package/skills/route/SKILL.md +42 -5
  32. package/skills/router-setup/SKILL.md +3 -3
  33. package/skills/server-actions/SKILL.md +78 -42
  34. package/skills/tailwind/SKILL.md +27 -3
  35. package/skills/testing/SKILL.md +129 -0
  36. package/skills/testing/bindings.md +89 -0
  37. package/skills/testing/cache-prerender.md +124 -0
  38. package/skills/testing/client-components.md +122 -0
  39. package/skills/testing/e2e-parity.md +125 -0
  40. package/skills/testing/flight.md +92 -0
  41. package/skills/testing/handles.md +129 -0
  42. package/skills/testing/loader.md +128 -0
  43. package/skills/testing/middleware.md +99 -0
  44. package/skills/testing/render-handler.md +121 -0
  45. package/skills/testing/response-routes.md +95 -0
  46. package/skills/testing/reverse-and-types.md +84 -0
  47. package/skills/testing/server-actions.md +107 -0
  48. package/skills/testing/server-tree.md +128 -0
  49. package/skills/testing/setup.md +120 -0
  50. package/skills/typesafety/SKILL.md +316 -26
  51. package/skills/use-cache/SKILL.md +36 -5
  52. package/skills/vercel/SKILL.md +107 -0
  53. package/skills/view-transitions/SKILL.md +294 -0
  54. package/src/__augment-tests__/augment.ts +81 -0
  55. package/src/__augment-tests__/augmented.check.ts +116 -0
  56. package/src/__internal.ts +0 -65
  57. package/src/browser/action-coordinator.ts +53 -36
  58. package/src/browser/action-fence.ts +47 -0
  59. package/src/browser/app-shell.ts +14 -27
  60. package/src/browser/cookie-name.ts +140 -0
  61. package/src/browser/event-controller.ts +37 -143
  62. package/src/browser/history-state.ts +21 -0
  63. package/src/browser/index.ts +3 -3
  64. package/src/browser/invalidate-client-cache.ts +52 -0
  65. package/src/browser/navigation-bridge.ts +30 -59
  66. package/src/browser/navigation-client.ts +96 -84
  67. package/src/browser/navigation-store-handle.ts +38 -0
  68. package/src/browser/navigation-store.ts +32 -82
  69. package/src/browser/navigation-transaction.ts +9 -59
  70. package/src/browser/partial-update.ts +60 -127
  71. package/src/browser/prefetch/cache.ts +82 -72
  72. package/src/browser/prefetch/fetch.ts +108 -33
  73. package/src/browser/prefetch/queue.ts +6 -3
  74. package/src/browser/rango-state.ts +157 -115
  75. package/src/browser/react/Link.tsx +0 -2
  76. package/src/browser/react/NavigationProvider.tsx +41 -48
  77. package/src/browser/react/ScrollRestoration.tsx +10 -6
  78. package/src/browser/react/filter-segment-order.ts +0 -2
  79. package/src/browser/react/index.ts +0 -48
  80. package/src/browser/react/location-state-shared.ts +166 -8
  81. package/src/browser/react/location-state.ts +39 -14
  82. package/src/browser/react/use-action.ts +6 -15
  83. package/src/browser/react/use-handle.ts +17 -14
  84. package/src/browser/react/use-link-status.ts +0 -4
  85. package/src/browser/react/use-navigation.ts +0 -3
  86. package/src/browser/react/use-params.ts +11 -11
  87. package/src/browser/react/use-reverse.ts +106 -0
  88. package/src/browser/react/use-router.ts +20 -5
  89. package/src/browser/react/use-search-params.ts +0 -5
  90. package/src/browser/react/use-segments.ts +0 -13
  91. package/src/browser/response-adapter.ts +52 -1
  92. package/src/browser/rsc-router.tsx +70 -34
  93. package/src/browser/scroll-restoration.ts +22 -14
  94. package/src/browser/segment-structure-assert.ts +2 -2
  95. package/src/browser/server-action-bridge.ts +168 -44
  96. package/src/browser/types.ts +36 -21
  97. package/src/browser/validate-redirect-origin.ts +43 -16
  98. package/src/build/collect-fallback-refs.ts +107 -0
  99. package/src/build/generate-manifest.ts +60 -35
  100. package/src/build/generate-route-types.ts +3 -0
  101. package/src/build/index.ts +8 -2
  102. package/src/build/prefix-tree-utils.ts +123 -0
  103. package/src/build/route-trie.ts +89 -10
  104. package/src/build/route-types/codegen.ts +4 -4
  105. package/src/build/route-types/include-resolution.ts +1 -1
  106. package/src/build/route-types/param-extraction.ts +6 -3
  107. package/src/build/route-types/per-module-writer.ts +7 -4
  108. package/src/build/route-types/router-processing.ts +122 -22
  109. package/src/build/route-types/scan-filter.ts +1 -1
  110. package/src/build/route-types/source-scan.ts +118 -0
  111. package/src/build/runtime-discovery.ts +9 -20
  112. package/src/cache/cache-error.ts +104 -0
  113. package/src/cache/cache-policy.ts +68 -28
  114. package/src/cache/cache-runtime.ts +134 -32
  115. package/src/cache/cache-scope.ts +100 -74
  116. package/src/cache/cache-tag.ts +98 -0
  117. package/src/cache/cf/cf-cache-store.ts +2255 -238
  118. package/src/cache/cf/index.ts +6 -16
  119. package/src/cache/document-cache.ts +61 -20
  120. package/src/cache/handle-snapshot.ts +63 -0
  121. package/src/cache/index.ts +22 -20
  122. package/src/cache/memory-segment-store.ts +136 -37
  123. package/src/cache/profile-registry.ts +6 -30
  124. package/src/cache/read-through-swr.ts +41 -11
  125. package/src/cache/segment-codec.ts +0 -16
  126. package/src/cache/tag-invalidation.ts +230 -0
  127. package/src/cache/types.ts +33 -100
  128. package/src/cache/vercel/index.ts +11 -0
  129. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  130. package/src/client.rsc.tsx +6 -21
  131. package/src/client.tsx +25 -61
  132. package/src/component-utils.ts +19 -0
  133. package/src/context-var.ts +17 -5
  134. package/src/decode-loader-results.ts +36 -0
  135. package/src/defer.ts +196 -0
  136. package/src/deps/ssr.ts +0 -1
  137. package/src/errors.ts +30 -4
  138. package/src/handle.ts +31 -23
  139. package/src/handles/MetaTags.tsx +0 -14
  140. package/src/handles/breadcrumbs.ts +16 -5
  141. package/src/handles/meta.ts +0 -39
  142. package/src/host/cookie-handler.ts +0 -36
  143. package/src/host/errors.ts +0 -24
  144. package/src/host/index.ts +8 -2
  145. package/src/host/pattern-matcher.ts +7 -50
  146. package/src/host/router.ts +107 -99
  147. package/src/host/testing.ts +40 -27
  148. package/src/host/types.ts +37 -4
  149. package/src/host/utils.ts +1 -1
  150. package/src/href-client.ts +137 -22
  151. package/src/index.rsc.ts +63 -9
  152. package/src/index.ts +64 -9
  153. package/src/internal-debug.ts +2 -4
  154. package/src/loader-store.ts +500 -0
  155. package/src/loader.rsc.ts +20 -13
  156. package/src/loader.ts +12 -11
  157. package/src/missing-id-error.ts +68 -0
  158. package/src/network-error-thrower.tsx +1 -6
  159. package/src/outlet-provider.tsx +1 -5
  160. package/src/prerender/param-hash.ts +10 -11
  161. package/src/prerender/store.ts +32 -37
  162. package/src/prerender.ts +61 -6
  163. package/src/redirect-origin.ts +100 -0
  164. package/src/response-utils.ts +9 -0
  165. package/src/reverse.ts +65 -40
  166. package/src/root-error-boundary.tsx +1 -19
  167. package/src/route-content-wrapper.tsx +7 -72
  168. package/src/route-definition/dsl-helpers.ts +244 -281
  169. package/src/route-definition/helper-factories.ts +29 -139
  170. package/src/route-definition/helpers-types.ts +40 -17
  171. package/src/route-definition/redirect.ts +43 -9
  172. package/src/route-definition/resolve-handler-use.ts +6 -0
  173. package/src/route-definition/use-item-types.ts +32 -0
  174. package/src/route-map-builder.ts +0 -16
  175. package/src/route-types.ts +19 -41
  176. package/src/router/basename.ts +14 -0
  177. package/src/router/content-negotiation.ts +15 -15
  178. package/src/router/error-handling.ts +13 -17
  179. package/src/router/find-match.ts +44 -23
  180. package/src/router/handler-context.ts +4 -41
  181. package/src/router/intercept-resolution.ts +14 -19
  182. package/src/router/lazy-includes.ts +9 -46
  183. package/src/router/loader-resolution.ts +91 -46
  184. package/src/router/logging.ts +0 -6
  185. package/src/router/manifest.ts +18 -29
  186. package/src/router/match-api.ts +0 -20
  187. package/src/router/match-context.ts +0 -22
  188. package/src/router/match-handlers.ts +57 -58
  189. package/src/router/match-middleware/background-revalidation.ts +0 -7
  190. package/src/router/match-middleware/cache-lookup.ts +150 -271
  191. package/src/router/match-middleware/cache-store.ts +3 -33
  192. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  193. package/src/router/match-middleware/segment-resolution.ts +0 -22
  194. package/src/router/match-pipelines.ts +1 -42
  195. package/src/router/match-result.ts +31 -80
  196. package/src/router/metrics.ts +0 -34
  197. package/src/router/middleware-types.ts +5 -112
  198. package/src/router/middleware.ts +118 -133
  199. package/src/router/navigation-snapshot.ts +0 -51
  200. package/src/router/params-util.ts +23 -0
  201. package/src/router/pattern-matching.ts +62 -67
  202. package/src/router/prerender-match.ts +99 -63
  203. package/src/router/preview-match.ts +3 -1
  204. package/src/router/request-classification.ts +28 -62
  205. package/src/router/revalidation.ts +50 -56
  206. package/src/router/route-snapshot.ts +0 -1
  207. package/src/router/router-context.ts +0 -27
  208. package/src/router/router-interfaces.ts +68 -35
  209. package/src/router/router-options.ts +55 -1
  210. package/src/router/router-registry.ts +2 -5
  211. package/src/router/segment-resolution/fresh.ts +44 -63
  212. package/src/router/segment-resolution/helpers.ts +34 -0
  213. package/src/router/segment-resolution/loader-cache.ts +40 -37
  214. package/src/router/segment-resolution/revalidation.ts +203 -285
  215. package/src/router/segment-resolution/static-store.ts +19 -5
  216. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  217. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  218. package/src/router/segment-resolution.ts +4 -1
  219. package/src/router/segment-wrappers.ts +0 -3
  220. package/src/router/state-cookie-name.ts +33 -0
  221. package/src/router/substitute-pattern-params.ts +56 -0
  222. package/src/router/telemetry-otel.ts +0 -20
  223. package/src/router/telemetry.ts +96 -19
  224. package/src/router/timeout.ts +0 -20
  225. package/src/router/trie-matching.ts +87 -48
  226. package/src/router/types.ts +9 -63
  227. package/src/router/url-params.ts +0 -5
  228. package/src/router.ts +80 -41
  229. package/src/rsc/handler-context.ts +3 -2
  230. package/src/rsc/handler.ts +83 -78
  231. package/src/rsc/helpers.ts +93 -5
  232. package/src/rsc/index.ts +1 -1
  233. package/src/rsc/json-route-result.ts +38 -0
  234. package/src/rsc/manifest-init.ts +28 -41
  235. package/src/rsc/origin-guard.ts +39 -25
  236. package/src/rsc/progressive-enhancement.ts +12 -1
  237. package/src/rsc/redirect-guard.ts +99 -0
  238. package/src/rsc/response-error.ts +79 -12
  239. package/src/rsc/response-route-handler.ts +76 -62
  240. package/src/rsc/rsc-rendering.ts +41 -60
  241. package/src/rsc/runtime-warnings.ts +23 -10
  242. package/src/rsc/server-action.ts +62 -67
  243. package/src/rsc/ssr-setup.ts +16 -0
  244. package/src/rsc/types.ts +10 -5
  245. package/src/runtime-env.ts +18 -0
  246. package/src/search-params.ts +4 -20
  247. package/src/segment-loader-promise.ts +14 -2
  248. package/src/segment-system.tsx +199 -142
  249. package/src/serialize.ts +243 -0
  250. package/src/server/context.ts +150 -51
  251. package/src/server/cookie-store.ts +80 -5
  252. package/src/server/handle-store.ts +7 -24
  253. package/src/server/loader-registry.ts +5 -24
  254. package/src/server/request-context.ts +165 -87
  255. package/src/ssr/index.tsx +14 -14
  256. package/src/static-handler.ts +10 -13
  257. package/src/testing/cache-status.ts +162 -0
  258. package/src/testing/collect-handle.ts +40 -0
  259. package/src/testing/dispatch.ts +618 -0
  260. package/src/testing/dom.entry.ts +22 -0
  261. package/src/testing/e2e/fixture.ts +188 -0
  262. package/src/testing/e2e/index.ts +128 -0
  263. package/src/testing/e2e/matchers.ts +35 -0
  264. package/src/testing/e2e/page-helpers.ts +272 -0
  265. package/src/testing/e2e/parity.ts +387 -0
  266. package/src/testing/e2e/server.ts +195 -0
  267. package/src/testing/flight-matchers.ts +97 -0
  268. package/src/testing/flight-normalize.ts +11 -0
  269. package/src/testing/flight-runtime.d.ts +57 -0
  270. package/src/testing/flight-tree.ts +682 -0
  271. package/src/testing/flight.entry.ts +52 -0
  272. package/src/testing/flight.ts +232 -0
  273. package/src/testing/generated-routes.ts +183 -0
  274. package/src/testing/index.ts +99 -0
  275. package/src/testing/internal/context.ts +348 -0
  276. package/src/testing/internal/flight-client-globals.ts +30 -0
  277. package/src/testing/internal/seed-vars.ts +54 -0
  278. package/src/testing/render-handler.ts +330 -0
  279. package/src/testing/render-route.tsx +566 -0
  280. package/src/testing/run-loader.ts +378 -0
  281. package/src/testing/run-middleware.ts +205 -0
  282. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  283. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  284. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  285. package/src/testing/vitest-stubs/version.ts +5 -0
  286. package/src/testing/vitest.ts +305 -0
  287. package/src/theme/ThemeProvider.tsx +0 -52
  288. package/src/theme/ThemeScript.tsx +0 -6
  289. package/src/theme/constants.ts +0 -12
  290. package/src/theme/index.ts +0 -7
  291. package/src/theme/theme-context.ts +1 -5
  292. package/src/theme/theme-script.ts +0 -14
  293. package/src/theme/use-theme.ts +0 -3
  294. package/src/types/boundaries.ts +0 -35
  295. package/src/types/cache-types.ts +13 -4
  296. package/src/types/error-types.ts +30 -90
  297. package/src/types/global-namespace.ts +54 -41
  298. package/src/types/handler-context.ts +97 -22
  299. package/src/types/index.ts +1 -10
  300. package/src/types/loader-types.ts +6 -3
  301. package/src/types/request-scope.ts +0 -19
  302. package/src/types/route-config.ts +6 -50
  303. package/src/types/route-entry.ts +0 -6
  304. package/src/types/segments.ts +18 -14
  305. package/src/urls/include-helper.ts +9 -56
  306. package/src/urls/index.ts +1 -11
  307. package/src/urls/path-helper-types.ts +19 -5
  308. package/src/urls/path-helper.ts +17 -106
  309. package/src/urls/pattern-types.ts +36 -19
  310. package/src/urls/response-types.ts +20 -19
  311. package/src/urls/type-extraction.ts +58 -139
  312. package/src/urls/urls-function.ts +1 -18
  313. package/src/use-loader.tsx +292 -107
  314. package/src/vite/debug.ts +1 -0
  315. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  316. package/src/vite/discovery/discover-routers.ts +95 -82
  317. package/src/vite/discovery/discovery-errors.ts +194 -0
  318. package/src/vite/discovery/prerender-collection.ts +26 -34
  319. package/src/vite/discovery/route-types-writer.ts +40 -84
  320. package/src/vite/discovery/state.ts +39 -1
  321. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  322. package/src/vite/index.ts +4 -0
  323. package/src/vite/plugin-types.ts +185 -10
  324. package/src/vite/plugins/cjs-to-esm.ts +3 -18
  325. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  326. package/src/vite/plugins/client-ref-hashing.ts +12 -11
  327. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  328. package/src/vite/plugins/expose-action-id.ts +4 -75
  329. package/src/vite/plugins/expose-id-utils.ts +3 -54
  330. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  331. package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
  332. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  333. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  334. package/src/vite/plugins/expose-internal-ids.ts +57 -67
  335. package/src/vite/plugins/performance-tracks.ts +9 -16
  336. package/src/vite/plugins/refresh-cmd.ts +1 -1
  337. package/src/vite/plugins/use-cache-transform.ts +26 -49
  338. package/src/vite/plugins/vercel-output.ts +258 -0
  339. package/src/vite/plugins/version-injector.ts +2 -32
  340. package/src/vite/plugins/version-plugin.ts +32 -23
  341. package/src/vite/plugins/virtual-entries.ts +35 -17
  342. package/src/vite/rango.ts +148 -115
  343. package/src/vite/router-discovery.ts +220 -68
  344. package/src/vite/utils/ast-handler-extract.ts +15 -31
  345. package/src/vite/utils/bundle-analysis.ts +10 -15
  346. package/src/vite/utils/client-chunks.ts +184 -0
  347. package/src/vite/utils/forward-user-plugins.ts +171 -0
  348. package/src/vite/utils/manifest-utils.ts +4 -59
  349. package/src/vite/utils/package-resolution.ts +1 -73
  350. package/src/vite/utils/prerender-utils.ts +0 -34
  351. package/src/vite/utils/shared-utils.ts +95 -43
  352. package/src/browser/action-response-classifier.ts +0 -99
  353. package/src/browser/react/use-client-cache.ts +0 -58
  354. package/src/browser/shallow.ts +0 -40
  355. package/src/handles/index.ts +0 -7
  356. package/src/router/middleware-cookies.ts +0 -55
@@ -5,11 +5,7 @@ import { OutletContext, type OutletContextValue } from "./outlet-context.js";
5
5
  import type { ResolvedSegment } from "./types.js";
6
6
 
7
7
  /**
8
- * Provider for outlet content - used internally by renderSegments
9
- *
10
- * Stores a reference to parent context so useLoader can walk up the chain
11
- * to find loader data from parent layouts. If this segment defines a loading
12
- * component, Outlet will wrap content with Suspense using that as fallback.
8
+ * Outlet content provider stores parent context for useLoader chain walking.
13
9
  */
14
10
  export function OutletProvider({
15
11
  content,
@@ -1,18 +1,10 @@
1
1
  /**
2
2
  * Deterministic param hashing for prerender storage keys.
3
- *
4
- * Used at build time (child process) to generate filenames and at
5
- * runtime (worker) to look up pre-rendered data. Both environments
6
- * must produce identical hashes for the same params.
7
- *
8
- * Uses a simple DJB2-based hash that works in all JS environments
9
- * (Node.js, Cloudflare Workers, browsers) without crypto imports.
3
+ * Used at build time and runtime; both must produce identical hashes.
4
+ * DJB2-based; works in all JS environments without crypto imports.
10
5
  */
11
6
 
12
- /**
13
- * Compute a deterministic hash string from route params.
14
- * For static routes (no params), returns "_".
15
- */
7
+ // For static routes (no params), returns "_".
16
8
  export function hashParams(params: Record<string, string>): string {
17
9
  const entries = Object.entries(params);
18
10
  if (entries.length === 0) return "_";
@@ -27,6 +19,13 @@ export function hashParams(params: Record<string, string>): string {
27
19
  /**
28
20
  * DJB2 hash returning an 8-char hex string.
29
21
  * Deterministic across all JS runtimes.
22
+ *
23
+ * 32-bit output: per-route collision probability hits ~50% near ~77k distinct
24
+ * param sets (birthday bound). The production store keys solely on
25
+ * routeName/paramHash and does not verify the canonical param string, so a
26
+ * collision serves the surviving entry for both param sets. Benign for typical
27
+ * catalogs; revisit (wider hash or stored-param verification) before
28
+ * pre-rendering hundreds of thousands of pages per route.
30
29
  */
31
30
  function djb2Hex(str: string): string {
32
31
  let hash = 5381;
@@ -1,21 +1,17 @@
1
1
  /**
2
- * Prerender Store
3
- *
4
- * Reads pre-rendered segment data from the worker bundle at build time.
5
- * The manifest module is lazily loaded via globalThis.__loadPrerenderManifestModule,
6
- * a function injected into the RSC entry that returns the manifest module
7
- * containing a key-to-specifier map and a `loadPrerenderAsset` function
8
- * that anchors import() resolution relative to the manifest file.
2
+ * Prerender Store — reads pre-rendered segment data from the worker bundle.
3
+ * Manifest module (injected via globalThis.__loadPrerenderManifestModule)
4
+ * contains key-to-specifier map and loadPrerenderAsset for import() resolution.
9
5
  */
10
6
 
11
- import type {
12
- SerializedSegmentData,
13
- SegmentHandleData,
14
- } from "../cache/types.js";
7
+ import type { SerializedSegmentData } from "../cache/types.js";
15
8
 
16
9
  export interface PrerenderEntry {
17
10
  segments: SerializedSegmentData[];
18
- handles: Record<string, SegmentHandleData>;
11
+ /** RSC-encoded handle map (see handle-snapshot.ts encodeHandles); "" when the
12
+ * route pushed no handles. Encoded so Promise/ReactNode handle values survive
13
+ * the JSON-serialized build artifact / dev wire, identical to the runtime cache. */
14
+ handles: string;
19
15
  }
20
16
 
21
17
  export interface PrerenderStore {
@@ -28,7 +24,9 @@ export interface PrerenderStore {
28
24
 
29
25
  export interface StaticEntry {
30
26
  encoded: string;
31
- handles: Record<string, unknown[]>;
27
+ /** RSC-encoded single-segment handle data (see encodeHandleValue); "" when the
28
+ * Static handler pushed no handles. */
29
+ handles: string;
32
30
  }
33
31
 
34
32
  export interface StaticStore {
@@ -99,13 +97,20 @@ export function createPrerenderStore(): PrerenderStore | null {
99
97
  if (!globalThis.__loadPrerenderManifestModule) return null;
100
98
 
101
99
  const cache = new Map<string, Promise<PrerenderEntry | null>>();
102
- let manifestModulePromise: Promise<PrerenderManifestModule | null> | null =
103
- null;
100
+ let manifestModulePromise: Promise<PrerenderManifestModule> | null = null;
104
101
 
105
- function loadManifestModule(): Promise<PrerenderManifestModule | null> {
102
+ function loadManifestModule(): Promise<PrerenderManifestModule> {
106
103
  if (!manifestModulePromise) {
104
+ // Do not cache a failed manifest-module load: clear the memoized promise
105
+ // on rejection so the next get() retries, and let the error propagate
106
+ // (consistent with the per-asset load policy below) instead of caching a
107
+ // null for the isolate lifetime, which would silently degrade every
108
+ // prerendered route to a miss after one transient failure.
107
109
  manifestModulePromise = globalThis.__loadPrerenderManifestModule!().catch(
108
- () => null,
110
+ (err) => {
111
+ manifestModulePromise = null;
112
+ throw err;
113
+ },
109
114
  );
110
115
  }
111
116
  return manifestModulePromise;
@@ -118,7 +123,6 @@ export function createPrerenderStore(): PrerenderStore | null {
118
123
  if (cached) return cached;
119
124
 
120
125
  const promise = loadManifestModule().then((mod) => {
121
- if (!mod) return null;
122
126
  const specifier = mod.default[key];
123
127
  if (!specifier) return null;
124
128
  // Let asset load errors propagate — a missing/corrupted artifact
@@ -127,29 +131,20 @@ export function createPrerenderStore(): PrerenderStore | null {
127
131
  // (which the handler stub would misreport as a 404).
128
132
  return mod.loadPrerenderAsset(specifier).then((asset) => asset.default);
129
133
  });
130
- cache.set(key, promise);
134
+ // Only memoize once the manifest module resolved: a manifest-load
135
+ // rejection must not poison the per-key cache, or the retry above is moot.
136
+ cache.set(
137
+ key,
138
+ promise.catch((err) => {
139
+ cache.delete(key);
140
+ throw err;
141
+ }),
142
+ );
131
143
  return promise;
132
144
  },
133
145
  };
134
146
  }
135
147
 
136
- /**
137
- * Load the prerender manifest index for test introspection.
138
- * Returns the key→specifier map or null if unavailable.
139
- */
140
- export async function loadPrerenderManifestIndex(): Promise<Record<
141
- string,
142
- string
143
- > | null> {
144
- if (!globalThis.__loadPrerenderManifestModule) return null;
145
- try {
146
- const mod = await globalThis.__loadPrerenderManifestModule();
147
- return mod.default;
148
- } catch {
149
- return null;
150
- }
151
- }
152
-
153
148
  /**
154
149
  * Create a static segment store.
155
150
  * Production only: backed by globalThis.__STATIC_MANIFEST injected at build time.
@@ -174,7 +169,7 @@ export function createStaticStore(): StaticStore | null {
174
169
  const val = mod.default;
175
170
  // Normalize: string-only (no handles) or { encoded, handles }
176
171
  if (typeof val === "string") {
177
- return { encoded: val, handles: {} } as StaticEntry;
172
+ return { encoded: val, handles: "" } as StaticEntry;
178
173
  }
179
174
  return val as StaticEntry;
180
175
  })
package/src/prerender.ts CHANGED
@@ -38,6 +38,7 @@ import type { ReverseFunction } from "./reverse.js";
38
38
  import type { DefaultReverseRouteMap } from "./types/global-namespace.js";
39
39
  import type { UseItems, HandlerUseItem } from "./route-types.js";
40
40
  import { isCachedFunction } from "./cache/taint.js";
41
+ import { isUnderTestRunner } from "./runtime-env.js";
41
42
 
42
43
  // -- Named route resolution types -------------------------------------------
43
44
 
@@ -69,9 +70,9 @@ type BuildReverseFunction = [DefaultReverseRouteMap] extends [
69
70
  * Default route map for Prerender named route resolution.
70
71
  * Uses GeneratedRouteMap (from gen file) to avoid circular dependencies.
71
72
  */
72
- type DefaultPrerenderRouteMap = keyof RSCRouter.GeneratedRouteMap extends never
73
+ type DefaultPrerenderRouteMap = keyof Rango.GeneratedRouteMap extends never
73
74
  ? {}
74
- : RSCRouter.GeneratedRouteMap;
75
+ : Rango.GeneratedRouteMap;
75
76
 
76
77
  /** Extract params from a route map entry (string pattern or { path } object). */
77
78
  type ExtractParamsFromEntry<TEntry> = TEntry extends string
@@ -273,6 +274,11 @@ export interface PrerenderHandlerDefinition<
273
274
  use?: () => UseItems<HandlerUseItem>;
274
275
  }
275
276
 
277
+ // Process-stable fallback id counter (mirrors createHandle / createLoader). Only
278
+ // assigned in a bare unit test where the Vite plugin did not inject an id; never
279
+ // fires in a real build (the plugin always injects).
280
+ let runtimePrerenderIdCounter = 0;
281
+
276
282
  // -- Overloads --------------------------------------------------------------
277
283
  //
278
284
  // T accepts: named route string (global or .local) OR explicit param object.
@@ -376,12 +382,27 @@ export function Prerender<TParams extends Record<string, any>>(
376
382
  );
377
383
  }
378
384
 
379
- if (!id) {
385
+ // Throw unless under a test runner. The plugin always injects $$id for a
386
+ // supported `export const` Prerender on every build, so a missing id means
387
+ // either no plugin (a bare test — fall back below) or an UNSUPPORTED shape the
388
+ // plugin silently skipped (dev OR a real build — fail loud; a synthetic id
389
+ // would degrade to a silent prerender miss). The message is already small (no
390
+ // stack-parsing diagnostic), so it ships as-is. isUnderTestRunner() is
391
+ // runtime-safe — never a bare `process.env` access.
392
+ if (!id && !isUnderTestRunner()) {
380
393
  throw new Error(
381
- "[rsc-router] Prerender: missing $$id. " +
382
- "Ensure the exposeInternalIds Vite plugin is configured.",
394
+ "[rango] Prerender: missing $$id. Use `export const X = Prerender(...)` " +
395
+ "and ensure the exposeInternalIds Vite plugin is configured.",
383
396
  );
384
397
  }
398
+ // Under vitest with no plugin id: assign a process-stable runtime id so a
399
+ // whole-app router with Prerender routes constructs in a bare test (for
400
+ // dispatch / assertGeneratedRoutesMatch). Never reached in a real build (the
401
+ // throw above fires there); prerender storage/lookup keys on routeName +
402
+ // paramHash, never $$id (mirrors createHandle / createLoader).
403
+ if (!id) {
404
+ id = `__rango_runtime_prerender_${runtimePrerenderIdCounter++}`;
405
+ }
385
406
 
386
407
  return {
387
408
  __brand: "prerenderHandler" as const,
@@ -421,6 +442,40 @@ export function isPrerenderPassthrough(
421
442
  );
422
443
  }
423
444
 
445
+ /**
446
+ * Detect whether any resolved segment carries the passthrough sentinel.
447
+ *
448
+ * A build handler signals passthrough by returning `ctx.passthrough()` (the
449
+ * PRERENDER_PASSTHROUGH sentinel), which lands on the segment's `component`.
450
+ * But when the route declares `loading()`, the handler result is deferred
451
+ * upstream (segment-resolution/fresh.ts), so `component` is a thenable resolving
452
+ * to the sentinel rather than the sentinel itself — a synchronous
453
+ * `isPrerenderPassthrough(component)` on the Promise returns false and the build
454
+ * bakes a corrupt artifact instead of deferring. Resolve thenables first.
455
+ *
456
+ * Rejections are swallowed here: a throwing build handler resurfaces during
457
+ * segment serialization, preserving the prior error-handling behavior.
458
+ */
459
+ export async function detectPrerenderPassthrough(
460
+ segments: ReadonlyArray<{ component: unknown }>,
461
+ ): Promise<boolean> {
462
+ for (const seg of segments) {
463
+ let component: unknown = seg.component;
464
+ if (
465
+ component &&
466
+ typeof (component as { then?: unknown }).then === "function"
467
+ ) {
468
+ try {
469
+ component = await component;
470
+ } catch {
471
+ continue;
472
+ }
473
+ }
474
+ if (isPrerenderPassthrough(component)) return true;
475
+ }
476
+ return false;
477
+ }
478
+
424
479
  // -- Type guards ------------------------------------------------------------
425
480
 
426
481
  /**
@@ -499,7 +554,7 @@ export function Passthrough<
499
554
  ): PassthroughHandlerDefinition<TParams, TEnv> {
500
555
  if (!isPrerenderHandler(prerenderDef)) {
501
556
  throw new Error(
502
- "[rsc-router] Passthrough: first argument must be a Prerender() definition.",
557
+ "[rango] Passthrough: first argument must be a Prerender() definition.",
503
558
  );
504
559
  }
505
560
  return {
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Runtime-neutral same-origin redirect rule.
3
+ *
4
+ * Shared by the client redirect guard (`browser/validate-redirect-origin.ts`,
5
+ * which validates redirect targets the client JS is about to navigate to) and
6
+ * the server outgoing-redirect guard (`rsc/redirect-guard.ts`, which validates
7
+ * every browser-followed `Location` header before it leaves the handler). Kept
8
+ * at the `src/` root so both layers import the ONE rule and cannot drift -- a
9
+ * cross-origin target blocked on the JS/fetch path is blocked identically on the
10
+ * no-JS (PE) and full-page document paths.
11
+ */
12
+
13
+ /**
14
+ * Resolve a redirect target against the current origin.
15
+ *
16
+ * Returns the canonical (normalized) same-origin href -- which also collapses
17
+ * protocol-relative (`//evil.com`) and other ambiguous forms -- or `null` when
18
+ * the target resolves to a different origin or is unparseable. Pure: no logging,
19
+ * no side effects.
20
+ */
21
+ export function resolveSameOriginRedirect(
22
+ url: string,
23
+ currentOrigin: string,
24
+ ): string | null {
25
+ try {
26
+ const target = new URL(url, currentOrigin);
27
+ if (target.origin !== currentOrigin) {
28
+ return null;
29
+ }
30
+ return target.href;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Validate an explicit off-origin redirect target (`redirect(url, { external:
38
+ * true })`).
39
+ *
40
+ * `external` opts out of the same-origin rule, but NOT out of scheme safety:
41
+ * only `http:`/`https:` targets are allowed. A redirect ultimately reaches the
42
+ * browser via `window.location.assign()` on the SPA/action client paths, so a
43
+ * forged or mistaken `redirect("javascript:...", { external: true })` would be a
44
+ * scriptable navigation if the scheme were not checked here. Returns the
45
+ * normalized href for an http(s) target (same- or cross-origin), or `null`
46
+ * otherwise. Pure: no logging, no side effects.
47
+ */
48
+ export function resolveExternalRedirect(
49
+ url: string,
50
+ currentOrigin: string,
51
+ ): string | null {
52
+ try {
53
+ const target = new URL(url, currentOrigin);
54
+ if (target.protocol !== "http:" && target.protocol !== "https:") {
55
+ return null;
56
+ }
57
+ return target.href;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Out-of-band brand for `redirect(url, { external: true })`.
65
+ *
66
+ * The external opt-in MUST be settable only by app code calling `redirect(...,
67
+ * { external: true })`, never by an attacker. An earlier design carried the
68
+ * opt-in as a wire header (`x-rango-redirect-external`), but a wire header is
69
+ * forgeable: a proxy-style response route that copies an attacker-controlled
70
+ * upstream response's headers would let `302 Location: https://evil` plus that
71
+ * header bypass the same-origin guard without app code ever opting in. So the
72
+ * opt-in is now an out-of-band brand on the Response object itself, tracked in a
73
+ * `WeakSet` that cannot cross the wire. `redirect()` brands the Response; the
74
+ * small set of internal redirect-rebuild paths (middleware `mergeResponse`,
75
+ * `carryOverRedirectHeaders`, the response-route rewrap) transfer the brand onto
76
+ * the rebuilt Response; the guard and the SPA intercept read it. An upstream
77
+ * Response an app proxies is never branded, so its forged header is inert.
78
+ *
79
+ * Fail-closed: if a rebuild path ever drops the brand, the redirect is
80
+ * neutralized to the app root rather than allowed off-host.
81
+ */
82
+ const externalRedirects = new WeakSet<Response>();
83
+
84
+ /** Brand a Response as an explicit `{ external: true }` redirect (out-of-band). */
85
+ export function markExternalRedirect(response: Response): void {
86
+ externalRedirects.add(response);
87
+ }
88
+
89
+ /** Read the out-of-band `{ external: true }` brand off a Response. */
90
+ export function isExternalRedirect(response: Response): boolean {
91
+ return externalRedirects.has(response);
92
+ }
93
+
94
+ /**
95
+ * Reserved internal header name. No longer a trust signal -- the external
96
+ * opt-in is the out-of-band brand above. It is kept only so the redirect-rebuild
97
+ * paths and the guard can defensively strip any value (e.g. one forged by a
98
+ * proxied upstream) and guarantee it never reaches the browser.
99
+ */
100
+ export const EXTERNAL_REDIRECT_MARKER: string = "x-rango-redirect-external";
@@ -26,3 +26,12 @@ export function isWebSocketUpgradeResponse(response: Response): boolean {
26
26
  (response as unknown as { webSocket?: unknown }).webSocket != null
27
27
  );
28
28
  }
29
+
30
+ // Location truthiness (not presence) so an empty `Location: ""` is not a redirect.
31
+ export function isRedirectResponse(response: Response): boolean {
32
+ return (
33
+ response.status >= 300 &&
34
+ response.status < 400 &&
35
+ Boolean(response.headers.get("Location"))
36
+ );
37
+ }
package/src/reverse.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { ExtractParams } from "./types.js";
2
2
  import type { SearchSchema, ResolveSearchSchema } from "./search-params.js";
3
3
  import { serializeSearchParams } from "./search-params.js";
4
- import { encodePathSegment } from "./router/url-params.js";
4
+ import { substitutePatternParams } from "./router/substitute-pattern-params.js";
5
5
 
6
6
  /**
7
7
  * Sanitize prefix string by removing leading slash
@@ -219,6 +219,67 @@ export type ExtractLocalRoutes<TPatterns> = TPatterns extends {
219
219
  ? TPatterns
220
220
  : Record<string, string>;
221
221
 
222
+ /**
223
+ * Params accepted by `useReverse(routes)`. The route's own params are
224
+ * required, and additional string keys are permitted so callers can
225
+ * override values that would otherwise be auto-filled from the matched
226
+ * route's `useParams()` (e.g. an enclosing `:tenantId` mount segment).
227
+ */
228
+ export type LocalReverseParams<TPattern extends string> =
229
+ ExtractParams<TPattern> & {
230
+ readonly [extra: string]: string | undefined;
231
+ };
232
+
233
+ /**
234
+ * Type-safe local reverse function.
235
+ *
236
+ * Returned by `useReverse(routes)` on the client. The route map is the
237
+ * exposure boundary (a generated `routes` from a `urls()` module) and the
238
+ * scope is implicit from that import. Names may be written with or without a
239
+ * leading dot — `reverse("post")` and `reverse(".post")` are identical. The dot
240
+ * is a cosmetic readability convention (and parity with `ctx.reverse(".name")`);
241
+ * there is no separate global namespace here, so it carries no meaning.
242
+ *
243
+ * @example
244
+ * ```typescript
245
+ * const reverse = useReverse(blogRoutes);
246
+ * reverse("index"); // ✓ no params (dot optional)
247
+ * reverse(".index"); // ✓ identical to the above
248
+ * reverse("post", { postId: "hello" }); // ✓ with params
249
+ * reverse("search", {}, { q: "hi" }); // ✓ with search schema
250
+ * reverse("typo"); // ✗ compile error
251
+ * ```
252
+ */
253
+ export type LocalReverseFunction<TLocalRoutes> = {
254
+ /**
255
+ * Route without params (leading dot optional)
256
+ */
257
+ <TName extends keyof TLocalRoutes & string>(
258
+ name: IsEmptyObject<
259
+ ExtractParams<RoutePatternFor<TLocalRoutes, TName>>
260
+ > extends true
261
+ ? TName | `.${TName}`
262
+ : never,
263
+ ): string;
264
+
265
+ /**
266
+ * Route with params (leading dot optional)
267
+ */
268
+ <TName extends keyof TLocalRoutes & string>(
269
+ name: TName | `.${TName}`,
270
+ params: LocalReverseParams<RoutePatternFor<TLocalRoutes, TName>>,
271
+ ): string;
272
+
273
+ /**
274
+ * Route with params and search (leading dot optional)
275
+ */
276
+ <TName extends keyof TLocalRoutes & string>(
277
+ name: TName | `.${TName}`,
278
+ params: LocalReverseParams<RoutePatternFor<TLocalRoutes, TName>>,
279
+ search: ResolveSearchSchema<ExtractSearchSchema<TLocalRoutes, TName>>,
280
+ ): string;
281
+ };
282
+
222
283
  /**
223
284
  * Extract the response data type for a named route from a UrlPatterns instance.
224
285
  * Re-exported from urls.ts for consumer convenience.
@@ -302,45 +363,9 @@ export function createReverse<TRoutes extends Record<string, string>>(
302
363
  throw new Error(`Unknown route: ${name}`);
303
364
  }
304
365
 
305
- let result = pattern;
306
- if (params) {
307
- // Replace :param placeholders with actual values
308
- // Strip constraint syntax: :param(a|b) -> use "param" as key
309
- // Optional params (:param?) are omitted when not provided
310
- let hadOmittedOptional = false;
311
- result = result.replace(
312
- /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(\?)/g,
313
- (_, key, _constraint, optional) => {
314
- const value = params[key];
315
- // Empty string is treated as omitted — the trie matcher fills
316
- // unmatched optional params with "" (not undefined), so reverse
317
- // must collapse those segments instead of leaving empty slots.
318
- if (value === undefined || value === "") {
319
- hadOmittedOptional = true;
320
- return "";
321
- }
322
- return encodePathSegment(value);
323
- },
324
- );
325
- // Second pass: required params (no trailing ?)
326
- result = result.replace(
327
- /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(?!\?)/g,
328
- (_, key) => {
329
- const value = params[key];
330
- if (value === undefined) {
331
- throw new Error(`Missing param "${key}" for route "${name}"`);
332
- }
333
- return encodePathSegment(value);
334
- },
335
- );
336
- // Clean up slashes only when an optional param was actually omitted,
337
- // so intentional trailing-slash patterns like "/blog/" are preserved.
338
- if (hadOmittedOptional) {
339
- const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
340
- result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
341
- if (hadTrailingSlash && !result.endsWith("/")) result += "/";
342
- }
343
- }
366
+ let result = params
367
+ ? substitutePatternParams(pattern, params, name)
368
+ : pattern;
344
369
 
345
370
  // Append search params as query string
346
371
  if (search) {
@@ -3,26 +3,17 @@
3
3
  import { Component, useState, type ReactNode } from "react";
4
4
  import type { ClientErrorBoundaryFallbackProps } from "./types.js";
5
5
 
6
- /**
7
- * Check if an error is a network-related error
8
- */
9
6
  function isNetworkError(error: Error): boolean {
10
7
  return error.name === "NetworkError";
11
8
  }
12
9
 
13
- /**
14
- * Network error fallback UI with retry functionality
15
- * Shows a connection-specific message and allows retrying via page refresh
16
- */
17
10
  function NetworkErrorFallback({
18
11
  error,
19
- reset,
20
12
  }: ClientErrorBoundaryFallbackProps): ReactNode {
21
13
  const [isRetrying, setIsRetrying] = useState(false);
22
14
 
23
15
  const handleRetry = (): void => {
24
16
  setIsRetrying(true);
25
- // Refresh the page to retry the request
26
17
  window.location.reload();
27
18
  };
28
19
 
@@ -42,7 +33,6 @@ function NetworkErrorFallback({
42
33
  marginBottom: "1rem",
43
34
  }}
44
35
  >
45
- {/* Simple cloud with x icon using CSS */}
46
36
  <span style={{ color: "#9ca3af" }}>&#9729;</span>
47
37
  </div>
48
38
  <h1
@@ -101,10 +91,6 @@ function NetworkErrorFallback({
101
91
  );
102
92
  }
103
93
 
104
- /**
105
- * Default fallback UI for root error boundary
106
- * This is shown when an unhandled error bubbles up to the root
107
- */
108
94
  function RootErrorFallback({
109
95
  error,
110
96
  reset,
@@ -230,7 +216,6 @@ export class RootErrorBoundary extends Component<
230
216
  }
231
217
 
232
218
  componentDidMount(): void {
233
- // Listen for popstate (back/forward navigation) to reset error state
234
219
  window.addEventListener("popstate", this.handlePopState);
235
220
  }
236
221
 
@@ -247,15 +232,13 @@ export class RootErrorBoundary extends Component<
247
232
  }
248
233
 
249
234
  componentDidUpdate(prevProps: { children: ReactNode }): void {
250
- // Reset error state when children change (e.g., navigation)
251
- // This allows the app to recover after navigation away from an errored route
235
+ // Reset error on children change (navigation).
252
236
  if (this.state.hasError && prevProps.children !== this.props.children) {
253
237
  this.setState({ hasError: false, error: null });
254
238
  }
255
239
  }
256
240
 
257
241
  handlePopState = (): void => {
258
- // Reset error state on back/forward navigation
259
242
  if (this.state.hasError) {
260
243
  this.setState({ hasError: false, error: null });
261
244
  }
@@ -276,7 +259,6 @@ export class RootErrorBoundary extends Component<
276
259
  segmentType: "route" as const,
277
260
  };
278
261
 
279
- // Use specialized fallback for network errors
280
262
  if (isNetworkError(this.state.error)) {
281
263
  return <NetworkErrorFallback error={errorInfo} reset={this.reset} />;
282
264
  }