@rangojs/router 0.0.0-experimental.97 → 0.0.0-experimental.98914650
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 +24 -9
- package/dist/bin/rango.js +157 -63
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +1584 -639
- package/package.json +71 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +60 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +222 -30
- package/skills/caching/SKILL.md +263 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +235 -28
- package/skills/host-router/SKILL.md +122 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +29 -5
- package/skills/layout/SKILL.md +13 -9
- package/skills/links/SKILL.md +173 -17
- package/skills/loader/SKILL.md +170 -23
- package/skills/middleware/SKILL.md +16 -10
- package/skills/migrate-nextjs/SKILL.md +38 -16
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +11 -7
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +250 -25
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +114 -47
- package/skills/route/SKILL.md +42 -5
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +78 -42
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +124 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +92 -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 +121 -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/skills/typesafety/SKILL.md +316 -26
- package/skills/use-cache/SKILL.md +36 -5
- package/skills/vercel/SKILL.md +107 -0
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +14 -27
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +37 -143
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +30 -59
- package/src/browser/navigation-client.ts +96 -84
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +32 -82
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/partial-update.ts +60 -127
- package/src/browser/prefetch/cache.ts +82 -72
- package/src/browser/prefetch/fetch.ts +108 -33
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +157 -115
- package/src/browser/react/Link.tsx +0 -2
- package/src/browser/react/NavigationProvider.tsx +41 -48
- 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 -48
- package/src/browser/react/location-state-shared.ts +166 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +17 -14
- 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 +11 -11
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +20 -5
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/response-adapter.ts +52 -1
- package/src/browser/rsc-router.tsx +70 -34
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +168 -44
- package/src/browser/types.ts +36 -21
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +89 -10
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +122 -22
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +68 -28
- package/src/cache/cache-runtime.ts +134 -32
- package/src/cache/cache-scope.ts +100 -74
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2255 -238
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +61 -20
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +22 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +33 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +799 -0
- package/src/client.rsc.tsx +6 -21
- package/src/client.tsx +25 -61
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +17 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +31 -23
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- 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 +8 -2
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +63 -9
- package/src/index.ts +64 -9
- package/src/internal-debug.ts +2 -4
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- 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 +32 -37
- package/src/prerender.ts +61 -6
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +9 -0
- package/src/reverse.ts +65 -40
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +244 -281
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +40 -17
- package/src/route-definition/redirect.ts +43 -9
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -15
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +4 -41
- package/src/router/intercept-resolution.ts +14 -19
- package/src/router/lazy-includes.ts +9 -46
- package/src/router/loader-resolution.ts +91 -46
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +18 -29
- 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 +150 -271
- package/src/router/match-middleware/cache-store.ts +3 -33
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +31 -80
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +5 -112
- package/src/router/middleware.ts +118 -133
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +62 -67
- package/src/router/prerender-match.ts +99 -63
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +28 -62
- package/src/router/revalidation.ts +50 -56
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +0 -27
- package/src/router/router-interfaces.ts +68 -35
- package/src/router/router-options.ts +55 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +44 -63
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +40 -37
- package/src/router/segment-resolution/revalidation.ts +203 -285
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +0 -3
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -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 +87 -48
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +80 -41
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +83 -78
- package/src/rsc/helpers.ts +93 -5
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +12 -1
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +76 -62
- package/src/rsc/rsc-rendering.ts +41 -60
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +62 -67
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +10 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-loader-promise.ts +14 -2
- package/src/segment-system.tsx +199 -142
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +150 -51
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +165 -87
- package/src/ssr/index.tsx +14 -14
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +618 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -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 +232 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -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 +330 -0
- package/src/testing/render-route.tsx +566 -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/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +97 -22
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +6 -3
- 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 +18 -14
- package/src/urls/include-helper.ts +9 -56
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +19 -5
- package/src/urls/path-helper.ts +17 -106
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +20 -19
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +292 -107
- package/src/vite/debug.ts +1 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/discover-routers.ts +95 -82
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/prerender-collection.ts +26 -34
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/state.ts +39 -1
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +185 -10
- package/src/vite/plugins/cjs-to-esm.ts +3 -18
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +12 -11
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
- package/src/vite/plugins/expose-action-id.ts +4 -75
- package/src/vite/plugins/expose-id-utils.ts +3 -54
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
- package/src/vite/plugins/expose-internal-ids.ts +57 -67
- package/src/vite/plugins/performance-tracks.ts +9 -16
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +26 -49
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +2 -32
- package/src/vite/plugins/version-plugin.ts +32 -23
- package/src/vite/plugins/virtual-entries.ts +35 -17
- package/src/vite/rango.ts +148 -115
- package/src/vite/router-discovery.ts +220 -68
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +0 -34
- package/src/vite/utils/shared-utils.ts +95 -43
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Pinning the Flight wire payload — renderToFlightString
|
|
2
|
+
|
|
3
|
+
**Layer:** RSC unit (react-server project) · **Import:** `@rangojs/router/testing/flight` + `@rangojs/router/testing/flight-matchers` · **DSL it tests:** an async Server Component / Flight output (see `/route`)
|
|
4
|
+
|
|
5
|
+
> **Prefer `renderServerTree` (see [`./server-tree.md`](./server-tree.md)) for assertions on a Flight render** — it deserializes to a traversable tree with TYPED boundary props (a `Date` is a `Date`, not the opaque `$D...` encoding). Reach for `renderToFlightString` + the wire matchers (`toMatchFlight`/`toMatchFlightSnapshot`) only to pin the raw wire payload SHAPE — a `toMatchFlightSnapshot` drift snapshot. That is the niche/escape-hatch case; for "testing an async Server Component (assert what it rendered)" start at `./server-tree.md`.
|
|
6
|
+
|
|
7
|
+
`renderToFlightString` runs the REAL react-server-dom serializer the router uses at runtime — your async Server Component genuinely renders to its Flight wire string in plain node, with a request context active for the render. What you SEED is the request, headers, env, params, routeName, and vars that context exposes.
|
|
8
|
+
|
|
9
|
+
## API
|
|
10
|
+
|
|
11
|
+
### Options — `RenderToFlightStringOptions`
|
|
12
|
+
|
|
13
|
+
| Field | Type | Meaning |
|
|
14
|
+
| ----------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
15
|
+
| `request` | `Request \| string` | The request the render runs under: a `Request`, or a URL string (absolute or path). Defaults to `http://localhost/`. A component reading `getRequestContext()` sees this request's url/cookies. When a `Request` is passed, its headers are used and `headers` is ignored. |
|
|
16
|
+
| `headers` | `HeadersInit` | Request headers (e.g. Cookie) visible to the server tree, used only when `request` is a string. |
|
|
17
|
+
| `env` | `unknown` | Env / bindings exposed as `ctx.env`. Defaults to `{}`. |
|
|
18
|
+
| `params` | `Record<string, string>` | Route params exposed via `ctx.params` and loader contexts. |
|
|
19
|
+
| `routeName` | `string` | Matched route name (drives `ctx.routeName` and scoped reverse). |
|
|
20
|
+
| `routeMap` | `Record<string, string>` | Route name -> pattern map scoping `ctx.reverse()` (like `renderHandler`). Without it, a component that reverses resolves against the GLOBAL route map and is order-dependent on whatever router registered last. Pass the router-under-test's map for deterministic reverse. |
|
|
21
|
+
| `vars` | `VarsInit` | Variables a prior middleware would have set, visible via `ctx.get(...)`. Object form (`{ user }`) or `[key, value]` tuples (`[[userVar, u]]`). |
|
|
22
|
+
|
|
23
|
+
### Context — `RequestContext` (what your component receives)
|
|
24
|
+
|
|
25
|
+
A request context is active for the whole render, so an async Server Component can read it via `getRequestContext()` / the router's server APIs. The notable surfaces seeded from the options above:
|
|
26
|
+
|
|
27
|
+
| Field | Type | Meaning |
|
|
28
|
+
| ----------- | ----------------------------------------- | --------------------------------------------------------------------- |
|
|
29
|
+
| `request` | `Request` | The backing request (from `request`/`headers`). |
|
|
30
|
+
| `url` | `URL` | The request URL. |
|
|
31
|
+
| `env` | `unknown` | Env / bindings (from `env`). |
|
|
32
|
+
| `params` | `Record<string, string>` | Route params (from `params`). |
|
|
33
|
+
| `routeName` | `string \| undefined` | Matched route name (from `routeName`). |
|
|
34
|
+
| `get` | `<T>(v: ContextVar<T>) => T \| undefined` | Read a var seeded via `vars` (by `createVar()` handle or string key). |
|
|
35
|
+
| `cookies` | reader | Cookies parsed from the request's Cookie header. |
|
|
36
|
+
|
|
37
|
+
### Returns — `Promise<string>`
|
|
38
|
+
|
|
39
|
+
The Flight wire string for the rendered tree. Assert on it with the matchers (register via `expect.extend(flightMatchers)`):
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
expect(await renderToFlightString(<C />)).toMatchFlight("substring"); // containment
|
|
43
|
+
expect(await renderToFlightString(<C />)).toMatchFlightSnapshot(); // normalized snapshot
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`toMatchFlight(substring)` is containment (not equality) on the normalized payload; `toMatchFlightSnapshot()` snapshots the normalized payload. Both matchers live at `@rangojs/router/testing/flight-matchers` and run ONLY under the react-server vitest project (see `./setup.md`).
|
|
47
|
+
|
|
48
|
+
## Recipe
|
|
49
|
+
|
|
50
|
+
Name the file `*.rsc-test.{ts,tsx}` and run `pnpm test:unit:rsc`:
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
import { it, expect } from "vitest";
|
|
54
|
+
import { renderToFlightString } from "@rangojs/router/testing/flight";
|
|
55
|
+
import { flightMatchers } from "@rangojs/router/testing/flight-matchers";
|
|
56
|
+
expect.extend(flightMatchers);
|
|
57
|
+
|
|
58
|
+
// Pure leaf server components: data comes in as props, not getRequestContext.
|
|
59
|
+
async function Greeting({ name }: { name: string }) {
|
|
60
|
+
const who = await Promise.resolve(name);
|
|
61
|
+
return <h1>Hello {who}</h1>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function ItemView({ id }: { id: string }) {
|
|
65
|
+
const item = await Promise.resolve({ id, label: `Item ${id}` });
|
|
66
|
+
return <article>{item.label}</article>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
it("renders an async server component to Flight", async () => {
|
|
70
|
+
const flight = await renderToFlightString(<Greeting name="Ada" />);
|
|
71
|
+
expect(flight).toMatchFlight("Ada");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("snapshots the normalized payload", async () => {
|
|
75
|
+
const flight = await renderToFlightString(<ItemView id="7" />);
|
|
76
|
+
expect(flight).toMatchFlightSnapshot();
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Caveats
|
|
81
|
+
|
|
82
|
+
- Leaf / server-only: a client island in the tree emits an un-hydratable `I[...]` import row against the empty client manifest. Keep Flight tests to leaf server components; test full pages at e2e.
|
|
83
|
+
- Requires the react-server vitest project (see `./setup.md`): `resolve.conditions` includes `react-server`, the `@rangojs/router -> index.rsc.ts` alias, `NODE_ENV=production`, and the worker `execArgv`. Name files `*.rsc-test.{ts,tsx}` and run `pnpm test:unit:rsc`. The main vitest project must NOT set `react-server` (it would flip React to the no-hooks server build).
|
|
84
|
+
- A component that imports a server API (`getRequestContext`, `cookies`) from the bare `@rangojs/router` barrel works ONLY with the `index.rsc.ts` alias wired (see `./setup.md`); without it the bare import resolves to the throwing out-of-react-server stub. `renderToFlightString` / `renderServerTree` self-diagnose this exact misconfiguration — they reject with an actionable message naming `rangoTestAliases`, rather than surfacing the opaque stub error. Pure-leaf components that take all data as props need no barrel import and are the simplest case.
|
|
85
|
+
- `toMatchFlight` is containment (substring), not equality — the row framing (prefixes/quoting) is an internal serializer detail, so pin the rendered text/shape, not the framing. `toMatchFlightSnapshot()` snapshots the normalized payload; run under `NODE_ENV=production` for the cleanest, most stable bytes.
|
|
86
|
+
- No hydration / no interaction here — that is the e2e tier. For typed assertions on a client boundary's props (a `Date` back as a `Date`), or to confirm an island actually crossed the boundary, use `renderServerTree` (see `./server-tree.md`).
|
|
87
|
+
|
|
88
|
+
## See also
|
|
89
|
+
|
|
90
|
+
- `/route` — the DSL this tests
|
|
91
|
+
- Siblings: `./setup.md`, `./server-tree.md`, `./render-handler.md`
|
|
92
|
+
- Long-form prose: [docs/testing.md](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/testing.md) — section "renderToFlightString — real async Server Components"
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Testing a handle — collectHandle, plus the loader and client read paths
|
|
2
|
+
|
|
3
|
+
**Layer:** unit (node + DOM) · **Import:** `@rangojs/router/testing` (collectHandle), `@rangojs/router/testing/dom` (renderRoute) · **DSL it tests:** a handle e.g. Breadcrumbs/Meta (see `/handler-use`, `/breadcrumbs`)
|
|
4
|
+
|
|
5
|
+
A handle's `collect`/accumulator (the `createHandle(collect)` argument that maps per-segment pushed values into one accumulated result) is otherwise unreachable — `createHandle` keeps it in a private registry keyed by `$$id`. These three primitives test it from different angles: `collectHandle` runs the REAL registered collect on per-segment values you SEED; `runLoader` seeds the POST-collect accumulated value a loader reads after the barrier; `renderRoute` seeds the RAW pushed values for a client component reading `useHandle`. None of them run the real push -> accumulate -> barrier wiring (that stays e2e).
|
|
6
|
+
|
|
7
|
+
## API
|
|
8
|
+
|
|
9
|
+
### `collectHandle(handle, segments)` — `src/testing/collect-handle.ts`
|
|
10
|
+
|
|
11
|
+
| Param | Type | Meaning |
|
|
12
|
+
| ---------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| `handle` | `Handle<TData, TAccumulated>` | The handle whose registered collect to run. |
|
|
14
|
+
| `segments` | `ReadonlyArray<ReadonlyArray<TData>>` | Per-segment pushed values, one inner array per route segment, in **parent -> child** order. Empty inner arrays are filtered before the collect runs (matching production `collectHandleData` — a segment that pushed nothing is not passed through). |
|
|
15
|
+
|
|
16
|
+
**Returns** `TAccumulated` — exactly what the handle's collect produces (a default-flatten array, or a custom accumulator's value). If the handle's module was never imported (collect unregistered), it warns and falls back to `segments.flat()`.
|
|
17
|
+
|
|
18
|
+
### runLoader option — `handles` — `src/testing/run-loader.ts`
|
|
19
|
+
|
|
20
|
+
| Field | Type | Meaning |
|
|
21
|
+
| ---------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
22
|
+
| `handles` | `ReadonlyArray<readonly [Handle, unknown]>` | Seeds the value `ctx.use(SomeHandle)` returns — the POST-collect **ACCUMULATED** value (singular `unknown`), what a loader reads after `await ctx.rendered()`. Matched by handle reference. Pair with `rendered`. |
|
|
23
|
+
| `rendered` | `boolean \| (() => void \| Promise<void>)` | Mocks the `ctx.rendered()` barrier (throws by default). `true` resolves it immediately; a function controls timing/side effects. A `ctx.use(handle)` read before the barrier settles throws, exactly as in production. |
|
|
24
|
+
|
|
25
|
+
### renderRoute option — `handles` — `src/testing/render-route.tsx`
|
|
26
|
+
|
|
27
|
+
| Field | Type | Meaning |
|
|
28
|
+
| --------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
29
|
+
| `handles` | `ReadonlyArray<readonly [Handle, unknown[]]>` | Seeds the CLIENT read path for `useHandle(handle)` — the RAW **pushed values array** (`unknown[]`), the values a route's handlers would have pushed. Attached to the leaf route segment under the handle's `$$id`, so `useHandle` runs the handle's REAL collect on them. |
|
|
30
|
+
|
|
31
|
+
**Shape contrast:** `renderRoute` feeds the barrier INPUT (the pushes, `unknown[]`); `runLoader` feeds its OUTPUT (the single accumulated value, `unknown`).
|
|
32
|
+
|
|
33
|
+
**Across navigation:** seeded `handles` are applied once at the initial render and PERSIST across `router.navigate()` within the same test (like `loaderData`) — unlike a real navigation, which re-runs handlers. A layout/page reading `useHandle` still resolves the seeded values after `navigate()`.
|
|
34
|
+
|
|
35
|
+
## Recipe
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// collectHandle.test.ts — the pure collect, no route match
|
|
39
|
+
import { describe, it, expect } from "vitest";
|
|
40
|
+
import { collectHandle } from "@rangojs/router/testing";
|
|
41
|
+
import { createHandle } from "@rangojs/router";
|
|
42
|
+
|
|
43
|
+
const Breadcrumbs = createHandle<{ label: string; href: string }>(); // default flatten
|
|
44
|
+
|
|
45
|
+
it("flattens per-segment crumbs in parent->child order", () => {
|
|
46
|
+
const home = { label: "Home", href: "/" };
|
|
47
|
+
const post = { label: "P", href: "/p" };
|
|
48
|
+
expect(collectHandle(Breadcrumbs, [[home], [post]])).toEqual([home, post]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("runs a custom 'last wins' collect", () => {
|
|
52
|
+
const PageTitle = createHandle<string, string>((s) => s.flat().at(-1) ?? "");
|
|
53
|
+
expect(collectHandle(PageTitle, [["Home"], ["Products"], ["Shoes"]])).toBe(
|
|
54
|
+
"Shoes",
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
// loader-reads-handle.test.ts — a loader reading accumulated handle data after the barrier
|
|
61
|
+
import { it, expect } from "vitest";
|
|
62
|
+
import { runLoader } from "@rangojs/router/testing";
|
|
63
|
+
import { RenderedProducts } from "../src/handles"; // a createHandle(...)
|
|
64
|
+
|
|
65
|
+
const livePricesBody = async (ctx) => {
|
|
66
|
+
await ctx.rendered(); // barrier: handle data is now readable
|
|
67
|
+
const ids = ctx.use(RenderedProducts) as string[];
|
|
68
|
+
return ids.map((id) => ({ id, price: 9.99 }));
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
it("reads the accumulated handle value (seed the OUTPUT, mock the barrier)", async () => {
|
|
72
|
+
const data = await runLoader(livePricesBody, {
|
|
73
|
+
rendered: true,
|
|
74
|
+
handles: [[RenderedProducts, ["widget-a", "widget-b"]]], // singular accumulated value
|
|
75
|
+
});
|
|
76
|
+
expect(data).toEqual([
|
|
77
|
+
{ id: "widget-a", price: 9.99 },
|
|
78
|
+
{ id: "widget-b", price: 9.99 },
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
// breadcrumb-trail.test.tsx — a client component reading useHandle
|
|
85
|
+
// @vitest-environment happy-dom
|
|
86
|
+
import { it, expect, afterEach } from "vitest";
|
|
87
|
+
import { cleanup } from "@testing-library/react";
|
|
88
|
+
import { renderRoute } from "@rangojs/router/testing/dom";
|
|
89
|
+
import { useHandle } from "@rangojs/router/client";
|
|
90
|
+
import { Breadcrumbs } from "../src/handles";
|
|
91
|
+
|
|
92
|
+
afterEach(cleanup);
|
|
93
|
+
|
|
94
|
+
function BreadcrumbTrail() {
|
|
95
|
+
const crumbs = useHandle(Breadcrumbs); // accumulated client-side via the real collect
|
|
96
|
+
return <nav>{crumbs.map((c) => c.label).join(" / ")}</nav>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
it("renders the seeded trail (seed the INPUT pushes, the collect runs)", async () => {
|
|
100
|
+
const { getByText } = await renderRoute(
|
|
101
|
+
[{ path: "/p", Component: BreadcrumbTrail }],
|
|
102
|
+
{
|
|
103
|
+
handles: [
|
|
104
|
+
[
|
|
105
|
+
Breadcrumbs,
|
|
106
|
+
[
|
|
107
|
+
{ label: "Home", href: "/" },
|
|
108
|
+
{ label: "P", href: "/p" },
|
|
109
|
+
],
|
|
110
|
+
],
|
|
111
|
+
], // raw pushes array
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
expect(getByText("Home / P")).toBeTruthy();
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Caveats
|
|
119
|
+
|
|
120
|
+
- `collectHandle` tests the pure collect/accumulator in ISOLATION (parent -> child segment order, empty arrays filtered to match production). It does NOT run the real push -> accumulate -> barrier wiring — that stays e2e.
|
|
121
|
+
- renderRoute `handles` seeds the CLIENT read path with the RAW pushed values array (`unknown[]`), attached to the leaf segment. Handle data accumulates GLOBALLY (not segment-scoped like loaders), so a LAYOUT reading the same handle sees the seeded values too, not just the leaf route.
|
|
122
|
+
- runLoader `handles` seeds the POST-collect ACCUMULATED value (singular `unknown`) a loader reads after `await ctx.rendered()`; pair with `{ rendered: true }`. Shape contrast: renderRoute feeds the barrier INPUT (pushes[]), runLoader feeds its OUTPUT (the accumulated value).
|
|
123
|
+
- The renderRoute path is the CLIENT tree only: it does NOT catch server/client boundary remount bugs, real Flight serialization errors, or loader execution.
|
|
124
|
+
|
|
125
|
+
## See also
|
|
126
|
+
|
|
127
|
+
- `/handler-use`, `/breadcrumbs` — the DSL this tests
|
|
128
|
+
- Siblings: `./loader.md`, `./client-components.md`, `./render-handler.md`
|
|
129
|
+
- Long-form prose: [docs/testing.md](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/testing.md) — section "Testing a handle's collect/accumulator"
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Testing a loader — runLoader
|
|
2
|
+
|
|
3
|
+
**Layer:** unit (node) · **Import:** `@rangojs/router/testing` · **DSL it tests:** `loader()` (see `/loader`)
|
|
4
|
+
|
|
5
|
+
`runLoader` runs a loader against a real `RequestContext` (cookies, headers, `ctx.get`, `ctx.reverse` all resolve) in plain node — that machinery is REAL; what you SEED is the params, env, vars, search, route map, and any `ctx.use` dependency data. Pass a registered `createLoader()` handle (its fn is recovered from the registry) or the raw async body `(ctx) => ...`.
|
|
6
|
+
|
|
7
|
+
## API
|
|
8
|
+
|
|
9
|
+
### Options — `RunLoaderOptions<TEnv>`
|
|
10
|
+
|
|
11
|
+
| Field | Type | Meaning |
|
|
12
|
+
| --------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| `params` | `Record<string, string>` | Route params; surfaced as `ctx.params` and `ctx.routeParams`. |
|
|
14
|
+
| `search` | `Record<string, string>` | Search params; merged into the request URL so `ctx.searchParams` reflects them. |
|
|
15
|
+
| `searchData` | `Record<string, unknown>` | The TYPED `ctx.search` object a route's search schema would produce. Distinct from `search` (which sets the raw `ctx.searchParams`). |
|
|
16
|
+
| `basename` | `string` | Router basename surfaced on the context; drives `redirect()` prefixing. |
|
|
17
|
+
| `theme` | `ThemeConfig \| true` | Theme config in the `createRouter({ theme })` shape (e.g. `true` or `{ themes: [...] }`). Without it `ctx.theme`/`ctx.setTheme` are inert. |
|
|
18
|
+
| `env` | `TEnv` | Environment bindings surfaced as `ctx.env`. |
|
|
19
|
+
| `request` | `Request \| string` | Override the backing Request. Defaults to a localhost GET. |
|
|
20
|
+
| `vars` | `VarsInit` | Variables a prior middleware would have set (object `{ key: value }`, or `[key, value]` tuples where the key may be a `createVar()` handle). |
|
|
21
|
+
| `routeMap` | `Record<string, string>` | Route name -> pattern map enabling `ctx.reverse()`. |
|
|
22
|
+
| `routeName` | `string` | Matched route name for scoped `.name` reverse resolution. |
|
|
23
|
+
| `method` | `string` | HTTP method surfaced as `ctx.method`. Defaults to `"GET"`. |
|
|
24
|
+
| `body` | `unknown` | Request body surfaced as `ctx.body`. |
|
|
25
|
+
| `formData` | `FormData` | Form data surfaced as `ctx.formData` (exposed verbatim; no multipart parsing). |
|
|
26
|
+
| `loaders` | `ReadonlyArray<readonly [LoaderDefinition<any, any>, unknown]>` | Seed `ctx.use(OtherLoader)` by REFERENCE as `[[OtherLoader, data]]` tuples (same shape as `renderHandler`/`renderRoute`). Checked before `use`. |
|
|
27
|
+
| `use` | `UseResolver` | Dynamic resolver for `ctx.use(OtherLoader)` composition. `loaders` wins when both match. |
|
|
28
|
+
| `cacheStore` | `SegmentCacheStore` | Cache store backing `use cache` functions. Without one, a cached function bypasses and runs uncached (its taint/profile guards never fire). |
|
|
29
|
+
| `cacheProfiles` | `Record<string, CacheProfile>` | Cache profiles, the `createRouter({ cacheProfiles })` shape. |
|
|
30
|
+
| `stateCookie` | `StateCookieSeed` (`{ prefix?, routerId?, version? }`) | Customize the rango state cookie a loader calling `invalidateClientCache()` rotates (the name is always seeded — default `rango-state_router_0`). |
|
|
31
|
+
| `rendered` | `boolean \| (() => void \| Promise<void>)` | Mock the `ctx.rendered()` render barrier so a loader that `await ctx.rendered()`s can be unit-tested. By default `ctx.rendered()` throws. `true` resolves immediately; a function controls timing/side effects. |
|
|
32
|
+
| `handles` | `ReadonlyArray<readonly [Handle<any, any>, unknown]>` | Seed the values `ctx.use(SomeHandle)` returns — the ACCUMULATED handle data read after `await ctx.rendered()`. Matched by handle reference. |
|
|
33
|
+
|
|
34
|
+
### Context — `TestLoaderContext<TEnv>` (what your loader receives)
|
|
35
|
+
|
|
36
|
+
| Field | Type | Meaning |
|
|
37
|
+
| ------------------ | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
38
|
+
| `params` | `Record<string, string>` | Route params (from `opts.params`). |
|
|
39
|
+
| `routeParams` | `Record<string, string>` | Same values as `params`. |
|
|
40
|
+
| `request` | `Request` | The backing request. |
|
|
41
|
+
| `searchParams` | `URLSearchParams` | Raw search params (from `opts.search` baked into the URL). |
|
|
42
|
+
| `search` | `Record<string, unknown>` | The TYPED search object (from `opts.searchData`); defaults to `{}`. |
|
|
43
|
+
| `pathname` | `string` | Request pathname. |
|
|
44
|
+
| `url` | `URL` | Request URL. |
|
|
45
|
+
| `originalUrl` | `URL` | Pre-basename-rewrite URL. |
|
|
46
|
+
| `env` | `TEnv` | Environment bindings (from `opts.env`). |
|
|
47
|
+
| `get` | `<T>(contextVar: ContextVar<T>) => T \| undefined` / `<T>(key: string) => T \| undefined` | Read a var seeded via `opts.vars` (by `createVar()` handle or string key). |
|
|
48
|
+
| `use` | `(dep) => ...` | Resolve `ctx.use(OtherLoader)`/`ctx.use(SomeHandle)`: handle seeds first, then loader seeds, then the `use` resolver, then the real context `use()`. |
|
|
49
|
+
| `method` | `string` | HTTP method (from `opts.method`, default `"GET"`). |
|
|
50
|
+
| `body` | `unknown` | Request body (from `opts.body`). |
|
|
51
|
+
| `formData` | `FormData \| undefined` | Form data (from `opts.formData`). |
|
|
52
|
+
| `reverse` | `(name, params?, search?) => string` | Build a URL; throws unless `opts.routeMap` was passed. |
|
|
53
|
+
| `rendered` | `() => Promise<void>` | The render barrier; throws by default, mocked via `opts.rendered`. |
|
|
54
|
+
| `waitUntil` | `(p: Promise<unknown>) => void` | Register background work (no-op accounting in tests). |
|
|
55
|
+
| `executionContext` | `ExecutionContext \| undefined` | Platform execution context from the backing request; pairs with `waitUntil`. |
|
|
56
|
+
|
|
57
|
+
### Returns — `Promise<T>`
|
|
58
|
+
|
|
59
|
+
The loader data DIRECTLY (no envelope). `T` is the loader's return type.
|
|
60
|
+
|
|
61
|
+
To assert a loader's EFFECTS — a `Set-Cookie`, a response header, or a
|
|
62
|
+
`throw redirect(...)` (the auth-loader pattern) — use the sibling
|
|
63
|
+
**`runLoaderResult(loader, opts)`** instead. Same options, but it returns an
|
|
64
|
+
envelope: `{ result, thrown, response, cookies, headers, locationState, stateCookieName }`
|
|
65
|
+
(parity with `runInRequestContext`; `result` is the loader's data). `runLoader`
|
|
66
|
+
discards those effects.
|
|
67
|
+
|
|
68
|
+
## Recipe
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { runLoader, runLoaderResult } from "@rangojs/router/testing";
|
|
72
|
+
import { createLoader, createVar } from "@rangojs/router";
|
|
73
|
+
|
|
74
|
+
const User = createVar<{ name: string }>();
|
|
75
|
+
// The registered loader — no separate body export needed for testability:
|
|
76
|
+
const ProductLoader = createLoader(async (ctx) => ({
|
|
77
|
+
id: ctx.params.id,
|
|
78
|
+
region: ctx.env.REGION,
|
|
79
|
+
user: ctx.get(User),
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
it("reads params, env, and seeded vars", async () => {
|
|
83
|
+
const data = await runLoader(ProductLoader, {
|
|
84
|
+
params: { id: "42" },
|
|
85
|
+
env: { REGION: "eu" },
|
|
86
|
+
vars: [[User, { name: "Ada" }]],
|
|
87
|
+
});
|
|
88
|
+
expect(data).toEqual({ id: "42", region: "eu", user: { name: "Ada" } });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("builds a self link via reverse", async () => {
|
|
92
|
+
// runLoader(async (ctx) => ({ ... }), opts) — the bare body — works identically.
|
|
93
|
+
const data = await runLoader(
|
|
94
|
+
async (ctx) => ({ self: ctx.reverse("product", { id: ctx.params.id }) }),
|
|
95
|
+
{ params: { id: "42" }, routeMap: { product: "/products/:id" } },
|
|
96
|
+
);
|
|
97
|
+
expect(data.self).toBe("/products/42");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("asserts a loader's set-cookie + redirect (runLoaderResult)", async () => {
|
|
101
|
+
// AuthLoader validates, sets a `session` cookie, then `throw redirect("/")`.
|
|
102
|
+
const { thrown, response, cookies } = await runLoaderResult(AuthLoader, {
|
|
103
|
+
request: new Request("https://app.test/login?token=ok"),
|
|
104
|
+
});
|
|
105
|
+
expect((thrown as Response).headers.get("Location")).toBe("/");
|
|
106
|
+
expect(cookies.session).toBeDefined();
|
|
107
|
+
expect(
|
|
108
|
+
response.headers.getSetCookie().some((c) => c.startsWith("session=")),
|
|
109
|
+
).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Caveats
|
|
114
|
+
|
|
115
|
+
- `ctx.reverse(...)` throws unless you pass `routeMap` (and `routeName` for scoped `.name` resolution). It does NOT fall back to the global route map.
|
|
116
|
+
- `ctx.rendered()` throws by default (the render barrier only exists in a full match); pass `{ rendered: true }` to mock it for post-barrier logic, and `{ handles: [[SomeHandle, data]] }` to seed `ctx.use(SomeHandle)`. `ctx.isAction(...)` is unavailable — cover those at e2e.
|
|
117
|
+
- Seeded `loaders` (by-reference tuples) are NOT executed — `ctx.use(OtherLoader)` returns the seeded value. The dynamic `use` resolver, by contrast, IS executed (it is a function called to compute the value). Either way the REAL loader body is not run; real loader execution and side-effects are e2e-only. `loaders` is checked before the `use` resolver.
|
|
118
|
+
- A handle imported through the CLIENT build has its body dropped — `runLoader` throws a clear error pointing to the `rangoTestConfig()` preset or the raw body. A router using `Prerender()`/`createLoader()`/`Static()` now constructs in a bare test (each assigns a runtime fallback `$$id`); only the whole router _file_ may still need the plugin (its page modules pull app deps / `virtual:` modules).
|
|
119
|
+
- No `cookies`/`headers` option: seed a cookie by passing a full Request with a Cookie header — `{ request: new Request(url, { headers: { Cookie: "sid=abc" } }) }`. (`search`/`method` are baked onto this request for you.)
|
|
120
|
+
- `ctx.search` (typed) defaults to `{}`; `opts.search` only sets the raw `ctx.searchParams`. Seed the typed object with `searchData`. (The harness seeds `searchData` verbatim — it does NOT run a typed-search SCHEMA, so schema parsing/validation is e2e.)
|
|
121
|
+
- `ctx.theme`/`ctx.setTheme` are NOT on the loader context — theme accessors are handler-only. (The `theme` option seeds the underlying request context for `use cache` theme resolution, but a loader body cannot read theme.) `redirect()` does no basename prefixing unless you seed `basename`.
|
|
122
|
+
- Platform bindings are yours to double via `env` (see `./bindings.md`).
|
|
123
|
+
|
|
124
|
+
## See also
|
|
125
|
+
|
|
126
|
+
- `/loader` — the DSL this tests
|
|
127
|
+
- Siblings: `./handles.md`, `./reverse-and-types.md`, `./bindings.md`, `./server-actions.md`
|
|
128
|
+
- Long-form prose: [docs/testing.md](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/testing.md) — section "Loaders — the raw body or a registered createLoader"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Testing middleware — runMiddleware
|
|
2
|
+
|
|
3
|
+
**Layer:** unit (node) · **Import:** `@rangojs/router/testing` · **DSL it tests:** `middleware()` (see `/middleware`)
|
|
4
|
+
|
|
5
|
+
`runMiddleware` executes your chain through the router's REAL `executeLoaderMiddleware`, so `next()`, return-Response and throw-Response short-circuits, double-next guards, and header/cookie merge are production-identical. You SEED the request and any prior-middleware state (`vars`, `params`, `env`, `routeMap`); everything else (cookie/header merge, request-context resolution) is real machinery.
|
|
6
|
+
|
|
7
|
+
## API
|
|
8
|
+
|
|
9
|
+
### Options — `RunMiddlewareOptions<TEnv>`
|
|
10
|
+
|
|
11
|
+
| Field | Type | Meaning |
|
|
12
|
+
| --------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| `request` | `Request \| string` | The request the chain runs under: a `Request`, or a URL string (absolute or path). Optional — defaults to `http://localhost/`; pass it for path-, header-, or cookie-driven middleware. |
|
|
14
|
+
| `env` | `TEnv` | Environment bindings surfaced as `ctx.env`. Your seam for doubling platform bindings (see `./bindings.md`). |
|
|
15
|
+
| `params` | `Record<string, string>` | Route params surfaced as `ctx.params`. |
|
|
16
|
+
| `vars` | `VarsInit` | Variables a prior middleware would have set (object form, or `[key, value]` tuples where `key` may be a `createVar()` handle). |
|
|
17
|
+
| `routeMap` | `Record<string, string>` | Route name -> pattern map enabling `ctx.reverse()`. |
|
|
18
|
+
| `routeName` | `string` | Matched route name surfaced as `ctx.routeName`. Does NOT enable scoped `.name` reverse: the chain's `reverse` is deliberately map-only, matching production app/response middleware. |
|
|
19
|
+
| `basename` | `string` | Router basename surfaced on the context (drives `redirect()` prefixing). |
|
|
20
|
+
| `theme` | `ThemeConfig \| true` | Theme config in the `createRouter({ theme })` shape; enables `ctx.theme`. |
|
|
21
|
+
| `next` | `() => Promise<Response>` | Terminal handler invoked when the chain calls `next()` all the way through. Defaults to a 200 empty Response. Use it to model the downstream route/handler response. |
|
|
22
|
+
| `cacheStore` | `SegmentCacheStore` | Cache store backing any `use cache` function a middleware invokes. Without it, `registerCachedFunction` bypasses, so the cached fn runs uncached and its taint/profile guards never fire. |
|
|
23
|
+
| `cacheProfiles` | `Record<string, CacheProfile>` | Cache profiles in the `createRouter({ cacheProfiles })` shape. |
|
|
24
|
+
| `stateCookie` | `StateCookieSeed` (`{ prefix?, routerId?, version? }`) | Customize the rango state cookie a middleware calling `invalidateClientCache()` rotates (the name is always seeded — default `rango-state_router_0`). Assert via the `Set-Cookie` on `result.response` / `result.cookies`, or against `result.stateCookieName` (without recomputing). |
|
|
25
|
+
|
|
26
|
+
### Context — `MiddlewareContext` (what your code receives)
|
|
27
|
+
|
|
28
|
+
The `ctx` your middleware reads. Notable fields:
|
|
29
|
+
|
|
30
|
+
| Field | Type | Meaning |
|
|
31
|
+
| --------------------------- | ----------------------- | ---------------------------------------------------------------------------------- |
|
|
32
|
+
| `params` | `TParams` | URL params from `opts.params`. |
|
|
33
|
+
| `env` | `TEnv` | Bindings from `opts.env`. |
|
|
34
|
+
| `get` / `set` | fns | Read/write context vars (shared with handlers); `get` resolves what `vars` seeded. |
|
|
35
|
+
| `header(name, value)` | fn | Queue a response header before `next()`, or set it directly after. |
|
|
36
|
+
| `reverse` | `ScopedReverseFunction` | URL-from-name. Map-only (no auto-fill); needs `routeMap`. |
|
|
37
|
+
| `setLocationState(entries)` | fn | Attach flash/location state to the response. |
|
|
38
|
+
| `theme` / `setTheme` | `Theme` / fn | Current theme; `undefined` unless `theme` is passed. |
|
|
39
|
+
| `routeName` | `string` | Matched route name (from `opts.routeName`). |
|
|
40
|
+
|
|
41
|
+
### Returns — `RunMiddlewareResult<TEnv>`
|
|
42
|
+
|
|
43
|
+
| Field | Type | Meaning |
|
|
44
|
+
| ----------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
45
|
+
| `response` | `Response` | The final Response: the downstream response, or a middleware short-circuit. |
|
|
46
|
+
| `ctx` | `RequestContext<TEnv>` | The underlying RequestContext (NOT a per-middleware `MiddlewareContext`). Use `ctx.get(...)` for anything the envelope above doesn't surface. |
|
|
47
|
+
| `nextCalled` | `number` | Times the terminal handler ran: `0` on short-circuit, `1` on pass-through. |
|
|
48
|
+
| `cookies` | `Record<string, string>` | Effective cookie view: request cookies merged with chain sets/deletes (last-write-wins), as `{ name: value }`. |
|
|
49
|
+
| `headers` | `Record<string, string>` | Final response headers as `{ name: value }`, lowercased, EXCLUDING `set-cookie` (use `cookies`). |
|
|
50
|
+
| `locationState` | `Record<string, unknown>` | Flat `{ key: value }` state set via `setLocationState()` / `redirect({ state })` (empty when none). |
|
|
51
|
+
| `stateCookieName` | `string` | The resolved rango state cookie name seeded for the run (default `rango-state_router_0`). Assert a middleware's `invalidateClientCache()` rotation against it without recomputing — parity with `runInRequestContext` / `runLoaderResult` / `renderHandler`. |
|
|
52
|
+
|
|
53
|
+
## Recipe
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { describe, it, expect } from "vitest";
|
|
57
|
+
import { runMiddleware } from "@rangojs/router/testing";
|
|
58
|
+
import type { Middleware } from "@rangojs/router";
|
|
59
|
+
|
|
60
|
+
const requireUser: Middleware = async (ctx, next) => {
|
|
61
|
+
if (!ctx.get("user")) return new Response(null, { status: 401 });
|
|
62
|
+
return next();
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
describe("requireUser", () => {
|
|
66
|
+
it("passes through when the user is present", async () => {
|
|
67
|
+
const { response, nextCalled } = await runMiddleware(requireUser, {
|
|
68
|
+
request: "/dashboard",
|
|
69
|
+
vars: { user: { id: 1 } }, // object form; or [[key, value]] tuples (key may be a createVar())
|
|
70
|
+
});
|
|
71
|
+
expect(nextCalled).toBe(1);
|
|
72
|
+
expect(response.status).toBe(200);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("short-circuits (return OR throw Response) when unauthenticated", async () => {
|
|
76
|
+
const { response, nextCalled } = await runMiddleware(requireUser, {
|
|
77
|
+
request: "/dashboard",
|
|
78
|
+
});
|
|
79
|
+
expect(nextCalled).toBe(0);
|
|
80
|
+
expect(response.status).toBe(401);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Pass an array to run several in order. Cookies set inside middleware via the standalone `cookies().set(...)` (imported from `@rangojs/router`, NOT a `ctx` method) surface on the result's `cookies` and on the merged response `Set-Cookie`.
|
|
86
|
+
|
|
87
|
+
## Caveats
|
|
88
|
+
|
|
89
|
+
- No `handles`/`rendered` option by design: middleware runs BEFORE the render barrier, so it has no post-barrier `ctx.use(Handle)` access in production. Read handle data in a loader/handler and test it with `runLoader` (see `./handles.md`).
|
|
90
|
+
- A COMPONENT route's guard stack cannot be exercised through `dispatch` (it throws on component routes), and `renderToFlightString`/`renderRoute` don't run route middleware. Extract the middleware fn and unit-test it here, or assert the guard stack at e2e.
|
|
91
|
+
- Middleware-phase `ctx.reverse` is map-only (no auto-fill from current params), matching production — enable it with `routeMap`. `routeName` only feeds `ctx.routeName`; it does NOT scope `.name` reverse (the chain reverse stays map-only by design).
|
|
92
|
+
- `ctx.theme` is `undefined` unless `theme` is passed; `redirect()` does no basename prefixing unless `basename` is seeded.
|
|
93
|
+
- Platform bindings are yours to double via `env` (see `./bindings.md`).
|
|
94
|
+
|
|
95
|
+
## See also
|
|
96
|
+
|
|
97
|
+
- `/middleware` — the DSL this tests
|
|
98
|
+
- Siblings: `./response-routes.md`, `./server-actions.md`, `./loader.md`, `./bindings.md`
|
|
99
|
+
- Long-form prose: [docs/testing.md](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/testing.md) — section "Middleware"
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Testing a route handler — renderHandler
|
|
2
|
+
|
|
3
|
+
**Layer:** RSC unit (react-server project) · **Import:** `@rangojs/router/testing/flight` · **DSL it tests:** a route handler `(ctx) => rsc` (see `/route`)
|
|
4
|
+
|
|
5
|
+
A Rango route handler is a pure function `(ctx) => rsc` — the function you pass to `path("/p/:slug", ProductPage)`, NOT a component. `renderHandler` runs it with the REAL `HandlerContext` the router builds at runtime (so `ctx.params`, `ctx.use(Loader)`, `ctx.use(Meta)`, `ctx.reverse`, `ctx.get`, response headers via `ctx.headers`, and the standalone `cookies()` all work), serializes the returned RSC, and deserializes it to an inspectable tree. The render and effects are real; loaders are SEEDED (no real loader runs — same model as `runLoader`).
|
|
6
|
+
|
|
7
|
+
## API
|
|
8
|
+
|
|
9
|
+
### Options — `RenderHandlerOptions`
|
|
10
|
+
|
|
11
|
+
| Field | Type | Meaning |
|
|
12
|
+
| ------------------ | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
13
|
+
| `params` | `Record<string, string>` | Route params surfaced as `ctx.params`. |
|
|
14
|
+
| `env` | `TEnv` | Environment bindings surfaced as `ctx.env`. |
|
|
15
|
+
| `request` | `Request \| string` | Backing Request (string or `Request`); defaults to a localhost GET. |
|
|
16
|
+
| `headers` | `HeadersInit` | Request headers (e.g. `Cookie`) the handler reads via `cookies()`. |
|
|
17
|
+
| `vars` | `VarsInit` (object or `[[Var, value]]` tuples) | Variables a prior middleware set, read via `ctx.get(...)`. |
|
|
18
|
+
| `routeName` | `string` | Matched route name (drives `ctx.routeName` and scoped reverse). |
|
|
19
|
+
| `routeMap` | `Record<string, string>` | Route name -> pattern map enabling `ctx.reverse()`. |
|
|
20
|
+
| `loaders` | `ReadonlyArray<readonly [LoaderDefinition, unknown]>` | Seed the data `ctx.use(SomeLoader)` returns. Matched by loader reference; NO real loader runs. |
|
|
21
|
+
| `clientComponents` | `Record<string, unknown>` | `"use client"` components in the handler's RSC, so they serialize as real boundaries when `rangoUseClientTransform()` is not wired. Keyed by name. |
|
|
22
|
+
| `stateCookie` | `StateCookieSeed` (`{ prefix?, routerId?, version? }`) | Customize the rango state cookie a handler calling `invalidateClientCache()` rotates. The name is ALWAYS seeded (default `rango-state_router_0`) so the rotation `Set-Cookie` fires like production rather than no-opping; override `prefix`/`routerId` to match your `createRouter({ stateCookiePrefix, id })`, or `version` (the value is `{version}:{timestamp}`, default `"0"`). |
|
|
23
|
+
| `cacheStore` | `SegmentCacheStore` | Segment cache store backing a `"use cache"` function the handler invokes (e.g. `new MemorySegmentCacheStore()`). WITHOUT it, `registerCachedFunction` takes the uncached bypass and the cached path is NOT exercised (the runtime emits a one-time warning under the test runner). Pair with `cacheProfiles`. |
|
|
24
|
+
| `cacheProfiles` | `Record<string, CacheProfile>` | Cache profiles in the `createRouter({ cacheProfiles })` shape, required for `"use cache: profileName"` resolution once a `cacheStore` is wired. |
|
|
25
|
+
|
|
26
|
+
### Context — `HandlerContext` (what your handler receives)
|
|
27
|
+
|
|
28
|
+
| Field | Type | Meaning |
|
|
29
|
+
| ------------------ | ------------------------------------ | ---------------------------------------------------------------------------------------------- |
|
|
30
|
+
| `params` | `Record<string, string>` | The seeded route params. |
|
|
31
|
+
| `env` | `TEnv` | The seeded environment bindings. |
|
|
32
|
+
| `request` | `Request` | The backing request. |
|
|
33
|
+
| `searchParams` | `URLSearchParams` | Parsed query of `request.url`. |
|
|
34
|
+
| `pathname` | `string` | Pathname of `request.url`. |
|
|
35
|
+
| `url` | `URL` | Parsed `request.url`. |
|
|
36
|
+
| `routeName` | `string \| undefined` | The matched route name (from `routeName`). |
|
|
37
|
+
| `use` | `(loaderOrHandle) => data \| pushFn` | A loader returns its seeded data; a handle returns a push fn that RECORDS to `result.handles`. |
|
|
38
|
+
| `reverse` | `(name, params?) => string` | Build a URL from `routeMap`. |
|
|
39
|
+
| `get` | `(Var) => value` | Read a seeded `vars` variable. |
|
|
40
|
+
| `headers` | `Headers` | Response headers; set via `ctx.headers.set(...)` (merged into `result.response`). |
|
|
41
|
+
| `setLocationState` | `(entries) => void` | Set location state (surfaced on `result.locationState`). |
|
|
42
|
+
| `waitUntil` | `(promise) => void` | Register background work. |
|
|
43
|
+
|
|
44
|
+
### Returns — `RenderHandlerResult`
|
|
45
|
+
|
|
46
|
+
| Field | Type | Meaning |
|
|
47
|
+
| ----------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
48
|
+
| `tree` | `unknown` | Deserialized RSC the handler returned; `undefined` when it returned/threw a `Response`. Inspect with `findClientBoundaries`. |
|
|
49
|
+
| `flight` | `string \| undefined` | Raw Flight wire string; `undefined` on a `Response`. |
|
|
50
|
+
| `thrown` | `unknown` | The value the handler THREW (a `redirect()`/`notFound()` Response), captured not re-thrown. |
|
|
51
|
+
| `response` | `Response` | Merged Response (status + headers + Set-Cookie), folding a thrown/returned redirect with accumulated effects. |
|
|
52
|
+
| `cookies` | `Record<string, string>` | Effective cookie view after the handler ran. |
|
|
53
|
+
| `headers` | `Record<string, string>` | Response headers (excludes set-cookie; includes a redirect `Location`). The `keepClientCache()` directive shows here as `x-rango-keep-cache: "1"`. |
|
|
54
|
+
| `stateCookieName` | `string` | The resolved rango state cookie name this run seeded (default `rango-state_router_0`). Assert an `invalidateClientCache()` rotation against it without recomputing. |
|
|
55
|
+
| `locationState` | `Record<string, unknown>` | Location state the handler set (`ctx.setLocationState`/`redirect({ state })`). |
|
|
56
|
+
| `handles` | `Map<Handle, unknown[]>` | What the handler pushed via `ctx.use(Handle)(...)` (e.g. `Meta`, `Breadcrumbs`), keyed by handle. |
|
|
57
|
+
|
|
58
|
+
## Recipe
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import {
|
|
62
|
+
renderHandler,
|
|
63
|
+
findClientBoundaries,
|
|
64
|
+
} from "@rangojs/router/testing/flight";
|
|
65
|
+
import { ProductPage } from "../src/pages/product"; // the real handler: (ctx) => rsc
|
|
66
|
+
import { ProductLoader } from "../src/loaders/product";
|
|
67
|
+
import { Tenant } from "../src/middleware/tenant";
|
|
68
|
+
import { Meta } from "../src/handles";
|
|
69
|
+
|
|
70
|
+
it("renders the product page for a tenant", async () => {
|
|
71
|
+
const { tree, handles } = await renderHandler(ProductPage, {
|
|
72
|
+
params: { slug: "wine" },
|
|
73
|
+
loaders: [[ProductLoader, { name: "Wine", price: 9 }]], // seeds ctx.use(ProductLoader)
|
|
74
|
+
vars: [[Tenant, { name: "Acme" }]], // seeds ctx.get(Tenant)
|
|
75
|
+
routeMap: { product: "/p/:slug" }, // enables ctx.reverse
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(JSON.stringify(tree)).toContain("Wine");
|
|
79
|
+
const [counter] = findClientBoundaries(tree, "Counter"); // islands inspectable too
|
|
80
|
+
expect(handles.get(Meta)).toEqual([{ title: "Wine - Shop" }]); // ctx.use(Meta) pushes
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("captures a guarded redirect", async () => {
|
|
84
|
+
const { thrown, response } = await renderHandler(ProductPage, {
|
|
85
|
+
params: { slug: "missing" },
|
|
86
|
+
loaders: [[ProductLoader, null]],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(thrown).toBeInstanceOf(Response); // throw redirect() is captured, not re-thrown
|
|
90
|
+
expect(response.status).toBe(302);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("asserts the client-cache directives", async () => {
|
|
94
|
+
// invalidateClientCache() rotates the state cookie -> a Set-Cookie on response.
|
|
95
|
+
const { response, stateCookieName } = await renderHandler(LogoutPage);
|
|
96
|
+
expect(
|
|
97
|
+
response.headers
|
|
98
|
+
.getSetCookie()
|
|
99
|
+
.some((c) => c.startsWith(stateCookieName + "=")),
|
|
100
|
+
).toBe(true);
|
|
101
|
+
|
|
102
|
+
// keepClientCache() sets the suppression directive header (no cookie).
|
|
103
|
+
const { headers } = await renderHandler(QuietPage);
|
|
104
|
+
expect(headers["x-rango-keep-cache"]).toBe("1");
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Caveats
|
|
109
|
+
|
|
110
|
+
- An unseeded `ctx.use(loader)` REJECTS with a setup error — seed every dependency via `{ loaders: [[OtherLoader, data]] }`, matched by reference. Loaders are SEEDED, not executed (same as `runLoader`).
|
|
111
|
+
- Same alias requirement as flight tests: without the `@rangojs/router -> index.rsc.ts` alias (see [`./setup.md`](./setup.md)), a handler reading `getRequestContext()`/`cookies()` hits the throwing out-of-react-server stub. Symptom: `tree: undefined` with the stub error on `thrown`.
|
|
112
|
+
- A `throw redirect()` is captured on `thrown` (with `tree` undefined, since it produced a `Response`) — assert on `thrown`/`response`, no try/catch needed.
|
|
113
|
+
- No hydration and no interaction — for clicks, forms, and navigation use e2e.
|
|
114
|
+
- `renderHandler` runs a handler FUNCTION `(ctx) => rsc`; for a plain ELEMENT `<Page/>` use `renderServerTree` (see [`./server-tree.md`](./server-tree.md)).
|
|
115
|
+
- A handler that calls a `"use cache"` function runs UNCACHED unless you seed `cacheStore` (and `cacheProfiles` for a named profile). With nothing seeded the runtime bypasses to the live body and warns once under the test runner — assert real cache behavior by passing `{ cacheStore: new MemorySegmentCacheStore(), cacheProfiles: { default: { ttl: 60 } } }`.
|
|
116
|
+
|
|
117
|
+
## See also
|
|
118
|
+
|
|
119
|
+
- `/route` — the DSL this tests
|
|
120
|
+
- Siblings: [`./server-tree.md`](./server-tree.md), [`./server-actions.md`](./server-actions.md), [`./setup.md`](./setup.md), [`./loader.md`](./loader.md)
|
|
121
|
+
- Long-form prose: [docs/testing.md](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/testing.md) — section "renderHandler — run a real route handler and assert its RSC"
|