@rangojs/router 0.0.0-experimental.29 → 0.0.0-experimental.2a0dea97
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.
- package/AGENTS.md +4 -0
- package/README.md +78 -19
- package/dist/bin/rango.js +138 -50
- package/dist/vite/index.js +853 -435
- package/package.json +17 -16
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +45 -4
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +22 -4
- package/skills/intercept/SKILL.md +20 -0
- package/skills/layout/SKILL.md +22 -0
- package/skills/links/SKILL.md +3 -1
- package/skills/loader/SKILL.md +71 -21
- package/skills/middleware/SKILL.md +34 -3
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +764 -0
- package/skills/parallel/SKILL.md +185 -0
- package/skills/prerender/SKILL.md +110 -68
- package/skills/rango/SKILL.md +24 -22
- package/skills/route/SKILL.md +56 -2
- package/skills/router-setup/SKILL.md +87 -2
- package/skills/typesafety/SKILL.md +33 -21
- package/src/__internal.ts +92 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +125 -16
- package/src/browser/navigation-client.ts +142 -57
- package/src/browser/navigation-store.ts +43 -8
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +94 -17
- package/src/browser/prefetch/cache.ts +82 -12
- package/src/browser/prefetch/fetch.ts +98 -27
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +92 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/react/Link.tsx +88 -9
- package/src/browser/react/NavigationProvider.tsx +40 -4
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-router.ts +21 -8
- package/src/browser/rsc-router.tsx +134 -59
- package/src/browser/scroll-restoration.ts +41 -42
- package/src/browser/segment-reconciler.ts +72 -10
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +55 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-trie.ts +50 -24
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +223 -74
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +48 -7
- package/src/cache/cf/cf-cache-store.ts +453 -11
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.rsc.tsx +2 -0
- package/src/client.tsx +6 -66
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/handle.ts +40 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/index.rsc.ts +6 -36
- package/src/index.ts +50 -43
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +138 -77
- package/src/reverse.ts +25 -1
- package/src/route-definition/dsl-helpers.ts +224 -37
- package/src/route-definition/helpers-types.ts +67 -19
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +11 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +111 -25
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +4 -1
- package/src/router/loader-resolution.ts +156 -21
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +9 -3
- package/src/router/match-api.ts +125 -190
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +94 -17
- package/src/router/match-middleware/cache-store.ts +53 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +61 -5
- package/src/router/match-result.ts +104 -10
- package/src/router/metrics.ts +6 -1
- package/src/router/middleware-types.ts +16 -22
- package/src/router/middleware.ts +24 -30
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/prerender-match.ts +114 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +36 -4
- package/src/router/router-options.ts +37 -11
- package/src/router/segment-resolution/fresh.ts +198 -20
- package/src/router/segment-resolution/helpers.ts +30 -25
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +438 -300
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/types.ts +1 -0
- package/src/router.ts +59 -6
- package/src/rsc/handler.ts +472 -372
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +14 -2
- package/src/rsc/rsc-rendering.ts +12 -1
- package/src/rsc/server-action.ts +8 -0
- package/src/rsc/ssr-setup.ts +2 -2
- package/src/rsc/types.ts +9 -1
- package/src/segment-content-promise.ts +33 -0
- package/src/segment-system.tsx +164 -23
- package/src/server/context.ts +140 -14
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +204 -28
- package/src/ssr/index.tsx +4 -0
- package/src/static-handler.ts +18 -6
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +149 -49
- package/src/types/loader-types.ts +36 -9
- package/src/types/route-entry.ts +8 -1
- package/src/types/segments.ts +6 -0
- package/src/urls/path-helper-types.ts +39 -6
- package/src/urls/path-helper.ts +48 -13
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +16 -6
- package/src/use-loader.tsx +77 -5
- package/src/vite/discovery/bundle-postprocess.ts +30 -33
- package/src/vite/discovery/discover-routers.ts +5 -1
- package/src/vite/discovery/prerender-collection.ts +128 -74
- package/src/vite/discovery/state.ts +13 -6
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +51 -79
- package/src/vite/plugins/expose-action-id.ts +1 -3
- package/src/vite/plugins/expose-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
- package/src/vite/plugins/expose-internal-ids.ts +257 -40
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +163 -211
- package/src/vite/router-discovery.ts +178 -45
- package/src/vite/utils/banner.ts +3 -3
- package/src/vite/utils/prerender-utils.ts +37 -5
- package/src/vite/utils/shared-utils.ts +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.0.0-experimental.
|
|
3
|
+
"version": "0.0.0-experimental.2a0dea97",
|
|
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 && 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",
|
|
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.
|
|
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": "
|
|
145
|
-
"@types/react-dom": "
|
|
153
|
+
"@types/react": "catalog:",
|
|
154
|
+
"@types/react-dom": "catalog:",
|
|
146
155
|
"esbuild": "^0.27.0",
|
|
147
156
|
"jiti": "^2.6.1",
|
|
148
|
-
"react": "
|
|
149
|
-
"react-dom": "
|
|
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.
|
|
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,250 @@
|
|
|
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
|
+
// Client component
|
|
146
|
+
("use client");
|
|
147
|
+
import { useHandle, type Breadcrumbs } from "@rangojs/router/client";
|
|
148
|
+
|
|
149
|
+
function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
|
|
150
|
+
const crumbs = useHandle(handle);
|
|
151
|
+
return (
|
|
152
|
+
<nav>
|
|
153
|
+
{crumbs.map((c) => (
|
|
154
|
+
<a href={c.href}>{c.label}</a>
|
|
155
|
+
))}
|
|
156
|
+
</nav>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Complete Example
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// urls.tsx
|
|
165
|
+
import { urls, Breadcrumbs, Meta } from "@rangojs/router";
|
|
166
|
+
import { Outlet, MetaTags } from "@rangojs/router/client";
|
|
167
|
+
import { BreadcrumbNav } from "./components/BreadcrumbNav";
|
|
168
|
+
|
|
169
|
+
function RootLayout() {
|
|
170
|
+
return (
|
|
171
|
+
<html lang="en">
|
|
172
|
+
<head><MetaTags /></head>
|
|
173
|
+
<body>
|
|
174
|
+
<BreadcrumbNav />
|
|
175
|
+
<main><Outlet /></main>
|
|
176
|
+
</body>
|
|
177
|
+
</html>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export const urlpatterns = urls(({ path, layout }) => [
|
|
182
|
+
layout((ctx) => {
|
|
183
|
+
ctx.use(Breadcrumbs)({ label: "Home", href: "/" });
|
|
184
|
+
ctx.use(Meta)({ title: "My App" });
|
|
185
|
+
return <RootLayout />;
|
|
186
|
+
}, () => [
|
|
187
|
+
path("/", () => <h1>Welcome</h1>, { name: "home" }),
|
|
188
|
+
|
|
189
|
+
layout((ctx) => {
|
|
190
|
+
ctx.use(Breadcrumbs)({ label: "Shop", href: "/shop" });
|
|
191
|
+
return <Outlet />;
|
|
192
|
+
}, () => [
|
|
193
|
+
path("/shop", () => <h1>Shop</h1>, { name: "shop" }),
|
|
194
|
+
path("/shop/:slug", (ctx) => {
|
|
195
|
+
ctx.use(Breadcrumbs)({
|
|
196
|
+
label: ctx.params.slug,
|
|
197
|
+
href: `/shop/${ctx.params.slug}`,
|
|
198
|
+
});
|
|
199
|
+
return <h1>Product: {ctx.params.slug}</h1>;
|
|
200
|
+
}, { name: "shop.product" }),
|
|
201
|
+
]),
|
|
202
|
+
]),
|
|
203
|
+
]);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Navigating to `/shop/widget` produces: `Home / Shop / widget`
|
|
207
|
+
|
|
208
|
+
## Custom Handles
|
|
209
|
+
|
|
210
|
+
Create your own handle with `createHandle()`:
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
import { createHandle } from "@rangojs/router";
|
|
214
|
+
|
|
215
|
+
// Default: flatten into array
|
|
216
|
+
export const PageTitle = createHandle<string, string>(
|
|
217
|
+
(segments) => segments.flat().at(-1) ?? "Default Title",
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// No collect function: default flattens into T[]
|
|
221
|
+
export const Warnings = createHandle<string>();
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The Vite `exposeInternalIds` plugin auto-injects a stable `$$id` based on
|
|
225
|
+
file path and export name. No manual naming required for project-local code.
|
|
226
|
+
|
|
227
|
+
### Handles in 3rd-party packages
|
|
228
|
+
|
|
229
|
+
The `exposeInternalIds` plugin skips `node_modules/`, so handles defined in
|
|
230
|
+
published packages won't get auto-injected IDs. Pass a manual tag as the
|
|
231
|
+
second argument to `createHandle()`:
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
import { createHandle } from "@rangojs/router";
|
|
235
|
+
|
|
236
|
+
// With a collect function (reducer): collect is first arg, tag is second
|
|
237
|
+
export const Breadcrumbs = createHandle<BreadcrumbItem, BreadcrumbItem[]>(
|
|
238
|
+
collectBreadcrumbs,
|
|
239
|
+
"__my_package_breadcrumbs__",
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Without a collect function: pass undefined, then the tag
|
|
243
|
+
export const Warnings = createHandle<string>(
|
|
244
|
+
undefined,
|
|
245
|
+
"__my_package_warnings__",
|
|
246
|
+
);
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The tag must be globally unique and stable across builds. Without it,
|
|
250
|
+
`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
|
package/skills/caching/SKILL.md
CHANGED
|
@@ -120,9 +120,9 @@ const store = new MemorySegmentCacheStore({
|
|
|
120
120
|
});
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
### Cloudflare
|
|
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
128
|
import { CFCacheStore } from "@rangojs/router/cache";
|
|
@@ -132,14 +132,55 @@ const router = createRouter<AppBindings>({
|
|
|
132
132
|
urls: urlpatterns,
|
|
133
133
|
cache: (env, ctx) => ({
|
|
134
134
|
store: new CFCacheStore({
|
|
135
|
-
|
|
136
|
-
|
|
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:
|