@rangojs/router 0.20.0 → 0.22.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 +39 -15
- package/dist/types/browser/event-controller.d.ts +49 -0
- package/dist/types/browser/history-state.d.ts +16 -4
- package/dist/types/browser/navigation-store.d.ts +9 -0
- package/dist/types/browser/navigation-transaction.d.ts +7 -0
- package/dist/types/browser/partial-update.d.ts +2 -1
- package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
- package/dist/types/browser/rango-state.d.ts +3 -1
- package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
- package/dist/types/browser/react/context.d.ts +25 -0
- package/dist/types/browser/react/location-state-shared.d.ts +84 -10
- package/dist/types/browser/react/location-state.d.ts +7 -2
- package/dist/types/browser/rsc-router.d.ts +13 -2
- package/dist/types/browser/types.d.ts +10 -0
- package/dist/types/build/generate-manifest.d.ts +2 -0
- package/dist/types/cache/background-task.d.ts +22 -0
- package/dist/types/cache/cache-key-utils.d.ts +50 -5
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +5 -6
- package/dist/types/cache/cf/cf-cache-store.d.ts +34 -7
- package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
- package/dist/types/cache/document-cache.d.ts +8 -5
- package/dist/types/cache/handle-snapshot.d.ts +59 -50
- package/dist/types/cache/index.d.ts +3 -1
- package/dist/types/cache/invalidation-order.d.ts +14 -0
- package/dist/types/cache/memory-segment-store.d.ts +33 -1
- package/dist/types/cache/shell-snapshot.d.ts +29 -4
- package/dist/types/cache/store-scope.d.ts +22 -0
- package/dist/types/cache/tag-invalidation.d.ts +7 -6
- package/dist/types/cache/types.d.ts +84 -24
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +35 -17
- package/dist/types/client-urls/navigation.d.ts +7 -1
- package/dist/types/client-urls/optimistic-location.d.ts +10 -4
- package/dist/types/errors.d.ts +6 -0
- package/dist/types/handles/deferred-resolution.d.ts +18 -1
- package/dist/types/internal-suspense-audit.d.ts +1 -0
- package/dist/types/loader-store.d.ts +4 -8
- package/dist/types/outlet-provider.d.ts +2 -1
- package/dist/types/prerender/build-shell-capture.d.ts +8 -6
- package/dist/types/prerender/cloudflare.d.ts +49 -0
- package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
- package/dist/types/prerender/index.d.ts +12 -0
- package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
- package/dist/types/prerender/on-demand.d.ts +272 -0
- package/dist/types/prerender/producer-guard.d.ts +33 -0
- package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
- package/dist/types/prerender/warm-request.d.ts +65 -0
- package/dist/types/prerender/warm.d.ts +65 -0
- package/dist/types/prerender/writable-store.d.ts +170 -0
- package/dist/types/prerender.d.ts +27 -0
- package/dist/types/reverse.d.ts +1 -1
- package/dist/types/route-content-wrapper.d.ts +3 -1
- package/dist/types/router/error-handling.d.ts +2 -0
- package/dist/types/router/handler-context.d.ts +1 -1
- package/dist/types/router/match-middleware/cache-lookup.d.ts +15 -1
- package/dist/types/router/navigation-snapshot.d.ts +8 -1
- package/dist/types/router/pattern-matching.d.ts +2 -0
- package/dist/types/router/prefetch-default.d.ts +4 -4
- package/dist/types/router/prerender-match.d.ts +23 -1
- package/dist/types/router/route-trie-builder.d.ts +8 -1
- package/dist/types/router/router-context.d.ts +1 -0
- package/dist/types/router/router-interfaces.d.ts +32 -1
- package/dist/types/router/router-options.d.ts +39 -5
- package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +23 -10
- package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
- package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
- package/dist/types/router/trie-matching.d.ts +2 -0
- package/dist/types/router-versions.d.ts +39 -0
- package/dist/types/rsc/handler-context.d.ts +5 -0
- package/dist/types/rsc/shell-build-manifest.d.ts +9 -6
- package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
- package/dist/types/rsc/shell-capture.d.ts +14 -1
- package/dist/types/rsc/shell-serve.d.ts +2 -2
- package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
- package/dist/types/rsc/types.d.ts +12 -6
- package/dist/types/segment-boundary-content.d.ts +17 -0
- package/dist/types/segment-loader-promise.d.ts +2 -6
- package/dist/types/server/build-version-table.d.ts +53 -0
- package/dist/types/server/context.d.ts +12 -4
- package/dist/types/server/handle-store.d.ts +89 -46
- package/dist/types/server/request-context.d.ts +84 -9
- package/dist/types/suspense-audit-react.d.ts +26 -0
- package/dist/types/suspense-audit.d.ts +157 -0
- package/dist/types/testing/action-form.d.ts +34 -0
- package/dist/types/testing/build-versions.d.ts +32 -0
- package/dist/types/testing/dom.entry.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +3 -1
- package/dist/types/testing/index.d.ts +6 -0
- package/dist/types/testing/internal/seed-vars.d.ts +7 -2
- package/dist/types/testing/internal/server-modules.d.ts +8 -0
- package/dist/types/testing/location-state-key.d.ts +5 -1
- package/dist/types/testing/render-route.d.ts +77 -6
- package/dist/types/testing/serve-shell-request.d.ts +55 -4
- package/dist/types/testing/shell-status.d.ts +14 -6
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/thenable-status.d.ts +6 -0
- package/dist/types/types/handler-context.d.ts +15 -5
- package/dist/types/types/route-entry.d.ts +5 -0
- package/dist/types/urls/path-helper.d.ts +18 -0
- package/dist/types/vite/discovery/build-versions.d.ts +393 -0
- package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
- package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
- package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
- package/dist/types/vite/discovery/state.d.ts +25 -0
- package/dist/types/vite/encryption-key.d.ts +18 -0
- package/dist/types/vite/inject-client-debug.d.ts +6 -25
- package/dist/types/vite/plugin-types.d.ts +27 -0
- package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
- package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
- package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
- package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
- package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
- package/dist/types/vite/plugins/virtual-entries.d.ts +18 -1
- package/dist/types/vite/router-discovery.d.ts +6 -0
- package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
- package/dist/types/vite/utils/client-chunks.d.ts +2 -0
- package/dist/types/vite/utils/compare-strings.d.ts +7 -0
- package/dist/types/vite/utils/package-resolution.d.ts +8 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
- package/dist/vite/index.js +1707 -931
- package/package.json +12 -3
- package/skills/cache-guide/SKILL.md +102 -37
- package/skills/caching/SKILL.md +56 -9
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +16 -0
- package/skills/cloudflare/SKILL.md +133 -1
- package/skills/comparison/references/framework-comparison.md +17 -13
- package/skills/deployment-caching/SKILL.md +33 -12
- package/skills/document-cache/SKILL.md +16 -14
- package/skills/hooks/SKILL.md +25 -0
- package/skills/hooks/handle-and-actions.md +5 -0
- package/skills/hooks/navigation.md +38 -0
- package/skills/hooks/state.md +213 -11
- package/skills/hooks/urls.md +40 -1
- package/skills/host-router/SKILL.md +31 -0
- package/skills/intercept/SKILL.md +6 -0
- package/skills/loader/SKILL.md +25 -5
- package/skills/middleware/SKILL.md +2 -0
- package/skills/migrate-nextjs/SKILL.md +82 -9
- package/skills/ppr/SKILL.md +150 -48
- package/skills/prerender/SKILL.md +651 -10
- package/skills/rango/SKILL.md +9 -7
- package/skills/response-routes/SKILL.md +5 -5
- package/skills/route/SKILL.md +29 -0
- package/skills/router-setup/SKILL.md +15 -1
- package/skills/testing/SKILL.md +32 -27
- package/skills/testing/cache-prerender.md +18 -17
- package/skills/testing/client-components.md +193 -8
- package/skills/testing/handles.md +2 -0
- package/skills/testing/loader.md +1 -1
- package/skills/testing/middleware.md +1 -1
- package/skills/testing/render-handler.md +18 -18
- package/skills/testing/serve-shell-request.md +167 -22
- package/skills/testing/server-actions.md +17 -17
- package/skills/testing/server-tree.md +1 -1
- package/skills/typesafety/generated-files-and-cli.md +1 -1
- package/skills/use-cache/SKILL.md +25 -10
- package/skills/vercel/SKILL.md +5 -3
- package/skills/view-transitions/SKILL.md +9 -4
- package/src/bin/rango.ts +1 -1
- package/src/browser/event-controller.ts +119 -2
- package/src/browser/history-state.ts +39 -17
- package/src/browser/link-interceptor.ts +2 -2
- package/src/browser/navigation-bridge.ts +104 -41
- package/src/browser/navigation-store.ts +26 -0
- package/src/browser/navigation-transaction.ts +24 -12
- package/src/browser/network-error-handler.ts +2 -0
- package/src/browser/partial-update.ts +58 -14
- package/src/browser/prefetch/default-strategy.ts +22 -42
- package/src/browser/rango-state.ts +3 -1
- package/src/browser/react/Link.tsx +37 -43
- package/src/browser/react/NavigationProvider.tsx +85 -1
- package/src/browser/react/context.ts +30 -0
- package/src/browser/react/location-state-shared.ts +200 -34
- package/src/browser/react/location-state.ts +28 -77
- package/src/browser/react/use-action.ts +4 -1
- package/src/browser/react/use-handle.ts +3 -0
- package/src/browser/react/use-link-status.ts +4 -3
- package/src/browser/react/use-navigation.ts +4 -1
- package/src/browser/react/use-params.ts +4 -1
- package/src/browser/react/use-pathname.ts +4 -1
- package/src/browser/react/use-search-params.ts +5 -3
- package/src/browser/react/use-segments.ts +36 -54
- package/src/browser/rsc-router.tsx +93 -31
- package/src/browser/segment-reconciler.ts +3 -3
- package/src/browser/server-action-bridge.ts +43 -7
- package/src/browser/types.ts +11 -0
- package/src/build/generate-manifest.ts +17 -0
- package/src/build/merge-full-manifests.ts +5 -0
- package/src/build/route-trie.ts +1 -0
- package/src/cache/background-task.ts +61 -0
- package/src/cache/cache-key-utils.ts +91 -12
- package/src/cache/cache-policy.ts +11 -2
- package/src/cache/cache-runtime.ts +23 -23
- package/src/cache/cache-scope.ts +48 -36
- package/src/cache/cf/cf-cache-store.ts +91 -66
- package/src/cache/cf/cf-cache-types.ts +12 -4
- package/src/cache/document-cache.ts +35 -14
- package/src/cache/handle-snapshot.ts +118 -86
- package/src/cache/index.ts +5 -0
- package/src/cache/invalidation-order.ts +53 -0
- package/src/cache/memory-segment-store.ts +52 -1
- package/src/cache/shell-snapshot.ts +52 -5
- package/src/cache/store-scope.ts +36 -0
- package/src/cache/tag-invalidation.ts +7 -5
- package/src/cache/types.ts +90 -24
- package/src/cache/vercel/vercel-cache-store.ts +68 -26
- package/src/client-urls/client-root.tsx +1 -0
- package/src/client-urls/navigation.ts +10 -0
- package/src/client-urls/optimistic-location.ts +10 -4
- package/src/client.tsx +40 -3
- package/src/errors.ts +13 -6
- package/src/handles/deferred-resolution.ts +36 -0
- package/src/internal-suspense-audit.ts +12 -0
- package/src/loader-store.ts +4 -9
- package/src/outlet-provider.tsx +25 -1
- package/src/prerender/build-shell-capture.ts +11 -9
- package/src/prerender/cloudflare.ts +130 -0
- package/src/prerender/create-prerender-trigger.ts +780 -0
- package/src/prerender/index.ts +39 -0
- package/src/prerender/memory-prerender-store.ts +115 -0
- package/src/prerender/on-demand.ts +339 -0
- package/src/prerender/producer-guard.ts +66 -0
- package/src/prerender/shell-manifest-key.ts +12 -17
- package/src/prerender/warm-request.ts +154 -0
- package/src/prerender/warm.ts +193 -0
- package/src/prerender/writable-store.ts +278 -0
- package/src/prerender.ts +29 -0
- package/src/reverse.ts +1 -1
- package/src/route-content-wrapper.tsx +107 -13
- package/src/router/error-handling.ts +12 -12
- package/src/router/find-match.ts +1 -0
- package/src/router/handler-context.ts +2 -0
- package/src/router/intercept-resolution.ts +15 -8
- package/src/router/loader-resolution.ts +2 -10
- package/src/router/manifest.ts +2 -0
- package/src/router/match-middleware/background-revalidation.ts +2 -0
- package/src/router/match-middleware/cache-lookup.ts +239 -54
- package/src/router/navigation-snapshot.ts +25 -6
- package/src/router/pattern-matching.ts +14 -0
- package/src/router/prefetch-default.ts +4 -4
- package/src/router/prerender-match.ts +460 -352
- package/src/router/request-classification.ts +15 -12
- package/src/router/route-trie-builder.ts +9 -0
- package/src/router/router-context.ts +1 -0
- package/src/router/router-interfaces.ts +35 -1
- package/src/router/router-options.ts +45 -5
- package/src/router/segment-resolution/fresh.ts +20 -19
- package/src/router/segment-resolution/loader-cache.ts +110 -77
- package/src/router/segment-resolution/revalidation.ts +34 -24
- package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
- package/src/router/trie-matching.ts +3 -0
- package/src/router-versions.ts +60 -0
- package/src/router.ts +107 -1
- package/src/rsc/full-payload.ts +6 -2
- package/src/rsc/handler-context.ts +5 -0
- package/src/rsc/handler.ts +64 -7
- package/src/rsc/progressive-enhancement.ts +5 -1
- package/src/rsc/response-cache-serve.ts +10 -4
- package/src/rsc/rsc-rendering.ts +65 -12
- package/src/rsc/shell-build-manifest.ts +17 -12
- package/src/rsc/shell-capture-constants.ts +39 -7
- package/src/rsc/shell-capture.ts +144 -45
- package/src/rsc/shell-serve.ts +12 -6
- package/src/rsc/ssr-module-loader.ts +26 -0
- package/src/rsc/types.ts +12 -6
- package/src/segment-boundary-content.ts +25 -0
- package/src/segment-loader-promise.ts +28 -38
- package/src/segment-system.tsx +117 -57
- package/src/server/build-version-table.ts +130 -0
- package/src/server/context.ts +40 -17
- package/src/server/cookie-store.ts +5 -0
- package/src/server/handle-store.ts +357 -127
- package/src/server/request-context.ts +149 -12
- package/src/ssr/ssr-root.tsx +32 -13
- package/src/suspense-audit-react.tsx +98 -0
- package/src/suspense-audit.ts +1030 -0
- package/src/testing/action-form.ts +66 -0
- package/src/testing/build-versions.ts +64 -0
- package/src/testing/dispatch.ts +6 -0
- package/src/testing/dom.entry.ts +1 -0
- package/src/testing/flight-runtime.d.ts +27 -0
- package/src/testing/flight.entry.ts +4 -0
- package/src/testing/index.ts +22 -0
- package/src/testing/internal/context.ts +2 -1
- package/src/testing/internal/seed-vars.ts +14 -2
- package/src/testing/internal/server-modules.ts +32 -0
- package/src/testing/location-state-key.ts +18 -8
- package/src/testing/render-handler.ts +2 -1
- package/src/testing/render-route.tsx +293 -46
- package/src/testing/serve-shell-request.ts +262 -37
- package/src/testing/shell-status.ts +17 -6
- package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
- package/src/testing/vitest-stubs/version.ts +4 -1
- package/src/thenable-status.ts +10 -0
- package/src/types/handler-context.ts +15 -5
- package/src/types/route-entry.ts +6 -0
- package/src/urls/path-helper.ts +69 -9
- package/src/use-loader.tsx +57 -30
- package/src/vite/discovery/build-versions.ts +1281 -0
- package/src/vite/discovery/bundle-postprocess.ts +4 -0
- package/src/vite/discovery/discover-routers.ts +15 -0
- package/src/vite/discovery/installed-packages.ts +159 -0
- package/src/vite/discovery/prerender-collection.ts +38 -1
- package/src/vite/discovery/router-versions-phase.ts +336 -0
- package/src/vite/discovery/shell-prerender-phase.ts +33 -43
- package/src/vite/discovery/state.ts +36 -1
- package/src/vite/discovery/virtual-module-codegen.ts +7 -2
- package/src/vite/encryption-key.ts +101 -9
- package/src/vite/inject-client-debug.ts +42 -20
- package/src/vite/plugin-types.ts +28 -0
- package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
- package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
- package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
- package/src/vite/plugins/expose-internal-ids.ts +12 -3
- package/src/vite/plugins/stable-rsc-output.ts +147 -0
- package/src/vite/plugins/version-plugin.ts +19 -24
- package/src/vite/plugins/version.d.ts +17 -2
- package/src/vite/plugins/virtual-entries.ts +39 -6
- package/src/vite/rango.ts +28 -3
- package/src/vite/router-discovery.ts +96 -13
- package/src/vite/utils/bundle-analysis.ts +39 -7
- package/src/vite/utils/client-chunks.ts +5 -1
- package/src/vite/utils/compare-strings.ts +9 -0
- package/src/vite/utils/package-resolution.ts +9 -0
- package/src/vite/utils/prerender-utils.ts +4 -1
- package/src/vite/utils/shared-utils.ts +1 -1
- package/dist/types/segment-content-promise.d.ts +0 -13
- package/src/segment-content-promise.ts +0 -67
|
@@ -14,6 +14,8 @@ The one stub is the HTML step (the SSR module), because `react-dom/server` does
|
|
|
14
14
|
|
|
15
15
|
So `prelude` is the shell as captured (Flight text, not HTML): a value the shell froze appears there, and a hole (a live loader under `loading()`) does not.
|
|
16
16
|
|
|
17
|
+
A `Prerender` route is served as in production: from the artifact `router.matchForPrerender` bakes for the URL (on its first request, kept until `resetShellTestState()`), through the production prerender store, so its handler runs once, at the bake, and never on a request. Its first `ppr` request is a MISS with a runtime capture, as a URL without a build-time shell is in production. The shell `vite build` bakes (served first in production for a URL without a query string) is not reproduced: keep that in e2e. The `env` option doubles as the bake's `buildEnv`. An on-demand route (`Prerender(..., { onDemand })`) is baked only when it is wrapped in `Passthrough()` and its `getParams()` lists the param, as a build bakes it; a plain on-demand route is not baked here, so its pages come from the prerender store alone (`router.prerender()`), and "a removed page the build baked" is a case for a `Passthrough` route or for e2e.
|
|
18
|
+
|
|
17
19
|
## API
|
|
18
20
|
|
|
19
21
|
### Options — `ServeShellRequestOptions`
|
|
@@ -27,20 +29,21 @@ So `prelude` is the shell as captured (Flight text, not HTML): a value the shell
|
|
|
27
29
|
|
|
28
30
|
### Returns — `ServeShellRequestResult`
|
|
29
31
|
|
|
30
|
-
| Field | Type
|
|
31
|
-
| -------------- |
|
|
32
|
-
| `shellStatus` | `"HIT" \| "MISS" \| null`
|
|
33
|
-
| `replayStatus` | `PprReplayStatus \| null`
|
|
34
|
-
| `prelude` | `string \| undefined`
|
|
35
|
-
| `flight` | `string \| undefined`
|
|
36
|
-
| `key` | `string`
|
|
37
|
-
| `readEntry` | `() => Promise<ShellCacheEntry \| null>`
|
|
38
|
-
| `
|
|
39
|
-
| `
|
|
32
|
+
| Field | Type | Meaning |
|
|
33
|
+
| -------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
34
|
+
| `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). |
|
|
35
|
+
| `replayStatus` | `PprReplayStatus \| null` | `x-rango-ppr-replay` on a partial request: `{ outcome: "HIT", freshness }` or `{ outcome: "BYPASS", reason }`. |
|
|
36
|
+
| `prelude` | `string \| undefined` | The prelude a HIT served (the capture's Flight text). `undefined` unless `shellStatus` is `"HIT"`. |
|
|
37
|
+
| `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). |
|
|
38
|
+
| `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. |
|
|
39
|
+
| `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. |
|
|
40
|
+
| `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. |
|
|
41
|
+
| `response` | `Response` | Status and headers. Its body is already read. |
|
|
42
|
+
| `body` | `string` | The body text. |
|
|
40
43
|
|
|
41
44
|
### `resetShellTestState(): Promise<void>`
|
|
42
45
|
|
|
43
|
-
A worker keeps some PPR state across requests, and so does the test process across tests: the capture's stampede guard and backoff (a refused capture backs its URL off for later tests too), the capture's and the serve path's once-per-key warnings, the build-shell manifest memo, and `CFCacheStore`'s isolate memos (shells, tag markers, tag hints), which every `CFCacheStore` shares by namespace and URL — a later test's first request can be a HIT from an earlier test's shell. `resetShellTestState()` clears all of it. Call it in `beforeEach`, never while a request is in flight. `VercelCacheStore`'s memos live on the `cache` handle you pass it: a new handle per test starts empty.
|
|
46
|
+
A worker keeps some PPR state across requests, and so does the test process across tests: the capture's stampede guard and backoff (a refused capture backs its URL off for later tests too), the capture's and the serve path's once-per-key warnings, the build-shell manifest memo, the `Prerender` artifacts baked so far, and `CFCacheStore`'s isolate memos (shells, tag markers, tag hints), which every `CFCacheStore` shares by namespace and URL — a later test's first request can be a HIT from an earlier test's shell. `resetShellTestState()` clears all of it. Call it in `beforeEach`, never while a request is in flight. `VercelCacheStore`'s memos live on the `cache` handle you pass it: a new handle per test starts empty.
|
|
44
47
|
|
|
45
48
|
## Recipe
|
|
46
49
|
|
|
@@ -99,6 +102,35 @@ it("replays the captured segments on a client navigation", async () => {
|
|
|
99
102
|
|
|
100
103
|
A store built per request from a function `cache` config gets the request's execution context (`ctx`), so its own `waitUntil` writes (`CFCacheStore({ ctx })`) settle with the call. A store built outside the request with its own `waitUntil` (`VercelCacheStore`'s option) is not settled by the call. For `CFCacheStore`, stub the `caches` global and pass a KV double in `env`; for `VercelCacheStore`, pass a `cache` double. A store's shell memo then behaves as in production (count your double's reads across two HITs).
|
|
101
104
|
|
|
105
|
+
## Simulating a deploy: setBuildVersions
|
|
106
|
+
|
|
107
|
+
A stored shell is stamped with the router's document version and a HIT requires the stamp to match. In a build, `vite build` computes that version per `createRouter()`; under test there is no build, so the primitives run unversioned. `setBuildVersions` (from `@rangojs/router/testing`) installs the versions a build would ship, and calling it again with a different `document` is a deploy that changed the router:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { setBuildVersions } from "@rangojs/router/testing";
|
|
111
|
+
|
|
112
|
+
afterEach(() => setBuildVersions()); // remove the versions: unversioned again
|
|
113
|
+
|
|
114
|
+
it("a deploy that changes the document version retires the shell", async () => {
|
|
115
|
+
const cacheStore = new MemorySegmentCacheStore();
|
|
116
|
+
setBuildVersions({ data: "d1", document: "h1" });
|
|
117
|
+
|
|
118
|
+
expect(
|
|
119
|
+
(await serveShellRequest(router, "/product/4", { cacheStore })).shellStatus,
|
|
120
|
+
).toBe("MISS");
|
|
121
|
+
expect(
|
|
122
|
+
(await serveShellRequest(router, "/product/4", { cacheStore })).shellStatus,
|
|
123
|
+
).toBe("HIT");
|
|
124
|
+
|
|
125
|
+
setBuildVersions({ data: "d1", document: "h2" }); // new SSR output or client assets
|
|
126
|
+
expect(
|
|
127
|
+
(await serveShellRequest(router, "/product/4", { cacheStore })).shellStatus,
|
|
128
|
+
).toBe("MISS");
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`setBuildVersions({ data, document, routers?: { [routerId]: { data, document } } })` takes the whole-build pair and, optionally, a pair per router id (a router with no entry gets the whole-build pair). `setBuildVersions()` with no argument removes them. The types `BuildVersions` and `RouterVersions` are exported too. `dispatch` sees the same versions.
|
|
133
|
+
|
|
102
134
|
## A HIT runs no handler
|
|
103
135
|
|
|
104
136
|
A HIT replays what the capture's handlers produced — their elements, the promises they passed or pushed, their handle values — and runs only middleware and loaders. Count runs to pin it:
|
|
@@ -134,9 +166,12 @@ const silver = await serveShellRequest(router, "/pricing", tier("silver"));
|
|
|
134
166
|
expect(silver.shellStatus).toBe("MISS"); // its own capture, never gold's shell
|
|
135
167
|
const gold = await serveShellRequest(router, "/pricing", tier("gold"));
|
|
136
168
|
expect(gold.shellStatus).toBe("HIT");
|
|
137
|
-
expect(gold.key).toBe(
|
|
138
|
-
|
|
139
|
-
|
|
169
|
+
expect(gold.key).toBe(
|
|
170
|
+
shellCacheKey(router, "/pricing", undefined, "tier:gold"),
|
|
171
|
+
);
|
|
172
|
+
// "{router.id}@localhost/pricing:shell|key%3Atier%253Agold": the router the
|
|
173
|
+
// shell belongs to, then the key() result namespaced as production stores it
|
|
174
|
+
// (issue #975).
|
|
140
175
|
expect(await gold.readEntry()).not.toBeNull();
|
|
141
176
|
```
|
|
142
177
|
|
|
@@ -149,7 +184,7 @@ Under nested keyed `cache()` boundaries the partition is their `key()` results c
|
|
|
149
184
|
// ]),
|
|
150
185
|
// ])
|
|
151
186
|
expect(result.key).toBe(
|
|
152
|
-
shellCacheKey("/plans", undefined, ["tier:gold", "v:a"]),
|
|
187
|
+
shellCacheKey(router, "/plans", undefined, ["tier:gold", "v:a"]),
|
|
153
188
|
);
|
|
154
189
|
```
|
|
155
190
|
|
|
@@ -159,24 +194,134 @@ A store `keyGenerator` result that partitions the record partitions the shell to
|
|
|
159
194
|
// createRouter({ cache: { store: localeStore } }), where localeStore's
|
|
160
195
|
// keyGenerator returns `${defaultKey}|${locale}`
|
|
161
196
|
expect(result.key).toBe(
|
|
162
|
-
shellCacheKey("/pricing", undefined, {
|
|
163
|
-
generated: [
|
|
197
|
+
shellCacheKey(router, "/pricing", undefined, {
|
|
198
|
+
generated: [`doc:${router.id}@localhost/pricing|de`],
|
|
164
199
|
}),
|
|
165
200
|
);
|
|
166
201
|
```
|
|
167
202
|
|
|
168
203
|
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.
|
|
169
204
|
|
|
205
|
+
## Two routers on one cache store
|
|
206
|
+
|
|
207
|
+
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:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
const store = new MemorySegmentCacheStore();
|
|
211
|
+
// appA and appB: createRouter({ id: "app-a" | "app-b", cache: { store } }),
|
|
212
|
+
// each with path("/pricing", ..., { ppr: true })
|
|
213
|
+
const url = "http://preview.dev/pricing";
|
|
214
|
+
|
|
215
|
+
await serveShellRequest(appA, url); // MISS + capture
|
|
216
|
+
const fromB = await serveShellRequest(appB, url);
|
|
217
|
+
expect(fromB.shellStatus).toBe("MISS"); // never app A's shell
|
|
218
|
+
expect((await serveShellRequest(appA, url)).shellStatus).toBe("HIT");
|
|
219
|
+
expect((await serveShellRequest(appB, url)).shellStatus).toBe("HIT");
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Vitest runs no Vite id transform, so give each router an explicit `id` when a test builds more than one, and do not reuse one `id` for routers with different routes in the same file: the route manifest is registered per id.
|
|
223
|
+
|
|
170
224
|
## Handles and loader data
|
|
171
225
|
|
|
172
|
-
|
|
226
|
+
`readHandles()` decodes the handle data the response carries, the way the browser reads it, with deferred values resolved. Each field is the raw handle data (`{ [handleId]: { [segmentId]: values[] } }`):
|
|
227
|
+
|
|
228
|
+
| Field | What it is |
|
|
229
|
+
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
230
|
+
| `hydration` | What the document hydrates with (`metadata.handles`, read to its end before hydration starts). For a `partial` request, the last state it streamed. |
|
|
231
|
+
| `late` | The states that arrived on the late channel (`metadata.handlesLate`), in order. Each replaces the client's handle data after hydration. Often `[]`. |
|
|
232
|
+
|
|
233
|
+
On a HIT the client hydrates with exactly what the shell was rendered from, so `hydration` is also what the shell's HTML holds, and whatever this request's loaders push, replace or drop is in `late` (see `/ppr`, "Handles on a shell HIT"):
|
|
173
234
|
|
|
174
235
|
```ts
|
|
175
|
-
// The handler pushed ctx.use(
|
|
176
|
-
|
|
177
|
-
|
|
236
|
+
// The handler pushed ctx.use(Notes)("from-handler"); a live loader pushes
|
|
237
|
+
// ctx.use(Notes)("in stock").
|
|
238
|
+
const values = (data) =>
|
|
239
|
+
Object.values(data ?? {}).flatMap((s) => Object.values(s).flat());
|
|
240
|
+
|
|
241
|
+
const hit = await serveShellRequest(router, "/product/1", { cacheStore });
|
|
242
|
+
const handles = await hit.readHandles();
|
|
243
|
+
expect(values(handles.hydration)).toEqual(["from-handler"]); // the shell's HTML, hydrates clean
|
|
244
|
+
expect(values(handles.late.at(-1))).toEqual(["from-handler", "in stock"]); // after hydration
|
|
178
245
|
```
|
|
179
246
|
|
|
247
|
+
Loader data has no decoder: it rides the payload, so assert it as text in `prelude` and `flight`. `normalizeFlight` and the `flightMatchers` (`toMatchFlight`) work on the text too. A live loader's data is only in the tail.
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
expect(hit.flight).toContain('"stock":"in stock"');
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Warming a route: router.prerender()
|
|
254
|
+
|
|
255
|
+
`router.prerender()` warms a route that is not `Prerender(..., { onDemand })`
|
|
256
|
+
through `router.fetch` (`/prerender` → "Warm any route before traffic").
|
|
257
|
+
Importing `serveShellRequest` gives that handler the same HTML stub, so the
|
|
258
|
+
whole path runs here: warm, then serve the next visitor's request.
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
class SharedMemoryStore extends MemorySegmentCacheStore {
|
|
262
|
+
// The shipped store is "local" and refused; a test store says it is shared.
|
|
263
|
+
readonly scope = "global" as const;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const router = createRouter({
|
|
267
|
+
cache: { store: new SharedMemoryStore() },
|
|
268
|
+
}).routes(
|
|
269
|
+
urls(({ path }) => [path("/product/:id", ProductPage, { ppr: true })]),
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const result = await router.prerender({ env: {} })(
|
|
273
|
+
"http://localhost/product/1",
|
|
274
|
+
);
|
|
275
|
+
expect(result).toMatchObject({ ok: true, path: "warm", status: "warmed" });
|
|
276
|
+
|
|
277
|
+
// No visitor has requested the URL: its first document is a HIT.
|
|
278
|
+
const hit = await serveShellRequest(router, "/product/1");
|
|
279
|
+
expect(hit.shellStatus).toBe("HIT");
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
- Configure the store on the router. A warm writes to the store
|
|
283
|
+
`createRouter({ cache })` resolves, not to the `cacheStore` option of
|
|
284
|
+
`serveShellRequest`.
|
|
285
|
+
- Give the target the origin `serveShellRequest` uses (`http://localhost`), as
|
|
286
|
+
a full URL or with `router.prerender({ env, origin })`: cache keys carry the
|
|
287
|
+
host. A path target with neither returns `skipped-no-origin`, unless the
|
|
288
|
+
runner is called from inside a served request (a route handler that calls
|
|
289
|
+
`router.prerender()`), where it takes that request's origin.
|
|
290
|
+
- To assert a replace, count handler runs or render a per-run stamp: fill the
|
|
291
|
+
cache with one request, move the data, warm, then check the next request
|
|
292
|
+
shows the new value and ran no handler.
|
|
293
|
+
- `router.fetch` binds its handler's document version once, as a production
|
|
294
|
+
isolate does, so `setBuildVersions()` between two warms does not move it.
|
|
295
|
+
|
|
296
|
+
## A form submitted before hydration
|
|
297
|
+
|
|
298
|
+
`serveShellRequest(router, url, { form })` POSTs a form the way a browser does before the page hydrates, through the production no-JS path: the action runs, the page re-renders, and for a `useActionState` form the document payload carries the form state. Build the body with `createActionForm(action, { id })` from the same entry; it writes React's hidden fields and registers `action` under `id` (`<module id>#<export name>`, any unique string of that shape in a test).
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
import {
|
|
302
|
+
createActionForm,
|
|
303
|
+
serveShellRequest,
|
|
304
|
+
} from "@rangojs/router/testing/flight";
|
|
305
|
+
|
|
306
|
+
const form = createActionForm(makeToken, { id: "actions/token#makeToken" });
|
|
307
|
+
const result = await serveShellRequest(router, "/token", { form });
|
|
308
|
+
|
|
309
|
+
// What the SSR render received, and what the browser entry hands hydrateRoot:
|
|
310
|
+
expect(result.formState).toEqual([
|
|
311
|
+
{ token: "tok-1" },
|
|
312
|
+
"k0",
|
|
313
|
+
"actions/token#makeToken",
|
|
314
|
+
0,
|
|
315
|
+
]);
|
|
316
|
+
expect(await result.readPayloadFormState()).toEqual(result.formState);
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
- `useActionState: false` builds a plain `<form action={fn}>` POST (`$ACTION_ID_`): the action runs as `(formData)` and no form state is carried. Default true runs `(prevState, formData)` with the hook's initial `null`. `fields` are the form's own fields (`formData.get("name")`).
|
|
320
|
+
- An action returning a state Flight cannot serialize (a class instance) still renders the document with status 200 and `formState` holds the raw value, but `readPayloadFormState()` rejects: the entry catches that and hydrates without it, as before the fix.
|
|
321
|
+
- A GET, a plain-form POST and a redirecting action carry no form state: `formState` and `readPayloadFormState()` are undefined, a redirect has no `flight`.
|
|
322
|
+
- Request headers come from `headers`. No `Origin` is sent by default, which the origin guard allows.
|
|
323
|
+
- Not with `partial`. The hydration itself (`hydrateRoot` taking the state, the hook not resetting) is e2e.
|
|
324
|
+
|
|
180
325
|
## Caveats
|
|
181
326
|
|
|
182
327
|
What the HTML stub cannot reproduce, so keep it in e2e:
|
|
@@ -192,7 +337,7 @@ Other caveats:
|
|
|
192
337
|
- Call `resetShellTestState()` in `beforeEach` (see above). Without it, a URL a previous test left backed off, or a shell memoized by another `CFCacheStore`, changes what the next test sees.
|
|
193
338
|
- `readEntry()` reads the document entry under `key`. A partial request that finds no entry captures a navigation-only entry, which is stored apart from it.
|
|
194
339
|
- `updateTag()` needs a request context with the store: call it through `runInRequestContext(fn, { cacheStore })` or your app's own endpoint (`dispatch`). A store refuses a capture that starts in the invalidation's millisecond (the invalidation wins) and backs the key off; `serveShellRequest` starts each request in a later millisecond than the call, so an invalidation made before the call cannot collide with it.
|
|
195
|
-
- GET
|
|
340
|
+
- GET, and one POST shape: a form submitted before hydration (see "A form submitted before hydration" above). Other server action requests are not served; test their effects with `runInRequestContext`.
|
|
196
341
|
- Same setup as every Flight test: the react-server condition and `rangoTestAliases()` (see [`./setup.md`](./setup.md)).
|
|
197
342
|
|
|
198
343
|
## See also
|
|
@@ -8,22 +8,22 @@
|
|
|
8
8
|
|
|
9
9
|
### Options — `CreateTestContextOptions<TEnv>`
|
|
10
10
|
|
|
11
|
-
| Field | Type | Meaning
|
|
12
|
-
| --------------- | ------------------------------------------------------ |
|
|
13
|
-
| `env` | `TEnv` | Platform bindings the action reads (`ctx.env`). Default `{}`. Double them yourself (see `./bindings.md`).
|
|
14
|
-
| `request` | `Request \| string` | The request to run under. A `string` becomes `new Request(url)`; pass a full `Request` to seed a `Cookie` header. Default origin `http://localhost/`.
|
|
15
|
-
| `requestInit` | `RequestInit` | Init merged when `request` is a string (e.g. `{ method, headers, body }`).
|
|
16
|
-
| `variables` | `Record<string, unknown>` | Raw backing store for `ctx.get()` / `ctx.set()`, pre-seeded from `vars`.
|
|
17
|
-
| `vars` | `VarsInit` | Vars a prior middleware would have set (object or `[token, value]` list).
|
|
18
|
-
| `routeMap` | `Record<string, string>` | Route name -> pattern map enabling `ctx.reverse()` without global state.
|
|
19
|
-
| `routeName` | `string` | Current route name (drives `ctx.reverse()` self-references).
|
|
20
|
-
| `params` | `Record<string, string>` | Route params on `ctx.params`.
|
|
21
|
-
| `basename` | `string` | Router basename, normalized exactly like `createRouter({ basename })`; drives `redirect()` prefixing. Default `undefined`.
|
|
22
|
-
| `cacheStore` | `SegmentCacheStore` | Backing store for `use cache` functions (same shape as `createRouter({ cache })`). Without it, cached functions run uncached and their guards never fire.
|
|
23
|
-
| `cacheProfiles` | `Record<string, CacheProfile>` | Profiles for `use cache: "name"`, same shape as `createRouter({ cacheProfiles })`. An unknown profile throws.
|
|
24
|
-
| `theme` | `ThemeConfig \| true` | Theme config (same shape as `createRouter({ theme })`). Without it `ctx.theme` / `ctx.setTheme` are inert.
|
|
25
|
-
| `build` | `boolean` | Seed `ctx.build` (default `false`) for code that branches on the build-time PPR shell-capture pass. With `true`, `ctx.waitUntil()` is inert, as at build time.
|
|
26
|
-
| `stateCookie` | `StateCookieSeed` (`{ prefix?, routerId?, version? }`) | Customize the rango state cookie an action calling `invalidateClientCache()` rotates. The name is ALWAYS seeded (default `rango-state_router_0`) so the rotation `Set-Cookie` fires like production; override `prefix`/`routerId` to match `createRouter({ stateCookiePrefix, id })`, or `version` (value is `{version}:{timestamp}`, default `"0"`). |
|
|
11
|
+
| Field | Type | Meaning |
|
|
12
|
+
| --------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| `env` | `TEnv` | Platform bindings the action reads (`ctx.env`). Default `{}`. Double them yourself (see `./bindings.md`). |
|
|
14
|
+
| `request` | `Request \| string` | The request to run under. A `string` becomes `new Request(url)`; pass a full `Request` to seed a `Cookie` header. Default origin `http://localhost/`. |
|
|
15
|
+
| `requestInit` | `RequestInit` | Init merged when `request` is a string (e.g. `{ method, headers, body }`). |
|
|
16
|
+
| `variables` | `Record<string, unknown>` | Raw backing store for `ctx.get()` / `ctx.set()`, pre-seeded from `vars`. |
|
|
17
|
+
| `vars` | `VarsInit` | Vars a prior middleware would have set (object or `[token, value]` list). |
|
|
18
|
+
| `routeMap` | `Record<string, string>` | Route name -> pattern map enabling `ctx.reverse()` without global state. |
|
|
19
|
+
| `routeName` | `string` | Current route name (drives `ctx.reverse()` self-references). |
|
|
20
|
+
| `params` | `Record<string, string>` | Route params on `ctx.params`. |
|
|
21
|
+
| `basename` | `string` | Router basename, normalized exactly like `createRouter({ basename })`; drives `redirect()` prefixing. Default `undefined`. |
|
|
22
|
+
| `cacheStore` | `SegmentCacheStore` | Backing store for `use cache` functions (same shape as `createRouter({ cache })`). Without it, cached functions run uncached and their guards never fire. |
|
|
23
|
+
| `cacheProfiles` | `Record<string, CacheProfile>` | Profiles for `use cache: "name"`, same shape as `createRouter({ cacheProfiles })`. An unknown profile throws. |
|
|
24
|
+
| `theme` | `ThemeConfig \| true` | Theme config (same shape as `createRouter({ theme })`). Without it `ctx.theme` / `ctx.setTheme` are inert. |
|
|
25
|
+
| `build` | `boolean` | Seed `ctx.build` (default `false`) for code that branches on the build-time PPR shell-capture pass. With `true`, `ctx.waitUntil()` is inert, as at build time. |
|
|
26
|
+
| `stateCookie` | `StateCookieSeed` (`{ prefix?, routerId?, version? }`) | Customize the rango state cookie an action calling `invalidateClientCache()` rotates. The name is ALWAYS seeded (default `rango-state_router_0`) so the rotation `Set-Cookie` fires like production; override `prefix`/`routerId` to match `createRouter({ stateCookiePrefix, id })`, or `version` (the app's version, as `createRouter({ version })` would set it; value is `{version}:{timestamp}`, default `"0"`). |
|
|
27
27
|
|
|
28
28
|
### Context — `RequestContext<TEnv>` (what your code receives)
|
|
29
29
|
|
|
@@ -110,7 +110,7 @@ it("asserts the client-cache directives an action issued", async () => {
|
|
|
110
110
|
## Caveats
|
|
111
111
|
|
|
112
112
|
- The snapshot fires whether `fn` RETURNS or THROWS. A `throw redirect("/app")` on the success path is captured on `thrown` (NOT re-thrown), so no try/catch is needed; assert on `thrown` for a throwing action.
|
|
113
|
-
- Location state needs a key. A `createLocationState()` definition gets it from the Vite plugin, which a unit-test project does not run, and outside production an unkeyed `Flash(value)` throws before `redirect()` runs: `thrown` is that missing-key error, not the redirect. Call `withLocationStateKey(Flash, "Flash")` once per definition; `locationState` then holds `{ __rsc_ls_Flash: value }` (the helper adds the `__rsc_ls_` prefix; `{ [Flash.__rsc_ls_key]: value }` works for any key).
|
|
113
|
+
- Location state needs a key. A `createLocationState()` definition gets it from the Vite plugin, which a unit-test project does not run, and outside production an unkeyed `Flash(value)` throws before `redirect()` runs: `thrown` is that missing-key error, not the redirect. Call `withLocationStateKey(Flash, "Flash")` once per definition; `locationState` then holds `{ __rsc_ls_Flash: value }` (the helper adds the `__rsc_ls_` prefix; `{ [Flash.__rsc_ls_key]: value }` works for any key, including one that carries the `clearOnReload` suffix, `__rsc_ls_Flash~r`).
|
|
114
114
|
- There is no cookies / headers option. Seed a request cookie by passing a full `Request` with the `Cookie` header (as in the recipe).
|
|
115
115
|
- An action has no loader context, so `runLoader` is the wrong shape for it even when the action reads cookies or vars; use `runInRequestContext`.
|
|
116
116
|
- Platform bindings are yours to double via `env` (see `./bindings.md`).
|
|
@@ -128,7 +128,7 @@ it("asserts the server-rendered host content", async () => {
|
|
|
128
128
|
- A client boundary's props come back as REAL JS values after deserialization (a `Date` is a `Date`, not a `$D...` encoding) — but there is NO hydration and NO interaction; boundaries are inert placeholders carrying props.
|
|
129
129
|
- Server COMPONENTS do not survive Flight as identities (they are executed during serialization), so `findElements` matches the host elements they PRODUCED, not the component function. Client islands keep identity — use `findClientBoundaries` for those.
|
|
130
130
|
- `findClientBoundaries` finds islands (`I` rows); `findElements` finds host elements. A `testId` on an island matches with `findClientBoundaries`; a `testId` on a host element matches with `findElements`. Use `textContent(node)` in place of `JSON.stringify(tree).toContain`.
|
|
131
|
-
- A true interactive, clickable DOM `renderServer` is intentionally NOT shipped: in-process happy-dom hydration re-tests React more than your app and misses
|
|
131
|
+
- A true interactive, clickable DOM `renderServer` is intentionally NOT shipped: in-process happy-dom hydration of a Flight tree re-tests React more than your app and misses the divergence that needs the real document (streaming order, client-reference identity, the browser's HTML parser). Test interaction at e2e. A CLIENT component's own server/client divergence is unit-reachable: `renderRoute` with `hydrate: true` ([`./client-components.md`](./client-components.md#hydration)).
|
|
132
132
|
|
|
133
133
|
## See also
|
|
134
134
|
|
|
@@ -113,7 +113,7 @@ and client helpers.
|
|
|
113
113
|
CLI flags: the default mode fails (and writes nothing) when an `include()` cannot
|
|
114
114
|
be resolved statically (factory functions, dynamic expressions); `--static`
|
|
115
115
|
accepts partial output with warnings; `--runtime` uses Vite-based discovery for
|
|
116
|
-
full coverage (needs `vite
|
|
116
|
+
full coverage (needs `vite`, optional
|
|
117
117
|
`--config <path>`). Passing a directory scans it for router and `urls()` files.
|
|
118
118
|
|
|
119
119
|
### Named Routes, `$$routeNames`, And `router.routeMap`
|
|
@@ -151,11 +151,15 @@ argument. The same holds for a loader's own `cache()` (`/loader` → "Cache Key"
|
|
|
151
151
|
|
|
152
152
|
The tainted `ctx` object is excluded from arg serialization (see below), but
|
|
153
153
|
route-identifying fields read off it are extracted into `serializedArgs`:
|
|
154
|
-
`url.host
|
|
155
|
-
(`
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
154
|
+
the serving router's id and `url.host` (`{routerId}@{host}`), route name
|
|
155
|
+
(`_routeName`), `pathname`, `params`, response type (`_responseType`), and the
|
|
156
|
+
user-facing sorted search params (internal `_rsc*`/`__` params excluded). The
|
|
157
|
+
same cached function called with `ctx` on different routes, param combinations,
|
|
158
|
+
hosts, response types, or query variants therefore produces distinct cache
|
|
159
|
+
entries -- not one shared entry. Two routers that call it on one host and path
|
|
160
|
+
keep two entries as well: a `ctx` is one router's (`/host-router`, "Shared
|
|
161
|
+
cache store"). A bare `Request` argument folds in its URL only, so it names no
|
|
162
|
+
router.
|
|
159
163
|
|
|
160
164
|
## Request-Scoped Arguments (ctx, Request, env)
|
|
161
165
|
|
|
@@ -485,11 +489,22 @@ the current time and expect freshness -- those are frozen at cache-write. The
|
|
|
485
489
|
`cookies()`/`headers()` read guard applies to the cached function body, NOT to an
|
|
486
490
|
inline action's body.
|
|
487
491
|
|
|
488
|
-
Deploy note: captured values are encrypted with a
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
492
|
+
Deploy note: captured values are encrypted with a key that is generated per
|
|
493
|
+
build by default. Set a stable one in the plugin options:
|
|
494
|
+
|
|
495
|
+
```ts
|
|
496
|
+
rango({ encryptionKey: process.env.RANGO_ENCRYPTION_KEY });
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
The value is base64-encoded 32 bytes (`openssl rand -base64 32`), never a
|
|
500
|
+
literal, and is validated when `rango()` is called. `undefined` falls back to the
|
|
501
|
+
`RANGO_ENCRYPTION_KEY` environment variable, then to a key generated per build.
|
|
502
|
+
The key is part of the cache version of every router whose server code encrypts
|
|
503
|
+
inline server-action bound arguments, so without a stable key those routers get a
|
|
504
|
+
new version (and a cleared cache) on every build, and the build prints one note
|
|
505
|
+
when that applies. With a stable key, actions embedded in entries written by a
|
|
506
|
+
previous deploy still decrypt. A router that renders no inline action with bound
|
|
507
|
+
arguments keeps its cache without a stable key. See `/cache-guide`.
|
|
493
508
|
|
|
494
509
|
## Vite Transform
|
|
495
510
|
|
package/skills/vercel/SKILL.md
CHANGED
|
@@ -98,9 +98,7 @@ const memoryStore = new MemorySegmentCacheStore({ defaults });
|
|
|
98
98
|
// passes, since the tag markers are regional and only expireTag is global. A
|
|
99
99
|
// platform expireTag issued outside rango writes no marker, so every memo
|
|
100
100
|
// serves the shell until its window passes.
|
|
101
|
-
const runtimeCache = process.env.VERCEL
|
|
102
|
-
? getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID })
|
|
103
|
-
: undefined;
|
|
101
|
+
const runtimeCache = process.env.VERCEL ? getCache() : undefined;
|
|
104
102
|
|
|
105
103
|
function resolveCache() {
|
|
106
104
|
if (runtimeCache) {
|
|
@@ -120,6 +118,10 @@ export const router = createRouter({ cache: resolveCache }).routes(/* ... */);
|
|
|
120
118
|
|
|
121
119
|
The cache factory receives `(env, ctx)`: on Vercel `env` is `process.env` and `ctx` is `{ waitUntil }`; off-platform `ctx` is `undefined`. Cache semantics and tag invalidation: `/caching`, `/cache-guide`.
|
|
122
120
|
|
|
121
|
+
`router.prerender()` can warm a route into this store before traffic (`/prerender` → "Warm any route before traffic"). The Runtime Cache is per region, so `VercelCacheStore` declares `scope: "regional"`: a warm fills the region the call runs in. That is all traffic when the project's functions run in one region (the default `iad1` for new projects, and every Hobby project); on a multi-region project it warms one region and the others fill on first use. The router does not detect the region count. The memory store used off-platform above is refused in production and allowed under the dev server.
|
|
122
|
+
|
|
123
|
+
`getCache()` takes no deployment id: `VercelCacheStore` puts a version in front of every key itself (data version for families `s`, `i`; document version for `r`, `h`; none for tag markers), computed per `createRouter()` from its built code. A deploy that does not change a router keeps its entries; a deploy that changes it reads under new keys. `version` on the store (or on `createRouter`) replaces both versions. To clear the cache on every deploy, set `version` to a per-deploy value or keep a deployment-scoped `getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID })`. Two environments built from the same code share entries if they share a cache namespace; give each its own namespace or `version`.
|
|
124
|
+
|
|
123
125
|
## Host routers (multi-app)
|
|
124
126
|
|
|
125
127
|
A multi-app host router deploys as a **single function** running `hostRouter.match()` for every request (mirrors the Cloudflare single-worker model). Two requirements:
|
|
@@ -359,10 +359,15 @@ interface TransitionWhenContext {
|
|
|
359
359
|
the route or loses its state
|
|
360
360
|
([#995](https://github.com/rangojs/rango/issues/995)).
|
|
361
361
|
- That holds for every kind, actions and revalidations included: a gated-off
|
|
362
|
-
action or revalidation commit is urgent too,
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
362
|
+
action or revalidation commit is urgent and unanimated too, and a segment it
|
|
363
|
+
re-renders shows its `loading()` only while something in it is still pending
|
|
364
|
+
(a gated-off navigation whose data is still streaming shows it; an action
|
|
365
|
+
whose response already carries the data does not in a production build,
|
|
366
|
+
while the dev server still shows it briefly because it resolves each
|
|
367
|
+
response's client components asynchronously). The router commits an
|
|
368
|
+
action's result after the response arrives, outside the transition React
|
|
369
|
+
opens for a form action or `useActionState` call, so React does not hold it
|
|
370
|
+
either.
|
|
366
371
|
- A predicate that throws counts as `false` and is logged with
|
|
367
372
|
`console.error` (the conservative default: no hold).
|
|
368
373
|
- Intercept (modal) commits skip the evaluation, including a back/forward
|
package/src/bin/rango.ts
CHANGED
|
@@ -76,7 +76,7 @@ if (command === "generate") {
|
|
|
76
76
|
Modes:
|
|
77
77
|
(default) Static parser with error on unresolvable includes
|
|
78
78
|
--runtime Vite-based runtime discovery (100% coverage)
|
|
79
|
-
Requires vite
|
|
79
|
+
Requires vite
|
|
80
80
|
--static Static parser, accept partial output with warnings
|
|
81
81
|
|
|
82
82
|
Options:
|