@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,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared internals for the consumer testing primitives.
|
|
3
|
+
*
|
|
4
|
+
* Builds a real RequestContext via the same createRequestContext the RSC
|
|
5
|
+
* handler uses, with test-friendly defaults, so loaders and middleware run
|
|
6
|
+
* with production-fidelity context (cookies, headers, get/set, use, reverse)
|
|
7
|
+
* instead of a hand-rolled mock.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
createRequestContext,
|
|
12
|
+
runWithRequestContext,
|
|
13
|
+
type RequestContext,
|
|
14
|
+
} from "../../server/request-context.js";
|
|
15
|
+
import { resolveLocationStateEntries } from "../../browser/react/location-state-shared.js";
|
|
16
|
+
import { createReverseFunction } from "../../router/handler-context.js";
|
|
17
|
+
import { normalizeBasename } from "../../router/basename.js";
|
|
18
|
+
import { seedVariables, type VarsInit } from "./seed-vars.js";
|
|
19
|
+
import type { ThemeConfig } from "../../theme/types.js";
|
|
20
|
+
import { resolveThemeConfig } from "../../theme/constants.js";
|
|
21
|
+
import type { SegmentCacheStore } from "../../cache/types.js";
|
|
22
|
+
import type { CacheProfile } from "../../cache/profile-registry.js";
|
|
23
|
+
|
|
24
|
+
const DEFAULT_ORIGIN = "http://localhost/";
|
|
25
|
+
|
|
26
|
+
// VarsInit + seedVariables live in ./seed-vars.js (react-server-safe) so the
|
|
27
|
+
// Flight tier can seed vars too; re-exported here for existing importers.
|
|
28
|
+
export type { VarsInit };
|
|
29
|
+
export { seedVariables };
|
|
30
|
+
|
|
31
|
+
/** Normalize a Request | string | undefined into a concrete Request. */
|
|
32
|
+
export function toRequest(
|
|
33
|
+
request: Request | string | undefined,
|
|
34
|
+
init?: RequestInit,
|
|
35
|
+
): Request {
|
|
36
|
+
if (request instanceof Request) return request;
|
|
37
|
+
if (typeof request === "string") {
|
|
38
|
+
return new Request(new URL(request, DEFAULT_ORIGIN), init);
|
|
39
|
+
}
|
|
40
|
+
return new Request(DEFAULT_ORIGIN, init);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface CreateTestContextOptions<TEnv> {
|
|
44
|
+
env?: TEnv;
|
|
45
|
+
request?: Request | string;
|
|
46
|
+
requestInit?: RequestInit;
|
|
47
|
+
/** Backing store for ctx.get()/ctx.set(); pre-seeded from `vars`. */
|
|
48
|
+
variables?: Record<string, unknown>;
|
|
49
|
+
/** Variables a prior middleware would have set (object or [key, value] list). */
|
|
50
|
+
vars?: VarsInit;
|
|
51
|
+
/** Route name -> pattern map enabling ctx.reverse() without global state. */
|
|
52
|
+
routeMap?: Record<string, string>;
|
|
53
|
+
routeName?: string;
|
|
54
|
+
params?: Record<string, string>;
|
|
55
|
+
/**
|
|
56
|
+
* Router basename for this request (what the RSC handler stores on the
|
|
57
|
+
* context). Drives redirect() prefixing. Normalized exactly like
|
|
58
|
+
* createRouter({ basename }) (leading slash forced, trailing stripped, bare
|
|
59
|
+
* "/" -> undefined) so passing the same value your router takes yields the
|
|
60
|
+
* same redirect Location. Defaults to undefined (no basename).
|
|
61
|
+
*/
|
|
62
|
+
basename?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Cache store backing `use cache` functions invoked during the test, the
|
|
65
|
+
* same shape `createRouter({ cache })` resolves. Without it,
|
|
66
|
+
* registerCachedFunction bypasses (it checks for a store FIRST), so a cached
|
|
67
|
+
* function runs uncached and its taint/profile guards never fire. Wire one
|
|
68
|
+
* (e.g. `new MemorySegmentCacheStore()`) to exercise real cache behavior.
|
|
69
|
+
*/
|
|
70
|
+
cacheStore?: SegmentCacheStore;
|
|
71
|
+
/**
|
|
72
|
+
* Cache profiles in the `createRouter({ cacheProfiles })` shape. Required for
|
|
73
|
+
* a `use cache: "profileName"` function to resolve its profile (an unknown
|
|
74
|
+
* profile throws), once a `cacheStore` is wired.
|
|
75
|
+
*/
|
|
76
|
+
cacheProfiles?: Record<string, CacheProfile>;
|
|
77
|
+
/**
|
|
78
|
+
* Theme config in the same shape `createRouter({ theme })` takes (resolved
|
|
79
|
+
* internally). Without it `ctx.theme`/`ctx.setTheme` are inert (undefined),
|
|
80
|
+
* mirroring an app with no theme configured. Pass one (e.g. `true`, or
|
|
81
|
+
* `{ themes: [...] }`) to exercise a handler that reads them.
|
|
82
|
+
*/
|
|
83
|
+
theme?: ThemeConfig | true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface TestRequestContext<TEnv> {
|
|
87
|
+
ctx: RequestContext<TEnv>;
|
|
88
|
+
request: Request;
|
|
89
|
+
url: URL;
|
|
90
|
+
variables: Record<string, unknown>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create a real RequestContext for unit-testing loaders/middleware.
|
|
95
|
+
*
|
|
96
|
+
* The returned `ctx` must be ENTERED before use — wrap your call in
|
|
97
|
+
* `runWithRequestContext(ctx, fn)` (re-exported from `@rangojs/router/testing`)
|
|
98
|
+
* so that cookie/header mutations and `getRequestContext()` resolve. For the
|
|
99
|
+
* common case prefer {@link runInRequestContext}, which builds AND enters the
|
|
100
|
+
* context in a single call.
|
|
101
|
+
*/
|
|
102
|
+
export function createTestRequestContext<TEnv>(
|
|
103
|
+
opts: CreateTestContextOptions<TEnv> = {},
|
|
104
|
+
): TestRequestContext<TEnv> {
|
|
105
|
+
const request = toRequest(opts.request, opts.requestInit);
|
|
106
|
+
const url = new URL(request.url);
|
|
107
|
+
const variables = seedVariables(opts.variables ?? {}, opts.vars);
|
|
108
|
+
const ctx = createRequestContext<TEnv>({
|
|
109
|
+
env: (opts.env ?? {}) as TEnv,
|
|
110
|
+
request,
|
|
111
|
+
url,
|
|
112
|
+
variables,
|
|
113
|
+
themeConfig:
|
|
114
|
+
opts.theme === undefined ? undefined : resolveThemeConfig(opts.theme),
|
|
115
|
+
cacheStore: opts.cacheStore,
|
|
116
|
+
cacheProfiles: opts.cacheProfiles,
|
|
117
|
+
});
|
|
118
|
+
if (opts.basename !== undefined)
|
|
119
|
+
ctx._basename = normalizeBasename(opts.basename);
|
|
120
|
+
if (opts.params) ctx.params = opts.params;
|
|
121
|
+
if (opts.routeMap) {
|
|
122
|
+
ctx._routeName = opts.routeName;
|
|
123
|
+
ctx.reverse = createReverseFunction(
|
|
124
|
+
opts.routeMap,
|
|
125
|
+
opts.routeName,
|
|
126
|
+
opts.params ?? {},
|
|
127
|
+
) as RequestContext<TEnv>["reverse"];
|
|
128
|
+
}
|
|
129
|
+
return { ctx, request, url, variables };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* What a run accumulated on the request context, surfaced as PUBLIC values so a
|
|
134
|
+
* test never has to cast through the `@internal` `ctx.res` / `ctx.cookies()` to
|
|
135
|
+
* assert what an action produced.
|
|
136
|
+
*/
|
|
137
|
+
export interface RunInRequestContextResult<T> {
|
|
138
|
+
/**
|
|
139
|
+
* The value `fn` returned (awaited), or `undefined` if `fn` threw — in which
|
|
140
|
+
* case the thrown value is on {@link thrown}. The snapshot below is captured
|
|
141
|
+
* either way.
|
|
142
|
+
*/
|
|
143
|
+
result: T | undefined;
|
|
144
|
+
/**
|
|
145
|
+
* The value `fn` threw, or `undefined` if it returned normally. Commonly a
|
|
146
|
+
* `Response` from `throw redirect(...)` / `throw notFound()` — the dominant
|
|
147
|
+
* cookie+flash case is an action that sets them then throws a redirect — so
|
|
148
|
+
* this (and the snapshot below) is observable WITHOUT wrapping the action in
|
|
149
|
+
* your own try/catch. NOTE: the value is captured, NOT re-thrown; assert on it
|
|
150
|
+
* for a throwing action.
|
|
151
|
+
*/
|
|
152
|
+
thrown: unknown;
|
|
153
|
+
/**
|
|
154
|
+
* A Response carrying the status, headers, and Set-Cookie the run set (via
|
|
155
|
+
* `cookies().set()`, `ctx.header()`, etc.). Assert Set-Cookie with
|
|
156
|
+
* `response.headers.getSetCookie()`. When `fn` threw a `Response` (a redirect),
|
|
157
|
+
* THIS is that Response with the accumulated Set-Cookie/headers merged in
|
|
158
|
+
* (mirroring how the framework merges them in production), so a redirect's
|
|
159
|
+
* Location AND the cookies it set are both observable here.
|
|
160
|
+
*/
|
|
161
|
+
response: Response;
|
|
162
|
+
/**
|
|
163
|
+
* The effective cookie view after the run: request cookies merged with
|
|
164
|
+
* anything the run set or deleted (last-write-wins), as `{ name: value }`.
|
|
165
|
+
*/
|
|
166
|
+
cookies: Record<string, string>;
|
|
167
|
+
/**
|
|
168
|
+
* The response headers the run set (via `ctx.header(...)`, plus a thrown
|
|
169
|
+
* redirect's `Location`), as a plain `{ name: value }` object — the same view
|
|
170
|
+
* as `response.headers`, but assertable like `cookies`/`locationState`.
|
|
171
|
+
* EXCLUDES `set-cookie` (use `cookies`, or `response.headers.getSetCookie()`).
|
|
172
|
+
* Header names are lowercased (HTTP headers are case-insensitive).
|
|
173
|
+
*/
|
|
174
|
+
headers: Record<string, string>;
|
|
175
|
+
/**
|
|
176
|
+
* Location state the run set via `ctx.setLocationState()` / `redirect({ state })`,
|
|
177
|
+
* resolved to the flat `{ key: value }` shape the client reads off
|
|
178
|
+
* `history.state` (empty object when none) — so a post-action flash ("Saved!")
|
|
179
|
+
* is assertable at the unit layer.
|
|
180
|
+
*/
|
|
181
|
+
locationState: Record<string, unknown>;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Snapshot the observable effects a run left on `ctx` (cookies + location
|
|
186
|
+
* state). Reads the fields directly off the ctx object, so it works both inside
|
|
187
|
+
* and outside the AsyncLocalStorage scope (no `getRequestContext()`). Headers are
|
|
188
|
+
* snapshotted separately from the final {@link Response} (via
|
|
189
|
+
* {@link headersToObject}) so a thrown redirect's `Location` is included.
|
|
190
|
+
*/
|
|
191
|
+
export function snapshotRunEffects<TEnv>(ctx: RequestContext<TEnv>): {
|
|
192
|
+
cookies: Record<string, string>;
|
|
193
|
+
locationState: Record<string, unknown>;
|
|
194
|
+
} {
|
|
195
|
+
return {
|
|
196
|
+
cookies: { ...ctx.cookies() },
|
|
197
|
+
locationState: resolveLocationStateEntries(ctx._locationState ?? []),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The response headers as a plain `{ name: value }` object, EXCLUDING
|
|
203
|
+
* `set-cookie` (surfaced parsed on `cookies`). Names are lowercased (HTTP header
|
|
204
|
+
* names are case-insensitive). Read from the final response so a thrown
|
|
205
|
+
* redirect's `Location` and any `ctx.header(...)` both appear.
|
|
206
|
+
*/
|
|
207
|
+
export function headersToObject(headers: Headers): Record<string, string> {
|
|
208
|
+
const out: Record<string, string> = {};
|
|
209
|
+
headers.forEach((value, name) => {
|
|
210
|
+
if (name.toLowerCase() === "set-cookie") return;
|
|
211
|
+
out[name] = value;
|
|
212
|
+
});
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Build the observable response from what the run accumulated on `ctx.res`. When
|
|
218
|
+
* `fn` threw a `Response` (a `redirect()`/`notFound()`), that Response IS the
|
|
219
|
+
* response — merge the accumulated Set-Cookie/other headers into it (the
|
|
220
|
+
* framework does this when it catches the thrown Response in production), with
|
|
221
|
+
* its status/Location preserved. Otherwise snapshot the stub (status + headers).
|
|
222
|
+
* The `Response`/`Headers` constructors copy, so the result is immutable.
|
|
223
|
+
*/
|
|
224
|
+
function buildRunResponse<TEnv>(
|
|
225
|
+
ctx: RequestContext<TEnv>,
|
|
226
|
+
thrown: unknown,
|
|
227
|
+
): Response {
|
|
228
|
+
const stub = ctx.res;
|
|
229
|
+
if (thrown instanceof Response) {
|
|
230
|
+
const headers = new Headers(thrown.headers);
|
|
231
|
+
for (const cookie of stub.headers.getSetCookie()) {
|
|
232
|
+
headers.append("set-cookie", cookie);
|
|
233
|
+
}
|
|
234
|
+
stub.headers.forEach((value, name) => {
|
|
235
|
+
if (name.toLowerCase() === "set-cookie") return;
|
|
236
|
+
if (!headers.has(name)) headers.set(name, value);
|
|
237
|
+
});
|
|
238
|
+
return new Response(null, { status: thrown.status, headers });
|
|
239
|
+
}
|
|
240
|
+
return new Response(null, { status: stub.status, headers: stub.headers });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Build a seeded RequestContext (via {@link createTestRequestContext}) and run
|
|
245
|
+
* `fn` inside it, so code under test that calls `getRequestContext()`,
|
|
246
|
+
* `cookies()`, or reads/mutates request headers resolves exactly as in
|
|
247
|
+
* production.
|
|
248
|
+
*
|
|
249
|
+
* This is the entry point for the advanced cases the unit wrappers
|
|
250
|
+
* (`runLoader` / `runMiddleware`) do not model — most notably a server ACTION
|
|
251
|
+
* that authenticates off the request cookie or sets a session cookie / flash:
|
|
252
|
+
* an action has no loader context, so `runLoader` is the wrong shape, yet it
|
|
253
|
+
* still needs a real request context to read the cookie and resolve
|
|
254
|
+
* `getRequestContext()`.
|
|
255
|
+
*
|
|
256
|
+
* Returns `{ result, thrown, response, cookies, headers, locationState }` so the
|
|
257
|
+
* action's OUTPUT (Set-Cookie, response headers, flash) is assertable without
|
|
258
|
+
* casting through the `@internal` `ctx.res` / `ctx.cookies()`. `fn` may be async — the context stays
|
|
259
|
+
* active across its awaits (AsyncLocalStorage), and the snapshot is captured
|
|
260
|
+
* whether `fn` returns OR throws. The throw path matters: the most common
|
|
261
|
+
* cookie+flash case is an auth action that sets a cookie + flash then
|
|
262
|
+
* `throw redirect(...)` on success — the thrown redirect is on `thrown` (NOT
|
|
263
|
+
* re-thrown) and its Location plus the cookies are on `response`/`cookies`.
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* ```ts
|
|
267
|
+
* const { result, cookies, response, thrown } = await runInRequestContext(
|
|
268
|
+
* () => loginAction(input), // sets a session cookie, then `throw redirect("/app")`
|
|
269
|
+
* {
|
|
270
|
+
* env,
|
|
271
|
+
* request: new Request("https://app.test/", {
|
|
272
|
+
* headers: { Cookie: "sid=abc" },
|
|
273
|
+
* }),
|
|
274
|
+
* },
|
|
275
|
+
* );
|
|
276
|
+
* expect(cookies.session).toBe("new-token");
|
|
277
|
+
* expect(headers.location).toBe("/app"); // response headers as a plain object
|
|
278
|
+
* expect((thrown as Response).headers.get("Location")).toBe("/app");
|
|
279
|
+
* expect(response.headers.getSetCookie()).toContainEqual(
|
|
280
|
+
* expect.stringContaining("session="),
|
|
281
|
+
* );
|
|
282
|
+
* ```
|
|
283
|
+
*/
|
|
284
|
+
export async function runInRequestContext<T, TEnv = unknown>(
|
|
285
|
+
fn: (ctx: RequestContext<TEnv>) => T | Promise<T>,
|
|
286
|
+
opts: CreateTestContextOptions<TEnv> = {},
|
|
287
|
+
): Promise<RunInRequestContextResult<T>> {
|
|
288
|
+
const { ctx } = createTestRequestContext<TEnv>(opts);
|
|
289
|
+
let result: T | undefined;
|
|
290
|
+
let thrown: unknown;
|
|
291
|
+
let didThrow = false;
|
|
292
|
+
try {
|
|
293
|
+
result = (await runWithRequestContext(ctx, () => fn(ctx))) as T;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
// Capture (do NOT re-throw): a redirect/notFound action throws its Response
|
|
296
|
+
// on the SUCCESS path, and its cookie/flash output must stay observable.
|
|
297
|
+
didThrow = true;
|
|
298
|
+
thrown = error;
|
|
299
|
+
}
|
|
300
|
+
const { cookies, locationState } = snapshotRunEffects(ctx);
|
|
301
|
+
const response = buildRunResponse(ctx, didThrow ? thrown : undefined);
|
|
302
|
+
const headers = headersToObject(response.headers);
|
|
303
|
+
return { result, thrown, response, cookies, headers, locationState };
|
|
304
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Side-effect module: define the webpack-style globals the vendored
|
|
3
|
+
* react-server-dom CLIENT deserializer reads at module-eval time.
|
|
4
|
+
*
|
|
5
|
+
* In a real app the plugin-rsc Vite plugin rewrites `__webpack_require__` ->
|
|
6
|
+
* `__vite_rsc_require__` and `__webpack_require__.u` -> `({}).u`
|
|
7
|
+
* (@vitejs/plugin-rsc `core/plugin.js`). That transform does NOT run in a bare
|
|
8
|
+
* Vitest process, so the vendored client's free `__webpack_require__` /
|
|
9
|
+
* `__webpack_chunk_load__` references would be undefined. We provide minimal
|
|
10
|
+
* shims: `__webpack_require__` routes to the loader installed via
|
|
11
|
+
* `setRequireModule`, and `__webpack_chunk_load__` is a no-op (renderServerTree
|
|
12
|
+
* serializes with empty `chunks`, so no chunk fetch ever happens).
|
|
13
|
+
*
|
|
14
|
+
* MUST be imported (for side effect) BEFORE `@vitejs/plugin-rsc/react/browser`,
|
|
15
|
+
* which is why flight-tree.ts lists it first.
|
|
16
|
+
*/
|
|
17
|
+
const g = globalThis as unknown as {
|
|
18
|
+
__webpack_require__?: ((id: string) => unknown) & { u?: unknown };
|
|
19
|
+
__webpack_chunk_load__?: (chunkId: string) => Promise<unknown>;
|
|
20
|
+
__vite_rsc_client_require__?: (id: string) => unknown;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
if (!g.__webpack_require__) {
|
|
24
|
+
g.__webpack_require__ = (id: string) => g.__vite_rsc_client_require__!(id);
|
|
25
|
+
}
|
|
26
|
+
if (!g.__webpack_chunk_load__) {
|
|
27
|
+
g.__webpack_chunk_load__ = async () => {};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variable seeding shared by the node/DOM testing tier (internal/context.ts)
|
|
3
|
+
* AND the react-server Flight tier (flight.ts). Depends only on the
|
|
4
|
+
* dependency-free `context-var` module, so it is safe to import under the
|
|
5
|
+
* `react-server` condition (unlike internal/context.ts, which pulls
|
|
6
|
+
* client/browser modules).
|
|
7
|
+
*/
|
|
8
|
+
import { contextSet, type ContextVar } from "../../context-var.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Initializer for seeded context variables (as a prior middleware would have
|
|
12
|
+
* set, or a server component would read during render). Either a plain object
|
|
13
|
+
* keyed by var name (the common, best-inferring form: `{ user: u }`) or a list
|
|
14
|
+
* of `[key, value]` tuples where the key may be a `createVar()` handle or a
|
|
15
|
+
* string (`[[userVar, u], ["flag", true]]`).
|
|
16
|
+
*/
|
|
17
|
+
export type VarsInit =
|
|
18
|
+
| Record<string, unknown>
|
|
19
|
+
| ReadonlyArray<readonly [ContextVar<unknown> | string, unknown]>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Preload variables as if set by upstream middleware (or visible to a rendered
|
|
23
|
+
* server tree). Accepts entries keyed by either a ContextVar (from createVar) or
|
|
24
|
+
* a string, matching ctx.set().
|
|
25
|
+
*/
|
|
26
|
+
export function seedVariables(
|
|
27
|
+
variables: Record<string, unknown>,
|
|
28
|
+
vars?: VarsInit,
|
|
29
|
+
): Record<string, unknown> {
|
|
30
|
+
if (!vars) return variables;
|
|
31
|
+
// Array/iterable form -> use the tuples as-is; plain object -> its entries.
|
|
32
|
+
const entries: Iterable<readonly [ContextVar<unknown> | string, unknown]> =
|
|
33
|
+
Symbol.iterator in (vars as object)
|
|
34
|
+
? (vars as ReadonlyArray<
|
|
35
|
+
readonly [ContextVar<unknown> | string, unknown]
|
|
36
|
+
>)
|
|
37
|
+
: Object.entries(vars as Record<string, unknown>);
|
|
38
|
+
for (const [key, value] of entries) {
|
|
39
|
+
contextSet(variables, key as ContextVar<unknown>, value);
|
|
40
|
+
}
|
|
41
|
+
return variables;
|
|
42
|
+
}
|