@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
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: prerender
3
- description: Pre-render route segments at build time with Prerender and Passthrough live fallback
3
+ description: Pre-render route segments at build time with Prerender and Passthrough live fallback. Use when a page's content is mostly static and shouldn't render on every request, speeding up cold responses, or deciding which routes to prerender vs render live.
4
4
  argument-hint: [passthrough]
5
5
  ---
6
6
 
@@ -11,6 +11,13 @@ deserialization path, same segment system. The worker handles every request --
11
11
  there are NO static .html or .rsc files served from assets. The worker reads
12
12
  pre-computed Flight payloads instead of executing handler code.
13
13
 
14
+ ## Not this skill if…
15
+
16
+ - You want a cached HTML shell captured at runtime, with holes and loaders
17
+ staying live per request — see `/ppr`.
18
+ - You want runtime segment caching with TTL/SWR — that is the `cache()` DSL:
19
+ see `/caching`. Prerender is the same cache filled at build time.
20
+
14
21
  ## API: Prerender
15
22
 
16
23
  ### Static Route (no params)
@@ -243,16 +250,16 @@ path("/blog/:slug", BlogPost, { name: "blog.post" }, () => [
243
250
 
244
251
  ## Interaction with DSL Items
245
252
 
246
- | DSL item | Behavior with Prerender |
247
- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
248
- | `loader()` | Live at runtime, bundled normally. Use `cache()` for caching. |
249
- | `revalidate()` | Not allowed without Passthrough. Allowed with Passthrough. |
250
- | `cache()` | Orthogonal -- use on parent layouts and loaders. |
251
- | `layout()` | Child layouts inside path are pre-rendered. Parent layouts are live. |
252
- | `parallel()` | Parallel slots inside path are pre-rendered. |
253
- | `middleware()` | Skipped during pre-render (no request). Runs at request time for loaders. |
254
- | `loading()` | Ignored without Passthrough. Works for live fallback with Passthrough. |
255
- | `intercept()` | Pre-rendered at build time. Intercept variant stored under `/i` key alongside main segments. At runtime, the correct variant is served based on `ctx.isIntercept`. `when()` conditions are skipped at build time (all intercepts are pre-rendered unconditionally). |
253
+ | DSL item | Behavior with Prerender |
254
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
255
+ | `loader()` | Live at runtime, bundled normally. Use `cache()` for caching. |
256
+ | `revalidate()` | Not allowed without Passthrough. Allowed with Passthrough. |
257
+ | `cache()` | Orthogonal -- use on parent layouts and loaders. |
258
+ | `layout()` | Child layouts inside path are pre-rendered. Parent layouts are live. |
259
+ | `parallel()` | Parallel slots inside path are pre-rendered. |
260
+ | `middleware()` | Skipped during pre-render (no request). Runs at request time for loaders. |
261
+ | `loading()` | Ignored without Passthrough. Works for live fallback with Passthrough. |
262
+ | `intercept()` | Pre-rendered at build time. Intercept variant stored under `/i` key alongside main segments. At runtime, the correct variant is served based on `ctx.isIntercept`. `when` config conditions are skipped at build time (all intercepts are pre-rendered unconditionally). |
256
263
 
257
264
  When Passthrough revalidation is enabled, remember that revalidation is
258
265
  still partial: opting a child segment into revalidation does not
@@ -609,12 +616,12 @@ At runtime, the cache-lookup middleware checks `ctx.isIntercept`:
609
616
  (filtered by `namespace?.startsWith("intercept:")`) and sets up slots.
610
617
  - **Direct navigation**: looks up `paramHash` (no suffix). Standard prerender path.
611
618
  - **Intercept miss (no `/i` entry)**: falls through to the normal pipeline so
612
- intercept-resolution middleware runs live. This handles `when()` conditions
619
+ intercept-resolution middleware runs live. This handles `when` config conditions
613
620
  that prevented pre-rendering.
614
621
 
615
- The `when()` callback receives an `InterceptSelectorContext` with `from.pathname`
622
+ The `when` config selector receives an `InterceptSelectorContext` with `from.pathname`
616
623
  which is unknown at build time. All intercepts are pre-rendered unconditionally;
617
- `when()` is evaluated at runtime by the intercept-resolution middleware.
624
+ `when` is evaluated at runtime by the intercept-resolution middleware.
618
625
 
619
626
  ### Example: Pre-rendered route with intercept
620
627
 
@@ -633,10 +640,13 @@ layout(ShopLayout, () => [
633
640
 
634
641
  // Intercept detail from shop index into a modal.
635
642
  // At build time, this is resolved and stored under the /i key.
636
- intercept("@modal", ".detail", <ProductModal />, () => [
637
- when(({ from }) => from.pathname === "/shop"),
638
- loader(ProductLoader),
639
- ]),
643
+ intercept(
644
+ "@modal",
645
+ ".detail",
646
+ <ProductModal />,
647
+ { when: ({ from }) => from.pathname === "/shop" },
648
+ () => [loader(ProductLoader)],
649
+ ),
640
650
  ])
641
651
  ```
642
652
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: rango
3
- description: Overview of @rangojs/router and available skills
3
+ description: Overview of @rangojs/router and available skills. Use when unsure which skill to reach for, starting a new task in a Rango app, or asking "what can this router do".
4
4
  argument-hint:
5
5
  ---
6
6
 
@@ -59,28 +59,73 @@ appears, not up front.
59
59
  To decide where something can live: **does it define a URL? structure, stays in
60
60
  `urls()`. Does it modify a node? config, compose freely.**
61
61
 
62
+ ## Passing data down the tree
63
+
64
+ Four ways to get per-request data to a segment below you, ordered safest-first.
65
+ Reach for the next rung only when the one above doesn't fit — the higher rungs
66
+ are immune to partial-revalidation staleness by construction.
67
+
68
+ 1. **A loader** (`loader()` + `useLoader()`). Loaders resolve fresh on every
69
+ pass — full renders, action revalidations, cache hits. Nothing to keep in
70
+ sync. If the data can be a loader, make it a loader.
71
+ 2. **Middleware `ctx.set()`**. Route middleware wraps every render pass,
72
+ including post-action revalidation and PE re-renders, so its variables are
73
+ never stale. Right for request-shaped context: auth, session, locale.
74
+ 3. **Handler `ctx.set()` to its own children** —
75
+ `path(handler, ..., () => [layout(...)])`. Orphan layouts and their
76
+ parallels belong to the route entry: on an action the whole entry re-runs
77
+ together by default (handler-first preserved), so the data stays consistent
78
+ with zero configuration. Right for data the page must compute anyway —
79
+ e.g. pagination, where the handler's search decides how many pages the
80
+ layout chrome renders. One rule: if you narrow the entry's revalidation
81
+ with a predicate that can return a hard `false`, put the same contract on
82
+ the entry's children too — a hard `false` on one side of a
83
+ producer/consumer pair desyncs it.
84
+ 4. **Cross-entry sharing** — an outer `layout()` entry feeding descendants.
85
+ Outer entries do NOT revalidate on actions by default (the revalidation
86
+ trace calls this `action:parent-chain-skip`), so this rung always requires
87
+ a shared revalidation contract: the same named `revalidate()` function on
88
+ the producer and every consumer. See `/layout` → "Revalidation Contracts".
89
+ Before writing one, check whether the producer can move down a rung.
90
+
91
+ The failure mode this ladder prevents: a consumer re-runs, its producer
92
+ doesn't, `ctx.get()` reads `undefined`, and fallback UI silently replaces good
93
+ UI after an action. Rungs 1–3 make that unrepresentable; rung 4 makes it a
94
+ stated, greppable contract.
95
+
62
96
  ## Pick a primitive
63
97
 
64
- | I need to… | Use | Skill |
65
- | ------------------------------------- | -------------------------------- | ----------------------- |
66
- | render data fresh every request | `loader()` + `useLoader()` | /loader |
67
- | cache a rendered subtree | `cache()` on a segment | /caching |
68
- | cache one function/component's result | `"use cache"` | /use-cache |
69
- | cache a loader's data | `loader(L, () => [cache()])` | /loader, /caching |
70
- | re-render a segment after an action | `revalidate()` | /loader |
71
- | mutate | `"use server"` action | /server-actions |
72
- | debug a slow request | `debugPerformance` / telemetry | /observability |
73
- | share config across routes | factory returning a helper array | /composability |
74
- | compose a sub-app / module | `include()` | /route |
75
- | modal / soft navigation | `intercept()` | /intercept |
76
- | pre-render a route at build time | `Prerender(...)` wrapper | /prerender |
77
- | stream SSE / upgrade a WebSocket | `path.stream()` / `path.any()` | /streams-and-websockets |
98
+ | I need to… | Use | Skill |
99
+ | --------------------------------------- | ---------------------------------- | ----------------------- |
100
+ | render data fresh every request | `loader()` + `useLoader()` | /loader |
101
+ | cache a rendered subtree | `cache()` on a segment | /caching |
102
+ | cache one function/component's result | `"use cache"` | /use-cache |
103
+ | cache a loader's data | `loader(L, () => [cache()])` | /loader, /caching |
104
+ | re-render a segment after an action | `revalidate()` | /loader |
105
+ | mutate | `"use server"` action | /server-actions |
106
+ | debug a slow request | `debugPerformance` / telemetry | /observability |
107
+ | share config across routes | factory returning a helper array | /composability |
108
+ | compose a sub-app / module | `include()` | /route |
109
+ | modal / soft navigation | `intercept()` | /intercept |
110
+ | pre-render a route at build time | `Prerender(...)` wrapper | /prerender |
111
+ | feed live loaders from a cached shell | replayed handle + `ctx.rendered()` | /shell-manifest |
112
+ | cache the HTML shell, keep loaders live | `ppr` path option | /ppr |
113
+ | stream SSE / upgrade a WebSocket | `path.stream()` / `path.any()` | /streams-and-websockets |
78
114
 
79
115
  ## Invariants
80
116
 
81
117
  - `path()`/`include()` are always visible in `urls()`; config helpers are extractable.
82
118
  - **Cache decides freshness; `revalidate()` decides client-update.** Orthogonal; compose.
83
119
  - Loaders resolve fresh every request (even inside `cache()`) and never run twice/request.
120
+ - **The consumption-lane rule.** For every shared artifact (`cache()`,
121
+ `"use cache"`, the PPR shell): server-side handler consumption
122
+ (`await ctx.use(loader)`) yields a BAKED copy — identity reads
123
+ (`cookies()`/`headers()`) are permitted there and the capture-time value
124
+ freezes into the shared artifact (a documented footgun; see `/caching` →
125
+ "Cache purity & tainted objects"). Client-side consumption (`useLoader` in
126
+ a `"use client"` component) is the LIVE lane. DSL `loader()` segments
127
+ follow their lane machinery (live under renderable `loading()`, bake
128
+ otherwise). Pinned by semantic-matrix row PPR3.
84
129
  - Inside `"use cache"`: `cookies()`/`headers()` and `ctx` side-effects
85
130
  (`set`/`header`/`setTheme`/`onResponse`/`setLocationState`) throw; `ctx.use(Handle)`
86
131
  is captured on miss and replayed on hit. (The non-cacheable read guard is a
@@ -117,6 +162,7 @@ Same words, different jobs — this is the most common source of the
117
162
  | Next.js `revalidateTag` / `updateTag` | **Axis 1** (cache) | Cache busting by tag. Tag via `cache({ tags })` / `cacheTag(...tags)`; invalidate with `updateTag(...tags)` (awaitable, read-your-own-writes) or `revalidateTag(...tags)` (background, non-blocking). Built-in stores index by tag. No `revalidatePath` (path-based busting); use tags. |
118
163
  | React Router / Remix `shouldRevalidate` | **Axis 2** | This is the correct mental model for Rango's `revalidate()`. |
119
164
  | HTTP `Cache-Control` / ISR | **Axis 1** | Edge/document layer — see `/document-cache`. Separate from both `cache()` and `revalidate()`. |
165
+ | Next.js PPR (partial prerendering) | HTML shell layer | Same idea, different wiring: the opt-in `ppr` path option captures at runtime (no build-time default); holes are render-defined — `loading()` subtrees plus pending promises under a consumer's own `<Suspense>`. See `/ppr`. |
120
166
  | Remix/RR `loader` | live data | Like Rango loaders, fresh per request — but Rango loaders run in parallel and stream (latency overlaps first paint), and can opt into caching on demand. |
121
167
 
122
168
  See `/cache-guide` for the axis-1 decision guide, `/loader` and `/route` for
@@ -185,6 +231,12 @@ resolve `dist/` outside `./vite`, and it may lag `src/`.
185
231
 
186
232
  Grouped by concern — read when you need to…
187
233
 
234
+ **Positioning & evaluation**:
235
+
236
+ | Skill | Description |
237
+ | ------------- | ------------------------------------------------------------ |
238
+ | `/comparison` | Compare Rango with Next.js, TanStack Start, and Waku fairly. |
239
+
188
240
  **Structure & routing** — shape URLs, layouts, navigation, and request processing:
189
241
 
190
242
  | Skill | Description |
@@ -206,15 +258,17 @@ Grouped by concern — read when you need to…
206
258
 
207
259
  **Data & caching** — fetch, mutate, and cache:
208
260
 
209
- | Skill | Description |
210
- | ----------------- | ----------------------------------------------------------------------- |
211
- | `/loader` | Data loaders with `createLoader()` and `revalidate()` |
212
- | `/server-actions` | Mutations with `"use server"`, useActionState, validation, revalidation |
213
- | `/caching` | Segment caching with memory or KV stores |
214
- | `/use-cache` | Function-level caching with `"use cache"` directive |
215
- | `/cache-guide` | When to use `cache()` vs `"use cache"` — differences and decision guide |
216
- | `/document-cache` | Edge caching with Cache-Control headers |
217
- | `/prerender` | Pre-render route segments at build time (Passthrough live fallback) |
261
+ | Skill | Description |
262
+ | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
263
+ | `/loader` | Data loaders with `createLoader()` and `revalidate()` |
264
+ | `/server-actions` | Mutations with `"use server"`, useActionState, validation, revalidation |
265
+ | `/caching` | Segment caching with memory or KV stores |
266
+ | `/use-cache` | Function-level caching with `"use cache"` directive |
267
+ | `/cache-guide` | When to use `cache()` vs `"use cache"` — differences and decision guide |
268
+ | `/document-cache` | Edge caching with Cache-Control headers |
269
+ | `/ppr` | PPR shell caching: cached shell served instantly, live holes resumed — a hole is a `loading()` subtree OR a pending promise under `<Suspense>` (no loader needed) |
270
+ | `/prerender` | Pre-render route segments at build time (Passthrough live fallback) |
271
+ | `/shell-manifest` | Replayed handles as cache metadata read by live loaders (frozen shell, batched live holes) |
218
272
 
219
273
  **Client & presentation** — build the client-side UX:
220
274
 
@@ -228,6 +282,7 @@ Grouped by concern — read when you need to…
228
282
  | `/scripts` | Inject third-party scripts (GTM/analytics) into head/body via the `Script` handle; nonce auto-applied to document-rendered scripts |
229
283
  | `/tailwind` | Set up Tailwind CSS v4 with `?url` imports |
230
284
  | `/view-transitions` | React View Transitions on layouts, routes, and parallel slots |
285
+ | `/defer-hydration` | Full body HTML in the PPR shell + hydration off the critical path (gated Suspense boundary, content-as-fallback) |
231
286
  | `/breadcrumbs` | Built-in Breadcrumbs handle for breadcrumb navigation |
232
287
  | `/react-compiler` | Enable React Compiler (opt-in) the vite-rsc way; client-only scope |
233
288
 
@@ -325,7 +380,11 @@ dotfiles, and existing `.gen.` files.
325
380
  ### Recursive includes
326
381
 
327
382
  The generator follows `include()` calls across files, resolving imports to build
328
- the full route tree. Circular includes are detected and warned about.
383
+ the full route tree. It resolves both the eager form `include("/x", patterns)`
384
+ and the code-split async form `include("/x", () => import("./x"))` — for the
385
+ latter it walks the imported module's `export default urls(...)`, including any
386
+ nested `include()`s inside it — so a code-split route group is still fully typed
387
+ (see `/composability`). Circular includes are detected and warned about.
329
388
 
330
389
  ### First-wins deduplication
331
390
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: response-routes
3
- description: Response routes (path.json, path.text, etc.) for non-RSC endpoints with typed responses
3
+ description: Response routes (path.json, path.text, etc.) for non-RSC endpoints with typed responses. Use when building a JSON/text API endpoint alongside your pages, or asking how to return raw JSON instead of RSC from a route.
4
4
  argument-hint: [json|text|html|xml|md|image|stream]
5
5
  ---
6
6
 
@@ -418,6 +418,20 @@ export const urlpatterns = urls(({ path, include }) => [
418
418
  ]);
419
419
  ```
420
420
 
421
+ A heavy module like this is a good code-split candidate. Pass an async provider
422
+ and the module — its handlers, response serializers, and any nested
423
+ `include()`s — loads on the first request under the prefix instead of at startup:
424
+
425
+ ```typescript
426
+ // blog/urls.tsx: `export default blogPatterns`
427
+ include("/blog", () => import("./blog/urls"), { name: "blog" }),
428
+ ```
429
+
430
+ Named routes and response types still resolve through the split: `TRoutes` and
431
+ the `_responses` phantom are inferred from the resolved `urls()` value, so
432
+ `Rango.PathResponse<"/blog/api/stats">` and `ctx.reverse` are unchanged. See
433
+ `/composability`.
434
+
421
435
  ### Type safety after mounting
422
436
 
423
437
  ```typescript
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: route
3
- description: Define routes with path() in @rangojs/router
3
+ description: Define routes with path() in @rangojs/router. Use when creating a new page or route, or asking how to define a URL path and its handler.
4
4
  argument-hint: [pattern]
5
5
  ---
6
6
 
@@ -53,6 +53,49 @@ For the common pattern of an optional locale prefix
53
53
  locale detection, fallback chains, URL generation with absent locale —
54
54
  see `/i18n`.
55
55
 
56
+ ### Named catch-all params (`:name+` / `:name*`)
57
+
58
+ A catch-all consumes the **rest of the path** and exposes it as a single
59
+ decoded string at `ctx.params.<name>`, with the internal `/` separators kept.
60
+ It must be the **last** segment of the pattern.
61
+
62
+ - `:name+` — **one-or-more** segments (Next `[...name]`, React-Router splat).
63
+ `/docs/:slug+` matches `/docs/a` and `/docs/a/b/c`, but **not** the bare
64
+ `/docs`.
65
+ - `:name*` — **zero-or-more** segments (Next `[[...name]]`). `/docs/:slug*`
66
+ additionally matches the bare `/docs`, binding `ctx.params.slug` to `""`.
67
+
68
+ ```typescript
69
+ urls(({ path }) => [
70
+ // /shop/electronics/phones -> ctx.params.path === "electronics/phones"
71
+ path("/shop/:path+", ShopCatchAll, { name: "shopCatchAll" }),
72
+
73
+ // /docs -> ctx.params.slug === ""
74
+ // /docs/intro -> ctx.params.slug === "intro"
75
+ // /docs/a/b -> ctx.params.slug === "a/b"
76
+ path("/docs/:slug*", (ctx) => {
77
+ const parts = ctx.params.slug === "" ? [] : ctx.params.slug.split("/");
78
+ return <Docs segments={parts} />;
79
+ }, { name: "docs" }),
80
+ ]);
81
+ ```
82
+
83
+ `ctx.params.<name>` is always a `string` for a catch-all (never `undefined`) —
84
+ `:name*` binds `""` for the empty case, so read it directly. `reverse()` /
85
+ `ctx.reverse()` rebuild the URL with separators preserved:
86
+ `reverse("docs", { slug: "a/b" })` -> `/docs/a/b`.
87
+
88
+ The value is the URL-decoded remainder. `split("/")` recovers the segments in the
89
+ common case, but note that a segment containing an encoded slash (`%2F`) decodes
90
+ to a literal `/` and is therefore indistinguishable from a separator — the same
91
+ trade-off the bare `*` splat has. If you need to distinguish those, match on the
92
+ raw pathname instead.
93
+
94
+ The bare unnamed wildcard `path("/files/*", …)` still works and is read at
95
+ `ctx.params["*"]`; prefer a named catch-all when you want a typed param key.
96
+ Combining a modifier with `?`, a literal suffix, or a constraint
97
+ (`:slug*?`, `:slug*.html`, `:slug(a|b)+`) is rejected at build time.
98
+
56
99
  ## Route Handler Patterns
57
100
 
58
101
  ### Component Function
@@ -154,6 +197,13 @@ first. Use `ctx.set(key, value)` to share data with children, who read it
154
197
  via `ctx.get(key)`. Caching wraps all segments together, so either all run
155
198
  or none do.
156
199
 
200
+ This pattern is also safe under partial action revalidation: on an action,
201
+ the route entry re-runs as a unit by default — route segment, loaders, and
202
+ `belongsToRoute` children (orphan layouts, entry parallels) all seed
203
+ revalidate-true, with handler-first ordering preserved. Handler-set data
204
+ stays consistent with no configuration. See `/rango` → "Passing data down
205
+ the tree" for the safest-first ladder.
206
+
157
207
  ### Typed context variables with createVar
158
208
 
159
209
  Use `createVar<T>()` to create a typed token for `ctx.set()`/`ctx.get()`.
@@ -240,9 +290,18 @@ Cacheable vars (the default) can be read freely inside cache scopes.
240
290
  > decides hit/miss/ttl/swr independently and never reads `revalidate()`. See
241
291
  > `/cache-guide` → "Two axes" and `/rango` → "The shape of rango".
242
292
 
243
- Handler-first guarantees apply within a single full render pass. For partial
244
- action revalidation, define named revalidation contracts and reuse them on both
245
- the producer route and the consumer child segments.
293
+ With no `revalidate()` configured, an entry needs no contract: on an action
294
+ the route handler and its children re-run together by default, so handler
295
+ data stays consistent on its own. Contracts matter in two cases:
296
+
297
+ 1. **You narrow the entry's revalidation** with a predicate that can return a
298
+ hard `false` (e.g. bare `ctx.isAction(X)`). A hard `false` on one side of a
299
+ producer/consumer pair desyncs it — the child re-runs by default and reads
300
+ `undefined`, or vice versa. Put the same named contract on the route and
301
+ its dependent children so they narrow together.
302
+ 2. **The producer is an outer entry** (a standalone `layout()` above this
303
+ route). Outer entries skip action revalidation by default, so the shared
304
+ contract is mandatory — see `/layout` → "Revalidation Contracts".
246
305
 
247
306
  ```typescript
248
307
  // revalidation-contracts.ts
@@ -420,6 +479,14 @@ urls(({ path, layout }) => [
420
479
  ])
421
480
  ```
422
481
 
482
+ For composing whole route MODULES, reach for `include()` — and prefer the
483
+ code-split form `include("/shop", () => import("./shop-patterns"))` for any
484
+ group that is a natural unit: it keeps the group off the cold-start path, and
485
+ measured first-hit cost scales with routes-per-chunk, so many small groups
486
+ beat one giant one. Sizing rules and the numbers behind them:
487
+ [skills/composability](../composability/SKILL.md) → "Sizing async include
488
+ groups (measured)".
489
+
423
490
  ## View Transitions
424
491
 
425
492
  A route can configure its own `transition()` — the wrap goes around the route's component itself (routes are leaves; they have no separate default outlet channel). If the route component renders a `<ParallelOutlet />` directly, that slot remains inside the route's VT subtree, so prefer mounting parallel slots in a layout when combining intercept modals with route-level transitions. See [skills/view-transitions](../view-transitions/SKILL.md) for examples and the wrap-location rules across layouts, routes, and slots.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: router-setup
3
- description: Create and configure the RSC router with createRouter
3
+ description: Create and configure the RSC router with createRouter. Use when bootstrapping a new Rango app, or configuring top-level router options like base path, cache store, or environment.
4
4
  argument-hint: [option]
5
5
  ---
6
6
 
@@ -60,8 +60,7 @@ urls(
60
60
  cache, // Configure caching
61
61
  middleware, // Add middleware
62
62
  revalidate, // Control revalidation
63
- intercept, // Intercept routes for modals
64
- when, // Conditional rendering
63
+ intercept, // Intercept routes for modals (conditional via intercept(..., { when }))
65
64
  errorBoundary, // Add an error boundary
66
65
  notFoundBoundary, // Add a not-found boundary
67
66
  transition, // Configure view transitions
@@ -399,6 +398,18 @@ export const urlpatterns = urls(({ path, include }) => [
399
398
  ]);
400
399
  ```
401
400
 
401
+ `include()` also accepts an async provider to code-split that group into its own
402
+ chunk, imported on the first request reaching the prefix instead of at startup:
403
+
404
+ ```typescript
405
+ // urls/shop.tsx: `export default shopPatterns`
406
+ include("/shop", () => import("./urls/shop"), { name: "shop" }),
407
+ ```
408
+
409
+ Build-time discovery still `await`s the provider, so route types, `href()`, and
410
+ prerender see every route in the split group. Reach for it when a group is a
411
+ large, independently-loadable unit — see `/composability`.
412
+
402
413
  ## Environment Types
403
414
 
404
415
  ```typescript
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: scripts
3
- description: Inject third-party scripts (GTM, analytics, widgets) into the document head/body via the Script handle
3
+ description: Inject third-party scripts (GTM, analytics, widgets) into the document head/body via the Script handle. Use when adding Google Tag Manager, an analytics snippet, or a third-party widget script to the page.
4
4
  argument-hint: "[vendor]"
5
5
  ---
6
6
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: server-actions
3
- description: Define and call server actions (`"use server"`) — forms, useActionState, useOptimistic, validation, error handling, redirects, revalidation
3
+ description: Define and call server actions (`"use server"`) — forms, useActionState, useOptimistic, validation, error handling, redirects, revalidation. Use when handling a form submission on the server, calling a server function from a client component, or needing optimistic UI after a mutation.
4
4
  argument-hint: "[action]"
5
5
  ---
6
6
 
@@ -24,7 +24,8 @@ with no framework wrapper. All standard React hooks (`useActionState`,
24
24
  Use loaders and route handlers for reads. Use actions for writes. After an
25
25
  action runs, the matched route tree can partially re-render so handlers and
26
26
  loaders that opt into revalidation see the new state — see "Revalidation"
27
- below.
27
+ below. For the read-side APIs (`createLoader`, `useLoader`, `useFetchLoader`),
28
+ see `/loader`.
28
29
 
29
30
  ## Revalidation Model
30
31
 
@@ -0,0 +1,185 @@
1
+ ---
2
+ name: shell-manifest
3
+ description: Shell manifest pattern — replayed handles as cache metadata that live loaders read, e.g. a prerendered product list with batched live prices. Use when a cached/prerendered shell needs to feed IDs or metadata to a live loader for freshly-fetched data.
4
+ argument-hint:
5
+ ---
6
+
7
+ # Shell Manifest — cache metadata for live loaders
8
+
9
+ Use this when a cached or prerendered shell has dynamic holes, and the live
10
+ data layer needs to know **what the shell actually contains** — which
11
+ products, which slots, which keys. The frozen render describes itself
12
+ through a handle; loaders (always live) read that description and fetch
13
+ exactly the dynamic data the shell needs, in one batch.
14
+
15
+ Canonical case: a prerendered product list where prices must stay live.
16
+
17
+ ## The problem this solves
18
+
19
+ Any cached-shell-plus-live-holes design has a coordination gap: how does the
20
+ live layer know what the holes need?
21
+
22
+ - **Per-hole fetching** (each `<Price>` component fetching for itself) is the
23
+ N+1 default — N visible products, N queries.
24
+ - **A loader that re-queries the list** ("current top products") drifts from
25
+ a stale shell — right prices attached to wrong products.
26
+
27
+ The shell manifest closes the gap with a consistency guarantee: the loader
28
+ reads the ids the shell _actually rendered_, replayed from the same stored
29
+ artifact, so the holes can never desync from the shell and the query is
30
+ batched.
31
+
32
+ ## The mechanism (three features composed)
33
+
34
+ 1. **Handles record data at render time.** The handler pushes to a handle
35
+ (`ctx.use(Handle)`) while it renders — at build time for `Prerender`, on
36
+ the cache miss for `cache()`.
37
+ 2. **Replay on every hit.** Handle data is stored with the Flight payload
38
+ and replayed into the handle store on cache/prerender hits — handler code
39
+ does not re-run, but its pushes do.
40
+ 3. **Loaders read after the render barrier.** A DSL loader can
41
+ `await ctx.rendered()` (waits for all non-loader segments to settle —
42
+ fresh render or replay alike), then `ctx.use(Handle)` returns the
43
+ **collected** handle data.
44
+
45
+ Loaders are live by default, so the read happens on every request even when
46
+ the shell is a hit.
47
+
48
+ ## Canonical example: prerendered list, live prices
49
+
50
+ ```tsx
51
+ // handles/rendered-products.ts
52
+ import { createHandle } from "@rangojs/router";
53
+
54
+ // TData = string (one push per product id), collected to a flat string[]
55
+ export const RenderedProducts = createHandle<string, string[]>((segments) =>
56
+ segments.flat(),
57
+ );
58
+ ```
59
+
60
+ ```tsx
61
+ // routes/products.tsx — the list is baked at build time; prices are not
62
+ import { Prerender } from "@rangojs/router";
63
+ import { RenderedProducts } from "../handles/rendered-products";
64
+ import { Price } from "../components/price";
65
+
66
+ export const ProductList = Prerender(
67
+ async () => [{ category: "espresso" }, { category: "filter" }],
68
+ async (ctx) => {
69
+ const products = await db.productsByCategory(ctx.params.category);
70
+ const track = ctx.use(RenderedProducts);
71
+ for (const p of products) track(p.id);
72
+ return (
73
+ <ul>
74
+ {products.map((p) => (
75
+ <li key={p.id}>
76
+ {p.name} <Price id={p.id} />
77
+ </li>
78
+ ))}
79
+ </ul>
80
+ );
81
+ },
82
+ );
83
+ ```
84
+
85
+ ```ts
86
+ // loaders/prices.ts — one batched query for exactly the rendered products
87
+ import { createLoader } from "@rangojs/router";
88
+ import { RenderedProducts } from "../handles/rendered-products";
89
+
90
+ export const PriceLoader = createLoader(async (ctx) => {
91
+ "use server";
92
+ await ctx.rendered();
93
+ const ids = ctx.use(RenderedProducts);
94
+ return db.pricesFor(ids); // Map<string, number> keyed by product id
95
+ });
96
+ ```
97
+
98
+ ```tsx
99
+ // urls.tsx — wire the route and register the loader
100
+ path("/products/:category", ProductList, { name: "products" }, () => [
101
+ loader(PriceLoader),
102
+ ]);
103
+ ```
104
+
105
+ ```tsx
106
+ // components/price.tsx — live hole in the frozen shell
107
+ "use client";
108
+ import { useLoader } from "@rangojs/router/client";
109
+ import { PriceLoader } from "../loaders/prices";
110
+
111
+ export function Price({ id }: { id: string }) {
112
+ const { data } = useLoader(PriceLoader);
113
+ return <span>{formatPrice(data[id])}</span>;
114
+ }
115
+ ```
116
+
117
+ Request flow on a hit: stored payload replays (handler never runs) → handle
118
+ data lands in the store → render barrier resolves → `PriceLoader` reads the
119
+ replayed ids → one query → prices stream into `<Price>` components.
120
+
121
+ ## Works with runtime cache() too
122
+
123
+ `Prerender` is build-time caching; the replay mechanism is identical for the
124
+ runtime segment cache. Wrap the route in `cache()` instead and the handler
125
+ pushes on the miss, replays on every hit:
126
+
127
+ ```tsx
128
+ cache({ ttl: 600, tags: ["products"] }, () => [
129
+ path("/products/:category", ProductList, { name: "products" }, () => [
130
+ loader(PriceLoader),
131
+ ]),
132
+ ]);
133
+ ```
134
+
135
+ ## Contract and gotchas
136
+
137
+ - **The manifest is exactly as fresh as the shell.** Replayed handle data is
138
+ frozen with the payload. To change _which_ products render, invalidate the
139
+ shell (`updateTag("products")`, TTL expiry, rebuild) — never treat the
140
+ loader as the refresh path for the list itself. This is the point:
141
+ shell and holes cannot desync because they share one artifact.
142
+ - **No request-scoped data in a manifest handle.** The handle data is baked
143
+ into a shared artifact — the same cross-user rule as any cached content.
144
+ Ids, slugs, slot names, variant keys: yes. Anything derived from
145
+ `cookies()`/`headers()`: no.
146
+ - **`ctx.rendered()` is experimental and DSL-loaders-only.** It throws in
147
+ fetchable/standalone loader calls that run outside a route render.
148
+ - **The reading loader serializes after the shell.** `await ctx.rendered()`
149
+ deliberately gives up loader/render parallelism — on a miss the loader
150
+ waits for segment resolution; on a hit (the common case for a cached
151
+ shell) replay is immediate and the wait is negligible. A
152
+ `debugPerformance` waterfall shows this loader after the render bar; for
153
+ this pattern that is the contract, not a regression.
154
+ - **`ctx.use(Handle)` before `await ctx.rendered()` throws** in a loader,
155
+ with an error saying to await the barrier first.
156
+ - **Deferred handle values are resolved before storage** (resolve-by-default),
157
+ so the manifest read always sees plain values, never promises.
158
+
159
+ ## Testing
160
+
161
+ `runLoader` seeds the barrier and the collected handle value directly —
162
+ matched by handle reference (the same seeding style as loader deps):
163
+
164
+ ```ts
165
+ import { runLoader } from "@rangojs/router/testing";
166
+
167
+ const prices = await runLoader(PriceLoader, {
168
+ rendered: true,
169
+ handles: [[RenderedProducts, ["widget-a", "widget-b"]]],
170
+ env: { DB: fakeDb },
171
+ });
172
+ ```
173
+
174
+ This tests the loader's post-barrier logic. The real
175
+ push → store → replay → barrier wiring is covered at the e2e tier (dev +
176
+ production), like every cache-path behavior.
177
+
178
+ ## Related
179
+
180
+ - `/prerender` — `Prerender`/`Passthrough`, build flow, passthrough fallback
181
+ - `/caching` — segment `cache()`, stores, tags
182
+ - `/loader` — loader context, `ctx.rendered()`, streaming
183
+ - `/hooks` — `useHandle` for reading handle data in client components
184
+ - `/rango` → "Passing data down the tree" — this pattern is the frozen→live
185
+ counterpart of that ladder