@rangojs/router 0.0.0-experimental.122 → 0.0.0-experimental.125

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 (260) hide show
  1. package/dist/bin/rango.js +10 -6
  2. package/dist/testing/vitest.js +82 -0
  3. package/dist/vite/index.js +55 -48
  4. package/package.json +61 -21
  5. package/skills/caching/SKILL.md +2 -1
  6. package/skills/hooks/SKILL.md +40 -29
  7. package/skills/host-router/SKILL.md +16 -2
  8. package/skills/intercept/SKILL.md +4 -2
  9. package/skills/layout/SKILL.md +11 -6
  10. package/skills/loader/SKILL.md +6 -2
  11. package/skills/middleware/SKILL.md +4 -2
  12. package/skills/migrate-nextjs/SKILL.md +3 -1
  13. package/skills/parallel/SKILL.md +9 -4
  14. package/skills/rango/SKILL.md +12 -0
  15. package/skills/route/SKILL.md +10 -2
  16. package/skills/testing/SKILL.md +129 -0
  17. package/skills/testing/bindings.md +89 -0
  18. package/skills/testing/cache-prerender.md +98 -0
  19. package/skills/testing/client-components.md +122 -0
  20. package/skills/testing/e2e-parity.md +125 -0
  21. package/skills/testing/flight.md +89 -0
  22. package/skills/testing/handles.md +129 -0
  23. package/skills/testing/loader.md +128 -0
  24. package/skills/testing/middleware.md +99 -0
  25. package/skills/testing/render-handler.md +118 -0
  26. package/skills/testing/response-routes.md +95 -0
  27. package/skills/testing/reverse-and-types.md +84 -0
  28. package/skills/testing/server-actions.md +107 -0
  29. package/skills/testing/server-tree.md +128 -0
  30. package/skills/testing/setup.md +120 -0
  31. package/src/__internal.ts +0 -65
  32. package/src/browser/action-coordinator.ts +1 -1
  33. package/src/browser/action-fence.ts +47 -0
  34. package/src/browser/cookie-name.ts +140 -0
  35. package/src/browser/event-controller.ts +1 -83
  36. package/src/browser/invalidate-client-cache.ts +52 -0
  37. package/src/browser/navigation-bridge.ts +14 -1
  38. package/src/browser/navigation-client.ts +14 -1
  39. package/src/browser/navigation-store-handle.ts +38 -0
  40. package/src/browser/navigation-store.ts +26 -51
  41. package/src/browser/navigation-transaction.ts +0 -32
  42. package/src/browser/partial-update.ts +1 -83
  43. package/src/browser/prefetch/cache.ts +6 -45
  44. package/src/browser/prefetch/fetch.ts +7 -0
  45. package/src/browser/prefetch/queue.ts +6 -3
  46. package/src/browser/rango-state.ts +157 -99
  47. package/src/browser/react/Link.tsx +0 -2
  48. package/src/browser/react/NavigationProvider.tsx +2 -1
  49. package/src/browser/react/ScrollRestoration.tsx +10 -6
  50. package/src/browser/react/filter-segment-order.ts +0 -2
  51. package/src/browser/react/index.ts +0 -51
  52. package/src/browser/react/location-state-shared.ts +0 -13
  53. package/src/browser/react/location-state.ts +0 -1
  54. package/src/browser/react/use-action.ts +6 -15
  55. package/src/browser/react/use-handle.ts +0 -5
  56. package/src/browser/react/use-link-status.ts +0 -4
  57. package/src/browser/react/use-navigation.ts +0 -3
  58. package/src/browser/react/use-params.ts +0 -2
  59. package/src/browser/react/use-search-params.ts +0 -5
  60. package/src/browser/react/use-segments.ts +0 -13
  61. package/src/browser/rsc-router.tsx +12 -4
  62. package/src/browser/server-action-bridge.ts +77 -15
  63. package/src/browser/types.ts +7 -2
  64. package/src/browser/validate-redirect-origin.ts +4 -5
  65. package/src/build/route-trie.ts +3 -0
  66. package/src/build/route-types/param-extraction.ts +6 -3
  67. package/src/build/route-types/router-processing.ts +0 -8
  68. package/src/cache/cache-policy.ts +0 -54
  69. package/src/cache/cache-runtime.ts +27 -24
  70. package/src/cache/cache-scope.ts +0 -27
  71. package/src/cache/cache-tag.ts +0 -37
  72. package/src/cache/cf/cf-cache-store.ts +94 -46
  73. package/src/cache/cf/index.ts +0 -24
  74. package/src/cache/document-cache.ts +11 -36
  75. package/src/cache/handle-snapshot.ts +0 -40
  76. package/src/cache/index.ts +0 -27
  77. package/src/cache/memory-segment-store.ts +2 -48
  78. package/src/cache/profile-registry.ts +7 -3
  79. package/src/cache/read-through-swr.ts +41 -11
  80. package/src/cache/segment-codec.ts +0 -16
  81. package/src/cache/types.ts +0 -98
  82. package/src/client.rsc.tsx +1 -22
  83. package/src/client.tsx +14 -38
  84. package/src/component-utils.ts +19 -0
  85. package/src/deps/ssr.ts +0 -1
  86. package/src/handle.ts +28 -18
  87. package/src/handles/MetaTags.tsx +0 -14
  88. package/src/handles/meta.ts +0 -39
  89. package/src/host/cookie-handler.ts +0 -36
  90. package/src/host/errors.ts +0 -24
  91. package/src/host/index.ts +6 -0
  92. package/src/host/pattern-matcher.ts +7 -50
  93. package/src/host/router.ts +1 -65
  94. package/src/host/testing.ts +40 -27
  95. package/src/host/types.ts +6 -2
  96. package/src/href-client.ts +0 -4
  97. package/src/index.rsc.ts +42 -3
  98. package/src/index.ts +31 -1
  99. package/src/internal-debug.ts +2 -4
  100. package/src/loader.rsc.ts +19 -9
  101. package/src/loader.ts +12 -4
  102. package/src/network-error-thrower.tsx +1 -6
  103. package/src/outlet-provider.tsx +1 -5
  104. package/src/prerender/param-hash.ts +10 -11
  105. package/src/prerender/store.ts +23 -30
  106. package/src/prerender.ts +58 -3
  107. package/src/root-error-boundary.tsx +1 -19
  108. package/src/route-content-wrapper.tsx +1 -44
  109. package/src/route-definition/dsl-helpers.ts +7 -19
  110. package/src/route-definition/helpers-types.ts +3 -3
  111. package/src/route-definition/redirect.ts +11 -1
  112. package/src/route-map-builder.ts +0 -16
  113. package/src/router/basename.ts +14 -0
  114. package/src/router/content-negotiation.ts +0 -13
  115. package/src/router/error-handling.ts +12 -16
  116. package/src/router/find-match.ts +4 -30
  117. package/src/router/intercept-resolution.ts +10 -1
  118. package/src/router/lazy-includes.ts +1 -57
  119. package/src/router/loader-resolution.ts +3 -2
  120. package/src/router/logging.ts +0 -6
  121. package/src/router/manifest.ts +1 -25
  122. package/src/router/match-api.ts +0 -20
  123. package/src/router/match-context.ts +0 -22
  124. package/src/router/match-handlers.ts +57 -58
  125. package/src/router/match-middleware/background-revalidation.ts +0 -7
  126. package/src/router/match-middleware/cache-lookup.ts +1 -54
  127. package/src/router/match-middleware/cache-store.ts +0 -31
  128. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  129. package/src/router/match-middleware/segment-resolution.ts +0 -21
  130. package/src/router/match-pipelines.ts +1 -42
  131. package/src/router/match-result.ts +1 -52
  132. package/src/router/metrics.ts +0 -34
  133. package/src/router/middleware-cookies.ts +0 -13
  134. package/src/router/middleware-types.ts +0 -115
  135. package/src/router/middleware.ts +7 -30
  136. package/src/router/navigation-snapshot.ts +0 -51
  137. package/src/router/params-util.ts +23 -0
  138. package/src/router/pattern-matching.ts +1 -33
  139. package/src/router/prerender-match.ts +33 -45
  140. package/src/router/request-classification.ts +1 -38
  141. package/src/router/revalidation.ts +5 -58
  142. package/src/router/router-context.ts +0 -26
  143. package/src/router/router-interfaces.ts +7 -0
  144. package/src/router/router-options.ts +30 -0
  145. package/src/router/segment-resolution/fresh.ts +25 -57
  146. package/src/router/segment-resolution/helpers.ts +34 -0
  147. package/src/router/segment-resolution/loader-cache.ts +10 -13
  148. package/src/router/segment-resolution/revalidation.ts +5 -42
  149. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  150. package/src/router/segment-resolution.ts +4 -1
  151. package/src/router/state-cookie-name.ts +33 -0
  152. package/src/router/telemetry-otel.ts +0 -20
  153. package/src/router/telemetry.ts +96 -19
  154. package/src/router/timeout.ts +0 -20
  155. package/src/router/trie-matching.ts +63 -40
  156. package/src/router/types.ts +1 -63
  157. package/src/router/url-params.ts +0 -5
  158. package/src/router.ts +40 -9
  159. package/src/rsc/handler.ts +14 -2
  160. package/src/rsc/helpers.ts +34 -0
  161. package/src/rsc/origin-guard.ts +0 -12
  162. package/src/rsc/progressive-enhancement.ts +4 -1
  163. package/src/rsc/rsc-rendering.ts +4 -7
  164. package/src/rsc/runtime-warnings.ts +14 -0
  165. package/src/rsc/server-action.ts +30 -28
  166. package/src/rsc/types.ts +2 -1
  167. package/src/runtime-env.ts +18 -0
  168. package/src/search-params.ts +0 -16
  169. package/src/segment-loader-promise.ts +14 -2
  170. package/src/segment-system.tsx +79 -88
  171. package/src/server/cookie-store.ts +52 -1
  172. package/src/server/handle-store.ts +7 -24
  173. package/src/server/loader-registry.ts +5 -24
  174. package/src/server/request-context.ts +74 -77
  175. package/src/ssr/index.tsx +14 -14
  176. package/src/static-handler.ts +10 -13
  177. package/src/testing/cache-status.ts +119 -0
  178. package/src/testing/collect-handle.ts +40 -0
  179. package/src/testing/dispatch.ts +581 -0
  180. package/src/testing/dom.entry.ts +22 -0
  181. package/src/testing/e2e/fixture.ts +188 -0
  182. package/src/testing/e2e/index.ts +127 -0
  183. package/src/testing/e2e/matchers.ts +35 -0
  184. package/src/testing/e2e/page-helpers.ts +272 -0
  185. package/src/testing/e2e/parity.ts +387 -0
  186. package/src/testing/e2e/server.ts +195 -0
  187. package/src/testing/flight-matchers.ts +97 -0
  188. package/src/testing/flight-normalize.ts +11 -0
  189. package/src/testing/flight-runtime.d.ts +57 -0
  190. package/src/testing/flight-tree.ts +682 -0
  191. package/src/testing/flight.entry.ts +52 -0
  192. package/src/testing/flight.ts +186 -0
  193. package/src/testing/generated-routes.ts +183 -0
  194. package/src/testing/index.ts +98 -0
  195. package/src/testing/internal/context.ts +348 -0
  196. package/src/testing/internal/flight-client-globals.ts +30 -0
  197. package/src/testing/internal/seed-vars.ts +54 -0
  198. package/src/testing/render-handler.ts +311 -0
  199. package/src/testing/render-route.tsx +504 -0
  200. package/src/testing/run-loader.ts +378 -0
  201. package/src/testing/run-middleware.ts +205 -0
  202. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  203. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  204. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  205. package/src/testing/vitest-stubs/version.ts +5 -0
  206. package/src/testing/vitest.ts +305 -0
  207. package/src/theme/ThemeProvider.tsx +0 -52
  208. package/src/theme/ThemeScript.tsx +0 -6
  209. package/src/theme/constants.ts +0 -12
  210. package/src/theme/index.ts +0 -7
  211. package/src/theme/theme-context.ts +1 -5
  212. package/src/theme/theme-script.ts +0 -14
  213. package/src/theme/use-theme.ts +0 -3
  214. package/src/types/boundaries.ts +0 -35
  215. package/src/types/error-types.ts +25 -89
  216. package/src/types/global-namespace.ts +15 -15
  217. package/src/types/handler-context.ts +16 -13
  218. package/src/types/index.ts +0 -10
  219. package/src/types/request-scope.ts +0 -19
  220. package/src/types/route-config.ts +6 -50
  221. package/src/types/route-entry.ts +0 -6
  222. package/src/types/segments.ts +0 -13
  223. package/src/urls/include-helper.ts +0 -4
  224. package/src/urls/index.ts +0 -6
  225. package/src/urls/path-helper-types.ts +2 -2
  226. package/src/urls/path-helper.ts +0 -54
  227. package/src/urls/urls-function.ts +0 -13
  228. package/src/use-loader.tsx +0 -186
  229. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  230. package/src/vite/discovery/discover-routers.ts +6 -7
  231. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  232. package/src/vite/plugin-types.ts +3 -1
  233. package/src/vite/plugins/cjs-to-esm.ts +0 -11
  234. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  235. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  236. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  237. package/src/vite/plugins/expose-action-id.ts +2 -73
  238. package/src/vite/plugins/expose-id-utils.ts +0 -55
  239. package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
  240. package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
  241. package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
  242. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  243. package/src/vite/plugins/expose-internal-ids.ts +10 -0
  244. package/src/vite/plugins/performance-tracks.ts +0 -3
  245. package/src/vite/plugins/use-cache-transform.ts +0 -36
  246. package/src/vite/plugins/version-injector.ts +0 -20
  247. package/src/vite/plugins/version-plugin.ts +1 -49
  248. package/src/vite/plugins/virtual-entries.ts +0 -15
  249. package/src/vite/rango.ts +1 -108
  250. package/src/vite/router-discovery.ts +2 -1
  251. package/src/vite/utils/ast-handler-extract.ts +0 -16
  252. package/src/vite/utils/bundle-analysis.ts +6 -13
  253. package/src/vite/utils/client-chunks.ts +0 -6
  254. package/src/vite/utils/forward-user-plugins.ts +0 -22
  255. package/src/vite/utils/manifest-utils.ts +0 -4
  256. package/src/vite/utils/package-resolution.ts +1 -73
  257. package/src/vite/utils/prerender-utils.ts +0 -35
  258. package/src/vite/utils/shared-utils.ts +3 -35
  259. package/src/browser/react/use-client-cache.ts +0 -58
  260. package/src/browser/shallow.ts +0 -40
