@rangojs/router 0.0.0-experimental.d7eeaa75 → 0.0.0-experimental.d98a8e9d
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 +120 -25
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +2154 -861
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +57 -11
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +45 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +46 -4
- package/skills/layout/SKILL.md +28 -7
- package/skills/links/SKILL.md +247 -17
- package/skills/loader/SKILL.md +219 -9
- package/skills/middleware/SKILL.md +47 -12
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +71 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +243 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +122 -47
- package/skills/route/SKILL.md +57 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +128 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +98 -0
- package/skills/testing/client-components.md +121 -0
- package/skills/testing/e2e-parity.md +124 -0
- package/skills/testing/flight.md +89 -0
- package/skills/testing/handles.md +127 -0
- package/skills/testing/loader.md +108 -0
- package/skills/testing/middleware.md +97 -0
- package/skills/testing/render-handler.md +102 -0
- package/skills/testing/response-routes.md +94 -0
- package/skills/testing/reverse-and-types.md +83 -0
- package/skills/testing/server-actions.md +89 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/skills/typesafety/SKILL.md +319 -27
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +86 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +84 -11
- package/src/browser/navigation-client.ts +104 -68
- package/src/browser/navigation-store.ts +32 -9
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +64 -26
- package/src/browser/prefetch/cache.ts +183 -44
- package/src/browser/prefetch/fetch.ts +228 -37
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +30 -2
- package/src/browser/react/NavigationProvider.tsx +72 -31
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +22 -2
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +32 -1
- package/src/browser/rsc-router.tsx +69 -22
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +21 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +8 -1
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +95 -25
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-scope.ts +28 -42
- package/src/cache/cf/cf-cache-store.ts +54 -13
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +96 -205
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -4
- package/src/handle.ts +32 -14
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -21
- package/src/index.rsc.ts +10 -6
- package/src/index.ts +54 -17
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +25 -7
- package/src/loader.ts +16 -9
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender.ts +27 -6
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -36
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +384 -257
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +100 -28
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +26 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/find-match.ts +54 -6
- package/src/router/handler-context.ts +21 -38
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +41 -22
- package/src/router/loader-resolution.ts +82 -36
- package/src/router/manifest.ts +41 -19
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/cache-lookup.ts +44 -91
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +53 -32
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +15 -26
- package/src/router/middleware.ts +99 -84
- package/src/router/pattern-matching.ts +116 -19
- package/src/router/prerender-match.ts +1 -1
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/router/revalidation.ts +58 -2
- package/src/router/router-interfaces.ts +45 -28
- package/src/router/router-options.ts +40 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +27 -6
- package/src/router/segment-resolution/revalidation.ts +147 -106
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +40 -16
- package/src/router/types.ts +8 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +52 -30
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +28 -69
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +57 -61
- package/src/rsc/rsc-rendering.ts +35 -51
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +17 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +8 -2
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -4
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +132 -116
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +175 -53
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +67 -51
- package/src/ssr/index.tsx +5 -1
- package/src/static-handler.ts +25 -3
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +581 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +326 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +110 -0
- package/src/testing/flight-normalize.ts +38 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +51 -0
- package/src/testing/flight.ts +234 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +106 -0
- package/src/testing/internal/context.ts +304 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +42 -0
- package/src/testing/render-handler.ts +323 -0
- package/src/testing/render-route.tsx +590 -0
- package/src/testing/run-loader.ts +363 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +285 -0
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +68 -50
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +11 -9
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +11 -0
- package/src/types/segments.ts +35 -2
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +1 -5
- package/src/urls/path-helper-types.ts +41 -7
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +106 -75
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +67 -26
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +54 -30
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +496 -486
- package/src/vite/plugins/performance-tracks.ts +29 -25
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +116 -29
- package/src/vite/router-discovery.ts +750 -100
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +8 -59
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +21 -6
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/// <reference path="./flight-runtime.d.ts" />
|
|
2
|
+
/**
|
|
3
|
+
* renderHandler — run a REAL route handler and assert what it renders.
|
|
4
|
+
*
|
|
5
|
+
* A Rango route handler is a pure function `(ctx) => RSC` (what you pass to
|
|
6
|
+
* `path("/p/:slug", ProductPage)`), NOT a component. To test one faithfully you
|
|
7
|
+
* must give it the HandlerContext the router builds at runtime, so `ctx.params`,
|
|
8
|
+
* `ctx.use(Loader)`, `ctx.use(Meta)` / `ctx.use(Breadcrumbs)` (handles),
|
|
9
|
+
* `ctx.reverse`, `ctx.get`/`ctx.header`/`cookies()` all work. renderHandler does
|
|
10
|
+
* exactly that, then serializes the handler's returned RSC and deserializes it
|
|
11
|
+
* to an inspectable tree (same serialize/deserialize core as renderServerTree).
|
|
12
|
+
*
|
|
13
|
+
* Loaders are SEEDED (no real loader execution) the same way `runLoader` seeds
|
|
14
|
+
* them — pass `loaders: [[ProductLoader, data]]`. Handle pushes
|
|
15
|
+
* (`ctx.use(Meta)({...})`) are captured on `result.handles`. The handler's
|
|
16
|
+
* cookie/header/flash effects and a thrown/returned redirect are surfaced too
|
|
17
|
+
* (like `runInRequestContext`). If the handler returns/throws a `Response`
|
|
18
|
+
* (a response route / `throw redirect()`), there is no RSC `tree`.
|
|
19
|
+
*
|
|
20
|
+
* Must run under the `react-server` export condition (the rsc Vitest project).
|
|
21
|
+
* Wire `rangoUseClientTransform()` so `"use client"` islands in the handler's RSC
|
|
22
|
+
* auto-register (or pass `clientComponents`).
|
|
23
|
+
*/
|
|
24
|
+
import type { ReactNode } from "react";
|
|
25
|
+
import {
|
|
26
|
+
createRequestContext,
|
|
27
|
+
runWithRequestContext,
|
|
28
|
+
setRequestContextParams,
|
|
29
|
+
type RequestContext,
|
|
30
|
+
} from "../server/request-context.js";
|
|
31
|
+
import { createHandlerContext } from "../router/handler-context.js";
|
|
32
|
+
import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
|
|
33
|
+
import { isHandle, type Handle } from "../handle.js";
|
|
34
|
+
import type { HandlerContext } from "../types/handler-context.js";
|
|
35
|
+
import type { LoaderDefinition } from "../types.js";
|
|
36
|
+
import { seedVariables, type VarsInit } from "./internal/seed-vars.js";
|
|
37
|
+
import { assertNoLegacyUrlOption, serializeNodeToFlight } from "./flight.js";
|
|
38
|
+
import {
|
|
39
|
+
deserializeFlight,
|
|
40
|
+
makeClientManifest,
|
|
41
|
+
registerClientComponents,
|
|
42
|
+
} from "./flight-tree.js";
|
|
43
|
+
|
|
44
|
+
const DEFAULT_URL = "http://localhost/";
|
|
45
|
+
|
|
46
|
+
/** A route handler under test: the `(ctx) => RSC | Response` function you pass to `path(...)`. */
|
|
47
|
+
export type TestableHandler<TEnv = any> = (
|
|
48
|
+
ctx: HandlerContext<any, TEnv>,
|
|
49
|
+
) => ReactNode | Promise<ReactNode> | Response | Promise<Response>;
|
|
50
|
+
|
|
51
|
+
/** Options for {@link renderHandler}. */
|
|
52
|
+
export interface RenderHandlerOptions<TEnv = any> {
|
|
53
|
+
/** Route params surfaced as `ctx.params`. */
|
|
54
|
+
params?: Record<string, string>;
|
|
55
|
+
/** Environment bindings surfaced as `ctx.env`. */
|
|
56
|
+
env?: TEnv;
|
|
57
|
+
/** Backing Request (string or Request); defaults to a localhost GET. */
|
|
58
|
+
request?: Request | string;
|
|
59
|
+
/** Request headers (e.g. Cookie) the handler reads via `cookies()`. */
|
|
60
|
+
headers?: HeadersInit;
|
|
61
|
+
/** Variables a prior middleware set, read via `ctx.get(...)`. Object or tuples. */
|
|
62
|
+
vars?: VarsInit;
|
|
63
|
+
/** Matched route name (drives `ctx.routeName` and scoped reverse). */
|
|
64
|
+
routeName?: string;
|
|
65
|
+
/** Route name -> pattern map enabling `ctx.reverse()`. */
|
|
66
|
+
routeMap?: Record<string, string>;
|
|
67
|
+
/**
|
|
68
|
+
* Seed the data `ctx.use(SomeLoader)` returns — NO real loader runs (same model
|
|
69
|
+
* as `runLoader`'s `loaders`). Matched by loader reference, so a real
|
|
70
|
+
* `createLoader()` handle resolves regardless of its build-injected `$$id`.
|
|
71
|
+
*/
|
|
72
|
+
loaders?: ReadonlyArray<readonly [LoaderDefinition<any, any>, unknown]>;
|
|
73
|
+
/**
|
|
74
|
+
* `"use client"` components in the handler's RSC, so they serialize as real
|
|
75
|
+
* boundaries when `rangoUseClientTransform()` is not wired. Keyed by name; see
|
|
76
|
+
* renderServerTree's `clientComponents`.
|
|
77
|
+
*/
|
|
78
|
+
clientComponents?: Record<string, unknown>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Result of {@link renderHandler}. */
|
|
82
|
+
export interface RenderHandlerResult {
|
|
83
|
+
/**
|
|
84
|
+
* The deserialized RSC the handler returned, as an inspectable React element
|
|
85
|
+
* tree — `undefined` when the handler returned or threw a `Response`. Use
|
|
86
|
+
* `findClientBoundaries` (from testing/flight) to locate client islands.
|
|
87
|
+
*/
|
|
88
|
+
tree: unknown;
|
|
89
|
+
/** The raw Flight wire string; `undefined` when the handler produced a `Response`. */
|
|
90
|
+
flight: string | undefined;
|
|
91
|
+
/** The value the handler THREW (a `redirect()`/`notFound()` Response), captured not re-thrown. */
|
|
92
|
+
thrown: unknown;
|
|
93
|
+
/** The merged Response (status + headers + Set-Cookie); a thrown/returned redirect merged with accumulated effects. */
|
|
94
|
+
response: Response;
|
|
95
|
+
/** Effective cookie view after the handler ran, as `{ name: value }`. */
|
|
96
|
+
cookies: Record<string, string>;
|
|
97
|
+
/** Response headers as `{ name: value }` (excludes set-cookie; includes a redirect Location). */
|
|
98
|
+
headers: Record<string, string>;
|
|
99
|
+
/** Location state the handler set (`ctx.setLocationState`/`redirect({ state })`), as `{ key: value }`. */
|
|
100
|
+
locationState: Record<string, unknown>;
|
|
101
|
+
/** What the handler pushed via `ctx.use(Handle)(...)` (e.g. Meta, Breadcrumbs), keyed by handle. */
|
|
102
|
+
handles: Map<Handle<any, any>, unknown[]>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A renderHandler MISCONFIGURATION (e.g. an unseeded loader) — distinct from a
|
|
107
|
+
* value the handler intentionally threw (a redirect). Setup errors REJECT;
|
|
108
|
+
* handler throws are captured on `result.thrown`.
|
|
109
|
+
*/
|
|
110
|
+
class RenderHandlerSetupError extends Error {}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Detect the server-only-API stub throw: when a handler/component imports
|
|
114
|
+
* getRequestContext()/cookies()/etc. from the BARE `@rangojs/router` specifier
|
|
115
|
+
* (the out-of-react-server stub in index.ts) instead of the react-server build.
|
|
116
|
+
* In an rsc test this happens when the vitest.rsc.config.ts `resolve.alias` does
|
|
117
|
+
* not map the bare specifier to `index.rsc.ts` (the `rangoTestAliases` preset).
|
|
118
|
+
* The dual-substring match keeps a legitimate handler throw from being
|
|
119
|
+
* reclassified as a setup error.
|
|
120
|
+
*/
|
|
121
|
+
function isServerOnlyStubError(error: unknown): boolean {
|
|
122
|
+
return (
|
|
123
|
+
error instanceof Error &&
|
|
124
|
+
error.message.includes("is only available from") &&
|
|
125
|
+
error.message.includes("react-server")
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function headersToObject(headers: Headers): Record<string, string> {
|
|
130
|
+
const out: Record<string, string> = {};
|
|
131
|
+
headers.forEach((value, name) => {
|
|
132
|
+
if (name.toLowerCase() !== "set-cookie") out[name] = value;
|
|
133
|
+
});
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function toRequest(
|
|
138
|
+
request: Request | string | undefined,
|
|
139
|
+
headers?: HeadersInit,
|
|
140
|
+
): Request {
|
|
141
|
+
if (request instanceof Request) return request;
|
|
142
|
+
if (typeof request === "string") {
|
|
143
|
+
return new Request(new URL(request, DEFAULT_URL), { headers });
|
|
144
|
+
}
|
|
145
|
+
return new Request(DEFAULT_URL, { headers });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Build the result `response` from the request-context stub and, when present,
|
|
150
|
+
* the Response the handler returned or threw (`source`). The stub cookies and
|
|
151
|
+
* headers are merged in (Set-Cookie appended to preserve duplicates, other stub
|
|
152
|
+
* headers filled in without clobbering the source), mirroring dispatch.ts's
|
|
153
|
+
* rewrap.
|
|
154
|
+
*
|
|
155
|
+
* The source's BODY is carried over (not dropped): a response route returns a
|
|
156
|
+
* `new Response(JSON.stringify(...))`, so callers reach for
|
|
157
|
+
* `await result.response.text()`/`.json()`. Pre-fix this rewrapped to
|
|
158
|
+
* `new Response(null, ...)` and the body was lost irrecoverably. A body is a
|
|
159
|
+
* single-use stream; `source` is not read again here or by renderHandler, so
|
|
160
|
+
* handing its body to the new Response is safe.
|
|
161
|
+
*/
|
|
162
|
+
function buildResponse(reqCtx: RequestContext<any>, source: unknown): Response {
|
|
163
|
+
const stub = reqCtx.res;
|
|
164
|
+
if (source instanceof Response) {
|
|
165
|
+
const merged = new Headers(source.headers);
|
|
166
|
+
for (const cookie of stub.headers.getSetCookie()) {
|
|
167
|
+
merged.append("set-cookie", cookie);
|
|
168
|
+
}
|
|
169
|
+
stub.headers.forEach((value, name) => {
|
|
170
|
+
if (name.toLowerCase() === "set-cookie") return;
|
|
171
|
+
if (!merged.has(name)) merged.set(name, value);
|
|
172
|
+
});
|
|
173
|
+
return new Response(source.body, {
|
|
174
|
+
status: source.status,
|
|
175
|
+
headers: merged,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return new Response(null, { status: stub.status, headers: stub.headers });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Run a route handler with a seeded HandlerContext and return its rendered RSC
|
|
183
|
+
* (deserialized tree) plus the effects it produced. See the module header.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```ts
|
|
187
|
+
* // ProductPage is the real handler: (ctx) => <main>{ctx.params.slug}...</main>
|
|
188
|
+
* const { tree, handles } = await renderHandler(ProductPage, {
|
|
189
|
+
* params: { slug: "wine" },
|
|
190
|
+
* loaders: [[ProductLoader, { name: "Wine", price: 9 }]],
|
|
191
|
+
* vars: [[Tenant, tenant]],
|
|
192
|
+
* routeMap: { product: "/p/:slug" },
|
|
193
|
+
* });
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
export async function renderHandler<TEnv = any>(
|
|
197
|
+
handler: TestableHandler<TEnv>,
|
|
198
|
+
opts: RenderHandlerOptions<TEnv> = {},
|
|
199
|
+
): Promise<RenderHandlerResult> {
|
|
200
|
+
assertNoLegacyUrlOption(opts, "renderHandler");
|
|
201
|
+
if (opts.clientComponents) registerClientComponents(opts.clientComponents);
|
|
202
|
+
const request = toRequest(opts.request, opts.headers);
|
|
203
|
+
const url = new URL(request.url);
|
|
204
|
+
const reqCtx = createRequestContext<TEnv>({
|
|
205
|
+
env: (opts.env ?? {}) as TEnv,
|
|
206
|
+
request,
|
|
207
|
+
url,
|
|
208
|
+
variables: seedVariables({}, opts.vars),
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const loaderSeeds = new Map<unknown, unknown>(opts.loaders ?? []);
|
|
212
|
+
const handlePushes = new Map<Handle<any, any>, unknown[]>();
|
|
213
|
+
|
|
214
|
+
let out: ReactNode | Response | undefined;
|
|
215
|
+
let flight: string | undefined;
|
|
216
|
+
let thrown: unknown;
|
|
217
|
+
let didThrow = false;
|
|
218
|
+
|
|
219
|
+
await runWithRequestContext(reqCtx as RequestContext<TEnv>, async () => {
|
|
220
|
+
setRequestContextParams(opts.params ?? {}, opts.routeName);
|
|
221
|
+
const hctx = createHandlerContext<TEnv>(
|
|
222
|
+
opts.params ?? {},
|
|
223
|
+
reqCtx.request,
|
|
224
|
+
reqCtx.searchParams,
|
|
225
|
+
reqCtx.pathname,
|
|
226
|
+
reqCtx.url,
|
|
227
|
+
reqCtx.env,
|
|
228
|
+
opts.routeMap ?? {},
|
|
229
|
+
opts.routeName,
|
|
230
|
+
);
|
|
231
|
+
// Seed ctx.use: a handle returns a push fn that RECORDS (so ctx.use(Meta)
|
|
232
|
+
// doesn't crash and pushes are assertable); a loader returns its seeded data
|
|
233
|
+
// (no real loader run).
|
|
234
|
+
(hctx as { use: unknown }).use = (item: unknown) => {
|
|
235
|
+
if (isHandle(item)) {
|
|
236
|
+
const handle = item as Handle<any, any>;
|
|
237
|
+
return (dataOrFn: unknown) => {
|
|
238
|
+
// Mirror production's push fn (loader-resolution.ts): a FUNCTION arg
|
|
239
|
+
// (ctx.use(Meta)(() => fetchMeta())) is CALLED and its result is
|
|
240
|
+
// recorded, not the function itself. An async callback records the
|
|
241
|
+
// promise it returns, same as production (which does not await it).
|
|
242
|
+
const value =
|
|
243
|
+
typeof dataOrFn === "function"
|
|
244
|
+
? (dataOrFn as () => unknown)()
|
|
245
|
+
: dataOrFn;
|
|
246
|
+
const pushed = handlePushes.get(handle) ?? [];
|
|
247
|
+
pushed.push(value);
|
|
248
|
+
handlePushes.set(handle, pushed);
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
if (loaderSeeds.has(item)) return loaderSeeds.get(item);
|
|
252
|
+
throw new RenderHandlerSetupError(
|
|
253
|
+
`renderHandler: ctx.use(loader) was not seeded. Pass ` +
|
|
254
|
+
`{ loaders: [[YourLoader, data]] } for each loader the handler reads.`,
|
|
255
|
+
);
|
|
256
|
+
};
|
|
257
|
+
(hctx as { _currentSegmentId?: string })._currentSegmentId = "test.segment";
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
out = await handler(hctx as HandlerContext<any, TEnv>);
|
|
261
|
+
// Serialize the RSC in THIS context, so nested async server components see
|
|
262
|
+
// getRequestContext()/cookies()/vars while they render.
|
|
263
|
+
if (out !== undefined && !(out instanceof Response)) {
|
|
264
|
+
flight = await serializeNodeToFlight(
|
|
265
|
+
out as ReactNode,
|
|
266
|
+
makeClientManifest(),
|
|
267
|
+
url.pathname,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
} catch (error) {
|
|
271
|
+
// A harness misconfiguration (unseeded loader) is the consumer's mistake —
|
|
272
|
+
// surface it as a rejection, not as a captured handler throw.
|
|
273
|
+
if (error instanceof RenderHandlerSetupError) throw error;
|
|
274
|
+
// Same for the server-only-API stub throw: the handler read
|
|
275
|
+
// getRequestContext()/cookies() but the bare `@rangojs/router` resolved to
|
|
276
|
+
// the throwing stub. Rethrow LOUDLY with the fix, instead of silently
|
|
277
|
+
// capturing it (which surfaces as an opaque tree:undefined + bare throw).
|
|
278
|
+
if (isServerOnlyStubError(error)) {
|
|
279
|
+
throw new RenderHandlerSetupError(
|
|
280
|
+
`renderHandler: the handler called a server-only API (getRequestContext/cookies/...) ` +
|
|
281
|
+
`but "@rangojs/router" resolved to the out-of-react-server stub. Add ` +
|
|
282
|
+
`rangoTestAliases({ preset }) to your vitest.rsc.config.ts \`resolve.alias\` so the ` +
|
|
283
|
+
`bare specifier maps to index.rsc.ts (the real react-server implementations). ` +
|
|
284
|
+
`Original: ${(error as Error).message}`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
// Otherwise captured, NOT re-thrown: a handler's success path is often
|
|
288
|
+
// `throw redirect(...)`; its cookies/flash must stay observable.
|
|
289
|
+
didThrow = true;
|
|
290
|
+
thrown = error;
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const cookies = { ...reqCtx.cookies() };
|
|
295
|
+
const responseSource = didThrow
|
|
296
|
+
? thrown
|
|
297
|
+
: out instanceof Response
|
|
298
|
+
? out
|
|
299
|
+
: undefined;
|
|
300
|
+
const response = buildResponse(reqCtx as RequestContext<any>, responseSource);
|
|
301
|
+
const headers = headersToObject(response.headers);
|
|
302
|
+
const locationState = resolveLocationStateEntries(
|
|
303
|
+
(
|
|
304
|
+
reqCtx as {
|
|
305
|
+
_locationState?: Parameters<typeof resolveLocationStateEntries>[0];
|
|
306
|
+
}
|
|
307
|
+
)._locationState ?? [],
|
|
308
|
+
);
|
|
309
|
+
// Deserialize outside the context (the client deserializer needs no ctx).
|
|
310
|
+
const tree =
|
|
311
|
+
flight !== undefined ? await deserializeFlight(flight) : undefined;
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
tree,
|
|
315
|
+
flight,
|
|
316
|
+
thrown,
|
|
317
|
+
response,
|
|
318
|
+
cookies,
|
|
319
|
+
headers,
|
|
320
|
+
locationState,
|
|
321
|
+
handles: handlePushes,
|
|
322
|
+
};
|
|
323
|
+
}
|