@rangojs/router 0.5.2 → 0.6.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/dist/bin/rango.js +343 -125
- package/dist/types/browser/react/use-router.d.ts +10 -3
- package/dist/types/browser/react/use-search-params.d.ts +57 -10
- package/dist/types/browser/types.d.ts +22 -0
- package/dist/types/build/merge-full-manifests.d.ts +3 -0
- package/dist/types/build/route-trie.d.ts +4 -73
- package/dist/types/build/route-types/per-module-writer.d.ts +6 -4
- package/dist/types/build/route-types/router-processing.d.ts +2 -3
- package/dist/types/cache/cache-exec-scope.d.ts +31 -0
- package/dist/types/cache/taint.d.ts +12 -6
- package/dist/types/client-urls/client-root.d.ts +38 -0
- package/dist/types/client-urls/client-urls.d.ts +5 -0
- package/dist/types/client-urls/navigation.d.ts +38 -0
- package/dist/types/client-urls/revalidation-protocol.d.ts +25 -0
- package/dist/types/client-urls/server-projection.d.ts +62 -0
- package/dist/types/client-urls/types.d.ts +144 -0
- package/dist/types/client.d.ts +12 -4
- package/dist/types/client.rsc.d.ts +4 -1
- package/dist/types/decode-loader-results.d.ts +37 -0
- package/dist/types/errors.d.ts +1 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/loader-redirect.d.ts +27 -0
- package/dist/types/outlet-context.d.ts +12 -0
- package/dist/types/outlet-provider.d.ts +3 -1
- package/dist/types/redirect-origin.d.ts +4 -0
- package/dist/types/route-content-wrapper.d.ts +42 -1
- package/dist/types/route-definition/helpers-types.d.ts +13 -2
- package/dist/types/router/error-handling.d.ts +35 -1
- package/dist/types/router/intercept-resolution.d.ts +12 -0
- package/dist/types/router/loader-resolution.d.ts +24 -2
- package/dist/types/router/revalidation.d.ts +7 -0
- package/dist/types/router/route-trie-builder.d.ts +77 -0
- package/dist/types/router/router-interfaces.d.ts +20 -0
- package/dist/types/router/segment-resolution/helpers.d.ts +1 -1
- package/dist/types/router/trie-matching.d.ts +1 -1
- package/dist/types/rsc/manifest-init.d.ts +5 -5
- package/dist/types/rsc/shell-capture.d.ts +9 -0
- package/dist/types/rsc/shell-serve.d.ts +11 -0
- package/dist/types/rsc/types.d.ts +30 -0
- package/dist/types/segment-system.d.ts +2 -0
- package/dist/types/server/context.d.ts +10 -0
- package/dist/types/server/handle-store.d.ts +34 -3
- package/dist/types/server/request-context.d.ts +11 -1
- package/dist/types/server.d.ts +1 -0
- package/dist/types/ssr/index.d.ts +22 -0
- package/dist/types/ssr/ssr-root.d.ts +10 -0
- package/dist/types/testing/dom.entry.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +16 -6
- package/dist/types/testing/run-loader.d.ts +9 -0
- package/dist/types/types/boundaries.d.ts +22 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/loader-types.d.ts +57 -5
- package/dist/types/types/segments.d.ts +7 -0
- package/dist/types/urls/path-helper-types.d.ts +10 -4
- package/dist/types/vite/discovery/client-urls-projection.d.ts +53 -0
- package/dist/types/vite/discovery/discover-routers.d.ts +1 -1
- package/dist/types/vite/discovery/state.d.ts +8 -1
- package/dist/vite/index.js +5313 -2365
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +39 -9
- package/skills/catalog.json +7 -1
- package/skills/client-urls/SKILL.md +338 -0
- package/skills/comparison/references/framework-comparison.md +23 -9
- package/skills/hooks/SKILL.md +2 -2
- package/skills/hooks/data.md +11 -2
- package/skills/hooks/handle-and-actions.md +7 -0
- package/skills/hooks/outlets.md +26 -5
- package/skills/hooks/urls.md +40 -3
- package/skills/loader/SKILL.md +132 -20
- package/skills/migrate-nextjs/SKILL.md +70 -10
- package/skills/migrate-react-router/SKILL.md +49 -13
- package/skills/migrate-react-router/component-migration.md +18 -13
- package/skills/migrate-react-router/data-and-actions.md +14 -3
- package/skills/migrate-react-router/route-mapping.md +15 -2
- package/skills/parallel/SKILL.md +32 -1
- package/skills/ppr/SKILL.md +16 -6
- package/skills/prerender/SKILL.md +8 -4
- package/skills/rango/SKILL.md +21 -17
- package/skills/react-compiler/SKILL.md +3 -3
- package/skills/route/SKILL.md +5 -2
- package/skills/router-setup/SKILL.md +16 -2
- package/skills/scripts/SKILL.md +16 -6
- package/skills/shell-manifest/SKILL.md +16 -7
- package/skills/testing/SKILL.md +2 -2
- package/skills/testing/client-components.md +6 -0
- package/skills/testing/handles.md +30 -8
- package/skills/testing/loader.md +51 -49
- package/skills/testing/middleware.md +1 -1
- package/skills/theme/SKILL.md +8 -5
- package/src/bin/rango.ts +7 -3
- package/src/browser/navigation-bridge.ts +6 -0
- package/src/browser/navigation-client.ts +5 -0
- package/src/browser/partial-update.ts +65 -13
- package/src/browser/react/use-router.ts +40 -11
- package/src/browser/react/use-search-params.ts +140 -17
- package/src/browser/rsc-router.tsx +59 -0
- package/src/browser/server-action-bridge.ts +26 -0
- package/src/browser/types.ts +22 -0
- package/src/build/merge-full-manifests.ts +161 -0
- package/src/build/route-trie.ts +9 -332
- package/src/build/route-types/include-resolution.ts +66 -11
- package/src/build/route-types/per-module-writer.ts +11 -6
- package/src/build/route-types/router-processing.ts +184 -153
- package/src/build/runtime-discovery.ts +23 -12
- package/src/cache/cache-exec-scope.ts +47 -0
- package/src/cache/cache-runtime.ts +24 -25
- package/src/cache/taint.ts +28 -9
- package/src/client-urls/client-root.tsx +168 -0
- package/src/client-urls/client-urls.ts +698 -0
- package/src/client-urls/navigation.ts +237 -0
- package/src/client-urls/revalidation-protocol.ts +56 -0
- package/src/client-urls/server-projection.ts +579 -0
- package/src/client-urls/types.ts +195 -0
- package/src/client.rsc.tsx +12 -0
- package/src/client.tsx +49 -6
- package/src/decode-loader-results.ts +113 -0
- package/src/errors.ts +14 -0
- package/src/handles/deferred-resolution.ts +14 -7
- package/src/index.ts +1 -0
- package/src/loader-redirect.tsx +64 -0
- package/src/outlet-context.ts +12 -0
- package/src/outlet-provider.tsx +15 -1
- package/src/redirect-origin.ts +29 -0
- package/src/route-content-wrapper.tsx +96 -3
- package/src/route-definition/dsl-helpers.ts +28 -3
- package/src/route-definition/helpers-types.ts +13 -0
- package/src/route-definition/redirect.ts +17 -18
- package/src/router/error-handling.ts +65 -11
- package/src/router/intercept-resolution.ts +29 -0
- package/src/router/loader-resolution.ts +261 -28
- package/src/router/match-result.ts +7 -0
- package/src/router/revalidation.ts +24 -11
- package/src/router/route-trie-builder.ts +334 -0
- package/src/router/router-interfaces.ts +38 -0
- package/src/router/segment-resolution/fresh.ts +47 -0
- package/src/router/segment-resolution/helpers.ts +9 -11
- package/src/router/segment-resolution/loader-cache.ts +14 -24
- package/src/router/segment-resolution/revalidation.ts +20 -1
- package/src/router/trie-matching.ts +3 -3
- package/src/router.ts +46 -1
- package/src/rsc/full-payload.ts +6 -0
- package/src/rsc/handler.ts +10 -7
- package/src/rsc/loader-fetch.ts +2 -2
- package/src/rsc/manifest-init.ts +28 -9
- package/src/rsc/rsc-rendering.ts +15 -1
- package/src/rsc/shell-capture.ts +12 -0
- package/src/rsc/shell-serve.ts +15 -2
- package/src/rsc/ssr-setup.ts +10 -1
- package/src/rsc/types.ts +31 -2
- package/src/segment-system.tsx +83 -26
- package/src/server/context.ts +10 -0
- package/src/server/cookie-store.ts +19 -19
- package/src/server/handle-store.ts +185 -48
- package/src/server/request-context.ts +30 -6
- package/src/server.ts +7 -0
- package/src/ssr/index.tsx +37 -2
- package/src/ssr/ssr-root.tsx +29 -2
- package/src/testing/dom.entry.ts +1 -1
- package/src/testing/render-route.tsx +22 -8
- package/src/testing/run-loader.ts +51 -13
- package/src/types/boundaries.ts +19 -0
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +60 -5
- package/src/types/segments.ts +7 -0
- package/src/urls/include-helper.ts +22 -4
- package/src/urls/path-helper-types.ts +14 -1
- package/src/use-loader.tsx +67 -6
- package/src/vite/discovery/client-urls-projection.ts +322 -0
- package/src/vite/discovery/discover-routers.ts +43 -17
- package/src/vite/discovery/state.ts +11 -1
- package/src/vite/discovery/virtual-module-codegen.ts +20 -0
- package/src/vite/plugins/virtual-entries.ts +12 -3
- package/src/vite/router-discovery.ts +163 -12
package/skills/ppr/SKILL.md
CHANGED
|
@@ -121,9 +121,9 @@ At capture the pending fetch cannot win the task-quantized quiet window, so
|
|
|
121
121
|
the boundary postpones — fallback in the frozen prelude, value resumed fresh
|
|
122
122
|
on every HIT. This is the PHYSICS class from the hole doctrine below, and it
|
|
123
123
|
is exactly how an existing Suspense-shaped tree (e.g. migrated from Next.js
|
|
124
|
-
PPR) works with zero restructuring. The e2e proof
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
PPR) works with zero restructuring. The e2e proof lives in the router
|
|
125
|
+
repository (not shipped in this package): a promise hole living in a LAYOUT
|
|
126
|
+
with no loader registration at all.
|
|
127
127
|
|
|
128
128
|
A route WITHOUT the `ppr` option is pure axis 1: no store read, no capture, no
|
|
129
129
|
logs, zero cost. `ppr` is per page route — declaring it on a layout is not
|
|
@@ -658,9 +658,19 @@ Three levers, in preference order:
|
|
|
658
658
|
lane — masked at capture, fresh per serve — at the cost of a fallback in
|
|
659
659
|
the shell.
|
|
660
660
|
|
|
661
|
-
Head material (Meta)
|
|
662
|
-
promises bake by design; make them cheap with lever 2. `bakeWaitMs`
|
|
663
|
-
capture debug event tells you what each capture actually paid.
|
|
661
|
+
Head material (Meta) pushed by HANDLERS cannot be a hole — the head is shell —
|
|
662
|
+
so those promises bake by design; make them cheap with lever 2. `bakeWaitMs`
|
|
663
|
+
on the capture debug event tells you what each capture actually paid. A
|
|
664
|
+
LOADER-pushed Meta is different: loaders are masked at capture, so the push
|
|
665
|
+
happens at request time and applies client-side (`metadata.handlesLate`) — it
|
|
666
|
+
is never in the cached shell's head, by construction.
|
|
667
|
+
|
|
668
|
+
One flag to know about here: `loader(Def, { stream: "navigation" })` (the
|
|
669
|
+
document-render await, `/loader`) is **inert under PPR** — capture renders
|
|
670
|
+
mask loaders and skip the await, and a shell HIT flushes the stored prelude
|
|
671
|
+
before loaders resolve. Flagging a loader on a `ppr` route does not bake it
|
|
672
|
+
into the shell and does not delay HIT serves; the flag only governs ordinary
|
|
673
|
+
(axis-1) document renders.
|
|
664
674
|
|
|
665
675
|
## Execution matrix
|
|
666
676
|
|
|
@@ -553,10 +553,14 @@ Passthrough entries are logged distinctly:
|
|
|
553
553
|
Loaders on pre-rendered routes run at request time. They are bundled normally
|
|
554
554
|
and need `cache()` for caching. Do not use build-only APIs in loaders.
|
|
555
555
|
|
|
556
|
-
###
|
|
557
|
-
|
|
558
|
-
Handle values pushed via `ctx.use()`
|
|
559
|
-
Flight payload
|
|
556
|
+
### Build-time handle data is frozen
|
|
557
|
+
|
|
558
|
+
Handle values pushed via `ctx.use()` DURING pre-rendering (handler pushes at
|
|
559
|
+
build) are baked into the Flight payload and do not update at request time.
|
|
560
|
+
Loader pushes are the exception by construction: loaders run live at request
|
|
561
|
+
time (previous section), so a loader-pushed handle (a data-derived Meta title,
|
|
562
|
+
say) is request-time data — delivery follows the loader race model, see
|
|
563
|
+
`/loader` → "Writing Handles from Loaders".
|
|
560
564
|
|
|
561
565
|
### Server actions work normally
|
|
562
566
|
|
package/skills/rango/SKILL.md
CHANGED
|
@@ -95,23 +95,26 @@ stated, greppable contract.
|
|
|
95
95
|
|
|
96
96
|
## Pick a primitive
|
|
97
97
|
|
|
98
|
-
| I need to… | Use
|
|
99
|
-
| --------------------------------------- |
|
|
100
|
-
| render data fresh every request | `loader()` + `useLoader()`
|
|
101
|
-
| cache a rendered subtree | `cache()` on a segment
|
|
102
|
-
| cache one function/component's result | `"use cache"`
|
|
103
|
-
| cache a loader's data | `loader(L, () => [cache()])`
|
|
104
|
-
| re-render a segment after an action | `revalidate()`
|
|
105
|
-
| mutate | `"use server"` action
|
|
106
|
-
| debug a slow request | `debugPerformance` / telemetry
|
|
107
|
-
| share config across routes | factory returning a helper array
|
|
108
|
-
| compose a sub-app / module | `include()`
|
|
109
|
-
| modal / soft navigation | `intercept()`
|
|
110
|
-
|
|
|
111
|
-
|
|
|
112
|
-
|
|
|
113
|
-
|
|
|
114
|
-
|
|
|
98
|
+
| I need to… | Use | Skill |
|
|
99
|
+
| --------------------------------------- | ------------------------------------- | ----------------------- |
|
|
100
|
+
| render data fresh every request | `loader()` + `useLoader()` | /loader |
|
|
101
|
+
| cache a rendered subtree | `cache()` on a segment | /caching |
|
|
102
|
+
| cache one function/component's result | `"use cache"` | /use-cache |
|
|
103
|
+
| cache a loader's data | `loader(L, () => [cache()])` | /loader, /caching |
|
|
104
|
+
| re-render a segment after an action | `revalidate()` | /loader |
|
|
105
|
+
| mutate | `"use server"` action | /server-actions |
|
|
106
|
+
| debug a slow request | `debugPerformance` / telemetry | /observability |
|
|
107
|
+
| share config across routes | factory returning a helper array | /composability |
|
|
108
|
+
| compose a sub-app / module | `include()` | /route |
|
|
109
|
+
| modal / soft navigation | `intercept()` | /intercept |
|
|
110
|
+
| route group of client components | `clientUrls()` in `"use client"` | /client-urls |
|
|
111
|
+
| set meta/breadcrumbs from loader data | `ctx.use(Handle)` in the loader | /loader |
|
|
112
|
+
| guarantee loader output in the SSR HTML | `loader(L, { stream: "navigation" })` | /loader |
|
|
113
|
+
| pre-render a route at build time | `Prerender(...)` wrapper | /prerender |
|
|
114
|
+
| feed live loaders from a cached shell | replayed handle + `ctx.rendered()` | /shell-manifest |
|
|
115
|
+
| cache the HTML shell, keep loaders live | `ppr` path option | /ppr |
|
|
116
|
+
| choose in-function vs CDN caching | deployment cache boundary | /deployment-caching |
|
|
117
|
+
| stream SSE / upgrade a WebSocket | `path.stream()` / `path.any()` | /streams-and-websockets |
|
|
115
118
|
|
|
116
119
|
## Invariants
|
|
117
120
|
|
|
@@ -243,6 +246,7 @@ Grouped by concern — read when you need to…
|
|
|
243
246
|
| ------------------------- | -------------------------------------------------------------------------- |
|
|
244
247
|
| `/router-setup` | Create and configure the RSC router |
|
|
245
248
|
| `/route` | Define routes with `urls()`, `path()`, and `include()` |
|
|
249
|
+
| `/client-urls` | Client-component route groups with `clientUrls()` — no handlers |
|
|
246
250
|
| `/layout` | Layouts that wrap child routes |
|
|
247
251
|
| `/parallel` | Multi-column layouts and sidebars |
|
|
248
252
|
| `/intercept` | Modal/slide-over patterns for soft navigation |
|
|
@@ -163,6 +163,6 @@ should find **none** — that is the client-only contract.
|
|
|
163
163
|
## Reference
|
|
164
164
|
|
|
165
165
|
A worked, tested wiring (dev + production e2e markers, incl. the client-only
|
|
166
|
-
contract) lives in the `@rangojs/router`
|
|
167
|
-
`react-compiler.test.ts` files under
|
|
168
|
-
and `tests/vite-rsc-demo`.
|
|
166
|
+
contract) lives in the `@rangojs/router` repository — not shipped in this
|
|
167
|
+
package: `docs/react-compiler.md` and the `react-compiler.test.ts` files under
|
|
168
|
+
`e2e/e2e-basic`, `tests/cloudflare-basic`, and `tests/vite-rsc-demo`.
|
package/skills/route/SKILL.md
CHANGED
|
@@ -358,8 +358,11 @@ path("/moved", () => redirect("/new-location", 301), { name: "moved" });
|
|
|
358
358
|
> **Redirecting from a route with `loading()`:** an `async` handler that returns
|
|
359
359
|
> a `Response`/`redirect()` on a route that also declares `loading()` is streamed,
|
|
360
360
|
> so the redirect is rendered into the RSC stream instead of becoming an HTTP
|
|
361
|
-
> redirect.
|
|
362
|
-
> handler return
|
|
361
|
+
> redirect. For a real HTTP redirect, issue it from `middleware` or a
|
|
362
|
+
> **synchronous** handler return — pre-stream redirect authority belongs there.
|
|
363
|
+
> (A loader `throw redirect()` also navigates the user, but it is ALWAYS a
|
|
364
|
+
> client-side replace on document loads — 200 document, never a 302; see
|
|
365
|
+
> `/loader` → "Loader Authority". Dev logs a warning if this is hit.)
|
|
363
366
|
|
|
364
367
|
### Redirect with location state
|
|
365
368
|
|
|
@@ -347,12 +347,19 @@ Two distinct 404 scenarios:
|
|
|
347
347
|
```typescript
|
|
348
348
|
import { notFound } from "@rangojs/router";
|
|
349
349
|
|
|
350
|
-
// In a handler
|
|
350
|
+
// In a handler
|
|
351
351
|
path("/product/:slug", async (ctx) => {
|
|
352
352
|
const product = await db.getProduct(ctx.params.slug);
|
|
353
353
|
if (!product) notFound("Product not found");
|
|
354
354
|
return <ProductPage product={product} />;
|
|
355
355
|
});
|
|
356
|
+
|
|
357
|
+
// In a loader — data-dependent authority lives with the data
|
|
358
|
+
export const ProductLoader = createLoader(async (ctx) => {
|
|
359
|
+
"use server";
|
|
360
|
+
if (!(await exists(ctx.params.slug))) notFound("Product not found");
|
|
361
|
+
return getProduct(ctx.params.slug);
|
|
362
|
+
});
|
|
356
363
|
```
|
|
357
364
|
|
|
358
365
|
### Fallback chain for `notFound()`
|
|
@@ -364,7 +371,14 @@ When `notFound()` is thrown, the router looks for a fallback in this order:
|
|
|
364
371
|
3. **`notFound`** — from `createRouter()` config (same component used for no-route-match)
|
|
365
372
|
4. **Default `<h1>Not Found</h1>`** — built-in fallback
|
|
366
373
|
|
|
367
|
-
|
|
374
|
+
Handler and no-match cases set HTTP 404 status. A LOADER-thrown `notFound()`
|
|
375
|
+
on a document load always streams the resolved not-found UI, but the 404
|
|
376
|
+
STATUS is opportunistic — real only when the rejection settles before the
|
|
377
|
+
document Response is constructed (loaders stream). Register the loader as
|
|
378
|
+
`loader(Def, { stream: "navigation" })` to make the 404 status deterministic;
|
|
379
|
+
on client navigations the 404 UI swaps in with the URL preserved (payload
|
|
380
|
+
stays 200 — the client owns presentation there). See `/loader` → "Loader
|
|
381
|
+
Authority".
|
|
368
382
|
|
|
369
383
|
### notFoundBoundary
|
|
370
384
|
|
package/skills/scripts/SKILL.md
CHANGED
|
@@ -7,9 +7,9 @@ argument-hint: "[vendor]"
|
|
|
7
7
|
# Scripts
|
|
8
8
|
|
|
9
9
|
Inject `<script>` tags into the document the idiomatic Rango way: push a config
|
|
10
|
-
from a **server** route/layout handler
|
|
11
|
-
them with the built-in **`<Scripts />`**
|
|
12
|
-
pair, but for scripts). The request CSP **nonce is applied automatically to
|
|
10
|
+
from a **server** route/layout handler — or a loader body — with
|
|
11
|
+
`ctx.use(Script)(config)`, and render them with the built-in **`<Scripts />`**
|
|
12
|
+
component (the `Meta` / `<MetaTags>` pair, but for scripts). The request CSP **nonce is applied automatically to
|
|
13
13
|
document-rendered scripts** — you never read or pass it. (The one exception is an
|
|
14
14
|
async script first encountered on a soft navigation; see the nonce caveat under
|
|
15
15
|
"Execution contract".)
|
|
@@ -40,7 +40,7 @@ export function Document({ children }) {
|
|
|
40
40
|
}
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
## Push from a handler
|
|
43
|
+
## Push from a handler (or loader)
|
|
44
44
|
|
|
45
45
|
`ScriptConfig` is a discriminated union — exactly one of three shapes, so invalid
|
|
46
46
|
combinations are compile errors:
|
|
@@ -101,6 +101,15 @@ inert (silently dead) `<script>`. Async configs stay reactive. Reusing an `id`
|
|
|
101
101
|
shapes the INITIAL document output (last-push-wins) — it does not re-run a script
|
|
102
102
|
during navigation.
|
|
103
103
|
|
|
104
|
+
> **Loader pushes meet the freeze.** A LOADER push to the Script handle
|
|
105
|
+
> follows the delivery race (`/loader`): it is in the initial HTML only if it
|
|
106
|
+
> settles before the handler barrier. A push that lands after a slow fetch
|
|
107
|
+
> arrives post-hydration — and for an inline/ordered script the frozen set
|
|
108
|
+
> means it is silently dropped. If a loader must contribute an inline script
|
|
109
|
+
> to the document, register it `loader(Def, { stream: "navigation" })` so the
|
|
110
|
+
> document render awaits the push; otherwise push from a handler (or use an
|
|
111
|
+
> `async` config, which stays reactive).
|
|
112
|
+
|
|
104
113
|
**Nonce caveat for soft-nav async.** The "nonce is applied automatically" claim
|
|
105
114
|
holds for DOCUMENT-RENDERED scripts (they carry the nonce in the SSR HTML). An
|
|
106
115
|
async script first encountered on a soft navigation is injected by React on the
|
|
@@ -143,7 +152,7 @@ per-route data into the FIRST (hard-load) page_view server-side — the Script
|
|
|
143
152
|
handle is collected after handlers run (parent → child, last-wins):
|
|
144
153
|
|
|
145
154
|
```ts
|
|
146
|
-
// root layout: generic bootstrap
|
|
155
|
+
// root layout: generic bootstrap (handler push — always pre-barrier)
|
|
147
156
|
ctx.use(Script)({ id: "gtm", children: gtmBootstrap("GTM-XXXX") });
|
|
148
157
|
// a route: same id, with content_group baked in
|
|
149
158
|
ctx.use(Script)({
|
|
@@ -176,4 +185,5 @@ Otherwise allow the vendor hosts. For GTM/GA4 (Google's wildcards): `script-src
|
|
|
176
185
|
`type: "text/partytown"` and wire Partytown's own nonce config manually.
|
|
177
186
|
|
|
178
187
|
A full GTM + GA4-style integration (page_view on first render + soft nav, nonce,
|
|
179
|
-
ecommerce events) lives in `tests/vite-rsc-demo
|
|
188
|
+
ecommerce events) lives in the router repository's `tests/vite-rsc-demo` app
|
|
189
|
+
(not shipped in this package).
|
|
@@ -33,14 +33,18 @@ batched.
|
|
|
33
33
|
|
|
34
34
|
1. **Handles record data at render time.** The handler pushes to a handle
|
|
35
35
|
(`ctx.use(Handle)`) while it renders — at build time for `Prerender`, on
|
|
36
|
-
the cache miss for `cache()`.
|
|
36
|
+
the cache miss for `cache()`. (Loader bodies can push handles too — see
|
|
37
|
+
`/loader` — but a loader push is request-time and is NOT part of the
|
|
38
|
+
replayed artifact; a manifest handle must be pushed by the code that gets
|
|
39
|
+
frozen with the shell.)
|
|
37
40
|
2. **Replay on every hit.** Handle data is stored with the Flight payload
|
|
38
41
|
and replayed into the handle store on cache/prerender hits — handler code
|
|
39
42
|
does not re-run, but its pushes do.
|
|
40
43
|
3. **Loaders read after the render barrier.** A DSL loader can
|
|
41
44
|
`await ctx.rendered()` (waits for all non-loader segments to settle —
|
|
42
|
-
fresh render or replay alike), then `ctx.
|
|
43
|
-
**collected** handle data.
|
|
45
|
+
fresh render or replay alike), then `ctx.get(Handle)` returns the
|
|
46
|
+
**collected** handle data. (`ctx.use(Handle)` in a loader is the WRITE —
|
|
47
|
+
it returns the push function; reads live on `ctx.get`.)
|
|
44
48
|
|
|
45
49
|
Loaders are live by default, so the read happens on every request even when
|
|
46
50
|
the shell is a hit.
|
|
@@ -90,7 +94,7 @@ import { RenderedProducts } from "../handles/rendered-products";
|
|
|
90
94
|
export const PriceLoader = createLoader(async (ctx) => {
|
|
91
95
|
"use server";
|
|
92
96
|
await ctx.rendered();
|
|
93
|
-
const ids = ctx.
|
|
97
|
+
const ids = ctx.get(RenderedProducts);
|
|
94
98
|
return db.pricesFor(ids); // Map<string, number> keyed by product id
|
|
95
99
|
});
|
|
96
100
|
```
|
|
@@ -144,15 +148,20 @@ cache({ ttl: 600, tags: ["products"] }, () => [
|
|
|
144
148
|
Ids, slugs, slot names, variant keys: yes. Anything derived from
|
|
145
149
|
`cookies()`/`headers()`: no.
|
|
146
150
|
- **`ctx.rendered()` is experimental and DSL-loaders-only.** It throws in
|
|
147
|
-
fetchable/standalone loader calls that run outside a route render
|
|
151
|
+
fetchable/standalone loader calls that run outside a route render, in
|
|
152
|
+
handler-invoked loaders (a handler already awaiting the loader via
|
|
153
|
+
`ctx.use()` is a detected deadlock), and in loaders registered with
|
|
154
|
+
`{ stream: "navigation" }` (the document render awaits the loader before
|
|
155
|
+
the barrier — a cycle by construction; see `/loader`).
|
|
148
156
|
- **The reading loader serializes after the shell.** `await ctx.rendered()`
|
|
149
157
|
deliberately gives up loader/render parallelism — on a miss the loader
|
|
150
158
|
waits for segment resolution; on a hit (the common case for a cached
|
|
151
159
|
shell) replay is immediate and the wait is negligible. A
|
|
152
160
|
`debugPerformance` waterfall shows this loader after the render bar; for
|
|
153
161
|
this pattern that is the contract, not a regression.
|
|
154
|
-
- **`ctx.
|
|
155
|
-
with an error saying to await the barrier first.
|
|
162
|
+
- **`ctx.get(handle)` before `await ctx.rendered()` throws** in a loader,
|
|
163
|
+
with an error saying to await the barrier first. (`ctx.use(Handle)` — the
|
|
164
|
+
push — is legal for the whole loader body, no barrier required.)
|
|
156
165
|
- **Deferred handle values are resolved before storage** (resolve-by-default),
|
|
157
166
|
so the manifest read always sees plain values, never promises.
|
|
158
167
|
|
package/skills/testing/SKILL.md
CHANGED
|
@@ -85,11 +85,11 @@ Each primitive links to its sub-file (API + recipe + caveats).
|
|
|
85
85
|
| ---------------------------------------------------------------------------------------------------------------------------------- | ------------ | --------------------------------------------------------------------------------------------- | -------------------------------- |
|
|
86
86
|
| a pure function / `reverse` / `href` / a predicate (`revalidate`, `isAction`) | unit + types | [`reverse`/`@ts-expect-error`](./reverse-and-types.md) | `@rangojs/router/testing` |
|
|
87
87
|
| one loader's data logic | unit (node) | [`runLoader`](./loader.md) | `@rangojs/router/testing` |
|
|
88
|
-
| a loader's cookie / header / redirect
|
|
88
|
+
| a loader's cookie / header / thrown `redirect()`/`notFound()` / handle-push output | unit (node) | [`runLoaderResult`](./loader.md) | `@rangojs/router/testing` |
|
|
89
89
|
| one middleware's ordering / short-circuit / cookie+header merge | unit (node) | [`runMiddleware`](./middleware.md) | `@rangojs/router/testing` |
|
|
90
90
|
| a `"use server"` action's cookie / header / flash output (even on `throw redirect()`) | unit (node) | [`runInRequestContext`](./server-actions.md) | `@rangojs/router/testing` |
|
|
91
91
|
| a `transition({ when })` gate (keep/drop) against nav source / target / action metadata | unit (node) | `runTransitionWhen` (`{ kept, whenContext }`; pass `{ ppr: true }` for pre-handler timing) | `@rangojs/router/testing` |
|
|
92
|
-
| a handle's `collect`/accumulator, or a
|
|
92
|
+
| a handle's `collect`/accumulator, a seeded handle read, or a loader handle write | unit | [`collectHandle` / seeded `handles` / `handlePushes`](./handles.md) | `@rangojs/router/testing` |
|
|
93
93
|
| a CLIENT component reading router context (`useParams`/`useReverse`/`Outlet`/`useNavigation`/`useLoader`) | unit (DOM) | [`renderRoute`](./client-components.md) | `@rangojs/router/testing/dom` |
|
|
94
94
|
| a redirect / status / headers / cookies / **response route** (json/text/html/xml/md), no Flight | integration | [`dispatch`](./response-routes.md) | `@rangojs/router/testing` |
|
|
95
95
|
| 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` |
|
|
@@ -12,6 +12,7 @@ RTL-style stub (peer of React Router's `createRoutesStub` / Expo's `renderRouter
|
|
|
12
12
|
| ----------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
13
13
|
| `request` | `Request \| string` | Initial location. Only the URL is read (client render — headers/method ignored). Defaults to the leaf spec's static prefix or `"/"`. |
|
|
14
14
|
| `loaderData` | `Record<string, unknown>` | Loader data keyed by loader `$$id`. `useLoader(L)` reads `loaderData[L.$$id]`. |
|
|
15
|
+
| `outletPending` | `boolean` | Seed `useOutlet().pending` through each synthetic segment's production `OutletProvider`. Defaults to `false`; this is context seeding, not a simulated navigation/Suspense/action lifecycle. |
|
|
15
16
|
| `loaders` | `ReadonlyArray<readonly [LoaderDefinition<any>, unknown]>` | Seed by REFERENCE: `[loader, data]` pairs. Robust for real `createLoader()` handles whose `$$id` is empty in a bare test. Prefer over `loaderData`. |
|
|
16
17
|
| `params` | `Record<string, string>` | Explicit params, merged over (and overriding) params extracted from the `request` URL. |
|
|
17
18
|
| `locationState` | `ReadonlyArray<readonly [LocationStateDefinition<any, any>, unknown]>` | Seed `useLocationState(def)` by REFERENCE: `[def, value]` pairs; written to `history.state`. |
|
|
@@ -43,6 +44,7 @@ RTL-style stub (peer of React Router's `createRoutesStub` / Expo's `renderRouter
|
|
|
43
44
|
| `useLocationState` | SEEDED `history.state` value. |
|
|
44
45
|
| `useHandle` | SEEDED handle output (globally accumulated). |
|
|
45
46
|
| `Outlet` | Renders the next segment in the chain (layout nesting). |
|
|
47
|
+
| `useOutlet` | Next-segment `content` plus SEEDED `options.outletPending`. |
|
|
46
48
|
| `useTheme` | Theme; throws without `options.theme` (see caveat). |
|
|
47
49
|
|
|
48
50
|
### Returns — `RenderRouteResult`
|
|
@@ -110,6 +112,10 @@ it("resolves params + reverse + Outlet through the layout chain", async () => {
|
|
|
110
112
|
|
|
111
113
|
- Client tree ONLY. Does NOT catch server/client boundary reference-identity remount bugs, real Flight serialization errors, loader execution, middleware, or handler ordering — those are `renderServerTree` / `renderHandler` / e2e territory. Loader data is SEEDED, never run.
|
|
112
114
|
- `router.navigate()` bypasses the navigation lifecycle, so the controller never leaves `idle`. `useNavigation()` / `useLinkStatus()` / `useAction()` non-idle states (loading/streaming/pending, action result/error) are NOT reachable — test those at e2e.
|
|
115
|
+
- `outletPending` seeds only the production-shaped outlet context. It is useful
|
|
116
|
+
for the two settled render states of a layout that reads `useOutlet()`, but it
|
|
117
|
+
does not prove the hydrated `clientUrls()` transition that toggles the value;
|
|
118
|
+
keep that transition in dev + production e2e.
|
|
113
119
|
- CATCH — streaming `use(promise)` Suspense content (e.g. an async breadcrumb `content: Promise<ReactNode>`): a plain `Promise.resolve(node)` does NOT flush its Suspense retry in RTL/happy-dom, so the DOM stays on the fallback. Assert the PENDING fallback with `new Promise(() => {})`; for the ARRIVED state pass an already-settled promise so `use()` reads it synchronously: `const p = Promise.resolve(node) as any; p.status = "fulfilled"; p.value = node;`. The real pending->resolved transition is an e2e concern.
|
|
114
120
|
- ARIA gotcha — an explicit `role` on a `<Link>` (e.g. `<Link role="tab">` in a tablist) OVERRIDES the implicit `link` role, so `getByRole("link")` finds nothing. Query the explicit role (`getByRole("tab")`) or fall back to `getByText` / `getByTestId` and assert `getAttribute("href")`.
|
|
115
121
|
- `ctx.theme` is undefined unless `theme` is passed; the typed `ctx.search` defaults to `{}` (seed `searchData` on `runLoader`, not here).
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Testing a handle — collectHandle, plus the loader and client read paths
|
|
1
|
+
# Testing a handle — collectHandle, plus the loader read/write and client read paths
|
|
2
2
|
|
|
3
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
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
|
|
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 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 (`ctx.get(handle)` after the barrier); `runLoaderResult(...).handlePushes` records what a loader WRITES (`ctx.use(SomeHandle)({...})`, in push order); `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
6
|
|
|
7
7
|
## API
|
|
8
8
|
|
|
@@ -17,10 +17,10 @@ A handle's `collect`/accumulator (the `createHandle(collect)` argument that maps
|
|
|
17
17
|
|
|
18
18
|
### runLoader option — `handles` — `src/testing/run-loader.ts`
|
|
19
19
|
|
|
20
|
-
| Field | Type | Meaning
|
|
21
|
-
| ---------- | ------------------------------------------- |
|
|
22
|
-
| `handles` | `ReadonlyArray<readonly [Handle, unknown]>` | Seeds the value `ctx.
|
|
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.
|
|
20
|
+
| Field | Type | Meaning |
|
|
21
|
+
| ---------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
22
|
+
| `handles` | `ReadonlyArray<readonly [Handle, unknown]>` | Seeds the value `ctx.get(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.get(handle)` read before the barrier settles throws, exactly as in production. (`ctx.use(handle)` — the WRITE — is never gated.) |
|
|
24
24
|
|
|
25
25
|
### renderRoute option — `handles` — `src/testing/render-route.tsx`
|
|
26
26
|
|
|
@@ -66,7 +66,7 @@ import { RenderedProducts } from "../src/handles"; // a createHandle(...)
|
|
|
66
66
|
|
|
67
67
|
const livePricesBody = async (ctx) => {
|
|
68
68
|
await ctx.rendered(); // barrier: handle data is now readable
|
|
69
|
-
const ids = ctx.
|
|
69
|
+
const ids = ctx.get(RenderedProducts) as string[];
|
|
70
70
|
return ids.map((id) => ({ id, price: 9.99 }));
|
|
71
71
|
};
|
|
72
72
|
|
|
@@ -82,6 +82,27 @@ it("reads the accumulated handle value (seed the OUTPUT, mock the barrier)", asy
|
|
|
82
82
|
});
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
```ts
|
|
86
|
+
// loader-writes-handle.test.ts — a loader PUSHING meta/breadcrumbs (handler parity)
|
|
87
|
+
import { it, expect } from "vitest";
|
|
88
|
+
import { runLoaderResult } from "@rangojs/router/testing";
|
|
89
|
+
import { Meta } from "../src/handles";
|
|
90
|
+
|
|
91
|
+
const productBody = async (ctx) => {
|
|
92
|
+
const product = { name: "Widget", slug: "widget" };
|
|
93
|
+
ctx.use(Meta)({ title: `${product.name} — Shop` });
|
|
94
|
+
return product;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
it("records loader handle writes in push order", async () => {
|
|
98
|
+
const { result, handlePushes } = await runLoaderResult(productBody);
|
|
99
|
+
expect(result?.name).toBe("Widget");
|
|
100
|
+
expect(handlePushes).toEqual([
|
|
101
|
+
{ handle: Meta, value: { title: "Widget — Shop" } },
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
85
106
|
```tsx
|
|
86
107
|
// breadcrumb-trail.test.tsx — a client component reading useHandle
|
|
87
108
|
// @vitest-environment happy-dom
|
|
@@ -121,7 +142,8 @@ it("renders the seeded trail (seed the INPUT pushes, the collect runs)", async (
|
|
|
121
142
|
|
|
122
143
|
- `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.
|
|
123
144
|
- 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.
|
|
124
|
-
- 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).
|
|
145
|
+
- runLoader `handles` seeds the POST-collect ACCUMULATED value (singular `unknown`) a loader reads via `ctx.get(handle)` after `await ctx.rendered()`; pair with `{ rendered: true }`. Shape contrast: renderRoute feeds the barrier INPUT (pushes[]), runLoader feeds its OUTPUT (the accumulated value).
|
|
146
|
+
- Loader WRITES are the other direction: `ctx.use(SomeHandle)({...})` records into `runLoaderResult(...).handlePushes` (push order; a `.defer()` resolver's value is recorded when the resolver runs). Nothing to seed — assert the envelope.
|
|
125
147
|
- The renderRoute path is the CLIENT tree only: it does NOT catch server/client boundary remount bugs, real Flight serialization errors, or loader execution.
|
|
126
148
|
|
|
127
149
|
## See also
|
package/skills/testing/loader.md
CHANGED
|
@@ -8,62 +8,64 @@
|
|
|
8
8
|
|
|
9
9
|
### Options — `RunLoaderOptions<TEnv>`
|
|
10
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.
|
|
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.get(SomeHandle)` returns — the ACCUMULATED handle data read after `await ctx.rendered()`. Matched by handle reference. (Loader handle WRITES need no seed — see `runLoaderResult(...).handlePushes`.) |
|
|
33
33
|
|
|
34
34
|
### Context — `TestLoaderContext<TEnv>` (what your loader receives)
|
|
35
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`
|
|
48
|
-
| `use` | `(dep) => ...` | Resolve `ctx.use(OtherLoader)
|
|
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`.
|
|
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` — or READ a handle: `ctx.get(handle)` is rendered-gated (throws before the barrier) and returns the `opts.handles` seed. |
|
|
48
|
+
| `use` | `(dep) => ...` | Resolve `ctx.use(OtherLoader)` (loader seeds, then the `use` resolver, then the real context `use()`) — or WRITE a handle: `ctx.use(SomeHandle)` returns the push function (withDefer-wrapped), recording into `runLoaderResult(...).handlePushes`. |
|
|
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
56
|
|
|
57
57
|
### Returns — `Promise<T>`
|
|
58
58
|
|
|
59
59
|
The loader data DIRECTLY (no envelope). `T` is the loader's return type.
|
|
60
60
|
|
|
61
|
-
To assert a loader's EFFECTS — a `Set-Cookie`, a response header,
|
|
62
|
-
`throw redirect(...)` (
|
|
63
|
-
**`runLoaderResult(loader, opts)`** instead. Same
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
To assert a loader's EFFECTS — a `Set-Cookie`, a response header, a
|
|
62
|
+
`throw redirect(...)` or `notFound()` (loader authority signals), or handle
|
|
63
|
+
writes — use the sibling **`runLoaderResult(loader, opts)`** instead. Same
|
|
64
|
+
options, but it returns an envelope:
|
|
65
|
+
`{ result, thrown, response, cookies, headers, locationState, stateCookieName, handlePushes }`
|
|
66
|
+
(parity with `runInRequestContext`; `result` is the loader's data;
|
|
67
|
+
`handlePushes` records every `ctx.use(SomeHandle)({...})` write in push
|
|
68
|
+
order). `runLoader` discards those effects.
|
|
67
69
|
|
|
68
70
|
## Recipe
|
|
69
71
|
|
|
@@ -113,7 +115,7 @@ it("asserts a loader's set-cookie + redirect (runLoaderResult)", async () => {
|
|
|
113
115
|
## Caveats
|
|
114
116
|
|
|
115
117
|
- `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.
|
|
118
|
+
- `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 the `ctx.get(SomeHandle)` read. `ctx.isAction(...)` is unavailable — cover those at e2e.
|
|
117
119
|
- 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
120
|
- 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
121
|
- 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.)
|
|
@@ -86,7 +86,7 @@ Pass an array to run several in order. Cookies set inside middleware via the sta
|
|
|
86
86
|
|
|
87
87
|
## Caveats
|
|
88
88
|
|
|
89
|
-
- No `handles`/`rendered` option by design: middleware
|
|
89
|
+
- No `handles`/`rendered` option by design: the middleware context has no handle APIs in production — no push (`ctx.use(Handle)` is a handler/loader API) and no post-barrier read (`ctx.get(handle)` is loader-only, and middleware runs BEFORE the render barrier anyway). Read handle data in a loader and test it with `runLoader`; assert loader pushes via `runLoaderResult(...).handlePushes` (see `./handles.md`).
|
|
90
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
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
92
|
- `ctx.theme` is `undefined` unless `theme` is passed; `redirect()` does no basename prefixing unless `basename` is seeded.
|