@rangojs/router 0.0.0-experimental.20 → 0.0.0-experimental.20dbba0c

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 (189) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +172 -50
  3. package/dist/bin/rango.js +138 -50
  4. package/dist/vite/index.js +1160 -508
  5. package/dist/vite/index.js.bak +5448 -0
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +17 -16
  8. package/skills/breadcrumbs/SKILL.md +252 -0
  9. package/skills/cache-guide/SKILL.md +32 -0
  10. package/skills/caching/SKILL.md +49 -8
  11. package/skills/document-cache/SKILL.md +2 -2
  12. package/skills/handler-use/SKILL.md +362 -0
  13. package/skills/hooks/SKILL.md +61 -51
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +20 -0
  16. package/skills/layout/SKILL.md +22 -0
  17. package/skills/links/SKILL.md +91 -17
  18. package/skills/loader/SKILL.md +107 -24
  19. package/skills/middleware/SKILL.md +34 -3
  20. package/skills/migrate-nextjs/SKILL.md +560 -0
  21. package/skills/migrate-react-router/SKILL.md +765 -0
  22. package/skills/parallel/SKILL.md +185 -0
  23. package/skills/prerender/SKILL.md +112 -70
  24. package/skills/rango/SKILL.md +24 -23
  25. package/skills/response-routes/SKILL.md +8 -0
  26. package/skills/route/SKILL.md +58 -4
  27. package/skills/router-setup/SKILL.md +95 -5
  28. package/skills/streams-and-websockets/SKILL.md +283 -0
  29. package/skills/typesafety/SKILL.md +38 -24
  30. package/src/__internal.ts +92 -0
  31. package/src/browser/app-shell.ts +52 -0
  32. package/src/browser/app-version.ts +14 -0
  33. package/src/browser/event-controller.ts +5 -0
  34. package/src/browser/link-interceptor.ts +4 -0
  35. package/src/browser/navigation-bridge.ts +175 -17
  36. package/src/browser/navigation-client.ts +177 -44
  37. package/src/browser/navigation-store.ts +68 -9
  38. package/src/browser/navigation-transaction.ts +11 -9
  39. package/src/browser/partial-update.ts +113 -17
  40. package/src/browser/prefetch/cache.ts +275 -28
  41. package/src/browser/prefetch/fetch.ts +191 -46
  42. package/src/browser/prefetch/policy.ts +6 -0
  43. package/src/browser/prefetch/queue.ts +123 -20
  44. package/src/browser/prefetch/resource-ready.ts +77 -0
  45. package/src/browser/rango-state.ts +53 -13
  46. package/src/browser/react/Link.tsx +98 -14
  47. package/src/browser/react/NavigationProvider.tsx +89 -14
  48. package/src/browser/react/context.ts +7 -2
  49. package/src/browser/react/use-handle.ts +9 -58
  50. package/src/browser/react/use-navigation.ts +22 -2
  51. package/src/browser/react/use-params.ts +11 -1
  52. package/src/browser/react/use-router.ts +29 -9
  53. package/src/browser/rsc-router.tsx +177 -66
  54. package/src/browser/scroll-restoration.ts +41 -42
  55. package/src/browser/segment-reconciler.ts +36 -9
  56. package/src/browser/server-action-bridge.ts +8 -6
  57. package/src/browser/types.ts +73 -5
  58. package/src/build/generate-manifest.ts +6 -6
  59. package/src/build/generate-route-types.ts +3 -0
  60. package/src/build/route-trie.ts +67 -25
  61. package/src/build/route-types/include-resolution.ts +8 -1
  62. package/src/build/route-types/router-processing.ts +223 -74
  63. package/src/build/route-types/scan-filter.ts +8 -1
  64. package/src/cache/cache-runtime.ts +15 -11
  65. package/src/cache/cache-scope.ts +48 -7
  66. package/src/cache/cf/cf-cache-store.ts +455 -15
  67. package/src/cache/cf/index.ts +5 -1
  68. package/src/cache/document-cache.ts +17 -7
  69. package/src/cache/index.ts +1 -0
  70. package/src/cache/taint.ts +55 -0
  71. package/src/client.rsc.tsx +2 -1
  72. package/src/client.tsx +85 -276
  73. package/src/context-var.ts +72 -2
  74. package/src/debug.ts +2 -2
  75. package/src/handle.ts +40 -0
  76. package/src/handles/breadcrumbs.ts +66 -0
  77. package/src/handles/index.ts +1 -0
  78. package/src/host/index.ts +0 -3
  79. package/src/index.rsc.ts +9 -36
  80. package/src/index.ts +79 -70
  81. package/src/outlet-context.ts +1 -1
  82. package/src/prerender/store.ts +57 -15
  83. package/src/prerender.ts +138 -77
  84. package/src/response-utils.ts +28 -0
  85. package/src/reverse.ts +27 -2
  86. package/src/route-definition/dsl-helpers.ts +240 -40
  87. package/src/route-definition/helpers-types.ts +67 -19
  88. package/src/route-definition/index.ts +3 -3
  89. package/src/route-definition/redirect.ts +11 -3
  90. package/src/route-definition/resolve-handler-use.ts +155 -0
  91. package/src/route-map-builder.ts +7 -1
  92. package/src/route-types.ts +18 -0
  93. package/src/router/content-negotiation.ts +100 -1
  94. package/src/router/find-match.ts +4 -2
  95. package/src/router/handler-context.ts +129 -26
  96. package/src/router/intercept-resolution.ts +11 -4
  97. package/src/router/lazy-includes.ts +10 -7
  98. package/src/router/loader-resolution.ts +160 -22
  99. package/src/router/logging.ts +5 -2
  100. package/src/router/manifest.ts +31 -16
  101. package/src/router/match-api.ts +128 -193
  102. package/src/router/match-middleware/background-revalidation.ts +30 -2
  103. package/src/router/match-middleware/cache-lookup.ts +94 -17
  104. package/src/router/match-middleware/cache-store.ts +53 -10
  105. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  106. package/src/router/match-middleware/segment-resolution.ts +61 -5
  107. package/src/router/match-result.ts +103 -18
  108. package/src/router/metrics.ts +238 -13
  109. package/src/router/middleware-types.ts +48 -27
  110. package/src/router/middleware.ts +201 -86
  111. package/src/router/navigation-snapshot.ts +182 -0
  112. package/src/router/pattern-matching.ts +77 -11
  113. package/src/router/prerender-match.ts +114 -10
  114. package/src/router/preview-match.ts +30 -102
  115. package/src/router/request-classification.ts +310 -0
  116. package/src/router/revalidation.ts +27 -7
  117. package/src/router/route-snapshot.ts +245 -0
  118. package/src/router/router-context.ts +6 -1
  119. package/src/router/router-interfaces.ts +50 -5
  120. package/src/router/router-options.ts +50 -19
  121. package/src/router/segment-resolution/fresh.ts +215 -19
  122. package/src/router/segment-resolution/helpers.ts +30 -25
  123. package/src/router/segment-resolution/loader-cache.ts +1 -0
  124. package/src/router/segment-resolution/revalidation.ts +454 -301
  125. package/src/router/segment-wrappers.ts +2 -0
  126. package/src/router/trie-matching.ts +30 -6
  127. package/src/router/types.ts +1 -0
  128. package/src/router/url-params.ts +49 -0
  129. package/src/router.ts +89 -17
  130. package/src/rsc/handler.ts +563 -364
  131. package/src/rsc/helpers.ts +69 -41
  132. package/src/rsc/index.ts +0 -20
  133. package/src/rsc/loader-fetch.ts +23 -3
  134. package/src/rsc/manifest-init.ts +5 -1
  135. package/src/rsc/progressive-enhancement.ts +37 -10
  136. package/src/rsc/response-route-handler.ts +14 -1
  137. package/src/rsc/rsc-rendering.ts +47 -44
  138. package/src/rsc/server-action.ts +24 -10
  139. package/src/rsc/ssr-setup.ts +128 -0
  140. package/src/rsc/types.ts +11 -1
  141. package/src/search-params.ts +16 -13
  142. package/src/segment-content-promise.ts +67 -0
  143. package/src/segment-loader-promise.ts +122 -0
  144. package/src/segment-system.tsx +109 -23
  145. package/src/server/context.ts +174 -19
  146. package/src/server/handle-store.ts +19 -0
  147. package/src/server/loader-registry.ts +9 -8
  148. package/src/server/request-context.ts +218 -65
  149. package/src/server.ts +6 -0
  150. package/src/ssr/index.tsx +4 -0
  151. package/src/static-handler.ts +18 -6
  152. package/src/theme/index.ts +4 -13
  153. package/src/types/cache-types.ts +4 -4
  154. package/src/types/handler-context.ts +140 -72
  155. package/src/types/loader-types.ts +41 -15
  156. package/src/types/request-scope.ts +126 -0
  157. package/src/types/route-config.ts +17 -8
  158. package/src/types/route-entry.ts +19 -1
  159. package/src/types/segments.ts +2 -5
  160. package/src/urls/include-helper.ts +24 -14
  161. package/src/urls/path-helper-types.ts +39 -6
  162. package/src/urls/path-helper.ts +48 -13
  163. package/src/urls/pattern-types.ts +12 -0
  164. package/src/urls/response-types.ts +18 -16
  165. package/src/use-loader.tsx +77 -5
  166. package/src/vite/discovery/bundle-postprocess.ts +61 -89
  167. package/src/vite/discovery/discover-routers.ts +7 -4
  168. package/src/vite/discovery/prerender-collection.ts +162 -88
  169. package/src/vite/discovery/state.ts +17 -13
  170. package/src/vite/index.ts +8 -3
  171. package/src/vite/plugin-types.ts +51 -79
  172. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  173. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  174. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  175. package/src/vite/plugins/expose-action-id.ts +1 -3
  176. package/src/vite/plugins/expose-id-utils.ts +12 -0
  177. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  178. package/src/vite/plugins/expose-internal-ids.ts +257 -40
  179. package/src/vite/plugins/performance-tracks.ts +88 -0
  180. package/src/vite/plugins/refresh-cmd.ts +127 -0
  181. package/src/vite/plugins/version-plugin.ts +13 -1
  182. package/src/vite/rango.ts +190 -217
  183. package/src/vite/router-discovery.ts +241 -45
  184. package/src/vite/utils/banner.ts +4 -4
  185. package/src/vite/utils/package-resolution.ts +34 -1
  186. package/src/vite/utils/prerender-utils.ts +97 -5
  187. package/src/vite/utils/shared-utils.ts +3 -2
  188. package/skills/testing/SKILL.md +0 -226
  189. package/src/route-definition/route-function.ts +0 -119