@@ -13,10 +13,15 @@
13
13
 
14
14
  import type { CacheItemResult, CacheItemOptions } from "./types.js";
15
15
  import { runBackground } from "./background-task.js";
16
+ import { reportCacheError } from "./cache-error.js";
17
+ import type { CacheErrorReporter } from "./cache-error.js";
16
18
 
17
- interface WaitUntilHost {
19
+ // The host carries both the optional waitUntil (for background scheduling) and
20
+ // the CacheErrorReporter seam (for routing degradation errors through onError).
21
+ // loader-cache.ts passes the request context, which provides both.
22
+ type WaitUntilHost = {
18
23
  waitUntil?: (fn: () => Promise<void>) => void;
19
- }
24
+ } & CacheErrorReporter;
20
25
 
21
26
  export interface ReadThroughItemConfig<T> {
22
27
  /** Retrieve a cached item by key */
@@ -74,11 +79,20 @@ export async function readThroughItem<T>(
74
79
  host,
75
80
  } = config;
76
81
 
77
- // Cache lookup
82
+ // Cache lookup. An infra read failure (getItem) is reported by the store
83
+ // itself, so here we just degrade to a miss. A deserialize failure is a
84
+ // corrupt/truncated stored entry, which this layer owns: report it LOUD as
85
+ // cache-corrupt, then fall through to a fresh execution (the miss-path write
86
+ // self-heals the bad entry).
87
+ let cached: CacheItemResult | null = null;
78
88
  try {
79
- const cached = await getItem(key);
89
+ cached = await getItem(key);
90
+ } catch {
91
+ cached = null;
92
+ }
80
93
 
81
- if (cached) {
94
+ if (cached) {
95
+ try {
82
96
  const data = await deserialize(cached.value);
83
97
 
84
98
  if (!cached.shouldRevalidate) {
@@ -97,16 +111,27 @@ export async function readThroughItem<T>(
97
111
  if (serialized !== null) {
98
112
  await setItem(key, serialized, storeOptions);
99
113
  }
100
- } catch {
101
- // Background revalidation failed silently
114
+ } catch (error) {
115
+ reportCacheError(
116
+ error,
117
+ "stale-revalidation",
118
+ "[read-through] background revalidation",
119
+ host ?? undefined,
120
+ );
102
121
  }
103
122
  },
104
123
  true,
105
124
  );
106
125
  return data;
126
+ } catch (error) {
127
+ reportCacheError(
128
+ error,
129
+ "cache-corrupt",
130
+ "[read-through] deserialize stored entry",
131
+ host ?? undefined,
132
+ );
133
+ // fall through to fresh execution
107
134
  }
108
- } catch {
109
- // Cache lookup failed, fall through to fresh execution
110
135
  }
111
136
 
112
137
  // Cache miss
@@ -123,8 +148,13 @@ export async function readThroughItem<T>(
123
148
  await setItem(key, serialized, storeOptions);
124
149
  onCached?.();
125
150
  }
126
- } catch {
127
- // Cache write failed silently
151
+ } catch (error) {
152
+ reportCacheError(
153
+ error,
154
+ "cache-write",
155
+ "[read-through] cache write",
156
+ host ?? undefined,
157
+ );
128
158
  }
