@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
@@ -0,0 +1,71 @@
1
+ import type { UrlPatterns } from "./pattern-types.js";
2
+
3
+ /**
4
+ * What an async `include()` provider may resolve to: a `urls()` value directly,
5
+ * or a module namespace whose `default` export is a `urls()` value (the shape
6
+ * produced by `() => import("./routes")` when the route module does
7
+ * `export default urls(...)`).
8
+ */
9
+ export type IncludeModule<TEnv = any> =
10
+ | UrlPatterns<TEnv>
11
+ | { default: UrlPatterns<TEnv> };
12
+
13
+ /**
14
+ * An async/lazy include provider: a thunk returning a `urls()` value (or a
15
+ * Promise of one). The thunk is stored unevaluated by `include()` and called
16
+ * once, on the first request that matches the prefix — so the route module and
17
+ * its (code-split) subtree are not evaluated at startup.
18
+ *
19
+ * Forward-compatible: `() => import("./routes")` today (async, separate chunk);
20
+ * `() => m.routes` with native `import defer` later (sync, deferred eval).
21
+ */
22
+ export type IncludeProvider<TEnv = any> = () =>
23
+ | IncludeModule<TEnv>
24
+ | Promise<IncludeModule<TEnv>>;
25
+
26
+ /** True when the include() argument is a provider thunk rather than a value. */
27
+ export function isIncludeProvider(value: unknown): value is IncludeProvider {
28
+ // A `urls()` value is a (branded) object; a provider is a function.
29
+ return typeof value === "function";
30
+ }
31
+
32
+ /** A `urls()` value is an object exposing a synchronous `handler()`. */
33
+ function isUrlPatterns(value: unknown): value is UrlPatterns {
34
+ return (
35
+ !!value && typeof (value as { handler?: unknown }).handler === "function"
36
+ );
37
+ }
38
+
39
+ /**
40
+ * Normalize an async provider's resolved value to a `UrlPatterns`. Accepts a
41
+ * `urls()` value directly or a module whose `default` export is one.
42
+ */
43
+ export function resolveIncludeModule<TEnv = any>(
44
+ mod: IncludeModule<TEnv>,
45
+ id?: string,
46
+ ): UrlPatterns<TEnv> {
47
+ // Prefer an explicit `default` export (the `export default urls(...)`
48
+ // convention) BEFORE duck-typing the namespace. isUrlPatterns() keys on a
49
+ // `.handler` function, but a routes module can legitimately carry a NAMED
50
+ // `export function handler(...)` alongside its `export default urls(...)`;
51
+ // checking the namespace first would then misidentify the whole module as the
52
+ // urls() value and invoke the user's helper as the DSL handler (the group
53
+ // 404s with a misleading error). A bare `() => urls(...)` provider (no
54
+ // module) has no `default`, so it still resolves via the mod-as-value branch.
55
+ const def = (mod as { default?: unknown })?.default;
56
+ if (isUrlPatterns(def)) return def as UrlPatterns<TEnv>;
57
+ if (isUrlPatterns(mod)) return mod as UrlPatterns<TEnv>;
58
+ // The common failure is a module namespace whose `default` is missing or not a
59
+ // urls() value (e.g. only named exports); `typeof` alone says "object" and
60
+ // hides that, so name the keys present. "provider" (not "async provider") —
61
+ // synchronous providers are supported (see IncludeProvider).
62
+ const got =
63
+ mod && typeof mod === "object"
64
+ ? `a module with keys [${Object.keys(mod).join(", ") || "none"}] but no valid \`default\``
65
+ : typeof mod;
66
+ throw new Error(
67
+ `[@rangojs/router] include() provider${id ? ` for "${id}"` : ""} must ` +
68
+ `resolve to a urls() value — either returned directly or as the module's ` +
69
+ `\`default\` export (e.g. \`export default urls(...)\`). Got ${got}.`,
70
+ );
71
+ }
package/src/urls/index.ts CHANGED
@@ -11,6 +11,7 @@ export type {
11
11
  UnnamedRoute,
12
12
  LocalOnlyInclude,
13
13
  PathOptions,
14
+ PartialPrerenderProps,
14
15
  UrlPatterns,
15
16
  IncludeOptions,
16
17
  } from "./pattern-types.js";
