@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1fa245e2

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 (160) hide show
  1. package/{CLAUDE.md → AGENTS.md} +4 -0
  2. package/README.md +122 -30
  3. package/dist/bin/rango.js +245 -63
  4. package/dist/vite/index.js +859 -418
  5. package/package.json +3 -3
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +32 -0
  8. package/skills/caching/SKILL.md +49 -8
  9. package/skills/document-cache/SKILL.md +2 -2
  10. package/skills/hooks/SKILL.md +33 -31
  11. package/skills/host-router/SKILL.md +218 -0
  12. package/skills/links/SKILL.md +3 -1
  13. package/skills/loader/SKILL.md +72 -22
  14. package/skills/middleware/SKILL.md +2 -0
  15. package/skills/parallel/SKILL.md +126 -0
  16. package/skills/prerender/SKILL.md +112 -70
  17. package/skills/rango/SKILL.md +0 -1
  18. package/skills/route/SKILL.md +34 -4
  19. package/skills/router-setup/SKILL.md +95 -5
  20. package/skills/typesafety/SKILL.md +35 -23
  21. package/src/__internal.ts +92 -0
  22. package/src/bin/rango.ts +18 -0
  23. package/src/browser/app-version.ts +14 -0
  24. package/src/browser/event-controller.ts +5 -0
  25. package/src/browser/link-interceptor.ts +4 -0
  26. package/src/browser/navigation-bridge.ts +114 -18
  27. package/src/browser/navigation-client.ts +126 -44
  28. package/src/browser/navigation-store.ts +43 -8
  29. package/src/browser/navigation-transaction.ts +11 -9
  30. package/src/browser/partial-update.ts +80 -15
  31. package/src/browser/prefetch/cache.ts +166 -27
  32. package/src/browser/prefetch/fetch.ts +52 -39
  33. package/src/browser/prefetch/policy.ts +6 -0
  34. package/src/browser/prefetch/queue.ts +92 -20
  35. package/src/browser/prefetch/resource-ready.ts +77 -0
  36. package/src/browser/react/Link.tsx +70 -14
  37. package/src/browser/react/NavigationProvider.tsx +40 -4
  38. package/src/browser/react/context.ts +7 -2
  39. package/src/browser/react/use-handle.ts +9 -58
  40. package/src/browser/react/use-router.ts +21 -8
  41. package/src/browser/rsc-router.tsx +143 -59
  42. package/src/browser/scroll-restoration.ts +41 -42
  43. package/src/browser/segment-reconciler.ts +6 -1
  44. package/src/browser/server-action-bridge.ts +454 -436
  45. package/src/browser/types.ts +60 -5
  46. package/src/build/generate-manifest.ts +6 -6
  47. package/src/build/generate-route-types.ts +5 -0
  48. package/src/build/route-trie.ts +19 -3
  49. package/src/build/route-types/include-resolution.ts +8 -1
  50. package/src/build/route-types/router-processing.ts +346 -87
  51. package/src/build/route-types/scan-filter.ts +8 -1
  52. package/src/cache/cache-runtime.ts +15 -11
  53. package/src/cache/cache-scope.ts +48 -7
  54. package/src/cache/cf/cf-cache-store.ts +453 -11
  55. package/src/cache/cf/index.ts +5 -1
  56. package/src/cache/document-cache.ts +17 -7
  57. package/src/cache/index.ts +1 -0
  58. package/src/cache/taint.ts +55 -0
  59. package/src/client.rsc.tsx +2 -1
  60. package/src/client.tsx +3 -102
  61. package/src/context-var.ts +72 -2
  62. package/src/debug.ts +2 -2
  63. package/src/handle.ts +40 -0
  64. package/src/handles/breadcrumbs.ts +66 -0
  65. package/src/handles/index.ts +1 -0
  66. package/src/host/index.ts +0 -3
  67. package/src/index.rsc.ts +8 -37
  68. package/src/index.ts +40 -66
  69. package/src/prerender/store.ts +57 -15
  70. package/src/prerender.ts +138 -77
  71. package/src/reverse.ts +22 -1
  72. package/src/route-definition/dsl-helpers.ts +73 -25
  73. package/src/route-definition/helpers-types.ts +10 -6
  74. package/src/route-definition/index.ts +3 -3
  75. package/src/route-definition/redirect.ts +11 -3
  76. package/src/route-definition/resolve-handler-use.ts +149 -0
  77. package/src/route-map-builder.ts +7 -1
  78. package/src/route-types.ts +11 -0
  79. package/src/router/content-negotiation.ts +100 -1
  80. package/src/router/find-match.ts +4 -2
  81. package/src/router/handler-context.ts +108 -25
  82. package/src/router/intercept-resolution.ts +11 -4
  83. package/src/router/lazy-includes.ts +4 -1
  84. package/src/router/loader-resolution.ts +123 -11
  85. package/src/router/logging.ts +5 -2
  86. package/src/router/manifest.ts +9 -3
  87. package/src/router/match-api.ts +125 -190
  88. package/src/router/match-middleware/background-revalidation.ts +30 -2
  89. package/src/router/match-middleware/cache-lookup.ts +88 -16
  90. package/src/router/match-middleware/cache-store.ts +53 -10
  91. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  92. package/src/router/match-middleware/segment-resolution.ts +61 -5
  93. package/src/router/match-result.ts +22 -15
  94. package/src/router/metrics.ts +238 -13
  95. package/src/router/middleware-types.ts +53 -12
  96. package/src/router/middleware.ts +172 -85
  97. package/src/router/navigation-snapshot.ts +182 -0
  98. package/src/router/pattern-matching.ts +20 -5
  99. package/src/router/prerender-match.ts +114 -10
  100. package/src/router/preview-match.ts +30 -102
  101. package/src/router/request-classification.ts +310 -0
  102. package/src/router/revalidation.ts +27 -7
  103. package/src/router/route-snapshot.ts +245 -0
  104. package/src/router/router-context.ts +6 -1
  105. package/src/router/router-interfaces.ts +50 -5
  106. package/src/router/router-options.ts +50 -19
  107. package/src/router/segment-resolution/fresh.ts +200 -19
  108. package/src/router/segment-resolution/helpers.ts +30 -25
  109. package/src/router/segment-resolution/loader-cache.ts +1 -0
  110. package/src/router/segment-resolution/revalidation.ts +429 -301
  111. package/src/router/segment-wrappers.ts +2 -0
  112. package/src/router/trie-matching.ts +20 -2
  113. package/src/router/types.ts +1 -0
  114. package/src/router.ts +88 -15
  115. package/src/rsc/handler.ts +546 -359
  116. package/src/rsc/index.ts +0 -20
  117. package/src/rsc/manifest-init.ts +5 -1
  118. package/src/rsc/progressive-enhancement.ts +25 -8
  119. package/src/rsc/rsc-rendering.ts +35 -43
  120. package/src/rsc/server-action.ts +16 -10
  121. package/src/rsc/ssr-setup.ts +128 -0
  122. package/src/rsc/types.ts +10 -1
  123. package/src/search-params.ts +16 -13
  124. package/src/segment-system.tsx +140 -4
  125. package/src/server/context.ts +148 -16
  126. package/src/server/loader-registry.ts +9 -8
  127. package/src/server/request-context.ts +182 -34
  128. package/src/server.ts +6 -0
  129. package/src/ssr/index.tsx +4 -0
  130. package/src/static-handler.ts +18 -6
  131. package/src/theme/index.ts +4 -13
  132. package/src/types/cache-types.ts +4 -4
  133. package/src/types/handler-context.ts +149 -49
  134. package/src/types/loader-types.ts +36 -9
  135. package/src/types/route-config.ts +17 -8
  136. package/src/types/route-entry.ts +8 -1
  137. package/src/types/segments.ts +2 -5
  138. package/src/urls/path-helper-types.ts +9 -2
  139. package/src/urls/path-helper.ts +48 -13
  140. package/src/urls/pattern-types.ts +12 -0
  141. package/src/urls/response-types.ts +16 -6
  142. package/src/use-loader.tsx +73 -4
  143. package/src/vite/discovery/bundle-postprocess.ts +61 -89
  144. package/src/vite/discovery/discover-routers.ts +23 -5
  145. package/src/vite/discovery/prerender-collection.ts +48 -15
  146. package/src/vite/discovery/state.ts +17 -13
  147. package/src/vite/index.ts +8 -3
  148. package/src/vite/plugin-types.ts +51 -79
  149. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  150. package/src/vite/plugins/expose-action-id.ts +1 -3
  151. package/src/vite/plugins/performance-tracks.ts +88 -0
  152. package/src/vite/plugins/refresh-cmd.ts +127 -0
  153. package/src/vite/plugins/version-plugin.ts +13 -1
  154. package/src/vite/rango.ts +174 -211
  155. package/src/vite/router-discovery.ts +169 -42
  156. package/src/vite/utils/banner.ts +3 -3
  157. package/src/vite/utils/prerender-utils.ts +78 -0
  158. package/src/vite/utils/shared-utils.ts +3 -2
  159. package/skills/testing/SKILL.md +0 -226
  160. package/src/route-definition/route-function.ts +0 -119
