@rangojs/router 0.0.0-experimental.0f44aca1

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 (305) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +899 -0
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +5214 -0
  5. package/package.json +176 -0
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +220 -0
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +112 -0
  11. package/skills/document-cache/SKILL.md +182 -0
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +704 -0
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +313 -0
  16. package/skills/layout/SKILL.md +310 -0
  17. package/skills/links/SKILL.md +239 -0
  18. package/skills/loader/SKILL.md +596 -0
  19. package/skills/middleware/SKILL.md +339 -0
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +305 -0
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +118 -0
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +385 -0
  26. package/skills/router-setup/SKILL.md +439 -0
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +79 -0
  29. package/skills/typesafety/SKILL.md +623 -0
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +273 -0
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/event-controller.ts +899 -0
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/index.ts +18 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +141 -0
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +134 -0
  42. package/src/browser/navigation-bridge.ts +645 -0
  43. package/src/browser/navigation-client.ts +215 -0
  44. package/src/browser/navigation-store.ts +806 -0
  45. package/src/browser/navigation-transaction.ts +295 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +550 -0
  48. package/src/browser/prefetch/cache.ts +146 -0
  49. package/src/browser/prefetch/fetch.ts +135 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +42 -0
  52. package/src/browser/prefetch/queue.ts +88 -0
  53. package/src/browser/rango-state.ts +112 -0
  54. package/src/browser/react/Link.tsx +360 -0
  55. package/src/browser/react/NavigationProvider.tsx +386 -0
  56. package/src/browser/react/ScrollRestoration.tsx +94 -0
  57. package/src/browser/react/context.ts +59 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +52 -0
  60. package/src/browser/react/location-state-shared.ts +162 -0
  61. package/src/browser/react/location-state.ts +107 -0
  62. package/src/browser/react/mount-context.ts +37 -0
  63. package/src/browser/react/nonce-context.ts +23 -0
  64. package/src/browser/react/shallow-equal.ts +27 -0
  65. package/src/browser/react/use-action.ts +218 -0
  66. package/src/browser/react/use-client-cache.ts +58 -0
  67. package/src/browser/react/use-handle.ts +162 -0
  68. package/src/browser/react/use-href.tsx +40 -0
  69. package/src/browser/react/use-link-status.ts +135 -0
  70. package/src/browser/react/use-mount.ts +31 -0
  71. package/src/browser/react/use-navigation.ts +99 -0
  72. package/src/browser/react/use-params.ts +65 -0
  73. package/src/browser/react/use-pathname.ts +47 -0
  74. package/src/browser/react/use-router.ts +63 -0
  75. package/src/browser/react/use-search-params.ts +56 -0
  76. package/src/browser/react/use-segments.ts +171 -0
  77. package/src/browser/response-adapter.ts +73 -0
  78. package/src/browser/rsc-router.tsx +431 -0
  79. package/src/browser/scroll-restoration.ts +400 -0
  80. package/src/browser/segment-reconciler.ts +216 -0
  81. package/src/browser/segment-structure-assert.ts +83 -0
  82. package/src/browser/server-action-bridge.ts +667 -0
  83. package/src/browser/shallow.ts +40 -0
  84. package/src/browser/types.ts +538 -0
  85. package/src/browser/validate-redirect-origin.ts +29 -0
  86. package/src/build/generate-manifest.ts +438 -0
  87. package/src/build/generate-route-types.ts +36 -0
  88. package/src/build/index.ts +35 -0
  89. package/src/build/route-trie.ts +265 -0
  90. package/src/build/route-types/ast-helpers.ts +25 -0
  91. package/src/build/route-types/ast-route-extraction.ts +98 -0
  92. package/src/build/route-types/codegen.ts +102 -0
  93. package/src/build/route-types/include-resolution.ts +411 -0
  94. package/src/build/route-types/param-extraction.ts +48 -0
  95. package/src/build/route-types/per-module-writer.ts +128 -0
  96. package/src/build/route-types/router-processing.ts +469 -0
  97. package/src/build/route-types/scan-filter.ts +78 -0
  98. package/src/build/runtime-discovery.ts +231 -0
  99. package/src/cache/background-task.ts +34 -0
  100. package/src/cache/cache-key-utils.ts +44 -0
  101. package/src/cache/cache-policy.ts +125 -0
  102. package/src/cache/cache-runtime.ts +338 -0
  103. package/src/cache/cache-scope.ts +382 -0
  104. package/src/cache/cf/cf-cache-store.ts +540 -0
  105. package/src/cache/cf/index.ts +25 -0
  106. package/src/cache/document-cache.ts +369 -0
  107. package/src/cache/handle-capture.ts +81 -0
  108. package/src/cache/handle-snapshot.ts +41 -0
  109. package/src/cache/index.ts +43 -0
  110. package/src/cache/memory-segment-store.ts +328 -0
  111. package/src/cache/profile-registry.ts +73 -0
  112. package/src/cache/read-through-swr.ts +134 -0
  113. package/src/cache/segment-codec.ts +256 -0
  114. package/src/cache/taint.ts +98 -0
  115. package/src/cache/types.ts +342 -0
  116. package/src/client.rsc.tsx +85 -0
  117. package/src/client.tsx +601 -0
  118. package/src/component-utils.ts +76 -0
  119. package/src/components/DefaultDocument.tsx +27 -0
  120. package/src/context-var.ts +86 -0
  121. package/src/debug.ts +243 -0
  122. package/src/default-error-boundary.tsx +88 -0
  123. package/src/deps/browser.ts +8 -0
  124. package/src/deps/html-stream-client.ts +2 -0
  125. package/src/deps/html-stream-server.ts +2 -0
  126. package/src/deps/rsc.ts +10 -0
  127. package/src/deps/ssr.ts +2 -0
  128. package/src/errors.ts +365 -0
  129. package/src/handle.ts +135 -0
  130. package/src/handles/MetaTags.tsx +246 -0
  131. package/src/handles/breadcrumbs.ts +66 -0
  132. package/src/handles/index.ts +7 -0
  133. package/src/handles/meta.ts +264 -0
  134. package/src/host/cookie-handler.ts +165 -0
  135. package/src/host/errors.ts +97 -0
  136. package/src/host/index.ts +53 -0
  137. package/src/host/pattern-matcher.ts +214 -0
  138. package/src/host/router.ts +352 -0
  139. package/src/host/testing.ts +79 -0
  140. package/src/host/types.ts +146 -0
  141. package/src/host/utils.ts +25 -0
  142. package/src/href-client.ts +222 -0
  143. package/src/index.rsc.ts +233 -0
  144. package/src/index.ts +277 -0
  145. package/src/internal-debug.ts +11 -0
  146. package/src/loader.rsc.ts +89 -0
  147. package/src/loader.ts +64 -0
  148. package/src/network-error-thrower.tsx +23 -0
  149. package/src/outlet-context.ts +15 -0
  150. package/src/outlet-provider.tsx +45 -0
  151. package/src/prerender/param-hash.ts +37 -0
  152. package/src/prerender/store.ts +185 -0
  153. package/src/prerender.ts +463 -0
  154. package/src/reverse.ts +330 -0
  155. package/src/root-error-boundary.tsx +289 -0
  156. package/src/route-content-wrapper.tsx +196 -0
  157. package/src/route-definition/dsl-helpers.ts +934 -0
  158. package/src/route-definition/helper-factories.ts +200 -0
  159. package/src/route-definition/helpers-types.ts +430 -0
  160. package/src/route-definition/index.ts +52 -0
  161. package/src/route-definition/redirect.ts +93 -0
  162. package/src/route-definition.ts +1 -0
  163. package/src/route-map-builder.ts +275 -0
  164. package/src/route-name.ts +53 -0
  165. package/src/route-types.ts +259 -0
  166. package/src/router/content-negotiation.ts +116 -0
  167. package/src/router/debug-manifest.ts +72 -0
  168. package/src/router/error-handling.ts +287 -0
  169. package/src/router/find-match.ts +158 -0
  170. package/src/router/handler-context.ts +451 -0
  171. package/src/router/intercept-resolution.ts +395 -0
  172. package/src/router/lazy-includes.ts +234 -0
  173. package/src/router/loader-resolution.ts +420 -0
  174. package/src/router/logging.ts +248 -0
  175. package/src/router/manifest.ts +267 -0
  176. package/src/router/match-api.ts +620 -0
  177. package/src/router/match-context.ts +266 -0
  178. package/src/router/match-handlers.ts +440 -0
  179. package/src/router/match-middleware/background-revalidation.ts +223 -0
  180. package/src/router/match-middleware/cache-lookup.ts +634 -0
  181. package/src/router/match-middleware/cache-store.ts +295 -0
  182. package/src/router/match-middleware/index.ts +81 -0
  183. package/src/router/match-middleware/intercept-resolution.ts +306 -0
  184. package/src/router/match-middleware/segment-resolution.ts +192 -0
  185. package/src/router/match-pipelines.ts +179 -0
  186. package/src/router/match-result.ts +219 -0
  187. package/src/router/metrics.ts +282 -0
  188. package/src/router/middleware-cookies.ts +55 -0
  189. package/src/router/middleware-types.ts +222 -0
  190. package/src/router/middleware.ts +748 -0
  191. package/src/router/pattern-matching.ts +563 -0
  192. package/src/router/prerender-match.ts +402 -0
  193. package/src/router/preview-match.ts +170 -0
  194. package/src/router/revalidation.ts +289 -0
  195. package/src/router/router-context.ts +316 -0
  196. package/src/router/router-interfaces.ts +452 -0
  197. package/src/router/router-options.ts +592 -0
  198. package/src/router/router-registry.ts +24 -0
  199. package/src/router/segment-resolution/fresh.ts +570 -0
  200. package/src/router/segment-resolution/helpers.ts +263 -0
  201. package/src/router/segment-resolution/loader-cache.ts +198 -0
  202. package/src/router/segment-resolution/revalidation.ts +1239 -0
  203. package/src/router/segment-resolution/static-store.ts +67 -0
  204. package/src/router/segment-resolution.ts +21 -0
  205. package/src/router/segment-wrappers.ts +289 -0
  206. package/src/router/telemetry-otel.ts +299 -0
  207. package/src/router/telemetry.ts +300 -0
  208. package/src/router/timeout.ts +148 -0
  209. package/src/router/trie-matching.ts +239 -0
  210. package/src/router/types.ts +170 -0
  211. package/src/router.ts +1002 -0
  212. package/src/rsc/handler-context.ts +45 -0
  213. package/src/rsc/handler.ts +1089 -0
  214. package/src/rsc/helpers.ts +198 -0
  215. package/src/rsc/index.ts +36 -0
  216. package/src/rsc/loader-fetch.ts +209 -0
  217. package/src/rsc/manifest-init.ts +86 -0
  218. package/src/rsc/nonce.ts +32 -0
  219. package/src/rsc/origin-guard.ts +141 -0
  220. package/src/rsc/progressive-enhancement.ts +379 -0
  221. package/src/rsc/response-error.ts +37 -0
  222. package/src/rsc/response-route-handler.ts +347 -0
  223. package/src/rsc/rsc-rendering.ts +235 -0
  224. package/src/rsc/runtime-warnings.ts +42 -0
  225. package/src/rsc/server-action.ts +348 -0
  226. package/src/rsc/ssr-setup.ts +128 -0
  227. package/src/rsc/types.ts +263 -0
  228. package/src/search-params.ts +230 -0
  229. package/src/segment-system.tsx +454 -0
  230. package/src/server/context.ts +591 -0
  231. package/src/server/cookie-store.ts +190 -0
  232. package/src/server/fetchable-loader-store.ts +37 -0
  233. package/src/server/handle-store.ts +308 -0
  234. package/src/server/loader-registry.ts +133 -0
  235. package/src/server/request-context.ts +914 -0
  236. package/src/server/root-layout.tsx +10 -0
  237. package/src/server/tsconfig.json +14 -0
  238. package/src/server.ts +51 -0
  239. package/src/ssr/index.tsx +365 -0
  240. package/src/static-handler.ts +114 -0
  241. package/src/theme/ThemeProvider.tsx +297 -0
  242. package/src/theme/ThemeScript.tsx +61 -0
  243. package/src/theme/constants.ts +62 -0
  244. package/src/theme/index.ts +48 -0
  245. package/src/theme/theme-context.ts +44 -0
  246. package/src/theme/theme-script.ts +155 -0
  247. package/src/theme/types.ts +182 -0
  248. package/src/theme/use-theme.ts +44 -0
  249. package/src/types/boundaries.ts +158 -0
  250. package/src/types/cache-types.ts +198 -0
  251. package/src/types/error-types.ts +192 -0
  252. package/src/types/global-namespace.ts +100 -0
  253. package/src/types/handler-context.ts +687 -0
  254. package/src/types/index.ts +88 -0
  255. package/src/types/loader-types.ts +183 -0
  256. package/src/types/route-config.ts +170 -0
  257. package/src/types/route-entry.ts +102 -0
  258. package/src/types/segments.ts +148 -0
  259. package/src/types.ts +1 -0
  260. package/src/urls/include-helper.ts +197 -0
  261. package/src/urls/index.ts +53 -0
  262. package/src/urls/path-helper-types.ts +339 -0
  263. package/src/urls/path-helper.ts +329 -0
  264. package/src/urls/pattern-types.ts +95 -0
  265. package/src/urls/response-types.ts +106 -0
  266. package/src/urls/type-extraction.ts +372 -0
  267. package/src/urls/urls-function.ts +98 -0
  268. package/src/urls.ts +1 -0
  269. package/src/use-loader.tsx +354 -0
  270. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  271. package/src/vite/discovery/discover-routers.ts +344 -0
  272. package/src/vite/discovery/prerender-collection.ts +385 -0
  273. package/src/vite/discovery/route-types-writer.ts +258 -0
  274. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  275. package/src/vite/discovery/state.ts +110 -0
  276. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  277. package/src/vite/index.ts +16 -0
  278. package/src/vite/plugin-types.ts +131 -0
  279. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  280. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  281. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  282. package/src/vite/plugins/expose-action-id.ts +365 -0
  283. package/src/vite/plugins/expose-id-utils.ts +287 -0
  284. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  285. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  286. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  287. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  288. package/src/vite/plugins/expose-ids/types.ts +45 -0
  289. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  290. package/src/vite/plugins/refresh-cmd.ts +65 -0
  291. package/src/vite/plugins/use-cache-transform.ts +323 -0
  292. package/src/vite/plugins/version-injector.ts +83 -0
  293. package/src/vite/plugins/version-plugin.ts +254 -0
  294. package/src/vite/plugins/version.d.ts +12 -0
  295. package/src/vite/plugins/virtual-entries.ts +123 -0
  296. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  297. package/src/vite/rango.ts +510 -0
  298. package/src/vite/router-discovery.ts +785 -0
  299. package/src/vite/utils/ast-handler-extract.ts +517 -0
  300. package/src/vite/utils/banner.ts +36 -0
  301. package/src/vite/utils/bundle-analysis.ts +137 -0
  302. package/src/vite/utils/manifest-utils.ts +70 -0
  303. package/src/vite/utils/package-resolution.ts +121 -0
  304. package/src/vite/utils/prerender-utils.ts +189 -0
  305. package/src/vite/utils/shared-utils.ts +169 -0