@@ -28,7 +28,6 @@ import type {
28
28
  ParallelUseItem,
29
29
  InterceptUseItem,
30
30
  LoaderUseItem,
31
- WhenItem,
32
31
  TypedCacheItem,
33
32
  TransitionItem,
34
33
  TypedTransitionItem,
@@ -42,7 +41,6 @@ import type {
42
41
  PassthroughHandlerDefinition,
43
42
  } from "../prerender.js";
44
43
  import type { StaticHandlerDefinition } from "../static-handler.js";
45
- import type { InterceptWhenFn } from "../server/context";
46
44
  import type {
47
45
  ResponseHandler,
48
46
  ResponseHandlerContext,
@@ -154,6 +152,30 @@ export type TextResponsePathFn<TEnv> = <
154
152
  use?: () => UseItems<ResponseRouteUseItem>,
155
153
  ) => TypedRouteItem<TName, TPattern, string, TSearch>;
156
154
 
155
+ /**
156
+ * What an async include() provider resolves to. Route types (`TRoutes`) are
157
+ * inferred from the resolved `urls()` value so `href()` and named routes stay
158
+ * type-safe through a code-split module (`() => import("./routes")`).
159
+ */
160
+ type IncludeResolved<
161
+ TEnv,
162
+ TRoutes extends Record<string, any>,
163
+ TResponses extends Record<string, unknown>,
164
+ > =
165
+ | UrlPatterns<TEnv, TRoutes, TResponses>
166
+ | { default: UrlPatterns<TEnv, TRoutes, TResponses> };
167
+
168
+ /** include() argument: an eager `urls()` value or an async provider thunk. */
169
+ export type IncludeArg<
170
+ TEnv,
171
+ TRoutes extends Record<string, any>,
172
+ TResponses extends Record<string, unknown>,
173
+ > =
174
+ | UrlPatterns<TEnv, TRoutes, TResponses>
175
+ | (() =>
176
+ | IncludeResolved<TEnv, TRoutes, TResponses>
177
+ | Promise<IncludeResolved<TEnv, TRoutes, TResponses>>);
178
+
157
179
  /**
158
180
  * Base include function signature.
159
181
  */
@@ -164,7 +186,7 @@ export type IncludeFn<TEnv> = <
164
186
  TResponses extends Record<string, unknown> = Record<string, unknown>,
165
187
  >(
166
188
  prefix: TUrlPrefix,
167
- patterns: UrlPatterns<TEnv, TRoutes, TResponses>,
189
+ patterns: IncludeArg<TEnv, TRoutes, TResponses>,
168
190
  options?: IncludeOptions<TNamePrefix>,
169
191
  ) => TypedIncludeItem<TRoutes, TNamePrefix, TUrlPrefix, TResponses>;
170
192
 
@@ -246,9 +268,16 @@ export type PathHelpers<TEnv> = {
246
268
  * `{ handler, use? }` whose `use` is scoped to that slot only. Per-slot
247
269
  * merge order is `handler.use` → shared `use` → slot-local `use`, with
248
270
  * narrowest scope winning for last-write-wins items like `loading()`.
271
+ *
272
+ * Not generic over the slots record: an inferred type parameter makes the
273
+ * object literal an inference site, which suppresses contextual typing of
274
+ * arrow slot handlers (`(ctx) => ...` was implicit any). Bare handlers infer
275
+ * now; a descriptor's `handler:` arrow still needs an explicit ctx annotation
276
+ * because StaticHandlerDefinition's own `.handler` joins the contextual union
277
+ * (two callables — see parallel-slot-handler-types.test.ts).
249
278
  */
250
- parallel: <
251
- TSlots extends Record<
279
+ parallel: (
280
+ slots: Record<
252
281
  `@${string}`,
253
282
  | Handler<any, any, TEnv>
254
283
  | ReactNode
@@ -261,8 +290,6 @@ export type PathHelpers<TEnv> = {
261
290
  use?: () => ParallelUseItem[];
262
291
  }
263
292
  >,
264
- >(
265
- slots: TSlots,
266
293
  use?: () => ParallelUseItem[],
267
294
  ) => ParallelItem;
268
295
 
@@ -275,12 +302,18 @@ export type PathHelpers<TEnv> = {
275
302
  slotName: `@${string}`,
276
303
  routeName: string,
277
304
  handler: ReactNode | Handler<any, any, TEnv>,
305
+ config?:
306
+ | import("../server/context.js").InterceptConfig<TEnv>
307
+ | (() => InterceptUseItem[]),
278
308
  use?: () => InterceptUseItem[],
279
309
  ) => InterceptItem
280
310
  : (
281
311
  slotName: `@${string}`,
282
312
  routeName: (keyof Rango.GeneratedRouteMap & string) | `.${string}`,
283
313
  handler: ReactNode | Handler<any, any, TEnv>,
314
+ config?:
315
+ | import("../server/context.js").InterceptConfig<TEnv>
316
+ | (() => InterceptUseItem[]),
284
317
  use?: () => InterceptUseItem[],
285
318
  ) => InterceptItem;
286
319
 
@@ -335,11 +368,6 @@ export type PathHelpers<TEnv> = {
335
368
  fallback: ReactNode | NotFoundBoundaryHandler,
336
369
  ) => NotFoundBoundaryItem;
337
370
 
338
- /**
339
- * Define a condition for when an intercept should activate
340
- */
341
- when: (fn: InterceptWhenFn) => WhenItem;
342
-
343
371
  /**
344
372
  * Define cache configuration for segments
345
373
  */
@@ -365,6 +393,10 @@ export type PathHelpers<TEnv> = {
365
393
  * `{ viewTransition: false }` to keep #1 without the router boundary. A view
366
394
  * transition cannot fire without a startTransition. See
367
395
  * skills/view-transitions for the startTransition x ViewTransition matrix.
396
+ *
397
+ * Pass `when: (ctx) => boolean` to gate the transition per request: it runs
398
+ * server-side after the route handler (can read `ctx.get(...)`), and returning
399
+ * false drops the transition so the navigation streams its loading() skeleton.
368
400
  */
369
401
  transition: {
370
402
  (): TransitionItem;
@@ -175,6 +175,11 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
175
175
  ...(resolveResponseType(options)
176
176
  ? { responseType: resolveResponseType(options) }
177
177
  : {}),
178
+ // PPR shell-caching opt-in (document-level). Stored raw; the integrated
179
+ // serve path normalizes it via resolvePprConfig (rsc/shell-serve.ts).
180
+ ...(options?.ppr !== undefined && options.ppr !== false
181
+ ? { ppr: options.ppr }
182
+ : {}),
178
183
  };
179
184
 
180
185
  if (isStaticHandler(handler) && handler.$$id && ctx.namePrefix) {
@@ -35,12 +35,48 @@ export type LocalOnlyInclude = string & { [LOCAL_ONLY_BRAND]: void };
35
35
  /**
36
36
  * Options for path() function
37
37
  */
38
+ /**
39
+ * Options for the `ppr` path option (PPR shell caching — Axis 2, see
40
+ * docs/design/ppr-shell-resume.md and the /ppr skill). Declaring
41
+ * `ppr: true | PartialPrerenderProps` on a page route opts that DOCUMENT into
42
+ * shell capture: the rendered HTML shell (everything that is not a live hole) is
43
+ * cached and, on a later GET, flushed immediately while fizz resumes only the
44
+ * holes. Serving is integral to the router — there is no middleware to mount;
45
+ * the shell store is the app-level `createRouter({ cache })` store (which must
46
+ * implement the `getShell`/`putShell` family).
47
+ */
48
+ export interface PartialPrerenderProps {
49
+ /**
50
+ * Shell time-to-live in seconds. Defaults to 300 (`ppr: true` uses the same
51
+ * default).
52
+ */
53
+ ttl?: number;
54
+ /**
55
+ * Stale-while-revalidate window in seconds: a stale shell is still served
56
+ * while a background recapture refreshes it.
57
+ */
58
+ swr?: number;
59
+ /**
60
+ * Operational tags attached to the captured shell entry for
61
+ * `updateTag()`/`revalidateTag()`-driven eviction. UNIONED with the tags the
62
+ * capture render auto-collects (the shell's own non-loader request tags).
63
+ */
64
+ tags?: string[];
65
+ }
66
+
38
67
  export interface PathOptions<
39
68
  TName extends string = string,
40
69
  TSearch extends SearchSchema = {},
41
70
  > {
42
71
  /** Route name for href() lookups */
43
72
  name?: TName;
73
+ /**
74
+ * PPR shell caching opt-in for this page route (document-level). `true` uses
75
+ * the default policy (ttl 300); an object sets ttl/swr/tags. See
76
+ * {@link PartialPrerenderProps}. Routes without this option are pure axis 1 —
77
+ * no capture, no store reads, no logs.
78
+ */
79
+ ppr?: boolean | PartialPrerenderProps;
44
80
  /** Search param schema for typed query parameters */
45
81
  search?: TSearch;
46
82
  /** Trailing slash behavior: "never" (redirect /path/ to /path), "always" (redirect /path to /path/), "ignore" (match both) */
@@ -81,16 +81,26 @@ type ExtractRoutesFromItem<T> =
81
81
  // When search schema is non-empty, value becomes { path, search } object
82
82
  T extends TypedRouteItem<infer TName, infer TPattern, any, infer TSearch>
83
83
  ? TName extends string
84
- ? TName extends UnnamedRoute
85
- ? {} // Exclude unnamed routes from type map
86
- : {} extends TSearch
87
- ? { [K in TName]: TPattern }
88
- : {
89
- [K in TName]: {
90
- readonly path: TPattern;
91
- readonly search: TSearch;
92
- };
93
- }
84
+ ? // Widened-name guard (#642): some name-less call forms — notably the
85
+ // 3-arg children-fn overload path(pattern, component, () => [...]) —
86
+ // let TName infer to the bare `string` constraint instead of the
87
+ // UnnamedRoute sentinel, because the children-fn argument structurally
88
+ // satisfies the all-optional PathOptions<TName> union member. Mapping
89
+ // over a bare `string` key would emit `{ [K in string]: TPattern }`, an
90
+ // index signature that poisons the whole sibling map (Rango.Path
91
+ // collapses to never). Treat an unresolved name as unnamed.
92
+ string extends TName
93
+ ? {}
94
+ : TName extends UnnamedRoute
95
+ ? {} // Exclude unnamed routes from type map
96
+ : {} extends TSearch
97
+ ? { [K in TName]: TPattern }
98
+ : {
99
+ [K in TName]: {
100
+ readonly path: TPattern;
101
+ readonly search: TSearch;
102
+ };
103
+ }
94
104
  : {}
95
105
  : // TypedIncludeItem: extract prefixed routes (both name and URL prefix)
96
106
  T extends TypedIncludeItem<
@@ -128,9 +138,15 @@ type ExtractRoutesFromItems<T extends readonly any[]> = T extends readonly any[]
128
138
  ? UnionToIntersection<
129
139
  { [K in keyof T]: ExtractRoutesFromItem<T[K]> }[number]
130
140
  > extends infer R
131
- ? R extends Record<string, any>
132
- ? R
133
- : {}
141
+ ? // Blast-radius guard: never let a single malformed item collapse the
142
+ // whole map. A `never` intersection satisfies `extends Record<string,any>`
143
+ // (never extends everything), so check it explicitly first and fall back
144
+ // to `{}` rather than propagating `never`. See #642.
145
+ [R] extends [never]
146
+ ? {}
147
+ : R extends Record<string, any>
148
+ ? R
149
+ : {}
134
150
  : {}
135
151
  : {};
136
152
 
@@ -169,9 +185,15 @@ type PrefixKeys<
169
185
  type ExtractResponsesFromItem<T> =
170
186
  T extends TypedRouteItem<infer TName, any, infer TData>
171
187
  ? TName extends string
172
- ? TName extends UnnamedRoute
188
+ ? // Widened-name guard (#642), parallels ExtractRoutesFromItem. A name-less
189
+ // children-fn path.json(pattern, handler, () => [...]) infers TName as
190
+ // bare `string`; without this the response map picks up an index
191
+ // signature { [K in string]: TData } that wipes named siblings.
192
+ string extends TName
173
193
  ? {}
174
- : { [K in TName]: TData }
194
+ : TName extends UnnamedRoute
195
+ ? {}
196
+ : { [K in TName]: TData }
175
197
  : {}
176
198
  : T extends TypedIncludeItem<any, infer TNamePrefix, any, infer TResponses>
177
199
  ? TNamePrefix extends LocalOnlyInclude
@@ -206,9 +228,12 @@ type ExtractResponsesFromItems<T extends readonly any[]> =
206
228
  ? UnionToIntersection<
207
229
  { [K in keyof T]: ExtractResponsesFromItem<T[K]> }[number]
208
230
  > extends infer R
209
- ? R extends Record<string, unknown>
210
- ? R
211
- : {}
231
+ ? // Blast-radius guard (parallels ExtractRoutesFromItems). See #642.
232
+ [R] extends [never]
233
+ ? {}
234
+ : R extends Record<string, unknown>
235
+ ? R
236
+ : {}
212
237
  : {}
213
238
  : {};
214
239
 
@@ -58,7 +58,6 @@ export function urls<
58
58
  loading: baseHelpers.loading,
59
59
  errorBoundary: baseHelpers.errorBoundary,
60
60
  notFoundBoundary: baseHelpers.notFoundBoundary,
61
- when: baseHelpers.when,
62
61
  cache: baseHelpers.cache as PathHelpers<TEnv>["cache"],
63
62
  transition: baseHelpers.transition as PathHelpers<TEnv>["transition"],
64
63
  };
@@ -44,15 +44,15 @@ import {
44
44
  } from "../router/telemetry-otel.js";
45
45
  import type {
46
46
  RouterTracingConfig,
47
- TracePhaseToggles,
47
+ TracingToggleOptions,
48
48
  } from "../router/tracing.js";
49
49
 
50
- /** Options for createVercelTracing. */
51
- export interface VercelTracingOptions {
52
- /** Master switch. Defaults to true. */
53
- enabled?: boolean;
54
- /** Per-phase span toggles. Omitted phases default to enabled. */
55
- spans?: TracePhaseToggles;
50
+ /**
51
+ * Options for createVercelTracing. Extends the shared
52
+ * {@link TracingToggleOptions} (`enabled` + per-phase `spans`) with the
53
+ * Vercel-specific tracer selectors.
54
+ */
55
+ export interface VercelTracingOptions extends TracingToggleOptions {
56
56
  /**
57
57
  * OTel instrumentation-scope name passed to `trace.getTracer()`. Defaults to
58
58
  * `"rango"`. Ignored when `tracer` is provided.
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Dev prerender result cache — memoizes /__rsc_prerender responses between
3
+ * HMR edits (issue #654).
4
+ *
5
+ * Freshness invariant (why identity keying is correct): the endpoint
6
+ * re-imports the user's entry module through a Vite module runner on every
7
+ * request. When ANY module in the entry → router → urls → handler chain was
8
+ * invalidated (file edit propagates up the importer graph), that import
9
+ * re-executes the entry, `createRouter()` re-runs, and `RouterRegistry.set`
10
+ * (router.ts) replaces the router instance with a NEW object. When nothing
11
+ * was invalidated, the import is a module-cache hit and the instance is the
12
+ * SAME object. Router-instance identity therefore IS the HMR generation:
13
+ * keying the cache on it gives "serve cached until an edit invalidates the
14
+ * chain" without any watcher bookkeeping, and the WeakMap lets every stale
15
+ * generation's bucket be garbage-collected the moment the registry drops
16
+ * the old instance.
17
+ *
18
+ * Staleness envelope: a cached body can only go stale through NON-module
19
+ * inputs (a getParams() reading a database, fs content outside the module
20
+ * graph). Production freezes those at build time, so serving them frozen
21
+ * between edits makes dev MORE faithful to production, not less — the
22
+ * pre-#654 behavior of re-rendering per request was the anomaly.
23
+ */
24
+
25
+ import type { SerializedSegmentData } from "../../cache/types.js";
26
+
27
+ /** Structural slice of `matchForPrerender`'s result the payload derivation needs. */
28
+ export interface DevPrerenderMatchPayloadSource {
29
+ segments: SerializedSegmentData[];
30
+ handles: string;
31
+ interceptSegments?: SerializedSegmentData[];
32
+ interceptHandles?: string;
33
+ }
34
+
35
+ /** Serialized response bodies for the two endpoint variants of one render. */
36
+ export interface DevPrerenderPayloadBodies {
37
+ /** `intercept != 1` response: the route's own segments + handles. */
38
+ main: string;
39
+ /**
40
+ * `intercept=1` response: main + intercept segments with the MERGED handle
41
+ * string. Falls back to `main` byte-for-byte when the route has no
42
+ * intercept segments — mirroring the endpoint's historical behavior.
43
+ */
44
+ intercept: string;
45
+ }
46
+
47
+ export interface DevPrerenderCache {
48
+ get(router: object, key: string): string | undefined;
49
+ set(router: object, key: string, body: string): void;
50
+ }
51
+
52
+ /**
53
+ * Derive both variant bodies from one matchForPrerender result, so a single
54
+ * render warms the main AND intercept cache keys (matchForPrerender computes
55
+ * intercept segments unconditionally either way — see prerender-match.ts
56
+ * step 14).
57
+ */
58
+ export function payloadBodiesFromResult(
59
+ result: DevPrerenderMatchPayloadSource,
60
+ ): DevPrerenderPayloadBodies {
61
+ const main = JSON.stringify({
62
+ segments: result.segments,
63
+ handles: result.handles,
64
+ });
65
+ const intercept = result.interceptSegments?.length
66
+ ? JSON.stringify({
67
+ segments: [...result.segments, ...result.interceptSegments],
68
+ handles: result.interceptHandles ?? "",
69
+ })
70
+ : main;
71
+ return { main, intercept };
72
+ }
73
+
74
+ /**
75
+ * Cache key for one endpoint request shape. `pathname` and `routeName` come
76
+ * straight from the query; `passthrough` selects the getParams-checked match
77
+ * path; `intercept` selects which variant body is served. All four dimensions
78
+ * change the response, so all four are in the key.
79
+ */
80
+ export function devPrerenderCacheKey(
81
+ pathname: string,
82
+ opts: { intercept: boolean; passthrough: boolean; routeName: string | null },
83
+ ): string {
84
+ return `${pathname}|i=${opts.intercept ? 1 : 0}|p=${opts.passthrough ? 1 : 0}|r=${opts.routeName ?? ""}`;
85
+ }
86
+
87
+ /**
88
+ * Per-router FIFO cap: a dev session that visits an unbounded set of
89
+ * prerendered pathnames (e.g. crawling param routes) must not grow one
90
+ * bucket without limit. 500 rendered payloads is far beyond any interactive
91
+ * session; eviction order does not matter for correctness (a miss just
92
+ * re-renders).
93
+ */
94
+ const MAX_ENTRIES_PER_ROUTER = 500;
95
+
96
+ export function createDevPrerenderCache(
97
+ maxEntriesPerRouter: number = MAX_ENTRIES_PER_ROUTER,
98
+ ): DevPrerenderCache {
99
+ const buckets = new WeakMap<object, Map<string, string>>();
100
+ return {
101
+ get(router: object, key: string): string | undefined {
102
+ return buckets.get(router)?.get(key);
103
+ },
104
+ set(router: object, key: string, body: string): void {
105
+ let bucket = buckets.get(router);
106
+ if (!bucket) {
107
+ bucket = new Map();
108
+ buckets.set(router, bucket);
109
+ }
110
+ if (!bucket.has(key) && bucket.size >= maxEntriesPerRouter) {
111
+ const oldest = bucket.keys().next().value;
112
+ if (oldest !== undefined) bucket.delete(oldest);
113
+ }
114
+ bucket.set(key, body);
115
+ },
116
+ };
117
+ }
@@ -186,7 +186,7 @@ export async function discoverRouters(
186
186
  continue;
187
187
  }
188
188
 
189
- const manifest = generateManifestFull(
189
+ const manifest = await generateManifestFull(
190
190
  router.urlpatterns,
191
191
  routerMountIndex,
192
192
  {
@@ -192,3 +192,64 @@ export class DiscoveryError extends Error {
192
192
  Object.setPrototypeOf(this, DiscoveryError.prototype);
193
193
  }
194
194
  }
195
+
196
+ /** How the dev caller should surface a discovery failure. */
197
+ export interface DiscoveryFailureReport {
198
+ level: "error" | "warn";
199
+ message: string;
200
+ }
201
+
202
+ /**
203
+ * Decide how to surface a dev-boot discovery failure in the terminal.
204
+ *
205
+ * The bare "no routers found" case (a DiscoveryError with no caught host-handler
206
+ * failures) is ambiguous. It is either:
207
+ * - a genuine misconfiguration — the entry never calls createRouter(), or the
208
+ * configured entry path is wrong; or
209
+ * - a transient artifact of a Vite dependency re-optimization racing with boot
210
+ * discovery (a module read before the entry import resolves to the
211
+ * pre-optimize copy of the runner graph while createRouter() populated the
212
+ * post-optimize copy — see router-discovery.ts).
213
+ *
214
+ * We can only tell them apart when the caller observed the dep optimizer's
215
+ * `browserHash` change across the discovery attempt (`reoptimizeObserved`): a
216
+ * reload-causing re-optimization landed mid-flight, so the empty read was
217
+ * transient and the app self-heals per-request (handler.ts builds the trie from
218
+ * the router's live urlpatterns) with discovery re-running on the next boot.
219
+ *
220
+ * A DiscoveryError that DOES carry caught host-handler failures already embeds
221
+ * the real cause in its message, and any non-DiscoveryError is a hard failure;
222
+ * both stay loud with full detail.
223
+ */
224
+ export function describeDiscoveryFailure(
225
+ err: unknown,
226
+ opts: { reoptimizeObserved?: boolean } = {},
227
+ ): DiscoveryFailureReport {
228
+ if (err instanceof DiscoveryError && err.caught.length === 0) {
229
+ const entry = err.entryPath ?? "the router entry";
230
+ if (opts.reoptimizeObserved) {
231
+ return {
232
+ level: "warn",
233
+ message:
234
+ `[rango] No routers found while Vite was re-optimizing dependencies on ` +
235
+ `dev boot. This is transient: routes are served per-request and ` +
236
+ `discovery re-runs automatically, so it clears on the next boot. If ` +
237
+ `routes still 404, confirm ${entry} calls createRouter().`,
238
+ };
239
+ }
240
+ return {
241
+ level: "error",
242
+ message:
243
+ `${err.message}\n` +
244
+ ` Ensure ${entry} calls createRouter() at module top level and that the ` +
245
+ `configured router entry path is correct.`,
246
+ };
247
+ }
248
+
249
+ const e = err as { stack?: string; message?: string };
250
+ const detail = e?.stack ?? e?.message ?? String(err);
251
+ return {
252
+ level: "error",
253
+ message: `[rango] Router discovery failed: ${detail}`,
254
+ };
255
+ }
package/src/vite/index.ts CHANGED
@@ -8,6 +8,13 @@
8
8
 
9
9
  export { rango } from "./rango.js";
10
10
  export { poke } from "./plugins/refresh-cmd.js";
11
+ // The built-in clientChunks strategy, exported so a custom `clientChunks`
12
+ // function can OVERLAY it (route a few modules to a dedicated chunk, delegate
13
+ // the rest) instead of replacing the whole route/marker grouping. Without this
14
+ // a consumer override silently loses app-fallback/route splitting for the
15
+ // entire app. Note: called without a ClientChunkContext the fallbackRefs-based
16
+ // `app-fallback` split is inactive — discovery wires it only for the built-in.
17
+ export { directoryClientChunks } from "./utils/client-chunks.js";
11
18
 
12
19
  export type {
13
20
  RangoNodeOptions,