@rangojs/router 0.16.0 → 0.17.0
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 +254 -89
- package/dist/testing/vitest.js +252 -0
- package/dist/types/browser/navigation-transaction.d.ts +6 -0
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
- package/dist/types/browser/rsc-router.d.ts +1 -1
- package/dist/types/browser/scroll-restoration.d.ts +7 -0
- package/dist/types/cache/cache-exec-scope.d.ts +18 -1
- package/dist/types/cache/cache-scope.d.ts +34 -5
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +53 -13
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +15 -3
- package/dist/types/client.d.ts +4 -4
- package/dist/types/client.rsc.d.ts +4 -5
- package/dist/types/components/DefaultDocument.d.ts +5 -2
- package/dist/types/context-var.d.ts +6 -5
- package/dist/types/escape-script.d.ts +1 -1
- package/dist/types/handles/MetaTags.d.ts +5 -4
- package/dist/types/handles/Scripts.d.ts +8 -7
- package/dist/types/handles/meta.d.ts +2 -2
- package/dist/types/handles/script.d.ts +17 -16
- package/dist/types/href-client.d.ts +1 -1
- package/dist/types/html.d.ts +12 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +3 -2
- package/dist/types/loader-store.d.ts +7 -4
- package/dist/types/reverse.d.ts +36 -4
- package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
- package/dist/types/route-definition/helpers-types.d.ts +33 -9
- package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
- package/dist/types/route-types.d.ts +1 -1
- package/dist/types/router/debug-manifest.d.ts +3 -2
- package/dist/types/router/error-handling.d.ts +20 -0
- package/dist/types/router/handler-context.d.ts +6 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +3 -2
- package/dist/types/router/router-context.d.ts +3 -1
- package/dist/types/router/router-interfaces.d.ts +24 -2
- package/dist/types/router/router-options.d.ts +8 -5
- package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +37 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
- package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
- package/dist/types/router/segment-wrappers.d.ts +1 -1
- package/dist/types/rsc/response-route-handler.d.ts +1 -1
- package/dist/types/rsc/rsc-rendering.d.ts +1 -2
- package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
- package/dist/types/rsc/shell-capture.d.ts +5 -4
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +10 -0
- package/dist/types/server/context.d.ts +33 -3
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +24 -21
- package/dist/types/ssr/index.d.ts +15 -0
- package/dist/types/static-handler.d.ts +4 -1
- package/dist/types/testing/generated-routes.d.ts +2 -2
- package/dist/types/testing/internal/context.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +32 -6
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
- package/dist/types/testing/vitest.d.ts +35 -1
- package/dist/types/theme/ThemeScript.d.ts +3 -3
- package/dist/types/types/cache-types.d.ts +14 -10
- package/dist/types/types/handler-context.d.ts +14 -0
- package/dist/types/types/loader-types.d.ts +8 -6
- package/dist/types/urls/response-types.d.ts +3 -12
- package/dist/types/use-loader.d.ts +12 -3
- package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
- package/dist/types/vite/rango.d.ts +1 -1
- package/dist/vite/index.js +1381 -1346
- package/package.json +2 -2
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +65 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +207 -97
- package/skills/caching/SKILL.md +295 -93
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +53 -31
- package/skills/cloudflare/SKILL.md +72 -20
- package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
- package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
- package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
- package/skills/comparison/SKILL.md +5 -0
- package/skills/comparison/references/framework-comparison.md +39 -33
- package/skills/composability/SKILL.md +38 -29
- package/skills/css/SKILL.md +10 -13
- package/skills/debug-manifest/SKILL.md +74 -31
- package/skills/defer-hydration/SKILL.md +2 -1
- package/skills/deployment-caching/SKILL.md +6 -1
- package/skills/document-cache/SKILL.md +92 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +53 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +23 -3
- package/skills/hooks/handle-and-actions.md +31 -16
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +37 -15
- package/skills/hooks/urls.md +7 -5
- package/skills/host-router/SKILL.md +64 -12
- package/skills/i18n/SKILL.md +60 -34
- package/skills/intercept/SKILL.md +161 -26
- package/skills/layout/SKILL.md +106 -50
- package/skills/links/SKILL.md +51 -19
- package/skills/loader/SKILL.md +242 -106
- package/skills/middleware/SKILL.md +119 -23
- package/skills/migrate-nextjs/SKILL.md +100 -45
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +49 -38
- package/skills/migrate-react-router/component-migration.md +25 -6
- package/skills/migrate-react-router/data-and-actions.md +18 -5
- package/skills/migrate-react-router/route-mapping.md +12 -6
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +28 -14
- package/skills/parallel/SKILL.md +75 -39
- package/skills/ppr/SKILL.md +345 -150
- package/skills/prerender/SKILL.md +189 -100
- package/skills/rango/SKILL.md +65 -34
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +165 -60
- package/skills/router-setup/SKILL.md +418 -240
- package/skills/scripts/SKILL.md +31 -20
- package/skills/server-actions/SKILL.md +220 -59
- package/skills/shell-manifest/SKILL.md +12 -7
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +68 -32
- package/skills/testing/cache-prerender.md +55 -29
- package/skills/testing/client-components.md +59 -6
- package/skills/testing/e2e-parity.md +12 -2
- package/skills/testing/flight.md +19 -11
- package/skills/testing/handles.md +8 -5
- package/skills/testing/loader.md +8 -5
- package/skills/testing/middleware.md +8 -3
- package/skills/testing/predicates.md +120 -0
- package/skills/testing/render-handler.md +70 -22
- package/skills/testing/response-routes.md +11 -4
- package/skills/testing/reverse-and-types.md +20 -14
- package/skills/testing/server-actions.md +12 -3
- package/skills/testing/server-tree.md +9 -0
- package/skills/testing/setup.md +9 -5
- package/skills/theme/SKILL.md +106 -29
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +126 -47
- package/skills/typesafety/generated-files-and-cli.md +26 -14
- package/skills/typesafety/params-and-search.md +44 -19
- package/skills/typesafety/route-types.md +50 -14
- package/skills/use-cache/SKILL.md +200 -82
- package/skills/vercel/SKILL.md +21 -11
- package/skills/view-transitions/SKILL.md +41 -20
- package/src/__augment-tests__/augmented.check.ts +26 -1
- package/src/browser/index.ts +2 -2
- package/src/browser/navigation-bridge.ts +3 -4
- package/src/browser/navigation-transaction.ts +22 -2
- package/src/browser/partial-update.ts +35 -25
- package/src/browser/react/ScrollRestoration.tsx +6 -6
- package/src/browser/rsc-router.tsx +1 -1
- package/src/browser/scroll-restoration.ts +53 -6
- package/src/cache/cache-exec-scope.ts +37 -4
- package/src/cache/cache-key-utils.ts +1 -6
- package/src/cache/cache-runtime.ts +230 -61
- package/src/cache/cache-scope.ts +76 -12
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +132 -28
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/tag-invalidation.ts +4 -3
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +16 -3
- package/src/client.rsc.tsx +10 -9
- package/src/client.tsx +5 -4
- package/src/components/DefaultDocument.tsx +7 -2
- package/src/context-var.ts +6 -5
- package/src/escape-script.ts +1 -1
- package/src/handles/MetaTags.tsx +13 -11
- package/src/handles/Scripts.tsx +11 -10
- package/src/handles/meta.ts +2 -2
- package/src/handles/script.ts +17 -16
- package/src/href-client.ts +1 -1
- package/src/html.ts +12 -0
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +1 -0
- package/src/loader-store.ts +7 -4
- package/src/prerender/build-shell-capture.ts +8 -11
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +117 -30
- package/src/route-definition/helpers-types.ts +33 -9
- package/src/route-definition/resolve-handler-use.ts +121 -6
- package/src/route-types.ts +0 -3
- package/src/router/debug-manifest.ts +10 -2
- package/src/router/error-handling.ts +51 -15
- package/src/router/handler-context.ts +34 -25
- package/src/router/intercept-resolution.ts +125 -39
- package/src/router/loader-resolution.ts +56 -11
- package/src/router/match-api.ts +12 -17
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +93 -10
- package/src/router/match-middleware/cache-store.ts +17 -90
- package/src/router/middleware-types.ts +3 -5
- package/src/router/middleware.ts +52 -9
- package/src/router/prerender-match.ts +53 -19
- package/src/router/route-snapshot.ts +11 -1
- package/src/router/router-context.ts +2 -0
- package/src/router/router-interfaces.ts +25 -2
- package/src/router/router-options.ts +8 -5
- package/src/router/segment-resolution/fresh.ts +44 -12
- package/src/router/segment-resolution/loader-cache.ts +205 -45
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +6 -6
- package/src/router/segment-resolution/mask-nested.ts +7 -7
- package/src/router/segment-resolution/revalidation.ts +34 -6
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +8 -45
- package/src/rsc/shell-capture-constants.ts +13 -0
- package/src/rsc/shell-capture.ts +111 -90
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +11 -0
- package/src/server/context.ts +88 -11
- package/src/server/cookie-store.ts +5 -4
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +35 -30
- package/src/ssr/index.tsx +40 -7
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/generated-routes.ts +2 -2
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/internal/flight-client-globals.ts +7 -5
- package/src/testing/render-route.tsx +85 -24
- package/src/testing/run-loader.ts +5 -1
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +8 -6
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +12 -3
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
|
@@ -38,11 +38,12 @@ export const urlpatterns = urls(({ path }) => [
|
|
|
38
38
|
"/events/ticks",
|
|
39
39
|
(ctx) => {
|
|
40
40
|
const encoder = new TextEncoder();
|
|
41
|
+
let interval: ReturnType<typeof setInterval> | undefined;
|
|
41
42
|
|
|
42
43
|
const stream = new ReadableStream({
|
|
43
|
-
|
|
44
|
+
start(controller) {
|
|
44
45
|
let count = 0;
|
|
45
|
-
|
|
46
|
+
interval = setInterval(() => {
|
|
46
47
|
controller.enqueue(
|
|
47
48
|
encoder.encode(`event: tick\ndata: ${++count}\n\n`),
|
|
48
49
|
);
|
|
@@ -51,9 +52,17 @@ export const urlpatterns = urls(({ path }) => [
|
|
|
51
52
|
// Honor client disconnect — signal comes from ctx.request.signal
|
|
52
53
|
ctx.request.signal.addEventListener("abort", () => {
|
|
53
54
|
clearInterval(interval);
|
|
54
|
-
|
|
55
|
+
try {
|
|
56
|
+
controller.close();
|
|
57
|
+
} catch {
|
|
58
|
+
// already closed or cancelled
|
|
59
|
+
}
|
|
55
60
|
});
|
|
56
61
|
},
|
|
62
|
+
// The runtime cancels the stream when the reader goes away
|
|
63
|
+
cancel() {
|
|
64
|
+
clearInterval(interval);
|
|
65
|
+
},
|
|
57
66
|
});
|
|
58
67
|
|
|
59
68
|
return new Response(stream, {
|
|
@@ -80,18 +89,21 @@ source.addEventListener("tick", (e) => console.log("tick", e.data));
|
|
|
80
89
|
|
|
81
90
|
### SSE caveats
|
|
82
91
|
|
|
83
|
-
- **Never
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
cache()
|
|
92
|
+
- **Never put an SSE route inside a `cache()` scope.** Response-route
|
|
93
|
+
`cache()` does not special-case `path.stream`: an SSE response is a `200`
|
|
94
|
+
like any other, so a `cache()` above it would try to store the stream. Keep
|
|
95
|
+
SSE routes outside every `cache()` boundary, and don't layer a custom
|
|
96
|
+
caching middleware on top either.
|
|
87
97
|
- **Middleware is fine.** Global/route middleware rewraps the SSE `Response`
|
|
88
98
|
as `new Response(response.body, { status, headers })` to merge stub headers.
|
|
89
99
|
The `ReadableStream` body is passed by reference, not consumed, so the
|
|
90
100
|
client sees the stream unchanged. (WebSocket upgrades are the exception —
|
|
91
101
|
those bypass rewrap entirely; see below.)
|
|
92
|
-
- **Honor `ctx.request.signal`.** Without wiring
|
|
93
|
-
(timer, DB cursor, upstream fetch), the stream leaks when the
|
|
94
|
-
|
|
102
|
+
- **Honor `ctx.request.signal` and `cancel()`.** Without wiring disconnect to
|
|
103
|
+
your source (timer, DB cursor, upstream fetch), the stream leaks when the
|
|
104
|
+
client goes away. Clean up in both places: depending on the runtime, a
|
|
105
|
+
disconnect surfaces as the request signal aborting, the stream being
|
|
106
|
+
cancelled, or both.
|
|
95
107
|
- **Disable Nginx/CDN buffering** via `x-accel-buffering: no` and ensure
|
|
96
108
|
no intermediate proxy rebuffers. On Cloudflare Workers this is a non-issue.
|
|
97
109
|
|
|
@@ -210,6 +222,8 @@ HTTP request instead (e.g. during login), then read them at upgrade time
|
|
|
210
222
|
via `ctx.request.headers.get("cookie")`.
|
|
211
223
|
|
|
212
224
|
```typescript
|
|
225
|
+
import { cookies } from "@rangojs/router";
|
|
226
|
+
|
|
213
227
|
// Avoid: this cookie may not land on the upgrade response, and the client
|
|
214
228
|
// never reads it during the handshake regardless.
|
|
215
229
|
router.use(async (ctx, next) => {
|
|
@@ -219,7 +233,7 @@ router.use(async (ctx, next) => {
|
|
|
219
233
|
|
|
220
234
|
// Prefer: authenticate by reading a cookie set on a prior HTTP request.
|
|
221
235
|
path.any("/ws", (ctx) => {
|
|
222
|
-
const session =
|
|
236
|
+
const session = cookies().get("session")?.value;
|
|
223
237
|
if (!verify(session)) return new Response("unauthorized", { status: 401 });
|
|
224
238
|
// ...upgrade
|
|
225
239
|
});
|
|
@@ -240,7 +254,8 @@ router.use(async (ctx, next) => {
|
|
|
240
254
|
|
|
241
255
|
## Caching
|
|
242
256
|
|
|
243
|
-
- **SSE** — do not
|
|
257
|
+
- **SSE** — do not place under `cache()`: it is a cacheable `200`, and an
|
|
258
|
+
endless stream cannot be stored or replayed.
|
|
244
259
|
- **WebSocket** — `cache()` is inert because only `status === 200` is cacheable.
|
|
245
260
|
|
|
246
261
|
## Runtime caveats
|
|
@@ -279,5 +294,8 @@ Object.defineProperty(upgrade, "webSocket", {
|
|
|
279
294
|
|
|
280
295
|
## See also
|
|
281
296
|
|
|
282
|
-
-
|
|
283
|
-
|
|
297
|
+
- `/response-routes` — the parent skill for `path.json/text/html/stream/any`
|
|
298
|
+
and `ResponseHandlerContext` (`ctx.header()`, `ctx.waitUntil()`,
|
|
299
|
+
`ctx.executionContext`).
|
|
300
|
+
- `/middleware` — how global and route-level middleware compose with handlers.
|
|
301
|
+
- `/cloudflare` — Workers bindings and Durable Object setup.
|
package/skills/tailwind/SKILL.md
CHANGED
|
@@ -6,7 +6,9 @@ argument-hint: [setup]
|
|
|
6
6
|
|
|
7
7
|
# Tailwind CSS
|
|
8
8
|
|
|
9
|
-
Set up Tailwind CSS v4
|
|
9
|
+
Set up Tailwind CSS v4 in a Rango app: the Vite plugin, the CSS entry, loading
|
|
10
|
+
it from the Document with Vite's `?url` import, theme tokens, class-based dark
|
|
11
|
+
mode, and fonts. The general stylesheet pattern is explained in `/css`.
|
|
10
12
|
|
|
11
13
|
## Install
|
|
12
14
|
|
|
@@ -18,11 +20,14 @@ pnpm add -D tailwindcss @tailwindcss/vite
|
|
|
18
20
|
|
|
19
21
|
```typescript
|
|
20
22
|
// vite.config.ts
|
|
23
|
+
import { defineConfig } from "vite";
|
|
21
24
|
import tailwindcss from "@tailwindcss/vite";
|
|
25
|
+
import { rango } from "@rangojs/router/vite";
|
|
22
26
|
|
|
23
27
|
export default defineConfig({
|
|
24
28
|
plugins: [
|
|
25
29
|
tailwindcss(),
|
|
30
|
+
rango(),
|
|
26
31
|
// ... other plugins
|
|
27
32
|
],
|
|
28
33
|
});
|
|
@@ -37,18 +42,17 @@ export default defineConfig({
|
|
|
37
42
|
|
|
38
43
|
## Document Component
|
|
39
44
|
|
|
40
|
-
Import the CSS file with `?url` to get
|
|
41
|
-
`<head>`. Give the `<link rel="stylesheet">` a `precedence` prop so React 19
|
|
42
|
-
manages it as a resource
|
|
43
|
-
(no flash of unstyled content).
|
|
44
|
-
[Stylesheets and cross-app navigation](#stylesheets-and-cross-app-navigation):
|
|
45
|
+
Import the CSS file with `?url` to get its hashed URL, then preload and link it
|
|
46
|
+
in `<head>`. Give the `<link rel="stylesheet">` a `precedence` prop so React 19
|
|
47
|
+
manages it as a resource: de-duped by `href`, ordered, and loaded before paint
|
|
48
|
+
(no flash of unstyled content).
|
|
45
49
|
|
|
46
50
|
```tsx
|
|
47
51
|
// src/document.tsx
|
|
48
52
|
"use client";
|
|
49
53
|
|
|
50
54
|
import type { ReactNode } from "react";
|
|
51
|
-
import {
|
|
55
|
+
import { Html } from "@rangojs/router/client";
|
|
52
56
|
import styles from "./index.css?url";
|
|
53
57
|
|
|
54
58
|
export function Document({ children }: { children: ReactNode }) {
|
|
@@ -57,7 +61,7 @@ export function Document({ children }: { children: ReactNode }) {
|
|
|
57
61
|
<head>
|
|
58
62
|
<link rel="preload" href={styles} as="style" precedence="default" />
|
|
59
63
|
<link rel="stylesheet" href={styles} precedence="default" />
|
|
60
|
-
<
|
|
64
|
+
<Html.Meta />
|
|
61
65
|
</head>
|
|
62
66
|
<body className="font-sans antialiased text-slate-900 bg-slate-50">
|
|
63
67
|
{children}
|
|
@@ -67,27 +71,15 @@ export function Document({ children }: { children: ReactNode }) {
|
|
|
67
71
|
}
|
|
68
72
|
```
|
|
69
73
|
|
|
70
|
-
|
|
74
|
+
The `?url` suffix tells Vite to return the processed CSS file's URL instead of
|
|
75
|
+
injecting it as a side effect, giving a stable, hashed asset path in both
|
|
76
|
+
development and production. A bare `import "./index.css"` also produces managed
|
|
77
|
+
CSS, but it is not guaranteed to be in the initial streamed `<head>`; prefer
|
|
78
|
+
`?url` + `<link precedence>` for document CSS (see `/css`).
|
|
71
79
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
recommended way to render a stylesheet link, which is why the example above uses
|
|
76
|
-
it. (A bare side-effect `import "./index.css"` also produces managed CSS via
|
|
77
|
-
`@vitejs/plugin-rsc`, but carries an SSR-streaming caveat — prefer the `?url` +
|
|
78
|
-
`<link precedence>` form for document CSS. See `/css`.)
|
|
79
|
-
|
|
80
|
-
For **host-router** apps (`/host-router`), a client-side navigation that crosses
|
|
81
|
-
an app boundary is a **full document load**, not a soft swap — the framework
|
|
82
|
-
redirects on an app switch. So each app's document (its stylesheets, theme, meta)
|
|
83
|
-
is always re-established cleanly by the target app's own load; you do not have to
|
|
84
|
-
coordinate stylesheet `href`s or `precedence` across apps. (This replaced an
|
|
85
|
-
earlier soft cross-app swap, under which a stylesheet shared across apps — every
|
|
86
|
-
app's `@import "tailwindcss"` compiles to one hashed asset — could be dropped by
|
|
87
|
-
React's by-`href` resource dedup if the apps disagreed on `precedence`. The full
|
|
88
|
-
reload removes that footgun entirely.)
|
|
89
|
-
|
|
90
|
-
The `?url` suffix tells Vite to return the processed CSS file's URL instead of injecting it as a side effect. This gives you a stable, hashed asset path that works in both development and production.
|
|
80
|
+
For **host-router** apps (`/host-router`), navigating between apps is a full
|
|
81
|
+
document load, so each app's Tailwind stylesheet is loaded by its own Document;
|
|
82
|
+
there is nothing to coordinate across apps.
|
|
91
83
|
|
|
92
84
|
## Customizing the Theme
|
|
93
85
|
|
|
@@ -107,17 +99,29 @@ Tailwind v4 uses CSS `@theme` for customization:
|
|
|
107
99
|
|
|
108
100
|
## Dark Mode
|
|
109
101
|
|
|
110
|
-
Combine with the Rango theme system (see `/theme`):
|
|
102
|
+
Combine with the Rango theme system (see `/theme`). With `attribute: "class"`
|
|
103
|
+
(the default) the theme script puts `class="dark"` or `class="light"` on
|
|
104
|
+
`<html>` before first paint; add `suppressHydrationWarning` to the Document's
|
|
105
|
+
`<html>` so React accepts that change:
|
|
111
106
|
|
|
112
107
|
```typescript
|
|
113
|
-
const router = createRouter({
|
|
108
|
+
export const router = createRouter({
|
|
114
109
|
document: Document,
|
|
115
110
|
urls: urlpatterns,
|
|
116
111
|
theme: { attribute: "class" },
|
|
117
112
|
});
|
|
118
113
|
```
|
|
119
114
|
|
|
120
|
-
|
|
115
|
+
Tailwind v4's `dark:` variant follows `prefers-color-scheme` by default. Point
|
|
116
|
+
it at the class instead, so the user's stored choice wins over the OS setting:
|
|
117
|
+
|
|
118
|
+
```css
|
|
119
|
+
/* src/index.css */
|
|
120
|
+
@import "tailwindcss";
|
|
121
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Then use the `dark:` variant as usual:
|
|
121
125
|
|
|
122
126
|
```tsx
|
|
123
127
|
<div className="bg-white dark:bg-slate-900 text-slate-900 dark:text-white">
|
|
@@ -147,7 +151,7 @@ No extra `<link>` tags needed in the Document -- Vite bundles the font files fro
|
|
|
147
151
|
|
|
148
152
|
## Notes
|
|
149
153
|
|
|
150
|
-
- `?url` import
|
|
151
|
-
- `<link rel="preload" as="style">`
|
|
154
|
+
- Use the `?url` import for the document stylesheet: a bare side-effect import is not guaranteed to be in the initial streamed `<head>` (see `/css`)
|
|
155
|
+
- `<link rel="preload" as="style">` starts the stylesheet download early
|
|
152
156
|
- Tailwind v4 does not need a `tailwind.config.js` -- use `@theme` in CSS instead
|
|
153
157
|
- The `@tailwindcss/vite` plugin handles content detection automatically
|
package/skills/testing/SKILL.md
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: testing
|
|
3
|
-
description: Test @rangojs/router apps — unit (loaders
|
|
3
|
+
description: Test @rangojs/router apps — unit (loaders, middleware, server actions, handles, transition/clientUrls predicates), client components (renderRoute), RSC/Flight (renderServerTree, renderHandler), integration (dispatch), and e2e (dev+prod parity, progressive enhancement). Use when writing a unit test for a loader, middleware, action, or handler, setting up vitest for a Rango app, or testing a route end-to-end in dev and production.
|
|
4
4
|
argument-hint: [layer]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Testing @rangojs/router apps
|
|
8
8
|
|
|
9
|
+
This skill picks the right test layer for a behavior in a Rango app and gives
|
|
10
|
+
the API, a recipe, and the caveats for each testing primitive. Use it when
|
|
11
|
+
adding or changing tests, or when wiring the vitest/Playwright setup.
|
|
12
|
+
|
|
9
13
|
Rango ships six consumer-facing testing entries, one per test runtime/dependency:
|
|
10
14
|
`@rangojs/router/testing` (unit + integration, under a Vite-driven Vitest
|
|
11
15
|
project), `@rangojs/router/testing/vitest` (the `rangoTestConfig`/`rangoTestAliases`
|
|
12
16
|
setup preset), `@rangojs/router/testing/dom` (`renderRoute`, needs RTL + a DOM
|
|
13
17
|
env), `@rangojs/router/testing/e2e` (the Playwright harness),
|
|
14
18
|
`@rangojs/router/testing/flight` (real Flight, react-server condition only), and
|
|
15
|
-
`@rangojs/router/testing/flight-matchers` (the Flight matchers).
|
|
19
|
+
`@rangojs/router/testing/flight-matchers` (the Flight matchers). Host-router
|
|
20
|
+
pattern helpers live separately at `@rangojs/router/host/testing` (see
|
|
21
|
+
`/host-router`).
|
|
16
22
|
|
|
17
23
|
The hard problem in an RSC app is that the layer you reach for is dictated by
|
|
18
24
|
**what the behavior touches** — a pure predicate is a one-line vitest test; a real
|
|
@@ -42,8 +48,8 @@ Use this skill when adding or changing tests for a Rango app: a loader,
|
|
|
42
48
|
middleware, a server action, a route map, a client component, a response route,
|
|
43
49
|
cache/SWR behavior, prerender, or a navigation/PE flow.
|
|
44
50
|
|
|
45
|
-
Two non-negotiable mandates (
|
|
46
|
-
consumer apps too):
|
|
51
|
+
Two non-negotiable mandates (the Rango repo holds itself to these, and they
|
|
52
|
+
apply to consumer apps too):
|
|
47
53
|
|
|
48
54
|
- **Every e2e covers BOTH dev and production.** A dev-only e2e is not acceptable.
|
|
49
55
|
Use `parityDescribe` — it generates the dev and production describes from one
|
|
@@ -54,8 +60,10 @@ consumer apps too):
|
|
|
54
60
|
|
|
55
61
|
## The read-first shape
|
|
56
62
|
|
|
57
|
-
Four
|
|
58
|
-
split is forced by hard walls, not
|
|
63
|
+
Four of the six entries hold the test primitives, each matched to the
|
|
64
|
+
dependency/runtime that can load it — this split is forced by hard walls, not
|
|
65
|
+
preference. (The other two are helpers: `/testing/vitest` is config-only, and
|
|
66
|
+
`/testing/flight-matchers` holds `toMatchFlight`/`toMatchFlightSnapshot`.)
|
|
59
67
|
|
|
60
68
|
- `@rangojs/router/testing` — unit + integration primitives. Run these under a
|
|
61
69
|
**Vite-driven Vitest** project with the rango Vite plugin active (the router
|
|
@@ -81,26 +89,29 @@ The single rule that drives everything:
|
|
|
81
89
|
|
|
82
90
|
Each primitive links to its sub-file (API + recipe + caveats).
|
|
83
91
|
|
|
84
|
-
| The behavior is… | Layer | Primitive
|
|
85
|
-
| ---------------------------------------------------------------------------------------------------------------------------------- | ------------ |
|
|
86
|
-
| a
|
|
87
|
-
| one loader's data logic | unit (node) | [`runLoader`](./loader.md)
|
|
88
|
-
| a loader's cookie / header / thrown `redirect()`/`notFound()` / handle-push output | unit (node) | [`runLoaderResult`](./loader.md)
|
|
89
|
-
| one middleware's ordering / short-circuit / cookie+header merge | unit (node) | [`runMiddleware`](./middleware.md)
|
|
90
|
-
| a `"use server"` action's cookie / header / flash output (even on `throw redirect()`) | unit (node) | [`runInRequestContext`](./server-actions.md)
|
|
91
|
-
| a `transition({ when })` gate (keep/drop) against nav source / target / action metadata | unit (node) | `runTransitionWhen` (`{ kept, whenContext }`;
|
|
92
|
-
| a `clientUrls()` `revalidate()` / `isAction(ref)` predicate | unit (node) | `runClientRevalidate` (production
|
|
93
|
-
| a handle's `collect`/accumulator, a seeded handle read, or a loader handle write | unit | [`collectHandle` / seeded `handles` / `handlePushes`](./handles.md)
|
|
94
|
-
| a CLIENT component reading router context (`useParams`/`useReverse`/`Outlet`/`useNavigation`/`useLoader`) | unit (DOM) | [`renderRoute`](./client-components.md)
|
|
95
|
-
| a
|
|
96
|
-
| a
|
|
97
|
-
|
|
|
98
|
-
|
|
|
99
|
-
|
|
|
100
|
-
|
|
|
101
|
-
|
|
|
102
|
-
|
|
|
103
|
-
|
|
|
92
|
+
| The behavior is… | Layer | Primitive | Import root |
|
|
93
|
+
| ---------------------------------------------------------------------------------------------------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------- | -------------------------------- |
|
|
94
|
+
| a `reverse` / `href` / params / env TYPE contract (a wrong name or missing param must not compile) | types (tsc) | [`@ts-expect-error` / `expectTypeOf`](./reverse-and-types.md) | `@rangojs/router/client` + `tsc` |
|
|
95
|
+
| one loader's data logic | unit (node) | [`runLoader`](./loader.md) | `@rangojs/router/testing` |
|
|
96
|
+
| a loader's cookie / header / thrown `redirect()`/`notFound()` / handle-push output | unit (node) | [`runLoaderResult`](./loader.md) | `@rangojs/router/testing` |
|
|
97
|
+
| one middleware's ordering / short-circuit / cookie+header merge | unit (node) | [`runMiddleware`](./middleware.md) | `@rangojs/router/testing` |
|
|
98
|
+
| a `"use server"` action's cookie / header / flash output (even on `throw redirect()`) | unit (node) | [`runInRequestContext`](./server-actions.md) | `@rangojs/router/testing` |
|
|
99
|
+
| a `transition({ when })` gate (keep/drop) against nav source / target / action metadata | unit (node) | [`runTransitionWhen`](./predicates.md) (`{ kept, whenContext }`; `{ ppr: true }` for pre-handler timing) | `@rangojs/router/testing` |
|
|
100
|
+
| a `clientUrls()` `revalidate()` / `isAction(ref)` predicate | unit (node) | [`runClientRevalidate`](./predicates.md) (production chain evaluator + locked defaults) | `@rangojs/router/testing` |
|
|
101
|
+
| a handle's `collect`/accumulator, a seeded handle read, or a loader handle write | unit | [`collectHandle` / seeded `handles` / `handlePushes`](./handles.md) | `@rangojs/router/testing` |
|
|
102
|
+
| a CLIENT component reading router context (`useParams`/`useReverse`/`Outlet`/`useNavigation`/`useLoader`) | unit (DOM) | [`renderRoute`](./client-components.md) | `@rangojs/router/testing/dom` |
|
|
103
|
+
| a stale indicator from `useLoader().isLoading` while a `transition()` navigation re-runs the loader | unit (DOM) | [`renderRoute` + `navigate(url, { loaders })`](./client-components.md#held-navigation) | `@rangojs/router/testing/dom` |
|
|
104
|
+
| a redirect / status / headers / cookies / **response route** (json/text/html/xml/md), no Flight | integration | [`dispatch`](./response-routes.md) | `@rangojs/router/testing` |
|
|
105
|
+
| a real async **Server Component** (assert what it rendered: typed boundary props, server-rendered host content, inlined-vs-island) | RSC unit | [`renderServerTree` + `findClientBoundaries`/`findElements`](./server-tree.md) | `@rangojs/router/testing/flight` |
|
|
106
|
+
| the exact Flight **wire payload** shape (a drift snapshot) | RSC unit | [`renderToFlightString` + `toMatchFlightSnapshot`](./flight.md) | `@rangojs/router/testing/flight` |
|
|
107
|
+
| a real route **handler** `(ctx) => rsc` (params/loaders/vars -> rendered RSC + effects) | RSC unit | [`renderHandler`](./render-handler.md) | `@rangojs/router/testing/flight` |
|
|
108
|
+
| navigation, hydration, PE parity, view transitions, real SSR | e2e | [`createRangoE2E` -> `parityDescribe`/`expectParity`](./e2e-parity.md) | `@rangojs/router/testing/e2e` |
|
|
109
|
+
| a `"use cache"` hit in a handler (the body runs once across renders; the value round-trips) | RSC unit | [`renderHandler` + seeded `cacheStore`](./render-handler.md#a-use-cache-hit) | `@rangojs/router/testing/flight` |
|
|
110
|
+
| a `"use cache"` hit in a loader (the same, in the react-server project) | RSC unit | [`runLoader` + seeded `cacheStore`](./loader.md) | `@rangojs/router/testing` |
|
|
111
|
+
| cache hit/miss/stale, prerender (= a cache hit by design) | e2e + signal | [`assertCacheStatus` (header) / `assertCacheDecision` (telemetry sink)](./cache-prerender.md) | `@rangojs/router/testing[/e2e]` |
|
|
112
|
+
| PPR shell HIT/MISS (`ppr` path option), shell store key / getShell after flush | e2e + signal | [`assertShellStatus` / `shellCacheKey` + store](./cache-prerender.md#ppr-shell-and-navigation-replay) | `@rangojs/router/testing[/e2e]` |
|
|
113
|
+
| generated route map drift vs runtime | unit (node) | [`assertGeneratedRoutesMatch`](./reverse-and-types.md) | `@rangojs/router/testing` |
|
|
114
|
+
| a platform binding (`env.DB` / Durable Object / `env.R2`) | unit/integr. | [your own double via `env`](./bindings.md) | (any primitive's `env` option) |
|
|
104
115
|
|
|
105
116
|
Cross-references to the DSL skills: `/loader`, `/middleware`, `/server-actions`,
|
|
106
117
|
`/handler-use`, `/hooks`, `/response-routes`, `/route`, `/caching`, `/prerender`,
|
|
@@ -111,22 +122,47 @@ Cross-references to the DSL skills: `/loader`, `/middleware`, `/server-actions`,
|
|
|
111
122
|
- Cross-cutting: [`setup.md`](./setup.md), [`bindings.md`](./bindings.md)
|
|
112
123
|
- Unit (node): [`loader.md`](./loader.md), [`middleware.md`](./middleware.md),
|
|
113
124
|
[`server-actions.md`](./server-actions.md), [`handles.md`](./handles.md),
|
|
114
|
-
[`reverse-and-types.md`](./reverse-and-types.md)
|
|
125
|
+
[`predicates.md`](./predicates.md), [`reverse-and-types.md`](./reverse-and-types.md)
|
|
115
126
|
- Unit (DOM): [`client-components.md`](./client-components.md)
|
|
116
127
|
- RSC unit: [`flight.md`](./flight.md), [`server-tree.md`](./server-tree.md),
|
|
117
128
|
[`render-handler.md`](./render-handler.md)
|
|
118
129
|
- Integration: [`response-routes.md`](./response-routes.md)
|
|
119
130
|
- E2E: [`e2e-parity.md`](./e2e-parity.md), [`cache-prerender.md`](./cache-prerender.md)
|
|
120
131
|
|
|
121
|
-
##
|
|
122
|
-
|
|
123
|
-
|
|
132
|
+
## Every export, by entry
|
|
133
|
+
|
|
134
|
+
Where each runtime export is documented (types such as `RunLoaderOptions` sit
|
|
135
|
+
beside their function):
|
|
136
|
+
|
|
137
|
+
| Entry | Exports | Documented in |
|
|
138
|
+
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------ |
|
|
139
|
+
| `@rangojs/router/testing` | `runLoader`, `runLoaderResult` | [`loader.md`](./loader.md) |
|
|
140
|
+
| | `runMiddleware` | [`middleware.md`](./middleware.md) |
|
|
141
|
+
| | `runInRequestContext`, `createTestRequestContext`, `runWithRequestContext`, `toRequest`, `seedVariables` | [`server-actions.md`](./server-actions.md) |
|
|
142
|
+
| | `runTransitionWhen`, `runClientRevalidate` | [`predicates.md`](./predicates.md) |
|
|
143
|
+
| | `collectHandle` | [`handles.md`](./handles.md) |
|
|
144
|
+
| | `dispatch` | [`response-routes.md`](./response-routes.md) |
|
|
145
|
+
| | `assertCacheStatus`, `assertCacheDecision`, `parseCacheHeader`, `createCacheSink`, `filterCacheDecisions` | [`cache-prerender.md`](./cache-prerender.md) |
|
|
146
|
+
| | `assertShellStatus`, `parseShellStatus`, `shellCacheKey`, `SHELL_STATUS_HEADER`, `assertPprReplayStatus`, `parsePprReplayStatus`, `PPR_REPLAY_STATUS_HEADER` | [`cache-prerender.md`](./cache-prerender.md) |
|
|
147
|
+
| | `assertGeneratedRoutesMatch`, `diffGeneratedRoutes` | [`reverse-and-types.md`](./reverse-and-types.md) |
|
|
148
|
+
| `@rangojs/router/testing/dom` | `renderRoute` | [`client-components.md`](./client-components.md) |
|
|
149
|
+
| `@rangojs/router/testing/flight` | `renderToFlightString`, `normalizeFlight`, `assertFlightRuntimeAvailable` | [`flight.md`](./flight.md) |
|
|
150
|
+
| | `renderServerTree`, `findClientBoundaries`, `findElements`, `textContent`, `assertFlightTreeRuntimeAvailable` | [`server-tree.md`](./server-tree.md) |
|
|
151
|
+
| | `renderHandler` | [`render-handler.md`](./render-handler.md) |
|
|
152
|
+
| `@rangojs/router/testing/flight-matchers` | `flightMatchers` (`toMatchFlight`, `toMatchFlightSnapshot`) | [`flight.md`](./flight.md) |
|
|
153
|
+
| `@rangojs/router/testing/vitest` | `rangoTestConfig`, `rangoTestAliases`, `rangoInlineDeps`, `rangoUseClientTransform`, `rangoUseCacheTransform` | [`setup.md`](./setup.md) |
|
|
154
|
+
| `@rangojs/router/testing/e2e` | `createRangoE2E` and its standalone helpers; the cache/shell status helpers above are re-exported here | [`e2e-parity.md`](./e2e-parity.md) |
|
|
155
|
+
|
|
156
|
+
## Pre-push checklist
|
|
157
|
+
|
|
158
|
+
Before pushing, run all of these and fix any failure (script names follow
|
|
159
|
+
[`./setup.md`](./setup.md); adjust to your `package.json`):
|
|
124
160
|
|
|
125
161
|
1. `pnpm run typecheck` (or `pnpm exec tsc --noEmit`)
|
|
126
162
|
2. `pnpm run test:unit` (node + DOM vitest)
|
|
127
163
|
3. `pnpm run test:unit:rsc` (the react-server Flight project)
|
|
128
|
-
4.
|
|
129
|
-
5.
|
|
164
|
+
4. your lint and format scripts
|
|
165
|
+
5. your Playwright suite (dev AND production projects)
|
|
130
166
|
|
|
131
167
|
And: **every e2e has a production counterpart.** `parityDescribe` makes this
|
|
132
168
|
automatic — if you wrote a plain `test.describe` for a behavior, convert it.
|
|
@@ -19,11 +19,11 @@ Use the header path when all you have is a black-box `Response` (a Playwright `A
|
|
|
19
19
|
|
|
20
20
|
### Options — `assertCacheStatus(target, segment, expected)`
|
|
21
21
|
|
|
22
|
-
| Field | Type
|
|
23
|
-
| ---------- |
|
|
24
|
-
| `target` | `Response \| { headers: Headers }` (`CacheStatusTarget`)
|
|
25
|
-
| `segment` | `string`
|
|
26
|
-
| `expected` | `"hit" \| "miss" \| "stale" \| "prerendered"
|
|
22
|
+
| Field | Type | Meaning |
|
|
23
|
+
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
24
|
+
| `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()) }`. |
|
|
25
|
+
| `segment` | `string` | The route NAME (e.g. `product.detail`), the same id the header carries — NOT the URL pattern (`/products/:id`). |
|
|
26
|
+
| `expected` | `"hit" \| "miss" \| "stale" \| "prerendered"` (`ExpectedCacheStatus` = `CacheSegmentStatus`; `"passthrough"` is in the union but never emitted — a passthrough route reports `"miss"`) | The cache status you assert for that route. |
|
|
27
27
|
|
|
28
28
|
### Context — what your code under test emits
|
|
29
29
|
|
|
@@ -33,7 +33,7 @@ The header / event is produced by the router's RSC render pipeline from `ctx.rou
|
|
|
33
33
|
| ------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------ |
|
|
34
34
|
| `CacheSegmentSignal.id` | `string` | Segment id. v1: the route key, since status is route-level. |
|
|
35
35
|
| `CacheSegmentSignal.type` | `string` | Segment type. v1: `"route"` for the coarse route-level entry. |
|
|
36
|
-
| `CacheSegmentSignal.cacheStatus` | `CacheSegmentStatus` | Resolved status (`hit`/`miss`/`stale`/`prerendered
|
|
36
|
+
| `CacheSegmentSignal.cacheStatus` | `CacheSegmentStatus` | Resolved status (`hit`/`miss`/`stale`/`prerendered`; passthrough routes report `miss`). |
|
|
37
37
|
| `CacheSegmentSignal.shouldRevalidate` | `boolean?` | Whether stale-while-revalidate was triggered for this segment. |
|
|
38
38
|
| `CacheDecisionEvent.segments` | `CacheSegmentSignal[]?` | The coarse route-level signal array (present only when telemetry or the debug gate is on). |
|
|
39
39
|
|
|
@@ -92,15 +92,19 @@ parityDescribe("product page caches", (f) => {
|
|
|
92
92
|
Zero-prod-surface alternative — the telemetry sink. No header at all; you inspect captured `cache.decision` events:
|
|
93
93
|
|
|
94
94
|
```ts
|
|
95
|
+
import { expect } from "vitest";
|
|
96
|
+
import { createRouter } from "@rangojs/router";
|
|
95
97
|
import {
|
|
96
98
|
createCacheSink,
|
|
97
99
|
assertCacheDecision,
|
|
98
100
|
filterCacheDecisions,
|
|
99
101
|
} from "@rangojs/router/testing";
|
|
102
|
+
import { urlpatterns } from "../src/urls";
|
|
100
103
|
|
|
101
104
|
const { sink, events } = createCacheSink();
|
|
102
105
|
const router = createRouter({ telemetry: sink }).routes(urlpatterns);
|
|
103
|
-
// ...drive a request through the router's RSC fetch path
|
|
106
|
+
// ...drive a request through the router's RSC fetch path (e.g. an e2e server
|
|
107
|
+
// built from this router); dispatch() never emits cache.decision...
|
|
104
108
|
|
|
105
109
|
// One-call assert (counterpart of assertCacheStatus), keyed by the route NAME:
|
|
106
110
|
assertCacheDecision(events, "product.detail", "stale");
|
|
@@ -117,15 +121,15 @@ expect(decision.segments?.[0].shouldRevalidate).toBe(true);
|
|
|
117
121
|
|
|
118
122
|
**DSL:** `ppr: true | PartialPrerenderProps` on a page route (see `/ppr`). **Not** the same header as `X-Rango-Cache` — shell is a second render axis.
|
|
119
123
|
|
|
120
|
-
| Helper | Import | Role
|
|
121
|
-
| ----------------------------------------- | ------------------------------------ |
|
|
122
|
-
| `assertShellStatus(res, "HIT" \| "MISS")` | `@rangojs/router/testing` or `…/e2e` | Assert `x-rango-shell` on a **real document** Response
|
|
123
|
-
| `parseShellStatus(res)` | same | `"HIT" \| "MISS" \| null` (null = header absent / unrecognized)
|
|
124
|
-
| `shellCacheKey(url)`
|
|
125
|
-
| `SHELL_STATUS_HEADER` | same | `"x-rango-shell"` constant
|
|
126
|
-
| `assertPprReplayStatus(res, expected)` | same | Assert fresh/stale replay or a bounded bypass decision
|
|
127
|
-
| `parsePprReplayStatus(res)` | same | Structured replay/bypass status, or null for an absent/unrecognized header
|
|
128
|
-
| `PPR_REPLAY_STATUS_HEADER` | same | `"x-rango-ppr-replay"` constant
|
|
124
|
+
| Helper | Import | Role |
|
|
125
|
+
| ----------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
126
|
+
| `assertShellStatus(res, "HIT" \| "MISS")` | `@rangojs/router/testing` or `…/e2e` | Assert `x-rango-shell` on a **real document** Response |
|
|
127
|
+
| `parseShellStatus(res)` | same | `"HIT" \| "MISS" \| null` (null = header absent / unrecognized) |
|
|
128
|
+
| `shellCacheKey(url, searchParams?)` | same | Production shell store key (`host+pathname+sorted search+:shell`) for `store.getShell` / custom stores. Pass your router's `cache.searchParams` config as the second argument when it sets one — the production key applies it. |
|
|
129
|
+
| `SHELL_STATUS_HEADER` | same | `"x-rango-shell"` constant |
|
|
130
|
+
| `assertPprReplayStatus(res, expected)` | same | Assert fresh/stale replay or a bounded bypass decision |
|
|
131
|
+
| `parsePprReplayStatus(res)` | same | Structured replay/bypass status, or null for an absent/unrecognized header |
|
|
132
|
+
| `PPR_REPLAY_STATUS_HEADER` | same | `"x-rango-ppr-replay"` constant |
|
|
129
133
|
|
|
130
134
|
### What unit can prove vs e2e
|
|
131
135
|
|
|
@@ -141,19 +145,41 @@ expect(decision.segments?.[0].shouldRevalidate).toBe(true);
|
|
|
141
145
|
### Recipe (e2e)
|
|
142
146
|
|
|
143
147
|
```ts
|
|
144
|
-
import {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
148
|
+
import { expect, test } from "@playwright/test";
|
|
149
|
+
import {
|
|
150
|
+
createRangoE2E,
|
|
151
|
+
assertShellStatus,
|
|
152
|
+
parseShellStatus,
|
|
153
|
+
} from "@rangojs/router/testing/e2e";
|
|
154
|
+
|
|
155
|
+
const { parityDescribe } = createRangoE2E({ test, expect });
|
|
156
|
+
|
|
157
|
+
parityDescribe("product shell", (f) => {
|
|
158
|
+
test("MISS captures, then HIT", async ({ page }) => {
|
|
159
|
+
// Document GET (not Accept: text/x-component):
|
|
160
|
+
const first = await page.request.get(f.url("/products/1"));
|
|
161
|
+
assertShellStatus({ headers: new Headers(first.headers()) }, "MISS");
|
|
162
|
+
// Capture runs in the background (putShell), so poll for the HIT:
|
|
163
|
+
await expect
|
|
164
|
+
.poll(async () => {
|
|
165
|
+
const res = await page.request.get(f.url("/products/1"));
|
|
166
|
+
return parseShellStatus({ headers: new Headers(res.headers()) });
|
|
167
|
+
})
|
|
168
|
+
.toBe("HIT");
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
// Unit (Vitest): the key the serve path uses, against your own store instance
|
|
175
|
+
import { expect, it } from "vitest";
|
|
176
|
+
import { shellCacheKey } from "@rangojs/router/testing";
|
|
177
|
+
|
|
178
|
+
it("stores the shell under the production key", async () => {
|
|
179
|
+
// ...after your code under test captured a shell into `store`...
|
|
180
|
+
const key = shellCacheKey(new URL("http://localhost/products/1"));
|
|
181
|
+
expect(await store.getShell(key)).not.toBeNull();
|
|
182
|
+
});
|
|
157
183
|
```
|
|
158
184
|
|
|
159
185
|
## Caveats
|