@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26

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 (225) hide show
  1. package/README.md +294 -28
  2. package/dist/bin/rango.js +355 -47
  3. package/dist/vite/index.js +1658 -1239
  4. package/package.json +3 -3
  5. package/skills/cache-guide/SKILL.md +9 -5
  6. package/skills/caching/SKILL.md +4 -4
  7. package/skills/document-cache/SKILL.md +2 -2
  8. package/skills/hooks/SKILL.md +40 -29
  9. package/skills/host-router/SKILL.md +218 -0
  10. package/skills/intercept/SKILL.md +79 -0
  11. package/skills/layout/SKILL.md +62 -2
  12. package/skills/loader/SKILL.md +229 -15
  13. package/skills/middleware/SKILL.md +109 -30
  14. package/skills/parallel/SKILL.md +57 -2
  15. package/skills/prerender/SKILL.md +189 -19
  16. package/skills/rango/SKILL.md +1 -2
  17. package/skills/response-routes/SKILL.md +3 -3
  18. package/skills/route/SKILL.md +44 -3
  19. package/skills/router-setup/SKILL.md +80 -3
  20. package/skills/theme/SKILL.md +5 -4
  21. package/skills/typesafety/SKILL.md +59 -16
  22. package/skills/use-cache/SKILL.md +16 -2
  23. package/src/__internal.ts +1 -1
  24. package/src/bin/rango.ts +56 -19
  25. package/src/browser/action-coordinator.ts +97 -0
  26. package/src/browser/event-controller.ts +29 -48
  27. package/src/browser/history-state.ts +80 -0
  28. package/src/browser/intercept-utils.ts +1 -1
  29. package/src/browser/link-interceptor.ts +19 -3
  30. package/src/browser/merge-segment-loaders.ts +9 -2
  31. package/src/browser/navigation-bridge.ts +66 -443
  32. package/src/browser/navigation-client.ts +34 -62
  33. package/src/browser/navigation-store.ts +4 -33
  34. package/src/browser/navigation-transaction.ts +295 -0
  35. package/src/browser/partial-update.ts +103 -151
  36. package/src/browser/prefetch/cache.ts +67 -0
  37. package/src/browser/prefetch/fetch.ts +137 -0
  38. package/src/browser/prefetch/observer.ts +65 -0
  39. package/src/browser/prefetch/policy.ts +42 -0
  40. package/src/browser/prefetch/queue.ts +88 -0
  41. package/src/browser/rango-state.ts +112 -0
  42. package/src/browser/react/Link.tsx +154 -44
  43. package/src/browser/react/NavigationProvider.tsx +32 -0
  44. package/src/browser/react/context.ts +6 -0
  45. package/src/browser/react/filter-segment-order.ts +11 -0
  46. package/src/browser/react/index.ts +2 -6
  47. package/src/browser/react/location-state-shared.ts +29 -11
  48. package/src/browser/react/location-state.ts +6 -4
  49. package/src/browser/react/nonce-context.ts +23 -0
  50. package/src/browser/react/shallow-equal.ts +27 -0
  51. package/src/browser/react/use-action.ts +23 -45
  52. package/src/browser/react/use-client-cache.ts +5 -3
  53. package/src/browser/react/use-handle.ts +21 -64
  54. package/src/browser/react/use-navigation.ts +7 -32
  55. package/src/browser/react/use-params.ts +5 -34
  56. package/src/browser/react/use-pathname.ts +2 -3
  57. package/src/browser/react/use-router.ts +3 -6
  58. package/src/browser/react/use-search-params.ts +2 -1
  59. package/src/browser/react/use-segments.ts +75 -114
  60. package/src/browser/response-adapter.ts +73 -0
  61. package/src/browser/rsc-router.tsx +46 -22
  62. package/src/browser/scroll-restoration.ts +10 -7
  63. package/src/browser/server-action-bridge.ts +458 -405
  64. package/src/browser/types.ts +21 -35
  65. package/src/browser/validate-redirect-origin.ts +29 -0
  66. package/src/build/generate-manifest.ts +38 -13
  67. package/src/build/generate-route-types.ts +4 -0
  68. package/src/build/index.ts +1 -0
  69. package/src/build/route-trie.ts +19 -3
  70. package/src/build/route-types/codegen.ts +13 -4
  71. package/src/build/route-types/include-resolution.ts +13 -0
  72. package/src/build/route-types/per-module-writer.ts +15 -3
  73. package/src/build/route-types/router-processing.ts +170 -18
  74. package/src/build/runtime-discovery.ts +13 -1
  75. package/src/cache/background-task.ts +34 -0
  76. package/src/cache/cache-key-utils.ts +44 -0
  77. package/src/cache/cache-policy.ts +125 -0
  78. package/src/cache/cache-runtime.ts +136 -123
  79. package/src/cache/cache-scope.ts +76 -83
  80. package/src/cache/cf/cf-cache-store.ts +12 -7
  81. package/src/cache/document-cache.ts +93 -69
  82. package/src/cache/handle-capture.ts +81 -0
  83. package/src/cache/index.ts +0 -15
  84. package/src/cache/memory-segment-store.ts +43 -69
  85. package/src/cache/profile-registry.ts +43 -8
  86. package/src/cache/read-through-swr.ts +134 -0
  87. package/src/cache/segment-codec.ts +140 -117
  88. package/src/cache/taint.ts +30 -3
  89. package/src/cache/types.ts +1 -115
  90. package/src/client.rsc.tsx +0 -1
  91. package/src/client.tsx +53 -76
  92. package/src/errors.ts +6 -1
  93. package/src/handle.ts +1 -1
  94. package/src/handles/MetaTags.tsx +5 -2
  95. package/src/host/cookie-handler.ts +8 -3
  96. package/src/host/index.ts +0 -3
  97. package/src/host/router.ts +14 -1
  98. package/src/href-client.ts +3 -1
  99. package/src/index.rsc.ts +53 -10
  100. package/src/index.ts +73 -43
  101. package/src/loader.rsc.ts +12 -4
  102. package/src/loader.ts +8 -0
  103. package/src/prerender/store.ts +60 -18
  104. package/src/prerender.ts +76 -18
  105. package/src/reverse.ts +11 -7
  106. package/src/root-error-boundary.tsx +30 -26
  107. package/src/route-definition/dsl-helpers.ts +9 -6
  108. package/src/route-definition/index.ts +0 -3
  109. package/src/route-definition/redirect.ts +15 -3
  110. package/src/route-map-builder.ts +38 -2
  111. package/src/route-name.ts +53 -0
  112. package/src/route-types.ts +7 -0
  113. package/src/router/content-negotiation.ts +1 -1
  114. package/src/router/debug-manifest.ts +16 -3
  115. package/src/router/handler-context.ts +96 -17
  116. package/src/router/intercept-resolution.ts +6 -4
  117. package/src/router/lazy-includes.ts +4 -0
  118. package/src/router/loader-resolution.ts +6 -11
  119. package/src/router/logging.ts +100 -3
  120. package/src/router/manifest.ts +32 -3
  121. package/src/router/match-api.ts +62 -54
  122. package/src/router/match-context.ts +3 -0
  123. package/src/router/match-handlers.ts +185 -11
  124. package/src/router/match-middleware/background-revalidation.ts +65 -85
  125. package/src/router/match-middleware/cache-lookup.ts +78 -10
  126. package/src/router/match-middleware/cache-store.ts +2 -0
  127. package/src/router/match-pipelines.ts +8 -43
  128. package/src/router/match-result.ts +0 -9
  129. package/src/router/metrics.ts +233 -13
  130. package/src/router/middleware-types.ts +34 -39
  131. package/src/router/middleware.ts +290 -130
  132. package/src/router/pattern-matching.ts +61 -10
  133. package/src/router/prerender-match.ts +36 -6
  134. package/src/router/preview-match.ts +7 -1
  135. package/src/router/revalidation.ts +61 -2
  136. package/src/router/router-context.ts +15 -0
  137. package/src/router/router-interfaces.ts +158 -40
  138. package/src/router/router-options.ts +223 -1
  139. package/src/router/router-registry.ts +5 -2
  140. package/src/router/segment-resolution/fresh.ts +165 -242
  141. package/src/router/segment-resolution/helpers.ts +263 -0
  142. package/src/router/segment-resolution/loader-cache.ts +102 -98
  143. package/src/router/segment-resolution/revalidation.ts +394 -272
  144. package/src/router/segment-resolution/static-store.ts +2 -2
  145. package/src/router/segment-resolution.ts +1 -3
  146. package/src/router/segment-wrappers.ts +3 -0
  147. package/src/router/telemetry-otel.ts +299 -0
  148. package/src/router/telemetry.ts +300 -0
  149. package/src/router/timeout.ts +148 -0
  150. package/src/router/trie-matching.ts +20 -2
  151. package/src/router/types.ts +7 -1
  152. package/src/router.ts +203 -18
  153. package/src/rsc/handler-context.ts +13 -2
  154. package/src/rsc/handler.ts +489 -438
  155. package/src/rsc/helpers.ts +125 -5
  156. package/src/rsc/index.ts +0 -20
  157. package/src/rsc/loader-fetch.ts +84 -42
  158. package/src/rsc/manifest-init.ts +3 -2
  159. package/src/rsc/origin-guard.ts +141 -0
  160. package/src/rsc/progressive-enhancement.ts +245 -19
  161. package/src/rsc/response-route-handler.ts +347 -0
  162. package/src/rsc/rsc-rendering.ts +47 -43
  163. package/src/rsc/runtime-warnings.ts +42 -0
  164. package/src/rsc/server-action.ts +166 -66
  165. package/src/rsc/ssr-setup.ts +128 -0
  166. package/src/rsc/types.ts +20 -2
  167. package/src/search-params.ts +38 -23
  168. package/src/server/context.ts +61 -7
  169. package/src/server/cookie-store.ts +190 -0
  170. package/src/server/fetchable-loader-store.ts +11 -6
  171. package/src/server/handle-store.ts +84 -12
  172. package/src/server/loader-registry.ts +11 -46
  173. package/src/server/request-context.ts +275 -49
  174. package/src/server.ts +6 -0
  175. package/src/ssr/index.tsx +67 -28
  176. package/src/static-handler.ts +7 -0
  177. package/src/theme/ThemeProvider.tsx +6 -1
  178. package/src/theme/index.ts +4 -18
  179. package/src/theme/theme-context.ts +1 -28
  180. package/src/theme/theme-script.ts +2 -1
  181. package/src/types/cache-types.ts +6 -1
  182. package/src/types/error-types.ts +3 -0
  183. package/src/types/global-namespace.ts +22 -0
  184. package/src/types/handler-context.ts +103 -16
  185. package/src/types/index.ts +1 -1
  186. package/src/types/loader-types.ts +9 -6
  187. package/src/types/route-config.ts +17 -26
  188. package/src/types/route-entry.ts +28 -0
  189. package/src/types/segments.ts +0 -5
  190. package/src/urls/include-helper.ts +49 -8
  191. package/src/urls/index.ts +1 -0
  192. package/src/urls/path-helper-types.ts +30 -12
  193. package/src/urls/path-helper.ts +17 -2
  194. package/src/urls/pattern-types.ts +21 -1
  195. package/src/urls/response-types.ts +29 -7
  196. package/src/urls/type-extraction.ts +23 -15
  197. package/src/use-loader.tsx +27 -9
  198. package/src/vite/discovery/bundle-postprocess.ts +32 -52
  199. package/src/vite/discovery/discover-routers.ts +52 -26
  200. package/src/vite/discovery/prerender-collection.ts +58 -41
  201. package/src/vite/discovery/route-types-writer.ts +7 -7
  202. package/src/vite/discovery/state.ts +7 -7
  203. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  204. package/src/vite/index.ts +10 -51
  205. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  206. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  207. package/src/vite/plugins/expose-internal-ids.ts +4 -3
  208. package/src/vite/plugins/refresh-cmd.ts +65 -0
  209. package/src/vite/plugins/use-cache-transform.ts +91 -3
  210. package/src/vite/plugins/version-plugin.ts +188 -18
  211. package/src/vite/rango.ts +61 -36
  212. package/src/vite/router-discovery.ts +173 -100
  213. package/src/vite/utils/prerender-utils.ts +81 -0
  214. package/src/vite/utils/shared-utils.ts +19 -9
  215. package/skills/testing/SKILL.md +0 -226
  216. package/src/browser/lru-cache.ts +0 -61
  217. package/src/browser/react/prefetch.ts +0 -27
  218. package/src/browser/request-controller.ts +0 -164
  219. package/src/cache/memory-store.ts +0 -253
  220. package/src/href-context.ts +0 -33
  221. package/src/route-definition/route-function.ts +0 -119
  222. package/src/router.gen.ts +0 -6
  223. package/src/static-handler.gen.ts +0 -5
  224. package/src/urls.gen.ts +0 -8
  225. /package/{CLAUDE.md → AGENTS.md} +0 -0
