@rangojs/router 0.16.0 → 0.18.0

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 (309) hide show
  1. package/README.md +255 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/cookie-name.d.ts +6 -0
  4. package/dist/types/browser/history-state.d.ts +16 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  6. package/dist/types/browser/partial-update.d.ts +11 -0
  7. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  8. package/dist/types/browser/rsc-router.d.ts +1 -1
  9. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  10. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  11. package/dist/types/cache/cache-key-utils.d.ts +17 -2
  12. package/dist/types/cache/cache-policy.d.ts +17 -0
  13. package/dist/types/cache/cache-scope.d.ts +132 -19
  14. package/dist/types/cache/cache-tag.d.ts +92 -7
  15. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  16. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  17. package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
  18. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  19. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  20. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
  21. package/dist/types/cache/handle-capture.d.ts +41 -3
  22. package/dist/types/cache/handle-snapshot.d.ts +54 -13
  23. package/dist/types/cache/index.d.ts +1 -0
  24. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  25. package/dist/types/cache/segment-codec.d.ts +10 -3
  26. package/dist/types/cache/shell-memo.d.ts +178 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +110 -21
  28. package/dist/types/cache/tag-invalidation.d.ts +30 -3
  29. package/dist/types/cache/taint.d.ts +1 -1
  30. package/dist/types/cache/types.d.ts +220 -44
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
  32. package/dist/types/client.d.ts +4 -4
  33. package/dist/types/client.rsc.d.ts +4 -5
  34. package/dist/types/components/DefaultDocument.d.ts +5 -2
  35. package/dist/types/context-var.d.ts +6 -5
  36. package/dist/types/escape-script.d.ts +1 -1
  37. package/dist/types/handles/MetaTags.d.ts +5 -4
  38. package/dist/types/handles/Scripts.d.ts +8 -7
  39. package/dist/types/handles/meta.d.ts +2 -2
  40. package/dist/types/handles/script.d.ts +17 -16
  41. package/dist/types/href-client.d.ts +1 -1
  42. package/dist/types/html.d.ts +12 -0
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +3 -2
  45. package/dist/types/loader-store.d.ts +7 -4
  46. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  47. package/dist/types/reverse.d.ts +36 -4
  48. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  49. package/dist/types/route-definition/helpers-types.d.ts +33 -9
  50. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  51. package/dist/types/route-types.d.ts +1 -1
  52. package/dist/types/router/debug-manifest.d.ts +3 -2
  53. package/dist/types/router/error-handling.d.ts +20 -0
  54. package/dist/types/router/handler-context.d.ts +6 -0
  55. package/dist/types/router/instrument.d.ts +8 -0
  56. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  57. package/dist/types/router/middleware-types.d.ts +9 -3
  58. package/dist/types/router/router-context.d.ts +3 -1
  59. package/dist/types/router/router-interfaces.d.ts +24 -2
  60. package/dist/types/router/router-options.d.ts +8 -5
  61. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  62. package/dist/types/router/segment-resolution/loader-cache.d.ts +66 -1
  63. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  64. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
  65. package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  67. package/dist/types/router/segment-wrappers.d.ts +1 -1
  68. package/dist/types/rsc/full-payload.d.ts +14 -0
  69. package/dist/types/rsc/handler.d.ts +1 -1
  70. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  71. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  72. package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
  73. package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
  74. package/dist/types/rsc/shell-capture.d.ts +151 -19
  75. package/dist/types/rsc/shell-serve.d.ts +140 -17
  76. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  77. package/dist/types/rsc/types.d.ts +17 -0
  78. package/dist/types/server/context.d.ts +139 -15
  79. package/dist/types/server/cookie-store.d.ts +21 -0
  80. package/dist/types/server/handle-store.d.ts +26 -2
  81. package/dist/types/server/request-context.d.ts +182 -53
  82. package/dist/types/ssr/index.d.ts +24 -0
  83. package/dist/types/ssr/ssr-root.d.ts +9 -8
  84. package/dist/types/static-handler.d.ts +4 -1
  85. package/dist/types/testing/flight.entry.d.ts +7 -0
  86. package/dist/types/testing/generated-routes.d.ts +2 -2
  87. package/dist/types/testing/internal/context.d.ts +1 -1
  88. package/dist/types/testing/render-route.d.ts +32 -6
  89. package/dist/types/testing/run-loader.d.ts +18 -3
  90. package/dist/types/testing/serve-shell-request.d.ts +132 -0
  91. package/dist/types/testing/shell-status.d.ts +23 -10
  92. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  93. package/dist/types/testing/vitest.d.ts +35 -1
  94. package/dist/types/theme/ThemeScript.d.ts +3 -3
  95. package/dist/types/types/cache-types.d.ts +14 -10
  96. package/dist/types/types/handler-context.d.ts +28 -1
  97. package/dist/types/types/loader-types.d.ts +8 -6
  98. package/dist/types/urls/pattern-types.d.ts +22 -18
  99. package/dist/types/urls/response-types.d.ts +3 -12
  100. package/dist/types/use-loader.d.ts +12 -3
  101. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  102. package/dist/types/vite/rango.d.ts +1 -1
  103. package/dist/types/vite/router-discovery.d.ts +29 -2
  104. package/dist/vite/index.js +1420 -1348
  105. package/package.json +2 -2
  106. package/skills/api-client/SKILL.md +22 -10
  107. package/skills/breadcrumbs/SKILL.md +75 -22
  108. package/skills/bundle-analysis/SKILL.md +22 -22
  109. package/skills/cache-guide/SKILL.md +306 -124
  110. package/skills/caching/SKILL.md +530 -98
  111. package/skills/catalog.json +16 -16
  112. package/skills/client-urls/SKILL.md +65 -38
  113. package/skills/cloudflare/SKILL.md +87 -20
  114. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  115. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  116. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  117. package/skills/comparison/SKILL.md +5 -0
  118. package/skills/comparison/references/framework-comparison.md +75 -57
  119. package/skills/composability/SKILL.md +38 -29
  120. package/skills/css/SKILL.md +10 -13
  121. package/skills/debug-manifest/SKILL.md +74 -31
  122. package/skills/defer-hydration/SKILL.md +52 -31
  123. package/skills/deployment-caching/SKILL.md +11 -3
  124. package/skills/document-cache/SKILL.md +98 -28
  125. package/skills/fonts/SKILL.md +16 -8
  126. package/skills/handler-use/SKILL.md +55 -17
  127. package/skills/hooks/SKILL.md +28 -20
  128. package/skills/hooks/data.md +23 -3
  129. package/skills/hooks/handle-and-actions.md +34 -17
  130. package/skills/hooks/navigation.md +39 -10
  131. package/skills/hooks/outlets.md +13 -6
  132. package/skills/hooks/state.md +39 -15
  133. package/skills/hooks/urls.md +7 -5
  134. package/skills/host-router/SKILL.md +64 -12
  135. package/skills/i18n/SKILL.md +60 -34
  136. package/skills/intercept/SKILL.md +161 -26
  137. package/skills/layout/SKILL.md +106 -50
  138. package/skills/links/SKILL.md +51 -19
  139. package/skills/loader/SKILL.md +386 -128
  140. package/skills/middleware/SKILL.md +129 -26
  141. package/skills/migrate-nextjs/SKILL.md +156 -70
  142. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  143. package/skills/migrate-react-router/SKILL.md +51 -39
  144. package/skills/migrate-react-router/component-migration.md +28 -7
  145. package/skills/migrate-react-router/data-and-actions.md +21 -6
  146. package/skills/migrate-react-router/route-mapping.md +19 -10
  147. package/skills/mime-routes/SKILL.md +12 -7
  148. package/skills/observability/SKILL.md +92 -14
  149. package/skills/parallel/SKILL.md +88 -47
  150. package/skills/ppr/SKILL.md +866 -379
  151. package/skills/prerender/SKILL.md +210 -108
  152. package/skills/rango/SKILL.md +115 -65
  153. package/skills/react-compiler/SKILL.md +6 -5
  154. package/skills/response-routes/SKILL.md +133 -25
  155. package/skills/route/SKILL.md +170 -60
  156. package/skills/router-setup/SKILL.md +420 -240
  157. package/skills/scripts/SKILL.md +39 -21
  158. package/skills/server-actions/SKILL.md +231 -59
  159. package/skills/shell-manifest/SKILL.md +28 -14
  160. package/skills/streams-and-websockets/SKILL.md +32 -14
  161. package/skills/tailwind/SKILL.md +37 -33
  162. package/skills/testing/SKILL.md +73 -33
  163. package/skills/testing/cache-prerender.md +60 -33
  164. package/skills/testing/client-components.md +59 -6
  165. package/skills/testing/e2e-parity.md +12 -2
  166. package/skills/testing/flight.md +19 -11
  167. package/skills/testing/handles.md +8 -5
  168. package/skills/testing/loader.md +26 -6
  169. package/skills/testing/middleware.md +8 -3
  170. package/skills/testing/predicates.md +120 -0
  171. package/skills/testing/render-handler.md +70 -22
  172. package/skills/testing/response-routes.md +11 -4
  173. package/skills/testing/reverse-and-types.md +20 -14
  174. package/skills/testing/serve-shell-request.md +188 -0
  175. package/skills/testing/server-actions.md +12 -3
  176. package/skills/testing/server-tree.md +9 -0
  177. package/skills/testing/setup.md +9 -5
  178. package/skills/theme/SKILL.md +155 -30
  179. package/skills/typesafety/SKILL.md +20 -9
  180. package/skills/typesafety/env-and-bindings.md +137 -50
  181. package/skills/typesafety/generated-files-and-cli.md +26 -14
  182. package/skills/typesafety/params-and-search.md +44 -19
  183. package/skills/typesafety/route-types.md +50 -14
  184. package/skills/use-cache/SKILL.md +224 -83
  185. package/skills/vercel/SKILL.md +35 -13
  186. package/skills/view-transitions/SKILL.md +42 -21
  187. package/src/__augment-tests__/augmented.check.ts +26 -1
  188. package/src/browser/cookie-name.ts +23 -0
  189. package/src/browser/history-state.ts +23 -0
  190. package/src/browser/index.ts +2 -2
  191. package/src/browser/navigation-bridge.ts +3 -4
  192. package/src/browser/navigation-transaction.ts +22 -2
  193. package/src/browser/partial-update.ts +35 -25
  194. package/src/browser/react/ScrollRestoration.tsx +6 -6
  195. package/src/browser/rsc-router.tsx +6 -1
  196. package/src/browser/scroll-restoration.ts +53 -6
  197. package/src/cache/cache-exec-scope.ts +37 -4
  198. package/src/cache/cache-key-utils.ts +23 -8
  199. package/src/cache/cache-policy.ts +22 -0
  200. package/src/cache/cache-runtime.ts +264 -68
  201. package/src/cache/cache-scope.ts +419 -43
  202. package/src/cache/cache-tag.ts +375 -11
  203. package/src/cache/cf/cf-base64.ts +36 -4
  204. package/src/cache/cf/cf-cache-constants.ts +12 -0
  205. package/src/cache/cf/cf-cache-store.ts +990 -348
  206. package/src/cache/cf/cf-cache-types.ts +32 -4
  207. package/src/cache/cf/cf-shell-frame.ts +258 -0
  208. package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
  209. package/src/cache/document-cache.ts +15 -0
  210. package/src/cache/handle-capture.ts +90 -10
  211. package/src/cache/handle-snapshot.ts +133 -28
  212. package/src/cache/index.ts +2 -0
  213. package/src/cache/isolate-tag-memo.ts +308 -0
  214. package/src/cache/segment-codec.ts +26 -7
  215. package/src/cache/shell-memo.ts +341 -0
  216. package/src/cache/shell-snapshot.ts +217 -27
  217. package/src/cache/tag-invalidation.ts +108 -22
  218. package/src/cache/taint.ts +1 -1
  219. package/src/cache/types.ts +225 -44
  220. package/src/cache/vercel/vercel-cache-store.ts +545 -61
  221. package/src/client.rsc.tsx +10 -9
  222. package/src/client.tsx +5 -4
  223. package/src/components/DefaultDocument.tsx +7 -2
  224. package/src/context-var.ts +6 -5
  225. package/src/escape-script.ts +1 -1
  226. package/src/handles/MetaTags.tsx +13 -11
  227. package/src/handles/Scripts.tsx +11 -10
  228. package/src/handles/meta.ts +2 -2
  229. package/src/handles/script.ts +17 -16
  230. package/src/href-client.ts +1 -1
  231. package/src/html.ts +12 -0
  232. package/src/index.rsc.ts +5 -0
  233. package/src/index.ts +1 -0
  234. package/src/loader-store.ts +7 -4
  235. package/src/prerender/build-shell-capture.ts +73 -22
  236. package/src/reverse.ts +44 -4
  237. package/src/route-definition/dsl-helpers.ts +121 -33
  238. package/src/route-definition/helpers-types.ts +33 -9
  239. package/src/route-definition/resolve-handler-use.ts +121 -6
  240. package/src/route-types.ts +0 -3
  241. package/src/router/debug-manifest.ts +10 -2
  242. package/src/router/error-handling.ts +51 -15
  243. package/src/router/handler-context.ts +71 -28
  244. package/src/router/instrument.ts +54 -2
  245. package/src/router/intercept-resolution.ts +141 -43
  246. package/src/router/loader-resolution.ts +115 -39
  247. package/src/router/match-api.ts +40 -26
  248. package/src/router/match-handlers.ts +5 -0
  249. package/src/router/match-middleware/background-revalidation.ts +161 -96
  250. package/src/router/match-middleware/cache-lookup.ts +110 -10
  251. package/src/router/match-middleware/cache-store.ts +48 -101
  252. package/src/router/middleware-types.ts +9 -6
  253. package/src/router/middleware.ts +75 -13
  254. package/src/router/prerender-match.ts +59 -19
  255. package/src/router/route-snapshot.ts +11 -1
  256. package/src/router/router-context.ts +2 -0
  257. package/src/router/router-interfaces.ts +25 -2
  258. package/src/router/router-options.ts +8 -5
  259. package/src/router/segment-resolution/fresh.ts +62 -21
  260. package/src/router/segment-resolution/helpers.ts +4 -2
  261. package/src/router/segment-resolution/loader-cache.ts +407 -76
  262. package/src/router/segment-resolution/loader-mask.ts +12 -22
  263. package/src/router/segment-resolution/loader-snapshot.ts +208 -56
  264. package/src/router/segment-resolution/mask-nested.ts +138 -44
  265. package/src/router/segment-resolution/revalidation.ts +61 -16
  266. package/src/router/segment-wrappers.ts +4 -0
  267. package/src/rsc/full-payload.ts +25 -1
  268. package/src/rsc/handler.ts +14 -2
  269. package/src/rsc/progressive-enhancement.ts +3 -2
  270. package/src/rsc/render-pipeline.ts +3 -0
  271. package/src/rsc/response-cache-serve.ts +9 -6
  272. package/src/rsc/response-route-handler.ts +11 -3
  273. package/src/rsc/rsc-rendering.ts +608 -225
  274. package/src/rsc/shell-build-manifest.ts +27 -9
  275. package/src/rsc/shell-capture-constants.ts +73 -6
  276. package/src/rsc/shell-capture.ts +1068 -296
  277. package/src/rsc/shell-serve.ts +316 -34
  278. package/src/rsc/ssr-setup.ts +14 -8
  279. package/src/rsc/types.ts +18 -0
  280. package/src/server/context.ts +300 -32
  281. package/src/server/cookie-store.ts +87 -61
  282. package/src/server/handle-store.ts +232 -7
  283. package/src/server/request-context.ts +297 -67
  284. package/src/ssr/index.tsx +136 -44
  285. package/src/ssr/ssr-root.tsx +9 -8
  286. package/src/static-handler.ts +4 -1
  287. package/src/testing/flight-runtime.d.ts +21 -0
  288. package/src/testing/flight.entry.ts +14 -0
  289. package/src/testing/generated-routes.ts +2 -2
  290. package/src/testing/internal/context.ts +1 -1
  291. package/src/testing/internal/flight-client-globals.ts +7 -5
  292. package/src/testing/render-route.tsx +85 -24
  293. package/src/testing/run-loader.ts +68 -6
  294. package/src/testing/serve-shell-request.ts +378 -0
  295. package/src/testing/shell-status.ts +34 -12
  296. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  297. package/src/testing/vitest.ts +62 -1
  298. package/src/theme/ThemeProvider.tsx +7 -6
  299. package/src/theme/ThemeScript.tsx +3 -3
  300. package/src/types/cache-types.ts +14 -10
  301. package/src/types/handler-context.ts +28 -1
  302. package/src/types/loader-types.ts +8 -6
  303. package/src/urls/pattern-types.ts +22 -18
  304. package/src/urls/response-types.ts +3 -23
  305. package/src/use-loader.tsx +12 -3
  306. package/src/vite/discovery/shell-prerender-phase.ts +9 -0
  307. package/src/vite/plugins/use-cache-transform.ts +118 -74
  308. package/src/vite/rango.ts +7 -3
  309. package/src/vite/router-discovery.ts +14 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -197,7 +197,7 @@
