@rangojs/router 0.24.3 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/bin/rango.js +363 -39
- package/dist/testing/vitest.js +37 -4
- package/dist/types/browser/event-controller.d.ts +24 -5
- package/dist/types/browser/intercept-utils.d.ts +5 -2
- package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
- package/dist/types/browser/navigation-transaction.d.ts +5 -5
- package/dist/types/browser/partial-update.d.ts +21 -13
- package/dist/types/browser/prefetch/cache.d.ts +8 -0
- package/dist/types/browser/rango-marker.d.ts +7 -0
- package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
- package/dist/types/browser/react/use-handle.d.ts +20 -0
- package/dist/types/browser/react/use-href.d.ts +6 -0
- package/dist/types/browser/react/use-router.d.ts +6 -1
- package/dist/types/browser/response-adapter.d.ts +5 -3
- package/dist/types/browser/scroll-restoration.d.ts +9 -3
- package/dist/types/browser/segment-reconciler.d.ts +10 -13
- package/dist/types/browser/types.d.ts +38 -8
- package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
- package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
- package/dist/types/cache/types.d.ts +8 -0
- package/dist/types/client-urls/client-root.d.ts +4 -1
- package/dist/types/client-urls/client-urls.d.ts +28 -2
- package/dist/types/client-urls/navigation.d.ts +6 -8
- package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
- package/dist/types/client-urls/server-projection.d.ts +20 -5
- package/dist/types/client-urls/types.d.ts +35 -17
- package/dist/types/client.d.ts +2 -2
- package/dist/types/client.rsc.d.ts +2 -2
- package/dist/types/decode-loader-results.d.ts +28 -7
- package/dist/types/href-client.d.ts +3 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/loader-redirect.d.ts +2 -3
- package/dist/types/outlet-context.d.ts +7 -6
- package/dist/types/prefetch-markers.d.ts +14 -5
- package/dist/types/route-content-wrapper.d.ts +17 -14
- package/dist/types/route-definition/helpers-types.d.ts +7 -2
- package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
- package/dist/types/route-definition/rewrite.d.ts +48 -5
- package/dist/types/route-types.d.ts +2 -2
- package/dist/types/router/handler-context.d.ts +14 -2
- package/dist/types/router/intercept-resolution.d.ts +19 -46
- package/dist/types/router/lazy-includes.d.ts +7 -0
- package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
- package/dist/types/router/middleware.d.ts +0 -16
- package/dist/types/router/navigation-snapshot.d.ts +2 -0
- package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
- package/dist/types/router/revalidation.d.ts +8 -1
- package/dist/types/router/router-context.d.ts +4 -16
- package/dist/types/router/router-interfaces.d.ts +0 -6
- package/dist/types/router/router-options.d.ts +4 -7
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
- package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
- package/dist/types/router/segment-resolution.d.ts +1 -1
- package/dist/types/router/segment-wrappers.d.ts +3 -5
- package/dist/types/router/substitute-pattern-params.d.ts +19 -2
- package/dist/types/router/tracing.d.ts +2 -2
- package/dist/types/rsc/full-payload.d.ts +14 -2
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/manifest-init.d.ts +20 -0
- package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
- package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
- package/dist/types/rsc/rewrite-passes.d.ts +12 -2
- package/dist/types/rsc/server-action.d.ts +6 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
- package/dist/types/rsc/shell-capture.d.ts +7 -0
- package/dist/types/rsc/types.d.ts +5 -0
- package/dist/types/segment-system.d.ts +15 -0
- package/dist/types/server/context.d.ts +64 -4
- package/dist/types/server/request-context.d.ts +41 -4
- package/dist/types/server/request-headers.d.ts +12 -0
- package/dist/types/server/rewrite-trail.d.ts +8 -1
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +1 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
- package/dist/types/testing/render-route.d.ts +21 -11
- package/dist/types/testing/run-loader.d.ts +7 -0
- package/dist/types/testing/run-middleware.d.ts +7 -0
- package/dist/types/testing/run-revalidate.d.ts +5 -4
- package/dist/types/testing/serve-shell-request.d.ts +41 -4
- package/dist/types/testing/shell-status.d.ts +17 -0
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/testing/vitest.d.ts +10 -1
- package/dist/types/thenable-status.d.ts +0 -6
- package/dist/types/types/handler-context.d.ts +12 -1
- package/dist/types/types/loader-types.d.ts +4 -3
- package/dist/types/types/route-entry.d.ts +2 -1
- package/dist/types/urls/path-helper-types.d.ts +4 -2
- package/dist/types/urls/path-helper.d.ts +5 -0
- package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
- package/dist/types/vite/discovery/state.d.ts +20 -0
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
- package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
- package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
- package/dist/vite/index.js +6816 -6547
- package/package.json +1 -1
- package/skills/app-structure/SKILL.md +255 -0
- package/skills/app-structure/content-site.md +333 -0
- package/skills/app-structure/e-commerce.md +435 -0
- package/skills/app-structure/large-app.md +885 -0
- package/skills/app-structure/multi-app.md +280 -0
- package/skills/breadcrumbs/SKILL.md +6 -1
- package/skills/caching/SKILL.md +3 -1
- package/skills/catalog.json +12 -0
- package/skills/client-urls/SKILL.md +195 -32
- package/skills/composability/SKILL.md +10 -8
- package/skills/handler-use/SKILL.md +48 -9
- package/skills/hooks/handle-and-actions.md +104 -0
- package/skills/hooks/navigation.md +24 -10
- package/skills/hooks/urls.md +1 -1
- package/skills/i18n/SKILL.md +2 -1
- package/skills/intercept/SKILL.md +60 -29
- package/skills/layout/SKILL.md +6 -3
- package/skills/links/SKILL.md +151 -17
- package/skills/loader/SKILL.md +32 -10
- package/skills/middleware/SKILL.md +111 -15
- package/skills/observability/SKILL.md +11 -0
- package/skills/parallel/SKILL.md +5 -5
- package/skills/ppr/SKILL.md +43 -8
- package/skills/prerender/SKILL.md +1 -2
- package/skills/rango/SKILL.md +3 -0
- package/skills/response-routes/SKILL.md +4 -1
- package/skills/scripts/SKILL.md +12 -1
- package/skills/server-actions/SKILL.md +60 -20
- package/skills/testing/SKILL.md +1 -0
- package/skills/testing/client-components.md +13 -10
- package/skills/testing/loader.md +25 -24
- package/skills/testing/middleware.md +11 -11
- package/skills/testing/response-routes.md +4 -1
- package/skills/testing/serve-shell-request.md +85 -21
- package/skills/ux-architecture/SKILL.md +980 -0
- package/skills/view-transitions/SKILL.md +43 -10
- package/src/__augment-tests__/augmented.check.ts +28 -0
- package/src/browser/event-controller.ts +44 -18
- package/src/browser/intercept-utils.ts +3 -2
- package/src/browser/merge-segment-loaders.ts +0 -85
- package/src/browser/navigation-bridge.ts +119 -98
- package/src/browser/navigation-client.ts +20 -15
- package/src/browser/navigation-store.ts +4 -2
- package/src/browser/navigation-transaction.ts +9 -8
- package/src/browser/partial-update.ts +90 -32
- package/src/browser/prefetch/cache.ts +8 -0
- package/src/browser/prefetch/fetch.ts +13 -3
- package/src/browser/rango-marker.ts +13 -0
- package/src/browser/react/NavigationProvider.tsx +25 -7
- package/src/browser/react/filter-segment-order.ts +13 -0
- package/src/browser/react/use-action.ts +9 -15
- package/src/browser/react/use-handle.ts +63 -71
- package/src/browser/react/use-href.tsx +24 -5
- package/src/browser/react/use-link-status.ts +1 -1
- package/src/browser/react/use-navigation.ts +30 -40
- package/src/browser/react/use-reverse.ts +4 -17
- package/src/browser/react/use-router.ts +37 -6
- package/src/browser/react/use-segments.ts +8 -12
- package/src/browser/response-adapter.ts +11 -5
- package/src/browser/rsc-router.tsx +3 -1
- package/src/browser/scroll-restoration.ts +34 -20
- package/src/browser/segment-reconciler.ts +11 -29
- package/src/browser/server-action-bridge.ts +52 -16
- package/src/browser/types.ts +40 -8
- package/src/build/generate-manifest.ts +12 -1
- package/src/build/route-types/codegen.ts +6 -2
- package/src/build/route-types/per-module-writer.ts +94 -61
- package/src/cache/cache-runtime.ts +9 -0
- package/src/cache/cache-scope.ts +2 -1
- package/src/cache/cf/cf-cache-store.ts +2 -0
- package/src/cache/cf/cf-shell-frame.ts +3 -0
- package/src/cache/types.ts +8 -0
- package/src/cache/vercel/vercel-cache-store.ts +6 -1
- package/src/client-urls/client-root.tsx +10 -5
- package/src/client-urls/client-urls.ts +246 -166
- package/src/client-urls/navigation.ts +45 -38
- package/src/client-urls/revalidate-registry.ts +12 -3
- package/src/client-urls/server-projection.ts +286 -75
- package/src/client-urls/types.ts +40 -16
- package/src/client.rsc.tsx +11 -1
- package/src/client.tsx +9 -1
- package/src/decode-loader-results.ts +64 -31
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +1 -0
- package/src/index.ts +12 -4
- package/src/loader-redirect.tsx +2 -3
- package/src/outlet-context.ts +7 -6
- package/src/prefetch-markers.ts +15 -5
- package/src/reverse.ts +3 -3
- package/src/route-content-wrapper.tsx +19 -17
- package/src/route-definition/dsl-helpers.ts +27 -7
- package/src/route-definition/helpers-types.ts +7 -2
- package/src/route-definition/resolve-handler-use.ts +23 -10
- package/src/route-definition/rewrite.ts +86 -19
- package/src/route-types.ts +1 -2
- package/src/router/find-match.ts +37 -1
- package/src/router/handler-context.ts +85 -40
- package/src/router/intercept-resolution.ts +161 -307
- package/src/router/lazy-includes.ts +43 -27
- package/src/router/loader-resolution.ts +21 -1
- package/src/router/manifest.ts +6 -8
- package/src/router/match-api.ts +11 -1
- package/src/router/match-middleware/background-revalidation.ts +2 -4
- package/src/router/match-middleware/cache-lookup.ts +16 -20
- package/src/router/match-middleware/intercept-resolution.ts +41 -60
- package/src/router/match-result.ts +31 -2
- package/src/router/middleware.ts +6 -147
- package/src/router/navigation-snapshot.ts +38 -2
- package/src/router/prefetch-cache-ttl.ts +10 -18
- package/src/router/prerender-match.ts +2 -1
- package/src/router/revalidation.ts +24 -3
- package/src/router/router-context.ts +5 -18
- package/src/router/router-interfaces.ts +0 -7
- package/src/router/router-options.ts +4 -7
- package/src/router/segment-resolution/fresh.ts +6 -2
- package/src/router/segment-resolution/helpers.ts +7 -1
- package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
- package/src/router/segment-resolution/revalidation.ts +75 -35
- package/src/router/segment-resolution.ts +0 -1
- package/src/router/segment-wrappers.ts +8 -13
- package/src/router/substitute-pattern-params.ts +37 -2
- package/src/router/tracing.ts +2 -2
- package/src/router.ts +2 -9
- package/src/rsc/full-payload.ts +26 -4
- package/src/rsc/handler.ts +83 -41
- package/src/rsc/helpers.ts +13 -0
- package/src/rsc/manifest-init.ts +56 -3
- package/src/rsc/prefetch-render-error.ts +38 -0
- package/src/rsc/progressive-enhancement.ts +161 -211
- package/src/rsc/rewrite-passes.ts +56 -9
- package/src/rsc/rsc-rendering.ts +79 -23
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture-constants.ts +22 -0
- package/src/rsc/shell-capture.ts +11 -0
- package/src/rsc/types.ts +5 -0
- package/src/segment-system.tsx +125 -119
- package/src/server/context.ts +96 -5
- package/src/server/handle-store.ts +3 -2
- package/src/server/request-context.ts +42 -3
- package/src/server/request-headers.ts +32 -0
- package/src/server/rewrite-trail.ts +55 -9
- package/src/ssr/ssr-root.tsx +6 -10
- package/src/testing/dispatch.ts +4 -5
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +1 -0
- package/src/testing/internal/client-urls-projection.ts +31 -11
- package/src/testing/render-route.tsx +83 -40
- package/src/testing/run-loader.ts +26 -2
- package/src/testing/run-middleware.ts +10 -0
- package/src/testing/run-revalidate.ts +13 -6
- package/src/testing/serve-shell-request.ts +77 -4
- package/src/testing/shell-status.ts +40 -0
- package/src/testing/vitest-stubs/version.ts +1 -0
- package/src/testing/vitest.ts +74 -7
- package/src/thenable-status.ts +0 -11
- package/src/types/handler-context.ts +12 -1
- package/src/types/loader-types.ts +4 -3
- package/src/types/route-entry.ts +2 -1
- package/src/urls/include-helper.ts +38 -11
- package/src/urls/path-helper-types.ts +4 -2
- package/src/urls/path-helper.ts +16 -3
- package/src/vite/discovery/client-urls-projection.ts +205 -16
- package/src/vite/discovery/discover-routers.ts +2 -2
- package/src/vite/discovery/prerender-collection.ts +2 -6
- package/src/vite/discovery/state.ts +26 -0
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
- package/src/vite/plugins/expose-internal-ids.ts +46 -40
- package/src/vite/plugins/hoist/source.ts +8 -7
- package/src/vite/plugins/transition-when-hoist.ts +1 -0
- package/src/vite/plugins/version-plugin.ts +7 -0
- package/src/vite/plugins/version.d.ts +7 -0
- package/src/vite/plugins/virtual-entries.ts +20 -0
- package/src/vite/rango.ts +4 -1
- package/src/vite/router-discovery.ts +39 -7
- package/src/vite/utils/prerender-utils.ts +17 -0
package/skills/testing/loader.md
CHANGED
|
@@ -8,29 +8,30 @@
|
|
|
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
|
-
| `
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
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
|
+
| `declaredIn` | `string` | Include name prefix of the module whose `layout()` / `parallel()` / `cache()` declares the loader (`"account"`, `""` for the root module). `.name` then resolves in that module, as such a loader does in production, instead of from `routeName`. |
|
|
24
|
+
| `method` | `string` | HTTP method surfaced as `ctx.method`. Defaults to `"GET"`. |
|
|
25
|
+
| `body` | `unknown` | Request body surfaced as `ctx.body`. |
|
|
26
|
+
| `formData` | `FormData` | Form data surfaced as `ctx.formData` (exposed verbatim; no multipart parsing). |
|
|
27
|
+
| `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`. |
|
|
28
|
+
| `use` | `UseResolver` | Dynamic resolver for `ctx.use(OtherLoader)` composition. `loaders` wins when both match. |
|
|
29
|
+
| `cacheStore` | `SegmentCacheStore` | Cache store backing `use cache` functions. Without one, a cached function bypasses and runs uncached (its taint/profile guards never fire). |
|
|
30
|
+
| `cacheProfiles` | `Record<string, CacheProfile>` | Cache profiles, the `createRouter({ cacheProfiles })` shape. |
|
|
31
|
+
| `cache` | `PartialCacheOptions` | Run the loader through the `cache()` a route binds it with (`loader(Loader, () => [cache({...})])` takes the same options), against `cache.store`, else `cacheStore`. See the caveat below. |
|
|
32
|
+
| `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`). |
|
|
33
|
+
| `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. |
|
|
34
|
+
| `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`.) |
|
|
34
35
|
|
|
35
36
|
### Context — `TestLoaderContext<TEnv>` (what your loader receives)
|
|
36
37
|
|
|
@@ -116,7 +117,7 @@ it("asserts a loader's set-cookie + redirect (runLoaderResult)", async () => {
|
|
|
116
117
|
|
|
117
118
|
## Caveats
|
|
118
119
|
|
|
119
|
-
- `ctx.reverse(...)` throws unless you pass `routeMap` (and `routeName` for scoped `.name` resolution). It does NOT fall back to the global route map.
|
|
120
|
+
- `ctx.reverse(...)` throws unless you pass `routeMap` (and `routeName` for scoped `.name` resolution, or `declaredIn` for a layout's or parallel's loader). It does NOT fall back to the global route map.
|
|
120
121
|
- `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.
|
|
121
122
|
- A loader has no `isAction()`: whether it re-runs after an action is decided by `revalidate()` on its registration, not inside the loader body. Assert post-action re-runs at e2e.
|
|
122
123
|
- 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.
|
|
@@ -44,17 +44,17 @@ The `ctx` your middleware reads. Notable fields:
|
|
|
44
44
|
|
|
45
45
|
### Returns — `RunMiddlewareResult<TEnv>`
|
|
46
46
|
|
|
47
|
-
| Field | Type | Meaning
|
|
48
|
-
| ----------------- | ------------------------- |
|
|
49
|
-
| `response` | `Response` | The final Response: the downstream response, or a middleware short-circuit.
|
|
50
|
-
| `ctx` | `RequestContext<TEnv>` | The underlying RequestContext (NOT a per-middleware `MiddlewareContext`). Use `ctx.get(...)` for anything the envelope above doesn't surface.
|
|
51
|
-
| `nextCalled` | `number` | Times the terminal handler ran: `0` on short-circuit, `1` on pass-through.
|
|
52
|
-
| `rewrite` | `RunMiddlewareRewrite?` | The target of a `throw rewrite(...)`: `{ pathname, search, routeName? }`, resolved as production resolves it (a path basename-prefixed, a name through `routeMap`). `undefined` when nothing rewrote. On a rewrite, `response` carries what the chain set before the throw. |
|
|
53
|
-
| `dynamic` | `boolean` | Whether the chain called `ctx.dynamic()` (the PPR shell opt-out).
|
|
54
|
-
| `cookies` | `Record<string, string>` | Effective cookie view: request cookies merged with chain sets/deletes (last-write-wins), as `{ name: value }`.
|
|
55
|
-
| `headers` | `Record<string, string>` | Final response headers as `{ name: value }`, lowercased, EXCLUDING `set-cookie` (use `cookies`).
|
|
56
|
-
| `locationState` | `Record<string, unknown>` | Flat `{ key: value }` state set via `setLocationState()` / `redirect({ state })` (empty when none).
|
|
57
|
-
| `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`.
|
|
47
|
+
| Field | Type | Meaning |
|
|
48
|
+
| ----------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
49
|
+
| `response` | `Response` | The final Response: the downstream response, or a middleware short-circuit. |
|
|
50
|
+
| `ctx` | `RequestContext<TEnv>` | The underlying RequestContext (NOT a per-middleware `MiddlewareContext`). Use `ctx.get(...)` for anything the envelope above doesn't surface. |
|
|
51
|
+
| `nextCalled` | `number` | Times the terminal handler ran: `0` on short-circuit, `1` on pass-through. |
|
|
52
|
+
| `rewrite` | `RunMiddlewareRewrite?` | The target of a `throw rewrite(...)`: `{ pathname, search, routeName?, mutationsDisallowed? }` (`mutationsDisallowed: true` only for `.disallowMutations()`, absent otherwise), resolved as production resolves it (a path basename-prefixed, a name through `routeMap`). `undefined` when nothing rewrote. On a rewrite, `response` carries what the chain set before the throw. |
|
|
53
|
+
| `dynamic` | `boolean` | Whether the chain called `ctx.dynamic()` (the PPR shell opt-out). |
|
|
54
|
+
| `cookies` | `Record<string, string>` | Effective cookie view: request cookies merged with chain sets/deletes (last-write-wins), as `{ name: value }`. |
|
|
55
|
+
| `headers` | `Record<string, string>` | Final response headers as `{ name: value }`, lowercased, EXCLUDING `set-cookie` (use `cookies`). |
|
|
56
|
+
| `locationState` | `Record<string, unknown>` | Flat `{ key: value }` state set via `setLocationState()` / `redirect({ state })` (empty when none). |
|
|
57
|
+
| `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`. |
|
|
58
58
|
|
|
59
59
|
## Recipe
|
|
60
60
|
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
`dispatch` runs the router's REAL matching (reusing `previewMatch`) and the real global + route-level middleware chain, with no RSC render — so redirects, 404s, response routes, content negotiation, and middleware short-circuits behave exactly as in production. You SEED the request and `env`; everything else (matching, middleware, header/cookie merge) is real machinery.
|
|
6
6
|
|
|
7
|
+
Matching goes through the route trie, as every request in `vite dev` and production does.
|
|
8
|
+
|
|
7
9
|
## API
|
|
8
10
|
|
|
9
11
|
### Options — `DispatchOptions<TEnv>`
|
|
@@ -78,7 +80,7 @@ describe("api routes via dispatch", () => {
|
|
|
78
80
|
|
|
79
81
|
`dispatch` also covers, with production code paths:
|
|
80
82
|
|
|
81
|
-
- trailing-slash
|
|
83
|
+
- trailing-slash redirects from the trie — a route with `trailingSlash: "never"` or `"always"` answers the other form with a 308 and the `Location` (query preserved). A route with no `trailingSlash` serves its URL with and without a trailing slash, with no redirect, as production does;
|
|
82
84
|
- the open-redirect guard on browser-followed redirects (a cross-origin `Location` is rewritten to the basename root unless `redirect(url, { external: true })`);
|
|
83
85
|
- content-negotiated routes (`Vary: Accept` appended);
|
|
84
86
|
- cached response routes (`cache({ ... })` around a `path.json` etc.): hit, SWR, and tag writes. The cache write is scheduled via `waitUntil` (a microtask in tests), so flush microtasks between the seeding `dispatch` and the one that asserts the hit;
|
|
@@ -90,6 +92,7 @@ describe("api routes via dispatch", () => {
|
|
|
90
92
|
- A COMPONENT route's guard stack cannot run here. Assert it at e2e, or extract the middleware fn and unit-test it with `runMiddleware` (see `./middleware.md`).
|
|
91
93
|
- JSON serialization is bare, applied in `response-route-handler.ts`: a `path.json` handler that returns a value is serialized verbatim (`JSON.stringify(value)`, status 200, `application/json`) — no envelope. Returning or throwing a `Response` (e.g. `Response.json(x)`) uses the same control-flow path. Any other thrown error takes the error's status (`RouterError.status`, else 500, or a non-200 status already set upstream in the request pipeline). On a `path.json` route the body is an RFC 9457 problem+json object `{ title, status, detail, code }` (`application/problem+json;charset=utf-8`), where `code` is the `RouterError.code`, else `"INTERNAL"`, and the `type` member is omitted. On text/html/xml/md routes the body is `text/plain`: the `RouterError` message, the error message in dev, or `"Internal Server Error"` in production. Assert the shape matching what your handler returns.
|
|
92
94
|
- Setup: needs the preset (alias + virtual stubs) or a Vite-RSC env (see `./setup.md`); a bare router import throws on Vite virtuals.
|
|
95
|
+
- Building the trie runs route discovery: every `include()`d `urls()` handler runs once more, and every async include provider (`() => import(...)`) is called. Count provider calls or handler runs after the first `dispatch` on that router, not from router construction. A provider that rejects fails the `dispatch`, as it fails `vite dev` and the build.
|
|
93
96
|
- A router using `Prerender()`/`createLoader()`/`Static()` now constructs in a bare test (each assigns a runtime fallback `$$id`). Importing the whole router _file_ may still need the plugin (its page modules pull app deps / `virtual:` modules) — build from a focused include (your API routes) for whole-router dispatch.
|
|
94
97
|
- A `throw rewrite(target)` from global or route middleware re-dispatches as the router does: global middleware matched on the target's path, the target's route middleware and handler, the same request context, `x-rango-rewrite` on the response, 508 on a loop. The visitor's URL is matched before middleware runs, so it must be a response route (or a redirect or 404) too; a rewrite target that is a component route throws the same error as a direct request.
|
|
95
98
|
- A `_rsc_partial` request to a response route runs global middleware first (an auth gate can still 401/redirect), then returns `X-RSC-Reload` — route-level middleware is skipped, exactly like production.
|
|
@@ -20,28 +20,29 @@ A `Prerender` route is served as in production: from the artifact `router.matchF
|
|
|
20
20
|
|
|
21
21
|
### Options — `ServeShellRequestOptions`
|
|
22
22
|
|
|
23
|
-
| Field | Type
|
|
24
|
-
| ------------ |
|
|
25
|
-
| `cacheStore` | `SegmentCacheStore`
|
|
26
|
-
| `env` | `TEnv`
|
|
27
|
-
| `headers` | `HeadersInit`
|
|
28
|
-
| `partial` | `true \| { from?: string; segments?: string[]; prefetch?: boolean; fill?: boolean }` | Serve the navigation request the browser sends instead of a document GET: from `from` (default the site root) with `segments` mounted (default none). `prefetch: true` sends it as a `<Link>` prefetch (`X-Rango-Prefetch`), so `prefetch: false` work is deferred. `fill: true` sends the follow-up request (`_rsc_fill=1`) the browser makes after adopting a prefetch; pass the prefetch's delivered ids, without the deferred ones, as `segments`, and the same URL as `from`. |
|
|
29
|
-
| `action` | `{ ref: Function; id: string; args?: unknown[]; segments?: string[] }`
|
|
23
|
+
| Field | Type | Meaning |
|
|
24
|
+
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
25
|
+
| `cacheStore` | `SegmentCacheStore` | The store for this request, in place of the store your `createRouter({ cache })` config returns. The rest of that config (`enabled`, `searchParams`) still applies. Omit it to use the router's own store. |
|
|
26
|
+
| `env` | `TEnv` | Bindings, as `router.fetch(request, { env })` passes them. A function `cache` config receives them. |
|
|
27
|
+
| `headers` | `HeadersInit` | Request headers (e.g. `cookie`). A document request defaults `accept` to `text/html`. |
|
|
28
|
+
| `partial` | `true \| { from?: string; rewriteTarget?: string; segments?: string[]; prefetch?: boolean; fill?: boolean; interceptSource?: string }` | Serve the navigation request the browser sends instead of a document GET: from `from` (default the site root) with `segments` mounted (default none). `prefetch: true` sends it as a `<Link>` prefetch (`X-Rango-Prefetch`), so `prefetch: false` work is deferred. `fill: true` sends the follow-up request (`_rsc_fill=1`) the browser makes after adopting a prefetch; pass the prefetch's delivered ids, without the deferred ones, as `segments`, and the same URL as `from`. `interceptSource` sends the intercept source (`X-RSC-Router-Intercept-Source`) the browser sends with the fill of an intercept, whose `from` is that source too. `rewriteTarget` sends what the page at `from` carried as its payload's `metadata.rewriteTarget` when middleware rewrote it (`_rsc_rt`), as the browser does, so the server diffs against the route that rendered that page. |
|
|
29
|
+
| `action` | `{ ref: Function; id: string; args?: unknown[]; segments?: string[]; rewriteTarget?: string }` | Send the request a JS server action call makes from the page at `url`: a POST of `args` (encoded as the browser encodes them) with `?_rsc_action`, the `rsc-action` header, `X-RSC-Router-Client-Path` and `_rsc_segments` (the page's mounted segment ids, default none), plus the page's `rewriteTarget` as for `partial`. Registers `ref` under `id` (`<module id>#<export name>`). Not with `partial` or `form`. |
|
|
30
30
|
|
|
31
31
|
### Returns — `ServeShellRequestResult`
|
|
32
32
|
|
|
33
|
-
| Field | Type | Meaning
|
|
34
|
-
| -------------- | ------------------------------------------------- |
|
|
35
|
-
| `shellStatus` | `"HIT" \| "MISS" \| null` | `x-rango-shell`; null when the serve path did not consider the request (not a `ppr` route, a nonce'd request, a partial request).
|
|
36
|
-
| `replayStatus` | `PprReplayStatus \| null` | `x-rango-ppr-replay` on a partial request: `{ outcome: "HIT", freshness }` or `{ outcome: "BYPASS", reason }`.
|
|
37
|
-
| `prelude` | `string \| undefined` | The prelude a HIT served (the capture's Flight text). `undefined` unless `shellStatus` is `"HIT"`.
|
|
38
|
-
| `flight` | `string \| undefined` | The Flight payload this request rendered: a HIT's tail, a document render, or a partial response. `undefined` when no Flight rendered (a redirect, a middleware response).
|
|
39
|
-
| `key` | `string` | The shell key the serve path resolved for a document it read (MISS or HIT), request partition included. Otherwise (no `ppr`, a partial request) the URL's key without a partition.
|
|
40
|
-
| `readEntry` | `() => Promise<ShellCacheEntry \| null>` | Reads the document entry under `key` from the request's store (a passive `getShell`). A read: on a store with a shell memo it warms the memo, so call it after the reads you count.
|
|
41
|
-
| `readHandles` | `() => Promise<ShellRequestHandles \| undefined>` | Decodes the response's handle data as the browser reads it: `{ hydration, late }` (see "Handles and loader data"). `undefined` when no Flight rendered.
|
|
42
|
-
| `readDeferred` | `() => Promise<string[] \| undefined>` | The ids of the segments the payload marks deferred (a prefetch skipped them; the fill renders them), in payload order. `[]` when nothing was deferred, `undefined` when no Flight rendered.
|
|
43
|
-
| `
|
|
44
|
-
| `
|
|
33
|
+
| Field | Type | Meaning |
|
|
34
|
+
| -------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
35
|
+
| `shellStatus` | `"HIT" \| "MISS" \| null` | `x-rango-shell`; null when the serve path did not consider the request (not a `ppr` route, a nonce'd request, a partial request). |
|
|
36
|
+
| `replayStatus` | `PprReplayStatus \| null` | `x-rango-ppr-replay` on a partial request: `{ outcome: "HIT", freshness }` or `{ outcome: "BYPASS", reason }`. |
|
|
37
|
+
| `prelude` | `string \| undefined` | The prelude a HIT served (the capture's Flight text). `undefined` unless `shellStatus` is `"HIT"`. |
|
|
38
|
+
| `flight` | `string \| undefined` | The Flight payload this request rendered: a HIT's tail, a document render, or a partial response. `undefined` when no Flight rendered (a redirect, a middleware response). |
|
|
39
|
+
| `key` | `string` | The shell key the serve path resolved for a document it read (MISS or HIT), request partition included. Otherwise (no `ppr`, a partial request) the URL's key without a partition. |
|
|
40
|
+
| `readEntry` | `() => Promise<ShellCacheEntry \| null>` | Reads the document entry under `key` from the request's store (a passive `getShell`). A read: on a store with a shell memo it warms the memo, so call it after the reads you count. |
|
|
41
|
+
| `readHandles` | `() => Promise<ShellRequestHandles \| undefined>` | Decodes the response's handle data as the browser reads it: `{ hydration, late }` (see "Handles and loader data"). `undefined` when no Flight rendered. |
|
|
42
|
+
| `readDeferred` | `() => Promise<string[] \| undefined>` | The ids of the segments the payload marks deferred (a prefetch skipped them; the fill renders them), in payload order. `[]` when nothing was deferred, `undefined` when no Flight rendered. |
|
|
43
|
+
| `readAction` | `() => Promise<ShellRequestAction \| undefined>` | For an `action` request, the call's answer as the browser reads it: `{ ok, result, revalidationError }` (see "A server action called from the page"). `undefined` when no Flight rendered or the payload carries no action result. |
|
|
44
|
+
| `response` | `Response` | Status and headers. Its body is already read. |
|
|
45
|
+
| `body` | `string` | The body text. |
|
|
45
46
|
|
|
46
47
|
### `resetShellTestState(): Promise<void>`
|
|
47
48
|
|
|
@@ -204,6 +205,22 @@ expect(result.key).toBe(
|
|
|
204
205
|
|
|
205
206
|
A partial request has no HTML step, so its `key` is the URL's key without a partition: read a partitioned route's entry from a document request's result.
|
|
206
207
|
|
|
208
|
+
## A rewritten request
|
|
209
|
+
|
|
210
|
+
A request middleware rewrote to a `ppr` route reads and captures no shell unless the rewrite picks one with `.shell()` (`/ppr`, "A request middleware rewrote"): its `shellStatus` is `null`. Under `.shell("visitor")` its shell is keyed by the visitor's URL and the target; `visitorShellCacheKey` builds that key (the visitor's URL, then the target; the target route's `cache.searchParams` and partition go fourth and fifth, as they go third and fourth for `shellCacheKey`):
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
// router.use((ctx, next) => {
|
|
214
|
+
// if (ctx.pathname === "/summer") throw rewrite("/item/1").shell("visitor");
|
|
215
|
+
// return next();
|
|
216
|
+
// })
|
|
217
|
+
await serveShellRequest(router, "/summer", { cacheStore }); // MISS + capture
|
|
218
|
+
const hit = await serveShellRequest(router, "/summer", { cacheStore });
|
|
219
|
+
expect(hit.shellStatus).toBe("HIT");
|
|
220
|
+
expect(hit.key).toBe(visitorShellCacheKey(router, "/summer", "/item/1"));
|
|
221
|
+
expect((await hit.readEntry())?.pathname).toBe("/summer"); // replayed on a HIT
|
|
222
|
+
```
|
|
223
|
+
|
|
207
224
|
## Two routers on one cache store
|
|
208
225
|
|
|
209
226
|
A shell's key starts with its router's id (`shellCacheKey(router, url)`), so two routers that share a store and serve the same host and path each read and write their own (why and when: `/host-router`, "Shared cache store"). Serve both against one store to pin it:
|
|
@@ -276,6 +293,33 @@ expect(await fill.readDeferred()).toEqual([]); // a fill defers nothing
|
|
|
276
293
|
|
|
277
294
|
`deliveredIds` is the payload's `metadata.matched`, read from `prefetch.flight`. Count the loader's runs in your own fixture: the prefetch ran it zero times, the fill once.
|
|
278
295
|
|
|
296
|
+
### Intercepts
|
|
297
|
+
|
|
298
|
+
A prefetch of an `intercept()`'s target from its source page defers the intercept's flagged loaders, loader segments of its slot (`${parent}.@modalD${i}.${$$id}`). The browser sends the fill of an intercept from the source, with the intercept source header: pass both.
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
// intercept("@modal", ".item", ItemModal, () => [
|
|
302
|
+
// loader(PriceLoader),
|
|
303
|
+
// loader(ReviewsLoader, { prefetch: false }),
|
|
304
|
+
// ])
|
|
305
|
+
const prefetch = await serveShellRequest(router, "/item/1", {
|
|
306
|
+
partial: { from: "/list", segments: listHeld, prefetch: true },
|
|
307
|
+
});
|
|
308
|
+
expect(await prefetch.readDeferred()).toHaveLength(1); // ReviewsLoader's segment
|
|
309
|
+
|
|
310
|
+
const fill = await serveShellRequest(router, "/item/1", {
|
|
311
|
+
partial: {
|
|
312
|
+
from: "/list",
|
|
313
|
+
interceptSource: "/list",
|
|
314
|
+
segments: held, // the prefetch's metadata.matched without the deferred id
|
|
315
|
+
fill: true,
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
// The fill runs ReviewsLoader alone: not the modal's handler, nor PriceLoader.
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
The pinned example is `src/testing/__tests__/prefetch-false-intercept.rsc-test.tsx`.
|
|
322
|
+
|
|
279
323
|
### `clientUrls()` routes
|
|
280
324
|
|
|
281
325
|
A router that mounts a `clientUrls()` group serves its routes here as in your app. The group is the default export of a `"use client"` module, a client reference in this project, and `rangoUseClientTransform()` ([`./setup.md`](./setup.md)) installs its server projection as the rango plugin's discovery does. Mount the module itself:
|
|
@@ -295,7 +339,7 @@ const prefetch = await serveShellRequest(router, "/shop/product/1", {
|
|
|
295
339
|
expect(await prefetch.readDeferred()).toHaveLength(1); // ReviewsLoader's segment
|
|
296
340
|
```
|
|
297
341
|
|
|
298
|
-
`held` is the `metadata.matched` of a navigation to `/shop`. On a group route only a loader's own flag defers (the group's, and a server layout's above the group); a flagged `loading()` defers nothing there.
|
|
342
|
+
`held` is the `metadata.matched` of a navigation to `/shop`. On a group route only a loader's own flag defers (the group's, and a server layout's above the group); a flagged `loading()` defers nothing there. A loader inside the group's `intercept()` takes the flag, as in `urls()` ("Intercepts" above). `dispatch` matches the group's routes through the same projection.
|
|
299
343
|
|
|
300
344
|
The transform also registers each group loader's `revalidate()` predicates, which Vitest leaves as plain functions in the module, so the server runs them as the hoisted predicates run in your app. A `partial` navigation, or an `action` with the page's `segments`, reaches them with the `urls()` arguments, `actionResult` and `formData` included.
|
|
301
345
|
|
|
@@ -392,9 +436,29 @@ const result = await serveShellRequest(router, "/shop", {
|
|
|
392
436
|
|
|
393
437
|
This is the request a `clientUrls()` group's `revalidate()` sees on an action: the action's return value as `actionResult`, its last `FormData` argument as `formData`. The pinned example is `src/testing/__tests__/client-urls-revalidate.rsc-test.tsx`. Not with `partial` or `form`.
|
|
394
438
|
|
|
439
|
+
`result.readAction()` reads the call's answer as `browser/server-action-bridge.ts` reads the response: `ok` (false when the action threw, and the call rejects with `result`), `result` (what the call resolves with), and `revalidationError`, the error of the first loader that threw while it re-ran in the action's revalidation, the value `useAction(action).revalidationError` reports (`null` when none did). Such a loader does not fail the call: in the browser it throws at its `useLoader` read, where the nearest error boundary catches it. A `notFound()` or `redirect()` from the loader is not an error.
|
|
440
|
+
|
|
441
|
+
```tsx
|
|
442
|
+
const answer = await (
|
|
443
|
+
await serveShellRequest(router, "/stock", {
|
|
444
|
+
action: {
|
|
445
|
+
ref: saveStock,
|
|
446
|
+
id: "src/actions/stock.ts#saveStock",
|
|
447
|
+
segments: held,
|
|
448
|
+
},
|
|
449
|
+
})
|
|
450
|
+
).readAction();
|
|
451
|
+
expect(answer).toEqual({
|
|
452
|
+
ok: true,
|
|
453
|
+
result: { saved: true },
|
|
454
|
+
// a build redacts the message; onError gets the error itself
|
|
455
|
+
revalidationError: expect.objectContaining({ message: "An error occurred" }),
|
|
456
|
+
});
|
|
457
|
+
```
|
|
458
|
+
|
|
395
459
|
## A page middleware rewrites
|
|
396
460
|
|
|
397
|
-
A request a middleware rewrites (`throw rewrite()`, see `/middleware`) runs through the same production handler: the visitor's pass, then the target's, with one request context. Assert what the visitor gets: the target's body and status, `x-rango-rewrite`, the visitor's pathname in the payload, and on a `ppr` target the
|
|
461
|
+
A request a middleware rewrites (`throw rewrite()`, see `/middleware`) runs through the same production handler: the visitor's pass, then the target's, with one request context. Assert what the visitor gets: the target's body and status, `x-rango-rewrite`, the visitor's pathname in the payload, and on a `ppr` target no shell (`shellStatus` `null`) unless the rewrite picked one (see "A rewritten request" above).
|
|
398
462
|
|
|
399
463
|
```ts
|
|
400
464
|
it("a locked page renders the maintenance route at its own URL", async () => {
|