@rangojs/router 0.24.3 → 0.25.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 (274) hide show
  1. package/README.md +1 -0
  2. package/dist/bin/rango.js +363 -39
  3. package/dist/testing/vitest.js +37 -4
  4. package/dist/types/browser/event-controller.d.ts +24 -5
  5. package/dist/types/browser/intercept-utils.d.ts +5 -2
  6. package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
  7. package/dist/types/browser/navigation-transaction.d.ts +5 -5
  8. package/dist/types/browser/partial-update.d.ts +21 -13
  9. package/dist/types/browser/prefetch/cache.d.ts +8 -0
  10. package/dist/types/browser/rango-marker.d.ts +7 -0
  11. package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
  12. package/dist/types/browser/react/use-handle.d.ts +20 -0
  13. package/dist/types/browser/react/use-href.d.ts +6 -0
  14. package/dist/types/browser/react/use-router.d.ts +6 -1
  15. package/dist/types/browser/response-adapter.d.ts +5 -3
  16. package/dist/types/browser/scroll-restoration.d.ts +9 -3
  17. package/dist/types/browser/segment-reconciler.d.ts +10 -13
  18. package/dist/types/browser/types.d.ts +38 -8
  19. package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
  20. package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
  21. package/dist/types/cache/types.d.ts +8 -0
  22. package/dist/types/client-urls/client-root.d.ts +4 -1
  23. package/dist/types/client-urls/client-urls.d.ts +28 -2
  24. package/dist/types/client-urls/navigation.d.ts +6 -8
  25. package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
  26. package/dist/types/client-urls/server-projection.d.ts +20 -5
  27. package/dist/types/client-urls/types.d.ts +35 -17
  28. package/dist/types/client.d.ts +2 -2
  29. package/dist/types/client.rsc.d.ts +2 -2
  30. package/dist/types/decode-loader-results.d.ts +28 -7
  31. package/dist/types/href-client.d.ts +3 -1
  32. package/dist/types/index.rsc.d.ts +1 -1
  33. package/dist/types/loader-redirect.d.ts +2 -3
  34. package/dist/types/outlet-context.d.ts +7 -6
  35. package/dist/types/prefetch-markers.d.ts +14 -5
  36. package/dist/types/route-content-wrapper.d.ts +17 -14
  37. package/dist/types/route-definition/helpers-types.d.ts +7 -2
  38. package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
  39. package/dist/types/route-definition/rewrite.d.ts +48 -5
  40. package/dist/types/route-types.d.ts +2 -2
  41. package/dist/types/router/handler-context.d.ts +14 -2
  42. package/dist/types/router/intercept-resolution.d.ts +19 -46
  43. package/dist/types/router/lazy-includes.d.ts +7 -0
  44. package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
  45. package/dist/types/router/middleware.d.ts +0 -16
  46. package/dist/types/router/navigation-snapshot.d.ts +2 -0
  47. package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
  48. package/dist/types/router/revalidation.d.ts +8 -1
  49. package/dist/types/router/router-context.d.ts +4 -16
  50. package/dist/types/router/router-interfaces.d.ts +0 -6
  51. package/dist/types/router/router-options.d.ts +4 -7
  52. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
  53. package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
  54. package/dist/types/router/segment-resolution.d.ts +1 -1
  55. package/dist/types/router/segment-wrappers.d.ts +3 -5
  56. package/dist/types/router/substitute-pattern-params.d.ts +19 -2
  57. package/dist/types/router/tracing.d.ts +2 -2
  58. package/dist/types/rsc/full-payload.d.ts +14 -2
  59. package/dist/types/rsc/helpers.d.ts +8 -0
  60. package/dist/types/rsc/manifest-init.d.ts +20 -0
  61. package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
  62. package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
  63. package/dist/types/rsc/rewrite-passes.d.ts +12 -2
  64. package/dist/types/rsc/server-action.d.ts +6 -0
  65. package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
  66. package/dist/types/rsc/shell-capture.d.ts +7 -0
  67. package/dist/types/rsc/types.d.ts +5 -0
  68. package/dist/types/segment-system.d.ts +15 -0
  69. package/dist/types/server/context.d.ts +64 -4
  70. package/dist/types/server/request-context.d.ts +41 -4
  71. package/dist/types/server/request-headers.d.ts +12 -0
  72. package/dist/types/server/rewrite-trail.d.ts +8 -1
  73. package/dist/types/testing/e2e/index.d.ts +1 -1
  74. package/dist/types/testing/flight.entry.d.ts +1 -1
  75. package/dist/types/testing/index.d.ts +1 -1
  76. package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
  77. package/dist/types/testing/render-route.d.ts +21 -11
  78. package/dist/types/testing/run-loader.d.ts +7 -0
  79. package/dist/types/testing/run-middleware.d.ts +7 -0
  80. package/dist/types/testing/run-revalidate.d.ts +5 -4
  81. package/dist/types/testing/serve-shell-request.d.ts +41 -4
  82. package/dist/types/testing/shell-status.d.ts +17 -0
  83. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  84. package/dist/types/testing/vitest.d.ts +10 -1
  85. package/dist/types/thenable-status.d.ts +0 -6
  86. package/dist/types/types/handler-context.d.ts +12 -1
  87. package/dist/types/types/loader-types.d.ts +4 -3
  88. package/dist/types/types/route-entry.d.ts +2 -1
  89. package/dist/types/urls/path-helper-types.d.ts +4 -2
  90. package/dist/types/urls/path-helper.d.ts +5 -0
  91. package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
  92. package/dist/types/vite/discovery/state.d.ts +20 -0
  93. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
  94. package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
  95. package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
  96. package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
  97. package/dist/vite/index.js +6816 -6547
  98. package/package.json +1 -1
  99. package/skills/app-structure/SKILL.md +255 -0
  100. package/skills/app-structure/content-site.md +333 -0
  101. package/skills/app-structure/e-commerce.md +435 -0
  102. package/skills/app-structure/large-app.md +885 -0
  103. package/skills/app-structure/multi-app.md +280 -0
  104. package/skills/breadcrumbs/SKILL.md +6 -1
  105. package/skills/caching/SKILL.md +3 -1
  106. package/skills/catalog.json +12 -0
  107. package/skills/client-urls/SKILL.md +195 -32
  108. package/skills/composability/SKILL.md +10 -8
  109. package/skills/handler-use/SKILL.md +48 -9
  110. package/skills/hooks/handle-and-actions.md +104 -0
  111. package/skills/hooks/navigation.md +24 -10
  112. package/skills/hooks/urls.md +1 -1
  113. package/skills/i18n/SKILL.md +2 -1
  114. package/skills/intercept/SKILL.md +60 -29
  115. package/skills/layout/SKILL.md +6 -3
  116. package/skills/links/SKILL.md +151 -17
  117. package/skills/loader/SKILL.md +32 -10
  118. package/skills/middleware/SKILL.md +111 -15
  119. package/skills/observability/SKILL.md +11 -0
  120. package/skills/parallel/SKILL.md +5 -5
  121. package/skills/ppr/SKILL.md +43 -8
  122. package/skills/prerender/SKILL.md +1 -2
  123. package/skills/rango/SKILL.md +3 -0
  124. package/skills/response-routes/SKILL.md +4 -1
  125. package/skills/scripts/SKILL.md +12 -1
  126. package/skills/server-actions/SKILL.md +60 -20
  127. package/skills/testing/SKILL.md +1 -0
  128. package/skills/testing/client-components.md +13 -10
  129. package/skills/testing/loader.md +25 -24
  130. package/skills/testing/middleware.md +11 -11
  131. package/skills/testing/response-routes.md +4 -1
  132. package/skills/testing/serve-shell-request.md +85 -21
  133. package/skills/ux-architecture/SKILL.md +980 -0
  134. package/skills/view-transitions/SKILL.md +43 -10
  135. package/src/__augment-tests__/augmented.check.ts +28 -0
  136. package/src/browser/event-controller.ts +44 -18
  137. package/src/browser/intercept-utils.ts +3 -2
  138. package/src/browser/merge-segment-loaders.ts +0 -85
  139. package/src/browser/navigation-bridge.ts +119 -98
  140. package/src/browser/navigation-client.ts +20 -15
  141. package/src/browser/navigation-store.ts +4 -2
  142. package/src/browser/navigation-transaction.ts +9 -8
  143. package/src/browser/partial-update.ts +90 -32
  144. package/src/browser/prefetch/cache.ts +8 -0
  145. package/src/browser/prefetch/fetch.ts +13 -3
  146. package/src/browser/rango-marker.ts +13 -0
  147. package/src/browser/react/NavigationProvider.tsx +25 -7
  148. package/src/browser/react/filter-segment-order.ts +13 -0
  149. package/src/browser/react/use-action.ts +9 -15
  150. package/src/browser/react/use-handle.ts +63 -71
  151. package/src/browser/react/use-href.tsx +24 -5
  152. package/src/browser/react/use-link-status.ts +1 -1
  153. package/src/browser/react/use-navigation.ts +30 -40
  154. package/src/browser/react/use-reverse.ts +4 -17
  155. package/src/browser/react/use-router.ts +37 -6
  156. package/src/browser/react/use-segments.ts +8 -12
  157. package/src/browser/response-adapter.ts +11 -5
  158. package/src/browser/rsc-router.tsx +3 -1
  159. package/src/browser/scroll-restoration.ts +34 -20
  160. package/src/browser/segment-reconciler.ts +11 -29
  161. package/src/browser/server-action-bridge.ts +52 -16
  162. package/src/browser/types.ts +40 -8
  163. package/src/build/generate-manifest.ts +12 -1
  164. package/src/build/route-types/codegen.ts +6 -2
  165. package/src/build/route-types/per-module-writer.ts +94 -61
  166. package/src/cache/cache-runtime.ts +9 -0
  167. package/src/cache/cache-scope.ts +2 -1
  168. package/src/cache/cf/cf-cache-store.ts +2 -0
  169. package/src/cache/cf/cf-shell-frame.ts +3 -0
  170. package/src/cache/types.ts +8 -0
  171. package/src/cache/vercel/vercel-cache-store.ts +6 -1
  172. package/src/client-urls/client-root.tsx +10 -5
  173. package/src/client-urls/client-urls.ts +246 -166
  174. package/src/client-urls/navigation.ts +45 -38
  175. package/src/client-urls/revalidate-registry.ts +12 -3
  176. package/src/client-urls/server-projection.ts +286 -75
  177. package/src/client-urls/types.ts +40 -16
  178. package/src/client.rsc.tsx +11 -1
  179. package/src/client.tsx +9 -1
  180. package/src/decode-loader-results.ts +64 -31
  181. package/src/href-client.ts +3 -1
  182. package/src/index.rsc.ts +1 -0
  183. package/src/index.ts +12 -4
  184. package/src/loader-redirect.tsx +2 -3
  185. package/src/outlet-context.ts +7 -6
  186. package/src/prefetch-markers.ts +15 -5
  187. package/src/reverse.ts +3 -3
  188. package/src/route-content-wrapper.tsx +19 -17
  189. package/src/route-definition/dsl-helpers.ts +27 -7
  190. package/src/route-definition/helpers-types.ts +7 -2
  191. package/src/route-definition/resolve-handler-use.ts +23 -10
  192. package/src/route-definition/rewrite.ts +86 -19
  193. package/src/route-types.ts +1 -2
  194. package/src/router/find-match.ts +37 -1
  195. package/src/router/handler-context.ts +85 -40
  196. package/src/router/intercept-resolution.ts +161 -307
  197. package/src/router/lazy-includes.ts +43 -27
  198. package/src/router/loader-resolution.ts +21 -1
  199. package/src/router/manifest.ts +6 -8
  200. package/src/router/match-api.ts +11 -1
  201. package/src/router/match-middleware/background-revalidation.ts +2 -4
  202. package/src/router/match-middleware/cache-lookup.ts +16 -20
  203. package/src/router/match-middleware/intercept-resolution.ts +41 -60
  204. package/src/router/match-result.ts +31 -2
  205. package/src/router/middleware.ts +6 -147
  206. package/src/router/navigation-snapshot.ts +38 -2
  207. package/src/router/prefetch-cache-ttl.ts +10 -18
  208. package/src/router/prerender-match.ts +2 -1
  209. package/src/router/revalidation.ts +24 -3
  210. package/src/router/router-context.ts +5 -18
  211. package/src/router/router-interfaces.ts +0 -7
  212. package/src/router/router-options.ts +4 -7
  213. package/src/router/segment-resolution/fresh.ts +6 -2
  214. package/src/router/segment-resolution/helpers.ts +7 -1
  215. package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
  216. package/src/router/segment-resolution/revalidation.ts +75 -35
  217. package/src/router/segment-resolution.ts +0 -1
  218. package/src/router/segment-wrappers.ts +8 -13
  219. package/src/router/substitute-pattern-params.ts +37 -2
  220. package/src/router/tracing.ts +2 -2
  221. package/src/router.ts +2 -9
  222. package/src/rsc/full-payload.ts +26 -4
  223. package/src/rsc/handler.ts +83 -41
  224. package/src/rsc/helpers.ts +13 -0
  225. package/src/rsc/manifest-init.ts +56 -3
  226. package/src/rsc/prefetch-render-error.ts +38 -0
  227. package/src/rsc/progressive-enhancement.ts +161 -211
  228. package/src/rsc/rewrite-passes.ts +56 -9
  229. package/src/rsc/rsc-rendering.ts +79 -23
  230. package/src/rsc/server-action.ts +25 -2
  231. package/src/rsc/shell-capture-constants.ts +22 -0
  232. package/src/rsc/shell-capture.ts +11 -0
  233. package/src/rsc/types.ts +5 -0
  234. package/src/segment-system.tsx +125 -119
  235. package/src/server/context.ts +96 -5
  236. package/src/server/handle-store.ts +3 -2
  237. package/src/server/request-context.ts +42 -3
  238. package/src/server/request-headers.ts +32 -0
  239. package/src/server/rewrite-trail.ts +55 -9
  240. package/src/ssr/ssr-root.tsx +6 -10
  241. package/src/testing/dispatch.ts +4 -5
  242. package/src/testing/e2e/index.ts +1 -0
  243. package/src/testing/flight.entry.ts +1 -0
  244. package/src/testing/index.ts +1 -0
  245. package/src/testing/internal/client-urls-projection.ts +31 -11
  246. package/src/testing/render-route.tsx +83 -40
  247. package/src/testing/run-loader.ts +26 -2
  248. package/src/testing/run-middleware.ts +10 -0
  249. package/src/testing/run-revalidate.ts +13 -6
  250. package/src/testing/serve-shell-request.ts +77 -4
  251. package/src/testing/shell-status.ts +40 -0
  252. package/src/testing/vitest-stubs/version.ts +1 -0
  253. package/src/testing/vitest.ts +74 -7
  254. package/src/thenable-status.ts +0 -11
  255. package/src/types/handler-context.ts +12 -1
  256. package/src/types/loader-types.ts +4 -3
  257. package/src/types/route-entry.ts +2 -1
  258. package/src/urls/include-helper.ts +38 -11
  259. package/src/urls/path-helper-types.ts +4 -2
  260. package/src/urls/path-helper.ts +16 -3
  261. package/src/vite/discovery/client-urls-projection.ts +205 -16
  262. package/src/vite/discovery/discover-routers.ts +2 -2
  263. package/src/vite/discovery/prerender-collection.ts +2 -6
  264. package/src/vite/discovery/state.ts +26 -0
  265. package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
  266. package/src/vite/plugins/expose-internal-ids.ts +46 -40
  267. package/src/vite/plugins/hoist/source.ts +8 -7
  268. package/src/vite/plugins/transition-when-hoist.ts +1 -0
  269. package/src/vite/plugins/version-plugin.ts +7 -0
  270. package/src/vite/plugins/version.d.ts +7 -0
  271. package/src/vite/plugins/virtual-entries.ts +20 -0
  272. package/src/vite/rango.ts +4 -1
  273. package/src/vite/router-discovery.ts +39 -7
  274. package/src/vite/utils/prerender-utils.ts +17 -0