197
197
  "@opentelemetry/api": "^1.9.0",
198
198
  "@opentelemetry/context-async-hooks": "^2.9.0",
199
199
  "@opentelemetry/sdk-trace-base": "^2.9.0",
200
- "@playwright/test": "^1.49.1",
200
+ "@playwright/test": "^1.62.0",
201
201
  "@testing-library/dom": "^10.4.1",
202
202
  "@testing-library/react": "^16.3.2",
203
203
  "@types/node": "^24.10.1",
@@ -5,6 +5,10 @@ description: Build a typed client for consuming your own response-route JSON API
5
5
 
6
6
  # Typed API Client
7
7
 
8
+ Use this skill when first-party TypeScript code (a client component, another
9
+ worker, a script) calls your own `path.json()` endpoints and you want typed
10
+ params and payloads without writing `fetch` + URL building by hand.
11
+
8
12
  Response routes (`path.json()`) already ship typed responses — `RouteResponse<typeof patterns, "name">` resolves to the **bare payload**, inferred from your handler with no codegen. This skill wraps that inference in a small **typed client** so first-party TypeScript code calls your endpoints like functions instead of hand-writing `fetch` + URL building per call site.
9
13
 
10
14
  This is a **recipe, not a framework feature** — copy the helper below into your app. It depends only on **type-only** imports from `@rangojs/router` (`RouteResponse`, `ExtractParams`, `ProblemDetails`), which are erased at build time, so it runs anywhere a `fetch` does — **browser, worker, or server**. Nothing new to install or version.
