@rangojs/router 0.0.0-experimental.002d056c

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 +9 -0
  2. package/README.md +899 -0
  3. package/dist/bin/rango.js +1606 -0
  4. package/dist/vite/index.js +5153 -0
  5. package/package.json +177 -0
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +253 -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 +638 -0
  43. package/src/browser/navigation-client.ts +261 -0
  44. package/src/browser/navigation-store.ts +806 -0
  45. package/src/browser/navigation-transaction.ts +297 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +582 -0
  48. package/src/browser/prefetch/cache.ts +206 -0
  49. package/src/browser/prefetch/fetch.ts +145 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +48 -0
  52. package/src/browser/prefetch/queue.ts +128 -0
  53. package/src/browser/rango-state.ts +112 -0
  54. package/src/browser/react/Link.tsx +368 -0
  55. package/src/browser/react/NavigationProvider.tsx +413 -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 +464 -0
  79. package/src/browser/scroll-restoration.ts +397 -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 +547 -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 +479 -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 +982 -0
  105. package/src/cache/cf/index.ts +29 -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 +44 -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 +281 -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 +160 -0
  170. package/src/router/handler-context.ts +451 -0
  171. package/src/router/intercept-resolution.ts +397 -0
  172. package/src/router/lazy-includes.ts +236 -0
  173. package/src/router/loader-resolution.ts +420 -0
  174. package/src/router/logging.ts +251 -0
  175. package/src/router/manifest.ts +269 -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 +193 -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 +749 -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 +320 -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 +1242 -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 +291 -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 +1006 -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 +237 -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 +920 -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 +109 -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 +108 -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 +48 -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 +363 -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 +266 -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 +445 -0
  298. package/src/vite/router-discovery.ts +777 -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,80 @@