@@ -0,0 +1,76 @@
1
+ // Node ESM loader hook that resolves `cloudflare:*` imports to the same
2
+ // stub ESM the Vite transform produces for rewritten specifiers.
3
+ //
4
+ // Why both? The Vite transform (cloudflare-protocol-stub.ts) catches
5
+ // imports in modules that flow through Vite's plugin pipeline — covers
6
+ // user source and any node_modules package Vite fetches and transforms.
7
+ // But Vite/Rollup externalize certain packages (e.g. `partyserver`,
8
+ // which has `import { DurableObject, env } from "cloudflare:workers"`
9
+ // at its top level, and similar "workerd-native" libraries). Externalized
10
+ // modules bypass the transform: Rollup hands their resolution to Node's
11
+ // native ESM loader, which rejects URL-scheme specifiers. This loader
12
+ // hook registers via `module.register()` from `createTempRscServer` and
13
+ // intercepts `cloudflare:*` at Node's resolve layer — before the default
14
+ // loader throws ERR_UNSUPPORTED_ESM_URL_SCHEME.
15
+ //
16
+ // Lifecycle: the hook runs in a dedicated worker thread (Node ESM loader
17
+ // architecture) with its own globalThis. It cannot see the main thread's
18
+ // `__rango_build_env__` bridge, so the `env` export here is always `{}`.
19
+ // That's fine in practice — externalized libraries don't typically touch
20
+ // `env` at module top level; they read it at request time in workerd
21
+ // where the real module exists. Build-time prerender handlers in user
22
+ // source DO read `env`, but they flow through the Vite transform (which
23
+ // does bridge `env` from `getPlatformProxy()`), not through this loader.
24
+ //
25
+ // Keep STUBS in sync with cloudflare-protocol-stub.ts — both paths need
26
+ // to hand out the same base classes.
27
+
28
+ const CF_PREFIX = "cloudflare:";
29
+
30
+ const STUBS = {
31
+ "cloudflare:workers": `
32
+ export class DurableObject { constructor(_ctx, _env) {} }
33
+ export class WorkerEntrypoint { constructor(_ctx, _env) {} }
34
+ export class WorkflowEntrypoint { constructor(_ctx, _env) {} }
35
+ export class RpcTarget {}
36
+ export const env = {};
37
+ export default {};
38
+ `,
39
+ "cloudflare:email": `
40
+ export class EmailMessage { constructor(_from, _to, _raw) {} }
41
+ export default {};
42
+ `,
43
+ "cloudflare:sockets": `
44
+ export function connect() { return {}; }
45
+ export default {};
46
+ `,
47
+ "cloudflare:workflows": `
48
+ export class NonRetryableError extends Error {
49
+ constructor(message, name) { super(message); this.name = name ?? "NonRetryableError"; }
50
+ }
51
+ export default {};
52
+ `,
53
+ };
54
+
55
+ // Policy: unknown `cloudflare:*` specifiers resolve permissively to an
56
+ // empty default export rather than throwing. Same reasoning as
57
+ // cloudflare-protocol-stub.ts's FALLBACK_STUB — we prioritize
58
+ // dependency-graph resilience over strict validation, because third-party
59
+ // packages can pull `cloudflare:*` modules we haven't curated.
60
+ const FALLBACK_STUB = `export default {};\n`;
61
+
62
+ function dataUrlFor(specifier) {
63
+ const body = STUBS[specifier] ?? FALLBACK_STUB;
64
+ return "data:text/javascript;base64," + Buffer.from(body).toString("base64");
65
+ }
66
+
67
+ export async function resolve(specifier, context, nextResolve) {
68
+ if (specifier.startsWith(CF_PREFIX)) {
69
+ return {
70
+ shortCircuit: true,
71
+ url: dataUrlFor(specifier),
72
+ format: "module",
73
+ };
74
+ }
75
+ return nextResolve(specifier, context);
76
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.0.0-experimental.20",
3
+ "version": "0.0.0-experimental.20dbba0c",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -132,8 +132,17 @@
132
132
  "access": "public",
133
133
  "tag": "experimental"
134
134
  },