package/src/index.ts CHANGED
@@ -10,9 +10,6 @@
10
10
  * import from "@rangojs/router/client"
11
11
  */
12
12
 
13
- // Universal rendering utilities (work on both server and client)
14
- export { renderSegments } from "./segment-system.js";
15
-
16
13
  // Error classes (can be used on both server and client)
17
14
  export {
18
15
  RouteNotFoundError,
@@ -22,9 +19,6 @@ export {
22
19
  HandlerError,
23
20
  BuildError,
24
21
  InvalidHandlerError,
25
- NetworkError,
26
- isNetworkError,
27
- sanitizeError,
28
22
  RouterError,
29
23
  Skip,
30
24
  isSkip,
@@ -41,7 +35,6 @@ export type {
41
35
  TrailingSlashMode,
42
36
  // Handler types
43
37
  Handler, // Supports params object, path pattern, or route name
44
- ScopedRouteMap, // Scoped view of GeneratedRouteMap for Handler<"localName", ScopedRouteMap<"prefix">>
45
38
  HandlerContext,
46
39
  ExtractParams,
47
40
  GenericParams,
@@ -95,6 +88,7 @@ export type {
95
88
  LayoutUseItem,
96
89
  AllUseItems,
97
90
  UseItems,
91
+ HandlerUseItem,
98
92
  } from "./route-types.js";
99
93
 
100
94
  // Response route types (usable in both server and client contexts)
@@ -115,25 +109,32 @@ export type {
115
109
  // Middleware context types
116
110
  export type { MiddlewareContext, CookieOptions } from "./router/middleware.js";
117
111
 
112
+ function serverOnlyStubError(name: string): Error {
113
+ return new Error(
114
+ `${name}() is only available from "@rangojs/router" in a react-server/RSC environment. ` +
115
+ `For client hooks and components, import from "@rangojs/router/client".`,
116
+ );
117
+ }
118
+
118
119
  /**
119
120
  * Error-throwing stub for server-only `urls` function.
120
121
  */
121
122
  export function urls(): never {
122
- throw new Error("urls() is server-only and requires RSC context.");
123
+ throw serverOnlyStubError("urls");
123
124
  }
124
125
 
125
126
  /**
126
127
  * Error-throwing stub for server-only `createRouter` function.
127
128
  */
128
129
  export function createRouter(): never {
129
- throw new Error("createRouter() is server-only and requires RSC context.");
130
+ throw serverOnlyStubError("createRouter");
130
131
  }
131
132
 
132
133
  /**
133
134
  * Error-throwing stub for server-only `redirect` function.
134
135
  */
135
136
  export function redirect(): never {
136
- throw new Error("redirect() is server-only and requires RSC context.");
137
+ throw serverOnlyStubError("redirect");
137
138
  }
138
139
 
139
140
  // Handle API (universal - works on both server and client)
@@ -149,102 +150,87 @@ export { nonce } from "./rsc/nonce.js";
149
150
  * Error-throwing stub for server-only `Prerender` function.
150
151
  */
151
152
  export function Prerender(): never {
152
- throw new Error("Prerender() is server-only and requires RSC context.");
153
+ throw serverOnlyStubError("Prerender");
154
+ }
155
+
156
+ /**
157
+ * Error-throwing stub for server-only `Passthrough` function.
158
+ */
159
+ export function Passthrough(): never {
160
+ throw serverOnlyStubError("Passthrough");
153
161
  }
154
162
 
155
163
  /**
156
164
  * Error-throwing stub for server-only `Static` function.
157
165
  */
158
166
  export function Static(): never {
159
- throw new Error("Static() is server-only and requires RSC context.");
167
+ throw serverOnlyStubError("Static");
160
168
  }
161
169
 
162
170
  /**
163
171
  * Error-throwing stub for server-only `getRequestContext` function.
164
172
  */
165
173
  export function getRequestContext(): never {
166
- throw new Error(
167
- "getRequestContext() is server-only and requires RSC context.",
168
- );
174
+ throw serverOnlyStubError("getRequestContext");
169
175
  }
170
176
 
171
177
  /**
172
178
  * Error-throwing stub for server-only `cookies` function.
173
179
  */
174
180
  export function cookies(): never {
175
- throw new Error("cookies() is server-only and requires RSC context.");
181
+ throw serverOnlyStubError("cookies");
176
182
  }
177
183
 
178
184
  /**
179
185
  * Error-throwing stub for server-only `headers` function.
180
186
  */
181
187
  export function headers(): never {
182
- throw new Error("headers() is server-only and requires RSC context.");
188
+ throw serverOnlyStubError("headers");
183
189
  }
184
190
 
185
191
  /**
186
192
  * Error-throwing stub for server-only `createReverse` function.
187
193
  */
188
194
  export function createReverse(): never {
189
- throw new Error("createReverse() is server-only and requires RSC context.");
190
- }
191
-
192
- /**
193
- * Error-throwing stub for server-only `enableMatchDebug` function.
194
- */
195
- export function enableMatchDebug(): never {
196
- throw new Error(
197
- "enableMatchDebug() is server-only and requires RSC context.",
198
- );
199
- }
200
-
201
- /**
202
- * Error-throwing stub for server-only `getMatchDebugStats` function.
203
- */
204
- export function getMatchDebugStats(): never {
205
- throw new Error(
206
- "getMatchDebugStats() is server-only and requires RSC context.",
207
- );
195
+ throw serverOnlyStubError("createReverse");
208
196
  }
209
197
 
210
198
  // Error-throwing stubs for server-only route helpers
211
199
  export function layout(): never {
212
- throw new Error("layout() is server-only and requires RSC context.");
200
+ throw serverOnlyStubError("layout");
213
201
  }
214
202
  export function cache(): never {
215
- throw new Error("cache() is server-only and requires RSC context.");
203
+ throw serverOnlyStubError("cache");
216
204
  }
217
205
  export function middleware(): never {
218
- throw new Error("middleware() is server-only and requires RSC context.");
206
+ throw serverOnlyStubError("middleware");
219
207
  }
220
208
  export function revalidate(): never {
221
- throw new Error("revalidate() is server-only and requires RSC context.");
209
+ throw serverOnlyStubError("revalidate");
222
210
  }
223
211
  export function loader(): never {
224
- throw new Error("loader() is server-only and requires RSC context.");
212
+ throw serverOnlyStubError("loader");
225
213
  }
226
214
  export function loading(): never {
227
- throw new Error("loading() is server-only and requires RSC context.");
215
+ throw serverOnlyStubError("loading");
228
216
  }
229
217
  export function parallel(): never {
230
- throw new Error("parallel() is server-only and requires RSC context.");
218
+ throw serverOnlyStubError("parallel");
231
219
  }
232
220
  export function intercept(): never {
233
- throw new Error("intercept() is server-only and requires RSC context.");
221
+ throw serverOnlyStubError("intercept");
234
222
  }
235
223
  export function when(): never {
236
- throw new Error("when() is server-only and requires RSC context.");
224
+ throw serverOnlyStubError("when");
237
225
  }
238
226
  export function errorBoundary(): never {
239
- throw new Error("errorBoundary() is server-only and requires RSC context.");
227
+ throw serverOnlyStubError("errorBoundary");
240
228
  }
241
229
  export function notFoundBoundary(): never {
242
- throw new Error(
243
- "notFoundBoundary() is server-only and requires RSC context.",
244
- );
230
+ throw serverOnlyStubError("notFoundBoundary");
245
231
  }
246
232
  export function transition(): never {
247
- throw new Error("transition() is server-only and requires RSC context.");
233
+ throw serverOnlyStubError("transition");
248
234
  }
249
235
 
250
236
  // Request context type (safe for client)
@@ -260,14 +246,15 @@ export type {
260
246
  // Meta types
261
247
  export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
262
248
 
249
+ // Breadcrumb types
250
+ export type { BreadcrumbItem } from "./handles/breadcrumbs.js";
251
+
263
252
  // Reverse type utilities for type-safe URL generation (Django-style URL reversal)
264
253
  export type {
265
254
  ScopedReverseFunction,
266
255
  ReverseFunction,
267
256
  ExtractLocalRoutes,
268
257
  ParamsFor,
269
- SanitizePrefix,
270
- MergeRoutes,
271
258
  } from "./reverse.js";
272
259
  // scopedReverse() helper for handlers to get locally-typed reverse
273
260
  export { scopedReverse } from "./reverse.js";
@@ -287,20 +274,7 @@ export type { PathResponse } from "./href-client.js";
287
274
  export { createConsoleSink } from "./router/telemetry.js";
288
275
  export { createOTelSink } from "./router/telemetry-otel.js";
289
276
  export type { OTelTracer, OTelSpan } from "./router/telemetry-otel.js";
290
- export type {
291
- TelemetrySink,
292
- TelemetryEvent,
293
- RequestStartEvent,
294
- RequestEndEvent,
295
- RequestErrorEvent,
296
- RequestTimeoutEvent,
297
- LoaderStartEvent,
298
- LoaderEndEvent,
299
- LoaderErrorEvent,
300
- HandlerErrorEvent,
301
- CacheDecisionEvent,
302
- RevalidationDecisionEvent,
303
- } from "./router/telemetry.js";
277
+ export type { TelemetrySink, TelemetryEvent } from "./router/telemetry.js";
304
278
 
305
279
  // Timeout types and error class
306
280
  export { RouterTimeoutError } from "./router/timeout.js";
@@ -2,9 +2,10 @@
2
2
  * Prerender Store
3
3
  *
4
4
  * Reads pre-rendered segment data from the worker bundle at build time.
5
- * The data is stored as globalThis.__PRERENDER_MANIFEST, a map of
6
- * "<routeName>/<paramHash>" to dynamic import functions that resolve
7
- * individual prerender entry modules.
5
+ * The manifest module is lazily loaded via globalThis.__loadPrerenderManifestModule,
6
+ * a function injected into the RSC entry that returns the manifest module
7
+ * containing a key-to-specifier map and a `loadPrerenderAsset` function
8
+ * that anchors import() resolution relative to the manifest file.
8
9
  */
9
10
 
10
11
  import type {
@@ -34,11 +35,20 @@ export interface StaticStore {
34
35
  get(handlerId: string): Promise<StaticEntry | null>;
35
36
  }
36
37
 
38
+ interface PrerenderManifestModule {
39
+ default: Record<string, string>;
40
+ loadPrerenderAsset: (
41
+ specifier: string,
42
+ ) => Promise<{ default: PrerenderEntry }>;
43
+ }
44
+
37
45
  declare global {
38
- // Injected by closeBundle post-processing: map of key -> () => import("./assets/__pr-*.js")
46
+ // Injected by closeBundle post-processing: lazy loader for the prerender
47
+ // manifest module. The module exports a key→specifier map and a
48
+ // loadPrerenderAsset function that anchors import() relative to the manifest.
39
49
  // eslint-disable-next-line no-var
40
- var __PRERENDER_MANIFEST:
41
- | Record<string, () => Promise<{ default: PrerenderEntry }>>
50
+ var __loadPrerenderManifestModule:
51
+ | (() => Promise<PrerenderManifestModule>)
42
52
  | undefined;
43
53
  // Injected by closeBundle post-processing: map of handlerId -> () => import("./assets/__st-*.js")
44
54
  // Asset default export is either a string (no handles) or { encoded, handles } object.
@@ -78,17 +88,28 @@ export function createDevPrerenderStore(devUrl: string): PrerenderStore {
78
88
  /**
79
89
  * Create a prerender store.
80
90
  * Dev mode: on-demand fetch from Vite dev server (node:fs works there).
81
- * Production: backed by globalThis.__PRERENDER_MANIFEST injected at build time.
91
+ * Production: backed by globalThis.__loadPrerenderManifestModule which lazily
92
+ * loads the manifest module on first access.
82
93
  * Returns null if no prerender data is available.
83
94
  */
84
95
  export function createPrerenderStore(): PrerenderStore | null {
85
96
  if (globalThis.__PRERENDER_DEV_URL) {
86
97
  return createDevPrerenderStore(globalThis.__PRERENDER_DEV_URL);
87
98
  }
88
- const manifest = globalThis.__PRERENDER_MANIFEST;
89
- if (!manifest || Object.keys(manifest).length === 0) return null;
99
+ if (!globalThis.__loadPrerenderManifestModule) return null;
90
100
 
91
101
  const cache = new Map<string, Promise<PrerenderEntry | null>>();
102
+ let manifestModulePromise: Promise<PrerenderManifestModule | null> | null =
103
+ null;
104
+
105
+ function loadManifestModule(): Promise<PrerenderManifestModule | null> {
106
+ if (!manifestModulePromise) {
107
+ manifestModulePromise = globalThis.__loadPrerenderManifestModule!().catch(
108
+ () => null,
109
+ );
110
+ }
111
+ return manifestModulePromise;
112
+ }
92
113
 
93
114
  return {
94
115
  get(routeName: string, paramHash: string): Promise<PrerenderEntry | null> {
@@ -96,18 +117,39 @@ export function createPrerenderStore(): PrerenderStore | null {
96
117
  const cached = cache.get(key);
97
118
  if (cached) return cached;
98
119
 
99
- const loader = manifest[key];
100
- if (!loader) return Promise.resolve(null);
101
-
102
- const promise = loader()
103
- .then((mod) => mod.default)
104
- .catch(() => null);
120
+ const promise = loadManifestModule().then((mod) => {
121
+ if (!mod) return null;
122
+ const specifier = mod.default[key];
123
+ if (!specifier) return null;
124
+ // Let asset load errors propagate — a missing/corrupted artifact
125
+ // for a key that exists in the manifest is a build/deploy error
126
+ // and should surface as a 500, not be silently swallowed as null
127
+ // (which the handler stub would misreport as a 404).
128
+ return mod.loadPrerenderAsset(specifier).then((asset) => asset.default);
129
+ });
105
130
  cache.set(key, promise);
106
131
  return promise;
107
132
  },
108
133
  };
109
134
  }
110
135
 
136
+ /**
137
+ * Load the prerender manifest index for test introspection.
138
+ * Returns the key→specifier map or null if unavailable.
139
+ */
140
+ export async function loadPrerenderManifestIndex(): Promise<Record<
141
+ string,
142
+ string
143
+ > | null> {
144
+ if (!globalThis.__loadPrerenderManifestModule) return null;
145
+ try {
146
+ const mod = await globalThis.__loadPrerenderManifestModule();
147
+ return mod.default;
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+
111
153
  /**
112
154
  * Create a static segment store.
113
155
  * Production only: backed by globalThis.__STATIC_MANIFEST injected at build time.
package/src/prerender.ts CHANGED
@@ -36,6 +36,7 @@ import type { Handle } from "./handle.js";
36
36
  import type { ContextVar } from "./context-var.js";
37
37
  import type { ReverseFunction } from "./reverse.js";
38
38
  import type { DefaultReverseRouteMap } from "./types/global-namespace.js";
39
+ import type { UseItems, HandlerUseItem } from "./route-types.js";
39
40
  import { isCachedFunction } from "./cache/taint.js";
40
41
 
41
42
  // -- Named route resolution types -------------------------------------------
@@ -105,13 +106,6 @@ type ResolvePrerenderParams<
105
106
  // -- Types ------------------------------------------------------------------
106
107
 
107
108
  export interface PrerenderOptions {
108
- /**
109
- * Keep handler in server bundle for live fallback (default: false).
110
- * false: handler replaced with stub, source-only APIs excluded from bundle.
111
- * true: handler stays in bundle, unknown params render live at request time.
112
- */
113
- passthrough?: boolean;
114
-
115
109
  /**
116
110
  * Maximum number of param sets to render in parallel (default: 1).
117
111
  * Only applies to dynamic Prerender handlers with getParams().
@@ -131,8 +125,8 @@ export interface PrerenderOptions {
131
125
 
132
126
  /**
133
127
  * Context passed to Prerender() handlers at build time.
134
- * Has a synthetic URL from getParams, params, and pathname.
135
- * No request, env, headers, cookies.
128
+ * Has a synthetic URL from getParams, params, pathname, and optionally env.
129
+ * No request, headers, cookies.
136
130
  */
137
131
  export interface BuildContext<TParams> {
138
132
  /** Params extracted from the route pattern (populated from getParams). */
@@ -141,6 +135,23 @@ export interface BuildContext<TParams> {
141
135
  /** True during build-time pre-rendering, false during passthrough live render. */
142
136
  build: true;
143
137
 
138
+ /**
139
+ * True when running in Vite dev mode (on-demand prerender), false during
140
+ * production `vite build`. Use this to branch on runtime mode without
141
+ * changing build semantics.
142
+ */
143
+ dev: boolean;
144
+
145
+ /**
146
+ * Build-time environment bindings (KV, D1, etc.) supplied by the Vite plugin.
147
+ * Only available when `buildEnv` is configured in rango() options.
148
+ * Throws with a clear error if not configured.
149
+ *
150
+ * This is NOT the live request env — it is shared across all prerender
151
+ * invocations for the build.
152
+ */
153
+ env: DefaultEnv;
154
+
144
155
  /** Read a variable set by getParams or a parent handler. */
145
156
  get: {
146
157
  <T>(contextVar: ContextVar<T>): T | undefined;
@@ -173,8 +184,8 @@ export interface BuildContext<TParams> {
173
184
 
174
185
  /**
175
186
  * Signal that this param set should not produce a local prerender artifact.
176
- * At runtime the handler runs live instead. Only valid on routes declared
177
- * with `{ passthrough: true }`.
187
+ * At runtime the live handler runs instead. Only valid on routes wrapped
188
+ * with `Passthrough()`.
178
189
  */
179
190
  passthrough: () => PrerenderPassthroughResult;
180
191
  }
@@ -187,6 +198,17 @@ export interface StaticBuildContext {
187
198
  /** Always true for Static handlers at build time. */
188
199
  build: true;
189
200
 
201
+ /**
202
+ * True when running in Vite dev mode, false during production build.
203
+ */
204
+ dev: boolean;
205
+
206
+ /**
207
+ * Build-time environment bindings supplied by the Vite plugin.
208
+ * Only available when `buildEnv` is configured in rango() options.
209
+ */
210
+ env: DefaultEnv;
211
+
190
212
  /** Read a variable (available for type consistency with BuildContext). */
191
213
  get: {
192
214
  <T>(contextVar: ContextVar<T>): T | undefined;
@@ -214,6 +236,17 @@ export interface GetParamsContext {
214
236
  /** Always true during build-time getParams execution. */
215
237
  build: true;
216
238
 
239
+ /**
240
+ * True when running in Vite dev mode, false during production build.
241
+ */
242
+ dev: boolean;
243
+
244
+ /**
245
+ * Build-time environment bindings supplied by the Vite plugin.
246
+ * Only available when `buildEnv` is configured in rango() options.
247
+ */
248
+ env: DefaultEnv;
249
+
217
250
  /** Set a variable that will be available to each handler invocation via ctx.get(). */
218
251
  set: {
219
252
  <T>(contextVar: ContextVar<T>, value: T): void;
@@ -224,23 +257,6 @@ export interface GetParamsContext {
224
257
  reverse: BuildReverseFunction;
225
258
  }
226
259
 
227
- /**
228
- * Context type for passthrough Prerender handlers.
229
- *
230
- * When `passthrough: true`, the handler runs both at build time and at request
231
- * time. The context is a full `HandlerContext` with `build: boolean`:
232
- * - `ctx.build === true`: build-time, env/request/res throw at runtime
233
- * - `ctx.build === false`: live request, full context available
234
- *
235
- * For `passthrough: false` (default), handlers receive `BuildContext` only.
236
- */
237
- export type PrerenderPassthroughContext<
238
- TParams = {},
239
- TEnv = DefaultEnv,
240
- > = HandlerContext<TParams, TEnv> & {
241
- passthrough: () => PrerenderPassthroughResult;
242
- };
243
-
244
260
  export interface PrerenderHandlerDefinition<
245
261
  TParams extends Record<string, any> = any,
246
262
  > {
@@ -253,6 +269,8 @@ export interface PrerenderHandlerDefinition<
253
269
  getParams?: (ctx: GetParamsContext) => Promise<TParams[]> | TParams[];
254
270
  /** Pre-render options. */
255
271
  options?: PrerenderOptions;
272
+ /** Composable default DSL items merged when the handler is mounted. */
273
+ use?: () => UseItems<HandlerUseItem>;
256
274
  }
257
275
 
258
276
  // -- Overloads --------------------------------------------------------------
@@ -263,7 +281,7 @@ export interface PrerenderHandlerDefinition<
263
281
  // Explicit params work as before:
264
282
  // Prerender<{ slug: string }> → params = { slug: string }
265
283
 
266
- // Overload 1: Static handler, no passthrough (build-time only)
284
+ // Overload 1: Static handler (build-time only)
267
285
  export function Prerender<
268
286
  T extends
269
287
  | keyof DefaultPrerenderRouteMap
@@ -273,34 +291,15 @@ export function Prerender<
273
291
  >(
274
292
  handler: (
275
293
  ctx: BuildContext<ResolvePrerenderParams<T, TRouteMap>>,
276
- ) => ReactNode | Promise<ReactNode>,
277
- options?: PrerenderOptions & { passthrough?: false },
278
- __injectedId?: string,
279
- ): PrerenderHandlerDefinition<ResolvePrerenderParams<T, TRouteMap>>;
280
-
281
- // Overload 2: Static handler, passthrough (build + live — full HandlerContext)
282
- export function Prerender<
283
- T extends
284
- | keyof DefaultPrerenderRouteMap
285
- | `.${keyof TRouteMap & string}`
286
- | Record<string, any> = {},
287
- TRouteMap extends {} = DefaultPrerenderRouteMap,
288
- TEnv = DefaultEnv,
289
- >(
290
- handler: (
291
- ctx: PrerenderPassthroughContext<
292
- ResolvePrerenderParams<T, TRouteMap>,
293
- TEnv
294
- >,
295
294
  ) =>
296
295
  | ReactNode
297
296
  | PrerenderPassthroughResult
298
297
  | Promise<ReactNode | PrerenderPassthroughResult>,
299
- options: PrerenderOptions & { passthrough: true },
298
+ options?: PrerenderOptions,
300
299
  __injectedId?: string,
301
300
  ): PrerenderHandlerDefinition<ResolvePrerenderParams<T, TRouteMap>>;
302
301
 
303
- // Overload 3: Dynamic handler, no passthrough (build-time only)
302
+ // Overload 2: Dynamic handler (build-time only)
304
303
  export function Prerender<
305
304
  T extends
306
305
  | keyof DefaultPrerenderRouteMap
@@ -315,35 +314,11 @@ export function Prerender<
315
314
  | ResolvePrerenderParams<T, TRouteMap>[],
316
315
  handler: (
317
316
  ctx: BuildContext<ResolvePrerenderParams<T, TRouteMap>>,
318
- ) => ReactNode | Promise<ReactNode>,
319
- options?: PrerenderOptions & { passthrough?: false },
320
- __injectedId?: string,
321
- ): PrerenderHandlerDefinition<ResolvePrerenderParams<T, TRouteMap>>;
322
-
323
- // Overload 4: Dynamic handler, passthrough (build + live — full HandlerContext)
324
- export function Prerender<
325
- T extends
326
- | keyof DefaultPrerenderRouteMap
327
- | `.${keyof TRouteMap & string}`
328
- | Record<string, any>,
329
- TRouteMap extends {} = DefaultPrerenderRouteMap,
330
- TEnv = DefaultEnv,
331
- >(
332
- getParams: (
333
- ctx: GetParamsContext,
334
- ) =>
335
- | Promise<ResolvePrerenderParams<T, TRouteMap>[]>
336
- | ResolvePrerenderParams<T, TRouteMap>[],
337
- handler: (
338
- ctx: PrerenderPassthroughContext<
339
- ResolvePrerenderParams<T, TRouteMap>,
340
- TEnv
341
- >,
342
317
  ) =>
343
318
  | ReactNode
344
319
  | PrerenderPassthroughResult
345
320
  | Promise<ReactNode | PrerenderPassthroughResult>,
346
- options: PrerenderOptions & { passthrough: true },
321
+ options?: PrerenderOptions,
347
322
  __injectedId?: string,
348
323
  ): PrerenderHandlerDefinition<ResolvePrerenderParams<T, TRouteMap>>;
349
324
 
@@ -422,7 +397,7 @@ export function Prerender<TParams extends Record<string, any>>(
422
397
  /**
423
398
  * Sentinel returned by `ctx.passthrough()` to signal that a specific param set
424
399
  * should not produce a local prerender artifact. The build skips writing the
425
- * entry; at runtime the handler runs live (requires `{ passthrough: true }`).
400
+ * entry; at runtime the Passthrough live handler runs instead.
426
401
  */
427
402
  export const PRERENDER_PASSTHROUGH: Readonly<{
428
403
  __brand: "prerenderPassthrough";
@@ -446,7 +421,7 @@ export function isPrerenderPassthrough(
446
421
  );
447
422
  }
448
423
 
449
- // -- Type guard -------------------------------------------------------------
424
+ // -- Type guards ------------------------------------------------------------
450
425
 
451
426
  /**
452
427
  * Type guard to check if a value is a PrerenderHandlerDefinition.
@@ -461,3 +436,89 @@ export function isPrerenderHandler(
461
436
  (value as { __brand: unknown }).__brand === "prerenderHandler"
462
437
  );
463
438
  }
439
+
440
+ // -- Passthrough wrapper ----------------------------------------------------
441
+
442
+ /**
443
+ * A prerender route with a live fallback handler for unknown params at runtime.
444
+ *
445
+ * Wraps a `Prerender(...)` definition with a separate handler that runs at
446
+ * request time for params not covered by `getParams()`.
447
+ *
448
+ * - Build time: `prerenderDef` provides getParams + build handler.
449
+ * - Runtime: `liveHandler` runs for unknown params with full HandlerContext.
450
+ *
451
+ * @example
452
+ * ```ts
453
+ * const BlogPrerender = Prerender(
454
+ * async () => [{ slug: "getting-started" }, { slug: "api-reference" }],
455
+ * async (ctx) => <BlogPost slug={ctx.params.slug} />,
456
+ * );
457
+ *
458
+ * // In route definition:
459
+ * path("/blog/:slug", Passthrough(BlogPrerender, async (ctx) => {
460
+ * const post = await ctx.env.DB.get(ctx.params.slug);
461
+ * return <BlogPost slug={ctx.params.slug} post={post} />;
462
+ * }))
463
+ * ```
464
+ */
465
+ export interface PassthroughHandlerDefinition<
466
+ TParams extends Record<string, any> = any,
467
+ TEnv = DefaultEnv,
468
+ > {
469
+ readonly __brand: "passthroughHandler";
470
+ /** The underlying prerender definition (build-time rendering). */
471
+ prerenderDef: PrerenderHandlerDefinition<TParams>;
472
+ /** Live handler for runtime fallback on unknown params. */
473
+ liveHandler: (
474
+ ctx: HandlerContext<TParams, TEnv>,
475
+ ) => ReactNode | Promise<ReactNode> | Response | Promise<Response>;
476
+ /** Composable default DSL items merged when the handler is mounted. */
477
+ use?: () => UseItems<HandlerUseItem>;
478
+ }
479
+
480
+ export function Passthrough<
481
+ TParams extends Record<string, any>,
482
+ TEnv = DefaultEnv,
483
+ >(
484
+ prerenderDef: PrerenderHandlerDefinition<TParams>,
485
+ liveHandler: (
486
+ ctx: HandlerContext<TParams, TEnv>,
487
+ ) => ReactNode | Promise<ReactNode> | Response | Promise<Response>,
488
+ ): PassthroughHandlerDefinition<TParams, TEnv>;
489
+
490
+ // Implementation
491
+ export function Passthrough<
492
+ TParams extends Record<string, any>,
493
+ TEnv = DefaultEnv,
494
+ >(
495
+ prerenderDef: PrerenderHandlerDefinition<TParams>,
496
+ liveHandler: (
497
+ ctx: HandlerContext<TParams, TEnv>,
498
+ ) => ReactNode | Promise<ReactNode> | Response | Promise<Response>,
499
+ ): PassthroughHandlerDefinition<TParams, TEnv> {
500
+ if (!isPrerenderHandler(prerenderDef)) {
501
+ throw new Error(
502
+ "[rsc-router] Passthrough: first argument must be a Prerender() definition.",
503
+ );
504
+ }
505
+ return {
506
+ __brand: "passthroughHandler" as const,
507
+ prerenderDef,
508
+ liveHandler,
509
+ };
510
+ }
511
+
512
+ /**
513
+ * Type guard to check if a value is a PassthroughHandlerDefinition.
514
+ */
515
+ export function isPassthroughHandler(
516
+ value: unknown,
517
+ ): value is PassthroughHandlerDefinition {
518
+ return (
519
+ typeof value === "object" &&
520
+ value !== null &&
521
+ "__brand" in value &&
522
+ (value as { __brand: unknown }).__brand === "passthroughHandler"
523
+ );
524
+ }