@@ -14,7 +18,9 @@ This is a **recipe, not a framework feature** — copy the helper below into you
14
18
  ## What you get
15
19
 
16
20
  ```ts
17
- const api = createApiClient(apiShopPatterns, routes, { baseUrl });
21
+ const api = createApiClient<typeof apiShopPatterns, typeof routes>(routes, {
22
+ baseUrl,
23
+ });
18
24
 
19
25
  await api.health.get(); // no params → callable bare
20
26
  await api.product.get({ params: { productId } }); // params typed + required
@@ -31,8 +37,8 @@ await api.cart.post({ body: { productId, qty: 2 } }); // body sent as JSON
31
37
 
32
38
  ## The two inputs
33
39
 
34
- 1. **The `urls()` patterns value** — the type source. `typeof apiShopPatterns` carries the per-route response payloads (`_responses`) and patterns (`_routes`).
35
- 2. **The generated route map** — the name → pattern source. `rango generate` emits a per-module `<name>.gen.ts` exporting `routes`:
40
+ 1. **The `urls()` patterns type** — the type source, passed as the first type argument. `typeof apiShopPatterns` carries the per-route response payloads and patterns. Only the type is needed, so import it with `import type` (see Notes).
41
+ 2. **The generated route map** — the name → pattern source, passed as the value argument (and its `typeof` as the second type argument). `rango generate` emits a per-module `<name>.gen.ts` exporting `routes`:
36
42
 
37
43
  ```ts