package/src/index.ts CHANGED
@@ -34,7 +34,6 @@ export {
34
34
  export type {
35
35
  // Configuration types
36
36
  DocumentProps,
37
- RouterEnv,
38
37
  DefaultEnv,
39
38
  RouteDefinition,
40
39
  RouteConfig,
@@ -116,25 +115,32 @@ export type {
116
115
  // Middleware context types
117
116
  export type { MiddlewareContext, CookieOptions } from "./router/middleware.js";
118
117
 
118
+ function serverOnlyStubError(name: string): Error {
119
+ return new Error(
120
+ `${name}() is only available from "@rangojs/router" in a react-server/RSC environment. ` +
121
+ `For client hooks and components, import from "@rangojs/router/client".`,
122
+ );
123
+ }
124
+
119
125
  /**
120
126
  * Error-throwing stub for server-only `urls` function.
121
127
  */
122
128
  export function urls(): never {
123
- throw new Error("urls() is server-only and requires RSC context.");
129
+ throw serverOnlyStubError("urls");
124
130
  }
125
131
 
126
132
  /**
127
133
  * Error-throwing stub for server-only `createRouter` function.
128
134
  */
129
135
  export function createRouter(): never {
130
- throw new Error("createRouter() is server-only and requires RSC context.");
136
+ throw serverOnlyStubError("createRouter");
131
137
  }
132
138
 
133
139
  /**
134
140
  * Error-throwing stub for server-only `redirect` function.
135
141
  */
136
142
  export function redirect(): never {
137
- throw new Error("redirect() is server-only and requires RSC context.");
143
+ throw serverOnlyStubError("redirect");
138
144
  }
139
145
 
140
146
  // Handle API (universal - works on both server and client)
@@ -150,108 +156,105 @@ export { nonce } from "./rsc/nonce.js";
150
156
  * Error-throwing stub for server-only `Prerender` function.
151
157
  */
152
158
  export function Prerender(): never {
153
- throw new Error("Prerender() is server-only and requires RSC context.");
159
+ throw serverOnlyStubError("Prerender");
154
160
  }
155
161
 
156
162
  /**
157
163
  * Error-throwing stub for server-only `Static` function.
158
164
  */
159
165
  export function Static(): never {
160
- throw new Error("Static() is server-only and requires RSC context.");
166
+ throw serverOnlyStubError("Static");
161
167
  }
162
168
 
163
169
  /**
164
170
  * Error-throwing stub for server-only `getRequestContext` function.
165
171
  */
166
172
  export function getRequestContext(): never {
167
- throw new Error(
168
- "getRequestContext() is server-only and requires RSC context.",
169
- );
173
+ throw serverOnlyStubError("getRequestContext");
170
174
  }
171
175
 
172
176
  /**
173
- * Error-throwing stub for server-only `requireRequestContext` function.
177
+ * Error-throwing stub for server-only `cookies` function.
174
178
  */
175
- export function requireRequestContext(): never {
176
- throw new Error(
177
- "requireRequestContext() is server-only and requires RSC context.",
178
- );
179
+ export function cookies(): never {
180
+ throw serverOnlyStubError("cookies");
181
+ }
182
+
183
+ /**
184
+ * Error-throwing stub for server-only `headers` function.
185
+ */
186
+ export function headers(): never {
187
+ throw serverOnlyStubError("headers");
179
188
  }
180
189
 
181
190
  /**
182
191
  * Error-throwing stub for server-only `createReverse` function.
183
192
  */
184
193
  export function createReverse(): never {
185
- throw new Error("createReverse() is server-only and requires RSC context.");
194
+ throw serverOnlyStubError("createReverse");
186
195
  }
187
196
 
188
197
  /**
189
198
  * Error-throwing stub for server-only `enableMatchDebug` function.
190
199
  */
191
200
  export function enableMatchDebug(): never {
192
- throw new Error(
193
- "enableMatchDebug() is server-only and requires RSC context.",
194
- );
201
+ throw serverOnlyStubError("enableMatchDebug");
195
202
  }
196
203
 
197
204
  /**
198
205
  * Error-throwing stub for server-only `getMatchDebugStats` function.
199
206
  */
200
207
  export function getMatchDebugStats(): never {
201
- throw new Error(
202
- "getMatchDebugStats() is server-only and requires RSC context.",
203
- );
204
- }
205
-
206
- /**
207
- * Error-throwing stub for server-only `track` function.
208
- */
209
- export function track(): never {
210
- throw new Error("track() is server-only and requires RSC context.");
208
+ throw serverOnlyStubError("getMatchDebugStats");
211
209
  }
212
210
 
213
211
  // Error-throwing stubs for server-only route helpers
214
212
  export function layout(): never {
215
- throw new Error("layout() is server-only and requires RSC context.");
213
+ throw serverOnlyStubError("layout");
216
214
  }
217
215
  export function cache(): never {
218
- throw new Error("cache() is server-only and requires RSC context.");
216
+ throw serverOnlyStubError("cache");
219
217
  }
220
218
  export function middleware(): never {
221
- throw new Error("middleware() is server-only and requires RSC context.");
219
+ throw serverOnlyStubError("middleware");
222
220
  }
223
221
  export function revalidate(): never {
224
- throw new Error("revalidate() is server-only and requires RSC context.");
222
+ throw serverOnlyStubError("revalidate");
225
223
  }
226
224
  export function loader(): never {
227
- throw new Error("loader() is server-only and requires RSC context.");
225
+ throw serverOnlyStubError("loader");
228
226
  }
229
227
  export function loading(): never {
230
- throw new Error("loading() is server-only and requires RSC context.");
228
+ throw serverOnlyStubError("loading");
231
229
  }
232
230
  export function parallel(): never {
233
- throw new Error("parallel() is server-only and requires RSC context.");
231
+ throw serverOnlyStubError("parallel");
234
232
  }
235
233
  export function intercept(): never {
236
- throw new Error("intercept() is server-only and requires RSC context.");
234
+ throw serverOnlyStubError("intercept");
237
235
  }
238
236
  export function when(): never {
239
- throw new Error("when() is server-only and requires RSC context.");
237
+ throw serverOnlyStubError("when");
240
238
  }
241
239
  export function errorBoundary(): never {
242
- throw new Error("errorBoundary() is server-only and requires RSC context.");
240
+ throw serverOnlyStubError("errorBoundary");
243
241
  }
244
242
  export function notFoundBoundary(): never {
245
- throw new Error(
246
- "notFoundBoundary() is server-only and requires RSC context.",
247
- );
243
+ throw serverOnlyStubError("notFoundBoundary");
248
244
  }
249
245
  export function transition(): never {
250
- throw new Error("transition() is server-only and requires RSC context.");
246
+ throw serverOnlyStubError("transition");
251
247
  }
252
248
 
253
249
  // Request context type (safe for client)
254
- export type { RequestContext } from "./server/request-context.js";
250
+ export type { PublicRequestContext as RequestContext } from "./server/request-context.js";
251
+
252
+ // Cookie store types (safe for client)
253
+ export type {
254
+ CookieStore,
255
+ Cookie,
256
+ ReadonlyHeaders,
257
+ } from "./server/cookie-store.js";
255
258
 
256
259
  // Meta types
257
260
  export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
@@ -278,3 +281,30 @@ export {
278
281
 
279
282
  // Path-based response type lookup from RegisteredRoutes
280
283
  export type { PathResponse } from "./href-client.js";
284
+
285
+ // Telemetry sink
286
+ export { createConsoleSink } from "./router/telemetry.js";
287
+ export { createOTelSink } from "./router/telemetry-otel.js";
288
+ export type { OTelTracer, OTelSpan } from "./router/telemetry-otel.js";
289
+ export type {
290
+ TelemetrySink,
291
+ TelemetryEvent,
292
+ RequestStartEvent,
293
+ RequestEndEvent,
294
+ RequestErrorEvent,
295
+ RequestTimeoutEvent,
296
+ LoaderStartEvent,
297
+ LoaderEndEvent,
298
+ LoaderErrorEvent,
299
+ HandlerErrorEvent,
300
+ CacheDecisionEvent,
301
+ RevalidationDecisionEvent,
302
+ } from "./router/telemetry.js";
303
+
304
+ // Timeout types and error class
305
+ export { RouterTimeoutError } from "./router/timeout.js";
306
+ export type {
307
+ RouterTimeouts,
308
+ TimeoutPhase,
309
+ TimeoutContext,
310
+ } from "./router/timeout.js";
package/src/loader.rsc.ts CHANGED
@@ -52,11 +52,19 @@ export function createLoader<T>(
52
52
  // For fetchable loaders, __injectedId is also passed as a parameter
53
53
  const loaderId = __injectedId || "";
54
54
 
55
- // If not fetchable, store fn in registry and return a plain object.
56
- // Server-side code looks up fn via getFetchableLoader($$id).
55
+ if (!loaderId && process.env.NODE_ENV === "development") {
56
+ throw new Error(
57
+ "[rsc-router] Loader is missing $$id. " +
58
+ "Make sure the exposeInternalIds Vite plugin is enabled and " +
59
+ "the loader is exported with: export const MyLoader = createLoader(...)",
60
+ );
61
+ }
62
+
63
+ // If not fetchable, store fn in registry (for SSR ctx.use() resolution)
64
+ // but mark fetchable=false so the _rsc_loader endpoint rejects it.
57
65
  if (fetchable === undefined) {
58
66
  if (fn && loaderId) {
59
- registerFetchableLoader(loaderId, fn, []);
67
+ registerFetchableLoader(loaderId, fn, [], false);
60
68
  }
61
69
  return {
62
70
  __brand: "loader",
@@ -71,7 +79,7 @@ export function createLoader<T>(
71
79
  // Register the function in the internal registry by $$id (server-side only)
72
80
  // The loader fetch handler looks it up by $$id when load() is called from the client.
73
81
  if (fn && loaderId) {
74
- registerFetchableLoader(loaderId, fn, middleware);
82
+ registerFetchableLoader(loaderId, fn, middleware, true);
75
83
  }
76
84
 
77
85
  return {
package/src/loader.ts CHANGED
@@ -49,6 +49,14 @@ export function createLoader<T>(
49
49
  ): LoaderDefinition<Awaited<T>, Record<string, string | undefined>> {
50
50
  const loaderId = __injectedId || "";
51
51
 
52
+ if (!loaderId && process.env.NODE_ENV === "development") {
53
+ throw new Error(
54
+ "[rsc-router] Loader is missing $$id. " +
55
+ "Make sure the exposeInternalIds Vite plugin is enabled and " +
56
+ "the loader is exported with: export const MyLoader = createLoader(...)",
57
+ );
58
+ }
59
+
52
60
  return {
53
61
  __brand: "loader",
54
62
  $$id: loaderId,
@@ -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 {
@@ -21,7 +22,7 @@ export interface PrerenderStore {
21
22
  get(
22
23
  routeName: string,
23
24
  paramHash: string,
24
- meta?: { pathname: string },
25
+ meta?: { pathname: string; isPassthroughRoute?: boolean },
25
26
  ): PrerenderEntry | null | Promise<PrerenderEntry | null>;
26
27
  }
27
28
 
@@ -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.
@@ -58,11 +68,12 @@ declare global {
58
68
  */
59
69
  export function createDevPrerenderStore(devUrl: string): PrerenderStore {
60
70
  return {
61
- async get(_routeName, paramHash, meta) {
71
+ async get(routeName, paramHash, meta) {
62
72
  if (!meta?.pathname) return null;
63
73
  const isIntercept = paramHash.endsWith("/i");
64
- let url = `${devUrl}/__rsc_prerender?pathname=${encodeURIComponent(meta.pathname)}`;
74
+ let url = `${devUrl}/__rsc_prerender?pathname=${encodeURIComponent(meta.pathname)}&routeName=${encodeURIComponent(routeName)}`;
65
75
  if (isIntercept) url += "&intercept=1";
76
+ if (meta.isPassthroughRoute) url += "&passthrough=1";
66
77
  try {
67
78
  const res = await fetch(url);
68
79
  if (!res.ok) return null;
@@ -77,17 +88,28 @@ export function createDevPrerenderStore(devUrl: string): PrerenderStore {
77
88
  /**
78
89
  * Create a prerender store.
79
90
  * Dev mode: on-demand fetch from Vite dev server (node:fs works there).
80
- * 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.
81
93
  * Returns null if no prerender data is available.
82
94
  */
83
95
  export function createPrerenderStore(): PrerenderStore | null {
84
96
  if (globalThis.__PRERENDER_DEV_URL) {
85
97
  return createDevPrerenderStore(globalThis.__PRERENDER_DEV_URL);
86
98
  }
87
- const manifest = globalThis.__PRERENDER_MANIFEST;
88
- if (!manifest || Object.keys(manifest).length === 0) return null;
99
+ if (!globalThis.__loadPrerenderManifestModule) return null;
89
100
 
90
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
+ }
91
113
 
92
114
  return {
93
115
  get(routeName: string, paramHash: string): Promise<PrerenderEntry | null> {
@@ -95,18 +117,38 @@ export function createPrerenderStore(): PrerenderStore | null {
95
117
  const cached = cache.get(key);
96
118
  if (cached) return cached;
97
119
 
98
- const loader = manifest[key];
99
- if (!loader) return Promise.resolve(null);
100
-
101
- const promise = loader()
102
- .then((mod) => mod.default)
103
- .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
+ return mod
125
+ .loadPrerenderAsset(specifier)
126
+ .then((asset) => asset.default)
127
+ .catch(() => null);
128
+ });
104
129
  cache.set(key, promise);
105
130
  return promise;
106
131
  },
107
132
  };
108
133
  }
109
134
 
135
+ /**
136
+ * Load the prerender manifest index for test introspection.
137
+ * Returns the key→specifier map or null if unavailable.
138
+ */
139
+ export async function loadPrerenderManifestIndex(): Promise<Record<
140
+ string,
141
+ string
142
+ > | null> {
143
+ if (!globalThis.__loadPrerenderManifestModule) return null;
144
+ try {
145
+ const mod = await globalThis.__loadPrerenderManifestModule();
146
+ return mod.default;
147
+ } catch {
148
+ return null;
149
+ }
150
+ }
151
+
110
152
  /**
111
153
  * Create a static segment store.
112
154
  * Production only: backed by globalThis.__STATIC_MANIFEST injected at build time.
package/src/prerender.ts CHANGED
@@ -34,10 +34,36 @@ import type {
34
34
  } from "./types.js";
35
35
  import type { Handle } from "./handle.js";
36
36
  import type { ContextVar } from "./context-var.js";
37
+ import type { ReverseFunction } from "./reverse.js";
38
+ import type { DefaultReverseRouteMap } from "./types/global-namespace.js";
37
39
  import { isCachedFunction } from "./cache/taint.js";
38
40
 
39
41
  // -- Named route resolution types -------------------------------------------
40
42
 
43
+ /**
44
+ * Reverse function for build contexts (BuildContext, StaticBuildContext, GetParamsContext).
45
+ * Global names get full autocomplete and param validation from the generated route map.
46
+ * Local `.name` calls are accepted but not validated (the include() scope is unknown
47
+ * at the type level).
48
+ */
49
+ type BuildReverseFunction = [DefaultReverseRouteMap] extends [
50
+ Record<string, string>,
51
+ ]
52
+ ? // No generated route map — permissive fallback
53
+ (
54
+ name: string,
55
+ params?: Record<string, string>,
56
+ search?: Record<string, unknown>,
57
+ ) => string
58
+ : // Generated route map available — typed globals + permissive locals
59
+ ReverseFunction<DefaultReverseRouteMap> & {
60
+ (
61
+ name: `.${string}`,
62
+ params?: Record<string, string>,
63
+ search?: Record<string, unknown>,
64
+ ): string;
65
+ };
66
+
41
67
  /**
42
68
  * Default route map for Prerender named route resolution.
43
69
  * Uses GeneratedRouteMap (from gen file) to avoid circular dependencies.
@@ -143,11 +169,14 @@ export interface BuildContext<TParams> {
143
169
  search: {};
144
170
 
145
171
  /** URL generation by route name. */
146
- reverse: (
147
- name: string,
148
- params?: Record<string, string>,
149
- search?: Record<string, unknown>,
150
- ) => string;
172
+ reverse: BuildReverseFunction;
173
+
174
+ /**
175
+ * 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 }`.
178
+ */
179
+ passthrough: () => PrerenderPassthroughResult;
151
180
  }
152
181
 
153
182
  /**
@@ -174,11 +203,7 @@ export interface StaticBuildContext {
174
203
  use: <T>(handle: Handle<T>) => (data: T) => void;
175
204
 
176
205
  /** URL generation by route name. */
177
- reverse: (
178
- name: string,
179
- params?: Record<string, string>,
180
- search?: Record<string, unknown>,
181
- ) => string;
206
+ reverse: BuildReverseFunction;
182
207
  }
183
208
 
184
209
  /**
@@ -196,11 +221,7 @@ export interface GetParamsContext {
196
221
  };
197
222
 
198
223
  /** URL generation by route name. */
199
- reverse: (
200
- name: string,
201
- params?: Record<string, string>,
202
- search?: Record<string, unknown>,
203
- ) => string;
224
+ reverse: BuildReverseFunction;
204
225
  }
205
226
 
206
227
  /**
@@ -216,7 +237,9 @@ export interface GetParamsContext {
216
237
  export type PrerenderPassthroughContext<
217
238
  TParams = {},
218
239
  TEnv = DefaultEnv,
219
- > = HandlerContext<TParams, TEnv>;
240
+ > = HandlerContext<TParams, TEnv> & {
241
+ passthrough: () => PrerenderPassthroughResult;
242
+ };
220
243
 
221
244
  export interface PrerenderHandlerDefinition<
222
245
  TParams extends Record<string, any> = any,
@@ -269,7 +292,10 @@ export function Prerender<
269
292
  ResolvePrerenderParams<T, TRouteMap>,
270
293
  TEnv
271
294
  >,
272
- ) => ReactNode | Promise<ReactNode>,
295
+ ) =>
296
+ | ReactNode
297
+ | PrerenderPassthroughResult
298
+ | Promise<ReactNode | PrerenderPassthroughResult>,
273
299
  options: PrerenderOptions & { passthrough: true },
274
300
  __injectedId?: string,
275
301
  ): PrerenderHandlerDefinition<ResolvePrerenderParams<T, TRouteMap>>;
@@ -313,7 +339,10 @@ export function Prerender<
313
339
  ResolvePrerenderParams<T, TRouteMap>,
314
340
  TEnv
315
341
  >,
316
- ) => ReactNode | Promise<ReactNode>,
342
+ ) =>
343
+ | ReactNode
344
+ | PrerenderPassthroughResult
345
+ | Promise<ReactNode | PrerenderPassthroughResult>,
317
346
  options: PrerenderOptions & { passthrough: true },
318
347
  __injectedId?: string,
319
348
  ): PrerenderHandlerDefinition<ResolvePrerenderParams<T, TRouteMap>>;
@@ -388,6 +417,35 @@ export function Prerender<TParams extends Record<string, any>>(
388
417
  };
389
418
  }
390
419
 
420
+ // -- Passthrough sentinel ---------------------------------------------------
421
+
422
+ /**
423
+ * Sentinel returned by `ctx.passthrough()` to signal that a specific param set
424
+ * should not produce a local prerender artifact. The build skips writing the
425
+ * entry; at runtime the handler runs live (requires `{ passthrough: true }`).
426
+ */
427
+ export const PRERENDER_PASSTHROUGH: Readonly<{
428
+ __brand: "prerenderPassthrough";
429
+ }> = Object.freeze({
430
+ __brand: "prerenderPassthrough" as const,
431
+ });
432
+
433
+ export type PrerenderPassthroughResult = typeof PRERENDER_PASSTHROUGH;
434
+
435
+ /**
436
+ * Type guard to check if a value is the passthrough sentinel.
437
+ */
438
+ export function isPrerenderPassthrough(
439
+ value: unknown,
440
+ ): value is PrerenderPassthroughResult {
441
+ return (
442
+ typeof value === "object" &&
443
+ value !== null &&
444
+ "__brand" in value &&
445
+ (value as { __brand: unknown }).__brand === "prerenderPassthrough"
446
+ );
447
+ }
448
+
391
449
  // -- Type guard -------------------------------------------------------------
392
450
 
393
451
  /**
package/src/reverse.ts CHANGED
@@ -304,13 +304,17 @@ export function createReverse<TRoutes extends Record<string, string>>(
304
304
  let result = pattern;
305
305
  if (params) {
306
306
  // Replace :param placeholders with actual values
307
- result = result.replace(/:([^/]+)/g, (_: string, key: string) => {
308
- const value = params[key];
309
- if (value === undefined) {
310
- throw new Error(`Missing param "${key}" for route "${name}"`);
311
- }
312
- return encodeURIComponent(value);
313
- });
307
+ // Strip constraint syntax: :param(a|b) -> use "param" as key
308
+ result = result.replace(
309
+ /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?\??/g,
310
+ (_, key) => {
311
+ const value = params[key];
312
+ if (value === undefined) {
313
+ throw new Error(`Missing param "${key}" for route "${name}"`);
314
+ }
315
+ return encodeURIComponent(value);
316
+ },
317
+ );
314
318
  }
315
319
 
316
320
  // Append search params as query string
@@ -109,6 +109,8 @@ function RootErrorFallback({
109
109
  error,
110
110
  reset,
111
111
  }: ClientErrorBoundaryFallbackProps): ReactNode {
112
+ const isDev = process.env.NODE_ENV !== "production";
113
+
112
114
  return (
113
115
  <div
114
116
  style={{
@@ -135,38 +137,40 @@ function RootErrorFallback({
135
137
  >
136
138
  An unexpected error occurred while processing your request.
137
139
  </p>
138
- <div
139
- style={{
140
- background: "#fef2f2",
141
- border: "1px solid #fecaca",
142
- borderRadius: "0.5rem",
143
- padding: "1rem",
144
- marginBottom: "1rem",
145
- }}
146
- >
147
- <p
140
+ {isDev && (
141
+ <div
148
142
  style={{
149
- fontWeight: 600,
150
- color: "#991b1b",
151
- marginBottom: "0.5rem",
143
+ background: "#fef2f2",
144
+ border: "1px solid #fecaca",
145
+ borderRadius: "0.5rem",
146
+ padding: "1rem",
147
+ marginBottom: "1rem",
152
148
  }}
153
149
  >
154
- {error.name}: {error.message}
155
- </p>
156
- {error.stack && (
157
- <pre
150
+ <p
158
151
  style={{
159
- fontSize: "0.75rem",
160
- color: "#6b7280",
161
- overflow: "auto",
162
- whiteSpace: "pre-wrap",
163
- wordBreak: "break-word",
152
+ fontWeight: 600,
153
+ color: "#991b1b",
154
+ marginBottom: "0.5rem",
164
155
  }}
165
156
  >
166
- {error.stack}
167
- </pre>
168
- )}
169
- </div>
157
+ {error.name}: {error.message}
158
+ </p>
159
+ {error.stack && (
160
+ <pre
161
+ style={{
162
+ fontSize: "0.75rem",
163
+ color: "#6b7280",
164
+ overflow: "auto",
165
+ whiteSpace: "pre-wrap",
166
+ wordBreak: "break-word",
167
+ }}
168
+ >
169
+ {error.stack}
170
+ </pre>
171
+ )}
172
+ </div>
173
+ )}
170
174
  <div style={{ display: "flex", gap: "1rem" }}>
171
175
  <button
172
176
  type="button"