@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.
- package/AGENTS.md +4 -0
- package/README.md +172 -50
- package/dist/bin/rango.js +138 -50
- package/dist/vite/index.js +1160 -508
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +17 -16
- package/skills/breadcrumbs/SKILL.md +252 -0
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +49 -8
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +61 -51
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +20 -0
- package/skills/layout/SKILL.md +22 -0
- package/skills/links/SKILL.md +91 -17
- package/skills/loader/SKILL.md +107 -24
- package/skills/middleware/SKILL.md +34 -3
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +765 -0
- package/skills/parallel/SKILL.md +185 -0
- package/skills/prerender/SKILL.md +112 -70
- package/skills/rango/SKILL.md +24 -23
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/route/SKILL.md +58 -4
- package/skills/router-setup/SKILL.md +95 -5
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +38 -24
- package/src/__internal.ts +92 -0
- package/src/browser/app-shell.ts +52 -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 +175 -17
- package/src/browser/navigation-client.ts +177 -44
- package/src/browser/navigation-store.ts +68 -9
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +113 -17
- package/src/browser/prefetch/cache.ts +275 -28
- package/src/browser/prefetch/fetch.ts +191 -46
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +123 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +98 -14
- package/src/browser/react/NavigationProvider.tsx +89 -14
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +11 -1
- package/src/browser/react/use-router.ts +29 -9
- package/src/browser/rsc-router.tsx +177 -66
- package/src/browser/scroll-restoration.ts +41 -42
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +73 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-trie.ts +67 -25
- 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 +455 -15
- 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 -1
- package/src/client.tsx +85 -276
- 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/host/index.ts +0 -3
- package/src/index.rsc.ts +9 -36
- package/src/index.ts +79 -70
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +57 -15
- package/src/prerender.ts +138 -77
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +27 -2
- package/src/route-definition/dsl-helpers.ts +240 -40
- package/src/route-definition/helpers-types.ts +67 -19
- package/src/route-definition/index.ts +3 -3
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +18 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +129 -26
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +10 -7
- package/src/router/loader-resolution.ts +160 -22
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +31 -16
- package/src/router/match-api.ts +128 -193
- 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 +103 -18
- package/src/router/metrics.ts +238 -13
- package/src/router/middleware-types.ts +48 -27
- package/src/router/middleware.ts +201 -86
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +77 -11
- 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/revalidation.ts +27 -7
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +50 -5
- package/src/router/router-options.ts +50 -19
- package/src/router/segment-resolution/fresh.ts +215 -19
- 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 +454 -301
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/trie-matching.ts +30 -6
- package/src/router/types.ts +1 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +89 -17
- package/src/rsc/handler.ts +563 -364
- package/src/rsc/helpers.ts +69 -41
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +37 -10
- package/src/rsc/response-route-handler.ts +14 -1
- package/src/rsc/rsc-rendering.ts +47 -44
- package/src/rsc/server-action.ts +24 -10
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +11 -1
- package/src/search-params.ts +16 -13
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +109 -23
- package/src/server/context.ts +174 -19
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +218 -65
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +4 -0
- package/src/static-handler.ts +18 -6
- package/src/theme/index.ts +4 -13
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +140 -72
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-config.ts +17 -8
- package/src/types/route-entry.ts +19 -1
- package/src/types/segments.ts +2 -5
- package/src/urls/include-helper.ts +24 -14
- 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 +18 -16
- package/src/use-loader.tsx +77 -5
- package/src/vite/discovery/bundle-postprocess.ts +61 -89
- package/src/vite/discovery/discover-routers.ts +7 -4
- package/src/vite/discovery/prerender-collection.ts +162 -88
- package/src/vite/discovery/state.ts +17 -13
- package/src/vite/index.ts +8 -3
- package/src/vite/plugin-types.ts +51 -79
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- 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 +127 -0
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +190 -217
- package/src/vite/router-discovery.ts +241 -45
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/package-resolution.ts +34 -1
- package/src/vite/utils/prerender-utils.ts +97 -5
- package/src/vite/utils/shared-utils.ts +3 -2
- package/skills/testing/SKILL.md +0 -226
- 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.
|
|
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.
|
|
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,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
|
package/skills/caching/SKILL.md
CHANGED
|
@@ -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/
|
|
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/
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
@@ -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/
|
|
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
|
|
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
|
|
137
|
+
import { CFCacheStore } from "@rangojs/router/cache";
|
|
138
138
|
import { urlpatterns } from "./urls";
|
|
139
139
|
|
|
140
140
|
const router = createRouter<AppBindings>({
|