@rangojs/router 0.2.0 → 0.4.1
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 +19 -1
- package/dist/types/browser/event-controller.d.ts +4 -0
- package/dist/types/browser/link-interceptor.d.ts +17 -7
- package/dist/types/browser/navigation-bridge.d.ts +13 -1
- package/dist/types/browser/notify-listeners.d.ts +2 -0
- package/dist/types/browser/prefetch/cache.d.ts +1 -1
- package/dist/types/browser/prefetch/default-strategy.d.ts +31 -0
- package/dist/types/browser/prefetch/invalidation.d.ts +6 -0
- package/dist/types/browser/prefetch/loader.d.ts +3 -1
- package/dist/types/browser/prefetch/observer.d.ts +3 -6
- package/dist/types/browser/prefetch/runtime.d.ts +0 -1
- package/dist/types/browser/rango-state.d.ts +4 -0
- package/dist/types/browser/react/Link.d.ts +11 -19
- package/dist/types/browser/react/context.d.ts +3 -0
- package/dist/types/browser/rsc-router.d.ts +7 -2
- package/dist/types/browser/types.d.ts +6 -0
- package/dist/types/cache/cache-key-utils.d.ts +11 -3
- package/dist/types/cache/cache-scope.d.ts +82 -3
- package/dist/types/cache/cf/cf-cache-store.d.ts +2 -2
- package/dist/types/cache/index.d.ts +3 -1
- package/dist/types/cache/search-params-filter.d.ts +64 -0
- package/dist/types/cache/shell-snapshot.d.ts +4 -4
- package/dist/types/cache/types.d.ts +36 -2
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -2
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.rsc.d.ts +1 -0
- package/dist/types/router/match-middleware/cache-lookup.d.ts +13 -0
- package/dist/types/router/navigation-snapshot.d.ts +29 -0
- package/dist/types/router/prefetch-default.d.ts +28 -0
- package/dist/types/router/router-interfaces.d.ts +7 -0
- package/dist/types/router/router-options.d.ts +54 -0
- package/dist/types/rsc/capture-queue.d.ts +6 -0
- package/dist/types/rsc/shell-build-manifest.d.ts +7 -1
- package/dist/types/rsc/shell-capture.d.ts +15 -7
- package/dist/types/rsc/shell-serve.d.ts +11 -4
- package/dist/types/rsc/types.d.ts +19 -0
- package/dist/types/server/request-context.d.ts +57 -4
- package/dist/types/testing/e2e/index.d.ts +2 -2
- package/dist/types/testing/e2e/page-helpers.d.ts +24 -0
- package/dist/types/testing/render-route.d.ts +10 -1
- package/dist/types/testing/shell-status.d.ts +6 -3
- package/dist/types/vite/plugin-types.d.ts +1 -1
- package/dist/vite/index.js +9 -6
- package/package.json +23 -22
- package/skills/caching/SKILL.md +39 -0
- package/skills/comparison/references/framework-comparison.md +9 -2
- package/skills/links/SKILL.md +24 -0
- package/skills/ppr/SKILL.md +80 -16
- package/skills/router-setup/SKILL.md +9 -0
- package/skills/testing/client-components.md +15 -14
- package/skills/vercel/SKILL.md +1 -1
- package/src/browser/event-controller.ts +110 -11
- package/src/browser/link-interceptor.ts +469 -20
- package/src/browser/navigation-bridge.ts +71 -2
- package/src/browser/navigation-store.ts +24 -1
- package/src/browser/notify-listeners.ts +22 -0
- package/src/browser/prefetch/cache.ts +4 -2
- package/src/browser/prefetch/default-strategy.ts +74 -0
- package/src/browser/prefetch/invalidation.ts +30 -0
- package/src/browser/prefetch/loader.ts +18 -17
- package/src/browser/prefetch/observer.ts +50 -22
- package/src/browser/prefetch/runtime.ts +0 -1
- package/src/browser/rango-state.ts +21 -0
- package/src/browser/react/Link.tsx +111 -101
- package/src/browser/react/NavigationProvider.tsx +1 -0
- package/src/browser/react/context.ts +4 -0
- package/src/browser/rsc-router.tsx +30 -9
- package/src/browser/types.ts +6 -0
- package/src/cache/cache-key-utils.ts +18 -4
- package/src/cache/cache-runtime.ts +7 -2
- package/src/cache/cache-scope.ts +152 -23
- package/src/cache/cf/cf-cache-store.ts +55 -16
- package/src/cache/document-cache.ts +7 -1
- package/src/cache/index.ts +7 -0
- package/src/cache/search-params-filter.ts +118 -0
- package/src/cache/shell-snapshot.ts +8 -4
- package/src/cache/types.ts +39 -2
- package/src/cache/vercel/vercel-cache-store.ts +22 -3
- package/src/index.rsc.ts +4 -0
- package/src/index.ts +6 -0
- package/src/router/match-middleware/cache-lookup.ts +146 -33
- package/src/router/match-middleware/cache-store.ts +70 -0
- package/src/router/navigation-snapshot.ts +46 -6
- package/src/router/prefetch-default.ts +59 -0
- package/src/router/router-interfaces.ts +8 -0
- package/src/router/router-options.ts +59 -1
- package/src/router.ts +7 -0
- package/src/rsc/capture-queue.ts +24 -1
- package/src/rsc/full-payload.ts +1 -0
- package/src/rsc/handler.ts +10 -0
- package/src/rsc/response-cache-serve.ts +1 -1
- package/src/rsc/rsc-rendering.ts +367 -40
- package/src/rsc/shell-build-manifest.ts +8 -1
- package/src/rsc/shell-capture.ts +99 -50
- package/src/rsc/shell-serve.ts +14 -6
- package/src/rsc/types.ts +19 -0
- package/src/server/request-context.ts +62 -3
- package/src/testing/dispatch.ts +21 -3
- package/src/testing/e2e/index.ts +6 -0
- package/src/testing/e2e/page-helpers.ts +47 -0
- package/src/testing/e2e/parity.ts +13 -0
- package/src/testing/render-route.tsx +86 -17
- package/src/testing/shell-status.ts +20 -3
- package/src/vite/plugin-types.ts +1 -1
- package/src/vite/plugins/vercel-output.ts +2 -2
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* subtree, pass the `mount` option so useMount() returns the mounted prefix
|
|
38
38
|
* (the segment chain is wrapped in a MountContext exactly as in production).
|
|
39
39
|
*/
|
|
40
|
-
import type
|
|
40
|
+
import { type ComponentType } from "react";
|
|
41
41
|
import type { RenderResult } from "@testing-library/react";
|
|
42
42
|
import type { NavigationStore } from "../browser/types.js";
|
|
43
43
|
import type { EventController } from "../browser/event-controller.js";
|
|
@@ -45,6 +45,7 @@ import type { LoaderDefinition } from "../types.js";
|
|
|
45
45
|
import type { LocationStateDefinition } from "../browser/react/location-state-shared.js";
|
|
46
46
|
import type { Handle } from "../handle.js";
|
|
47
47
|
import type { ThemeConfig } from "../theme/types.js";
|
|
48
|
+
import type { PrefetchStrategy } from "../router/prefetch-default.js";
|
|
48
49
|
/**
|
|
49
50
|
* Seed shape for `options.handle`, matching the handle wire format:
|
|
50
51
|
* `{ [handleName]: { [segmentId]: pushedValues[] } }` (each segment accumulates
|
|
@@ -218,6 +219,14 @@ export interface RenderRouteOptions {
|
|
|
218
219
|
* expect(getByTestId("nonce").textContent).toBe("test-nonce");
|
|
219
220
|
*/
|
|
220
221
|
nonce?: string;
|
|
222
|
+
/**
|
|
223
|
+
* Router default prefetch strategy scoped to this rendered tree. This mirrors
|
|
224
|
+
* `createRouter({ defaultPrefetch })` for Links and eligible plain anchors
|
|
225
|
+
* inside `basename`. `data-prefetch="false"`/`"none"` opts out; `"true"`
|
|
226
|
+
* allows an application route with a common static-resource suffix but does
|
|
227
|
+
* not override a `"none"` default.
|
|
228
|
+
*/
|
|
229
|
+
defaultPrefetch?: PrefetchStrategy;
|
|
221
230
|
}
|
|
222
231
|
/**
|
|
223
232
|
* Imperative handle returned alongside the RTL result.
|
|
@@ -27,13 +27,14 @@
|
|
|
27
27
|
* Import from `@rangojs/router/testing` (Vitest) or `@rangojs/router/testing/e2e`
|
|
28
28
|
* (Playwright — same pure helpers, no Vite virtuals).
|
|
29
29
|
*/
|
|
30
|
+
import { type CacheSearchParams } from "../cache/search-params-filter.js";
|
|
30
31
|
/** Production header name (`rsc/shell-serve.ts` `SHELL_STATUS_HEADER`). */
|
|
31
32
|
export declare const SHELL_STATUS_HEADER: string;
|
|
32
33
|
/** Partial-navigation replay decision header from `rsc/rsc-rendering.ts`. */
|
|
33
34
|
export declare const PPR_REPLAY_STATUS_HEADER: string;
|
|
34
35
|
/** Values the serve path writes on `x-rango-shell`. */
|
|
35
36
|
export type ShellStatus = "HIT" | "MISS";
|
|
36
|
-
declare const PPR_REPLAY_BYPASS_REASONS: readonly ["method", "dynamic", "nonce", "store-unavailable", "passive-read-unsupported", "read-error", "no-entry", "invalid-version", "corrupt-entry", "handler-live-holes", "transition-when", "no-segment-snapshot", "snapshot-miss", "stale-build-entry"];
|
|
37
|
+
declare const PPR_REPLAY_BYPASS_REASONS: readonly ["method", "dynamic", "nonce", "store-unavailable", "passive-read-unsupported", "no-navigation-context", "prerender-store", "intercept", "cache-disabled", "read-error", "no-entry", "invalid-version", "corrupt-entry", "handler-live-holes", "transition-when", "no-segment-snapshot", "snapshot-miss", "explicit-cache-hit", "stale-build-entry"];
|
|
37
38
|
/** Bounded reasons a PPR partial request can fall open to ordinary matching. */
|
|
38
39
|
export type PprReplayBypassReason = (typeof PPR_REPLAY_BYPASS_REASONS)[number];
|
|
39
40
|
/** Parsed `x-rango-ppr-replay` value. */
|
|
@@ -55,9 +56,11 @@ export type ShellStatusTarget = Response | {
|
|
|
55
56
|
* module (it pulls React). A parity test pins the two implementations.
|
|
56
57
|
*
|
|
57
58
|
* Accepts a `URL` or an absolute/relative request URL string (relative strings
|
|
58
|
-
* resolve against `http://localhost`).
|
|
59
|
+
* resolve against `http://localhost`). Pass the router's `cache.searchParams`
|
|
60
|
+
* config as `searchParams` when the app under test sets one — the production
|
|
61
|
+
* key applies it, so the expected key must too.
|
|
59
62
|
*/
|
|
60
|
-
export declare function shellCacheKey(url: URL | string): string;
|
|
63
|
+
export declare function shellCacheKey(url: URL | string, searchParams?: CacheSearchParams): string;
|
|
61
64
|
/**
|
|
62
65
|
* Read `x-rango-shell` from a response. Returns `null` when the header is
|
|
63
66
|
* absent (axis-1 / non-ppr / ineligible request).
|
|
@@ -236,7 +236,7 @@ export interface RangoCloudflareOptions extends RangoBaseOptions {
|
|
|
236
236
|
* `.vc-config.json` (and `config.json` for `functionName`).
|
|
237
237
|
*/
|
|
238
238
|
export interface VercelPresetOptions {
|
|
239
|
-
/** Node runtime for the function. @default "
|
|
239
|
+
/** Node runtime for the function. @default "nodejs24.x" */
|
|
240
240
|
runtime?: string;
|
|
241
241
|
/** Max execution time in seconds. @default 30 */
|
|
242
242
|
maxDuration?: number;
|
package/dist/vite/index.js
CHANGED
|
@@ -2530,7 +2530,7 @@ import { resolve } from "node:path";
|
|
|
2530
2530
|
// package.json
|
|
2531
2531
|
var package_default = {
|
|
2532
2532
|
name: "@rangojs/router",
|
|
2533
|
-
version: "0.
|
|
2533
|
+
version: "0.4.1",
|
|
2534
2534
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
2535
2535
|
keywords: [
|
|
2536
2536
|
"react",
|
|
@@ -2727,7 +2727,8 @@ var package_default = {
|
|
|
2727
2727
|
picomatch: "^4.0.4",
|
|
2728
2728
|
"rsc-html-stream": "^0.0.7",
|
|
2729
2729
|
srvx: "^0.11.15",
|
|
2730
|
-
tinyexec: "^0.3.2"
|
|
2730
|
+
tinyexec: "^0.3.2",
|
|
2731
|
+
typescript: "^5.3.0 || ^6.0.0"
|
|
2731
2732
|
},
|
|
2732
2733
|
devDependencies: {
|
|
2733
2734
|
"@opentelemetry/api": "^1.9.0",
|
|
@@ -2745,7 +2746,6 @@ var package_default = {
|
|
|
2745
2746
|
jiti: "^2.7.0",
|
|
2746
2747
|
react: "catalog:",
|
|
2747
2748
|
"react-dom": "catalog:",
|
|
2748
|
-
typescript: "^5.3.0",
|
|
2749
2749
|
vitest: "^4.1.9"
|
|
2750
2750
|
},
|
|
2751
2751
|
peerDependencies: {
|
|
@@ -4353,7 +4353,7 @@ export default toNodeHandler(fetchHandler);
|
|
|
4353
4353
|
function assertVercelNodeRuntime(runtime) {
|
|
4354
4354
|
if (runtime != null && !runtime.startsWith("nodejs")) {
|
|
4355
4355
|
throw new Error(
|
|
4356
|
-
`[rango] preset "vercel": runtime "${runtime}" is not supported. This preset emits a Node serverless function; use a "nodejs*" runtime (default "
|
|
4356
|
+
`[rango] preset "vercel": runtime "${runtime}" is not supported. This preset emits a Node serverless function; use a "nodejs*" runtime (default "nodejs24.x"). The Edge runtime is not supported.`
|
|
4357
4357
|
);
|
|
4358
4358
|
}
|
|
4359
4359
|
}
|
|
@@ -4368,7 +4368,7 @@ function assertValidVercelFunctionName(functionName) {
|
|
|
4368
4368
|
}
|
|
4369
4369
|
function buildVercelVcConfig(vercel) {
|
|
4370
4370
|
const vcConfig = {
|
|
4371
|
-
runtime: vercel.runtime ?? "
|
|
4371
|
+
runtime: vercel.runtime ?? "nodejs24.x",
|
|
4372
4372
|
handler: "index.mjs",
|
|
4373
4373
|
launcherType: "Nodejs",
|
|
4374
4374
|
shouldAddHelpers: false,
|
|
@@ -4715,10 +4715,13 @@ var PUSH_CALLBACK_SCOPE_KEY = /* @__PURE__ */ Symbol.for(
|
|
|
4715
4715
|
);
|
|
4716
4716
|
var pushCallbackScopeALS = globalThis[PUSH_CALLBACK_SCOPE_KEY] ??= new AsyncLocalStorage();
|
|
4717
4717
|
|
|
4718
|
+
// src/rsc/shell-capture-constants.ts
|
|
4719
|
+
var SHELL_CAPTURE_MAX_WAIT_MS = 15e3;
|
|
4720
|
+
|
|
4718
4721
|
// src/rsc/shell-serve.ts
|
|
4719
4722
|
var DEV_SHELL_PROBE_TIMEOUT_MS = 1e4;
|
|
4720
4723
|
function normalizeCaptureTimeout(value) {
|
|
4721
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 1 ? value : void 0;
|
|
4724
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 1 ? Math.min(value, SHELL_CAPTURE_MAX_WAIT_MS) : void 0;
|
|
4722
4725
|
}
|
|
4723
4726
|
|
|
4724
4727
|
// src/vite/inject-client-debug.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -176,6 +176,19 @@
|
|
|
176
176
|
"access": "public",
|
|
177
177
|
"tag": "latest"
|
|
178
178
|
},
|
|
179
|
+
"scripts": {
|
|
180
|
+
"build": "pnpm run build:types && pnpm exec 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 exec esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && pnpm exec 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",
|
|
181
|
+
"build:types": "rm -rf dist/types && pnpm exec tsc -p tsconfig.types.json",
|
|
182
|
+
"prepublishOnly": "pnpm build",
|
|
183
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
184
|
+
"test": "playwright test",
|
|
185
|
+
"test:ui": "playwright test --ui",
|
|
186
|
+
"test:hmr-local": "playwright test --project=dev-warmup --project=hmr-basename --project=hmr-prerender --no-deps --workers=1 && RANGO_E2E_ROUTE_HMR_ONLY=1 playwright test --project=hmr-routes --no-deps --workers=1",
|
|
187
|
+
"test:hmr-routes-local": "RANGO_E2E_ROUTE_HMR_ONLY=1 playwright test --project=hmr-routes --no-deps --workers=1",
|
|
188
|
+
"test:unit": "pnpm run build:types && vitest run",
|
|
189
|
+
"test:unit:watch": "vitest",
|
|
190
|
+
"test:unit:rsc": "vitest run --config vitest.rsc.config.ts"
|
|
191
|
+
},
|
|
179
192
|
"dependencies": {
|
|
180
193
|
"@types/debug": "^4.1.12",
|
|
181
194
|
"@vitejs/plugin-rsc": "^0.5.27",
|
|
@@ -184,26 +197,26 @@
|
|
|
184
197
|
"picomatch": "^4.0.4",
|
|
185
198
|
"rsc-html-stream": "^0.0.7",
|
|
186
199
|
"srvx": "^0.11.15",
|
|
187
|
-
"tinyexec": "^0.3.2"
|
|
200
|
+
"tinyexec": "^0.3.2",
|
|
201
|
+
"typescript": "^5.3.0 || ^6.0.0"
|
|
188
202
|
},
|
|
189
203
|
"devDependencies": {
|
|
190
204
|
"@opentelemetry/api": "^1.9.0",
|
|
191
205
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
192
206
|
"@opentelemetry/sdk-trace-base": "^2.9.0",
|
|
193
207
|
"@playwright/test": "^1.49.1",
|
|
208
|
+
"@shared/e2e": "workspace:*",
|
|
194
209
|
"@testing-library/dom": "^10.4.1",
|
|
195
210
|
"@testing-library/react": "^16.3.2",
|
|
196
211
|
"@types/node": "^24.10.1",
|
|
197
|
-
"@types/react": "
|
|
198
|
-
"@types/react-dom": "
|
|
212
|
+
"@types/react": "catalog:",
|
|
213
|
+
"@types/react-dom": "catalog:",
|
|
199
214
|
"esbuild": "^0.28.1",
|
|
200
215
|
"happy-dom": "^20.10.1",
|
|
201
216
|
"jiti": "^2.7.0",
|
|
202
|
-
"react": "
|
|
203
|
-
"react-dom": "
|
|
204
|
-
"
|
|
205
|
-
"vitest": "^4.1.9",
|
|
206
|
-
"@shared/e2e": "0.0.1"
|
|
217
|
+
"react": "catalog:",
|
|
218
|
+
"react-dom": "catalog:",
|
|
219
|
+
"vitest": "^4.1.9"
|
|
207
220
|
},
|
|
208
221
|
"peerDependencies": {
|
|
209
222
|
"@cloudflare/vite-plugin": "^1.42.1",
|
|
@@ -242,17 +255,5 @@
|
|
|
242
255
|
},
|
|
243
256
|
"engines": {
|
|
244
257
|
"node": ">=24.0.0"
|
|
245
|
-
},
|
|
246
|
-
"scripts": {
|
|
247
|
-
"build": "pnpm run build:types && pnpm exec 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 exec esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && pnpm exec 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",
|
|
248
|
-
"build:types": "rm -rf dist/types && pnpm exec tsc -p tsconfig.types.json",
|
|
249
|
-
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
250
|
-
"test": "playwright test",
|
|
251
|
-
"test:ui": "playwright test --ui",
|
|
252
|
-
"test:hmr-local": "playwright test --project=dev-warmup --project=hmr-basename --project=hmr-prerender --no-deps --workers=1 && RANGO_E2E_ROUTE_HMR_ONLY=1 playwright test --project=hmr-routes --no-deps --workers=1",
|
|
253
|
-
"test:hmr-routes-local": "RANGO_E2E_ROUTE_HMR_ONLY=1 playwright test --project=hmr-routes --no-deps --workers=1",
|
|
254
|
-
"test:unit": "pnpm run build:types && vitest run",
|
|
255
|
-
"test:unit:watch": "vitest",
|
|
256
|
-
"test:unit:rsc": "vitest run --config vitest.rsc.config.ts"
|
|
257
258
|
}
|
|
258
|
-
}
|
|
259
|
+
}
|
package/skills/caching/SKILL.md
CHANGED
|
@@ -267,6 +267,45 @@ const router = createRouter({
|
|
|
267
267
|
});
|
|
268
268
|
```
|
|
269
269
|
|
|
270
|
+
### Search param key filtering (`cache.searchParams`)
|
|
271
|
+
|
|
272
|
+
By default every non-reserved query param keys the cache, so
|
|
273
|
+
`?utm_source=tw` and `?utm_source=ig` occupy separate slots in every tier.
|
|
274
|
+
The global `searchParams` option controls which params participate in default
|
|
275
|
+
cache-key generation:
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
import { createRouter, TRACKING_SEARCH_PARAMS } from "@rangojs/router";
|
|
279
|
+
|
|
280
|
+
const router = createRouter({
|
|
281
|
+
document: Document,
|
|
282
|
+
urls: urlpatterns,
|
|
283
|
+
cache: {
|
|
284
|
+
store,
|
|
285
|
+
// "all" (default) | "none" | { include: string[] } | { exclude: string[] }
|
|
286
|
+
searchParams: { exclude: TRACKING_SEARCH_PARAMS },
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
- **Key-only**: `ctx.searchParams` and the request URL are untouched — handlers
|
|
292
|
+
and loaders still see the full query string.
|
|
293
|
+
- **Matching**: exact names plus a `*` SUFFIX wildcard (`"utm_*"`). No RegExp.
|
|
294
|
+
- **All tiers**: segment, document, response, PPR shell capture/lookup, the
|
|
295
|
+
baked-shell manifest gate (a URL whose only params are excluded ones now
|
|
296
|
+
matches the prerendered shell), and `"use cache"` ctx key normalization.
|
|
297
|
+
- **`cache({ key })` override wins**: a custom key bypasses the filter along
|
|
298
|
+
with the rest of default key generation.
|
|
299
|
+
- **The footgun**: excluding a param promises the rendered output does not
|
|
300
|
+
depend on it. If it does, the first variant is cached and served to everyone.
|
|
301
|
+
That is why the default is `"all"`.
|
|
302
|
+
- `TRACKING_SEARCH_PARAMS` (also exported from `@rangojs/router/cache`) covers
|
|
303
|
+
`utm_*`, `gclid`, `fbclid`, `msclkid`, `ttclid`, `mc_cid`/`mc_eid`, and the
|
|
304
|
+
other common click-id params.
|
|
305
|
+
|
|
306
|
+
Global-only by design — the per-route "this page varies only by `q`" case is
|
|
307
|
+
already expressible with `cache({ key })`.
|
|
308
|
+
|
|
270
309
|
## Cache Stores
|
|
271
310
|
|
|
272
311
|
### Memory Store
|
|
@@ -539,9 +539,16 @@ analog but operates on loader/query reload, not RSC-segment render.
|
|
|
539
539
|
|
|
540
540
|
### Prefetching: stability and control
|
|
541
541
|
|
|
542
|
-
`<Link prefetch="hover|viewport|render|adaptive|none">` (default
|
|
542
|
+
`<Link prefetch="hover|viewport|render|adaptive|none">` (default: the router's
|
|
543
|
+
`defaultPrefetch`, `"none"` in development and `"viewport"` in production), plus
|
|
543
544
|
`prefetchKey` (`":source"` scopes a prefetch to the originating page for routes
|
|
544
|
-
whose response branches on `currentUrl`).
|
|
545
|
+
whose response branches on `currentUrl`). Intercepted plain `<a href>` elements
|
|
546
|
+
inside the router basename follow the same default; common static-resource
|
|
547
|
+
extensions are excluded unless `data-prefetch="true"` identifies an application
|
|
548
|
+
route, and `data-prefetch="false"` or `"none"` opts out unsafe GET links. A
|
|
549
|
+
container with `data-prefetch-scope="false"` or `"none"` is a hard subtree boundary for both
|
|
550
|
+
Links and plain anchors, so one annotation suppresses speculative work for a
|
|
551
|
+
whole navigation section. The distinguishing part is the stability
|
|
545
552
|
gating: a queued prefetch (`viewport`/`render`) will not fire until **both** the
|
|
546
553
|
main thread is idle (`requestIdleCallback`, 200ms fallback) **and**
|
|
547
554
|
`waitForViewportImages()` resolves — in-viewport images that are not `.complete`
|
package/skills/links/SKILL.md
CHANGED
|
@@ -423,6 +423,30 @@ Don't edit the file by hand — re-run codegen when patterns change.
|
|
|
423
423
|
}
|
|
424
424
|
```
|
|
425
425
|
|
|
426
|
+
## Prefetch boundaries
|
|
427
|
+
|
|
428
|
+
`Link` follows its `prefetch` prop or the router's `defaultPrefetch`; eligible
|
|
429
|
+
plain anchors follow the router default. When a whole DOM section must never
|
|
430
|
+
speculate, mark its container instead of repeating per-link opt-outs:
|
|
431
|
+
|
|
432
|
+
```tsx
|
|
433
|
+
<section data-prefetch-scope="none">
|
|
434
|
+
<Link to="/activity" prefetch="viewport">
|
|
435
|
+
Activity
|
|
436
|
+
</Link>
|
|
437
|
+
<a href="/export">Export</a>
|
|
438
|
+
</section>
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
The scope is a hard opt-out for every descendant Link and plain anchor; `"false"`
|
|
442
|
+
and `"none"` are equivalent. An explicit `prefetch` prop or
|
|
443
|
+
`data-prefetch="true"` cannot override it, and navigation still works normally.
|
|
444
|
+
Use `data-prefetch="false"` or `"none"` directly on a plain anchor when only that
|
|
445
|
+
anchor is unsafe. Dynamic scope changes re-evaluate only descendant anchors and
|
|
446
|
+
mounted viewport/render Links. Links share one document-level scope observer,
|
|
447
|
+
not one observer per Link or container. Adding a scope cannot recall work already
|
|
448
|
+
queued or in flight; removing it re-arms both Link types.
|
|
449
|
+
|
|
426
450
|
## When to use what
|
|
427
451
|
|
|
428
452
|
| Context | API | Resolves | Use for |
|
package/skills/ppr/SKILL.md
CHANGED
|
@@ -175,12 +175,14 @@ On a document GET to a ppr route the router runs:
|
|
|
175
175
|
point is after the chain, an unauthorized request NEVER sees shell bytes — put
|
|
176
176
|
auth middleware anywhere (global or route DSL) and it guards PPR for free.
|
|
177
177
|
|
|
178
|
-
### Soft navigation reuses the
|
|
178
|
+
### Soft navigation caches and reuses the handler layer
|
|
179
179
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
180
|
+
Ordinary partial RSC navigations to a `ppr` URL use the same handler-layer cache
|
|
181
|
+
contract even when no document request has captured an HTML shell yet. When a
|
|
182
|
+
shell snapshot exists, the server replays its canonical document segment record.
|
|
183
|
+
On a cold partial request, normal matching renders the response and schedules a
|
|
184
|
+
background navigation-only shell capture; later navigations and prefetches
|
|
185
|
+
replay its eligible snapshot. In both cases `matchPartial()`:
|
|
184
186
|
|
|
185
187
|
- preserves client-owned shared layouts by segment id;
|
|
186
188
|
- returns only new or revalidating destination segments;
|
|
@@ -190,10 +192,30 @@ record through the normal `matchPartial()` pipeline, which then:
|
|
|
190
192
|
This is deliberately invisible to the browser: the response is the same
|
|
191
193
|
`RscPayload` shape as any other partial navigation. Captured item/response values
|
|
192
194
|
and loader-container pins are NOT replayed on this path, so loader reads stay
|
|
193
|
-
live.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
195
|
+
live.
|
|
196
|
+
|
|
197
|
+
A route's own `cache()` scope — including one inherited from an ancestor, the
|
|
198
|
+
common app-wide storefront shape — COMPOSES with replay instead of disabling
|
|
199
|
+
it. The explicit tier stays authoritative: its lookup runs first with its
|
|
200
|
+
normal store, key, TTL, SWR, tags, and condition, and when it supplies the
|
|
201
|
+
match the response reports `BYPASS; reason=explicit-cache-hit` (never a false
|
|
202
|
+
replay `HIT`). Only when the explicit tier misses does the shell snapshot's
|
|
203
|
+
canonical doc segment record supply the match and report `HIT`. To make that
|
|
204
|
+
possible, a capture of such a route records the doc segment record into the
|
|
205
|
+
shell snapshot IN ADDITION to the scope's normal store write; the record rides
|
|
206
|
+
only inside the shell entry, never the real store. Two opt-outs stay absolute:
|
|
207
|
+
`cache(false)` and a `condition()` returning false mean "do not serve this
|
|
208
|
+
request's segments from any cache" — no doc record is captured for them and
|
|
209
|
+
the seeded fallback never rescues a refused read. `cache(false)` is static, so
|
|
210
|
+
replay reports `BYPASS; reason=cache-disabled` before performing a single
|
|
211
|
+
shell read; a `condition()` refusal is request-time state, decided at the
|
|
212
|
+
lookup itself and reported as the same `cache-disabled` post-match (the gate
|
|
213
|
+
must not pre-decide a predicate that could flap between the two evaluations).
|
|
214
|
+
An errored explicit read — a throwing `key()`, or a built-in store's swallowed
|
|
215
|
+
backend failure (`CACHE_READ_ERROR`) — also never falls back: it renders
|
|
216
|
+
uncached, exactly as without ppr.
|
|
217
|
+
|
|
218
|
+
`transition({ when })` is evaluated from the
|
|
197
219
|
matched manifest before route handlers on every PPR match, so it can vary by
|
|
198
220
|
URL/params/action or middleware context without disabling replay; handler-set
|
|
199
221
|
context is unavailable by design. Intercepts, handler-live holes, an active
|
|
@@ -201,23 +223,60 @@ nonce, and an absent/corrupt segment snapshot fall open to the ordinary partial
|
|
|
201
223
|
path when encountered by the shell capture. A transition already replayed from
|
|
202
224
|
an explicit cache tier remains frozen by that tier's normal semantics.
|
|
203
225
|
|
|
226
|
+
Two more decisions are made before any shell-store read, so probes and
|
|
227
|
+
prerendered routes never spend passive `getShell` I/O:
|
|
228
|
+
|
|
229
|
+
- A partial request carrying neither `X-RSC-Router-Client-Path` nor `Referer`
|
|
230
|
+
(a curl probe, a synthetic monitor) can never produce a partial match; it
|
|
231
|
+
reports `BYPASS; reason=no-navigation-context`. Alert on replay hit-rate
|
|
232
|
+
with this in mind — such probes are not cache misses.
|
|
233
|
+
- A `Prerender()` route's partial is served from the build-time prerender
|
|
234
|
+
store inside matching (a better-than-HIT outcome); it reports
|
|
235
|
+
`BYPASS; reason=prerender-store`. Its captures never record a doc segment
|
|
236
|
+
record (the prerender store short-circuits the cache write), so replay
|
|
237
|
+
seeding would be impossible anyway.
|
|
238
|
+
|
|
204
239
|
Fresh and stale-within-SWR runtime shells replay. The stale read is passive: it
|
|
205
240
|
uses `getShell(key, { claimRevalidation: false })`, does not claim SWR ownership,
|
|
206
241
|
and cannot recapture HTML. A later document request owns the background
|
|
207
|
-
recapture; hard-expired entries
|
|
208
|
-
build manifest
|
|
209
|
-
|
|
242
|
+
recapture; hard-expired entries schedule a navigation-only capture. Production
|
|
243
|
+
may also use a fresh local build manifest. Development does not probe
|
|
244
|
+
`/__rsc_shell`; it schedules the same local background capture instead. Custom `SegmentCacheStore`
|
|
210
245
|
implementations must set `supportsPassiveShellReads: true` and honor the
|
|
211
246
|
non-claiming read option.
|
|
212
247
|
|
|
248
|
+
The first cold partial request reports `BYPASS; reason=no-entry` because no
|
|
249
|
+
artifact could supply that response; its successful render schedules
|
|
250
|
+
`scheduleShellCapture` with a navigation-only marker. The next request reports
|
|
251
|
+
`HIT` when the capture produced an eligible snapshot. Navigation snapshots use
|
|
252
|
+
a separate shell key, so they cannot overwrite or be served as a document shell.
|
|
253
|
+
The capture runs with the stripped target document URL and loads SSR support in
|
|
254
|
+
the bounded background queue, outside the triggering partial response's latency.
|
|
255
|
+
An `allReady` decision still declines capture.
|
|
256
|
+
|
|
213
257
|
Partial responses expose the actual decision as `x-rango-ppr-replay`:
|
|
214
258
|
`HIT; freshness=fresh|stale` or `BYPASS; reason=<bounded-token>`. With
|
|
215
259
|
performance metrics enabled, the same decision appears as
|
|
216
260
|
`ppr-navigation-replay` in `Server-Timing`. `HIT` means matching consumed the
|
|
217
261
|
seeded segment record after it decoded successfully, not merely that a snapshot
|
|
218
262
|
existed. An explicit `cache()` scope that supplies the match cannot produce a
|
|
219
|
-
false HIT
|
|
220
|
-
by normal Flight streaming, not reuse of
|
|
263
|
+
false HIT — it reports `explicit-cache-hit`. There is still no Flight resume
|
|
264
|
+
API; this is segment replay followed by normal Flight streaming, not reuse of
|
|
265
|
+
the HTML `prelude`/`postponed` bytes.
|
|
266
|
+
|
|
267
|
+
The bounded bypass tokens, grouped by when they are decided:
|
|
268
|
+
|
|
269
|
+
| Token | Decided | Meaning |
|
|
270
|
+
| --------------------------------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
271
|
+
| `method`, `dynamic`, `nonce`, `store-unavailable`, `passive-read-unsupported` | pre-read | request/route/store ineligible for replay |
|
|
272
|
+
| `no-navigation-context` | pre-read | no `X-RSC-Router-Client-Path`/`Referer`; a partial match is impossible |
|
|
273
|
+
| `prerender-store` | pre-read or match | `Prerender()` route served by its baked artifact (pre-read probe of the normal variant; reclassified post-match when the store actually served, either variant) |
|
|
274
|
+
| `intercept` | match | the navigation resolved to an intercept — replay is never armed for intercepts (they keep their normal cache path); no heal capture |
|
|
275
|
+
| `cache-disabled` | pre-read or match | `cache(false)` (pre-read, static) or `condition()` false (decided at the lookup); consumer opt-out is absolute |
|
|
276
|
+
| `read-error`, `no-entry`, `invalid-version`, `corrupt-entry`, `stale-build-entry` | shell read | no usable shell entry (`no-entry`/`invalid-version`/`corrupt-entry`/`stale-build-entry` schedule the navigation-only heal capture) |
|
|
277
|
+
| `handler-live-holes`, `transition-when`, `no-segment-snapshot` | eligibility | entry exists but its snapshot cannot replay (no canonical doc record, or handler/transition liveness); on a route with an enabled `cache()` scope, `no-segment-snapshot` heals when the lookup did not refuse (a `condition()` false-at-capture entry becomes replayable) |
|
|
278
|
+
| `explicit-cache-hit` | match | the route's own `cache()` tier supplied the match |
|
|
279
|
+
| `snapshot-miss` | match | an eligible snapshot was seeded but matching did not consume it |
|
|
221
280
|
|
|
222
281
|
### Capture-generation invalidation
|
|
223
282
|
|
|
@@ -292,7 +351,9 @@ curl -s -D - -o /dev/null https://app.example.com/products/1 | grep -i x-rango-s
|
|
|
292
351
|
- Structured capture diagnostics: `createRouter({ debugShellCapture: true })`
|
|
293
352
|
logs one line per capture attempt/skip (outcome, durations, prelude and
|
|
294
353
|
snapshot bytes, backoff state); pass a function to receive each
|
|
295
|
-
`ShellCaptureDebugEvent` instead.
|
|
354
|
+
`ShellCaptureDebugEvent` instead. `skip-capacity` means the isolate already
|
|
355
|
+
has 32 queued/running captures; the dropped best-effort capture can retry on a
|
|
356
|
+
later request. In dev, with `debugPerformance` on, the
|
|
296
357
|
last capture outcome for a key also rides the next document GET's
|
|
297
358
|
`Server-Timing` as `ppr-capture;desc="…"`.
|
|
298
359
|
|
|
@@ -750,7 +811,10 @@ cache"` value baked into the shell is PINNED at capture (the capture data
|
|
|
750
811
|
it: call `cacheTag(...)` from the shell-material render code (the render-time
|
|
751
812
|
lever), or add the tag to `ppr.tags` (operational tags the render cannot know —
|
|
752
813
|
a tenant id, a deploy marker). Ring-1/ring-3 tag invalidation does NOT drop the
|
|
753
|
-
shell.
|
|
814
|
+
shell. Tags are optional: if TTL/SWR is the complete freshness policy, leave
|
|
815
|
+
the shell untagged. Rango does not warn for that choice; with
|
|
816
|
+
`debugShellCapture` enabled, a stored event reports `untaggedBake: true` when
|
|
817
|
+
bake-lane loader material uses TTL/SWR-only invalidation.
|
|
754
818
|
- **Uncached nondeterminism in the shell is a hydration hazard**: a raw
|
|
755
819
|
`Date.now()` / `Math.random()` / uncached `fetch` rendered directly in shell
|
|
756
820
|
material (outside any cache ring) drifts between capture and hit and the
|
|
@@ -120,6 +120,15 @@ interface RangoOptions<TEnv> {
|
|
|
120
120
|
// Set to false to disable prefetch caching.
|
|
121
121
|
prefetchCacheTTL?: number | false;
|
|
122
122
|
|
|
123
|
+
// Default prefetch strategy for Links without a `prefetch` prop and for
|
|
124
|
+
// intercepted plain anchors inside basename. false/none opts out; true allows
|
|
125
|
+
// an application route whose path has a common static-resource extension.
|
|
126
|
+
// data-prefetch-scope="false"/"none" on a container is a hard subtree
|
|
127
|
+
// opt-out for both Links and plain anchors, including explicit opt-ins.
|
|
128
|
+
// (dev: "none", production: "viewport").
|
|
129
|
+
// Per-Link props win over the router default, not a disabled container scope.
|
|
130
|
+
defaultPrefetch?: "hover" | "viewport" | "render" | "adaptive" | "none";
|
|
131
|
+
|
|
123
132
|
// CSP nonce provider (for router.fetch)
|
|
124
133
|
nonce?: (
|
|
125
134
|
request: Request,
|
|
@@ -8,20 +8,21 @@ RTL-style stub (peer of React Router's `createRoutesStub` / Expo's `renderRouter
|
|
|
8
8
|
|
|
9
9
|
### Options — `RenderRouteOptions`
|
|
10
10
|
|
|
11
|
-
| Field
|
|
12
|
-
|
|
|
13
|
-
| `request`
|
|
14
|
-
| `loaderData`
|
|
15
|
-
| `loaders`
|
|
16
|
-
| `params`
|
|
17
|
-
| `locationState`
|
|
18
|
-
| `handles`
|
|
19
|
-
| `handle`
|
|
20
|
-
| `routeMap`
|
|
21
|
-
| `basename`
|
|
22
|
-
| `mount`
|
|
23
|
-
| `theme`
|
|
24
|
-
| `nonce`
|
|
11
|
+
| Field | Type | Meaning |
|
|
12
|
+
| ----------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| `request` | `Request \| string` | Initial location. Only the URL is read (client render — headers/method ignored). Defaults to the leaf spec's static prefix or `"/"`. |
|
|
14
|
+
| `loaderData` | `Record<string, unknown>` | Loader data keyed by loader `$$id`. `useLoader(L)` reads `loaderData[L.$$id]`. |
|
|
15
|
+
| `loaders` | `ReadonlyArray<readonly [LoaderDefinition<any>, unknown]>` | Seed by REFERENCE: `[loader, data]` pairs. Robust for real `createLoader()` handles whose `$$id` is empty in a bare test. Prefer over `loaderData`. |
|
|
16
|
+
| `params` | `Record<string, string>` | Explicit params, merged over (and overriding) params extracted from the `request` URL. |
|
|
17
|
+
| `locationState` | `ReadonlyArray<readonly [LocationStateDefinition<any, any>, unknown]>` | Seed `useLocationState(def)` by REFERENCE: `[def, value]` pairs; written to `history.state`. |
|
|
18
|
+
| `handles` | `ReadonlyArray<readonly [Handle<any, any>, unknown[]]>` | Seed `useHandle(handle)` by REFERENCE: `[handle, pushedValues[]]`. Accumulated GLOBALLY (not segment-scoped). |
|
|
19
|
+
| `handle` | `HandleDataSeed` | Advanced: raw wire format `{ [handleId]: { [segmentId]: pushedValues[] } }`. Prefer `handles`. Merged with it. |
|
|
20
|
+
| `routeMap` | `Record<string, string>` | Name -> pattern map (informational; client `useReverse` takes its map as an argument, so this is not consumed). |
|
|
21
|
+
| `basename` | `string` | `createRouter({ basename })` value. Wired into `NavigationProvider` so `useRouter().basename`, `<Link>` prefixing, `useMount`/`useHref` resolve against the mount. Normalized like `createRouter`. Defaults to root. |
|
|
22
|
+
| `mount` | `string` | `include()` mount prefix. Wraps the segment chain in a `MountContext` so `useMount()` returns the prefix. Normalized like a path prefix. Defaults to `"/"`. |
|
|
23
|
+
| `theme` | `ThemeConfig \| true` | Theme config (`createRouter({ theme })` shape) to wrap the tree in a `ThemeProvider`. Defaults to no provider. A component calling `useTheme()` REQUIRES one. |
|
|
24
|
+
| `nonce` | `string` | CSP nonce to seed via `NonceContext`, so a component calling `useNonce()` (e.g. an analytics/GTM head script) sees it — mirroring SSR. Defaults to `undefined` (the browser default). |
|
|
25
|
+
| `defaultPrefetch` | `PrefetchStrategy` | Router default for `<Link>` and eligible plain anchors. `data-prefetch="false"`/`"none"` opts out one anchor; ancestor `data-prefetch-scope="false"`/`"none"` hard-disables the subtree; `"true"` permits routed resource suffixes elsewhere. |
|
|
25
26
|
|
|
26
27
|
`RenderRouteSpec = { path, Component, layout?, loaderIds?, name? }` — one node of the route definition. The array is the layout chain root-to-leaf; the LAST entry is the leaf route (its pattern is matched against `request` to extract params; layout patterns are informational). `loaderIds` attaches seeded loaders to THIS node's segment; `layout` on the leaf wraps it; `name` is informational.
|
|
27
28
|
|
package/skills/vercel/SKILL.md
CHANGED
|
@@ -56,7 +56,7 @@ Per-function knobs go under `vercel` and are written into `.vc-config.json`:
|
|
|
56
56
|
rango({
|
|
57
57
|
preset: "vercel",
|
|
58
58
|
vercel: {
|
|
59
|
-
runtime: "
|
|
59
|
+
runtime: "nodejs24.x", // default
|
|
60
60
|
maxDuration: 30, // seconds, default
|
|
61
61
|
memory: 1024, // MB (platform default when omitted)
|
|
62
62
|
regions: ["fra1"], // pin regions (platform default when omitted)
|