@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2

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 (222) hide show
  1. package/AGENTS.md +6 -10
  2. package/README.md +289 -938
  3. package/dist/bin/rango.js +271 -46
  4. package/dist/vite/index.js +673 -193
  5. package/package.json +10 -8
  6. package/skills/api-client/SKILL.md +1 -1
  7. package/skills/breadcrumbs/SKILL.md +31 -14
  8. package/skills/cache-guide/SKILL.md +5 -2
  9. package/skills/caching/SKILL.md +59 -4
  10. package/skills/catalog.json +271 -0
  11. package/skills/comparison/SKILL.md +50 -0
  12. package/skills/comparison/agents/openai.yaml +4 -0
  13. package/skills/comparison/references/framework-comparison.md +837 -0
  14. package/skills/composability/SKILL.md +83 -2
  15. package/skills/debug-manifest/SKILL.md +1 -1
  16. package/skills/defer-hydration/SKILL.md +235 -0
  17. package/skills/document-cache/SKILL.md +9 -1
  18. package/skills/fonts/SKILL.md +1 -1
  19. package/skills/handler-use/SKILL.md +8 -8
  20. package/skills/hooks/SKILL.md +54 -892
  21. package/skills/hooks/data.md +273 -0
  22. package/skills/hooks/handle-and-actions.md +103 -0
  23. package/skills/hooks/navigation.md +110 -0
  24. package/skills/hooks/outlets.md +41 -0
  25. package/skills/hooks/state.md +228 -0
  26. package/skills/hooks/urls.md +135 -0
  27. package/skills/host-router/SKILL.md +4 -4
  28. package/skills/i18n/SKILL.md +1 -1
  29. package/skills/intercept/SKILL.md +46 -14
  30. package/skills/layout/SKILL.md +27 -10
  31. package/skills/links/SKILL.md +1 -1
  32. package/skills/loader/SKILL.md +23 -1
  33. package/skills/middleware/SKILL.md +7 -3
  34. package/skills/migrate-nextjs/SKILL.md +167 -6
  35. package/skills/migrate-react-router/SKILL.md +59 -677
  36. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  37. package/skills/migrate-react-router/component-migration.md +196 -0
  38. package/skills/migrate-react-router/data-and-actions.md +225 -0
  39. package/skills/migrate-react-router/route-mapping.md +271 -0
  40. package/skills/mime-routes/SKILL.md +1 -1
  41. package/skills/observability/SKILL.md +9 -1
  42. package/skills/parallel/SKILL.md +23 -4
  43. package/skills/ppr/SKILL.md +622 -0
  44. package/skills/prerender/SKILL.md +28 -18
  45. package/skills/rango/SKILL.md +84 -25
  46. package/skills/response-routes/SKILL.md +15 -1
  47. package/skills/route/SKILL.md +71 -4
  48. package/skills/router-setup/SKILL.md +14 -3
  49. package/skills/scripts/SKILL.md +1 -1
  50. package/skills/server-actions/SKILL.md +3 -2
  51. package/skills/shell-manifest/SKILL.md +185 -0
  52. package/skills/streams-and-websockets/SKILL.md +1 -1
  53. package/skills/tailwind/SKILL.md +1 -1
  54. package/skills/testing/SKILL.md +2 -1
  55. package/skills/testing/handles.md +4 -2
  56. package/skills/testing/render-handler.md +15 -14
  57. package/skills/testing/reverse-and-types.md +8 -7
  58. package/skills/theme/SKILL.md +1 -1
  59. package/skills/typesafety/SKILL.md +45 -919
  60. package/skills/typesafety/env-and-bindings.md +254 -0
  61. package/skills/typesafety/generated-files-and-cli.md +335 -0
  62. package/skills/typesafety/params-and-search.md +153 -0
  63. package/skills/typesafety/route-types.md +209 -0
  64. package/skills/use-cache/SKILL.md +30 -3
  65. package/skills/vercel/SKILL.md +1 -1
  66. package/skills/view-transitions/SKILL.md +44 -1
  67. package/src/browser/event-controller.ts +62 -10
  68. package/src/browser/logging.ts +28 -0
  69. package/src/browser/merge-segment-loaders.ts +6 -4
  70. package/src/browser/navigation-bridge.ts +65 -16
  71. package/src/browser/navigation-client.ts +32 -2
  72. package/src/browser/navigation-store.ts +128 -14
  73. package/src/browser/network-error-handler.ts +34 -7
  74. package/src/browser/partial-update.ts +76 -17
  75. package/src/browser/prefetch/cache.ts +51 -11
  76. package/src/browser/prefetch/fetch.ts +59 -21
  77. package/src/browser/prefetch/queue.ts +19 -4
  78. package/src/browser/react/Link.tsx +13 -3
  79. package/src/browser/react/NavigationProvider.tsx +108 -4
  80. package/src/browser/response-adapter.ts +38 -9
  81. package/src/browser/rsc-router.tsx +54 -4
  82. package/src/browser/scroll-restoration.ts +7 -5
  83. package/src/browser/segment-reconciler.ts +31 -21
  84. package/src/browser/server-action-bridge.ts +22 -10
  85. package/src/browser/types.ts +54 -1
  86. package/src/build/generate-manifest.ts +155 -131
  87. package/src/build/index.ts +3 -1
  88. package/src/build/route-trie.ts +35 -7
  89. package/src/build/route-types/include-resolution.ts +347 -47
  90. package/src/build/runtime-discovery.ts +4 -1
  91. package/src/cache/cache-key-utils.ts +29 -0
  92. package/src/cache/cache-runtime.ts +262 -71
  93. package/src/cache/cache-scope.ts +2 -17
  94. package/src/cache/cache-tag.ts +60 -14
  95. package/src/cache/cf/cf-cache-store.ts +243 -20
  96. package/src/cache/document-cache.ts +54 -21
  97. package/src/cache/index.ts +1 -0
  98. package/src/cache/memory-segment-store.ts +110 -3
  99. package/src/cache/profile-registry.ts +15 -0
  100. package/src/cache/read-through-swr.ts +15 -1
  101. package/src/cache/segment-codec.ts +4 -4
  102. package/src/cache/shell-snapshot.ts +417 -0
  103. package/src/cache/types.ts +158 -0
  104. package/src/cache/vercel/vercel-cache-store.ts +401 -124
  105. package/src/client.rsc.tsx +0 -3
  106. package/src/client.tsx +0 -3
  107. package/src/cloudflare/tracing.ts +7 -8
  108. package/src/defer.ts +11 -22
  109. package/src/handle.ts +37 -15
  110. package/src/handles/MetaTags.tsx +16 -82
  111. package/src/handles/breadcrumbs.ts +12 -14
  112. package/src/handles/deferred-resolution.ts +127 -0
  113. package/src/handles/is-thenable.ts +7 -8
  114. package/src/handles/meta.ts +7 -44
  115. package/src/host/errors.ts +15 -0
  116. package/src/host/index.ts +1 -0
  117. package/src/index.rsc.ts +8 -2
  118. package/src/index.ts +19 -13
  119. package/src/internal-debug.ts +11 -8
  120. package/src/prerender.ts +17 -4
  121. package/src/redirect-origin.ts +14 -0
  122. package/src/render-error-thrower.tsx +20 -0
  123. package/src/route-content-wrapper.tsx +12 -5
  124. package/src/route-definition/dsl-helpers.ts +21 -32
  125. package/src/route-definition/helper-factories.ts +0 -2
  126. package/src/route-definition/helpers-types.ts +43 -43
  127. package/src/route-definition/index.ts +1 -2
  128. package/src/route-definition/resolve-handler-use.ts +0 -1
  129. package/src/route-definition/use-item-types.ts +3 -6
  130. package/src/route-map-builder.ts +41 -4
  131. package/src/route-types.ts +0 -5
  132. package/src/router/find-match.ts +86 -8
  133. package/src/router/instrument.ts +9 -4
  134. package/src/router/lazy-includes.ts +72 -12
  135. package/src/router/loader-resolution.ts +14 -2
  136. package/src/router/manifest.ts +56 -11
  137. package/src/router/match-api.ts +76 -32
  138. package/src/router/match-handlers.ts +181 -135
  139. package/src/router/match-middleware/background-revalidation.ts +40 -23
  140. package/src/router/match-middleware/cache-store.ts +39 -24
  141. package/src/router/match-result.ts +35 -15
  142. package/src/router/middleware.ts +64 -38
  143. package/src/router/navigation-snapshot.ts +7 -5
  144. package/src/router/parse-pattern.ts +115 -0
  145. package/src/router/pattern-matching.ts +53 -64
  146. package/src/router/prefetch-limits.ts +37 -0
  147. package/src/router/prerender-match.ts +11 -5
  148. package/src/router/preview-match.ts +3 -1
  149. package/src/router/request-classification.ts +23 -8
  150. package/src/router/route-snapshot.ts +14 -2
  151. package/src/router/router-context.ts +3 -1
  152. package/src/router/router-interfaces.ts +32 -1
  153. package/src/router/router-options.ts +30 -0
  154. package/src/router/segment-resolution/fresh.ts +39 -3
  155. package/src/router/segment-resolution/loader-cache.ts +93 -2
  156. package/src/router/segment-resolution/loader-mask.ts +60 -0
  157. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  158. package/src/router/segment-resolution/mask-nested.ts +83 -0
  159. package/src/router/segment-resolution/revalidation.ts +3 -0
  160. package/src/router/segment-resolution/view-transition-default.ts +35 -15
  161. package/src/router/substitute-pattern-params.ts +54 -35
  162. package/src/router/telemetry-otel.ts +6 -8
  163. package/src/router/telemetry.ts +9 -1
  164. package/src/router/tracing.ts +14 -5
  165. package/src/router/trie-matching.ts +19 -11
  166. package/src/router/url-params.ts +13 -0
  167. package/src/router.ts +47 -16
  168. package/src/rsc/full-payload.ts +70 -0
  169. package/src/rsc/handler.ts +60 -33
  170. package/src/rsc/manifest-init.ts +1 -1
  171. package/src/rsc/nonce.ts +10 -1
  172. package/src/rsc/progressive-enhancement.ts +61 -4
  173. package/src/rsc/redirect-guard.ts +2 -1
  174. package/src/rsc/rsc-rendering.ts +429 -37
  175. package/src/rsc/server-action.ts +25 -2
  176. package/src/rsc/shell-capture.ts +1190 -0
  177. package/src/rsc/shell-serve.ts +181 -0
  178. package/src/rsc/transition-gate.ts +89 -0
  179. package/src/rsc/types.ts +30 -0
  180. package/src/segment-loader-promise.ts +18 -0
  181. package/src/segment-system.tsx +149 -14
  182. package/src/server/context.ts +67 -9
  183. package/src/server/cookie-store.ts +73 -1
  184. package/src/server/loader-registry.ts +13 -1
  185. package/src/server/request-context.ts +169 -10
  186. package/src/ssr/index.tsx +462 -178
  187. package/src/ssr/inject-rsc-eager.ts +167 -0
  188. package/src/ssr/ssr-root.tsx +228 -0
  189. package/src/testing/collect-handle.ts +14 -8
  190. package/src/testing/dispatch.ts +152 -40
  191. package/src/testing/generated-routes.ts +27 -11
  192. package/src/testing/index.ts +6 -0
  193. package/src/testing/render-handler.ts +14 -0
  194. package/src/testing/render-route.tsx +13 -10
  195. package/src/testing/run-transition-when.ts +164 -0
  196. package/src/theme/ThemeProvider.tsx +36 -26
  197. package/src/types/handler-context.ts +1 -1
  198. package/src/types/index.ts +2 -0
  199. package/src/types/route-config.ts +19 -7
  200. package/src/types/segments.ts +100 -0
  201. package/src/urls/include-helper.ts +10 -8
  202. package/src/urls/include-provider.ts +71 -0
  203. package/src/urls/index.ts +1 -0
  204. package/src/urls/path-helper-types.ts +44 -12
  205. package/src/urls/path-helper.ts +5 -0
  206. package/src/urls/pattern-types.ts +36 -0
  207. package/src/urls/type-extraction.ts +43 -18
  208. package/src/urls/urls-function.ts +0 -1
  209. package/src/vercel/tracing.ts +7 -7
  210. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  211. package/src/vite/discovery/discover-routers.ts +1 -1
  212. package/src/vite/discovery/discovery-errors.ts +61 -0
  213. package/src/vite/index.ts +7 -0
  214. package/src/vite/inject-client-debug.ts +88 -0
  215. package/src/vite/plugins/vercel-output.ts +114 -25
  216. package/src/vite/plugins/version-injector.ts +22 -7
  217. package/src/vite/plugins/virtual-entries.ts +80 -22
  218. package/src/vite/rango.ts +29 -19
  219. package/src/vite/router-discovery.ts +171 -43
  220. package/src/vite/utils/prerender-utils.ts +17 -4
  221. package/src/vite/utils/shared-utils.ts +47 -0
  222. package/src/network-error-thrower.tsx +0 -18
