@rangojs/router 0.0.0-experimental.77 → 0.0.0-experimental.77ed8945

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 (239) hide show
  1. package/README.md +120 -25
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/vite/index.js +2103 -861
  4. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  5. package/package.json +13 -8
  6. package/skills/api-client/SKILL.md +211 -0
  7. package/skills/breadcrumbs/SKILL.md +3 -1
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +220 -30
  10. package/skills/caching/SKILL.md +116 -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 +229 -20
  16. package/skills/host-router/SKILL.md +66 -20
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +26 -4
  19. package/skills/layout/SKILL.md +6 -7
  20. package/skills/links/SKILL.md +247 -17
  21. package/skills/loader/SKILL.md +219 -9
  22. package/skills/middleware/SKILL.md +47 -12
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +27 -0
  26. package/skills/observability/SKILL.md +137 -0
  27. package/skills/parallel/SKILL.md +12 -6
  28. package/skills/prerender/SKILL.md +14 -33
  29. package/skills/rango/SKILL.md +238 -22
  30. package/skills/react-compiler/SKILL.md +168 -0
  31. package/skills/response-routes/SKILL.md +122 -47
  32. package/skills/route/SKILL.md +33 -4
  33. package/skills/router-setup/SKILL.md +3 -3
  34. package/skills/server-actions/SKILL.md +751 -0
  35. package/skills/streams-and-websockets/SKILL.md +283 -0
  36. package/skills/tailwind/SKILL.md +27 -3
  37. package/skills/typesafety/SKILL.md +319 -27
  38. package/skills/use-cache/SKILL.md +34 -5
  39. package/skills/view-transitions/SKILL.md +294 -0
  40. package/src/__augment-tests__/augment.ts +81 -0
  41. package/src/__augment-tests__/augmented.check.ts +116 -0
  42. package/src/browser/action-coordinator.ts +53 -36
  43. package/src/browser/app-shell.ts +39 -0
  44. package/src/browser/event-controller.ts +86 -70
  45. package/src/browser/history-state.ts +21 -0
  46. package/src/browser/index.ts +3 -3
  47. package/src/browser/navigation-bridge.ts +29 -9
  48. package/src/browser/navigation-client.ts +99 -77
  49. package/src/browser/navigation-store.ts +7 -8
  50. package/src/browser/navigation-transaction.ts +10 -28
  51. package/src/browser/partial-update.ts +60 -40
  52. package/src/browser/prefetch/cache.ts +196 -49
  53. package/src/browser/prefetch/fetch.ts +203 -59
  54. package/src/browser/prefetch/queue.ts +36 -5
  55. package/src/browser/rango-state.ts +37 -13
  56. package/src/browser/react/Link.tsx +18 -13
  57. package/src/browser/react/NavigationProvider.tsx +75 -31
  58. package/src/browser/react/filter-segment-order.ts +51 -7
  59. package/src/browser/react/index.ts +3 -0
  60. package/src/browser/react/location-state-shared.ts +175 -4
  61. package/src/browser/react/location-state.ts +39 -13
  62. package/src/browser/react/use-handle.ts +17 -9
  63. package/src/browser/react/use-navigation.ts +22 -2
  64. package/src/browser/react/use-params.ts +20 -8
  65. package/src/browser/react/use-reverse.ts +106 -0
  66. package/src/browser/react/use-router.ts +23 -2
  67. package/src/browser/react/use-segments.ts +11 -8
  68. package/src/browser/response-adapter.ts +52 -1
  69. package/src/browser/rsc-router.tsx +71 -22
  70. package/src/browser/scroll-restoration.ts +22 -14
  71. package/src/browser/segment-reconciler.ts +10 -14
  72. package/src/browser/segment-structure-assert.ts +2 -2
  73. package/src/browser/server-action-bridge.ts +44 -30
  74. package/src/browser/types.ts +12 -2
  75. package/src/build/collect-fallback-refs.ts +107 -0
  76. package/src/build/generate-manifest.ts +60 -35
  77. package/src/build/generate-route-types.ts +2 -0
  78. package/src/build/index.ts +8 -1
  79. package/src/build/prefix-tree-utils.ts +123 -0
  80. package/src/build/route-trie.ts +45 -1
  81. package/src/build/route-types/codegen.ts +4 -4
  82. package/src/build/route-types/include-resolution.ts +1 -1
  83. package/src/build/route-types/per-module-writer.ts +7 -4
  84. package/src/build/route-types/router-processing.ts +55 -14
  85. package/src/build/route-types/scan-filter.ts +1 -1
  86. package/src/build/route-types/source-scan.ts +118 -0
  87. package/src/build/runtime-discovery.ts +9 -20
  88. package/src/cache/cache-runtime.ts +17 -5
  89. package/src/cache/cache-scope.ts +51 -49
  90. package/src/cache/cf/cf-cache-store.ts +502 -32
  91. package/src/cache/cf/index.ts +3 -0
  92. package/src/cache/handle-snapshot.ts +103 -0
  93. package/src/cache/index.ts +3 -0
  94. package/src/cache/memory-segment-store.ts +3 -2
  95. package/src/cache/types.ts +10 -6
  96. package/src/client.rsc.tsx +3 -0
  97. package/src/client.tsx +96 -205
  98. package/src/context-var.ts +5 -5
  99. package/src/decode-loader-results.ts +36 -0
  100. package/src/errors.ts +30 -4
  101. package/src/handle.ts +4 -6
  102. package/src/host/index.ts +2 -2
  103. package/src/host/router.ts +129 -57
  104. package/src/host/types.ts +31 -2
  105. package/src/host/utils.ts +1 -1
  106. package/src/href-client.ts +140 -21
  107. package/src/index.rsc.ts +10 -6
  108. package/src/index.ts +17 -8
  109. package/src/loader-store.ts +500 -0
  110. package/src/loader.rsc.ts +2 -5
  111. package/src/loader.ts +3 -10
  112. package/src/missing-id-error.ts +68 -0
  113. package/src/outlet-context.ts +1 -1
  114. package/src/prerender/store.ts +9 -7
  115. package/src/prerender.ts +4 -4
  116. package/src/response-utils.ts +37 -0
  117. package/src/reverse.ts +65 -39
  118. package/src/route-content-wrapper.tsx +6 -28
  119. package/src/route-definition/dsl-helpers.ts +253 -265
  120. package/src/route-definition/helper-factories.ts +29 -139
  121. package/src/route-definition/helpers-types.ts +43 -15
  122. package/src/route-definition/resolve-handler-use.ts +6 -0
  123. package/src/route-definition/use-item-types.ts +32 -0
  124. package/src/route-types.ts +26 -41
  125. package/src/router/content-negotiation.ts +15 -2
  126. package/src/router/error-handling.ts +1 -1
  127. package/src/router/find-match.ts +54 -6
  128. package/src/router/handler-context.ts +21 -41
  129. package/src/router/intercept-resolution.ts +4 -18
  130. package/src/router/lazy-includes.ts +41 -22
  131. package/src/router/loader-resolution.ts +82 -36
  132. package/src/router/manifest.ts +41 -19
  133. package/src/router/match-api.ts +4 -3
  134. package/src/router/match-handlers.ts +1 -0
  135. package/src/router/match-middleware/cache-lookup.ts +57 -95
  136. package/src/router/match-middleware/cache-store.ts +3 -2
  137. package/src/router/match-result.ts +53 -32
  138. package/src/router/metrics.ts +1 -1
  139. package/src/router/middleware-types.ts +15 -26
  140. package/src/router/middleware.ts +99 -84
  141. package/src/router/pattern-matching.ts +116 -19
  142. package/src/router/prerender-match.ts +40 -15
  143. package/src/router/preview-match.ts +3 -1
  144. package/src/router/request-classification.ts +40 -37
  145. package/src/router/revalidation.ts +58 -2
  146. package/src/router/router-interfaces.ts +51 -35
  147. package/src/router/router-options.ts +25 -1
  148. package/src/router/router-registry.ts +2 -5
  149. package/src/router/segment-resolution/fresh.ts +27 -6
  150. package/src/router/segment-resolution/revalidation.ts +147 -106
  151. package/src/router/segment-resolution/static-store.ts +19 -5
  152. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  153. package/src/router/substitute-pattern-params.ts +56 -0
  154. package/src/router/trie-matching.ts +40 -16
  155. package/src/router/types.ts +8 -0
  156. package/src/router/url-params.ts +49 -0
  157. package/src/router.ts +37 -25
  158. package/src/rsc/handler-context.ts +2 -2
  159. package/src/rsc/handler.ts +58 -77
  160. package/src/rsc/helpers.ts +72 -43
  161. package/src/rsc/index.ts +1 -1
  162. package/src/rsc/manifest-init.ts +28 -41
  163. package/src/rsc/origin-guard.ts +30 -10
  164. package/src/rsc/progressive-enhancement.ts +4 -0
  165. package/src/rsc/response-error.ts +79 -12
  166. package/src/rsc/response-route-handler.ts +76 -61
  167. package/src/rsc/rsc-rendering.ts +45 -51
  168. package/src/rsc/runtime-warnings.ts +9 -10
  169. package/src/rsc/server-action.ts +33 -39
  170. package/src/rsc/ssr-setup.ts +16 -0
  171. package/src/rsc/types.ts +8 -2
  172. package/src/search-params.ts +4 -4
  173. package/src/segment-content-promise.ts +67 -0
  174. package/src/segment-loader-promise.ts +122 -0
  175. package/src/segment-system.tsx +132 -116
  176. package/src/serialize.ts +243 -0
  177. package/src/server/context.ts +175 -53
  178. package/src/server/cookie-store.ts +28 -4
  179. package/src/server/request-context.ts +57 -51
  180. package/src/ssr/index.tsx +5 -1
  181. package/src/static-handler.ts +1 -1
  182. package/src/types/global-namespace.ts +39 -26
  183. package/src/types/handler-context.ts +68 -50
  184. package/src/types/index.ts +1 -0
  185. package/src/types/loader-types.ts +11 -9
  186. package/src/types/request-scope.ts +126 -0
  187. package/src/types/route-entry.ts +11 -0
  188. package/src/types/segments.ts +35 -2
  189. package/src/urls/include-helper.ts +34 -67
  190. package/src/urls/index.ts +1 -5
  191. package/src/urls/path-helper-types.ts +17 -3
  192. package/src/urls/path-helper.ts +17 -52
  193. package/src/urls/pattern-types.ts +36 -19
  194. package/src/urls/response-types.ts +22 -29
  195. package/src/urls/type-extraction.ts +58 -139
  196. package/src/urls/urls-function.ts +1 -5
  197. package/src/use-loader.tsx +413 -42
  198. package/src/vite/debug.ts +185 -0
  199. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  200. package/src/vite/discovery/discover-routers.ts +106 -75
  201. package/src/vite/discovery/discovery-errors.ts +194 -0
  202. package/src/vite/discovery/gate-state.ts +171 -0
  203. package/src/vite/discovery/prerender-collection.ts +72 -31
  204. package/src/vite/discovery/route-types-writer.ts +40 -84
  205. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  206. package/src/vite/discovery/state.ts +33 -0
  207. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  208. package/src/vite/index.ts +2 -0
  209. package/src/vite/plugin-types.ts +67 -0
  210. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  211. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  212. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  213. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  214. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  215. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  216. package/src/vite/plugins/expose-action-id.ts +54 -30
  217. package/src/vite/plugins/expose-id-utils.ts +12 -8
  218. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  219. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  220. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  221. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  222. package/src/vite/plugins/expose-internal-ids.ts +496 -486
  223. package/src/vite/plugins/performance-tracks.ts +29 -25
  224. package/src/vite/plugins/use-cache-transform.ts +65 -50
  225. package/src/vite/plugins/version-injector.ts +39 -23
  226. package/src/vite/plugins/version-plugin.ts +59 -2
  227. package/src/vite/plugins/virtual-entries.ts +2 -2
  228. package/src/vite/rango.ts +116 -29
  229. package/src/vite/router-discovery.ts +753 -104
  230. package/src/vite/utils/ast-handler-extract.ts +15 -15
  231. package/src/vite/utils/banner.ts +1 -1
  232. package/src/vite/utils/bundle-analysis.ts +4 -2
  233. package/src/vite/utils/client-chunks.ts +190 -0
  234. package/src/vite/utils/forward-user-plugins.ts +193 -0
  235. package/src/vite/utils/manifest-utils.ts +8 -59
  236. package/src/vite/utils/package-resolution.ts +41 -1
  237. package/src/vite/utils/prerender-utils.ts +5 -4
  238. package/src/vite/utils/shared-utils.ts +107 -26
  239. package/src/browser/action-response-classifier.ts +0 -99