@@ -0,0 +1,171 @@
1
+ "use client";
2
+
3
+ import { useContext, useState, useEffect, useRef } from "react";
4
+ import { NavigationStoreContext } from "./context.js";
5
+ import { shallowEqual } from "./shallow-equal.js";
6
+
7
+ /**
8
+ * Segments state returned by useSegments hook
9
+ */
10
+ export interface SegmentsState {
11
+ /** URL path segments (e.g., /shop/products/123 → ["shop", "products", "123"]) */
12
+ path: readonly string[];
13
+ /** Matched segment IDs in order (layouts and routes only, e.g., ["L0", "L0L1", "L0L1R0"]) */
14
+ segmentIds: readonly string[];
15
+ /** Current URL location */
16
+ location: URL;
17
+ }
18
+
19
+ /**
20
+ * Parse pathname into path segments
21
+ * /shop/products/123 → ["shop", "products", "123"]
22
+ */
23
+ function parsePathname(pathname: string): string[] {
24
+ return pathname.split("/").filter(Boolean);
25
+ }
26
+
27
+ /**
28
+ * Build segments state from event controller
29
+ */
30
+ function buildSegmentsState(
31
+ location: URL,
32
+ segmentOrder: string[],
33
+ ): SegmentsState {
34
+ return {
35
+ path: parsePathname(location.pathname),
36
+ segmentIds: segmentOrder,
37
+ location,
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Hook to access current route segments with optional selector for performance
43
+ *
44
+ * Provides information about the current URL path and matched route segments.
45
+ * Uses the event controller for reactive state management.
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * // Get full segments state
50
+ * const { path, segmentIds, location } = useSegments();
51
+ *
52
+ * // Use selector for specific values (better performance)
53
+ * const path = useSegments(s => s.path);
54
+ * const isShopRoute = useSegments(s => s.path[0] === "shop");
55
+ * ```
56
+ */
57
+ export function useSegments(): SegmentsState;
58
+ export function useSegments<T>(selector: (state: SegmentsState) => T): T;
59
+ export function useSegments<T>(
60
+ selector?: (state: SegmentsState) => T,
61
+ ): T | SegmentsState {
62
+ const ctx = useContext(NavigationStoreContext);
63
+
64
+ // Build initial state from event controller when context exists.
65
+ // Inlined rather than calling recompute() because the segmentsCache ref
66
+ // is not yet initialized during the useState initializer.
67
+ const [state, setState] = useState<T | SegmentsState>(() => {
68
+ if (!ctx) {
69
+ const fallbackLocation = new URL("/", "http://localhost");
70
+ const fallbackState = buildSegmentsState(fallbackLocation, []);
71
+ return selector ? selector(fallbackState) : fallbackState;
72
+ }
73
+ const location = ctx.eventController.getLocation();
74
+ const handleState = ctx.eventController.getHandleState();
75
+ const segmentsState = buildSegmentsState(
76
+ location as URL,
77
+ handleState.segmentOrder,
78
+ );
79
+ return selector ? selector(segmentsState) : segmentsState;
80
+ });
81
+
82
+ const prevState = useRef(state);
83
+ const selectorRef = useRef(selector);
84
+ selectorRef.current = selector;
85
+
86
+ // Track selector identity to detect when the selector function changes.
87
+ // Only then do we eagerly recompute during render to avoid staleness.
88
+ // Without this guard, no-selector mode causes infinite re-renders because
89
+ // buildSegmentsState creates fresh arrays that fail Object.is checks.
90
+ const prevSelectorIdentity = useRef(selector);
91
+
92
+ // Cache SegmentsState to stabilize nested references (path, segmentIds
93
+ // arrays) so selectors returning composite values don't cause spurious
94
+ // render-time setState calls.
95
+ const segmentsCache = useRef<{
96
+ location: URL;
97
+ segmentOrder: string[];
98
+ state: SegmentsState;
99
+ } | null>(null);
100
+
101
+ // Recompute selected value from current store state and apply selector.
102
+ // Shared by the render-time eager check and the subscription callback.
103
+ function recompute(
104
+ sel: ((state: SegmentsState) => T) | undefined,
105
+ ): T | SegmentsState {
106
+ const location = ctx!.eventController.getLocation();
107
+ const handleState = ctx!.eventController.getHandleState();
108
+
109
+ // Reuse cached state when inputs haven't changed by reference,
110
+ // keeping array/object references stable for composite selectors.
111
+ const cache = segmentsCache.current;
112
+ let segmentsState: SegmentsState;
113
+ if (
114
+ cache &&
115
+ cache.location === location &&
116
+ cache.segmentOrder === handleState.segmentOrder
117
+ ) {
118
+ segmentsState = cache.state;
119
+ } else {
120
+ segmentsState = buildSegmentsState(
121
+ location as URL,
122
+ handleState.segmentOrder,
123
+ );
124
+ segmentsCache.current = {
125
+ location: location as URL,
126
+ segmentOrder: handleState.segmentOrder,
127
+ state: segmentsState,
128
+ };
129
+ }
130
+ return sel ? sel(segmentsState) : segmentsState;
131
+ }
132
+
133
+ if (ctx && selector !== prevSelectorIdentity.current) {
134
+ prevSelectorIdentity.current = selector;
135
+ const nextSelected = recompute(selector);
136
+ if (!shallowEqual(nextSelected, prevState.current)) {
137
+ prevState.current = nextSelected;
138
+ setState(nextSelected);
139
+ }
140
+ }
141
+
142
+ // Subscribe to store changes. The eager block above handles selector
143
+ // changes and SSR drift, so no initial updateState() call is needed.
144
+ useEffect(() => {
145
+ if (!ctx) {
146
+ return;
147
+ }
148
+
149
+ const updateState = () => {
150
+ const nextSelected = recompute(selectorRef.current);
151
+ if (!shallowEqual(nextSelected, prevState.current)) {
152
+ prevState.current = nextSelected;
153
+ setState(nextSelected);
154
+ }
155
+ };
156
+
157
+ const unsubscribeNav = ctx.eventController.subscribe(updateState);
158
+ const unsubscribeHandles =
159
+ ctx.eventController.subscribeToHandles(updateState);
160
+
161
+ return () => {
162
+ unsubscribeNav();
163
+ unsubscribeHandles();
164
+ };
165
+ // Stable subscription: selector changes are handled via selectorRef,
166
+ // state comparison uses prevState ref. No re-subscribe needed.
167
+ // eslint-disable-next-line react-hooks/exhaustive-deps
168
+ }, []);
169
+
170
+ return state as T | SegmentsState;
171
+ }
@@ -0,0 +1,73 @@
1
+ import { validateRedirectOrigin } from "./validate-redirect-origin.js";
2
+
3
+ type HeaderResult = { url: string } | "blocked" | null;
4
+
5
+ /**
6
+ * Extract and validate an RSC response header URL (X-RSC-Reload, X-RSC-Redirect).
7
+ * Returns { url } if valid, "blocked" if present but invalid origin, null if absent.
8
+ */
9
+ export function extractRscHeaderUrl(
10
+ response: Response,
11
+ header: string,
12
+ ): HeaderResult {
13
+ const raw = response.headers.get(header);
14
+ if (!raw) return null;
15
+ const url = validateRedirectOrigin(raw, window.location.origin);
16
+ return url ? { url } : "blocked";
17
+ }
18
+
19
+ /**
20
+ * Empty 200 response that won't choke Flight parsing.
21
+ * Used when a header URL is blocked by origin validation.
22
+ */
23
+ export function emptyResponse(): Response {
24
+ return new Response(null, { status: 200 });
25
+ }
26
+
27
+ /**
28
+ * Tee a response body for RSC parsing and stream completion tracking.
29
+ * Returns a new Response with one branch; the other is consumed to detect
30
+ * end-of-stream, calling onComplete when done.
31
+ *
32
+ * If the response has no body, onComplete fires synchronously.
33
+ * If signal is provided, an abort cancels the tracking reader.
34
+ */
35
+ export function teeWithCompletion(
36
+ response: Response,
37
+ onComplete: () => void,
38
+ signal?: AbortSignal,
39
+ ): Response {
40
+ if (!response.body) {
41
+ onComplete();
42
+ return response;
43
+ }
44
+
45
+ const [rscStream, trackingStream] = response.body.tee();
46
+
47
+ (async () => {
48
+ const reader = trackingStream.getReader();
49
+ const onAbort = signal ? reader.cancel.bind(reader) : undefined;
50
+ if (onAbort) signal!.addEventListener("abort", onAbort, { once: true });
51
+ try {
52
+ while (true) {
53
+ const { done } = await reader.read();
54
+ if (done) break;
55
+ }
56
+ } finally {
57
+ if (onAbort) signal!.removeEventListener("abort", onAbort);
58
+ reader.releaseLock();
59
+ onComplete();
60
+ }
61
+ })().catch((error) => {
62
+ if (!signal?.aborted) {
63
+ console.error("[Browser] Error reading tracking stream:", error);
64
+ }
65
+ onComplete();
66
+ });
67
+
68
+ return new Response(rscStream, {
69
+ headers: response.headers,
70
+ status: response.status,
71
+ statusText: response.statusText,
72
+ });
73
+ }
@@ -0,0 +1,431 @@
1
+ import React from "react";
2
+ import {
3
+ renderSegments as baseRenderSegments,
4
+ type RenderSegmentsOptions,
5
+ } from "../segment-system.js";
6
+ import {
7
+ createNavigationStore,
8
+ generateHistoryKey,
9
+ } from "./navigation-store.js";
10
+ import { createEventController } from "./event-controller.js";
11
+ import { createNavigationClient } from "./navigation-client.js";
12
+ import { createServerActionBridge } from "./server-action-bridge.js";
13
+ import { createNavigationBridge } from "./navigation-bridge.js";
14
+ import { NavigationProvider } from "./react/index.js";
15
+ import type {
16
+ RscPayload,
17
+ RscBrowserDependencies,
18
+ ResolvedSegment,
19
+ NavigationStore,
20
+ NavigationBridge,
21
+ } from "./types.js";
22
+ import type { EventController } from "./event-controller.js";
23
+ import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
24
+ import { initRangoState } from "./rango-state.js";
25
+ import { initPrefetchCache } from "./prefetch/cache.js";
26
+ import {
27
+ isInterceptSegment,
28
+ splitInterceptSegments,
29
+ } from "./intercept-utils.js";
30
+
31
+ // Vite HMR types are provided by vite/client
32
+
33
+ /**
34
+ * Options for initializing the browser app
35
+ */
36
+ export interface InitBrowserAppOptions {
37
+ /**
38
+ * RSC stream containing the initial payload (from rsc-html-stream/client)
39
+ */
40
+ rscStream: ReadableStream<Uint8Array>;
41
+
42
+ /**
43
+ * RSC browser dependencies from @vitejs/plugin-rsc/browser
44
+ */
45
+ deps: RscBrowserDependencies;
46
+
47
+ /**
48
+ * Optional store configuration
49
+ */
50
+ storeOptions?: {
51
+ /**
52
+ * Maximum number of history entries to cache
53
+ * @default 10
54
+ */
55
+ cacheSize?: number;
56
+ };
57
+
58
+ /**
59
+ * Enable global link interception for SPA navigation.
60
+ * When enabled, clicks on same-origin anchor elements are intercepted
61
+ * and handled via client-side navigation instead of full page loads.
62
+ *
63
+ * Links rendered with the Link component handle their own navigation
64
+ * regardless of this setting.
65
+ *
66
+ * Set to false to disable global interception and rely solely on
67
+ * Link components for SPA navigation.
68
+ *
69
+ * @default true
70
+ */
71
+ linkInterception?: boolean;
72
+
73
+ /**
74
+ * Theme configuration from router.
75
+ * When provided, enables theme support via useTheme hook.
76
+ * Pass router.themeConfig here to enable theme features.
77
+ *
78
+ * @example
79
+ * ```tsx
80
+ * import { router } from "./router.js";
81
+ *
82
+ * await initBrowserApp({
83
+ * rscStream,
84
+ * deps: rscBrowser,
85
+ * themeConfig: router.themeConfig,
86
+ * initialTheme: document.documentElement.className.includes("dark") ? "dark" : "light",
87
+ * });
88
+ * ```
89
+ */
90
+ themeConfig?: ResolvedThemeConfig | null;
91
+
92
+ /**
93
+ * Initial theme from server (typically read from cookie).
94
+ * Only used when themeConfig is provided.
95
+ */
96
+ initialTheme?: Theme;
97
+ }
98
+
99
+ /**
100
+ * Result from initializing the browser app
101
+ */
102
+ export interface BrowserAppContext {
103
+ store: NavigationStore;
104
+ eventController: EventController;
105
+ bridge: NavigationBridge;
106
+ initialPayload: RscPayload;
107
+ initialTree: React.ReactNode | Promise<React.ReactNode>;
108
+ /** Theme configuration (null if theme not enabled) */
109
+ themeConfig?: ResolvedThemeConfig | null;
110
+ /** Initial theme from server */
111
+ initialTheme?: Theme;
112
+ /** Whether connection warmup is enabled */
113
+ warmupEnabled?: boolean;
114
+ /** App version for prefetch version mismatch detection */
115
+ version?: string;
116
+ }
117
+
118
+ // Module-level state for the initialized app
119
+ let browserAppContext: BrowserAppContext | null = null;
120
+
121
+ /**
122
+ * Initialize the browser app. Must be called before rendering RSCRouter.
123
+ *
124
+ * This function:
125
+ * - Loads the initial RSC payload from the stream
126
+ * - Creates the navigation store and event controller
127
+ * - Sets up action and navigation bridges
128
+ * - Configures HMR support
129
+ */
130
+ export async function initBrowserApp(
131
+ options: InitBrowserAppOptions,
132
+ ): Promise<BrowserAppContext> {
133
+ const {
134
+ rscStream,
135
+ deps,
136
+ storeOptions,
137
+ linkInterception = true,
138
+ themeConfig,
139
+ initialTheme,
140
+ } = options;
141
+
142
+ // Load initial payload from SSR-injected __FLIGHT_DATA__
143
+ const initialPayload =
144
+ await deps.createFromReadableStream<RscPayload>(rscStream);
145
+
146
+ // Extract themeConfig and initialTheme from payload if not explicitly provided
147
+ // This allows virtual entries to work without importing the router
148
+ const effectiveThemeConfig =
149
+ themeConfig ?? initialPayload.metadata?.themeConfig ?? null;
150
+ const effectiveInitialTheme =
151
+ initialTheme ?? initialPayload.metadata?.initialTheme;
152
+
153
+ // Get initial segments and compute history key from current URL
154
+ const initialSegments = (initialPayload.metadata?.segments ??
155
+ []) as ResolvedSegment[];
156
+ const initialHistoryKey = generateHistoryKey(window.location.href);
157
+
158
+ // Create navigation store with history-based caching
159
+ const store = createNavigationStore({
160
+ initialLocation: window.location,
161
+ initialSegmentIds: initialSegments.map((s) => s.id),
162
+ initialHistoryKey,
163
+ initialSegments,
164
+ ...(storeOptions?.cacheSize && { cacheSize: storeOptions.cacheSize }),
165
+ });
166
+
167
+ // Create event controller for reactive state management
168
+ const eventController = createEventController({
169
+ initialLocation: new URL(window.location.href),
170
+ });
171
+
172
+ // Initialize event controller with segment order (even without handles)
173
+ eventController.setHandleData({}, initialPayload.metadata?.matched);
174
+
175
+ // Initialize route params
176
+ eventController.setParams(initialPayload.metadata?.params ?? {});
177
+
178
+ // Initialize handle data from initial payload BEFORE hydration
179
+ // This ensures useHandle returns correct data during hydration to avoid mismatch
180
+ // The handles property is an async generator that yields on each push
181
+ if (initialPayload.metadata?.handles) {
182
+ const handlesGenerator = initialPayload.metadata.handles;
183
+ let lastHandleData: Record<string, Record<string, unknown[]>> = {};
184
+ for await (const handleData of handlesGenerator) {
185
+ lastHandleData = handleData;
186
+ }
187
+ // Initialize event controller with initial handle state before hydration.
188
+ eventController.setHandleData(
189
+ lastHandleData,
190
+ initialPayload.metadata?.matched,
191
+ );
192
+
193
+ // Update the initial cache entry with the processed handleData
194
+ // The cache entry was created by createNavigationStore but without handleData
195
+ store.updateCacheHandleData(initialHistoryKey, lastHandleData);
196
+ }
197
+
198
+ // Create composable utilities
199
+ const client = createNavigationClient(deps);
200
+
201
+ // Extract rootLayout and version from metadata for browser-side re-renders
202
+ const rootLayout = initialPayload.metadata?.rootLayout;
203
+ const version = initialPayload.metadata?.version;
204
+
205
+ // Initialize the localStorage state key for cache invalidation.
206
+ // Uses the build version so a new deploy automatically busts all cached prefetches.
207
+ initRangoState(version ?? "0");
208
+
209
+ // Initialize the in-memory prefetch cache TTL from server config.
210
+ // A value of 0 disables the cache; undefined falls back to the module default.
211
+ const prefetchCacheTTL = initialPayload.metadata?.prefetchCacheTTL;
212
+ if (prefetchCacheTTL !== undefined) {
213
+ initPrefetchCache(prefetchCacheTTL);
214
+ }
215
+
216
+ // Create a bound renderSegments that includes rootLayout
217
+ const renderSegments = (
218
+ segments: ResolvedSegment[],
219
+ options?: RenderSegmentsOptions,
220
+ ) => baseRenderSegments(segments, { ...options, rootLayout });
221
+
222
+ // Lazy reference for navigation bridge — the action bridge is created first
223
+ // but may need to trigger SPA navigation for action redirects.
224
+ let navigateFn: ((url: string, options?: any) => Promise<void>) | null = null;
225
+
226
+ // Setup server action bridge
227
+ const actionBridge = createServerActionBridge({
228
+ store,
229
+ eventController,
230
+ client,
231
+ deps,
232
+ onUpdate: (update) => store.emitUpdate(update),
233
+ renderSegments,
234
+ version,
235
+ onNavigate: (url, options) => {
236
+ if (!navigateFn) {
237
+ window.location.href = url;
238
+ return Promise.resolve();
239
+ }
240
+ return navigateFn(url, options);
241
+ },
242
+ });
243
+ actionBridge.register();
244
+
245
+ // Setup navigation bridge
246
+ const navigationBridge = createNavigationBridge({
247
+ store,
248
+ eventController,
249
+ client,
250
+ onUpdate: (update) => store.emitUpdate(update),
251
+ renderSegments,
252
+ version,
253
+ });
254
+
255
+ // Connect action redirect → navigation bridge (now that both are initialized)
256
+ navigateFn = (url, options) => navigationBridge.navigate(url, options);
257
+
258
+ // Optionally enable global link interception
259
+ if (linkInterception) {
260
+ navigationBridge.registerLinkInterception();
261
+ }
262
+
263
+ // Build initial tree with rootLayout
264
+ const initialTree = renderSegments(initialPayload.metadata!.segments);
265
+
266
+ // Setup HMR
267
+ if (import.meta.hot) {
268
+ import.meta.hot.on("rsc:update", async () => {
269
+ console.log("[RSCRouter] HMR: Server update, refetching RSC");
270
+
271
+ const handle = eventController.startNavigation(window.location.href, {
272
+ replace: true,
273
+ });
274
+ const streamingToken = handle.startStreaming();
275
+
276
+ const interceptSourceUrl = store.getInterceptSourceUrl();
277
+
278
+ try {
279
+ const { payload, streamComplete } = await client.fetchPartial({
280
+ targetUrl: window.location.href,
281
+ segmentIds: [],
282
+ previousUrl: store.getSegmentState().currentUrl,
283
+ interceptSourceUrl: interceptSourceUrl || undefined,
284
+ hmr: true,
285
+ });
286
+
287
+ if (payload.metadata?.isPartial) {
288
+ const segments = payload.metadata.segments || [];
289
+ const matched = payload.metadata.matched || [];
290
+
291
+ // Derive intercept state from the returned payload, not the
292
+ // pre-fetch store snapshot. If the HMR edit removed intercept
293
+ // behavior, the response won't contain intercept segments.
294
+ const responseIsIntercept = segments.some(isInterceptSegment);
295
+
296
+ // Sync store intercept state with what the server returned
297
+ if (!responseIsIntercept && interceptSourceUrl) {
298
+ store.setInterceptSourceUrl(null);
299
+ }
300
+
301
+ store.setSegmentIds(matched);
302
+ store.setCurrentUrl(window.location.href);
303
+
304
+ const historyKey = generateHistoryKey(window.location.href, {
305
+ intercept: responseIsIntercept,
306
+ });
307
+ store.setHistoryKey(historyKey);
308
+ const currentHandleData = eventController.getHandleState().data;
309
+ store.cacheSegmentsForHistory(
310
+ historyKey,
311
+ segments,
312
+ currentHandleData,
313
+ );
314
+
315
+ const { main, intercept } = splitInterceptSegments(segments);
316
+ store.emitUpdate({
317
+ root: renderSegments(main, {
318
+ interceptSegments: intercept.length > 0 ? intercept : undefined,
319
+ }),
320
+ metadata: payload.metadata,
321
+ });
322
+ }
323
+
324
+ await streamComplete;
325
+ handle.complete(new URL(window.location.href));
326
+ console.log("[RSCRouter] HMR: RSC stream complete");
327
+ } finally {
328
+ streamingToken.end();
329
+ handle[Symbol.dispose]();
330
+ }
331
+ });
332
+ }
333
+
334
+ // Store context for RSCRouter component
335
+ const context: BrowserAppContext = {
336
+ store,
337
+ eventController,
338
+ bridge: navigationBridge,
339
+ initialPayload,
340
+ initialTree,
341
+ themeConfig: effectiveThemeConfig,
342
+ initialTheme: effectiveInitialTheme,
343
+ warmupEnabled: initialPayload.metadata?.warmupEnabled ?? true,
344
+ version,
345
+ };
346
+ browserAppContext = context;
347
+
348
+ return context;
349
+ }
350
+
351
+ /**
352
+ * Get the browser app context. Throws if initBrowserApp hasn't been called.
353
+ */
354
+ export function getBrowserAppContext(): BrowserAppContext {
355
+ if (!browserAppContext) {
356
+ throw new Error(
357
+ "RSCRouter: initBrowserApp() must be called before rendering RSCRouter",
358
+ );
359
+ }
360
+ return browserAppContext;
361
+ }
362
+
363
+ /**
364
+ * Reset the browser app context (for testing)
365
+ */
366
+ export function resetBrowserAppContext(): void {
367
+ browserAppContext = null;
368
+ }
369
+
370
+ /**
371
+ * Props for the RSCRouter component
372
+ */
373
+ export interface RSCRouterProps {}
374
+
375
+ /**
376
+ * RSCRouter component - renders the RSC router with all internal wiring.
377
+ *
378
+ * Must be called after initBrowserApp() has completed.
379
+ *
380
+ * @example
381
+ * ```tsx
382
+ * import { initBrowserApp, RSCRouter } from "rsc-router/browser";
383
+ * import { rscStream } from "rsc-html-stream/client";
384
+ * import * as rscBrowser from "@vitejs/plugin-rsc/browser";
385
+ *
386
+ * async function main() {
387
+ * await initBrowserApp({ rscStream, deps: rscBrowser });
388
+ *
389
+ * hydrateRoot(
390
+ * document,
391
+ * <React.StrictMode>
392
+ * <RSCRouter />
393
+ * </React.StrictMode>
394
+ * );
395
+ * }
396
+ * main();
397
+ * ```
398
+ */
399
+ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
400
+ const {
401
+ store,
402
+ eventController,
403
+ bridge,
404
+ initialPayload,
405
+ initialTree,
406
+ themeConfig,
407
+ initialTheme,
408
+ warmupEnabled,
409
+ version,
410
+ } = getBrowserAppContext();
411
+
412
+ // Signal that the React tree has hydrated. useEffect only fires after
413
+ // hydration completes, so this attribute is a stable readiness marker
414
+ // that does not depend on React internals like __reactFiber.
415
+ React.useEffect(() => {
416
+ document.documentElement.dataset.hydrated = "";
417
+ }, []);
418
+
419
+ return (
420
+ <NavigationProvider
421
+ store={store}
422
+ eventController={eventController}
423
+ initialPayload={{ root: initialTree, metadata: initialPayload.metadata! }}
424
+ bridge={bridge}
425
+ themeConfig={themeConfig}
426
+ initialTheme={initialTheme}
427
+ warmupEnabled={warmupEnabled}
428
+ version={version}
429
+ />
430
+ );
431
+ }