@@ -64,6 +64,21 @@ export class NoRouteMatchError extends HostRouterError {
64
64
  }
65
65
  }
66
66
 
67
+ /**
68
+ * True when `err` is a NoRouteMatchError — including one thrown by a
69
+ * DUPLICATED @rangojs/router copy in the module graph (a workspace pinning a
70
+ * second version), whose class identity differs so a bare `instanceof` misses
71
+ * it and an unmatched-host 404 becomes an opaque 500. Use this in worker
72
+ * catch blocks instead of `instanceof`; it also matches by the stable `name`
73
+ * the constructor stamps.
74
+ */
75
+ export function isNoRouteMatchError(err: unknown): err is NoRouteMatchError {
76
+ return (
77
+ err instanceof NoRouteMatchError ||
78
+ (err instanceof Error && err.name === "NoRouteMatchError")
79
+ );
80
+ }
81
+
67
82
  export class InvalidHandlerError extends HostRouterError {
68
83
  constructor(handler: unknown, options?: ErrorOptions) {
69
84
  super(`Invalid handler type: ${typeof handler}`, options);
package/src/host/index.ts CHANGED
@@ -42,6 +42,7 @@ export {
42
42
  InvalidHostnameError,
43
43
  HostValidationError,
44
44
  NoRouteMatchError,
45
+ isNoRouteMatchError,
45
46
  InvalidHandlerError,
46
47
  } from "./errors.js";
47
48
 
package/src/index.rsc.ts CHANGED
@@ -65,6 +65,12 @@ export type {
65
65
  ErrorPhase,
66
66
  OnErrorContext,
67
67
  OnErrorCallback,
68
+ // View transition types (also exported from the default entry; route
69
+ // definitions resolve to this react-server entry, so they must be here too)
70
+ TransitionConfig,
71
+ TransitionWhenFn,
72
+ TransitionWhenContext,
73
+ ViewTransitionClass,
68
74
  } from "./types.js";
69
75
 
70
76
  // Router options type (server-only, so import directly)
@@ -97,7 +103,6 @@ export {
97
103
  loading,
98
104
  parallel,
99
105
  intercept,
100
- when,
101
106
  errorBoundary,
102
107
  notFoundBoundary,
103
108
  transition,
@@ -117,7 +122,6 @@ export { createHandle, isHandle, type Handle } from "./handle.js";
117
122
  export {
118
123
  DEFAULT_DEFER_TIMEOUT_MS,
119
124
  type DeferOptions,
120
- type DeferredHandleEntry,
121
125
  type HandlePush,
122
126
  type HandlePushFn,
123
127
  } from "./defer.js";
@@ -153,6 +157,7 @@ export {
153
157
  urls,
154
158
  type PathHelpers,
155
159
  type PathOptions,
160
+ type PartialPrerenderProps,
156
161
  type UrlPatterns,
157
162
  type IncludeOptions,
158
163
  type IncludeItem,
@@ -303,6 +308,7 @@ export type {
303
308
  RouterTracingConfig,
304
309
  TracePhase,
305
310
  TracePhaseToggles,
311
+ TracingToggleOptions,
306
312
  } from "./router/tracing.js";
307
313
 
308
314
  // Timeout types and error class
package/src/index.ts CHANGED
@@ -82,7 +82,12 @@ export { createLoader } from "./loader.js";
82
82
 
83
83
  // Route definition types (safe to import anywhere)
84
84
  export type { RouteHelpers, RouteHandlers } from "./route-definition.js";
85
- export type { TransitionConfig, ViewTransitionClass } from "./types.js";
85
+ export type {
86
+ TransitionConfig,
87
+ TransitionWhenFn,
88
+ TransitionWhenContext,
89
+ ViewTransitionClass,
90
+ } from "./types.js";
86
91
 
87
92
  // Composition types for reusable callback factories
88
93
  export type {
@@ -105,6 +110,7 @@ export type {
105
110
  TextResponsePathFn,
106
111
  RouteResponse,
107
112
  ProblemDetails,
113
+ PartialPrerenderProps,
108
114
  } from "./urls.js";
109
115
 
110
116
  // Middleware context types
@@ -143,7 +149,6 @@ export { createHandle, isHandle, type Handle } from "./handle.js";
143
149
  export {
144
150
  DEFAULT_DEFER_TIMEOUT_MS,
145
151
  type DeferOptions,
146
- type DeferredHandleEntry,
147
152
  type HandlePush,
148
153
  type HandlePushFn,
149
154
  } from "./defer.js";
@@ -279,9 +284,6 @@ export function parallel(): never {
279
284
  export function intercept(): never {
280
285
  throw serverOnlyStubError("intercept");
281
286
  }
282
- export function when(): never {
283
- throw serverOnlyStubError("when");
284
- }
285
287
  export function errorBoundary(): never {
286
288
  throw serverOnlyStubError("errorBoundary");
287
289
  }
@@ -341,14 +343,17 @@ export {
341
343
  // Path and response types are ambient on the `Rango` namespace (`Rango.Path`,
342
344
  // `Rango.PathResponse`, declared in href-client.ts) — no import needed.
343
345
 
344
- // Telemetry types only — the createConsoleSink/createOTelSink values are
345
- // server-only and live in index.rsc.ts (the `react-server` condition of the
346
- // bare `@rangojs/router` import). Re-exporting them as values from this
347
- // (default/client) entry would pull telemetry.ts and telemetry-otel.ts into
348
- // the client module graph; both tree-shake to zero bytes but still appear in
349
- // bundle analysis output and slow build-time module resolution. Consumers
350
- // who need the values in non-RSC contexts can import from
351
- // `@rangojs/router/server`.
346
+ // Telemetry types only — the createConsoleSink / createOTelSink /
347
+ // createOTelTracing VALUES are server-only and live in index.rsc.ts (the
348
+ // `react-server` condition of the bare `@rangojs/router` import). Re-exporting
349
+ // them as values from this (default/client) entry would pull telemetry.ts and
350
+ // telemetry-otel.ts into the client module graph; both tree-shake to zero bytes
351
+ // but still appear in bundle analysis output and slow build-time module
352
+ // resolution. The factory values are NOT re-exported from `@rangojs/router/server`
353
+ // either — that subpath is internal, not user-facing (see server.ts header).
354
+ // Non-RSC server code imports these TYPES from the root and obtains the factory
355
+ // VALUES from its own router definition module, which resolves to index.rsc.ts
356
+ // under the `react-server` condition.
352
357
  export type {
353
358
  OTelTracer,
354
359
  OTelActiveSpanTracer,
@@ -384,6 +389,7 @@ export type {
384
389
  RouterTracingConfig,
385
390
  TracePhase,
386
391
  TracePhaseToggles,
392
+ TracingToggleOptions,
387
393
  } from "./router/tracing.js";
388
394
 
389
395
  // Timeout types and error class
@@ -1,9 +1,12 @@
1
- // Vite define for compile-time injection; falls back to process.env (tests, Node).
2
- // Works in all runtimes including Cloudflare Workers where process.env is unavailable.
1
+ // Runtime fallback for non-Vite contexts (Node, tests, raw imports). In the Vite
2
+ // pipeline -- dev and build, every environment -- the router discovery plugin's
3
+ // transform replaces this module with the build-time-resolved flag (see
4
+ // vite/inject-client-debug.ts), so the CLIENT debug flag no longer depends on a
5
+ // `__RANGO_DEBUG__` define being delivered to the browser (which dev did only as
6
+ // an injected global whose presence varied across consumer setups).
7
+ //
8
+ // Keep this module to the single INTERNAL_RANGO_DEBUG export: the transform
9
+ // replaces the whole file, so a second export would be dropped from the bundle.
3
10
  export const INTERNAL_RANGO_DEBUG: boolean =
4
- typeof __RANGO_DEBUG__ !== "undefined"
5
- ? __RANGO_DEBUG__
6
- : typeof process !== "undefined" &&
7
- Boolean((process as any).env?.INTERNAL_RANGO_DEBUG);
8
-
9
- declare const __RANGO_DEBUG__: boolean;
11
+ typeof process !== "undefined" &&
12
+ Boolean((process as any).env?.INTERNAL_RANGO_DEBUG);
package/src/prerender.ts CHANGED
@@ -33,6 +33,7 @@ import type {
33
33
  ExtractParams,
34
34
  } from "./types.js";
35
35
  import type { Handle } from "./handle.js";
36
+ import type { HandlePush } from "./defer.js";
36
37
  import type { ContextVar } from "./context-var.js";
37
38
  import type { ReverseFunction } from "./reverse.js";
38
39
  import type { DefaultReverseRouteMap } from "./types/global-namespace.js";
@@ -165,8 +166,14 @@ export interface BuildContext<TParams> {
165
166
  (key: string, value: any): void;
166
167
  };
167
168
 
168
- /** Push handle data (frozen into pre-rendered output at build time). */
169
- use: <T>(handle: Handle<T>) => (data: T) => void;
169
+ /**
170
+ * Push handle data (frozen into pre-rendered output at build time). Returns
171
+ * the full push function, including `.defer()` — a deferred slot resolved by
172
+ * a deep async component during the prerender render is awaited before the
173
+ * artifact is baked (resolve-by-default), so the baked output holds the
174
+ * resolved value.
175
+ */
176
+ use: <T>(handle: Handle<T>) => HandlePush<T>;
170
177
 
171
178
  /** Synthetic URL built from pattern + params (no real request). */
172
179
  url: URL;
@@ -222,8 +229,14 @@ export interface StaticBuildContext {
222
229
  (key: string, value: any): void;
223
230
  };
224
231
 
225
- /** Push handle data (frozen into pre-rendered output at build time). */
226
- use: <T>(handle: Handle<T>) => (data: T) => void;
232
+ /**
233
+ * Push handle data (frozen into pre-rendered output at build time). Returns
234
+ * the full push function, including `.defer()` — a deferred slot resolved by
235
+ * a deep async component during the prerender render is awaited before the
236
+ * artifact is baked (resolve-by-default), so the baked output holds the
237
+ * resolved value.
238
+ */
239
+ use: <T>(handle: Handle<T>) => HandlePush<T>;
227
240
 
228
241
  /** URL generation by route name. */
229
242
  reverse: BuildReverseFunction;
@@ -60,6 +60,20 @@ export function resolveExternalRedirect(
60
60
  }
61
61
  }
62
62
 
63
+ /**
64
+ * The safe same-origin landing for a blocked redirect.
65
+ *
66
+ * Every guard that neutralizes a cross-origin/unsafe redirect target sends the
67
+ * browser here instead: the app's basename root, or `"/"` when unset. Kept
68
+ * beside the resolvers so the "where does a blocked redirect go" answer lives
69
+ * in ONE place -- the server 3xx guard (`rsc/redirect-guard.ts`) and the
70
+ * shell-HIT degradation path (`rsc/rsc-rendering.ts`) must agree, or a blocked
71
+ * redirect lands differently depending on which exit it took.
72
+ */
73
+ export function safeSameOriginLanding(basename: string | undefined): string {
74
+ return basename && basename !== "/" ? basename : "/";
75
+ }
76
+
63
77
  /**
64
78
  * Out-of-band brand for `redirect(url, { external: true })`.
65
79
  *
@@ -0,0 +1,20 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+
5
+ interface RenderErrorThrowerProps {
6
+ error: unknown;
7
+ }
8
+
9
+ /**
10
+ * Client component that throws the given error during render, so the nearest
11
+ * error boundary catches it. Errors thrown during render are caught by error
12
+ * boundaries; async errors (rejected promises) are not -- which is why the
13
+ * navigation bridge funnels processing failures through this component instead
14
+ * of letting them surface as uncaught rejections.
15
+ */
16
+ export function RenderErrorThrower({
17
+ error,
18
+ }: RenderErrorThrowerProps): ReactNode {
19
+ throw error;
20
+ }
@@ -1,7 +1,6 @@
1
1
  "use client";
2
2
  import type { ReactNode } from "react";
3
3
  import { Suspense, use } from "react";
4
- import { invariant } from "./errors";
5
4
  import { OutletProvider } from "./outlet-provider.js";
6
5
  import type { ResolvedSegment } from "./types.js";
7
6
  import { decodeLoaderResults } from "./decode-loader-results.js";
@@ -22,7 +21,9 @@ export function RouteContentWrapper({
22
21
  fallback,
23
22
  segmentId,
24
23
  }: {
25
- content: Promise<ReactNode>;
24
+ // Normally a pending promise (use() suspends -> fallback). forceAwait paths
25
+ // pass an already-resolved node so Suspender renders it without suspending.
26
+ content: Promise<ReactNode> | ReactNode;
26
27
  fallback?: ReactNode;
27
28
  segmentId?: string;
28
29
  }): ReactNode {
@@ -41,9 +42,15 @@ const Suspender = ({
41
42
  }: {
42
43
  content: Promise<ReactNode> | ReactNode;
43
44
  }): ReactNode => {
44
- invariant(content instanceof Promise, "Suspender expects a Promise content");
45
-
46
- return use(content);
45
+ // Normally content is a pending promise -> use() suspends and the wrapping
46
+ // Suspense shows the loading() fallback. forceAwait paths (popstate,
47
+ // stale-revalidation, fully-prefetched nav) instead pass the ALREADY-RESOLVED
48
+ // node so first render does not suspend for a microtask and flash the loading()
49
+ // fallback on a NORMAL (non-transition) commit. The wrapper tree
50
+ // (RouteContentWrapper > Suspense > Suspender) is identical either way, so this
51
+ // preserves tree structure (see docs/tree-structure.md) — only whether use()
52
+ // suspends differs, exactly like LoaderResolver's resolved-data branch.
53
+ return content instanceof Promise ? use(content) : content;
47
54
  };
48
55
 
49
56
  /**
@@ -17,6 +17,7 @@ import {
17
17
  type EntryPropSegments,
18
18
  type HelperContext,
19
19
  type InterceptEntry,
20
+ type InterceptConfig,
20
21
  } from "../server/context";
21
22
  import { invariant } from "../errors";
22
23
  import { validateUserRouteName } from "../route-name.js";
@@ -36,7 +37,6 @@ import type {
36
37
  ErrorBoundaryItem,
37
38
  NotFoundBoundaryItem,
38
39
  LayoutItem,
39
- WhenItem,
40
40
  CacheItem,
41
41
  TransitionItem,
42
42
  UseItems,
@@ -265,34 +265,6 @@ const notFoundBoundary: RouteHelpers<any, any>["notFoundBoundary"] = (
265
265
  return { name, type: "notFoundBoundary" } as NotFoundBoundaryItem;
266
266
  };
267
267
 
268
- /**
269
- * When helper - defines a condition for intercept activation
270
- *
271
- * Only valid inside intercept() use() callback. The when() function
272
- * is captured by the intercept and stored in its `when` array.
273
- * During soft navigation, all when() conditions must return true
274
- * for the intercept to activate.
275
- */
276
- const when: RouteHelpers<any, any>["when"] = (fn) => {
277
- const { store, ctx } = requireDslContext(
278
- "when() must be called inside intercept()",
279
- );
280
-
281
- // The when() function needs to be captured by the intercept's tempParent
282
- // which should have a `when` array. If not present, we're not inside intercept()
283
- const parent = ctx.parent as any;
284
- if (!parent || !("when" in parent)) {
285
- invariant(
286
- false,
287
- "when() can only be used inside intercept() use() callback",
288
- );
289
- }
290
-
291
- const name = `$${store.getNextIndex("when")}`;
292
- parent.when.push(fn);
293
- return { name, type: "when" } as WhenItem;
294
- };
295
-
296
268
  /**
297
269
  * Cache helper - defines caching configuration for segments
298
270
  *
@@ -701,8 +673,19 @@ const intercept = (
701
673
  slotName: `@${string}`,
702
674
  routeName: string,
703
675
  handler: any,
676
+ configOrUse?: InterceptConfig | (() => any[]),
704
677
  use?: () => any[],
705
678
  ) => {
679
+ // arg4 discrimination: a function is the use() callback (no config); an object
680
+ // is the config carrying `when`. With config given, the use() callback is
681
+ // arg5. Keeps the no-config form intercept(slot, route, handler, () => [...])
682
+ // working unchanged.
683
+ const config: InterceptConfig | undefined =
684
+ typeof configOrUse === "function" || configOrUse == null
685
+ ? undefined
686
+ : configOrUse;
687
+ const useFn = typeof configOrUse === "function" ? configOrUse : use;
688
+
706
689
  const { store, ctx } = requireDslContext(
707
690
  "intercept() must be called inside urls()",
708
691
  );
@@ -740,9 +723,17 @@ const intercept = (
740
723
  when: [], // Selector conditions for conditional interception
741
724
  };
742
725
 
726
+ // Conditional interception: `when` from the config object — a single selector
727
+ // or an array (ALL must return true to activate). Replaces the former when()
728
+ // use-item captured inside the callback.
729
+ if (config?.when) {
730
+ const selectors = Array.isArray(config.when) ? config.when : [config.when];
731
+ entry.when.push(...selectors);
732
+ }
733
+
743
734
  // Merge handler.use defaults with explicit use
744
735
  const handlerUseFn = resolveHandlerUse(handler);
745
- const mergedUse = mergeHandlerUse(handlerUseFn, use, "intercept");
736
+ const mergedUse = mergeHandlerUse(handlerUseFn, useFn, "intercept");
746
737
 
747
738
  // Run merged use callback to collect loaders, revalidate, middleware, etc.
748
739
  if (mergedUse) {
@@ -759,7 +750,6 @@ const intercept = (
759
750
  notFoundBoundary: entry.notFoundBoundary,
760
751
  loader: entry.loader,
761
752
  layout: capturedLayouts, // Capture layout() calls
762
- when: entry.when, // Capture when() conditions
763
753
  get loading() {
764
754
  return entry.loading;
765
755
  },
@@ -1114,7 +1104,6 @@ export {
1114
1104
  revalidate,
1115
1105
  parallel,
1116
1106
  intercept,
1117
- when,
1118
1107
  errorBoundary,
1119
1108
  notFoundBoundary,
1120
1109
  route,
@@ -8,7 +8,6 @@ import {
8
8
  revalidate,
9
9
  parallel,
10
10
  intercept,
11
- when,
12
11
  errorBoundary,
13
12
  notFoundBoundary,
14
13
  route,
@@ -40,7 +39,6 @@ function buildRouteHelpers<T extends RouteDefinition, TEnv>(): RouteHelpers<
40
39
  loading,
41
40
  errorBoundary,
42
41
  notFoundBoundary,
43
- when,
44
42
  cache,
45
43
  transition,
46
44
  } as unknown as RouteHelpers<T, TEnv>;
@@ -29,12 +29,10 @@ import type {
29
29
  ParallelUseItem,
30
30
  InterceptUseItem,
31
31
  LoaderUseItem,
32
- WhenItem,
33
32
  CacheItem,
34
33
  TransitionItem,
35
34
  UseItems,
36
35
  } from "../route-types.js";
37
- import type { InterceptWhenFn } from "../server/context";
38
36
 
39
37
  // Re-export route item types for backward compatibility
40
38
  export type {
@@ -52,12 +50,12 @@ export type {
52
50
  RouteUseItem,
53
51
  ParallelUseItem,
54
52
  InterceptUseItem,
55
- WhenItem,
56
53
  CacheItem,
57
54
  } from "../route-types.js";
58
55
 
59
56
  // Re-export intercept selector types for use in handlers
60
57
  export type {
58
+ InterceptConfig,
61
59
  InterceptSelectorContext,
62
60
  InterceptSegmentsState,
63
61
  InterceptWhenFn,
@@ -151,8 +149,11 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
151
149
  * so they take precedence on `loading()` and other last-write-wins
152
150
  * fields.
153
151
  */
154
- parallel: <
155
- TSlots extends Record<
152
+ // Not generic over the slots record: an inferred type parameter makes the
153
+ // object literal an inference site, which suppresses contextual typing of
154
+ // arrow slot handlers (`(ctx) => ...` was implicit any).
155
+ parallel: (
156
+ slots: Record<
156
157
  `@${string}`,
157
158
  | Handler<any, any, TEnv>
158
159
  | ReactNode
@@ -161,8 +162,6 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
161
162
  use?: () => UseItems<ParallelUseItem>;
162
163
  }
163
164
  >,
164
- >(
165
- slots: TSlots,
166
165
  use?: () => UseItems<ParallelUseItem>,
167
166
  ) => ParallelItem;
168
167
  /**
@@ -183,10 +182,26 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
183
182
  * loader(CardModalLoader),
184
183
  * revalidate(() => false),
185
184
  * ])
185
+ *
186
+ * // Conditional activation via the config object's `when` selector
187
+ * intercept("@modal", "card", <CardModal />, {
188
+ * when: ({ from }) => from.pathname.startsWith("/board"),
189
+ * })
190
+ *
191
+ * // Config + other use-items: config is arg 4, use is arg 5
192
+ * intercept(
193
+ * "@modal",
194
+ * "card",
195
+ * <CardModal />,
196
+ * { when: ({ from }) => from.pathname.startsWith("/board") },
197
+ * () => [loader(CardDetailLoader)],
198
+ * )
186
199
  * ```
187
200
  * @param slotName - Named slot (prefixed with @) where intercept renders
188
201
  * @param routeName - Route name to intercept
189
202
  * @param handler - Component or handler for intercepted render
203
+ * @param config - Optional InterceptConfig (e.g. `{ when }`), or the use
204
+ * callback directly when there is no config
190
205
  * @param use - Optional callback for loaders, middleware, revalidate, etc.
191
206
  */
192
207
  intercept: {
@@ -195,6 +210,9 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
195
210
  slotName: `@${string}`,
196
211
  routeName: `.${K}`,
197
212
  handler: ReactNode | Handler<ExtractRouteParams<T, K>, {}, TEnv>,
213
+ config?:
214
+ | import("../server/context.js").InterceptConfig<TEnv>
215
+ | (() => UseItems<InterceptUseItem>),
198
216
  use?: () => UseItems<InterceptUseItem>,
199
217
  ): InterceptItem;
200
218
  // Global: unprefixed, params inferred from global route map
@@ -202,6 +220,9 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
202
220
  slotName: `@${string}`,
203
221
  routeName: K,
204
222
  handler: ReactNode | Handler<K, Rango.GeneratedRouteMap, TEnv>,
223
+ config?:
224
+ | import("../server/context.js").InterceptConfig<TEnv>
225
+ | (() => UseItems<InterceptUseItem>),
205
226
  use?: () => UseItems<InterceptUseItem>,
206
227
  ): InterceptItem;
207
228
  };
@@ -347,40 +368,6 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
347
368
  notFoundBoundary: (
348
369
  fallback: ReactNode | NotFoundBoundaryHandler,
349
370
  ) => NotFoundBoundaryItem;
350
- /**
351
- * Define a condition for when an intercept should activate
352
- *
353
- * Only valid inside intercept() use() callback. When multiple when() calls
354
- * are present, ALL must return true for the intercept to activate.
355
- * If no when() is defined, the intercept always activates on soft navigation.
356
- *
357
- * Context properties:
358
- * - `from` - Source URL (where user is navigating from)
359
- * - `to` - Destination URL (where user is navigating to)
360
- * - `params` - Matched route params
361
- * - `segments` - Client's current segments with `path` and `ids`
362
- *
363
- * ```typescript
364
- * // Only intercept when coming from the board page
365
- * intercept("@modal", "card", <CardModal />, () => [
366
- * when(({ from }) => from.pathname.startsWith("/board")),
367
- * loader(CardDetailLoader),
368
- * ])
369
- *
370
- * // Use segments to check current route context
371
- * intercept("@modal", "card", <CardModal />, () => [
372
- * when(({ segments }) => segments.path[0] === "kanban"),
373
- * ])
374
- *
375
- * // Multiple conditions (AND logic)
376
- * intercept("@modal", "card", <CardModal />, () => [
377
- * when(({ from }) => from.pathname.startsWith("/board")),
378
- * when(({ segments }) => segments.ids.includes("kanban-layout")),
379
- * ])
380
- * ```
381
- * @param fn - Selector function receiving navigation context, returns boolean
382
- */
383
- when: (fn: InterceptWhenFn) => WhenItem;
384
371
  /**
385
372
  * Define cache configuration for segments
386
373
  *
@@ -472,6 +459,13 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
472
459
  * transition({}) is startTransition + ViewTransition under the default and
473
460
  * startTransition only when the router sets viewTransition: false.
474
461
  *
462
+ * Conditional hold: pass `when: (ctx) => boolean` to gate the transition per
463
+ * request. It runs server-side AFTER the route handler (so it can read state
464
+ * the handler set via `ctx.get(...)`); returning false drops this transition
465
+ * for the request, so the navigation streams its loading() skeleton instead of
466
+ * holding. This is a post-handler predicate — distinct from intercept()'s
467
+ * match-time `when` config selector (`intercept(slot, route, Comp, { when })`).
468
+ *
475
469
  * ```typescript
476
470
  * // Attach to a single route
477
471
  * path("/about", AboutPage, { name: "about" }, () => [
@@ -488,10 +482,16 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
488
482
  * path("/product/:id", ProductPage, { name: "product" }, () => [
489
483
  * transition({ viewTransition: false }),
490
484
  * ])
485
+ *
486
+ * // Hold only when the handler decided to (post-handler predicate):
487
+ * path("/product/:id", ProductPage, { name: "product" }, () => [
488
+ * transition({ when: (ctx) => ctx.get(KeepScroll) === true }),
489
+ * ])
491
490
  * ```
492
491
  * @param config - ViewTransition configuration (enter, exit, update, share,
493
- * default, name) plus `viewTransition: "auto" | false` to toggle the router
494
- * boundary (createRouter({ viewTransition }) sets the app-wide default)
492
+ * default, name), `viewTransition: "auto" | false` to toggle the router
493
+ * boundary (createRouter({ viewTransition }) sets the app-wide default), and
494
+ * `when: (ctx) => boolean` to gate the transition per request post-handler
495
495
  * @param children - Optional callback returning child routes to wrap
496
496
  */
497
497
  transition: {
@@ -15,8 +15,8 @@ export type {
15
15
  RouteUseItem,
16
16
  ParallelUseItem,
17
17
  InterceptUseItem,
18
- WhenItem,
19
18
  CacheItem,
19
+ InterceptConfig,
20
20
  InterceptSelectorContext,
21
21
  InterceptSegmentsState,
22
22
  InterceptWhenFn,
@@ -30,7 +30,6 @@ export {
30
30
  revalidate,
31
31
  parallel,
32
32
  intercept,
33
- when,
34
33
  errorBoundary,
35
34
  notFoundBoundary,
36
35
  loader,
@@ -100,7 +100,6 @@ const MOUNT_SITE_ALLOWED_TYPES: Record<string, Set<string>> = {
100
100
  "notFoundBoundary",
101
101
  "layout",
102
102
  "route",
103
- "when",
104
103
  "transition",
105
104
  ]),
106
105
  // LoaderUseItem — only revalidate + cache can attach to a loader entry
@@ -1,16 +1,14 @@
1
- import type { AllUseItems, WhenItem } from "../route-types.js";
1
+ import type { AllUseItems } from "../route-types.js";
2
2
 
3
3
  /**
4
4
  * The set of valid use-item `type` discriminants — the single runtime source of
5
5
  * truth for "is this a well-formed use item?" shape validation.
6
6
  *
7
7
  * Declared via a `Record<...>` so that adding a member to the union without
8
- * updating this map is a compile error. `when` is included because when() items
9
- * are valid inside intercept() even though WhenItem is not part of AllUseItems
10
- * (it lives only in InterceptUseItem). This is shape validation only; per-mount-
8
+ * updating this map is a compile error. This is shape validation only; per-mount-
11
9
  * site rules remain the narrower hand-written tables in resolve-handler-use.ts.
12
10
  */
13
- const USE_ITEM_TYPES: Record<AllUseItems["type"] | WhenItem["type"], true> = {
11
+ const USE_ITEM_TYPES: Record<AllUseItems["type"], true> = {
14
12
  layout: true,
15
13
  route: true,
16
14
  middleware: true,
@@ -21,7 +19,6 @@ const USE_ITEM_TYPES: Record<AllUseItems["type"] | WhenItem["type"], true> = {
21
19
  loading: true,
22
20
  errorBoundary: true,
23
21
  notFoundBoundary: true,
24
- when: true,
25
22
  cache: true,
26
23
  transition: true,
27
24
  include: true,