135
+ "scripts": {
136
+ "build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
137
+ "prepublishOnly": "pnpm build",
138
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit",
139
+ "test": "playwright test",
140
+ "test:ui": "playwright test --ui",
141
+ "test:unit": "vitest run",
142
+ "test:unit:watch": "vitest"
143
+ },
135
144
  "dependencies": {
136
- "@vitejs/plugin-rsc": "^0.5.14",
145
+ "@vitejs/plugin-rsc": "^0.5.23",
137
146
  "magic-string": "^0.30.17",
138
147
  "picomatch": "^4.0.3",
139
148
  "rsc-html-stream": "^0.0.7"
@@ -141,19 +150,19 @@
141
150
  "devDependencies": {
142
151
  "@playwright/test": "^1.49.1",
143
152
  "@types/node": "^24.10.1",
144
- "@types/react": "^19.2.7",
145
- "@types/react-dom": "^19.2.3",
153
+ "@types/react": "catalog:",
154
+ "@types/react-dom": "catalog:",
146
155
  "esbuild": "^0.27.0",
147
156
  "jiti": "^2.6.1",
148
- "react": "^19.2.4",
149
- "react-dom": "^19.2.4",
157
+ "react": "catalog:",
158
+ "react-dom": "catalog:",
150
159
  "tinyexec": "^0.3.2",
151
160
  "typescript": "^5.3.0",
152
161
  "vitest": "^4.0.0"
153
162
  },
154
163
  "peerDependencies": {
155
164
  "@cloudflare/vite-plugin": "^1.25.0",
156
- "@vitejs/plugin-rsc": "^0.5.14",
165
+ "@vitejs/plugin-rsc": "^0.5.23",
157
166
  "react": "^18.0.0 || ^19.0.0",
158
167
  "vite": "^7.3.0"
159
168
  },
@@ -164,13 +173,5 @@
164
173
  "vite": {
165
174
  "optional": true
166
175
  }
167
- },
168
- "scripts": {
169
- "build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
170
- "typecheck": "tsc --noEmit",
171
- "test": "playwright test",
172
- "test:ui": "playwright test --ui",
173
- "test:unit": "vitest run",
174
- "test:unit:watch": "vitest"
175
176
  }