@@ -0,0 +1,280 @@
1
+ # Several apps under createHostRouter()
2
+
3
+ One deployment, several independently routed apps: marketing on the apex
4
+ (`example.com`, `www.example.com`), the product on `app.`, the staff tools
5
+ on `admin.`, the docs on `docs.`. Each app is an ordinary `createRouter()`
6
+ app with its own `urls()`, Document and generated route map; the host router
7
+ only picks which one handles a request.
8
+
9
+ Reach for this when parts of the product want different documents, chrome,
10
+ deploy cadence or hostnames. Sections that share one document belong in one
11
+ router as `include()`s ([`./large-app.md`](./large-app.md)): a navigation
12
+ between apps is always a document load.
13
+
14
+ References in the router repository: `examples/cloudflare-multi-router` (a
15
+ Cloudflare host entry, path-mounted and host-mounted apps, a shared cache)
16
+ and `packages/rangojs-router/e2e/test-app/.host-fixture` (the Node preset,
17
+ the host-override cookie, two apps on one store).
18
+
19
+ ## File tree
20
+
21
+ ```
22
+ src/
23
+ worker.rsc.tsx the host router (Cloudflare: you own the entry)
24
+ env.ts bindings shared by every app
25
+ shared/
26
+ document.tsx "use client" Document the apps pass to createRouter()
27
+ styles.css one stylesheet (e.g. @import "tailwindcss")
28
+ theme.ts one ThemeConfig
29
+ cache.ts one cache store factory
30
+ origins.ts each app's origin, per environment
31
+ top-nav.tsx components every app renders
32
+ apps/
33
+ site/ router.tsx handler.ts urls.tsx router.named-routes.gen.ts tsconfig.json
34
+ app/ router.tsx handler.ts urls.tsx router.named-routes.gen.ts tsconfig.json …
35
+ admin/ …
36
+ docs/ …
37
+ tsconfig.json the host entry's program only
38
+ ```
39
+
40
+ ## The host router
41
+
42
+ ```tsx
43
+ // src/worker.rsc.tsx
44
+ import {
45
+ createHostRouter,
46
+ defineHosts,
47
+ isNoRouteMatchError,
48
+ } from "@rangojs/router/host";
49
+ import type { AppBindings } from "./env";
50
+
51
+ export const hosts = defineHosts({
52
+ site: [".", "www.*"],
53
+ app: "app.*",
54
+ admin: "admin.*",
55
+ docs: "docs.*",
56
+ });
57
+
58
+ // Dev: every app answers on localhost, and the x-rango-host cookie picks one
59
+ // (x-rango-host=admin.example.com). The request is forwarded unchanged.
60
+ const hostRouter = createHostRouter({
61
+ hostOverride: { cookieName: "x-rango-host", allowedHosts: ["localhost"] },
62
+ });
63
+
64
+ // Lazy mounts: an app's module graph loads on its first request, and each app
65
+ // keeps its own cache version.
66
+ hostRouter.host(hosts.admin).lazy(() => import("./apps/admin/handler"));
67
+ hostRouter.host(hosts.app).lazy(() => import("./apps/app/handler"));
68
+ hostRouter.host(hosts.docs).lazy(() => import("./apps/docs/handler"));
69
+ hostRouter.host(hosts.site).lazy(() => import("./apps/site/handler"));
70
+ // localhost has one label: neither "." nor "*." matches it.
71
+ hostRouter.host(["localhost"]).lazy(() => import("./apps/site/handler"));
72
+
73
+ export default {
74
+ async fetch(request, env, ctx) {
75
+ try {
76
+ return await hostRouter.match(request, { env, ctx });
77
+ } catch (err) {
78
+ if (isNoRouteMatchError(err))
79
+ return new Response("Not Found", { status: 404 });
80
+ throw err;
81
+ }
82
+ },
83
+ } satisfies ExportedHandler<AppBindings>;
84
+ ```
85
+
86
+ Patterns are tried in registration order, first match wins; "apex" means
87
+ exactly two labels (no public-suffix list), so `example.co.uk` needs its own
88
+ pattern (`/host-router` → "Pattern Syntax").
89
+
90
+ On the `node` and `vercel` presets rango owns the entry: export the instance
91
+ (`export default hostRouter`), drop the `fetch` wrapper, and an unmatched host
92
+ answers 404 on its own. Point `rango({ hostRouter: "./src/worker.rsc.tsx" })`
93
+ at the module when auto-detection cannot pick it.
94
+
95
+ ## Each app
96
+
97
+ ```ts
98
+ // src/apps/app/handler.ts: what the host router's lazy mount imports
99
+ import { router } from "./router";
100
+
101
+ export default (request: Request, input: any) => router.fetch(request, input);
102
+ ```
103
+
104
+ ```tsx
105
+ // src/apps/app/router.tsx
106
+ import { createRouter } from "@rangojs/router";
107
+ import { Document } from "../../shared/document";
108
+ import { appCache } from "../../shared/cache";
109
+ import { theme } from "../../shared/theme";
110
+ import type { AppBindings } from "../../env";
111
+ import { loadSession } from "./session";
112
+ import { appPatterns } from "./urls";
113
+
114
+ export const router = createRouter<AppBindings>({
115
+ document: Document,
116
+ cache: appCache,
117
+ theme,
118
+ })
119
+ .use(loadSession)
120
+ .routes(appPatterns);
121
+ ```
122
+
123
+ ```tsx
124
+ // src/apps/app/urls.tsx
125
+ import { urls } from "@rangojs/router";
126
+ import { Outlet } from "@rangojs/router/client";
127
+ import { TopNav } from "../../shared/top-nav";
128
+ import { DashboardPage, LoginPage } from "./pages";
129
+ import { requireUser } from "./session";
130
+
131
+ function Shell() {
132
+ return (
133
+ <>
134
+ <TopNav />
135
+ <Outlet />
136
+ </>
137
+ );
138
+ }
139
+
140
+ export const appPatterns = urls(({ path, layout, middleware }) => [
141
+ path("/login", LoginPage, { name: "login" }),
142
+ layout(Shell, () => [
143
+ middleware(requireUser),
144
+ path("/", DashboardPage, { name: "dashboard" }),
145
+ // … the app's sections as includes, as in ./large-app.md
146
+ ]),
147
+ ]);
148
+ ```
149
+
150
+ Inside an app, everything in the other recipes applies unchanged: the `app.`
151
+ app is [`./large-app.md`](./large-app.md) without its marketing pages, the
152
+ `docs.` app is [`./content-site.md`](./content-site.md), and `site` is a
153
+ handful of `ppr` pages:
154
+
155
+ ```tsx
156
+ // src/apps/site/urls.tsx (excerpt)
157
+ export const sitePatterns = urls(({ path, layout }) => [
158
+ layout(SiteLayout, () => [
159
+ path("/", Home, { name: "home", ppr: true }),
160
+ path("/pricing", Pricing, { name: "pricing", ppr: true }),
161
+ ]),
162
+ ]);
163
+ ```
164
+
165
+ ## What is shared, and how
166
+
167
+ | Shared | How | Notes |
168
+ | ------------- | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
169
+ | Components | ordinary imports from `src/shared/` | One Vite build and one client manifest: a shared client component ships as the same hashed asset to every app. Keep route-name-typed calls (`ctx.reverse("…")`, `href()`) out of shared code; each app's names are its own. |
170
+ | The Document | one `"use client"` component every `createRouter({ document })` passes, or one per app | Each document load is that app's own; apps may differ freely |
171
+ | CSS | `import styles from "../../shared/styles.css?url"` in the Document, `<link precedence>` | Compiles to one hashed asset, cached across apps. Apps need not agree on `precedence`: a cross-app navigation is a document load (`/css`) |
172
+ | Theme | one `ThemeConfig` object passed as `theme` to every router | The stored choice is a host-only cookie (`src/theme/ThemeProvider.tsx`, no `Domain`), so a theme picked on `app.` does not follow the user to `docs.` |
173
+ | Cache store | one factory (`cache: appCache`), one KV namespace | Keys holding one router's output start with that router's id, so two apps never read each other's shells, records or loader entries. Keep `createRouter({ ... })` an inline object literal (or pass `id`) so ids are stable |
174
+ | Session | a cookie on the parent domain, `cookies().set("session", token, { domain: "example.com", … })` | Set by the app that signs users in; every app resolves it in its own `router.use()` |
175
+ | Client caches | not shared | The state cookie is `rango-state_{routerId}`, one per app, so sibling apps on one origin do not collide |
176
+
177
+ ## TypeScript: one program per app
178
+
179
+ Every router's `router.named-routes.gen.ts` augments the one global
180
+ `Rango.GeneratedRouteMap`, so two apps with overlapping route names
181
+ (`home`, `login`) collide in one program. Give each app its own
182
+ `tsconfig.json`, and keep the root program to the host entry:
183
+
184
+ ```jsonc
185
+ // tsconfig.json: the host entry only
186
+ { "extends": "./tsconfig.base.json", "include": ["src/env.ts", "src/worker.rsc.tsx"] }
187
+
188
+ // src/apps/app/tsconfig.json: one app, its generated map, the shared code
189
+ { "extends": "../../../tsconfig.base.json", "include": ["**/*", "../../shared/**/*", "../../env.ts"] }
190
+ ```
191
+
192
+ The root program still type-checks the apps' source through the lazy
193
+ `import()`s, but not their generated maps, as long as no app source imports
194
+ its own `*.named-routes.gen.ts`. Run `tsc -p` for each program in CI.
195
+ Generated and committed: one `router.named-routes.gen.ts` per app (plus
196
+ per-module `*.gen.ts` where an app uses `useReverse(routes)`). See
197
+ `/typesafety` → multi-project setup.
198
+
199
+ ## Dev: the host-override cookie
200
+
201
+ `vite dev` serves every app on one origin, `localhost`. The
202
+ `hostOverride` option picks the app from a cookie when the request's real
203
+ host matches `allowedHosts`:
204
+
205
+ ```js
206
+ // browser devtools, on http://localhost:5173
207
+ document.cookie = "x-rango-host=admin.example.com; path=/";
208
+ ```
209
+
210
+ Reload and the admin app answers; delete the cookie for the site. Without a
211
+ `fallback()`, a cookie the router refuses (a malformed hostname, a host not
212
+ in `allowedHosts`) answers 400 and clears the cookie. Playwright sets it with
213
+ `context.addCookies([{ name: "x-rango-host", value: "admin.example.com", url }])`,
214
+ which is how both reference apps run their dev and production e2e.
215
+
216
+ All apps answer under one origin in this mode, so links to another app's
217
+ production host leave the dev server; switch apps by changing the cookie.
218
+
219
+ ## Links between apps
220
+
221
+ A navigation from one app to another is a full document load, by design. A
222
+ soft in-place swap between apps shipped once (#476) and was replaced by a
223
+ document navigation (#555): a stylesheet two apps share by `href` could drop
224
+ on the swap (React dedups stylesheets by `href`), and the theme, warmup and
225
+ prefetch settings, which are document-lifetime, would remount or never take
226
+ effect. A document load re-establishes the target app's whole document.
227
+
228
+ | The other app is | Link with | What happens |
229
+ | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
230
+ | On another host (`app.example.com`) | an absolute URL: `<Link to="https://app.example.com/">` or `<a href>` | Another origin is external to `<Link>` (`src/browser/react/Link.tsx`) and to the plain-anchor interceptor: the browser navigates, nothing is prefetched |
231
+ | On the same origin under a path (`host(["example.com/docs"])`) | `<Link to="/docs" reloadDocument prefetch="none">` or `<a href="/docs" data-no-intercept="true">` | A document navigation at the click. A plain same-origin `<Link>` also ends in a document load, after a wasted round trip: the server answers the partial request with `X-RSC-Reload` |
232
+
233
+ ```tsx
234
+ // src/shared/origins.ts
235
+ export const ORIGINS = {
236
+ site: import.meta.env.VITE_SITE_ORIGIN ?? "https://example.com",
237
+ app: import.meta.env.VITE_APP_ORIGIN ?? "https://app.example.com",
238
+ docs: import.meta.env.VITE_DOCS_ORIGIN ?? "https://docs.example.com",
239
+ } as const;
240
+
241
+ // src/shared/top-nav.tsx: a server component every app renders
242
+ import { Link } from "@rangojs/router/client";
243
+ import { ORIGINS } from "./origins";
244
+
245
+ export function TopNav() {
246
+ return (
247
+ <nav>
248
+ <Link to={`${ORIGINS.site}/`}>Home</Link>
249
+ <Link to={`${ORIGINS.docs}/`}>Docs</Link>
250
+ <Link to={`${ORIGINS.app}/`}>Open the app</Link>
251
+ </nav>
252
+ );
253
+ }
254
+ ```
255
+
256
+ `reloadDocument` alone does not stop the prefetch: without
257
+ `prefetch="none"`, a visible link still sends one, which the other app
258
+ answers with `X-RSC-Reload` and the client drops. Make each app's entry pages
259
+ fast as documents (`ppr`, or `Prerender`), since every cross-app click lands
260
+ on one.
261
+
262
+ ## Deploys
263
+
264
+ Each `createRouter()` app has its own cache version, computed from the code
265
+ its router can run up to a lazy mount. A deploy that changes the admin app's
266
+ server code clears the admin app's cached data and stored HTML and reloads
267
+ its open tabs; the other apps keep theirs. Code shared by statically
268
+ importing modules moves every app that imports it (`/host-router` →
269
+ "Deploys and the cache").
270
+
271
+ ## Why
272
+
273
+ | Choice | Why |
274
+ | ---------------------------- | --------------------------------------------------------------------------------------------- |
275
+ | One `createRouter()` per app | Own document, routes, names, middleware, cache version |
276
+ | `.lazy()` mounts | An app's graph loads on its first request, and the mount is where its cache version stops |
277
+ | `defineHosts` | One typed table of patterns, reused by tests |
278
+ | The cookie override in dev | One dev origin, hydration same-origin; the reference apps' e2e drive every app through it |
279
+ | Absolute URLs between hosts | Another origin is another document; no request is wasted on a prefetch or a reload round trip |
280
+ | A program per app | Route-name augmentations of different apps cannot collide |
@@ -137,6 +137,11 @@ path("/product/:id", async (ctx) => {
137
137
  Async content is a `Promise<ReactNode>`. Resolve it in your component
138
138
  with React's `use()` hook wrapped in `<Suspense>`.
139
139
 
140
+ A server component that pushes a crumb after an `await` throws
141
+ `LateHandlePushError`. Reserve the crumb in the handler with `.defer()` and
142
+ resolve it from the component (see "Deferred content" below); the same works
143
+ for a `Meta` title.
144
+
140
145
  A crumb the HANDLER pushes is handler output. In a route `cache()` entry and
141
146
  in a `ppr` shell it is stored resolved: the cache write, or the shell capture
142
147
  (within `ppr.captureTimeout`), waits for a `content` promise and for a
@@ -202,7 +207,7 @@ consumer sees it — `useHandle(Breadcrumbs)` returns the resolved item, never a
202
207
  `Promise`, so you read it like any sync crumb (no `use()`, no thenable narrowing).
203
208
  On a full/SSR load the value is resolved server-side; on a soft navigation the
204
209
  breadcrumbs HOLD the previous resolved value until the deferred value lands, then
205
- swap in — no blank, no pending entry. If the slot times out to `else: null`/
210
+ swap in — no blank, no pending entry. The hold-then-swap is deliberate: a handle never holds the page and never shows the next page's values over the old one (see [hooks](../hooks/handle-and-actions.md#when-handle-values-change-by-design)). If the slot times out to `else: null`/
206
211
  undefined, the entry is simply dropped. Use `.defer()` only when even
207
212
  `label`/`href` are unknown at handler time AND the producer is a component,
208
213
  not a loader — a loader-produced crumb pushes directly from the loader body
@@ -894,7 +894,9 @@ and a read that worked only on direct visits would break the first time a
894
894
  rewritten request reached it. A PPR shell capture and a render the document
895
895
  cache stores read it as `undefined` instead of throwing: those renders are
896
896
  the target page as a direct visit renders it, and the rewritten visitor's
897
- own render (outside them) still sees its URL. A response route under
897
+ own render (outside them) still sees its URL. A shell keyed by the visitor's
898
+ URL (`rewrite(...).shell("visitor")`, `/ppr`) reads it, since its key covers
899
+ that URL. A response route under
898
900
  `cache()` whose handler read it is not stored.
899
901
 
900
902
  A loader body invoked from a handler with `await ctx.use(Loader)` may read
@@ -9,6 +9,12 @@
9
9
  "argumentHint": "",
10
10
  "path": "skills/api-client/SKILL.md"
11
11
  },
12
+ {
13
+ "name": "app-structure",
14
+ "description": "Concrete urls() layouts for whole Rango apps, with the file tree, router.tsx, the root urls.tsx with sync include() of section modules, and where middleware, server layouts, clientUrls() groups, response routes, caching, prerender, PPR and boundaries sit. Recipes for a large signed-in app with a protected admin area (auth, onboarding, app, settings and admin as clientUrls() groups under one server shell), several apps under createHostRouter(), a docs site or blog, and a shop. Use when starting or restructuring a Rango app, laying out urls() files, splitting an app into sections or separate apps, or setting up a dashboard, SaaS, multi-app, static or content site, or e-commerce app.",
15
+ "argumentHint": "[large-app|multi-app|content-site|e-commerce]",
16
+ "path": "skills/app-structure/SKILL.md"
17
+ },
12
18
  {
13
19
  "name": "breadcrumbs",
14
20
  "description": "Built-in Breadcrumbs handle for accumulating breadcrumb navigation across route segments. Use when building a breadcrumb trail for nested routes, or asking how to show the current navigation path in a layout.",
@@ -273,6 +279,12 @@
273
279
  "argumentHint": "[profile-name]",
274
280
  "path": "skills/use-cache/SKILL.md"
275
281
  },
282
+ {
283
+ "name": "ux-architecture",
284
+ "description": "Architect a Rango app so navigation is instant and stable — when a server urls() page or a clientUrls() group wins, where layouts, loaders, Suspense boundaries, revalidate() rules, prefetch, transition(), intercept() modals and actions go, with worked recipes (shop, dashboard tabs, settings forms, search filters, a modal over a list) and the anti-patterns behind remounts, skeleton flashes and slow clicks. Use when designing or reviewing a Rango app's routing, layout, loading or navigation UX, choosing between urls() and clientUrls(), placing a loader or a boundary, writing revalidate() rules, or debugging a remount, a re-shown fallback or a click that waits.",
285
+ "argumentHint": "[shop|dashboard|settings|search|modal]",
286
+ "path": "skills/ux-architecture/SKILL.md"
287
+ },
276
288
  {
277
289
  "name": "vercel",
278
290
  "description": "Deploy a Rango app to Vercel Functions (Build Output API v3). Use when deploying a Rango app to Vercel, or asking how the router works with Vercel Functions and the Build Output API.",