38
44
  // api-shop.gen.ts (generated — do not edit)
@@ -44,7 +50,9 @@ export const routes = {
44
50
  } as const;
45
51
  ```
46
52
 
47
- Routes that declare a **search schema** are generated as objects instead — `index: { path: "/", search: { q: "string" } }`. The helper accepts both the string and `{ path }` forms. If a `urls()` block is mounted under a name prefix, build a local-keyed map from your global `NamedRoutes` so the keys match the block's route names (e.g. `{ catalog: NamedRoutes["apiShop.catalog"], ... } as const`).
53
+ Routes that declare a **search schema** are generated as objects instead — `index: { path: "/", search: { q: "string" } }`. The helper accepts both the string and `{ path }` forms.
54
+
55
+ The per-module map's keys are the block's local route names (matching `RouteResponse`), but its patterns are **mount-relative**: `"/catalog"`, not `"/api/shop/catalog"`. When the block is mounted under a URL prefix, either put the prefix in `baseUrl` (`` `${origin}/api/shop` ``) or build a local-keyed map of absolute patterns from the global `NamedRoutes` in `router.named-routes.gen.ts` (e.g. `{ catalog: NamedRoutes["apiShop.catalog"], ... } as const`).
48
56
 
49
57
  ## The helper (copy into your app)
50
58
 
@@ -54,6 +62,7 @@ import type {
54
62
  RouteResponse,
55
63
  ExtractParams,
56
64
  ProblemDetails,
65
+ UrlPatterns,
57
66
  } from "@rangojs/router";
58
67
 
59
68
  type SearchParams = Record<string, string | number | boolean>;
@@ -121,11 +130,12 @@ function fillPath(pattern: string, params?: Record<string, string>): string {
121
130
  .replace(/\/{2,}/g, "/");
122
131
  }
123
132
 
133
+ // Both type arguments are explicit: TPatterns has no value to infer from,
134
+ // and TypeScript does not mix explicit and inferred type arguments.
124
135
  export function createApiClient<
125
- TPatterns,
126
- const TRouteMap extends Record<string, RouteMapEntry>,
136
+ TPatterns extends UrlPatterns<any, any, Record<string, unknown>>,
137
+ TRouteMap extends Record<string, RouteMapEntry>,
127
138
  >(
128
- _patterns: TPatterns,
129
139
  routeMap: TRouteMap,
130
140
  opts: { baseUrl?: string; fetch?: typeof fetch } = {},
131
141
  ): ApiClient<TPatterns, TRouteMap> {
@@ -179,11 +189,11 @@ export function createApiClient<
179
189
  ## Using it
180
190
 
181
191
  ```ts