176
- }
177
+ }
@@ -0,0 +1,252 @@
1
+ ---
2
+ name: breadcrumbs
3
+ description: Built-in Breadcrumbs handle for accumulating breadcrumb navigation across route segments
4
+ argument-hint: [setup]
5
+ ---
6
+
7
+ # Breadcrumbs
8
+
9
+ Built-in handle for accumulating breadcrumb items across route segments.
10
+ Each layout/route pushes items via `ctx.use(Breadcrumbs)`, and they are
11
+ collected in parent-to-child order with automatic deduplication by `href`.
12
+
13
+ ## BreadcrumbItem Type
14
+
15
+ ```typescript
16
+ interface BreadcrumbItem {
17
+ label: string; // Display text
18
+ href: string; // URL the breadcrumb links to
19
+ content?: ReactNode | Promise<ReactNode>; // Optional extra content (sync or async)
20
+ }
21
+ ```
22
+
23
+ ## Pushing Breadcrumbs (Server)
24
+
25
+ Import `Breadcrumbs` from `@rangojs/router` in RSC/server context:
26
+
27
+ ```typescript
28
+ import { urls, Breadcrumbs } from "@rangojs/router";
29
+ import { Outlet } from "@rangojs/router/client";
30
+
31
+ export const urlpatterns = urls(({ path, layout }) => [
32
+ // Root layout pushes "Home"
33
+ layout((ctx) => {
34
+ const breadcrumb = ctx.use(Breadcrumbs);
35
+ breadcrumb({ label: "Home", href: "/" });
36
+ return <RootLayout />;
37
+ }, () => [
38
+ path("/", HomePage, { name: "home" }),
39
+
40
+ // Nested layout pushes "Blog"
41
+ layout((ctx) => {
42
+ const breadcrumb = ctx.use(Breadcrumbs);
43
+ breadcrumb({ label: "Blog", href: "/blog" });
44
+ return <BlogLayout />;
45
+ }, () => [
46
+ path("/blog", BlogIndex, { name: "blog.index" }),
47
+
48
+ // Route handler pushes post title
49
+ path("/blog/:slug", (ctx) => {
50
+ const breadcrumb = ctx.use(Breadcrumbs);
51
+ breadcrumb({ label: ctx.params.slug, href: `/blog/${ctx.params.slug}` });
52
+ return <BlogPost slug={ctx.params.slug} />;
53
+ }, { name: "blog.post" }),
54
+ ]),
55
+ ]),
56
+ ]);
57
+ ```
58
+
59
+ On `/blog/my-post`, breadcrumbs accumulate: `Home > Blog > my-post`.
60
+
61
+ ## Async Content
62
+
63
+ The `content` field supports `Promise<ReactNode>` for streaming:
64
+
65
+ ```typescript
66
+ path("/product/:id", async (ctx) => {
67
+ const breadcrumb = ctx.use(Breadcrumbs);
68
+ const productPromise = fetchProduct(ctx.params.id);
69
+
70
+ breadcrumb({
71
+ label: "Product",
72
+ href: `/product/${ctx.params.id}`,
73
+ content: productPromise.then((p) => <span>({p.category})</span>),
74
+ });
75
+
76
+ const product = await productPromise;
77
+ return <ProductPage product={product} />;
78
+ }, { name: "product" })
79
+ ```
80
+
81
+ Async content is a `Promise<ReactNode>`. Resolve it in your component
82
+ with React's `use()` hook wrapped in `<Suspense>`.
83
+
84
+ ## Consuming Breadcrumbs (Client)
85
+
86
+ Use `useHandle(Breadcrumbs)` in a client component to read the accumulated items:
87
+
88
+ ```tsx
89
+ "use client";
90
+ import { useHandle, Breadcrumbs, Link } from "@rangojs/router/client";
91
+
92
+ function BreadcrumbNav() {
93
+ const breadcrumbs = useHandle(Breadcrumbs);
94
+
95
+ if (!breadcrumbs.length) return null;
96
+
97
+ return (
98
+ <nav aria-label="Breadcrumb">
99
+ <ol>
100
+ {breadcrumbs.map((crumb, i) => (
101
+ <li key={crumb.href}>
102
+ {i === breadcrumbs.length - 1 ? (
103
+ <span aria-current="page">{crumb.label}</span>
104
+ ) : (
105
+ <Link to={crumb.href}>{crumb.label}</Link>
106
+ )}
107
+ </li>
108
+ ))}
109
+ </ol>
110
+ </nav>
111
+ );
112
+ }
113
+ ```
114
+
115
+ ### With Selector
116
+
117
+ Re-render only when the selected value changes:
118
+
119
+ ```tsx
120
+ // Only the last breadcrumb
121
+ const current = useHandle(Breadcrumbs, (data) => data.at(-1));
122
+
123
+ // Breadcrumb count
124
+ const count = useHandle(Breadcrumbs, (data) => data.length);
125
+ ```
126
+
127
+ ## Deduplication
128
+
129
+ The built-in collect function deduplicates by `href`. If multiple segments
130
+ push the same `href`, the last one wins. This prevents duplicates when
131
+ navigating between sibling routes that share a common breadcrumb.
132
+
133
+ ## Passing as Props
134
+
135
+ Breadcrumbs handle can be passed from server to client components:
136
+
137
+ ```tsx
138
+ // Server component
139
+ path("/dashboard", (ctx) => {
140
+ const breadcrumb = ctx.use(Breadcrumbs);
141
+ breadcrumb({ label: "Dashboard", href: "/dashboard" });
142
+ return <DashboardNav handle={Breadcrumbs} />;
143
+ });
144
+ ```
145
+
146
+ ```tsx
147
+ // Client component
148
+ "use client";
149
+ import { useHandle, type Breadcrumbs } from "@rangojs/router/client";
150
+
151
+ function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
152
+ const crumbs = useHandle(handle);
153
+ return (
154
+ <nav>
155
+ {crumbs.map((c) => (
156
+ <a href={c.href}>{c.label}</a>
157
+ ))}
158
+ </nav>
159
+ );
160
+ }
161
+ ```
162
+
163
+ ## Complete Example
164
+
165
+ ```typescript
166
+ // urls.tsx
167
+ import { urls, Breadcrumbs, Meta } from "@rangojs/router";
168
+ import { Outlet, MetaTags } from "@rangojs/router/client";
169
+ import { BreadcrumbNav } from "./components/BreadcrumbNav";
170
+
171
+ function RootLayout() {
172
+ return (
173
+ <html lang="en">
174
+ <head><MetaTags /></head>
175
+ <body>
176
+ <BreadcrumbNav />
177
+ <main><Outlet /></main>
178
+ </body>
179
+ </html>
180
+ );
181
+ }
182
+
183
+ export const urlpatterns = urls(({ path, layout }) => [
184
+ layout((ctx) => {
185
+ ctx.use(Breadcrumbs)({ label: "Home", href: "/" });
186
+ ctx.use(Meta)({ title: "My App" });
187
+ return <RootLayout />;
188
+ }, () => [
189
+ path("/", () => <h1>Welcome</h1>, { name: "home" }),
190
+
191
+ layout((ctx) => {
192
+ ctx.use(Breadcrumbs)({ label: "Shop", href: "/shop" });
193
+ return <Outlet />;
194
+ }, () => [
195
+ path("/shop", () => <h1>Shop</h1>, { name: "shop" }),
196
+ path("/shop/:slug", (ctx) => {
197
+ ctx.use(Breadcrumbs)({
198
+ label: ctx.params.slug,
199
+ href: `/shop/${ctx.params.slug}`,
200
+ });
201
+ return <h1>Product: {ctx.params.slug}</h1>;
202
+ }, { name: "shop.product" }),
203
+ ]),
204
+ ]),
205
+ ]);
206
+ ```
207
+
208
+ Navigating to `/shop/widget` produces: `Home / Shop / widget`
209
+
210
+ ## Custom Handles
211
+
212
+ Create your own handle with `createHandle()`:
213
+
214
+ ```typescript
215
+ import { createHandle } from "@rangojs/router";
216
+
217
+ // Default: flatten into array
218
+ export const PageTitle = createHandle<string, string>(
219
+ (segments) => segments.flat().at(-1) ?? "Default Title",
220
+ );
221
+
222
+ // No collect function: default flattens into T[]
223
+ export const Warnings = createHandle<string>();
224
+ ```
225
+
226
+ The Vite `exposeInternalIds` plugin auto-injects a stable `$$id` based on
227
+ file path and export name. No manual naming required for project-local code.
228
+
229
+ ### Handles in 3rd-party packages
230
+
231
+ The `exposeInternalIds` plugin skips `node_modules/`, so handles defined in
232
+ published packages won't get auto-injected IDs. Pass a manual tag as the
233
+ second argument to `createHandle()`:
234
+
235
+ ```typescript
236
+ import { createHandle } from "@rangojs/router";
237
+
238
+ // With a collect function (reducer): collect is first arg, tag is second
239
+ export const Breadcrumbs = createHandle<BreadcrumbItem, BreadcrumbItem[]>(
240
+ collectBreadcrumbs,
241
+ "__my_package_breadcrumbs__",
242
+ );
243
+
244
+ // Without a collect function: pass undefined, then the tag
245
+ export const Warnings = createHandle<string>(
246
+ undefined,
247
+ "__my_package_warnings__",
248
+ );
249
+ ```
250
+
251
+ The tag must be globally unique and stable across builds. Without it,
252
+ `createHandle` throws in development mode.
@@ -162,6 +162,38 @@ middleware(async (ctx, next) => {
162
162
  });