1
+ import {
2
+ isLocationStateEntry,
3
+ resolveLocationStateEntries,
4
+ } from "./react/location-state-shared.js";
5
+
6
+ /**
7
+ * Check if state is from typed LocationStateEntry[] (has __rsc_ls_ keys)
8
+ */
9
+ function isTypedLocationState(
10
+ state: unknown,
11
+ ): state is Record<string, unknown> {
12
+ if (state === null || typeof state !== "object") return false;
13
+ return Object.keys(state).some((key) => key.startsWith("__rsc_ls_"));
14
+ }
15
+
16
+ /**
17
+ * Resolve navigation state - handles both LocationStateEntry[] and plain formats
18
+ */
19
+ export function resolveNavigationState(state: unknown): unknown {
20
+ if (
21
+ Array.isArray(state) &&
22
+ state.length > 0 &&
23
+ isLocationStateEntry(state[0])
24
+ ) {
25
+ return resolveLocationStateEntries(state);
26
+ }
27
+ return state;
28
+ }
29
+
30
+ /**
31
+ * Build history state object from user state
32
+ * - Typed state: spread directly into history.state
33
+ * - Plain state: store in history.state.state
34
+ */
35
+ export function buildHistoryState(
36
+ userState: unknown,
37
+ routerState?: { intercept?: boolean; sourceUrl?: string },
38
+ serverState?: Record<string, unknown>,
39
+ ): Record<string, unknown> | null {
40
+ const result: Record<string, unknown> = {};
41
+
42
+ if (routerState?.intercept) {
43
+ result.intercept = true;
44
+ if (routerState.sourceUrl) {
45
+ result.sourceUrl = routerState.sourceUrl;
46
+ }
47
+ }
48
+
49
+ if (userState !== undefined) {
50
+ if (isTypedLocationState(userState)) {
51
+ Object.assign(result, userState);
52
+ } else {
53
+ result.state = userState;
54
+ }
55
+ }
56
+
57
+ if (serverState) {
58
+ Object.assign(result, serverState);
59
+ }
60
+
61
+ return Object.keys(result).length > 0 ? result : null;
62
+ }
63
+
64
+ /**
65
+ * Merge server-set location state into the current history entry.
66
+ * Replaces the current history state and dispatches notification event
67
+ * so useLocationState hooks re-read from history.state.
68
+ */
69
+ export function mergeLocationState(
70
+ locationState: Record<string, unknown>,
71
+ ): void {
72
+ const merged = {
73
+ ...window.history.state,
74
+ ...locationState,
75
+ };
76
+ window.history.replaceState(merged, "", window.location.href);
77
+ if (Object.keys(locationState).some((k) => k.startsWith("__rsc_ls_"))) {
78
+ window.dispatchEvent(new Event("__rsc_locationstate"));
79
+ }
80
+ }
@@ -0,0 +1,18 @@
1
+ // ============================================================================
2
+ // Browser Module - Browser entry point for RSC Router
3
+ // ============================================================================
4
+ //
5
+ // Usage:
6
+ // import { initBrowserApp, RSCRouter } from "rsc-router/browser";
7
+ //
8
+ // For React components (Link, useNavigation, etc.):
9
+ // import { Link, useNavigation, useAction, href } from "rsc-router/client";
10
+ //
11
+ // ============================================================================
12
+
13
+ // Browser app initialization
14
+ export {
15
+ initBrowserApp,
16
+ RSCRouter,
17
+ type InitBrowserAppOptions,
18
+ } from "./rsc-router.js";
@@ -0,0 +1,52 @@
1
+ import type { ResolvedSegment } from "./types.js";
2
+ import type { SlotState } from "../types.js";
3
+
4
+ /**
5
+ * Check if a segment is an intercept segment.
6
+ * Intercept segments have namespace starting with "intercept:" — both the
7
+ * parallel container (@modal) and its content children receive this namespace
8
+ * from intercept-resolution.ts. Regular parallel segments like @sidebar do not.
9
+ */
10
+ export function isInterceptSegment(s: ResolvedSegment): boolean {
11
+ return s.namespace?.startsWith("intercept:") === true;
12
+ }
13
+
14
+ /**
15
+ * Split an array of segments into main and intercept groups.
16
+ * Intercept segments are separated for explicit injection into the render tree
17
+ * via the interceptSegments render option.
18
+ */
19
+ export function splitInterceptSegments(segments: ResolvedSegment[]): {
20
+ main: ResolvedSegment[];
21
+ intercept: ResolvedSegment[];
22
+ } {
23
+ const main: ResolvedSegment[] = [];
24
+ const intercept: ResolvedSegment[] = [];
25
+ for (const s of segments) {
26
+ if (isInterceptSegment(s)) {
27
+ intercept.push(s);
28
+ } else {
29
+ main.push(s);
30
+ }
31
+ }
32
+ return { main, intercept };
33
+ }
34
+
35
+ /**
36
+ * Check if any slot is currently active (has content to render).
37
+ * Active slots indicate an intercept response where a parallel segment
38
+ * (e.g., @modal) has matched and should be rendered.
39
+ */
40
+ export function hasActiveIntercept(slots?: Record<string, SlotState>): boolean {
41
+ if (!slots) return false;
42
+ return Object.values(slots).some((slot) => slot.active);
43
+ }
44
+
45
+ /**
46
+ * Check if cached segments contain any intercept segments.
47
+ * Intercept caches shouldn't be used for cached SWR rendering since
48
+ * whether interception happens depends on the current page context.
49
+ */
50
+ export function isInterceptOnlyCache(segments: ResolvedSegment[]): boolean {
51
+ return segments.some(isInterceptSegment);
52
+ }
@@ -0,0 +1,141 @@
1
+ import type { LinkInterceptorOptions, NavigateOptions } from "./types.js";
2
+
3
+ /**
4
+ * Check if an anchor points to the same page with only a hash change.
5
+ * Used by both Link component and link-interceptor to let the browser
6
+ * handle anchor scrolling natively.
7
+ */
8
+ export function isHashOnlyNavigation(anchor: HTMLAnchorElement): boolean {
9
+ return (
10
+ anchor.pathname === window.location.pathname &&
11
+ anchor.search === window.location.search &&
12
+ !!anchor.hash
13
+ );
14
+ }
15
+
16
+ /**
17
+ * Default link interception predicate
18
+ *
19
+ * Returns true if the link should be intercepted for SPA navigation.
20
+ * Filters out:
21
+ * - Cross-origin links
22
+ * - Links with download attribute
23
+ * - Links with target other than _self
24
+ * - Links with data-no-intercept attribute
25
+ *
26
+ * @param link - The anchor element to check
27
+ * @returns true if the link should be intercepted
28
+ */
29
+ export function defaultShouldIntercept(link: HTMLAnchorElement): boolean {
30
+ // Only intercept same-origin links
31
+ if (link.origin !== window.location.origin) {
32
+ return false;
33
+ }
34
+
35
+ // Don't intercept if it has download attribute
36
+ if (link.hasAttribute("download")) {
37
+ return false;
38
+ }
39
+
40
+ // Don't intercept if target is set to something other than _self
41
+ if (link.target && link.target !== "_self") {
42
+ return false;
43
+ }
44
+
45
+ // Don't intercept if explicitly disabled
46
+ if (link.getAttribute("data-no-intercept") === "true") {
47
+ return false;
48
+ }
49
+
50
+ // Don't intercept Link component anchors - they handle their own navigation
51
+ if (link.hasAttribute("data-link-component")) {
52
+ return false;
53
+ }
54
+
55
+ // Don't intercept external links
56
+ if (link.hasAttribute("data-external")) {
57
+ return false;
58
+ }
59
+
60
+ // Don't intercept hash-only navigation (same path, only fragment changes).
61
+ // Let the browser handle anchor scrolling natively.
62
+ if (isHashOnlyNavigation(link)) {
63
+ return false;
64
+ }
65
+
66
+ return true;
67
+ }
68
+
69
+ /**
70
+ * Set up link interception for SPA navigation
71
+ *
72
+ * Attaches a global click handler to intercept clicks on anchor elements
73
+ * and call the onNavigate callback instead of performing a full page load.
74
+ *
75
+ * @param onNavigate - Callback when a link should navigate via SPA
76
+ * @param options - Configuration options
77
+ * @returns Cleanup function to remove the event listener
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * const cleanup = setupLinkInterception((url) => {
82
+ * window.history.pushState({}, "", url);
83
+ * fetchPartialUpdate(url);
84
+ * });
85
+ *
86
+ * // Later, to clean up:
87
+ * cleanup();
88
+ * ```
89
+ */
90
+ export function setupLinkInterception(
91
+ onNavigate: (url: string, options?: NavigateOptions) => void,
92
+ options?: LinkInterceptorOptions,
93
+ ): () => void {
94
+ const shouldIntercept = options?.shouldIntercept ?? defaultShouldIntercept;
95
+
96
+ const handleClick = (event: MouseEvent) => {
97
+ // If event was already handled by Link component (or other handler), skip
98
+ if (event.defaultPrevented) {
99
+ return;
100
+ }
101
+
102
+ const target = event.target as HTMLElement;
103
+ const link = target.closest("a");
104
+
105
+ if (!link || !shouldIntercept(link)) {
106
+ return;
107
+ }
108
+
109
+ // Don't intercept if modifier keys are pressed (open in new tab, etc.)
110
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
111
+ return;
112
+ }
113
+
114
+ event.preventDefault();
115
+ const href = link.href;
116
+
117
+ // Read navigation options from data attributes (set by Link component)
118
+ const scrollAttr = link.getAttribute("data-scroll");
119
+ const replaceAttr = link.getAttribute("data-replace");
120
+ const revalidateAttr = link.getAttribute("data-revalidate");
121
+
122
+ const navigateOptions: NavigateOptions = {};
123
+ if (scrollAttr === "false") {
124
+ navigateOptions.scroll = false;
125
+ }
126
+ if (replaceAttr === "true") {
127
+ navigateOptions.replace = true;
128
+ }
129
+ if (revalidateAttr === "false") {
130
+ navigateOptions.revalidate = false;
131
+ }
132
+
133
+ onNavigate(href, navigateOptions);
134
+ };
135
+
136
+ document.addEventListener("click", handleClick);
137
+
138
+ return () => {
139
+ document.removeEventListener("click", handleClick);
140
+ };
141
+ }
@@ -0,0 +1,55 @@
1
+ import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
2
+
3
+ interface BrowserLogContext {
4
+ requestId: string;
5
+ txId: string;
6
+ operation: string;
7
+ }
8
+
9
+ let txCounter = 0;
10
+ let requestCounter = 0;
11
+
12
+ export function isBrowserDebugEnabled(): boolean {
13
+ return INTERNAL_RANGO_DEBUG;
14
+ }
15
+
16
+ function nextId(prefix: string, counter: number): string {
17
+ return `${prefix}${counter.toString(36)}`;
18
+ }
19
+
20
+ export function startBrowserTransaction(operation: string): BrowserLogContext {
21
+ txCounter += 1;
22
+ requestCounter += 1;
23
+ return {
24
+ operation,
25
+ txId: nextId("ctx-", txCounter),
26
+ requestId: nextId("creq-", requestCounter),
27
+ };
28
+ }
29
+
30
+ export function browserDebugLog(
31
+ ctx: BrowserLogContext,
32
+ message: string,
33
+ details?: Record<string, unknown>,
34
+ ): void {
35
+ if (!INTERNAL_RANGO_DEBUG) return;
36
+
37
+ const prefix = `[Browser][req:${ctx.requestId}][tx:${ctx.operation}-${ctx.txId}]`;
38
+ if (details) {
39
+ console.log(`${prefix} ${message}`, details);
40
+ return;
41
+ }
42
+
43
+ console.log(`${prefix} ${message}`);
44
+ }
45
+
46
+ /**
47
+ * Simple gated console.log for browser-side debug output.
48
+ * Unlike browserDebugLog, this doesn't require a transaction context -
49
+ * use it for standalone debug messages in partial-update, navigation-bridge, etc.
50
+ */
51
+ export function debugLog(msg: string, ...args: unknown[]): void {
52
+ if (INTERNAL_RANGO_DEBUG) {
53
+ console.log(msg, ...args);
54
+ }
55
+ }
@@ -0,0 +1,134 @@
1
+ import type { ResolvedSegment } from "./types.js";
2
+ import { debugLog } from "./logging.js";
3
+
4
+ /**
5
+ * Merge partial loader data from server with cached loader data.
6
+ *
7
+ * During partial revalidation (stale or action), the server may return only
8
+ * some loaders that pass the revalidation check. The component still needs
9
+ * all loader data, so we merge fresh data with cached data.
10
+ *
11
+ * @param fromServer - Segment returned from server with partial loaders
12
+ * @param fromCache - Cached segment with full loader data
13
+ * @returns Merged segment with complete loader data
14
+ */
15
+ export function mergeSegmentLoaders(
16
+ fromServer: ResolvedSegment,
17
+ fromCache: ResolvedSegment,
18
+ ): ResolvedSegment {
19
+ const serverLoaderIds = fromServer.loaderIds || [];
20
+ const cachedLoaderIds = fromCache.loaderIds || [];
21
+
22
+ debugLog(
23
+ `[Browser] Merging partial loaders: server has ${serverLoaderIds.join(", ")}, cache has ${cachedLoaderIds.join(", ")}`,
24
+ );
25
+
26
+ return {
27
+ ...fromCache,
28
+ // Keep cached component (server's might be a fresh Promise that needs the loaders)
29
+ component: fromCache.component,
30
+ // Merge loader data - await both and combine
31
+ loaderDataPromise: Promise.all([
32
+ fromServer.loaderDataPromise!,
33
+ fromCache.loaderDataPromise!,
34
+ ]).then(([newData, cachedData]) => {
35
+ // Build merged array: use new data for updated loaders, cached for rest
36
+ return cachedLoaderIds.map((id: string, i: number) => {
37
+ const newIndex = serverLoaderIds.indexOf(id);
38
+ if (newIndex !== -1) {
39
+ return (newData as any[])[newIndex]; // Use fresh data
40
+ }
41
+ return (cachedData as any[])[i]; // Use cached data
42
+ });
43
+ }),
44
+ // Keep all loader IDs from cache
45
+ loaderIds: fromCache.loaderIds,
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Check if segments need loader merging during partial revalidation.
51
+ *
52
+ * Returns true when:
53
+ * - Server returned fewer loaders than cached (partial revalidation)
54
+ * - Both segments have loader data promises
55
+ */
56
+ export function needsLoaderMerge(
57
+ fromServer: ResolvedSegment,
58
+ fromCache: ResolvedSegment | undefined,
59
+ ): fromCache is ResolvedSegment {
60
+ return !!(
61
+ fromCache &&
62
+ fromServer.loaderIds &&
63
+ fromCache.loaderIds &&
64
+ fromServer.loaderIds.length < fromCache.loaderIds.length &&
65
+ fromServer.loaderDataPromise &&
66
+ fromCache.loaderDataPromise
67
+ );
68
+ }
69
+
70
+ /**
71
+ * Insert diff segments that aren't in the matched array into allSegments.
72
+ *
73
+ * During consolidation fetch for concurrent actions, loader segments may be
74
+ * excluded from the request. The server returns them in the diff but not in
75
+ * the matched array. This function inserts them at the correct position
76
+ * (after their parent layout segment).
77
+ *
78
+ * Loader segment IDs follow the pattern: {parentLayoutId}D{index}.{loaderId}
79
+ * Example: M9L0L1D0.actionCounter has parent layout M9L0L1
80
+ *
81
+ * @param allSegments - Mutable array of segments to insert into
82
+ * @param diff - Array of segment IDs that changed (from server response)
83
+ * @param matchedIdSet - Set of segment IDs from matched array
84
+ * @param newSegmentMap - Map of segment ID to segment data from server
85
+ */
86
+ export function insertMissingDiffSegments(
87
+ allSegments: ResolvedSegment[],
88
+ diff: string[] | undefined,
89
+ matchedIdSet: Set<string>,
90
+ newSegmentMap: Map<string, ResolvedSegment>,
91
+ ): void {
92
+ if (!diff || diff.length === 0) return;
93
+
94
+ // Track how many siblings have been inserted per parent so each new
95
+ // sibling goes after the last one rather than always at parentIndex + 1
96
+ // (which would reverse the server order).
97
+ const insertedPerParent = new Map<string, number>();
98
+
99
+ diff.forEach((diffId: string) => {
100
+ if (!matchedIdSet.has(diffId)) {
101
+ const fromServer = newSegmentMap.get(diffId);
102
+ if (fromServer) {
103
+ // Loader segment IDs have pattern like M9L0L1D0.actionCounter
104
+ // Parent layout ID is the prefix before D\d+ (e.g., M9L0L1)
105
+ const loaderMatch = diffId.match(/^(.+?)D\d+\./);
106
+ if (loaderMatch) {
107
+ const parentLayoutId = loaderMatch[1];
108
+ const parentIndex = allSegments.findIndex(
109
+ (s) => s.id === parentLayoutId,
110
+ );
111
+ if (parentIndex !== -1) {
112
+ const alreadyInserted = insertedPerParent.get(parentLayoutId) ?? 0;
113
+ const insertAt = parentIndex + 1 + alreadyInserted;
114
+ allSegments.splice(insertAt, 0, fromServer);
115
+ insertedPerParent.set(parentLayoutId, alreadyInserted + 1);
116
+ debugLog(
117
+ `[Browser] Inserted diff segment ${diffId} after ${parentLayoutId}`,
118
+ );
119
+ } else {
120
+ // Fallback: append to end if parent not found
121
+ allSegments.push(fromServer);
122
+ console.warn(
123
+ `[Browser] Appended diff segment ${diffId} (parent ${parentLayoutId} not found)`,
124
+ );
125
+ }
126
+ } else {
127
+ // Non-loader diff segment not in matched - append to end
128
+ allSegments.push(fromServer);
129
+ debugLog(`[Browser] Appended diff segment ${diffId}`);
130
+ }
131
+ }
132
+ }
133
+ });
134
+ }