182
- import { apiShopPatterns } from "./urls/api-shop";
192
+ import type { apiShopPatterns } from "./urls/api-shop";
183
193
  import { routes } from "./urls/api-shop.gen";
184
194
  import { createApiClient, ApiError } from "./lib/api-client";
185
195
 
186
- const api = createApiClient(apiShopPatterns, routes, {
196
+ const api = createApiClient<typeof apiShopPatterns, typeof routes>(routes, {
187
197
  baseUrl: import.meta.env.VITE_API_URL ?? "",
188
198
  });
189
199
 
@@ -202,8 +212,10 @@ try {
202
212
  ## Notes
203
213
 
204
214
  - **Client-safe by construction.** The helper imports only **types** from `@rangojs/router` (erased at build) and builds URLs itself by substituting `:params` into the pattern — it does **not** use `createReverse`, which is a server/RSC-only export that throws in the browser. So `createApiClient` works in client components, workers, and on the server alike.
215
+ - **Import the `urls()` module with `import type`.** `urls()` only runs on the server: in client code a value import throws when the module loads, and it would pull the route handlers into the client bundle. A type import is erased at build, so the same call works in client components, workers and server code. The generated `*.gen.ts` map has no imports and is safe to import anywhere.
216
+ - **Always pass both type arguments.** If you leave them out, `TPatterns` falls back to its constraint and every response is typed `never`, so the first property access on a result is a compile error rather than a silent `any`.
205
217
  - **Params are route-typed; search and body are not.** Path params come from the route pattern (`ExtractParams`), so they are precise and required. `search` is generically typed (`Record<string, string | number | boolean>`), and `body` is `unknown` (serialized to JSON). Typed request **input** needs a declared schema layer, which is intentionally out of scope here — thread per-route schemas in yourself if you want typed search/body.
206
- - **Verb-agnostic wire.** Rango response routes do not dispatch on HTTP method — `.get`/`.post`/etc. set the request method but hit the same handler. Use whichever verb reads best for the operation.
218
+ - **Verb-agnostic wire.** Rango response routes do not dispatch on HTTP method — `.get`/`.post`/etc. set the request method but hit the same handler (branch on `ctx.request.method` inside it if you need to). Use whichever verb reads best for the operation. Response-route `cache()` only serves and stores GET/HEAD.
207
219
  - **Path building.** `fillPath` handles standard `:param`, optional `:param?`, and constrained `:param(a|b)` forms. For exotic patterns or strict trailing-slash policies, swap in your own builder (or the router's `reverse` on the server).
208
220
  - **Want a return-based style instead of throwing?** Branch on `res.ok` yourself: the wire is the bare value on 2xx and `ProblemDetails` on non-2xx (see `/response-routes`). Wrapping the calls in a `{ ok, data } | { ok: false, error }` result type is a small variation on the same helper.
209
221
  - **Third parties.** The typed client is TypeScript-only and needs your route types. External consumers in any language use the plain wire as-is (bare JSON + problem+json); no client required.
@@ -7,9 +7,14 @@ argument-hint: "[setup]"
7
7
  # Breadcrumbs
8
8
 
9
9
  Built-in handle for accumulating breadcrumb items across route segments.
10
- Handlers and loader bodies push items via `ctx.use(Breadcrumbs)`, and they
11
- are collected in parent-to-child order with automatic deduplication by
12
- `href`.
10
+ Route, layout, and parallel-slot handlers and loader bodies push items via
11
+ `ctx.use(Breadcrumbs)`; client components read the collected trail with
12
+ `useHandle(Breadcrumbs)`. Items are collected in parent-to-child order with
13
+ automatic deduplication by `href`.
14
+
15
+ `Breadcrumbs` is exported from both `@rangojs/router` (server, for pushing) and
16
+ `@rangojs/router/client` (for `useHandle`). For page titles and meta tags, use
17
+ the `Meta` handle the same way.
13
18
 
14
19
  ## BreadcrumbItem Type
15
20
 
@@ -59,6 +64,20 @@ export const urlpatterns = urls(({ path, layout }) => [
59
64
 
60
65
  On `/blog/my-post`, breadcrumbs accumulate: `Home > Blog > my-post`.
61
66
 
67
+ The push function also accepts a `Promise` of an item, or an async function
68
+ (called immediately) that returns one:
69
+
70
+ ```typescript
71
+ const breadcrumb = ctx.use(Breadcrumbs);
72
+ breadcrumb(async () => {
73
+ const post = await getPost(ctx.params.slug);
74
+ return { label: post.title, href: `/blog/${post.slug}` };
75
+ });
76
+ ```
77
+
78
+ Either way the item is resolved before any consumer reads it — `useHandle`
79
+ never sees a pending crumb.
80
+
62
81
  ## Pushing from Loaders
63
82
 
64
83
  A data-derived crumb belongs where the data lives — the loader body pushes
@@ -66,6 +85,8 @@ with the same API (handler parity), so routes without handlers (`useLoader`
66
85
  consumption, `clientUrls()` groups) still build trails:
67
86
 
68
87
  ```typescript
88
+ import { Breadcrumbs, createLoader } from "@rangojs/router";
89
+
69
90
  export const ProductLoader = createLoader(async (ctx) => {
70
91
  const product = await getProduct(ctx.params.slug);
71
92
 
@@ -82,7 +103,16 @@ Delivery follows the loader race model: a push that beats the handler barrier
82
103
  is in the SSR'd document; a push after a slow fetch streams and applies
83
104
  client-side (`useHandle` re-renders when it lands). To guarantee document
84
105
  delivery, register the loader as `loader(Def, { ssr: false })` —
85
- see `/loader` → "Writing Handles from Loaders".
106
+ see `/loader` → "Writing Handles from Loaders". On a `ppr` route the race is
107
+ decided at capture: a loader without `ssr: false` never runs there, so its
108
+ crumb is not in the shell and applies client-side on every HIT; with
109
+ `ssr: false` the crumb bakes into the shell (`/ppr` → "On a shell HIT").
110
+ Under `cache()` loader crumbs are not stored with the cached segments; the
111
+ loader re-pushes them on every hit, so they appear once even without an `href`.
112
+ A loader with its own `cache()` keeps its crumbs on its own hits too: they are
113
+ replayed from the loader's cache entry. A crumb from a loader it awaits appears
114
+ once even when a sibling loader or the handler reads that loader too; that live
115
+ run's crumb replaces the cached one in the trail.
86
116
 
87
117
  ## Async Content
88
118
 
@@ -107,6 +137,13 @@ path("/product/:id", async (ctx) => {
107
137
  Async content is a `Promise<ReactNode>`. Resolve it in your component
108
138
  with React's `use()` hook wrapped in `<Suspense>`.
109
139
 
140
+ A crumb the HANDLER pushes is handler output. In a route `cache()` entry and
141
+ in a `ppr` shell it is stored resolved: the cache write, or the shell capture
142
+ (within `ppr.captureTimeout`), waits for a `content` promise and for a
143
+ `.defer()` slot (below), and every HIT replays the crumb as it was resolved
144
+ then, without running the handler. For per-request crumb content, push it
145
+ from a loader without `ssr: false` (see "Pushing from Loaders").
146
+
110
147
  ### Deferred content (decide now, resolve from a deep component)
111
148
 
112
149
  When the handler should DECIDE to push a crumb early but the value is produced
@@ -219,9 +256,11 @@ const count = useHandle(Breadcrumbs, (data) => data.length);
219
256
 
220
257
  ## Deduplication
221
258
 
222
- The built-in collect function deduplicates by `href`. If multiple segments
223
- push the same `href`, the last one wins. This prevents duplicates when
224
- navigating between sibling routes that share a common breadcrumb.
259
+ The built-in collect function deduplicates by `href`: each `href` appears once,
260
+ at the position of its **first** push, with the value of its **last** push. A
261
+ child that re-pushes a parent's `href` therefore updates that crumb's label in
262
+ place without reordering the trail. Items without a string `href` are kept
263
+ as-is and skip deduplication.
225
264
 
226
265
  ## Passing as Props
227
266
 
@@ -246,7 +285,9 @@ function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
246
285
  return (
247
286
  <nav>
248
287
  {crumbs.map((c) => (
249
- <a href={c.href}>{c.label}</a>
288
+ <a key={c.href} href={c.href}>
289
+ {c.label}
290
+ </a>
250
291
  ))}
251
292
  </nav>
252
293
  );
@@ -256,20 +297,31 @@ function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
256
297
  ## Complete Example
257
298
 
258
299
  ```typescript
300
+ // document.tsx — passed to createRouter({ document: Document, ... })
301
+ "use client";
302
+ import type { ReactNode } from "react";
303
+ import { Html } from "@rangojs/router/client";
304
+
305
+ export function Document({ children }: { children: ReactNode }) {
306
+ return (
307
+ <html lang="en">
308
+ <head><Html.Meta /></head>
309
+ <body>{children}</body>
310
+ </html>
311
+ );
312
+ }
313
+
259
314
  // urls.tsx
260
315
  import { urls, Breadcrumbs, Meta } from "@rangojs/router";
261
- import { Outlet, MetaTags } from "@rangojs/router/client";
262
- import { BreadcrumbNav } from "./components/BreadcrumbNav";
316
+ import { Outlet } from "@rangojs/router/client";
317
+ import { BreadcrumbNav } from "./components/BreadcrumbNav"; // the client component above
263
318
 
264
319
  function RootLayout() {
265
320
  return (
266
- <html lang="en">
267
- <head><MetaTags /></head>
268
- <body>
269
- <BreadcrumbNav />
270
- <main><Outlet /></main>
271
- </body>
272
- </html>
321
+ <>
322
+ <BreadcrumbNav />
323
+ <main><Outlet /></main>
324
+ </>
273
325
  );
274
326
  }
275
327
 
@@ -342,9 +394,9 @@ second argument to `createHandle()`:
342
394
  import { createHandle } from "@rangojs/router";
343
395
 
344
396
  // With a collect function (reducer): collect is first arg, tag is second
345
- export const Breadcrumbs = createHandle<BreadcrumbItem, BreadcrumbItem[]>(
346
- collectBreadcrumbs,
347
- "__my_package_breadcrumbs__",
397
+ export const PackageCrumbs = createHandle<CrumbItem, CrumbItem[]>(
398
+ collectCrumbs,
399
+ "__my_package_crumbs__",
348
400
  );
349
401
 
350
402
  // Without a collect function: pass undefined, then the tag
@@ -354,5 +406,6 @@ export const Warnings = createHandle<string>(
354
406
  );
355
407
  ```
356
408
 
357
- The tag must be globally unique and stable across builds. Without it,
358
- `createHandle` throws in development mode.
409
+ The tag must be globally unique and stable across builds. Without it (and
410
+ without a plugin-injected id), `createHandle` throws — in dev and in production
411
+ builds. Only test runners fall back to a synthetic id.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: bundle-analysis
3
- description: Audit a Rango app's production bundle for server-side code leaking into the client, dev/prod React duplication, oversized chunks, and inefficient client-reference grouping. Use when investigating bundle size growth, before a production deploy, or when the client/SSR/RSC output suddenly balloons.
3
+ description: Audit a Rango app's production bundle for server-side code leaking into the client, dev/prod React duplication, oversized chunks, and poor client-chunk grouping. Use when investigating bundle size growth, before a production deploy, or when the client/SSR/RSC output suddenly balloons.
4
4
  argument-hint: "<app-dir>"
5
5
  ---
6
6
 
@@ -13,11 +13,11 @@ Use this when you want **proof** that your Rango app is shipping the bundles you
13
13
  Your app builds in three Vite environments — `client`, `ssr`, and `rsc` — and each ships its own bundle. The most common bundle bugs in a Rango app are:
14
14
 
15
15
  1. **Server code leaking into the client.** A file that imports `node:fs`, calls a database, or contains action logic ends up in the client bundle because a client component pulled it in transitively. Symptom: your client bundle is much larger than expected, sometimes with imports that fail at runtime.
16
- 2. **Both dev and prod React in the SSR/RSC bundle.** When `process.env.NODE_ENV` isn't folded at build time, React's CJS files ship both `.development.js` _and_ `.production.js` variants — doubling React's footprint. The Cloudflare vite plugin folds NODE_ENV automatically; vanilla `vite build` does it for client but not always for SSR/RSC.
16
+ 2. **Both dev and prod React in the SSR/RSC bundle.** When `process.env.NODE_ENV` isn't folded at build time, React's CJS files ship both `.development.js` _and_ `.production.js` variants — doubling React's footprint. The Cloudflare Vite plugin and rango's `vercel` preset fold NODE_ENV automatically; on the default `node` preset, `vite build` does it for the client but not always for SSR/RSC, so add the `define` from Step 2.
17
17
  3. **An oversized routes-manifest in your RSC worker.** The `virtual:rsc-router/routes-manifest/<routerId>` chunk holds your route trie and precomputed entries — large only in proportion to your route count. If it's surprisingly big, you may have unintentionally generated routes (e.g., parametrized fixtures) that bloated the trie.
18
- 4. **Inefficient client-reference grouping.** Each `"use client"` boundary becomes a chunk. Too many small client components = many tiny chunks; one giant client component = one giant chunk that defeats code-splitting.
18
+ 4. **Poor client-chunk grouping.** `rango({ clientChunks })` decides how `"use client"` modules are grouped into browser chunks. The default groups them by route directory (`routes/<id>/**` → `app-<id>`), with flat folders like `src/components/` in one shared chunk; `clientChunks: false` ships one client chunk per router. A route that pulls in unrelated routes' client code, or one oversized shared chunk, points at the grouping.
19
19
 
20
- Tree-shaking does _not_ catch (1) generated data inlined as string literals or (2) data-dependent conditionals like React's. You need a visualizer.
20
+ Tree-shaking does _not_ catch generated data inlined as string literals, or data-dependent conditionals like React's `NODE_ENV` branches. You need a visualizer.
21
21
 
22
22
  ## Step 1: Install the visualizer
23
23
 
@@ -64,9 +64,9 @@ export default defineConfig(({ command }) => ({
64
64
  // your existing plugins...
65
65
  ...analyze(),
66
66
  ],
67
- // For non-Cloudflare apps, fold NODE_ENV explicitly so React's CJS files
68
- // emit only the .production.js variants in SSR/RSC. Skip if your build
69
- // setup already does this (the Cloudflare vite plugin does).
67
+ // For node-preset apps, fold NODE_ENV explicitly so React's CJS files
68
+ // emit only the .production.js variants in SSR/RSC. Skip it on the
69
+ // cloudflare and vercel presets, which already do this.
70
70
  define:
71
71
  command === "build"
72
72
  ? { "process.env.NODE_ENV": JSON.stringify("production") }
@@ -104,12 +104,12 @@ The treemap shows nested boxes; box area = uncompressed size. Hover for gzip/bro
104
104
  **Look for:**
105
105
 
106
106
  - **Your server code.** Any of your own files that contain database queries, secret keys, server actions implementation (not the action _reference_), or `node:` imports. If they appear in the client treemap with non-zero bytes, they leaked. Common causes:
107
- - A shared module that mixes client and server code without a `"use client"` or `"use server"` directive.
107
+ - A shared module that mixes client-safe helpers and server-only code, imported from a `"use client"` component.
108
108
  - A barrel file (`index.ts`) that re-exports both client and server symbols. Tree-shaking should help, but `JSON.parse('{...}')` data and side-effecting top-level statements survive.
109
109
  - Client component imports a server-only utility through an indirect path (e.g., shared types file that pulls server modules).
110
110
  - **Multiple copies of the same package.** Look for two boxes with the same package name but different version paths. Usually means a transitive dep pinned a different version.
111
- - **The `@rangojs/router` chunk** should be roughly **50 KB gzip** (74 files). If significantly larger, you might be importing client-incompatible APIs from the wrong subpath.
112
- - **Per-route client-reference chunks** (named like `chunk-<hash>.js`). Each `"use client"` boundary can become its own chunk. If you have hundreds of tiny chunks, you may have over-split (every leaf component as a client component); if you have one massive 200 KB chunk, you've under-split (a wide client tree behind one boundary).
111
+ - **The router runtime chunk** (`router-<hash>.js`) is around **40–45 KB gzip**; the Rango repository's CI caps it at 44 KB on its reference app. If yours is much larger, check that client components import from `@rangojs/router/client`, not from a server path.
112
+ - **Per-route client chunks** (groups named like `app-<route>`, plus `app-fallback` for error/not-found boundaries). A route's chunk should hold that route's client components. One massive shared chunk usually means client components live in a flat folder (or `clientChunks: false`); move route-specific ones under a route directory or pass a `clientChunks` function (see `/router-setup`).
113
113
 
114
114
  ### Now check `ssr.html`
115
115
 
@@ -123,23 +123,23 @@ The treemap shows nested boxes; box area = uncompressed size. Hover for gzip/bro
123
123
 
124
124
  **Look for:**
125
125
 
126
- - **`virtual:rsc-router/routes-manifest`** should be **tiny** (< 1 KB). If it's > 100 KB, you're on an old version of `@rangojs/router` that inlined the trie eagerly — upgrade to a release that includes commit `d10a2470`.
127
- - **`virtual:rsc-router/routes-manifest/<hash>`** is the lazy per-router chunk. Its size is proportional to your route count. For a typical app: 5–50 KB gzip. For a stress-test app with thousands of routes: hundreds of KB. If yours is unexpectedly huge, check whether you're generating routes you don't need.
126
+ - **`virtual:rsc-router/routes-manifest`** (the eager module) should be **tiny**: well under 2 KB gzip even with tens of thousands of routes. If it is large, route data is being inlined eagerly; report it (see below).
127
+ - **`virtual:rsc-router/routes-manifest/<routerId>`** is the lazy per-router chunk. Its size is proportional to your route count. For a typical app: 5–50 KB gzip. For a stress-test app with thousands of routes: hundreds of KB. If yours is unexpectedly huge, check whether you're generating routes you don't need.
128
128
  - **`<your-router>.named-routes.gen.ts`** — generated route map. Should match your route count.
129
129
  - **Your action and loader implementations** — these run server-side. Expected to be here, not in client.
130
130
  - **Worker-incompatible code** (Node-only imports like `node:fs` that Cloudflare doesn't support). The build will usually fail before the analyzer runs, but if you're seeing runtime errors at the edge, the RSC treemap shows what made it in.
131
131
 
132
132
  ## Step 5: Fix what you find
133
133
 
134
- | Finding | Fix |
135
- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
136
- | Your server code in `client.html` (non-zero bytes) | Audit the import chain. Add `"use server"` to server-only files. Move shared data out of barrel files. Use the `@rangojs/router/server` subpath for explicitly server APIs. |
137
- | Your server code in `client.html` listed but 0 bytes | Tree-shaking already eliminated it. Cosmetic. Leave it. |
138
- | `react-dom-server.edge.development-*.js` in SSR or RSC | Add the `define` block from Step 2 to your vite config. |
139
- | Routes-manifest > 100 KB gzip in RSC eager chunk | Update `@rangojs/router` to a release that includes the lazy-only manifest fix. |
140
- | Same package version present twice | Run `pnpm dedupe` (or `npm dedupe`). If the duplication persists, a transitive dep pins an incompatible version — open a PR upstream or pin the resolution. |
141
- | Client chunk > 500 KB gzip with a single dominant module | That module is your largest client component. Consider lazy-loading via dynamic `import()` or moving non-interactive parts to server components. |
142
- | Hundreds of tiny client chunks | You've sprinkled `"use client"` too liberally. Hoist directives to higher boundaries so React groups them. |
134
+ | Finding | Fix |
135
+ | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
136
+ | Your server code in `client.html` (non-zero bytes) | Audit the import chain from the `"use client"` file. Add `import "server-only"` to server-only modules so the client build fails on the import (`@vitejs/plugin-rsc` checks it; no package install needed). Move shared data out of barrel files. Do not add `"use server"` to a utility module: that turns every export into a client-callable server action. |
137
+ | Your server code in `client.html` listed but 0 bytes | Tree-shaking already eliminated it. Cosmetic. Leave it. |
138
+ | `react-dom-server.edge.development-*.js` in SSR or RSC | Add the `define` block from Step 2 to your vite config. |
139
+ | Eager routes-manifest is large in RSC | Update `@rangojs/router`; if it persists, report it as a regression. |
140
+ | Same package version present twice | Run `pnpm dedupe` (or `npm dedupe`). If the duplication persists, a transitive dep pins an incompatible version — open a PR upstream or pin the resolution. |
141
+ | Client chunk > 500 KB gzip with a single dominant module | That module is your largest client component. Consider lazy-loading via dynamic `import()` or moving non-interactive parts to server components. |
142
+ | One route downloads other routes' client code | Move route-specific client components under a route directory (`routes/<id>/`), or group them yourself with a `clientChunks` function. |
143
143
 
144
144
  ## When to re-run
145
145
 
@@ -156,4 +156,4 @@ If a finding looks like a `@rangojs/router` regression (the framework is shippin
156
156
  - The `@rangojs/router` version (`pnpm why @rangojs/router`).
157
157
  - Your `vite.config.ts`.
158
158
 
159
- The framework maintainers run a similar audit internally — the methodology in this skill mirrors what they use to validate every release.
159
+ The Rango repository runs the same analysis in CI (client leak checks, a router-chunk size cap, and an eager-manifest ceiling).