163
163
  ```
164
164
 
165
+ ## Context Variable Cache Safety
166
+
167
+ Context variables created with `createVar()` are cacheable by default and can
168
+ be read freely inside `cache()` and `"use cache"` scopes. Non-cacheable vars
169
+ throw at read time to prevent request-specific data from being captured.
170
+
171
+ There are two ways to mark a value as non-cacheable:
172
+
173
+ ```typescript
174
+ // Var-level policy — inherently request-specific data
175
+ const Session = createVar<SessionData>({ cache: false });
176
+
177
+ // Write-level escalation — this specific write is non-cacheable
178
+ ctx.set(Theme, derivedTheme, { cache: false });
179
+ ```
180
+
181
+ "Least cacheable wins": if either the var definition or the `ctx.set()` call
182
+ specifies `cache: false`, the value is non-cacheable.
183
+
184
+ **Behavior inside cache scopes:**
185
+
186
+ | Operation | Inside `cache()` / `"use cache"` |
187
+ | ----------------------------------- | -------------------------------- |
188
+ | `ctx.get(cacheableVar)` | Allowed |
189
+ | `ctx.get(nonCacheableVar)` | Throws |
190
+ | `ctx.set(var, value)` (cacheable) | Allowed |
191
+ | `ctx.header()`, `ctx.cookie()`, etc | Throws (response side effects) |
192
+
193
+ Write is dumb — `ctx.set()` stores the cache metadata but does not enforce.
194
+ Enforcement happens at read time (`ctx.get()`), where ALS detects the cache
195
+ scope and rejects non-cacheable reads.
196
+
165
197
  ## Loaders Are Always Fresh
166
198
 
167
199
  Loaders are **never cached** by route-level `cache()`. Even on a full cache hit
@@ -89,7 +89,7 @@ Configure a cache store in the router:
89
89
 
90
90
  ```typescript