@@ -13,6 +13,8 @@
13
13
  export {
14
14
  CFCacheStore,
15
15
  type CFCacheStoreOptions,
16
+ type CFCacheDebug,
17
+ type CFCacheReadDebugEvent,
16
18
  type KVNamespace,
17
19
  } from "./cf-cache-store.js";
18
20
 
@@ -20,6 +22,7 @@ export {
20
22
  export {
21
23
  CACHE_STALE_AT_HEADER,
22
24
  CACHE_STATUS_HEADER,
25
+ CACHE_REVALIDATING_AT_HEADER,
23
26
  } from "./cf-cache-store.js";
24
27
 
25
28
  // Internal exports (re-exported for backwards compatibility, marked @internal in source)
@@ -9,6 +9,109 @@
9
9
  import type { ResolvedSegment } from "../types.js";
10
10
  import type { HandleStore } from "../server/handle-store.js";
11
11
  import type { SegmentHandleData } from "./types.js";
12
+ import { serializeResult, deserializeResult } from "./segment-codec.js";
13
+
14
+ /**
15
+ * Bound on the background cache-write encode of handle data. A pushed handle
16
+ * value can be a Promise (request-context push-a-promise) or a Promise<ReactNode>
17
+ * (Breadcrumbs content), which the Flight encoder awaits while draining. The
18
+ * encode runs in waitUntil/runBackground, so a never-resolving handle value
19
+ * would otherwise pin a background slot indefinitely; on timeout the entry's
20
+ * handles coalesce to empty rather than hanging or poisoning the whole write.
21
+ */
22
+ const HANDLE_ENCODE_TIMEOUT_MS = 5000;
23
+
24
+ type HandleRecord = Record<string, SegmentHandleData>;
25
+
26
+ // captureHandles builds a per-segment map keyed by every cached segment id, even
27
+ // segments that pushed nothing (their entry is an empty object). "No handle data"
28
+ // means no segment has any handle, in which case we skip the Flight encode and
29
+ // store an empty string — so the common handle-free route pays neither an encode
30
+ // on write nor a decode on every cache hit.
31
+ function hasHandleData(handles: HandleRecord): boolean {
32
+ for (const segId in handles) {
33
+ for (const _ in handles[segId]) return true;
34
+ }
35
+ return false;
36
+ }
37
+
38
+ function withTimeout<T>(p: Promise<T>, ms: number, onTimeout: T): Promise<T> {
39
+ let timer: ReturnType<typeof setTimeout>;
40
+ const timeout = new Promise<T>((resolve) => {
41
+ timer = setTimeout(() => resolve(onTimeout), ms);
42
+ });
43
+ return Promise.race([
44
+ p.then(
45
+ (v) => {
46
+ clearTimeout(timer);
47
+ return v;
48
+ },
49
+ (e) => {
50
+ clearTimeout(timer);
51
+ throw e;
52
+ },
53
+ ),
54
+ timeout,
55
+ ]);
56
+ }
57
+
58
+ /**
59
+ * Encode a captured handle map to a string for cache storage.
60
+ *
61
+ * Handle values can be Promises or React elements (e.g. Breadcrumbs `content`).
62
+ * JSON.stringify destroys those (Promise -> {}, ReactNode non-representable), so
63
+ * persisting the raw map silently corrupts non-scalar handle values on stores
64
+ * that serialize to JSON (the Cloudflare cache). Routing the map through the same
65
+ * RSC-Flight codec the segments/value already use awaits Promises and serializes
66
+ * React elements, so the stored field is a lossless, JSON-safe string. The
67
+ * in-memory store keeps the same string by reference, so both backends replay
68
+ * identical decoded values.
69
+ */
70
+ export async function encodeHandles(handles: HandleRecord): Promise<string> {
71
+ // No handle was pushed anywhere — store an empty marker (decoded as "skip").
72
+ if (!hasHandleData(handles)) return "";
73
+ return encodeHandleValue(handles);
74
+ }
75
+
76
+ /**
77
+ * Decode a stored handle string back to a handle map. Returns null on any
78
+ * decode failure (e.g. a cross-version entry read under a pinned static
79
+ * version), so the caller can skip handle restore without discarding the
80
+ * otherwise-valid cached segments alongside it.
81
+ */
82
+ export function decodeHandles(encoded: string): Promise<HandleRecord | null> {
83
+ return decodeHandleValue<HandleRecord>(encoded);
84
+ }
85
+
86
+ /**
87
+ * Encode an arbitrary handle-data value to a Flight string. Used directly by the
88
+ * prerender/static pipeline, whose static path holds a single segment's
89
+ * `SegmentHandleData` (not a segId-keyed map). Bounded by the same timeout as
90
+ * encodeHandles; failure/timeout coalesces to "". The caller owns the empty
91
+ * check (an empty value still encodes to a non-empty Flight string, so skip the
92
+ * call when there is nothing to store).
93
+ */
94
+ export async function encodeHandleValue(value: unknown): Promise<string> {
95
+ const encoded = await withTimeout(
96
+ serializeResult(value),
97
+ HANDLE_ENCODE_TIMEOUT_MS,
98
+ null,
99
+ );
100
+ return encoded ?? "";
101
+ }
102
+
103
+ /**
104
+ * Decode a Flight-encoded handle-data string. Returns null on any decode
105
+ * failure so the caller can skip handle restore without discarding valid
106
+ * cached/prerendered segments.
107
+ */
108
+ export async function decodeHandleValue<T>(encoded: string): Promise<T | null> {
109
+ try {
110
+ return await deserializeResult<T>(encoded);
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
12
115
 
13
116
  /**
14
117
  * Capture handle data for a set of segments from the handle store.
@@ -29,9 +29,12 @@ export { MemorySegmentCacheStore } from "./memory-segment-store.js";
29
29
  export {
30
30
  CFCacheStore,
31
31
  type CFCacheStoreOptions,
32
+ type CFCacheDebug,
33
+ type CFCacheReadDebugEvent,
32
34
  type KVNamespace,
33
35
  CACHE_STALE_AT_HEADER,
34
36
  CACHE_STATUS_HEADER,
37
+ CACHE_REVALIDATING_AT_HEADER,
35
38
  } from "./cf/index.js";
36
39
 
37
40
  // Cache scope
@@ -12,7 +12,6 @@ import type {
12
12
  CacheGetResult,
13
13
  CacheItemResult,
14
14
  CacheItemOptions,
15
- SegmentHandleData,
16
15
  } from "./types.js";
17
16
  import type { RequestContext } from "../server/request-context.js";
18
17
  import {
@@ -56,7 +55,9 @@ interface CachedResponseEntry {
56
55
 
57
56
  interface CachedItemEntry {
58
57
  value: string;
59
- handles?: Record<string, SegmentHandleData>;
58
+ /** RSC-encoded handle data (see handle-snapshot.ts encodeHandles). Stored as
59
+ * the encoded string by reference, identical to the JSON-serializing stores. */
60
+ handles?: string;
60
61
  expiresAt: number;
61
62
  staleAt: number;
62
63
  }
@@ -175,8 +175,10 @@ export interface SegmentCacheStore<TEnv = unknown> {
175
175
  export interface CacheItemResult {
176
176
  /** RSC-serialized return value */
177
177
  value: string;
178
- /** Handle data captured during execution (breadcrumbs, metadata, etc.) */
179
- handles?: Record<string, SegmentHandleData>;
178
+ /** RSC-encoded handle data captured during execution (breadcrumbs, metadata,
179
+ * etc.). Encoded via the Flight codec so Promise/ReactNode handle values
180
+ * survive JSON-serializing stores — see handle-snapshot.ts encodeHandles. */
181
+ handles?: string;
180
182
  /** Whether the entry is stale and should be revalidated */
181
183
  shouldRevalidate: boolean;
182
184
  }
@@ -185,8 +187,8 @@ export interface CacheItemResult {
185
187
  * Options for setItem() for function-level caching ("use cache").
186
188
  */
187
189
  export interface CacheItemOptions {
188
- /** Handle data to store alongside the value */
189
- handles?: Record<string, SegmentHandleData>;
190
+ /** RSC-encoded handle data to store alongside the value (see encodeHandles). */
191
+ handles?: string;
190
192
  /** Time-to-live in seconds */
191
193
  ttl?: number;
192
194
  /** Stale-while-revalidate window in seconds */
@@ -227,8 +229,10 @@ export interface SerializedSegmentData {
227
229
  export interface CachedEntryData {
228
230
  /** Serialized segments for this entry */
229
231
  segments: SerializedSegmentData[];
230
- /** Handle data keyed by segment ID */
231
- handles: Record<string, SegmentHandleData>;
232
+ /** RSC-encoded handle data keyed by segment ID. Encoded via the Flight codec
233
+ * (see handle-snapshot.ts encodeHandles) so Promise/ReactNode handle values
234
+ * round-trip through JSON-serializing stores instead of being flattened. */
235
+ handles: string;
232
236
  /** Expiration timestamp (ms since epoch) */
233
237
  expiresAt: number;
234
238
  }
@@ -78,6 +78,9 @@ export {
78
78
  // Re-export useHref - it's a "use client" hook
79
79
  export { useHref } from "./browser/react/use-href.js";
80
80
 
81
+ // Re-export useReverse - it's a "use client" hook
82
+ export { useReverse } from "./browser/react/use-reverse.js";
83
+
81
84
  // Re-export useHandle - it's a "use client" hook
82
85
  export { useHandle } from "./browser/react/use-handle.js";
83
86
 
package/src/client.tsx CHANGED
@@ -21,6 +21,83 @@ import {
21
21
  } from "./route-content-wrapper.js";
22
22
  import { OutletProvider } from "./outlet-provider.js";
23
23
  import { MountContextProvider } from "./browser/react/mount-context.js";
24
+ import { getMemoizedContentPromise } from "./segment-content-promise.js";
25
+
26
+ /**
27
+ * Render the content for a named parallel/intercept slot segment.
28
+ *
29
+ * Shared by Outlet (with `name` prop) and ParallelOutlet — both resolve a
30
+ * segment from context.parallel by slot name and then render it through the
31
+ * same layout/loader/mountPath wrapping pipeline.
32
+ */
33
+ function renderSlotContent(segment: ResolvedSegment | null): ReactNode {
34
+ if (!segment) return null;
35
+
36
+ const content: ReactNode =
37
+ segment.loading || segment.component instanceof Promise ? (
38
+ <RouteContentWrapper
39
+ content={getMemoizedContentPromise(segment.component)}
40
+ fallback={segment.loading}
41
+ segmentId={segment.id}
42
+ />
43
+ ) : (
44
+ (segment.component ?? null)
45
+ );
46
+
47
+ const hasOwnLoaders = !!(segment.loaderDataPromise && segment.loaderIds);
48
+ const loaderWrapped = hasOwnLoaders ? (
49
+ <LoaderBoundary
50
+ loaderDataPromise={segment.loaderDataPromise!}
51
+ loaderIds={segment.loaderIds!}
52
+ fallback={segment.loading}
53
+ outletKey={segment.id + "-loader"}
54
+ outletContent={null}
55
+ segment={segment}
56
+ >
57
+ {content}
58
+ </LoaderBoundary>
59
+ ) : null;
60
+
61
+ let result: ReactNode;
62
+ if (segment.layout) {
63
+ // Layout renders immediately; if loaders exist, the LoaderBoundary becomes
64
+ // the outlet content so layout's <Outlet /> suspends until loaders resolve.
65
+ result = (
66
+ <OutletProvider
67
+ content={hasOwnLoaders ? loaderWrapped : content}
68
+ segment={segment}
69
+ >
70
+ {segment.layout}
71
+ </OutletProvider>
72
+ );
73
+ } else if (hasOwnLoaders) {
74
+ // No layout but has loaders — wrap content with LoaderBoundary for useLoader context.
75
+ // Common for intercept routes that use useLoader without a custom layout.
76
+ result = loaderWrapped;
77
+ } else {
78
+ result = content;
79
+ }
80
+
81
+ if (segment.mountPath) {
82
+ return (
83
+ <MountContextProvider value={segment.mountPath}>
84
+ {result}
85
+ </MountContextProvider>
86
+ );
87
+ }
88
+
89
+ return result;
90
+ }
91
+
92
+ function useSlotSegment(
93
+ context: OutletContextValue | null,
94
+ name: `@${string}` | undefined,
95
+ ): ResolvedSegment | null {
96
+ return useMemo(() => {
97
+ if (!name || !context?.parallel) return null;
98
+ return context.parallel.find((seg) => seg.slot === name) ?? null;
99
+ }, [context, name]);
100
+ }
24
101
 
25
102
  /**
26
103
  * Outlet component - renders child content in layouts
@@ -61,95 +138,10 @@ import { MountContextProvider } from "./browser/react/mount-context.js";
61
138
  */
62
139
  export function Outlet({ name }: { name?: `@${string}` } = {}): ReactNode {
63
140
  const context = useContext(OutletContext);
141
+ const namedSegment = useSlotSegment(context, name);
64
142
 
65
- // If name provided, render parallel/intercept content for that slot
66
143
  if (name) {
67
- const segment = context?.parallel?.find((seg) => seg.slot === name) ?? null;
68
-
69
- if (!segment) return null;
70
-
71
- // Determine the content to render
72
- let content: ReactNode;
73
- if (segment.loading || segment.component instanceof Promise) {
74
- // Use RouteContentWrapper to handle Suspense wrapping properly
75
- content = (
76
- <RouteContentWrapper
77
- content={
78
- segment.component instanceof Promise
79
- ? segment.component
80
- : Promise.resolve(segment.component)
81
- }
82
- fallback={segment.loading}
83
- segmentId={segment.id}
84
- />
85
- );
86
- } else {
87
- content = segment.component ?? null;
88
- }
89
-
90
- let result: ReactNode;
91
-
92
- // If segment has a layout, wrap appropriately
93
- if (segment.layout) {
94
- // Check if this segment has loaders that need streaming
95
- // The layout renders immediately, LoaderBoundary becomes the outlet content
96
- // When layout renders <Outlet />, it gets the LoaderBoundary which suspends
97
- if (segment.loaderDataPromise && segment.loaderIds) {
98
- const loaderAwareContent = (
99
- <LoaderBoundary
100
- loaderDataPromise={segment.loaderDataPromise}
101
- loaderIds={segment.loaderIds}
102
- fallback={segment.loading}
103
- outletKey={segment.id + "-loader"}
104
- outletContent={null}
105
- segment={segment}
106
- >
107
- {content}
108
- </LoaderBoundary>
109
- );
110
-
111
- result = (
112
- <OutletProvider content={loaderAwareContent} segment={segment}>
113
- {segment.layout}
114
- </OutletProvider>
115
- );
116
- } else {
117
- // No loaders - wrap in OutletProvider so layout can use <Outlet />
118
- result = (
119
- <OutletProvider content={content} segment={segment}>
120
- {segment.layout}
121
- </OutletProvider>
122
- );
123
- }
124
- } else if (segment.loaderDataPromise && segment.loaderIds) {
125
- // No layout but has loaders - wrap content with LoaderBoundary for useLoader context
126
- // This is common for intercept routes that use useLoader without a custom layout
127
- result = (
128
- <LoaderBoundary
129
- loaderDataPromise={segment.loaderDataPromise}
130
- loaderIds={segment.loaderIds}
131
- fallback={segment.loading}
132
- outletKey={segment.id + "-loader"}
133
- outletContent={null}
134
- segment={segment}
135
- >
136
- {content}
137
- </LoaderBoundary>
138
- );
139
- } else {
140
- result = content;
141
- }
142
-
143
- // Wrap with MountContextProvider for include() scoped parallel/intercept slots
144
- if (segment.mountPath) {
145
- return (
146
- <MountContextProvider value={segment.mountPath}>
147
- {result}
148
- </MountContextProvider>
149
- );
150
- }
151
-
152
- return result;
144
+ return renderSlotContent(namedSegment);
153
145
  }
154
146
 
155
147
  // Default: render child content
@@ -163,6 +155,7 @@ export function Outlet({ name }: { name?: `@${string}` } = {}): ReactNode {
163
155
 
164
156
  return content;
165
157
  }
158
+
166
159
  /**
167
160
  * ParallelOutlet component - renders content for a named parallel slot
168
161
  *
@@ -187,94 +180,9 @@ export function Outlet({ name }: { name?: `@${string}` } = {}): ReactNode {
187
180
  */
188
181
  export function ParallelOutlet({ name }: { name: `@${string}` }): ReactNode {
189
182
  const context = useContext(OutletContext);
190
- const segment = useMemo(() => {
191
- if (!context?.parallel) return null;
192
- return context.parallel.find((seg) => seg.slot === name) ?? null;
193
- }, [context, name]);
194
-
195
- if (!segment) return null;
196
-
197
- // Determine the content to render
198
- let content: ReactNode;
199
- if (segment.loading || segment.component instanceof Promise) {
200
- // Use RouteContentWrapper to handle Suspense wrapping properly
201
- content = (
202
- <RouteContentWrapper
203
- content={
204
- segment.component instanceof Promise
205
- ? segment.component
206
- : Promise.resolve(segment.component)
207
- }
208
- fallback={segment.loading}
209
- segmentId={segment.id}
210
- />
211
- );
212
- } else {
213
- content = segment.component ?? null;
214
- }
215
-
216
- let result: ReactNode;
217
-
218
- // If segment has a layout, wrap appropriately
219
- if (segment.layout) {
220
- // Check if this segment has loaders that need streaming
221
- // The layout renders immediately, LoaderBoundary becomes the outlet content
222
- if (segment.loaderDataPromise && segment.loaderIds) {
223
- const loaderAwareContent = (
224
- <LoaderBoundary
225
- loaderDataPromise={segment.loaderDataPromise}
226
- loaderIds={segment.loaderIds}
227
- fallback={segment.loading}
228
- outletKey={segment.id + "-loader"}
229
- outletContent={null}
230
- segment={segment}
231
- >
232
- {content}
233
- </LoaderBoundary>
234
- );
235
-
236
- result = (
237
- <OutletProvider content={loaderAwareContent} segment={segment}>
238
- {segment.layout}
239
- </OutletProvider>
240
- );
241
- } else {
242
- // No loaders - wrap in OutletProvider so layout can use <Outlet />
243
- result = (
244
- <OutletProvider content={content} segment={segment}>
245
- {segment.layout}
246
- </OutletProvider>
247
- );
248
- }
249
- } else if (segment.loaderDataPromise && segment.loaderIds) {
250
- // No layout but has loaders - wrap content with LoaderBoundary for useLoader context
251
- // This is common for intercept routes that use useLoader without a custom layout
252
- result = (
253
- <LoaderBoundary
254
- loaderDataPromise={segment.loaderDataPromise}
255
- loaderIds={segment.loaderIds}
256
- fallback={segment.loading}
257
- outletKey={segment.id + "-loader"}
258
- outletContent={null}
259
- segment={segment}
260
- >
261
- {content}
262
- </LoaderBoundary>
263
- );
264
- } else {
265
- result = content;
266
- }
267
-
268
- // Wrap with MountContextProvider for include() scoped parallel/intercept slots
269
- if (segment.mountPath) {
270
- return (
271
- <MountContextProvider value={segment.mountPath}>
272
- {result}
273
- </MountContextProvider>
274
- );
275
- }
183
+ const segment = useSlotSegment(context, name);
276
184
 
277
- return result;
185
+ return renderSlotContent(segment);
278
186
  }
279
187
 
280
188
  // OutletProvider is defined in outlet-provider.tsx to break a circular
@@ -306,6 +214,7 @@ export function useOutlet(): ReactNode {
306
214
  export {
307
215
  useLoader,
308
216
  useFetchLoader,
217
+ useRefreshLoaders,
309
218
  type LoadFunction,
310
219
  type UseLoaderResult,
311
220
  type UseFetchLoaderResult,
@@ -501,37 +410,15 @@ export {
501
410
  type LocationStateOptions,
502
411
  } from "./browser/react/location-state.js";
503
412
 
504
- // Type-safe href for client-side path validation
505
- export {
506
- href,
507
- type ValidPaths,
508
- type PatternToPath,
509
- type PathResponse,
510
- } from "./href-client.js";
413
+ // Type-safe href for client-side path validation. The path and response types
414
+ // are ambient as `Rango.Path` / `Rango.PathResponse` (declared in
415
+ // href-client.ts) — no import needed.
416
+ export { href, type PatternToPath } from "./href-client.js";
511
417
 
512
- // Response envelope types for consuming JSON response routes
513
- export type { ResponseEnvelope, ResponseError } from "./urls.js";
514
-
515
- /**
516
- * Type guard for checking if a response envelope contains an error.
517
- *
518
- * @example
519
- * ```typescript
520
- * const result: ResponseEnvelope<Product> = await fetch(url).then(r => r.json());
521
- * if (isResponseError(result)) {
522
- * console.log(result.error.message, result.error.code);
523
- * return;
524
- * }
525
- * result.data // fully typed as Product
526
- * ```
527
- */
528
- export function isResponseError<T>(
529
- result: import("./urls.js").ResponseEnvelope<T>,
530
- ): result is import("./urls.js").ResponseEnvelope<T> & {
531
- error: import("./urls.js").ResponseError;
532
- } {
533
- return result.error !== undefined;
534
- }
418
+ // Problem Details (RFC 9457) error body type for consuming JSON response routes.
419
+ // On a non-2xx response, `await res.json()` yields this shape; on success the
420
+ // body is the bare value (no envelope). Discriminate on `res.ok` / status.
421
+ export type { ProblemDetails } from "./urls.js";
535
422
 
536
423
  // Mount context for include() scoped components
537
424
  export { useMount } from "./browser/react/use-mount.js";
@@ -540,8 +427,12 @@ export { MountContext } from "./browser/react/mount-context.js";
540
427
  // Mount-aware href hook - auto-prefixes paths with include() mount
541
428
  export { useHref } from "./browser/react/use-href.js";
542
429
 
430
+ // Mount-aware reverse hook - resolves dot-prefixed names against an imported
431
+ // generated routes map (from a urls() module's .gen.ts).
432
+ export { useReverse } from "./browser/react/use-reverse.js";
433
+
543
434
  // Type-safe scoped reverse function for scopedReverse<typeof patterns>()
544
- export type { ScopedReverseFunction } from "./reverse.js";
435
+ export type { ScopedReverseFunction, LocalReverseFunction } from "./reverse.js";
545
436
 
546
437
  // Loader definition type - for typing loader props in client components
547
438
  export type { LoaderDefinition } from "./types.js";
@@ -12,7 +12,7 @@
12
12
  * interface PaginationData { current: number; total: number }
13
13
  * export const Pagination = createVar<PaginationData>();
14
14
  *
15
- * // Non-cacheable var — throws if set/get inside cache() or "use cache"
15
+ * // Non-cacheable var — ctx.get(User) throws inside a cache() boundary
16
16
  * export const User = createVar<UserData>({ cache: false });
17
17
  *
18
18
  * // handler
@@ -26,7 +26,7 @@
26
26
  export interface ContextVar<T> {
27
27
  readonly __brand: "context-var";
28
28
  readonly key: symbol;
29
- /** When false, the var is non-cacheable — throws inside cache() / "use cache" */
29
+ /** When false, ctx.get(var) throws inside a cache() boundary. */
30
30
  readonly cache: boolean;
31
31
  /** Phantom field to carry the type parameter. Never set at runtime. */
32
32
  readonly __type?: T;
@@ -35,9 +35,9 @@ export interface ContextVar<T> {
35
35
  export interface ContextVarOptions {
36
36
  /**
37
37
  * When false, marks this variable as non-cacheable.
38
- * Setting or getting this var inside a cache() boundary or "use cache"
39
- * function will throw. Use for inherently request-specific data (user
40
- * sessions, auth tokens, etc.) that must never be baked into cached segments.
38
+ * Reading this var with ctx.get() inside a cache() boundary throws. Use for
39
+ * inherently request-specific data (user sessions, auth tokens, etc.) that
40
+ * must never be baked into cached segments.
41
41
  *
42
42
  * @default true
43
43
  */
@@ -0,0 +1,36 @@
1
+ import type { ReactNode } from "react";
2
+ import { isLoaderDataResult } from "./types.js";
3
+
4
+ // Shared by segment-system (server) and LoaderResolver (client) so the
5
+ // legacy/ok/error-fallback/throw decode of resolved loader values lives once.
6
+ // Last failing loader wins errorFallback; an error without a fallback throws.
7
+ export function decodeLoaderResults(
8
+ resolvedData: any[],
9
+ loaderIds: string[],
10
+ ): { loaderData: Record<string, any>; errorFallback: ReactNode } {
11
+ const loaderData: Record<string, any> = {};
12
+ let errorFallback: ReactNode = null;
13
+
14
+ for (let i = 0; i < loaderIds.length; i++) {
15
+ const id = loaderIds[i];
16
+ const result = resolvedData[i];
17
+
18
+ if (!isLoaderDataResult(result)) {
19
+ loaderData[id] = result;
20
+ continue;
21
+ }
22
+
23
+ if (result.ok) {
24
+ loaderData[id] = result.data;
25
+ continue;
26
+ }
27
+
28
+ if (result.fallback) {
29
+ errorFallback = result.fallback;
30
+ } else {
31
+ throw new Error(result.error.message);
32
+ }
33
+ }
34
+
35
+ return { loaderData, errorFallback };
36
+ }