129
159
  },
130
160
  true,
@@ -16,10 +16,6 @@ import {
16
16
  } from "@vitejs/plugin-rsc/rsc";
17
17
  import { createFromReadableStream } from "@vitejs/plugin-rsc/rsc";
18
18
 
19
- // ============================================================================
20
- // Stream Utilities (internal)
21
- // ============================================================================
22
-
23
19
  /**
24
20
  * Convert a ReadableStream to a string.
25
21
  */
@@ -55,10 +51,6 @@ export function stringToStream(str: string): ReadableStream<Uint8Array> {
55
51
  });
56
52
  }
57
53
 
58
- // ============================================================================
59
- // RSC Serialization Primitives (internal)
60
- // ============================================================================
61
-
62
54
  /**
63
55
  * RSC-serialize a value using React Server Components stream.
64
56
  * Used for serializing loaderData, layout, loading components etc.
@@ -90,10 +82,6 @@ export async function rscDeserialize<T>(
90
82
  return createFromReadableStream<T>(stream, { temporaryReferences });
91
83
  }
92
84
 
93
- // ============================================================================
94
- // Null-Preserving RSC Serialization (for caching)
95
- // ============================================================================
96
-
97
85
  /**
98
86
  * RSC-serialize any value including null.
99
87
  * Unlike rscSerialize(), this does NOT skip null — it serializes it through
@@ -122,10 +110,6 @@ export async function deserializeResult<T>(encoded: string): Promise<T> {
122
110
  return createFromReadableStream<T>(stream, { temporaryReferences });
123
111
  }
124
112
 
125
- // ============================================================================
126
- // Public API
127
- // ============================================================================
128
-
129
113
  /**
130
114
  * RSC-deserialize a single encoded component string back to a React element.
131
115
  * Used by the static handler runtime to revive pre-rendered components.
@@ -12,10 +12,6 @@
12
12
  import type { ResolvedSegment } from "../types.js";
13
13
  import type { RequestContext } from "../server/request-context.js";
14
14
 
15
- // ============================================================================
16
- // Segment Cache Store (low-level storage interface)
17
- // ============================================================================
18
-
19
15
  /**
20
16
  * Result from cache get() including data and revalidation status
21
17
  */
