@rangojs/router 0.0.0-experimental.dfdb0387 → 0.0.0-experimental.e16b7c00
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/README.md +120 -25
- package/dist/bin/rango.js +147 -57
- package/dist/vite/index.js +2106 -842
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +13 -8
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +222 -30
- package/skills/caching/SKILL.md +188 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +45 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +46 -4
- package/skills/layout/SKILL.md +28 -7
- package/skills/links/SKILL.md +247 -17
- package/skills/loader/SKILL.md +219 -9
- package/skills/middleware/SKILL.md +47 -12
- package/skills/migrate-nextjs/SKILL.md +582 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +71 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +236 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- package/skills/route/SKILL.md +57 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +319 -27
- package/skills/use-cache/SKILL.md +36 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +86 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +86 -11
- package/src/browser/navigation-client.ts +45 -25
- package/src/browser/navigation-store.ts +32 -9
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +61 -28
- package/src/browser/prefetch/cache.ts +124 -26
- package/src/browser/prefetch/fetch.ts +129 -37
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +18 -13
- package/src/browser/react/NavigationProvider.tsx +72 -31
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +22 -2
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +64 -22
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-reconciler.ts +10 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +21 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-trie.ts +52 -25
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +95 -1
- package/src/cache/cache-runtime.ts +79 -13
- package/src/cache/cache-scope.ts +77 -46
- package/src/cache/cache-tag.ts +135 -0
- package/src/cache/cf/cf-cache-store.ts +1067 -176
- package/src/cache/cf/index.ts +4 -1
- package/src/cache/document-cache.ts +59 -7
- package/src/cache/index.ts +6 -0
- package/src/cache/memory-segment-store.ts +158 -14
- package/src/cache/tag-invalidation.ts +206 -0
- package/src/cache/types.ts +27 -0
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +92 -182
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +4 -6
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +16 -4
- package/src/index.ts +65 -15
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender.ts +4 -4
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -36
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +384 -257
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +100 -28
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +26 -41
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/handler-context.ts +21 -38
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +8 -8
- package/src/router/loader-resolution.ts +19 -2
- package/src/router/manifest.ts +22 -13
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +1 -0
- package/src/router/match-middleware/cache-lookup.ts +46 -92
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +53 -32
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +15 -26
- package/src/router/middleware.ts +99 -84
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/prerender-match.ts +3 -1
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/router/revalidation.ts +58 -2
- package/src/router/router-interfaces.ts +45 -28
- package/src/router/router-options.ts +25 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +27 -6
- package/src/router/segment-resolution/loader-cache.ts +8 -17
- package/src/router/segment-resolution/revalidation.ts +147 -106
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/trie-matching.ts +18 -13
- package/src/router/types.ts +8 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +23 -18
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +38 -70
- package/src/rsc/helpers.ts +72 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-route-handler.ts +54 -54
- package/src/rsc/rsc-rendering.ts +35 -51
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +17 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +8 -2
- package/src/search-params.ts +4 -4
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +132 -116
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +143 -53
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +46 -44
- package/src/ssr/index.tsx +5 -1
- package/src/static-handler.ts +1 -1
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +5 -1
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +68 -50
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +5 -6
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +11 -0
- package/src/types/segments.ts +35 -2
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +41 -7
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +101 -51
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +67 -26
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- 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 +54 -30
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +496 -486
- package/src/vite/plugins/performance-tracks.ts +29 -25
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +116 -29
- package/src/vite/router-discovery.ts +750 -100
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +21 -6
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -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.e16b7c00",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -133,22 +133,26 @@
|
|
|
133
133
|
"tag": "experimental"
|
|
134
134
|
},
|
|
135
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",
|
|
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
137
|
"prepublishOnly": "pnpm build",
|
|
138
|
-
"typecheck": "tsc --noEmit",
|
|
138
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
139
139
|
"test": "playwright test",
|
|
140
140
|
"test:ui": "playwright test --ui",
|
|
141
|
+
"test:hmr-local": "playwright test --project=dev-warmup --project=hmr-routes --project=hmr-basename --project=hmr-prerender --no-deps --workers=1",
|
|
141
142
|
"test:unit": "vitest run",
|
|
142
143
|
"test:unit:watch": "vitest"
|
|
143
144
|
},
|
|
144
145
|
"dependencies": {
|
|
145
|
-
"@
|
|
146
|
+
"@types/debug": "^4.1.12",
|
|
147
|
+
"@vitejs/plugin-rsc": "^0.5.26",
|
|
148
|
+
"debug": "^4.4.1",
|
|
146
149
|
"magic-string": "^0.30.17",
|
|
147
150
|
"picomatch": "^4.0.3",
|
|
148
151
|
"rsc-html-stream": "^0.0.7"
|
|
149
152
|
},
|
|
150
153
|
"devDependencies": {
|
|
151
154
|
"@playwright/test": "^1.49.1",
|
|
155
|
+
"@shared/e2e": "workspace:*",
|
|
152
156
|
"@types/node": "^24.10.1",
|
|
153
157
|
"@types/react": "catalog:",
|
|
154
158
|
"@types/react-dom": "catalog:",
|
|
@@ -161,10 +165,11 @@
|
|
|
161
165
|
"vitest": "^4.0.0"
|
|
162
166
|
},
|
|
163
167
|
"peerDependencies": {
|
|
164
|
-
"@cloudflare/vite-plugin": "^1.
|
|
165
|
-
"@vitejs/plugin-rsc": "^0.5.
|
|
166
|
-
"react": "
|
|
167
|
-
"
|
|
168
|
+
"@cloudflare/vite-plugin": "^1.38.0",
|
|
169
|
+
"@vitejs/plugin-rsc": "^0.5.26",
|
|
170
|
+
"react": ">=19.2.6 <20",
|
|
171
|
+
"react-dom": ">=19.2.6 <20",
|
|
172
|
+
"vite": "^8.0.0"
|
|
168
173
|
},
|
|
169
174
|
"peerDependenciesMeta": {
|
|
170
175
|
"@cloudflare/vite-plugin": {
|
|
@@ -141,9 +141,11 @@ path("/dashboard", (ctx) => {
|
|
|
141
141
|
breadcrumb({ label: "Dashboard", href: "/dashboard" });
|
|
142
142
|
return <DashboardNav handle={Breadcrumbs} />;
|
|
143
143
|
});
|
|
144
|
+
```
|
|
144
145
|
|
|
146
|
+
```tsx
|
|
145
147
|
// Client component
|
|
146
|
-
|
|
148
|
+
"use client";
|
|
147
149
|
import { useHandle, type Breadcrumbs } from "@rangojs/router/client";
|
|
148
150
|
|
|
149
151
|
function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bundle-analysis
|
|
3
|
+
description: Audit a Rango app's production bundle for server-side code leaking into the client, dev/prod React duplication, oversized chunks, and inefficient client-reference grouping. Use when investigating bundle size growth, before a production deploy, or when the client/SSR/RSC output suddenly balloons.
|
|
4
|
+
argument-hint: "<app-dir>"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Bundle Analysis
|
|
8
|
+
|
|
9
|
+
Use this when you want **proof** that your Rango app is shipping the bundles you expect: small client, no server leaks, no doubled React, reasonable RSC worker size.
|
|
10
|
+
|
|
11
|
+
## What this checks
|
|
12
|
+
|
|
13
|
+
Your app builds in three Vite environments — `client`, `ssr`, and `rsc` — and each ships its own bundle. The most common bundle bugs in a Rango app are:
|
|
14
|
+
|
|
15
|
+
1. **Server code leaking into the client.** A file that imports `node:fs`, calls a database, or contains action logic ends up in the client bundle because a client component pulled it in transitively. Symptom: your client bundle is much larger than expected, sometimes with imports that fail at runtime.
|
|
16
|
+
2. **Both dev and prod React in the SSR/RSC bundle.** When `process.env.NODE_ENV` isn't folded at build time, React's CJS files ship both `.development.js` _and_ `.production.js` variants — doubling React's footprint. The Cloudflare vite plugin folds NODE_ENV automatically; vanilla `vite build` does it for client but not always for SSR/RSC.
|
|
17
|
+
3. **An oversized routes-manifest in your RSC worker.** The `virtual:rsc-router/routes-manifest/<routerId>` chunk holds your route trie and precomputed entries — large only in proportion to your route count. If it's surprisingly big, you may have unintentionally generated routes (e.g., parametrized fixtures) that bloated the trie.
|
|
18
|
+
4. **Inefficient client-reference grouping.** Each `"use client"` boundary becomes a chunk. Too many small client components = many tiny chunks; one giant client component = one giant chunk that defeats code-splitting.
|
|
19
|
+
|
|
20
|
+
Tree-shaking does _not_ catch (1) generated data inlined as string literals or (2) data-dependent conditionals like React's. You need a visualizer.
|
|
21
|
+
|
|
22
|
+
## Step 1: Install the visualizer
|
|
23
|
+
|
|
24
|
+
In your app's directory:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pnpm add -D rollup-plugin-visualizer
|
|
28
|
+
# or: npm install --save-dev rollup-plugin-visualizer
|
|
29
|
+
# or: yarn add -D rollup-plugin-visualizer
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Step 2: Wire it into your `vite.config.ts`
|
|
33
|
+
|
|
34
|
+
Add a small helper that registers one visualizer instance **per Vite environment** (not just one global). The plugin caches its options after the first call, so a single instance can't handle multi-environment builds — you'll get a report for one environment and silence for the others.
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// vite.config.ts
|
|
38
|
+
import { defineConfig, type PluginOption, type Plugin } from "vite";
|
|
39
|
+
import { visualizer } from "rollup-plugin-visualizer";
|
|
40
|
+
import { join } from "node:path";
|
|
41
|
+
// ... your other imports ...
|
|
42
|
+
|
|
43
|
+
function analyze(): PluginOption[] {
|
|
44
|
+
if (!process.env.ANALYZE) return [];
|
|
45
|
+
return (["client", "ssr", "rsc"] as const).map((envName) => {
|
|
46
|
+
const inner = visualizer({
|
|
47
|
+
filename: join("bundle-stats", `${envName}.html`),
|
|
48
|
+
template: "treemap",
|
|
49
|
+
gzipSize: true,
|
|
50
|
+
brotliSize: true,
|
|
51
|
+
}) as Plugin;
|
|
52
|
+
return {
|
|
53
|
+
...inner,
|
|
54
|
+
name: `analyze-${envName}`,
|
|
55
|
+
applyToEnvironment(env) {
|
|
56
|
+
return env.name === envName;
|
|
57
|
+
},
|
|
58
|
+
} as Plugin;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default defineConfig(({ command }) => ({
|
|
63
|
+
plugins: [
|
|
64
|
+
// your existing plugins...
|
|
65
|
+
...analyze(),
|
|
66
|
+
],
|
|
67
|
+
// For non-Cloudflare apps, fold NODE_ENV explicitly so React's CJS files
|
|
68
|
+
// emit only the .production.js variants in SSR/RSC. Skip if your build
|
|
69
|
+
// setup already does this (the Cloudflare vite plugin does).
|
|
70
|
+
define:
|
|
71
|
+
command === "build"
|
|
72
|
+
? { "process.env.NODE_ENV": JSON.stringify("production") }
|
|
73
|
+
: undefined,
|
|
74
|
+
}));
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Add `bundle-stats/` to your `.gitignore`.
|
|
78
|
+
|
|
79
|
+
## Step 3: Build with the analyzer enabled
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
ANALYZE=1 pnpm exec vite build
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
You'll get three HTML reports in `bundle-stats/`:
|
|
86
|
+
|
|
87
|
+
- `bundle-stats/client.html` — what runs in the browser
|
|
88
|
+
- `bundle-stats/ssr.html` — what runs during HTML stream
|
|
89
|
+
- `bundle-stats/rsc.html` — what runs in your RSC server (Worker / Node)
|
|
90
|
+
|
|
91
|
+
Open them with a quick local server (file:// has CORS issues with the embedded scripts):
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pnpm dlx serve -l 5050 .
|
|
95
|
+
# then visit http://localhost:5050/bundle-stats/client.html
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Step 4: Triage the reports
|
|
99
|
+
|
|
100
|
+
### Open `client.html` first
|
|
101
|
+
|
|
102
|
+
The treemap shows nested boxes; box area = uncompressed size. Hover for gzip/brotli numbers.
|
|
103
|
+
|
|
104
|
+
**Look for:**
|
|
105
|
+
|
|
106
|
+
- **Your server code.** Any of your own files that contain database queries, secret keys, server actions implementation (not the action _reference_), or `node:` imports. If they appear in the client treemap with non-zero bytes, they leaked. Common causes:
|
|
107
|
+
- A shared module that mixes client and server code without a `"use client"` or `"use server"` directive.
|
|
108
|
+
- A barrel file (`index.ts`) that re-exports both client and server symbols. Tree-shaking should help, but `JSON.parse('{...}')` data and side-effecting top-level statements survive.
|
|
109
|
+
- Client component imports a server-only utility through an indirect path (e.g., shared types file that pulls server modules).
|
|
110
|
+
- **Multiple copies of the same package.** Look for two boxes with the same package name but different version paths. Usually means a transitive dep pinned a different version.
|
|
111
|
+
- **The `@rangojs/router` chunk** should be roughly **50 KB gzip** (74 files). If significantly larger, you might be importing client-incompatible APIs from the wrong subpath.
|
|
112
|
+
- **Per-route client-reference chunks** (named like `chunk-<hash>.js`). Each `"use client"` boundary can become its own chunk. If you have hundreds of tiny chunks, you may have over-split (every leaf component as a client component); if you have one massive 200 KB chunk, you've under-split (a wide client tree behind one boundary).
|
|
113
|
+
|
|
114
|
+
### Now check `ssr.html`
|
|
115
|
+
|
|
116
|
+
**Look for:**
|
|
117
|
+
|
|
118
|
+
- **`react-dom-server.edge.development-*.js`** (or any `*.development*.js` chunk). This is the dev/prod React doubling. Fix: add `define: { "process.env.NODE_ENV": '"production"' }` to your vite config (see Step 2).
|
|
119
|
+
- **Your client components** appearing in SSR. They're _expected_ here — SSR hydration needs to produce HTML for them. The same components show up in `client.html` too because the browser hydrates them. This is not a leak.
|
|
120
|
+
- **Total SSR size**: a reasonable Rango SSR is ~140 KB gzip plus your app code. If it's >300 KB, almost always (1) dev/prod React duplication or (2) a giant data structure being inlined.
|
|
121
|
+
|
|
122
|
+
### Now check `rsc.html`
|
|
123
|
+
|
|
124
|
+
**Look for:**
|
|
125
|
+
|
|
126
|
+
- **`virtual:rsc-router/routes-manifest`** should be **tiny** (< 1 KB). If it's > 100 KB, you're on an old version of `@rangojs/router` that inlined the trie eagerly — upgrade to a release that includes commit `d10a2470`.
|
|
127
|
+
- **`virtual:rsc-router/routes-manifest/<hash>`** is the lazy per-router chunk. Its size is proportional to your route count. For a typical app: 5–50 KB gzip. For a stress-test app with thousands of routes: hundreds of KB. If yours is unexpectedly huge, check whether you're generating routes you don't need.
|
|
128
|
+
- **`<your-router>.named-routes.gen.ts`** — generated route map. Should match your route count.
|
|
129
|
+
- **Your action and loader implementations** — these run server-side. Expected to be here, not in client.
|
|
130
|
+
- **Worker-incompatible code** (Node-only imports like `node:fs` that Cloudflare doesn't support). The build will usually fail before the analyzer runs, but if you're seeing runtime errors at the edge, the RSC treemap shows what made it in.
|
|
131
|
+
|
|
132
|
+
## Step 5: Fix what you find
|
|
133
|
+
|
|
134
|
+
| Finding | Fix |
|
|
135
|
+
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
136
|
+
| Your server code in `client.html` (non-zero bytes) | Audit the import chain. Add `"use server"` to server-only files. Move shared data out of barrel files. Use the `@rangojs/router/server` subpath for explicitly server APIs. |
|
|
137
|
+
| Your server code in `client.html` listed but 0 bytes | Tree-shaking already eliminated it. Cosmetic. Leave it. |
|
|
138
|
+
| `react-dom-server.edge.development-*.js` in SSR or RSC | Add the `define` block from Step 2 to your vite config. |
|
|
139
|
+
| Routes-manifest > 100 KB gzip in RSC eager chunk | Update `@rangojs/router` to a release that includes the lazy-only manifest fix. |
|
|
140
|
+
| Same package version present twice | Run `pnpm dedupe` (or `npm dedupe`). If the duplication persists, a transitive dep pins an incompatible version — open a PR upstream or pin the resolution. |
|
|
141
|
+
| Client chunk > 500 KB gzip with a single dominant module | That module is your largest client component. Consider lazy-loading via dynamic `import()` or moving non-interactive parts to server components. |
|
|
142
|
+
| Hundreds of tiny client chunks | You've sprinkled `"use client"` too liberally. Hoist directives to higher boundaries so React groups them. |
|
|
143
|
+
|
|
144
|
+
## When to re-run
|
|
145
|
+
|
|
146
|
+
- Before every production deploy, especially after adding new dependencies.
|
|
147
|
+
- After upgrading `@rangojs/router`, React, or `@vitejs/plugin-rsc`.
|
|
148
|
+
- After adding routes that scale with data (e.g., one route per item from a content directory) — the manifest may have grown.
|
|
149
|
+
- When CI starts reporting larger artifact sizes.
|
|
150
|
+
|
|
151
|
+
## Reporting Rango regressions
|
|
152
|
+
|
|
153
|
+
If a finding looks like a `@rangojs/router` regression (the framework is shipping more than it should, not your app), open an issue at the [@rangojs/router GitHub](https://github.com/ivogt/vite-rsc/issues) and include:
|
|
154
|
+
|
|
155
|
+
- The output of `client.html` / `rsc.html` (screenshots or the JSON `data = {...}` block from the HTML).
|
|
156
|
+
- The `@rangojs/router` version (`pnpm why @rangojs/router`).
|
|
157
|
+
- Your `vite.config.ts`.
|
|
158
|
+
|
|
159
|
+
The framework maintainers run a similar audit internally — the methodology in this skill mirrors what they use to validate every release.
|
|
@@ -6,8 +6,140 @@ argument-hint:
|
|
|
6
6
|
|
|
7
7
|
# cache() vs "use cache" — When to Use Which
|
|
8
8
|
|
|
9
|
-
Both mechanisms share the same backing store
|
|
10
|
-
|
|
9
|
+
Both mechanisms share the same backing store and cache profiles, and both accept
|
|
10
|
+
an optional `tags` field (honored by the built-in stores — invalidate with
|
|
11
|
+
`updateTag`/`revalidateTag`; see "Two axes" below). They differ in scope, cache
|
|
12
|
+
key, execution model, and runtime control.
|
|
13
|
+
|
|
14
|
+
## Two axes — do not conflate
|
|
15
|
+
|
|
16
|
+
Everything on this page is **axis 1: stored-value freshness** — _is a cached
|
|
17
|
+
value still good?_ There is a second, orthogonal axis it is easy to mistake for
|
|
18
|
+
caching:
|
|
19
|
+
|
|
20
|
+
1. **Stored-value freshness** — _is a cached value still good?_
|
|
21
|
+
→ `"use cache"` (fn/component), `cache()` (segment), loader `cache()` (loader data).
|
|
22
|
+
Entries expire by **TTL/SWR** and can be tagged (`cache({ tags })` or runtime
|
|
23
|
+
`cacheTag(...tags)`). Built-in stores (`MemorySegmentCacheStore`, `CFCacheStore`)
|
|
24
|
+
index by tag; invalidate on demand with `updateTag(...tags)` (awaitable,
|
|
25
|
+
read-your-own-writes) or `revalidateTag(...tags)` (background, non-blocking).
|
|
26
|
+
Both hard-purge; the difference is awaitability, not stale-serving.
|
|
27
|
+
2. **Client-update selection** — _should this segment re-run and stream to the
|
|
28
|
+
client on this navigation/action?_
|
|
29
|
+
→ `revalidate()`. Covered in `/loader` and `/route`, **not here**.
|
|
30
|
+
|
|
31
|
+
They are orthogonal and compose: a segment selected by `revalidate()` still
|
|
32
|
+
consults its cache (hit → no recompute); a cache bust does **not** force a client
|
|
33
|
+
update, and `revalidate()` never reads, writes, or expires a cached value. If you
|
|
34
|
+
know React Router, `revalidate()` is `shouldRevalidate`, not `Cache-Control`. See
|
|
35
|
+
`/rango` → "Coming from another framework" for the cross-framework mapping.
|
|
36
|
+
|
|
37
|
+
## Fast choice
|
|
38
|
+
|
|
39
|
+
Read this first; use the rest of the page when the choice has edge cases.
|
|
40
|
+
|
|
41
|
+
1. Do you want to cache an entire route or group of routes?
|
|
42
|
+
**Yes** -> `cache()`
|
|
43
|
+
2. Do you need runtime conditions, such as skip for authed users or key by
|
|
44
|
+
locale?
|
|
45
|
+
**Yes** -> `cache()` with `condition` / `key`
|
|
46
|
+
3. Do you want to cache a data fetch or helper shared across routes?
|
|
47
|
+
**Yes** -> `"use cache"`
|
|
48
|
+
4. Do you need different cache entries for different function arguments?
|
|
49
|
+
**Yes** -> `"use cache"` (keyed by args)
|
|
50
|
+
5. Is the expensive part rendering a subtree?
|
|
51
|
+
**Yes** -> `cache()` (caches rendered segments)
|
|
52
|
+
6. Is the expensive part one query inside a larger live handler?
|
|
53
|
+
**Yes** -> `"use cache"` on the query function
|
|
54
|
+
|
|
55
|
+
## Correctness & invalidation
|
|
56
|
+
|
|
57
|
+
rango's caches are built so a hit can't serve wrong or stale-shaped data. These
|
|
58
|
+
guarantees are mostly automatic — worth knowing so you don't reimplement
|
|
59
|
+
protection the framework already gives you (or assume one it deliberately
|
|
60
|
+
doesn't).
|
|
61
|
+
|
|
62
|
+
There are two guard models to keep separate. Both block response side effects
|
|
63
|
+
(`ctx.header()`, cookie writes) that would be lost on a hit; they differ in what
|
|
64
|
+
else they allow:
|
|
65
|
+
|
|
66
|
+
- **`cache()` boundary guard** (route-level) — fires while the handler runs on a
|
|
67
|
+
miss. `cookies()` and `headers()` throw (request-scoped data would be baked into
|
|
68
|
+
the shared cached shell), `ctx.get(nonCacheableVar)` throws (a tainted value
|
|
69
|
+
would be baked in), and response side effects (`ctx.header()`, `ctx.setCookie()`,
|
|
70
|
+
`ctx.setStatus()`, `ctx.onResponse()`) throw. `ctx.set()` of a cacheable var is
|
|
71
|
+
**allowed** — children are cached too and can read it. **Loaders are exempt**
|
|
72
|
+
(they always run fresh) — read request data inside a loader.
|
|
73
|
+
- **`"use cache"` exec-guard** (function-level) — the same request-scoped APIs
|
|
74
|
+
throw inside the cached function (`cookies()`, `headers()`, `ctx.set()`,
|
|
75
|
+
`ctx.header()`); additionally, tainted `ctx`/`env`/`req` args are excluded from
|
|
76
|
+
the cache key.
|
|
77
|
+
|
|
78
|
+
### Cross-deploy safety: version-segmented store keys
|
|
79
|
+
|
|
80
|
+
`CFCacheStore` prefixes every **physical** store key (the CF Cache API URL and
|
|
81
|
+
the KV key) with the build version — auto-generated from the
|
|
82
|
+
`@rangojs/router:version` virtual module, overridable via the store's `version`
|
|
83
|
+
option. A new deploy reads under a new prefix, so it can **never** read a
|
|
84
|
+
previous build's entries: no cross-deploy shape drift, and no dead client-chunk
|
|
85
|
+
references baked into cached RSC.
|
|
86
|
+
|
|
87
|
+
The tradeoff to know: **loader/data caches use the same store**, so they're
|
|
88
|
+
version-segmented too. Every deploy is therefore a _cold data cache_ — SWR can't
|
|
89
|
+
soften it, because no stale entry exists under the new key. For high-traffic,
|
|
90
|
+
frequently-deploying, data-bound apps that's a deploy-time origin warm-up. Decide
|
|
91
|
+
deliberately: accept it (correctness over hit-rate), or split the policy — let
|
|
92
|
+
the render/edge cache auto-version while a separate data store gets a stable
|
|
93
|
+
`version` so its entries survive deploys. (Per-process stores like
|
|
94
|
+
`MemorySegmentCacheStore` are cold on every restart anyway; this matters for
|
|
95
|
+
persistent stores.) See `/caching` for store setup.
|
|
96
|
+
|
|
97
|
+
### Client cache: forward/back is mutation-aware
|
|
98
|
+
|
|
99
|
+
The browser keeps a history (forward/back) cache of rendered segments. Any
|
|
100
|
+
client-side mutation (a server action) marks those entries **stale** and
|
|
101
|
+
broadcasts it to other tabs. On back/forward (popstate) the router looks up the
|
|
102
|
+
entry, sees it's stale, and revalidates — so your `revalidate()` predicates re-run
|
|
103
|
+
and the segment refreshes (SWR: the stale view paints instantly, fresh data
|
|
104
|
+
streams in). It's the client-side analog of the server-cache correctness problem,
|
|
105
|
+
solved on the partial-render axis.
|
|
106
|
+
|
|
107
|
+
### Request-scoped data: the `cache: false` taint
|
|
108
|
+
|
|
109
|
+
`createVar({ cache: false })` (or a `ctx.set(var, v, { cache: false })` write)
|
|
110
|
+
taints a value as request-scoped; reading it **directly** with `ctx.get()` inside
|
|
111
|
+
a `cache()` boundary throws — the guard against the catastrophic "serve user A's
|
|
112
|
+
data to user B" bug. The guarantee is precise and intentionally narrow — see
|
|
113
|
+
"Context Variable Cache Safety" below for exactly what it does and does not catch.
|
|
114
|
+
|
|
115
|
+
## Stale-while-revalidate
|
|
116
|
+
|
|
117
|
+
SWR is a first-class cache behavior when the backing store supports it: while an
|
|
118
|
+
entry is within its SWR window the cache serves the **stale value instantly** and
|
|
119
|
+
refreshes it in the **background** (`waitUntil`), so users never wait on a
|
|
120
|
+
recompute for a merely-aging entry.
|
|
121
|
+
|
|
122
|
+
- **`"use cache"`** resolves to the `default` profile `{ ttl: 900, swr: 1800 }`,
|
|
123
|
+
so function/component caching gets a 30-minute SWR window **out of the box**.
|
|
124
|
+
Tune or add profiles via `createRouter({ cacheProfiles: { … } })`
|
|
125
|
+
(`"use cache: short"` → the `short` profile).
|
|
126
|
+
- **`cache()` DSL and loader caches** take an explicit `swr` in seconds (or
|
|
127
|
+
inherit `store.defaults.swr`): `cache({ ttl: 60, swr: 300 })` → fresh ≤60s,
|
|
128
|
+
stale-served 60–360s, miss after 360s in stores that implement SWR for that
|
|
129
|
+
layer.
|
|
130
|
+
- **Client forward/back** is SWR after a mutation — see "Correctness &
|
|
131
|
+
invalidation" → Client cache.
|
|
132
|
+
- **Edge / document layer** uses the HTTP `stale-while-revalidate` directive; see
|
|
133
|
+
`/document-cache`.
|
|
134
|
+
|
|
135
|
+
SWR softens normal TTL expiry, **not** a cross-deploy cold cache — a new build
|
|
136
|
+
has no stale entry to serve (see version-segmented store keys above).
|
|
137
|
+
|
|
138
|
+
Store support is layer-specific. `CFCacheStore` supports SWR for segment,
|
|
139
|
+
document/response, and `"use cache"` item entries. `MemorySegmentCacheStore`
|
|
140
|
+
supports SWR for response and `"use cache"` item entries, but its route-segment
|
|
141
|
+
entries expire at TTL and never background-revalidate. Use the memory store for
|
|
142
|
+
local/dev behavior, not as proof that segment SWR is active.
|
|
11
143
|
|
|
12
144
|
## Key Differences
|
|
13
145
|
|
|
@@ -18,7 +150,7 @@ invalidation. They differ in scope, cache key, execution model, and runtime cont
|
|
|
18
150
|
| **Cache key** | Request type + pathname + params (+ optional custom) | Function identity + serialized non-tainted args |
|
|
19
151
|
| **Execution on hit** | All-or-nothing: entire handler skipped | Partial: function body skipped, calling code runs |
|
|
20
152
|
| **Runtime control** | `condition` to disable, custom `key` function | None — if the directive is present, it caches |
|
|
21
|
-
| **Side effects** |
|
|
153
|
+
| **Side effects** | Response side effects throw inside the boundary | `ctx.header()`, `ctx.set()`, etc. throw at runtime |
|
|
22
154
|
| **Handle data** | Captured and replayed | Captured and replayed |
|
|
23
155
|
| **Loaders** | Always fresh — excluded from cache, opt-in per loader | Can be used inside loaders |
|
|
24
156
|
| **Nesting** | Nest `cache()` boundaries with different TTLs | Compose by calling cached functions from uncached |
|
|
@@ -144,13 +276,38 @@ On cache hit for the route, the handler doesn't run and `getProductData` is neve
|
|
|
144
276
|
called. On cache miss, the handler runs and `getProductData` may itself return a
|
|
145
277
|
cached value from a previous call with the same slug.
|
|
146
278
|
|
|
279
|
+
### Nesting rule: the outer window bounds the inner
|
|
280
|
+
|
|
281
|
+
A cache's window bounds everything rendered inside it (loaders excepted). An
|
|
282
|
+
inner shorter TTL only takes effect when the **enclosing** cache recomputes — it
|
|
283
|
+
does **not** keep a value fresher than its parent:
|
|
284
|
+
|
|
285
|
+
- Outer `cache()` **fresh hit** → the subtree is served from stored RSC, so inner
|
|
286
|
+
`"use cache"` functions are **not consulted** (frozen at the outer's age — no
|
|
287
|
+
code inside the boundary runs on a hit).
|
|
288
|
+
- Outer **miss / SWR revalidation** → inner caches are consulted, each per its own
|
|
289
|
+
ttl/swr. With SWR on the outer, a stale subtree serves instantly and refreshes
|
|
290
|
+
in the background, so under traffic it keeps refreshing rather than rotting to
|
|
291
|
+
the worst case.
|
|
292
|
+
- **Loaders are the exception** — excluded from the segment cache, re-resolved
|
|
293
|
+
live even on an outer hit.
|
|
294
|
+
|
|
295
|
+
So `"use cache: short"` (60s) inside `cache({ ttl: 600 })` yields ~600s freshness
|
|
296
|
+
on hits, **not** 60s. This is not a bug: setting `cache({ ttl: 600 })` declares
|
|
297
|
+
"this subtree may be ~600s stale." **If a value must be fresher than its
|
|
298
|
+
enclosing segment, put it in a loader** (always live). `debugPerformance` prints
|
|
299
|
+
cache hits per layer, so the actual per-request behavior is observable.
|
|
300
|
+
|
|
147
301
|
## Headers and Cookies
|
|
148
302
|
|
|
149
303
|
Neither mechanism caches response headers or cookies.
|
|
150
304
|
|
|
151
|
-
- **cache()**:
|
|
152
|
-
|
|
153
|
-
(
|
|
305
|
+
- **cache()**: Response-level side effects throw inside the cache boundary even
|
|
306
|
+
on a miss: `ctx.header()`, `ctx.setCookie()`, `ctx.deleteCookie()`,
|
|
307
|
+
`ctx.setStatus()`, `ctx.onResponse()`, and direct `ctx.headers` mutation. On a
|
|
308
|
+
hit the handler would be skipped, so allowing the write on a miss would produce
|
|
309
|
+
inconsistent responses. If you need headers or cookies on every response, set
|
|
310
|
+
them in middleware or a live segment outside the cache boundary.
|
|
154
311
|
- **"use cache"**: cookies() and headers() throw inside the cached function
|
|
155
312
|
(both reads and writes). ctx.header() also throws. Move them outside.
|
|
156
313
|
|
|
@@ -165,8 +322,9 @@ middleware(async (ctx, next) => {
|
|
|
165
322
|
## Context Variable Cache Safety
|
|
166
323
|
|
|
167
324
|
Context variables created with `createVar()` are cacheable by default and can
|
|
168
|
-
be read freely inside
|
|
169
|
-
|
|
325
|
+
be read freely inside cached scopes. A non-cacheable var throws when read
|
|
326
|
+
**directly** with `ctx.get()` inside a `cache()` boundary — where the value would
|
|
327
|
+
otherwise be serialized into the stored segment.
|
|
170
328
|
|
|
171
329
|
There are two ways to mark a value as non-cacheable:
|
|
172
330
|
|
|
@@ -181,19 +339,68 @@ ctx.set(Theme, derivedTheme, { cache: false });
|
|
|
181
339
|
"Least cacheable wins": if either the var definition or the `ctx.set()` call
|
|
182
340
|
specifies `cache: false`, the value is non-cacheable.
|
|
183
341
|
|
|
184
|
-
**Behavior inside cache
|
|
342
|
+
**Behavior inside a `cache()` boundary:**
|
|
185
343
|
|
|
186
|
-
| Operation
|
|
187
|
-
|
|
|
188
|
-
| `
|
|
189
|
-
| `ctx.get(
|
|
190
|
-
| `ctx.
|
|
191
|
-
| `ctx.
|
|
344
|
+
| Operation | Inside a `cache()` boundary |
|
|
345
|
+
| ----------------------------------------- | ------------------------------------------------------ |
|
|
346
|
+
| `cookies()` / `headers()` (read or write) | Throws (request-scoped, would poison the shared entry) |
|
|
347
|
+
| `ctx.get(cacheableVar)` | Allowed |
|
|
348
|
+
| `ctx.get(nonCacheableVar)` | Throws (would be baked in) |
|
|
349
|
+
| `ctx.set(var, value)` (cacheable) | Allowed |
|
|
350
|
+
| `ctx.header()` / cookie writes | Throws (response side effect would be lost on hit) |
|
|
351
|
+
| Any of the above **inside a loader** | Allowed (loaders always run fresh) |
|
|
352
|
+
|
|
353
|
+
(Both scopes block the same request-scoped APIs — `cookies()`, `headers()`,
|
|
354
|
+
response side effects, and non-cacheable `ctx.get()` — because each would leak
|
|
355
|
+
per-request data into a shared cache entry. The `cache()` boundary tracks the
|
|
356
|
+
scope via `isInsideCacheScope()`; `"use cache"` uses the exec guard and also
|
|
357
|
+
excludes tainted `ctx`/`env`/`req` args from the cache key. Loaders are exempt in
|
|
358
|
+
both — see "Headers and Cookies" and the precise guarantee below.)
|
|
192
359
|
|
|
193
360
|
Write is dumb — `ctx.set()` stores the cache metadata but does not enforce.
|
|
194
361
|
Enforcement happens at read time (`ctx.get()`), where ALS detects the cache
|
|
195
362
|
scope and rejects non-cacheable reads.
|
|
196
363
|
|
|
364
|
+
### The guarantee is precise — a direct read inside `cache()`, not propagating
|
|
365
|
+
|
|
366
|
+
The guard fires on a **direct** `ctx.get(taintedVar)` **inside a `cache()`
|
|
367
|
+
boundary** (the scope `isInsideCacheScope` detects). The taint lives on the
|
|
368
|
+
variable; a value **derived** from it and read **outside** the boundary is not
|
|
369
|
+
tracked:
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
// CAUGHT — direct read of a tainted var inside a cache() boundary
|
|
373
|
+
cache({ ttl: 60 }, () => [
|
|
374
|
+
path("/dashboard", (ctx) => {
|
|
375
|
+
const user = ctx.get(User); // throws: non-cacheable read inside cache()
|
|
376
|
+
return <Dashboard user={user} />;
|
|
377
|
+
}, { name: "dashboard" }),
|
|
378
|
+
]);
|
|
379
|
+
|
|
380
|
+
// NOT CAUGHT — read outside the boundary, derived value cached
|
|
381
|
+
layout((ctx) => {
|
|
382
|
+
const name = ctx.get(User).name; // allowed — this layout is not cached
|
|
383
|
+
ctx.set(UserName, name); // now a plain (cacheable) string
|
|
384
|
+
return <Outlet />;
|
|
385
|
+
}, () => [
|
|
386
|
+
cache({ ttl: 60 }, () => [
|
|
387
|
+
// a child reads ctx.get(UserName) and silently caches user-derived data
|
|
388
|
+
]),
|
|
389
|
+
]);
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
So do **not** read this as "you can't cache user data" — that overstates it and
|
|
393
|
+
breeds the false confidence that makes the derived leak _more_ likely. The guard
|
|
394
|
+
is deliberately non-propagating (propagation would cost a wrapper per derivation
|
|
395
|
+
on the hot path), and it is scoped to the `cache()` segment boundary. `"use
|
|
396
|
+
cache"` functions block the same request-scoped reads (`cookies()` / `headers()`
|
|
397
|
+
throw inside them) and additionally exclude tainted `ctx`/`env`/`req` args from
|
|
398
|
+
the cache key. The pattern that stays safe is also the natural one:
|
|
399
|
+
**read tainted context at the point of use, in the path that needs it (a loader or
|
|
400
|
+
live segment) — never extract user data into a plain value and cache that.**
|
|
401
|
+
Loaders are exempt because they run outside the cache scope and resolve fresh
|
|
402
|
+
every request.
|
|
403
|
+
|
|
197
404
|
## Loaders Are Always Fresh
|
|
198
405
|
|
|
199
406
|
Loaders are **never cached** by route-level `cache()`. Even on a full cache hit
|
|
@@ -272,21 +479,6 @@ data is cached independently from the route's segment cache. Loader caching
|
|
|
272
479
|
supports custom keys, tags, SWR, conditional bypass, and per-loader store
|
|
273
480
|
overrides — see `/loader` for the full reference.
|
|
274
481
|
|
|
275
|
-
## Decision Flowchart
|
|
276
|
-
|
|
277
|
-
1. Do you want to cache an entire route or group of routes?
|
|
278
|
-
**Yes** -> `cache()`
|
|
279
|
-
2. Do you need runtime conditions (skip for auth users, key by locale)?
|
|
280
|
-
**Yes** -> `cache()` with `condition` / `key`
|
|
281
|
-
3. Do you want to cache a data fetch shared across routes?
|
|
282
|
-
**Yes** -> `"use cache"`
|
|
283
|
-
4. Do you need different cache entries for different arguments?
|
|
284
|
-
**Yes** -> `"use cache"` (keyed by args)
|
|
285
|
-
5. Is the expensive part rendering, not data fetching?
|
|
286
|
-
**Yes** -> `cache()` (caches rendered segments)
|
|
287
|
-
6. Is the expensive part a single query inside a larger handler?
|
|
288
|
-
**Yes** -> `"use cache"` on the query function
|
|
289
|
-
|
|
290
482
|
## See Also
|
|
291
483
|
|
|
292
484
|
- `/caching` — cache() DSL setup, stores, nested boundaries
|