91
91
  import { createRouter } from "@rangojs/router";
92
- import { MemorySegmentCacheStore } from "@rangojs/router/rsc";
92
+ import { MemorySegmentCacheStore } from "@rangojs/router/cache";
93
93
 
94
94
  const store = new MemorySegmentCacheStore({
95
95
  defaults: { ttl: 60, swr: 300 },
@@ -112,7 +112,7 @@ const router = createRouter({
112
112
  For single-instance deployments:
113
113
 
114
114
  ```typescript
115
- import { MemorySegmentCacheStore } from "@rangojs/router/rsc";
115
+ import { MemorySegmentCacheStore } from "@rangojs/router/cache";
116
116
 
117
117
  const store = new MemorySegmentCacheStore({
118
118
  defaults: { ttl: 60, swr: 300 },
@@ -120,26 +120,67 @@ const store = new MemorySegmentCacheStore({
120
120
  });
121
121
  ```
122
122
 
123
- ### Cloudflare KV Store
123
+ ### Cloudflare Edge Cache Store
124
124
 
125
- For distributed caching on Cloudflare Workers:
125
+ For distributed caching on Cloudflare Workers using the Cache API:
126
126
 
127
127
  ```typescript
128
- import { CFCacheStore } from "@rangojs/router/cache/cf";
128
+ import { CFCacheStore } from "@rangojs/router/cache";
129
129
 
130
130
  const router = createRouter<AppBindings>({
131
131
  document: Document,
132
132
  urls: urlpatterns,
133
133
  cache: (env, ctx) => ({
134
134
  store: new CFCacheStore({
135
- kv: env.CACHE_KV,
136
- waitUntil: (fn) => ctx!.waitUntil(fn),
135
+ ctx,
136
+ defaults: { ttl: 60, swr: 300 },
137
137
  }),
138
138
  enabled: true,
139
139
  }),
140
140
  });
141
141
  ```
142
142
 
143
+ ### With KV L2 Persistence
144
+
145
+ Add a KV namespace for global cross-colo persistence. On Cache API miss, KV is
146
+ checked and hits are promoted back to L1. Writes go to both layers.
147
+
148
+ ```typescript
149
+ import { CFCacheStore } from "@rangojs/router/cache";
150
+
151
+ const router = createRouter<AppBindings>({
152
+ document: Document,
153
+ urls: urlpatterns,
154
+ cache: (env, ctx) => ({
155
+ store: new CFCacheStore({
156
+ ctx,
157
+ kv: env.CACHE_KV, // optional KV namespace binding
158
+ defaults: { ttl: 60, swr: 300 },
159
+ }),
160
+ enabled: true,
161
+ }),
162
+ });
163
+ ```
164
+
165
+ **How the two layers work:**
166
+
167
+ | Scenario | L1 (Cache API) | L2 (KV) | Result |
168
+ | ------------ | -------------- | ------- | ----------------------------- |
169
+ | Hot request | HIT | — | Serve from L1 (fast) |
170
+ | Cold colo | MISS | HIT | Serve from KV, promote to L1 |
171
+ | First render | MISS | MISS | Render, write to both L1 + KV |
172
+
173
+ KV entries require `expirationTtl >= 60s`. Short-lived entries (< 60s total TTL)
174
+ are only cached in L1.
175
+
176
+ ## Context Variables Inside Cache Boundaries
177
+
178
+ Context variables (`createVar`) are cacheable by default and can be read and
179
+ written inside `cache()` scopes. Variables marked with `{ cache: false }` (at
180
+ the var level or write level) throw when read inside a cache scope. Response
181
+ side effects (`ctx.header()`, `ctx.cookie()`) always throw inside cache
182
+ boundaries. See `/cache-guide` for the full cache safety table.
183
+
143
184
  ## Nested Cache Boundaries
144
185
 
145
186
  Override cache settings for specific sections:
@@ -175,7 +216,7 @@ cache({ store: checkoutCache }, () => [
175
216
 
176
217
  ```typescript
177
218
  import { urls } from "@rangojs/router";
178
- import { MemorySegmentCacheStore } from "@rangojs/router/rsc";
219
+ import { MemorySegmentCacheStore } from "@rangojs/router/cache";
179
220
 
180
221
  // Custom store for checkout (short TTL)
181
222
  const checkoutCache = new MemorySegmentCacheStore({
@@ -14,7 +14,7 @@ Configure document cache in router:
14
14
 
15
15
  ```typescript
16
16
  import { createRouter } from "@rangojs/router";
17
- import { CFCacheStore } from "@rangojs/router/cache/cf";
17
+ import { CFCacheStore } from "@rangojs/router/cache";
18
18
  import { urlpatterns } from "./urls";
19
19
 
20
20
  const router = createRouter<AppBindings>({
@@ -134,7 +134,7 @@ Segment hash ensures different cached responses for navigations from different s
134
134
  ```typescript
135
135
  // router.tsx
136
136
  import { createRouter } from "@rangojs/router";
137
- import { CFCacheStore } from "@rangojs/router/cache/cf";
137
+ import { CFCacheStore } from "@rangojs/router/cache";
138
138
  import { urlpatterns } from "./urls";
139
139
 
140
140
  const router = createRouter<AppBindings>({