@rangojs/router 0.0.0-experimental.122 → 0.0.0-experimental.125
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/dist/bin/rango.js +10 -6
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +55 -48
- package/package.json +61 -21
- package/skills/caching/SKILL.md +2 -1
- package/skills/hooks/SKILL.md +40 -29
- package/skills/host-router/SKILL.md +16 -2
- package/skills/intercept/SKILL.md +4 -2
- package/skills/layout/SKILL.md +11 -6
- package/skills/loader/SKILL.md +6 -2
- package/skills/middleware/SKILL.md +4 -2
- package/skills/migrate-nextjs/SKILL.md +3 -1
- package/skills/parallel/SKILL.md +9 -4
- package/skills/rango/SKILL.md +12 -0
- package/skills/route/SKILL.md +10 -2
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +98 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +89 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +118 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +1 -83
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +14 -1
- package/src/browser/navigation-client.ts +14 -1
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +26 -51
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/partial-update.ts +1 -83
- package/src/browser/prefetch/cache.ts +6 -45
- package/src/browser/prefetch/fetch.ts +7 -0
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +157 -99
- package/src/browser/react/Link.tsx +0 -2
- package/src/browser/react/NavigationProvider.tsx +2 -1
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +0 -2
- package/src/browser/react/index.ts +0 -51
- package/src/browser/react/location-state-shared.ts +0 -13
- package/src/browser/react/location-state.ts +0 -1
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +0 -5
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +0 -3
- package/src/browser/react/use-params.ts +0 -2
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/rsc-router.tsx +12 -4
- package/src/browser/server-action-bridge.ts +77 -15
- package/src/browser/types.ts +7 -2
- package/src/browser/validate-redirect-origin.ts +4 -5
- package/src/build/route-trie.ts +3 -0
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/router-processing.ts +0 -8
- package/src/cache/cache-policy.ts +0 -54
- package/src/cache/cache-runtime.ts +27 -24
- package/src/cache/cache-scope.ts +0 -27
- package/src/cache/cache-tag.ts +0 -37
- package/src/cache/cf/cf-cache-store.ts +94 -46
- package/src/cache/cf/index.ts +0 -24
- package/src/cache/document-cache.ts +11 -36
- package/src/cache/handle-snapshot.ts +0 -40
- package/src/cache/index.ts +0 -27
- package/src/cache/memory-segment-store.ts +2 -48
- package/src/cache/profile-registry.ts +7 -3
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/types.ts +0 -98
- package/src/client.rsc.tsx +1 -22
- package/src/client.tsx +14 -38
- package/src/component-utils.ts +19 -0
- package/src/deps/ssr.ts +0 -1
- package/src/handle.ts +28 -18
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +6 -0
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +1 -65
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +0 -4
- package/src/index.rsc.ts +42 -3
- package/src/index.ts +31 -1
- package/src/internal-debug.ts +2 -4
- package/src/loader.rsc.ts +19 -9
- package/src/loader.ts +12 -4
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +23 -30
- package/src/prerender.ts +58 -3
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +1 -44
- package/src/route-definition/dsl-helpers.ts +7 -19
- package/src/route-definition/helpers-types.ts +3 -3
- package/src/route-definition/redirect.ts +11 -1
- package/src/route-map-builder.ts +0 -16
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +0 -13
- package/src/router/error-handling.ts +12 -16
- package/src/router/find-match.ts +4 -30
- package/src/router/intercept-resolution.ts +10 -1
- package/src/router/lazy-includes.ts +1 -57
- package/src/router/loader-resolution.ts +3 -2
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +1 -25
- package/src/router/match-api.ts +0 -20
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +57 -58
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +1 -54
- package/src/router/match-middleware/cache-store.ts +0 -31
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -21
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +1 -52
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-cookies.ts +0 -13
- package/src/router/middleware-types.ts +0 -115
- package/src/router/middleware.ts +7 -30
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +1 -33
- package/src/router/prerender-match.ts +33 -45
- package/src/router/request-classification.ts +1 -38
- package/src/router/revalidation.ts +5 -58
- package/src/router/router-context.ts +0 -26
- package/src/router/router-interfaces.ts +7 -0
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +25 -57
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +10 -13
- package/src/router/segment-resolution/revalidation.ts +5 -42
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +63 -40
- package/src/router/types.ts +1 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +40 -9
- package/src/rsc/handler.ts +14 -2
- package/src/rsc/helpers.ts +34 -0
- package/src/rsc/origin-guard.ts +0 -12
- package/src/rsc/progressive-enhancement.ts +4 -1
- package/src/rsc/rsc-rendering.ts +4 -7
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +30 -28
- package/src/rsc/types.ts +2 -1
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +0 -16
- package/src/segment-loader-promise.ts +14 -2
- package/src/segment-system.tsx +79 -88
- package/src/server/cookie-store.ts +52 -1
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +74 -77
- package/src/ssr/index.tsx +14 -14
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +119 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +581 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +127 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +186 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +98 -0
- package/src/testing/internal/context.ts +348 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +311 -0
- package/src/testing/render-route.tsx +504 -0
- package/src/testing/run-loader.ts +378 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/error-types.ts +25 -89
- package/src/types/global-namespace.ts +15 -15
- package/src/types/handler-context.ts +16 -13
- package/src/types/index.ts +0 -10
- package/src/types/request-scope.ts +0 -19
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +0 -13
- package/src/urls/include-helper.ts +0 -4
- package/src/urls/index.ts +0 -6
- package/src/urls/path-helper-types.ts +2 -2
- package/src/urls/path-helper.ts +0 -54
- package/src/urls/urls-function.ts +0 -13
- package/src/use-loader.tsx +0 -186
- package/src/vite/discovery/bundle-postprocess.ts +2 -1
- package/src/vite/discovery/discover-routers.ts +6 -7
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/plugin-types.ts +3 -1
- package/src/vite/plugins/cjs-to-esm.ts +0 -11
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +0 -10
- package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
- package/src/vite/plugins/expose-action-id.ts +2 -73
- package/src/vite/plugins/expose-id-utils.ts +0 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
- package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
- package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
- package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
- package/src/vite/plugins/expose-internal-ids.ts +10 -0
- package/src/vite/plugins/performance-tracks.ts +0 -3
- package/src/vite/plugins/use-cache-transform.ts +0 -36
- package/src/vite/plugins/version-injector.ts +0 -20
- package/src/vite/plugins/version-plugin.ts +1 -49
- package/src/vite/plugins/virtual-entries.ts +0 -15
- package/src/vite/rango.ts +1 -108
- package/src/vite/router-discovery.ts +2 -1
- package/src/vite/utils/ast-handler-extract.ts +0 -16
- package/src/vite/utils/bundle-analysis.ts +6 -13
- package/src/vite/utils/client-chunks.ts +0 -6
- package/src/vite/utils/forward-user-plugins.ts +0 -22
- package/src/vite/utils/manifest-utils.ts +0 -4
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +0 -35
- package/src/vite/utils/shared-utils.ts +3 -35
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
|
@@ -189,12 +189,26 @@ Logs pattern matching, route registration, and cookie override decisions to cons
|
|
|
189
189
|
## Testing
|
|
190
190
|
|
|
191
191
|
```typescript
|
|
192
|
-
import {
|
|
192
|
+
import {
|
|
193
|
+
createTestRequest,
|
|
194
|
+
testPattern,
|
|
195
|
+
matchesHost,
|
|
196
|
+
} from "@rangojs/router/host/testing";
|
|
193
197
|
|
|
194
|
-
// Test pattern matching
|
|
198
|
+
// Test pattern matching (host-only)
|
|
195
199
|
testPattern("admin.*", "admin.example.com"); // true
|
|
196
200
|
testPattern([".", "www.*"], "example.com"); // true
|
|
197
201
|
|
|
202
|
+
// Path-based patterns need the third pathname arg (defaults to "/", so a
|
|
203
|
+
// host-only pattern still works with two args):
|
|
204
|
+
testPattern("**.workers.dev/admin", "foo.workers.dev", "/admin"); // true
|
|
205
|
+
|
|
206
|
+
// Or match a pattern against a real Request (hostname + pathname from the URL):
|
|
207
|
+
matchesHost(
|
|
208
|
+
"**.workers.dev/admin",
|
|
209
|
+
new Request("https://foo.workers.dev/admin"),
|
|
210
|
+
); // true
|
|
211
|
+
|
|
198
212
|
// Create requests for integration tests
|
|
199
213
|
const request = createTestRequest({
|
|
200
214
|
host: "admin.example.com",
|
|
@@ -107,8 +107,10 @@ Use named revalidation contracts on both the outer producer and the intercept
|
|
|
107
107
|
consumer when they share `ctx.set()` data:
|
|
108
108
|
|
|
109
109
|
```typescript
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
import * as ProductActions from "./actions/product";
|
|
111
|
+
|
|
112
|
+
export const revalidateProductShell = (ctx) =>
|
|
113
|
+
ctx.isAction(ProductActions) || undefined;
|
|
112
114
|
|
|
113
115
|
layout(ProductLayout, () => [
|
|
114
116
|
revalidate(revalidateProductShell), // producer reruns
|
package/skills/layout/SKILL.md
CHANGED
|
@@ -202,8 +202,10 @@ layout(<ShopLayout />, () => [
|
|
|
202
202
|
])
|
|
203
203
|
|
|
204
204
|
// Or revalidate based on conditions
|
|
205
|
+
import * as CartActions from "./actions/cart";
|
|
206
|
+
|
|
205
207
|
layout(<CartLayout />, () => [
|
|
206
|
-
revalidate((
|
|
208
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
207
209
|
|
|
208
210
|
path("/cart", CartPage, { name: "cart" }),
|
|
209
211
|
])
|
|
@@ -221,8 +223,9 @@ them on both producer and consumer segments:
|
|
|
221
223
|
|
|
222
224
|
```typescript
|
|
223
225
|
// revalidation-contracts.ts
|
|
224
|
-
|
|
225
|
-
|
|
226
|
+
import { addToCart } from "./actions/cart";
|
|
227
|
+
|
|
228
|
+
export const revalidateCartData = (ctx) => ctx.isAction(addToCart) || undefined;
|
|
226
229
|
```
|
|
227
230
|
|
|
228
231
|
```typescript
|
|
@@ -242,9 +245,10 @@ You can also package them as importable handoff helpers:
|
|
|
242
245
|
```typescript
|
|
243
246
|
// revalidation-contracts.ts
|
|
244
247
|
import { revalidate } from "@rangojs/router";
|
|
248
|
+
import * as AuthActions from "./actions/auth";
|
|
245
249
|
|
|
246
|
-
export const revalidateAuthData = (
|
|
247
|
-
|
|
250
|
+
export const revalidateAuthData = (ctx) =>
|
|
251
|
+
ctx.isAction(AuthActions) || undefined;
|
|
248
252
|
export const revalidateAuth = () => [revalidate(revalidateAuthData)];
|
|
249
253
|
```
|
|
250
254
|
|
|
@@ -262,6 +266,7 @@ layout(<ShellLayout />, () => [
|
|
|
262
266
|
```typescript
|
|
263
267
|
import { urls } from "@rangojs/router";
|
|
264
268
|
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
269
|
+
import * as CartActions from "./actions/cart";
|
|
265
270
|
|
|
266
271
|
function ShopLayout() {
|
|
267
272
|
return (
|
|
@@ -291,7 +296,7 @@ export const shopPatterns = urls(({ path, layout, parallel, loader, revalidate }
|
|
|
291
296
|
}, () => [
|
|
292
297
|
// Layout loaders
|
|
293
298
|
loader(CartLoader, () => [
|
|
294
|
-
revalidate((
|
|
299
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
295
300
|
]),
|
|
296
301
|
|
|
297
302
|
// Parallel routes
|
package/skills/loader/SKILL.md
CHANGED
|
@@ -249,6 +249,8 @@ export const OrderLoader = createLoader(async (ctx) => {
|
|
|
249
249
|
Add caching or revalidation to specific loaders:
|
|
250
250
|
|
|
251
251
|
```typescript
|
|
252
|
+
import * as CartActions from "./actions/cart";
|
|
253
|
+
|
|
252
254
|
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
253
255
|
// Cached loader
|
|
254
256
|
loader(ProductLoader, () => [cache({ ttl: 300 })]),
|
|
@@ -261,7 +263,7 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
261
263
|
// Loader that revalidates after cart actions (defer otherwise — keeps the
|
|
262
264
|
// permissive loader defaults for navigation and other actions intact)
|
|
263
265
|
loader(CartLoader, () => [
|
|
264
|
-
revalidate((
|
|
266
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
265
267
|
]),
|
|
266
268
|
]);
|
|
267
269
|
```
|
|
@@ -781,10 +783,12 @@ export const CartLoader = createLoader(async (ctx) => {
|
|
|
781
783
|
});
|
|
782
784
|
|
|
783
785
|
// urls.tsx — register loaders in the DSL
|
|
786
|
+
import * as CartActions from "./actions/cart";
|
|
787
|
+
|
|
784
788
|
export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalidate }) => [
|
|
785
789
|
layout(<ShopLayout />, () => [
|
|
786
790
|
loader(CartLoader, () => [
|
|
787
|
-
revalidate((
|
|
791
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
788
792
|
]),
|
|
789
793
|
|
|
790
794
|
path("/shop/product/:slug", ProductPage, { name: "product" }, () => [
|
|
@@ -67,8 +67,10 @@ For shared segment data, use named revalidation contracts on both the producer
|
|
|
67
67
|
and consumer segments, even when middleware is present in the chain.
|
|
68
68
|
|
|
69
69
|
```typescript
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
import * as CartActions from "./actions/cart";
|
|
71
|
+
|
|
72
|
+
export const revalidateCartData = (ctx) =>
|
|
73
|
+
ctx.isAction(CartActions) || undefined;
|
|
72
74
|
|
|
73
75
|
layout(CartLayout, () => [
|
|
74
76
|
middleware(cartRenderMiddleware),
|
|
@@ -317,9 +317,11 @@ NOT cache invalidation — it is `revalidate()`, controlling which segments
|
|
|
317
317
|
re-rendering:
|
|
318
318
|
|
|
319
319
|
```typescript
|
|
320
|
+
import { updateBlog } from "./actions/blog";
|
|
321
|
+
|
|
320
322
|
// Re-run this layout when a blog action fires
|
|
321
323
|
layout(BlogLayout, () => [
|
|
322
|
-
revalidate((
|
|
324
|
+
revalidate((ctx) => ctx.isAction(updateBlog) || undefined),
|
|
323
325
|
path("/blog/:slug", BlogPost, { name: "blogPost" }),
|
|
324
326
|
]);
|
|
325
327
|
|
package/skills/parallel/SKILL.md
CHANGED
|
@@ -330,6 +330,8 @@ parallel({
|
|
|
330
330
|
Control when parallel routes revalidate:
|
|
331
331
|
|
|
332
332
|
```typescript
|
|
333
|
+
import * as CartActions from "./actions/cart";
|
|
334
|
+
|
|
333
335
|
parallel(
|
|
334
336
|
{
|
|
335
337
|
"@cart": () => <CartSummary />,
|
|
@@ -337,7 +339,7 @@ parallel(
|
|
|
337
339
|
() => [
|
|
338
340
|
loader(CartLoader),
|
|
339
341
|
// Revalidate when cart actions occur
|
|
340
|
-
revalidate((
|
|
342
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
341
343
|
]
|
|
342
344
|
)
|
|
343
345
|
```
|
|
@@ -360,8 +362,10 @@ the parallel consumer:
|
|
|
360
362
|
|
|
361
363
|
```typescript
|
|
362
364
|
// revalidation-contracts.ts
|
|
363
|
-
|
|
364
|
-
|
|
365
|
+
import * as CartActions from "./actions/cart";
|
|
366
|
+
|
|
367
|
+
export const revalidateCartData = (ctx) =>
|
|
368
|
+
ctx.isAction(CartActions) || undefined;
|
|
365
369
|
|
|
366
370
|
layout(CartLayout, () => [
|
|
367
371
|
revalidate(revalidateCartData), // producer reruns
|
|
@@ -429,6 +433,7 @@ function MyLayout() {
|
|
|
429
433
|
```typescript
|
|
430
434
|
import { urls } from "@rangojs/router";
|
|
431
435
|
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
436
|
+
import * as CartActions from "./actions/cart";
|
|
432
437
|
|
|
433
438
|
function ShopLayout() {
|
|
434
439
|
return (
|
|
@@ -479,7 +484,7 @@ export const shopPatterns = urls(({
|
|
|
479
484
|
() => [
|
|
480
485
|
loader(CartLoader),
|
|
481
486
|
loading(<CartSkeleton />),
|
|
482
|
-
revalidate((
|
|
487
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
483
488
|
]
|
|
484
489
|
),
|
|
485
490
|
|
package/skills/rango/SKILL.md
CHANGED
|
@@ -154,6 +154,12 @@ returned, for outcome-conditional revalidation. The arg also exposes `actionId`
|
|
|
154
154
|
(raw `path#export`), `actionUrl`, `formData`, `method`, and `stale` (cross-tab
|
|
155
155
|
`_rsc_stale` signal). All are `undefined` on plain navigation (no action).
|
|
156
156
|
|
|
157
|
+
Two idioms, picked by what an _unrelated_ action should do. `ctx.isAction()`
|
|
158
|
+
returns a raw boolean, so combine it with `|| undefined` to **defer** ("mine,
|
|
159
|
+
else let the default decide": `ctx.isAction(CartActions) || undefined`) or leave
|
|
160
|
+
it bare to **suppress** ("mine only": `ctx.isAction(CartActions)`). Prefer the
|
|
161
|
+
defer form unless a sibling segment must own the unrelated-action decision.
|
|
162
|
+
|
|
157
163
|
```ts
|
|
158
164
|
// re-render only when checkout actually succeeded; defer otherwise
|
|
159
165
|
revalidate((ctx) => (ctx.isAction(checkout) && ctx.actionResult?.ok) || undefined),
|
|
@@ -232,6 +238,12 @@ Grouped by concern — read when you need to…
|
|
|
232
238
|
| `/bundle-analysis` | Audit your app's production bundle for server leaks and oversized chunks |
|
|
233
239
|
| `/debug-manifest` | Inspect route manifest structure |
|
|
234
240
|
|
|
241
|
+
**Testing**:
|
|
242
|
+
|
|
243
|
+
| Skill | Description |
|
|
244
|
+
| ---------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
245
|
+
| `/testing` | Unit (loaders/middleware/reverse/components), integration (dispatch/Flight), and e2e (dev+prod parity, progressive enhancement) |
|
|
246
|
+
|
|
235
247
|
**Setup, types & migration**:
|
|
236
248
|
|
|
237
249
|
| Skill | Description |
|
package/skills/route/SKILL.md
CHANGED
|
@@ -246,10 +246,12 @@ the producer route and the consumer child segments.
|
|
|
246
246
|
|
|
247
247
|
```typescript
|
|
248
248
|
// revalidation-contracts.ts
|
|
249
|
+
import * as CheckoutActions from "./actions/checkout";
|
|
250
|
+
|
|
249
251
|
// Defer (|| undefined), not ?? false: a hard `false` short-circuits the chain,
|
|
250
252
|
// so when the same segment composes multiple contracts the later ones never run.
|
|
251
|
-
export const revalidateCheckoutData = (
|
|
252
|
-
|
|
253
|
+
export const revalidateCheckoutData = (ctx) =>
|
|
254
|
+
ctx.isAction(CheckoutActions) || undefined;
|
|
253
255
|
|
|
254
256
|
path("/checkout", CheckoutPage, { name: "checkout" }, () => [
|
|
255
257
|
revalidate(revalidateCheckoutData), // producer (route handler) reruns
|
|
@@ -294,6 +296,12 @@ path("/old-page", () => redirect("/new-page"), { name: "oldPage" });
|
|
|
294
296
|
path("/moved", () => redirect("/new-location", 301), { name: "moved" });
|
|
295
297
|
```
|
|
296
298
|
|
|
299
|
+
> **Redirecting from a route with `loading()`:** an `async` handler that returns
|
|
300
|
+
> a `Response`/`redirect()` on a route that also declares `loading()` is streamed,
|
|
301
|
+
> so the redirect is rendered into the RSC stream instead of becoming an HTTP
|
|
302
|
+
> redirect. Issue the redirect from `middleware`, a loader, or a **synchronous**
|
|
303
|
+
> handler return instead. (Dev logs a warning if this is hit.)
|
|
304
|
+
|
|
297
305
|
### Redirect with location state
|
|
298
306
|
|
|
299
307
|
Carry typed state through redirects (e.g. flash messages):
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing
|
|
3
|
+
description: Test @rangojs/router apps — unit (loaders/middleware/reverse/components), integration (dispatch/Flight), and e2e (dev+prod parity, progressive enhancement)
|
|
4
|
+
argument-hint: [layer]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Testing @rangojs/router apps
|
|
8
|
+
|
|
9
|
+
Rango ships six consumer-facing testing entries, one per test runtime/dependency:
|
|
10
|
+
`@rangojs/router/testing` (unit + integration, under a Vite-driven Vitest
|
|
11
|
+
project), `@rangojs/router/testing/vitest` (the `rangoTestConfig`/`rangoTestAliases`
|
|
12
|
+
setup preset), `@rangojs/router/testing/dom` (`renderRoute`, needs RTL + a DOM
|
|
13
|
+
env), `@rangojs/router/testing/e2e` (the Playwright harness),
|
|
14
|
+
`@rangojs/router/testing/flight` (real Flight, react-server condition only), and
|
|
15
|
+
`@rangojs/router/testing/flight-matchers` (the Flight matchers).
|
|
16
|
+
|
|
17
|
+
The hard problem in an RSC app is that the layer you reach for is dictated by
|
|
18
|
+
**what the behavior touches** — a pure predicate is a one-line vitest test; a real
|
|
19
|
+
async Server Component cannot be a plain node test at all. Pick the layer
|
|
20
|
+
**first**, then the primitive. Reaching one layer too high (e2e for a reverse
|
|
21
|
+
function) is slow; one too low (a node test for Flight) fails to compile or
|
|
22
|
+
silently asserts nothing.
|
|
23
|
+
|
|
24
|
+
This page is the router. Each primitive's full API (options, the seeded context
|
|
25
|
+
your code receives, the return shape), a minimal recipe, and its caveats live in a
|
|
26
|
+
dedicated sub-file linked from the decision tree below. Read the one for your case.
|
|
27
|
+
|
|
28
|
+
> **Setup is the first wall.** The vitest projects, the `rangoTestConfig` vs
|
|
29
|
+
> `rangoTestAliases` choice (Node >= 23), and the react-server `@rangojs/router ->
|
|
30
|
+
index.rsc.ts` alias are all in [`./setup.md`](./setup.md). Read it before writing
|
|
31
|
+
> `vitest.config.ts`. Platform bindings (`env.DB`/DO/R2) are your own double —
|
|
32
|
+
> [`./bindings.md`](./bindings.md).
|
|
33
|
+
|
|
34
|
+
For the long-form prose guide (setup walkthrough + migration), see
|
|
35
|
+
[`docs/testing.md`](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/testing.md)
|
|
36
|
+
(the `docs/` directory is not shipped in the published package, so this is an
|
|
37
|
+
absolute link).
|
|
38
|
+
|
|
39
|
+
## When to use
|
|
40
|
+
|
|
41
|
+
Use this skill when adding or changing tests for a Rango app: a loader,
|
|
42
|
+
middleware, a server action, a route map, a client component, a response route,
|
|
43
|
+
cache/SWR behavior, prerender, or a navigation/PE flow.
|
|
44
|
+
|
|
45
|
+
Two non-negotiable mandates (from the repo's `CLAUDE.md`, and they apply to
|
|
46
|
+
consumer apps too):
|
|
47
|
+
|
|
48
|
+
- **Every e2e covers BOTH dev and production.** A dev-only e2e is not acceptable.
|
|
49
|
+
Use `parityDescribe` — it generates the dev and production describes from one
|
|
50
|
+
body, so you cannot forget the prod half. See [`./e2e-parity.md`](./e2e-parity.md).
|
|
51
|
+
- **Progressive-enhancement parity** is a first-class assertion. A form-driven
|
|
52
|
+
flow must produce the same observable result with JS on and JS off. Use
|
|
53
|
+
`expectParity`.
|
|
54
|
+
|
|
55
|
+
## The read-first shape
|
|
56
|
+
|
|
57
|
+
Four import roots, each matched to the dependency/runtime that can load it — this
|
|
58
|
+
split is forced by hard walls, not preference:
|
|
59
|
+
|
|
60
|
+
- `@rangojs/router/testing` — unit + integration primitives. Run these under a
|
|
61
|
+
**Vite-driven Vitest** project with the rango Vite plugin active (the router
|
|
62
|
+
internals import the `@rangojs/router:version` virtual; without the plugin, the
|
|
63
|
+
preset stubs it). References neither React, RTL, Playwright, nor the RSC runtime.
|
|
64
|
+
- `@rangojs/router/testing/dom` — `renderRoute` (the RTL component stub). Kept
|
|
65
|
+
separate so the unit barrel stays free of React/RTL; it lazy-loads
|
|
66
|
+
`@testing-library/react` and needs a DOM env (happy-dom/jsdom).
|
|
67
|
+
- `@rangojs/router/testing/e2e` — the Playwright harness. Kept separate so it
|
|
68
|
+
loads in a plain (non-Vite) Playwright runner; the helpers take your
|
|
69
|
+
`test`/`expect`, so this entry never imports `@playwright/test` at runtime.
|
|
70
|
+
- `@rangojs/router/testing/flight` — real Flight rendering. Its serializer loads
|
|
71
|
+
only under the `react-server` node condition; pulling it elsewhere throws.
|
|
72
|
+
|
|
73
|
+
The single rule that drives everything:
|
|
74
|
+
|
|
75
|
+
> **If the behavior needs a real Flight render, it cannot be a plain vitest node
|
|
76
|
+
> test.** It is either `renderToFlightString`/`renderServerTree`/`renderHandler`
|
|
77
|
+
> (under the react-server vitest project) or an e2e test. There is no middle
|
|
78
|
+
> ground in node.
|
|
79
|
+
|
|
80
|
+
## Decision tree: behavior -> layer -> primitive
|
|
81
|
+
|
|
82
|
+
Each primitive links to its sub-file (API + recipe + caveats).
|
|
83
|
+
|
|
84
|
+
| The behavior is… | Layer | Primitive | Import root |
|
|
85
|
+
| --------------------------------------------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------ | -------------------------------- |
|
|
86
|
+
| a pure function / `reverse` / `href` / a predicate (`revalidate`, `isAction`) | unit + types | [`reverse`/`@ts-expect-error`](./reverse-and-types.md) | `@rangojs/router/testing` |
|
|
87
|
+
| one loader's data logic | unit (node) | [`runLoader`](./loader.md) | `@rangojs/router/testing` |
|
|
88
|
+
| a loader's cookie / header / redirect output (auth-loader pattern) | unit (node) | [`runLoaderResult`](./loader.md) | `@rangojs/router/testing` |
|
|
89
|
+
| one middleware's ordering / short-circuit / cookie+header merge | unit (node) | [`runMiddleware`](./middleware.md) | `@rangojs/router/testing` |
|
|
90
|
+
| a `"use server"` action's cookie / header / flash output (even on `throw redirect()`) | unit (node) | [`runInRequestContext`](./server-actions.md) | `@rangojs/router/testing` |
|
|
91
|
+
| a handle's `collect`/accumulator, or a seeded handle read | unit | [`collectHandle` / seeded `handles`](./handles.md) | `@rangojs/router/testing[/dom]` |
|
|
92
|
+
| a CLIENT component reading router context (`useParams`/`useReverse`/`Outlet`/`useNavigation`/`useLoader`) | unit (DOM) | [`renderRoute`](./client-components.md) | `@rangojs/router/testing/dom` |
|
|
93
|
+
| a redirect / status / headers / cookies / **response route** (json/text/html/xml/md), no Flight | integration | [`dispatch`](./response-routes.md) | `@rangojs/router/testing` |
|
|
94
|
+
| a real async **Server Component** / Flight serialization shape | RSC unit | [`renderToFlightString` + `toMatchFlight`](./flight.md) | `@rangojs/router/testing/flight` |
|
|
95
|
+
| a client island's **typed props** / the **server-rendered** host content | RSC unit | [`renderServerTree` + `findClientBoundaries`/`findElements`](./server-tree.md) | `@rangojs/router/testing/flight` |
|
|
96
|
+
| a real route **handler** `(ctx) => rsc` (params/loaders/vars -> rendered RSC + effects) | RSC unit | [`renderHandler`](./render-handler.md) | `@rangojs/router/testing/flight` |
|
|
97
|
+
| navigation, hydration, PE parity, view transitions, real SSR | e2e | [`createRangoE2E` -> `parityDescribe`/`expectParity`](./e2e-parity.md) | `@rangojs/router/testing/e2e` |
|
|
98
|
+
| cache hit/miss/stale, prerender (= a cache hit by design) | e2e + signal | [`assertCacheStatus` / telemetry sink](./cache-prerender.md) | `@rangojs/router/testing[/e2e]` |
|
|
99
|
+
| generated route map drift vs runtime | unit (node) | [`assertGeneratedRoutesMatch`](./reverse-and-types.md) | `@rangojs/router/testing` |
|
|
100
|
+
| a platform binding (`env.DB` / Durable Object / `env.R2`) | unit/integr. | [your own double via `env`](./bindings.md) | (any primitive's `env` option) |
|
|
101
|
+
|
|
102
|
+
Cross-references to the DSL skills: `/loader`, `/middleware`, `/server-actions`,
|
|
103
|
+
`/handler-use`, `/hooks`, `/response-routes`, `/route`, `/caching`, `/prerender`,
|
|
104
|
+
`/typesafety`.
|
|
105
|
+
|
|
106
|
+
## Sub-files
|
|
107
|
+
|
|
108
|
+
- Cross-cutting: [`setup.md`](./setup.md), [`bindings.md`](./bindings.md)
|
|
109
|
+
- Unit (node): [`loader.md`](./loader.md), [`middleware.md`](./middleware.md),
|
|
110
|
+
[`server-actions.md`](./server-actions.md), [`handles.md`](./handles.md),
|
|
111
|
+
[`reverse-and-types.md`](./reverse-and-types.md)
|
|
112
|
+
- Unit (DOM): [`client-components.md`](./client-components.md)
|
|
113
|
+
- RSC unit: [`flight.md`](./flight.md), [`server-tree.md`](./server-tree.md),
|
|
114
|
+
[`render-handler.md`](./render-handler.md)
|
|
115
|
+
- Integration: [`response-routes.md`](./response-routes.md)
|
|
116
|
+
- E2E: [`e2e-parity.md`](./e2e-parity.md), [`cache-prerender.md`](./cache-prerender.md)
|
|
117
|
+
|
|
118
|
+
## Pre-push checklist (mirror CLAUDE.md)
|
|
119
|
+
|
|
120
|
+
Before pushing, run all of these and fix any failure:
|
|
121
|
+
|
|
122
|
+
1. `pnpm run typecheck` (or `pnpm exec tsc --noEmit`)
|
|
123
|
+
2. `pnpm run test:unit` (node + DOM vitest)
|
|
124
|
+
3. `pnpm run test:unit:rsc` (the react-server Flight project)
|
|
125
|
+
4. `pnpm run lint`
|
|
126
|
+
5. `pnpm run format`
|
|
127
|
+
|
|
128
|
+
And: **every e2e has a production counterpart.** `parityDescribe` makes this
|
|
129
|
+
automatic — if you wrote a plain `test.describe` for a behavior, convert it.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Testing platform bindings — your double is the seam
|
|
2
|
+
|
|
3
|
+
**Layer:** cross-cutting (unit/integration) · **Seam:** the `env` option every primitive takes
|
|
4
|
+
|
|
5
|
+
The node primitives test the router's seams; the moment your loader/middleware/action calls a **platform binding** (`env.DB`, a Durable Object stub, `env.R2`), you have crossed out of rango and into your app's I/O. The router machinery is real — what you seed is the binding double behind it, injected through `env`.
|
|
6
|
+
|
|
7
|
+
## Where it plugs in
|
|
8
|
+
|
|
9
|
+
rango ships **no doubles** for platform bindings — they are app- and schema-specific. You build the double and inject it through the `env` option that every primitive already accepts:
|
|
10
|
+
|
|
11
|
+
- `runLoader(body, { env })`
|
|
12
|
+
- `runMiddleware(fn, { request, env })`
|
|
13
|
+
- `runInRequestContext(fn, { request, env })`
|
|
14
|
+
- `renderHandler(handler, { request, env })`
|
|
15
|
+
- `dispatch(router, { request, env })`
|
|
16
|
+
- `renderToFlightString(el, { env })`
|
|
17
|
+
|
|
18
|
+
Inside the run, `getRequestContext().env` (and anything that reads it — `cache()`, your loaders, your middleware) sees the object you passed.
|
|
19
|
+
|
|
20
|
+
## Driver contract
|
|
21
|
+
|
|
22
|
+
The work here is matching the binding's **driver contract**, not its public API. A double that satisfies the public surface but not the driver's wire shape mounts green and proves nothing.
|
|
23
|
+
|
|
24
|
+
- **Per-method shapes.** `drizzle-orm/d1` serves SELECTs through `.raw()` and writes (INSERT/UPDATE/DELETE) through `.run()`. The two return different shapes and hit different code paths in the decoder. Model **both**.
|
|
25
|
+
- **`.raw()` (reads).** Must serve **positional row arrays in schema-column order**, with the driver-level encodings so the decoder round-trips `Date`/JSON. NOT `{ column: value }` objects.
|
|
26
|
+
- **`.run()` (writes).** Returns `{ success, meta }` — no rows — and bypasses the row responder entirely.
|
|
27
|
+
|
|
28
|
+
## Recipe
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { describe, it, expect } from "vitest";
|
|
32
|
+
import {
|
|
33
|
+
runLoader,
|
|
34
|
+
runMiddleware,
|
|
35
|
+
runInRequestContext,
|
|
36
|
+
} from "@rangojs/router/testing";
|
|
37
|
+
import { bundleLoaderBody } from "../app/loaders";
|
|
38
|
+
import { requireMembership } from "../app/middleware";
|
|
39
|
+
import { authorizeAction } from "../app/actions";
|
|
40
|
+
|
|
41
|
+
// A D1Database double satisfying drizzle-orm/d1's driver contract.
|
|
42
|
+
const fakeD1 = makeFakeD1({
|
|
43
|
+
// .raw() serves positional rows in schema-column order, driver-encoded.
|
|
44
|
+
raw: () => [[1, "acme", "2026-01-01T00:00:00.000Z"]],
|
|
45
|
+
// .run() returns { success, meta }, no rows.
|
|
46
|
+
run: () => ({ success: true, meta: { changes: 1 } }),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("bindings seam", () => {
|
|
50
|
+
it("loader reads through env.DB", async () => {
|
|
51
|
+
const result = await runLoader(bundleLoaderBody, { env: { DB: fakeD1 } });
|
|
52
|
+
expect(result).toMatchObject({ slug: "acme" });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("middleware reads through env.DB", async () => {
|
|
56
|
+
const { nextCalled, response } = await runMiddleware(requireMembership, {
|
|
57
|
+
request: "/t/acme/edit",
|
|
58
|
+
env: { DB: fakeD1 },
|
|
59
|
+
});
|
|
60
|
+
expect(nextCalled).toBe(1); // membership passed, chain continued
|
|
61
|
+
expect(response.status).toBe(200);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("action reads through env.DB", async () => {
|
|
65
|
+
const { result } = await runInRequestContext(
|
|
66
|
+
() => authorizeAction({ id: 1 }),
|
|
67
|
+
{
|
|
68
|
+
env: { DB: fakeD1 },
|
|
69
|
+
request: "/t/acme/edit",
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
expect(result).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Caveats
|
|
78
|
+
|
|
79
|
+
- rango ships **no doubles** for platform bindings (`env.DB`, Durable Objects, `env.R2`) by design — they are app- and schema-specific. Inject your own double through the `env` option every primitive takes.
|
|
80
|
+
- This is usually the **single biggest effort** in a consumer unit suite, and the work is matching the **driver contract**, not the binding's public API.
|
|
81
|
+
- `drizzle-orm/d1`: a `D1Database` double must serve **positional row arrays in schema-column order** for drizzle's `.raw()` path (with driver-level encodings so the decoder round-trips `Date`/JSON), NOT `{ column: value }` objects — an object-shaped double returns silently-wrong or empty rows.
|
|
82
|
+
- The contract is **per-method**: SELECTs go through `.raw()` (positional rows); writes (INSERT/UPDATE/DELETE) go through `.run()`, which returns `{ success, meta }` (no rows) and bypasses the row responder entirely. Model **both** paths — a read-only `.raw()` double silently no-ops every write.
|
|
83
|
+
- Keep the double at the **binding boundary**; never mock a rango primitive to dodge building it.
|
|
84
|
+
|
|
85
|
+
## See also
|
|
86
|
+
|
|
87
|
+
- (cross-cutting)
|
|
88
|
+
- Siblings: `./loader.md`, `./middleware.md`, `./server-actions.md`
|
|
89
|
+
- Long-form prose: [docs/testing.md](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/testing.md) — section "What these primitives deliberately don't cover (the platform-bindings paragraph)"
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Testing cache / SWR / prerender — assertCacheStatus
|
|
2
|
+
|
|
3
|
+
**Layer:** e2e + signal · **Import:** the cache-status helpers (`assertCacheStatus`/`parseCacheHeader`/`createCacheSink`/`filterCacheDecisions`) are re-exported from BOTH entries — use `@rangojs/router/testing` from a Vitest unit/integration test, and `@rangojs/router/testing/e2e` from a plain Playwright runner (the e2e barrel avoids the Vite-only virtuals the main barrel pulls in). · **DSL it tests:** `cache()` / `"use cache"` / loader cache / `Prerender(...)` (see `/caching`, `/prerender`, `/use-cache`)
|
|
4
|
+
|
|
5
|
+
The router's REAL cache pipeline runs (runtime cache, SWR revalidation, prerender lookup); you SEED nothing — you drive a request through the real fetch path and read the resulting cache decision. The decision surfaces two ways: the `X-Rango-Cache` response header (a debug gate) or a captured `cache.decision` telemetry event.
|
|
6
|
+
|
|
7
|
+
## API
|
|
8
|
+
|
|
9
|
+
### Options — `assertCacheStatus(target, segment, expected)`
|
|
10
|
+
|
|
11
|
+
| Field | Type | Meaning |
|
|
12
|
+
| ---------- | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| `target` | `Response \| { headers: Headers }` (`CacheStatusTarget`) | The thing carrying the `X-Rango-Cache` header: a `Response` from `router.fetch(...)`, or any `{ headers: Headers }`. A Playwright `APIResponse` exposes headers as a method, so wrap it: `{ headers: new Headers(res.headers()) }`. |
|
|
14
|
+
| `segment` | `string` | The route NAME (e.g. `product.detail`), the same id the header carries — NOT the URL pattern (`/products/:id`). |
|
|
15
|
+
| `expected` | `"hit" \| "miss" \| "stale" \| "prerendered" \| "passthrough"` (`ExpectedCacheStatus` = `CacheSegmentStatus`) | The cache status you assert for that route. |
|
|
16
|
+
|
|
17
|
+
### Context — what your code under test emits
|
|
18
|
+
|
|
19
|
+
The header / event is produced by the router's RSC render pipeline from `ctx.routeKey`. Your code does not call these helpers — it just runs under a router with the gate (or telemetry sink) wired. The helpers READ the emitted signal.
|
|
20
|
+
|
|
21
|
+
| Field | Type | Meaning |
|
|
22
|
+
| ------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------ |
|
|
23
|
+
| `CacheSegmentSignal.id` | `string` | Segment id. v1: the route key, since status is route-level. |
|
|
24
|
+
| `CacheSegmentSignal.type` | `string` | Segment type. v1: `"route"` for the coarse route-level entry. |
|
|
25
|
+
| `CacheSegmentSignal.cacheStatus` | `CacheSegmentStatus` | Resolved status (`hit`/`miss`/`stale`/`prerendered`/`passthrough`). |
|
|
26
|
+
| `CacheSegmentSignal.shouldRevalidate` | `boolean?` | Whether stale-while-revalidate was triggered for this segment. |
|
|
27
|
+
| `CacheDecisionEvent.segments` | `CacheSegmentSignal[]?` | The coarse route-level signal array (present only when telemetry or the debug gate is on). |
|
|
28
|
+
|
|
29
|
+
### Returns
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// assertCacheStatus throws on mismatch / missing header / unknown segment; returns void.
|
|
33
|
+
assertCacheStatus(target, segment, expected): void
|
|
34
|
+
|
|
35
|
+
// parseCacheHeader -> the raw { routeKey: status } map. "a=hit, b=stale" -> { a: "hit", b: "stale" }.
|
|
36
|
+
parseCacheHeader(headerValue: string | null | undefined): Record<string, string>
|
|
37
|
+
|
|
38
|
+
// createCacheSink -> a sink to wire via createRouter({ telemetry: sink }), plus the array it records into.
|
|
39
|
+
createCacheSink(): { sink: TelemetrySink; events: TelemetryEvent[] }
|
|
40
|
+
|
|
41
|
+
// filterCacheDecisions -> narrow captured events to cache.decision events.
|
|
42
|
+
filterCacheDecisions(events: readonly TelemetryEvent[]): CacheDecisionEvent[]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Recipe
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
// In a Playwright e2e, import the cache-status helpers from the e2e entry —
|
|
49
|
+
// the @rangojs/router/testing barrel pulls a build-only virtual that does not
|
|
50
|
+
// resolve in a plain Playwright runner.
|
|
51
|
+
import { assertCacheStatus } from "@rangojs/router/testing/e2e";
|
|
52
|
+
|
|
53
|
+
parityDescribe("product page caches", (f) => {
|
|
54
|
+
test("second request is a hit", async ({ page }) => {
|
|
55
|
+
// The key is the route NAME (the X-Rango-Cache id), NOT the URL pattern.
|
|
56
|
+
// Playwright APIResponse.headers() is a method returning a plain record, so
|
|
57
|
+
// wrap it in a Headers to match CacheStatusTarget (`{ headers: Headers }`).
|
|
58
|
+
const first = await page.request.get(f.url("/products/1"));
|
|
59
|
+
assertCacheStatus(
|
|
60
|
+
{ headers: new Headers(first.headers()) },
|
|
61
|
+
"product.detail",
|
|
62
|
+
"miss",
|
|
63
|
+
);
|
|
64
|
+
const second = await page.request.get(f.url("/products/1"));
|
|
65
|
+
assertCacheStatus(
|
|
66
|
+
{ headers: new Headers(second.headers()) },
|
|
67
|
+
"product.detail",
|
|
68
|
+
"hit",
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Zero-prod-surface alternative — the telemetry sink. No header at all; you inspect captured `cache.decision` events:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { createCacheSink, filterCacheDecisions } from "@rangojs/router/testing";
|
|
78
|
+
|
|
79
|
+
const { sink, events } = createCacheSink();
|
|
80
|
+
const router = createRouter({ telemetry: sink }).routes(urlpatterns);
|
|
81
|
+
// ...drive a request through the router's RSC fetch path...
|
|
82
|
+
const decision = filterCacheDecisions(events)[0];
|
|
83
|
+
expect(decision.segments?.[0].cacheStatus).toBe("stale");
|
|
84
|
+
expect(decision.segments?.[0].shouldRevalidate).toBe(true);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Caveats
|
|
88
|
+
|
|
89
|
+
- The `X-Rango-Cache` header is emitted ONLY when the gate is on: `createRouter({ debugCacheSignal: true })` or `process.env.RANGO_TEST_SIGNALS === "1"`. Off by default — zero production surface. With the gate off, `assertCacheStatus` throws a clear "header missing" error.
|
|
90
|
+
- v1 is COARSE: route-level, keyed by the route NAME (e.g. `product.detail`), NOT the URL pattern (`/products/:id`); not per-individual-segment. The signal is built from `ctx.routeKey`, so a pattern-shaped key never matches. (`parseCacheHeader` exposes the raw `{ routeKey: status }` map if you need it.)
|
|
91
|
+
- Prerender is indistinguishable from a cache hit by design — no static `.html`/`.rsc` files, the worker handles every request and looks up a stored Flight payload; the browser cannot tell. Do not assert "prerendered" from the DOM. Assert via the signal (`assertCacheStatus(res, seg, "prerendered")`) and run prerender assertions in PRODUCTION mode (the build-time artifacts only exist after `pnpm build`).
|
|
92
|
+
- In a Playwright e2e import the cache-status helpers from the `/e2e` entry — the `@rangojs/router/testing` barrel is Vitest-only (it pulls a build-only virtual that does not resolve in a plain Playwright runner). Zero-prod-surface alternative: the telemetry sink (`createCacheSink`/`filterCacheDecisions`), no header at all. Note: the non-RSC `dispatch()` primitive never emits this header — get the Response from the router's real RSC fetch path.
|
|
93
|
+
|
|
94
|
+
## See also
|
|
95
|
+
|
|
96
|
+
- `/caching`, `/prerender`, `/use-cache` — the DSL this tests
|
|
97
|
+
- Siblings: [`./e2e-parity.md`](./e2e-parity.md), [`./response-routes.md`](./response-routes.md)
|
|
98
|
+
- Long-form prose: [docs/testing.md](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/testing.md) — section "Cache, SWR, and prerender"
|