@@ -116,12 +112,6 @@ export interface SegmentCacheStore<TEnv = unknown> {
116
112
  */
117
113
  clear?(): Promise<void>;
118
114
 
119
- // ============================================================================
120
- // Document Cache Methods (optional)
121
- // ============================================================================
122
- // These methods are for caching full HTTP responses (document-level caching).
123
- // Stores that support response caching should implement these methods.
124
-
125
115
  /**
126
116
  * Get a cached Response by key.
127
117
  * Returns the response and whether it should be revalidated (SWR).
@@ -146,12 +136,6 @@ export interface SegmentCacheStore<TEnv = unknown> {
146
136
  tags?: string[],
147
137
  ): Promise<void>;
148
138
 
149
- // ============================================================================
150
- // Function Cache Methods (optional, for "use cache" directive)
151
- // ============================================================================
152
- // These methods cache individual function/component return values.
153
- // Stores that support "use cache" should implement these methods.
154
-
155
139
  /**
156
140
  * Get a cached function result by key.
157
141
  * Returns the serialized value, optional handle data, and staleness flag.
@@ -170,10 +154,6 @@ export interface SegmentCacheStore<TEnv = unknown> {
170
154
  options?: CacheItemOptions,
171
155
  ): Promise<void>;
172
156
 
173
- // ============================================================================
174
- // Tag-based Invalidation (optional)
175
- // ============================================================================
176
-
177
157
  /**
178
158
  * Invalidate every cache entry (segment, response, item) tagged with any of
179
159
  * `tags`. Store-level primitive that the public updateTag()/revalidateTag()
@@ -264,10 +244,6 @@ export interface CachedEntryData {
264
244
  taggedAt?: number;
265
245
  }
266
246
 
267
- // ============================================================================
268
- // Cache Configuration
269
- // ============================================================================
270
-
271
247
  /**
272
248
  * Default cache options applied to all cache() boundaries.
273
249
  * Individual cache() calls can override any of these values.
@@ -292,82 +268,8 @@ export interface CacheDefaults {
292
268
  swr?: number;
293
269
  }
294
270
 
295
- /**
296
- * Cache configuration for RSC handler
297
- */
298
- export interface CacheConfig {
299
- /** Cache store implementation (includes defaults) */
300
- store: SegmentCacheStore;
301
- /** Enable/disable caching (default: true) */
302
- enabled?: boolean;
303
- }
304
-
305
- /**
306
- * Cache configuration - can be static or a function receiving env
307
- */
308
- export type CacheConfigOrFactory<TEnv> =
309
- | CacheConfig
310
- | ((env: TEnv) => CacheConfig);
311
-
312
- // ============================================================================
313
- // Segment Cache Provider (request-level interface)
314
- // ============================================================================
315
-
316
271
  /**
317
272
  * Handle data for a single segment
318
273
  * Structure: { handleName: [values...] }
319
274
  */
320
275
  export type SegmentHandleData = Record<string, unknown[]>;
321
-
322
- /**
323
- * Result from cache get() including segments and their handle data
324
- * Each entry can produce multiple segments (main + parallels)
325
- */
326
- export interface CachedEntryResult {
327
- /** All segments for this entry (main segment + parallels) */
328
- segments: ResolvedSegment[];
329
- /** Handle data keyed by segment ID */
330
- handles: Record<string, SegmentHandleData>;
331
- }
332
-
333
- /**
334
- * Segment cache provider interface
335
- *
336
- * Used by router to check/store segment cache during matching.
337
- * Accessed via request context - if not present, caching is disabled.
338
- *
339
- * @internal Not currently implemented - CacheScope is used directly.
340
- * Reserved for future extensibility.
341
- */
342
- export interface SegmentCacheProvider {
343
- /** Whether caching is enabled for this request */
344
- readonly enabled: boolean;
345
-
346
- /**
347
- * Get cached segments and restore handles/loaders.
348
- *
349
- * Combines cache get with handle replay and loader data restoration.
350
- * Returns tuple of [segments, segmentIds] if cache hit, null if miss or disabled.
351
- *
352
- * @param cacheKey - Cache key to look up
353
- * @param params - Route params for cache key generation
354
- * @param loaderPromises - Map to restore loader data into
355
- * @returns Tuple of [segments, segmentIds] or null if miss
356
- */
357
- restore(
358
- cacheKey: string,
359
- params: Record<string, string>,
360
- loaderPromises: Map<string, Promise<any>>,
361
- ): Promise<[ResolvedSegment[], string[]] | null>;
362
-
363
- /**
364
- * Cache entry with automatic handle collection (non-blocking).
365
- *
366
- * Schedules caching via waitUntil - handles are collected after they settle.
367
- * Validates segments have actual components before caching.
368
- *
369
- * @param cacheKey - The cache key to store under
370
- * @param segments - All resolved segments for this entry
371
- */
372
- cacheEntry(cacheKey: string, segments: ResolvedSegment[]): void;
373
- }
@@ -14,60 +14,43 @@
14
14
  export {
15
15
  Outlet,
16
16
  ParallelOutlet,
17
- OutletProvider,
18
17
  useOutlet,
19
18
  useLoader,
20
19
  ErrorBoundary,
21
20
  type ErrorBoundaryProps,
22
21
  } from "./client.js";
23
22
 
24
- // Re-export the server's createLoader for RSC context
25
- // This version includes the actual loader function
26
23
  export { createLoader } from "./route-definition.js";
27
24
 
28
- // Re-export Link component (can be used in server components)
29
25
  export {
30
26
  Link,
31
27
  type LinkProps,
32
28
  type PrefetchStrategy,
33
29
  } from "./browser/react/Link.js";
34
30
 
35
- // Re-export ScrollRestoration (can be used in server components)
36
31
  export {
37
32
  ScrollRestoration,
38
33
  type ScrollRestorationProps,
39
34
  } from "./browser/react/ScrollRestoration.js";
40
35
 
41
- // Re-export NavigationProvider (needed for setup)
42
36
  export {
43
37
  NavigationProvider,
44
38
  type NavigationProviderProps,
45
39
  } from "./browser/react/NavigationProvider.js";
46
40
 
47
- // Re-export href function (can be used in server components)
48
41
  export { href } from "./href-client.js";
49
42
 
50
- // Mount context re-exports (useMount is client-only, but MountContext can be referenced)
51
43
  export { MountContext } from "./browser/react/mount-context.js";
52
44
 
53
- // Note: useNavigation, useAction, useClientCache are NOT re-exported here
54
- // because they use client-side state and should only be used in client components
45
+ // useNavigation and useAction are NOT re-exported here because they use client-side state
55
46
 
56
- // Handle API - for accumulating data across route segments
57
- // Works in both RSC and client contexts
58
47
  export { createHandle, isHandle, type Handle } from "./handle.js";
59
48
 
60
- // Built-in handles
61
- // Meta handle works in RSC context
62
49
  export { Meta } from "./handles/meta.js";
63
- // MetaTags is a "use client" component that can be imported from RSC
64
50
  export { MetaTags } from "./handles/MetaTags.js";
65
51
  export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
66
- // Breadcrumbs handle works in RSC context
67
52
  export { Breadcrumbs, type BreadcrumbItem } from "./handles/breadcrumbs.js";
68
53
 
69
- // Location state - createLocationState works in RSC (just creates definition)
70
- // useLocationState is NOT exported here as it uses client hooks
71
54
  export {
72
55
  createLocationState,
73
56
  type LocationStateDefinition,
@@ -75,14 +58,10 @@ export {
75
58
  type LocationStateOptions,
76
59
  } from "./browser/react/location-state-shared.js";
77
60
 
78
- // Re-export useHref - it's a "use client" hook
79
61
  export { useHref } from "./browser/react/use-href.js";
80
62
 
81
- // Re-export useReverse - it's a "use client" hook
82
63
  export { useReverse } from "./browser/react/use-reverse.js";
83
64
 
84
- // Re-export useHandle - it's a "use client" hook
85
65
  export { useHandle } from "./browser/react/use-handle.js";
86
66
 
87
- // Re-export useLocationState - it's a "use client" hook
88
67
  export { useLocationState } from "./browser/react/location-state.js";
package/src/client.tsx CHANGED
@@ -111,6 +111,11 @@ function useSlotSegment(
111
111
  * the parallel segment with that slot name instead of the default content.
112
112
  * This is used for parallel routes and intercepting routes.
113
113
  *
114
+ * For a named slot, `<Outlet name="@x" />` is equivalent to
115
+ * `<ParallelOutlet name="@x" />` — both run the same resolution + wrapping
116
+ * pipeline. Convention: use bare `<Outlet />` for default content and
117
+ * `<ParallelOutlet name="@x" />` for named slots.
118
+ *
114
119
  * @param name - Optional slot name for parallel/intercept content (must start with @)
115
120
  *
116
121
  * @example
@@ -163,6 +168,9 @@ export function Outlet({ name }: { name?: `@${string}` } = {}): ReactNode {
163
168
  * is wrapped in Suspense with the loading component as fallback.
164
169
  * This enables streaming and navigation loading states for parallels.
165
170
  *
171
+ * Equivalent to `<Outlet name="@x" />` for a named slot; ParallelOutlet
172
+ * requires `name` and is named-slot-only, which reads clearer at the call site.
173
+ *
166
174
  * @param name - The slot name (must start with @, e.g., "@modal", "@sidebar")
167
175
  *
168
176
  * @example
@@ -186,10 +194,9 @@ export function ParallelOutlet({ name }: { name: `@${string}` }): ReactNode {
186
194
  }
187
195
 
188
196
  // OutletProvider is defined in outlet-provider.tsx to break a circular
189
- // dependency between client.tsx and route-content-wrapper.tsx.
190
- // Imported at the top of this file for local use in Outlet/ParallelOutlet,
191
- // and re-exported here for backwards compatibility.
192
- export { OutletProvider };
197
+ // dependency between client.tsx and route-content-wrapper.tsx. It is imported
198
+ // at the top of this file for local use in Outlet/ParallelOutlet only; it is an
199
+ // internal component and is intentionally not part of the public ./client API.
193
200
 
194
201
  /**
195
202
  * Hook to access outlet content programmatically
@@ -210,7 +217,6 @@ export function useOutlet(): ReactNode {
210
217
  return context?.content ?? null;
211
218
  }
212
219
 
213
- // Loader hooks - re-exported from dedicated file
214
220
  export {
215
221
  useLoader,
216
222
  useFetchLoader,
@@ -329,12 +335,6 @@ export class ErrorBoundary extends Component<
329
335
  }
330
336
  }
331
337
 
332
- // ============================================================================
333
- // Re-exports from browser/react for convenience
334
- // These are the most commonly used client-side navigation utilities
335
- // ============================================================================
336
-
337
- // Navigation hooks
338
338
  export { useNavigation } from "./browser/react/use-navigation.js";
339
339
  export { useRouter } from "./browser/react/use-router.js";
340
340
  export { usePathname } from "./browser/react/use-pathname.js";
@@ -346,62 +346,48 @@ export type {
346
346
  ReadonlyURLSearchParams,
347
347
  } from "./browser/types.js";
348
348
 
349
- // Action state tracking hook
350
349
  export {
351
350
  useAction,
352
351
  type ServerActionFunction,
353
352
  } from "./browser/react/use-action.js";
354
353
 
355
- // Segments state hook
356
354
  export {
357
355
  useSegments,
358
356
  type SegmentsState,
359
357
  } from "./browser/react/use-segments.js";
360
358
 
361
- // Client cache controls hook
362
- export {
363
- useClientCache,
364
- type ClientCacheControls,
365
- } from "./browser/react/use-client-cache.js";
366
-
367
- // Provider
368
359
  export {
369
360
  NavigationProvider,
370
361
  type NavigationProviderProps,
371
362
  } from "./browser/react/NavigationProvider.js";
372
363
 
373
- // Link component
374
364
  export {
375
365
  Link,
376
366
  type LinkProps,
377
367
  type PrefetchStrategy,
378
368
  type StateOrGetter,
369
+ type LinkState,
379
370
  } from "./browser/react/Link.js";
380
371
 
381
- // Link status hook
382
372
  export {
383
373
  useLinkStatus,
384
374
  type LinkStatus,
385
375
  } from "./browser/react/use-link-status.js";
386
376
 
387
- // Scroll restoration
388
377
  export {
389
378
  ScrollRestoration,
390
379
  useScrollRestoration,
391
380
  type ScrollRestorationProps,
392
381
  } from "./browser/react/ScrollRestoration.js";
393
382
 
394
- // Handle data hook (client-side only — createHandle/isHandle are server APIs from the root export)
395
383
  export { type Handle } from "./handle.js";
396
384
  export { useHandle } from "./browser/react/use-handle.js";
397
385
 
398
- // Built-in handles
399
386
  export { Meta } from "./handles/meta.js";
400
387
  export { MetaTags } from "./handles/MetaTags.js";
401
388
  export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
402
389
  export { Breadcrumbs, type BreadcrumbItem } from "./handles/breadcrumbs.js";
403
390
 
404
- // Location state - type-safe navigation state
405
391
  export {
406
392
  createLocationState,
407
393
  useLocationState,
@@ -410,29 +396,19 @@ export {
410
396
  type LocationStateOptions,
411
397
  } from "./browser/react/location-state.js";
412
398
 
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.
399
+ // Ambient Rango.Path / Rango.PathResponse types (declared in href-client.ts)
416
400
  export { href, type PatternToPath } from "./href-client.js";
417
401
 
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.
402
+ // RFC 9457 error type for JSON response routes
421
403
  export type { ProblemDetails } from "./urls.js";
422
404
 
423
- // Mount context for include() scoped components
424
405
  export { useMount } from "./browser/react/use-mount.js";
425
406
  export { MountContext } from "./browser/react/mount-context.js";
426
407
 
427
- // Mount-aware href hook - auto-prefixes paths with include() mount
428
408
  export { useHref } from "./browser/react/use-href.js";
429
409
 
430
- // Mount-aware reverse hook - resolves dot-prefixed names against an imported
431
- // generated routes map (from a urls() module's .gen.ts).
432
410
  export { useReverse } from "./browser/react/use-reverse.js";
433
411
 
434
- // Type-safe scoped reverse function for scopedReverse<typeof patterns>()
435
412
  export type { ScopedReverseFunction, LocalReverseFunction } from "./reverse.js";
436
413
 
437
- // Loader definition type - for typing loader props in client components
438
414
  export type { LoaderDefinition } from "./types.js";
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { ComponentType } from "react";
8
+ import { isUnderTestRunner } from "./runtime-env.js";
8
9
 
9
10
  /**
10
11
  * Symbol used by React to mark client component references.
@@ -48,11 +49,21 @@ export function isClientComponent(
48
49
  *
49
50
  * @param component - The component to check
50
51
  * @param name - Name to use in error message (e.g., "document")
52
+ * @param opts.allowServerInTest - When true AND running under a test runner
53
+ * (`isUnderTestRunner()`), relax ONLY the "use client" requirement: a server
54
+ * component is accepted. The plugin's "use client" transform does not run in a
55
+ * bare unit test, so a real exported `document` (almost every app sets one) has
56
+ * no client marker and would otherwise throw at `createRouter`, blocking
57
+ * `dispatch`/`assertGeneratedRoutesMatch` against the real router. The document
58
+ * reference is irrelevant to those (no Flight render). The "not a JSX element"
59
+ * guard still fires, and a real dev/build still throws (mirrors the runtime
60
+ * fallback-id gating in handle.ts/loader.ts).
51
61
  * @throws Error if the component is not a client component
52
62
  */
53
63
  export function assertClientComponent(
54
64
  component: ComponentType<unknown> | unknown,
55
65
  name: string,
66
+ opts?: { allowServerInTest?: boolean },
56
67
  ): asserts component is ComponentType<unknown> {
57
68
  if (typeof component !== "function") {
58
69
  throw new Error(
@@ -62,6 +73,14 @@ export function assertClientComponent(
62
73
  );
63
74
  }
64
75
 
76
+ // Under a test runner the "use client" transform did not run, so a real
77
+ // server-rendered `document` has no client marker; accept it (the reference is
78
+ // never serialized in dispatch/route-map checks). Outside a test runner this
79
+ // still throws — the build-time safety net is preserved.
80
+ if (opts?.allowServerInTest && isUnderTestRunner()) {
81
+ return;
82
+ }
83
+
65
84
  if (!isClientComponent(component)) {
66
85
  throw new Error(
67
86
  `${name} must be a client component with "use client" directive at the top of the file. ` +
package/src/deps/ssr.ts CHANGED
@@ -1,2 +1 @@
1
- // Re-export @vitejs/plugin-rsc/ssr for internal use by virtual entries
2
1
  export { createFromReadableStream } from "@vitejs/plugin-rsc/ssr";
package/src/handle.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { missingInjectedIdError } from "./missing-id-error.js";
2
+ import { isUnderTestRunner } from "./runtime-env.js";
2
3
 
3
4
  /**
4
5
  * Handle definition for accumulating data across route segments.
@@ -45,10 +46,10 @@ function defaultCollect<T>(segments: T[][]): T[] {
45
46
  // Used by useHandle() to recover collect when handle is deserialized from RSC prop.
46
47
  const collectRegistry = new Map<string, (segments: unknown[][]) => unknown>();
47
48
 
48
- /**
49
- * Look up a collect function from the registry by handle $$id.
50
- * Returns undefined if not registered (falls back to defaultCollect in useHandle).
51
- */
49
+ // Monotonic counter for runtime fallback ids (see createHandle). Only used
50
+ // when no build id was injected (a bare unit test).
51
+ let runtimeHandleIdCounter = 0;
52
+
52
53
  export function getCollectFn(
53
54
  id: string,
54
55
  ): ((segments: unknown[][]) => unknown) | undefined {
@@ -95,24 +96,36 @@ export function createHandle<TData, TAccumulated = TData[]>(
95
96
  collect?: (segments: TData[][]) => TAccumulated,
96
97
  __injectedId?: string,
97
98
  ): Handle<TData, TAccumulated> {
98
- const handleId = __injectedId ?? "";
99
+ let handleId = __injectedId ?? "";
99
100
 
100
- if (!handleId && process.env.NODE_ENV === "development") {
101
- throw missingInjectedIdError("Handle", "createHandle");
101
+ // No build-injected id. Under a test runner: fall back to a synthetic id so the
102
+ // collect registers below and the handle is exercisable in tests (useHandle,
103
+ // collectHandle, renderRoute's `handles` run the REAL collect). Otherwise (dev
104
+ // or a real build) it means an UNSUPPORTED handler shape the plugin skipped —
105
+ // fail loud. The rich, stack-parsing diagnostic stays behind the NODE_ENV check
106
+ // so a production build folds it away and tree-shakes missing-id-error.ts out,
107
+ // shipping the small throw instead. isUnderTestRunner() is runtime-safe.
108
+ if (!handleId) {
109
+ if (isUnderTestRunner()) {
110
+ handleId = `__rango_runtime_handle_${runtimeHandleIdCounter++}`;
111
+ } else if (process.env.NODE_ENV !== "production") {
112
+ throw missingInjectedIdError("Handle", "createHandle");
113
+ } else {
114
+ throw new Error(
115
+ "[rango] Handle is missing $$id — the build plugin did not inject one. " +
116
+ "Export it as `export const X = createHandle(...)`.",
117
+ );
118
+ }
102
119
  }
103
120
 
104
121
  const collectFn =
105
122
  collect ??
106
123
  (defaultCollect as unknown as (segments: TData[][]) => TAccumulated);
107
124
 
108
- // Register collect in module-level registry so useHandle() can recover it
109
- // when the handle is deserialized from RSC props (toJSON strips collect).
110
- if (handleId) {
111
- collectRegistry.set(
112
- handleId,
113
- collectFn as (segments: unknown[][]) => unknown,
114
- );
115
- }
125
+ collectRegistry.set(
126
+ handleId,
127
+ collectFn as (segments: unknown[][]) => unknown,
128
+ );
116
129
 
117
130
  return {
118
131
  __brand: "handle" as const,
@@ -120,9 +133,6 @@ export function createHandle<TData, TAccumulated = TData[]>(
120
133
  };
121
134
  }
122
135
 
123
- /**
124
- * Type guard to check if a value is a Handle.
125
- */
126
136
  export function isHandle(value: unknown): value is Handle<unknown, unknown> {
127
137
  return (
128
138
  typeof value === "object" &&