@rangojs/router 0.0.0-experimental.124 → 0.0.0-experimental.126
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 +6 -4
- package/dist/bin/rango.js +3 -4
- package/dist/vite/index.js +315 -68
- package/package.json +19 -18
- package/skills/breadcrumbs/SKILL.md +60 -0
- package/skills/hooks/SKILL.md +2 -2
- package/skills/route/SKILL.md +6 -0
- package/skills/server-actions/SKILL.md +25 -1
- package/skills/testing/SKILL.md +17 -17
- package/skills/testing/cache-prerender.md +29 -3
- package/skills/testing/flight.md +13 -10
- package/skills/testing/render-handler.md +3 -0
- package/skills/testing/server-tree.md +1 -1
- package/skills/testing/setup.md +1 -1
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- package/src/browser/action-fence.ts +10 -0
- package/src/browser/event-controller.ts +1 -83
- package/src/browser/navigation-store-handle.ts +3 -4
- package/src/browser/navigation-store.ts +0 -39
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/partial-update.ts +23 -84
- package/src/browser/prefetch/cache.ts +6 -45
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +2 -23
- package/src/browser/react/Link.tsx +0 -2
- package/src/browser/react/NavigationProvider.tsx +2 -1
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +0 -2
- package/src/browser/react/index.ts +0 -45
- package/src/browser/react/location-state-shared.ts +0 -13
- package/src/browser/react/location-state.ts +0 -1
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +0 -5
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +0 -3
- package/src/browser/react/use-params.ts +0 -2
- package/src/browser/react/use-router.ts +2 -1
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/rsc-router.tsx +10 -3
- package/src/browser/server-action-bridge.ts +51 -3
- package/src/browser/types.ts +23 -5
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/index.ts +8 -9
- package/src/build/route-trie.ts +46 -11
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/router-processing.ts +0 -8
- package/src/cache/cache-policy.ts +0 -54
- package/src/cache/cache-runtime.ts +48 -24
- package/src/cache/cache-scope.ts +0 -27
- package/src/cache/cache-tag.ts +0 -37
- package/src/cache/cf/cf-cache-store.ts +72 -45
- package/src/cache/cf/index.ts +0 -24
- package/src/cache/document-cache.ts +10 -36
- package/src/cache/handle-snapshot.ts +0 -40
- package/src/cache/index.ts +0 -27
- package/src/cache/memory-segment-store.ts +0 -52
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/types.ts +0 -98
- package/src/client.rsc.tsx +4 -22
- package/src/client.tsx +19 -32
- package/src/context-var.ts +12 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/handle.ts +2 -12
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +6 -0
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +1 -65
- package/src/host/testing.ts +0 -16
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +0 -4
- package/src/index.rsc.ts +27 -2
- package/src/index.ts +7 -0
- package/src/internal-debug.ts +2 -4
- package/src/loader.rsc.ts +4 -15
- package/src/loader.ts +3 -9
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +23 -30
- package/src/prerender.ts +34 -0
- package/src/redirect-origin.ts +100 -0
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +1 -44
- package/src/route-definition/dsl-helpers.ts +7 -19
- package/src/route-definition/helpers-types.ts +3 -3
- package/src/route-definition/redirect.ts +43 -9
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-map-builder.ts +0 -16
- package/src/router/content-negotiation.ts +0 -13
- package/src/router/error-handling.ts +12 -16
- package/src/router/find-match.ts +4 -31
- package/src/router/intercept-resolution.ts +10 -1
- package/src/router/lazy-includes.ts +1 -57
- package/src/router/loader-resolution.ts +25 -23
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +1 -25
- package/src/router/match-api.ts +0 -20
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +0 -43
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +96 -179
- package/src/router/match-middleware/cache-store.ts +0 -31
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +1 -52
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +0 -116
- package/src/router/middleware.ts +77 -60
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +5 -56
- package/src/router/prerender-match.ts +56 -51
- package/src/router/request-classification.ts +1 -38
- package/src/router/revalidation.ts +14 -62
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +0 -27
- package/src/router/router-interfaces.ts +10 -0
- package/src/router/segment-resolution/fresh.ts +25 -57
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +35 -23
- package/src/router/segment-resolution/revalidation.ts +188 -283
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +0 -3
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +0 -22
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +66 -45
- package/src/router/types.ts +1 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +8 -11
- package/src/rsc/handler-context.ts +1 -0
- package/src/rsc/handler.ts +20 -4
- package/src/rsc/helpers.ts +71 -3
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/origin-guard.ts +9 -15
- package/src/rsc/progressive-enhancement.ts +10 -1
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-route-handler.ts +23 -18
- package/src/rsc/rsc-rendering.ts +2 -7
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +34 -29
- package/src/rsc/types.ts +6 -3
- package/src/search-params.ts +0 -16
- package/src/segment-loader-promise.ts +14 -2
- package/src/segment-system.tsx +79 -88
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +29 -92
- package/src/ssr/index.tsx +14 -14
- package/src/static-handler.ts +2 -27
- package/src/testing/cache-status.ts +44 -48
- package/src/testing/collect-handle.ts +1 -24
- package/src/testing/dispatch.ts +43 -6
- package/src/testing/e2e/index.ts +1 -22
- package/src/testing/e2e/matchers.ts +0 -16
- package/src/testing/flight-matchers.ts +0 -13
- package/src/testing/flight-normalize.ts +3 -30
- package/src/testing/flight.ts +46 -48
- package/src/testing/generated-routes.ts +1 -41
- package/src/testing/index.ts +1 -21
- package/src/testing/internal/context.ts +3 -45
- package/src/testing/internal/seed-vars.ts +0 -26
- package/src/testing/render-handler.ts +31 -61
- package/src/testing/render-route.tsx +75 -103
- package/src/testing/run-loader.ts +0 -96
- package/src/testing/run-middleware.ts +0 -26
- package/src/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/error-types.ts +25 -89
- package/src/types/global-namespace.ts +4 -14
- package/src/types/handler-context.ts +28 -9
- package/src/types/index.ts +0 -10
- package/src/types/request-scope.ts +0 -19
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +0 -13
- package/src/urls/include-helper.ts +0 -4
- package/src/urls/index.ts +0 -6
- package/src/urls/path-helper-types.ts +2 -2
- package/src/urls/path-helper.ts +0 -54
- package/src/urls/urls-function.ts +0 -13
- package/src/use-loader.tsx +0 -186
- package/src/vite/discovery/bundle-postprocess.ts +2 -1
- package/src/vite/discovery/discover-routers.ts +28 -18
- package/src/vite/discovery/prerender-collection.ts +2 -4
- package/src/vite/discovery/state.ts +5 -0
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/plugin-types.ts +35 -9
- package/src/vite/plugins/cjs-to-esm.ts +0 -11
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +0 -10
- package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
- package/src/vite/plugins/expose-action-id.ts +2 -73
- package/src/vite/plugins/expose-id-utils.ts +0 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
- package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
- package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
- package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
- package/src/vite/plugins/expose-internal-ids.ts +10 -0
- package/src/vite/plugins/performance-tracks.ts +0 -3
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +21 -46
- package/src/vite/plugins/version-injector.ts +0 -20
- package/src/vite/plugins/version-plugin.ts +1 -49
- package/src/vite/plugins/virtual-entries.ts +0 -15
- package/src/vite/rango.ts +2 -108
- package/src/vite/router-discovery.ts +9 -1
- package/src/vite/utils/ast-handler-extract.ts +0 -16
- package/src/vite/utils/bundle-analysis.ts +6 -13
- package/src/vite/utils/client-chunks.ts +0 -6
- package/src/vite/utils/forward-user-plugins.ts +0 -22
- package/src/vite/utils/manifest-utils.ts +0 -4
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +0 -35
- package/src/vite/utils/shared-utils.ts +3 -35
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
* client context (see the `loaders` / `locationState` / `handles` options) —
|
|
24
24
|
* nothing is executed on the server. This exercises the read path
|
|
25
25
|
* (useLoader / useLocationState / useHandle from context), not the run path.
|
|
26
|
+
* - navigate() commits synchronously, so it does NOT drive the navigation
|
|
27
|
+
* lifecycle: useNavigation().state, useLinkStatus().pending, and
|
|
28
|
+
* useAction().state stay "idle". Assert pending/loading/submitting transition
|
|
29
|
+
* states with renderServerTree / e2e instead (navigate() warns once if used).
|
|
26
30
|
* What it DOES cover: client hooks that read NavigationProvider /
|
|
27
31
|
* OutletContext — useParams, useReverse, useHref, useMount, useNavigation,
|
|
28
32
|
* useRouter, usePathname, useSearchParams, Outlet nesting, useLoader /
|
|
@@ -53,6 +57,7 @@ import type { LocationStateDefinition } from "../browser/react/location-state-sh
|
|
|
53
57
|
import type { Handle } from "../handle.js";
|
|
54
58
|
import type { ThemeConfig } from "../theme/types.js";
|
|
55
59
|
import { resolveThemeConfig } from "../theme/constants.js";
|
|
60
|
+
import { isUnderTestRunner } from "../runtime-env.js";
|
|
56
61
|
|
|
57
62
|
const TEST_ORIGIN = "http://localhost";
|
|
58
63
|
|
|
@@ -63,11 +68,6 @@ const TEST_ORIGIN = "http://localhost";
|
|
|
63
68
|
*/
|
|
64
69
|
export type HandleDataSeed = Record<string, Record<string, unknown[]>>;
|
|
65
70
|
|
|
66
|
-
// Loaders and location-state defs carry an id (`$$id` / `__rsc_ls_key`) that the
|
|
67
|
-
// Vite plugin injects at build time; in a bare test it is "". These helpers
|
|
68
|
-
// assign a synthetic stable id (mutating the handle, tracked per-object) so that
|
|
69
|
-
// seeding by reference lines up with the read path (useLoader / useLocationState
|
|
70
|
-
// both read the id off the handle at call time).
|
|
71
71
|
const syntheticIds = new WeakMap<object, string>();
|
|
72
72
|
let syntheticIdCounter = 0;
|
|
73
73
|
|
|
@@ -86,7 +86,6 @@ function ensureSyntheticId(
|
|
|
86
86
|
return id;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
/** One-level clone of a raw handle seed so we don't mutate the caller's object. */
|
|
90
89
|
function cloneHandleSeed(seed?: HandleDataSeed): HandleDataSeed {
|
|
91
90
|
const out: HandleDataSeed = {};
|
|
92
91
|
for (const [name, segMap] of Object.entries(seed ?? {})) {
|
|
@@ -95,12 +94,6 @@ function cloneHandleSeed(seed?: HandleDataSeed): HandleDataSeed {
|
|
|
95
94
|
return out;
|
|
96
95
|
}
|
|
97
96
|
|
|
98
|
-
/**
|
|
99
|
-
* One node of the route definition passed to renderRoute. The array models a
|
|
100
|
-
* single matched route plus its optional layout chain — element order is
|
|
101
|
-
* outermost layout first, the leaf route last (the same root-to-leaf order the
|
|
102
|
-
* real matcher produces).
|
|
103
|
-
*/
|
|
104
97
|
export interface RenderRouteSpec {
|
|
105
98
|
/**
|
|
106
99
|
* The route pattern this node matches, e.g. "/products/:productId". The LAST
|
|
@@ -172,7 +165,10 @@ export interface RenderRouteOptions {
|
|
|
172
165
|
/**
|
|
173
166
|
* Explicit params. Merged over (and overriding) params extracted from the
|
|
174
167
|
* `request` URL. Use this when the URL alone cannot express the params, or to
|
|
175
|
-
* avoid relying on URL parsing.
|
|
168
|
+
* avoid relying on URL parsing. Supplying params also OPTS OUT of the
|
|
169
|
+
* request/leaf match check: a `request` whose pathname does not resolve the
|
|
170
|
+
* leaf is normally rejected under the test runner, but passing params here
|
|
171
|
+
* tells renderRoute the request is intentionally not the param source.
|
|
176
172
|
*/
|
|
177
173
|
params?: Record<string, string>;
|
|
178
174
|
/**
|
|
@@ -236,6 +232,10 @@ export interface RenderRouteOptions {
|
|
|
236
232
|
* exactly as `renderSegments` does in production (a segment whose `mountPath`
|
|
237
233
|
* is set is wrapped in a MountContextProvider). Normalized like a path prefix
|
|
238
234
|
* (leading slash forced, trailing stripped, bare "/" -> root). Defaults to "/".
|
|
235
|
+
* An explicitly-passed `request` must match the leaf `path` directly (paths are
|
|
236
|
+
* include-RELATIVE; the mount does NOT rewrite the request) — pass the relative
|
|
237
|
+
* path, not the mount-prefixed one, or renderRoute throws rather than silently
|
|
238
|
+
* rendering empty params.
|
|
239
239
|
*
|
|
240
240
|
* @example
|
|
241
241
|
* renderRoute([{ path: "/c/wine", Component: ProductPage }], { mount: "/shop" });
|
|
@@ -281,11 +281,6 @@ interface ResolvedMatch {
|
|
|
281
281
|
pathname: string;
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
/**
|
|
285
|
-
* Match a pathname against the leaf spec's pattern and extract params.
|
|
286
|
-
* Returns null when the pattern does not match (params then fall back to the
|
|
287
|
-
* caller-provided `options.params`).
|
|
288
|
-
*/
|
|
289
284
|
function matchLeaf(
|
|
290
285
|
pattern: string,
|
|
291
286
|
pathname: string,
|
|
@@ -303,7 +298,6 @@ function matchLeaf(
|
|
|
303
298
|
return params;
|
|
304
299
|
}
|
|
305
300
|
|
|
306
|
-
/** Derive a usable initial pathname from a leaf pattern when none is given. */
|
|
307
301
|
function staticPrefix(pattern: string): string {
|
|
308
302
|
const out: string[] = [];
|
|
309
303
|
for (const part of pattern.split("/")) {
|
|
@@ -314,13 +308,6 @@ function staticPrefix(pattern: string): string {
|
|
|
314
308
|
return "/" + out.join("/");
|
|
315
309
|
}
|
|
316
310
|
|
|
317
|
-
/**
|
|
318
|
-
* Build the synthetic ResolvedSegment[] for a matched route. Produces, in
|
|
319
|
-
* root-to-leaf order: one layout segment per non-leaf spec, then the leaf route
|
|
320
|
-
* segment, plus a loader segment for each seeded loader id attached to the
|
|
321
|
-
* owning spec. Segment ids follow the real convention (L0, L0L1, ..., the leaf
|
|
322
|
-
* route as L0...R{n}; loaders as {parentId}D{i}.{loaderId}).
|
|
323
|
-
*/
|
|
324
311
|
function buildSegments(
|
|
325
312
|
routes: RenderRouteSpec[],
|
|
326
313
|
params: Record<string, string>,
|
|
@@ -353,20 +340,13 @@ function buildSegments(
|
|
|
353
340
|
params,
|
|
354
341
|
belongsToRoute: true,
|
|
355
342
|
};
|
|
356
|
-
// Model an include() mount: every component segment in the chain shares the
|
|
357
|
-
// same prefix, so renderSegments wraps each in a MountContextProvider and
|
|
358
|
-
// useMount() resolves the mounted prefix (production sets mountPath on every
|
|
359
|
-
// segment of an included subtree). Must be applied identically at both
|
|
360
|
-
// buildSegments call sites or segment-structure-assert flags a remount.
|
|
361
343
|
if (mount) node.mountPath = mount;
|
|
362
|
-
// A leaf-owned layout component wraps the route via its own layout element.
|
|
363
344
|
if (isLeaf && spec.layout) {
|
|
364
345
|
const Layout = spec.layout;
|
|
365
346
|
node.layout = <Layout />;
|
|
366
347
|
}
|
|
367
348
|
segments.push(node);
|
|
368
349
|
|
|
369
|
-
// Determine which seeded loader ids this spec owns.
|
|
370
350
|
const ownedIds = spec.loaderIds
|
|
371
351
|
? spec.loaderIds.filter((id) => id in loaderData)
|
|
372
352
|
: isLeaf
|
|
@@ -390,30 +370,6 @@ function buildSegments(
|
|
|
390
370
|
return segments;
|
|
391
371
|
}
|
|
392
372
|
|
|
393
|
-
/**
|
|
394
|
-
* Render a CLIENT component (and its layout chain) inside the router's
|
|
395
|
-
* NavigationProvider for unit testing. Exported from `@rangojs/router/testing/dom`
|
|
396
|
-
* (its own entry, kept out of the main `@rangojs/router/testing` barrel so that
|
|
397
|
-
* barrel never references React/@testing-library/react). Async so the heavy
|
|
398
|
-
* @testing-library/react dependency is loaded only at call time.
|
|
399
|
-
*
|
|
400
|
-
* @example
|
|
401
|
-
* ```tsx
|
|
402
|
-
* // @vitest-environment happy-dom
|
|
403
|
-
* import { renderRoute } from "@rangojs/router/testing/dom";
|
|
404
|
-
*
|
|
405
|
-
* function Product() {
|
|
406
|
-
* const { productId } = useParams<{ productId: string }>();
|
|
407
|
-
* const reverse = useReverse({ product: "/products/:productId" });
|
|
408
|
-
* return <a href={reverse("product", { productId: "2" })}>{productId}</a>;
|
|
409
|
-
* }
|
|
410
|
-
*
|
|
411
|
-
* const { getByText, router } = await renderRoute(
|
|
412
|
-
* [{ path: "/products/:productId", Component: Product }],
|
|
413
|
-
* { request: "/products/1" },
|
|
414
|
-
* );
|
|
415
|
-
* ```
|
|
416
|
-
*/
|
|
417
373
|
export async function renderRoute(
|
|
418
374
|
routes: RenderRouteSpec[],
|
|
419
375
|
options: RenderRouteOptions = {},
|
|
@@ -421,9 +377,6 @@ export async function renderRoute(
|
|
|
421
377
|
if (routes.length === 0) {
|
|
422
378
|
throw new Error("renderRoute: `routes` must contain at least one entry");
|
|
423
379
|
}
|
|
424
|
-
// The pre-rename `initialUrl` option was renamed to `request`. A plain-JS or
|
|
425
|
-
// spread-defeated caller still passing it would otherwise be silently ignored;
|
|
426
|
-
// fail loud with the migration name instead.
|
|
427
380
|
if ("initialUrl" in options) {
|
|
428
381
|
throw new Error(
|
|
429
382
|
"renderRoute: the `initialUrl` option was renamed to `request`. " +
|
|
@@ -439,30 +392,19 @@ export async function renderRoute(
|
|
|
439
392
|
const initialUrl = requestUrl ?? staticPrefix(leaf.path) ?? "/";
|
|
440
393
|
const url = new URL(initialUrl, TEST_ORIGIN);
|
|
441
394
|
|
|
442
|
-
// Seed loader data: explicit-id entries from `loaderData`, plus by-reference
|
|
443
|
-
// entries from `loaders` (assigning synthetic ids to real handles whose `$$id`
|
|
444
|
-
// is empty in a bare test).
|
|
445
395
|
const loaderData: Record<string, unknown> = { ...(options.loaderData ?? {}) };
|
|
446
396
|
for (const [loader, data] of options.loaders ?? []) {
|
|
447
397
|
loaderData[ensureSyntheticId(loader as object, "$$id")] = data;
|
|
448
398
|
}
|
|
449
399
|
|
|
450
|
-
// Seed location state into history.state so useLocationState(def) resolves.
|
|
451
|
-
// Keyed defs read history.state[def.__rsc_ls_key]; assign a synthetic key when
|
|
452
|
-
// the injected one is empty (bare test). RESET history.state to only this
|
|
453
|
-
// call's seeds (not a merge) so a previous render's seeded state does not leak
|
|
454
|
-
// into a later render in the same DOM environment.
|
|
455
400
|
if (typeof window !== "undefined") {
|
|
456
401
|
const stateObj: Record<string, unknown> = {};
|
|
457
402
|
for (const [def, value] of options.locationState ?? []) {
|
|
458
403
|
stateObj[ensureSyntheticId(def as object, "__rsc_ls_key")] = value;
|
|
459
404
|
}
|
|
460
|
-
// No URL arg: useLocationState reads history.state (not the URL), and passing
|
|
461
|
-
// a TEST_ORIGIN URL would trip the DOM env's same-origin check.
|
|
462
405
|
window.history.replaceState(stateObj, "");
|
|
463
406
|
}
|
|
464
407
|
|
|
465
|
-
// Resolve params: URL-extracted params first, explicit params override.
|
|
466
408
|
const resolve = (pathname: string): ResolvedMatch => {
|
|
467
409
|
const matched = matchLeaf(leaf.path, pathname) ?? {};
|
|
468
410
|
return {
|
|
@@ -472,11 +414,34 @@ export async function renderRoute(
|
|
|
472
414
|
};
|
|
473
415
|
const initialMatch = resolve(url.pathname);
|
|
474
416
|
|
|
475
|
-
// Reuse the real browser primitives so context shape matches production.
|
|
476
417
|
const historyKey = generateHistoryKey(url.href);
|
|
477
|
-
// Normalize the include() mount prefix once and apply it at BOTH buildSegments
|
|
478
|
-
// call sites (initial + navigate) so mountPath is consistent across renders.
|
|
479
418
|
const mount = normalizeBasename(options.mount);
|
|
419
|
+
// Fail loud on a request that cannot resolve the leaf route (a typo, or the
|
|
420
|
+
// mount-prefixed-vs-relative confusion) instead of silently rendering empty
|
|
421
|
+
// params (matchLeaf -> null -> {}). renderRoute paths are include-RELATIVE and
|
|
422
|
+
// resolve() matches the request against the leaf as-is, so the request must be
|
|
423
|
+
// the relative form — a mount does NOT rewrite it. Only checked when `request`
|
|
424
|
+
// was passed explicitly (a defaulted request is staticPrefix of the leaf and
|
|
425
|
+
// always matches). Skipped when explicit `params` are supplied: those are
|
|
426
|
+
// merged over the URL-extracted params in resolve(), so the request is
|
|
427
|
+
// intentionally not the param source and an empty matchLeaf is not the trap.
|
|
428
|
+
// Gated on the test runner so it can never affect production.
|
|
429
|
+
if (
|
|
430
|
+
options.request !== undefined &&
|
|
431
|
+
Object.keys(options.params ?? {}).length === 0 &&
|
|
432
|
+
isUnderTestRunner() &&
|
|
433
|
+
matchLeaf(leaf.path, url.pathname) === null
|
|
434
|
+
) {
|
|
435
|
+
throw new Error(
|
|
436
|
+
`renderRoute: request "${url.pathname}" does not match the leaf route ` +
|
|
437
|
+
`"${leaf.path}"${mount ? ` (mount "${mount}")` : ""}. renderRoute paths ` +
|
|
438
|
+
`are include-RELATIVE: pass a request that matches "${leaf.path}" ` +
|
|
439
|
+
`(e.g. "${staticPrefix(leaf.path)}"). A mount does NOT auto-rewrite the ` +
|
|
440
|
+
`request — pass the relative path, not the mount-prefixed one. If the ` +
|
|
441
|
+
`request URL intentionally does not carry the params, pass them ` +
|
|
442
|
+
`explicitly via the \`params\` option to bypass this check.`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
480
445
|
const initialSegments = buildSegments(
|
|
481
446
|
routes,
|
|
482
447
|
initialMatch.params,
|
|
@@ -490,20 +455,12 @@ export async function renderRoute(
|
|
|
490
455
|
initialSegments,
|
|
491
456
|
crossTabSync: false,
|
|
492
457
|
});
|
|
493
|
-
// Seed handle data: raw `handle` entries plus by-reference `handles` attached
|
|
494
|
-
// to the leaf route segment under each handle's id (so useHandle(handle)
|
|
495
|
-
// resolves the pushed values).
|
|
496
458
|
const leafRouteSegmentId =
|
|
497
459
|
[...initialSegments].reverse().find((s) => s.type === "route")?.id ??
|
|
498
460
|
initialSegments[initialSegments.length - 1]?.id;
|
|
499
461
|
const handleSeed: HandleDataSeed = cloneHandleSeed(options.handle);
|
|
500
462
|
for (const [handle, values] of options.handles ?? []) {
|
|
501
463
|
if (leafRouteSegmentId === undefined) continue;
|
|
502
|
-
// createHandle always has a non-empty $$id (the Vite plugin injects one, and
|
|
503
|
-
// createHandle assigns a runtime fallback otherwise) with its REAL collect
|
|
504
|
-
// registered — so seeding under handle.$$id makes useHandle(handle) run the
|
|
505
|
-
// handle's actual collect/accumulator (custom collects included), not just a
|
|
506
|
-
// default flatten.
|
|
507
464
|
const id = (handle as unknown as { $$id: string }).$$id;
|
|
508
465
|
(handleSeed[id] ??= {})[leafRouteSegmentId] = values;
|
|
509
466
|
}
|
|
@@ -515,15 +472,23 @@ export async function renderRoute(
|
|
|
515
472
|
initialSegments.map((s) => s.id),
|
|
516
473
|
);
|
|
517
474
|
|
|
518
|
-
|
|
519
|
-
// re-render. No server fetch — only routes passed to renderRoute exist. The
|
|
520
|
-
// store update is flushed inside act() so React commits before callers
|
|
521
|
-
// assert, mirroring how a real navigation lands a single payload swap.
|
|
522
|
-
// NOTE: the seeded `loaderData` is reused for the target route too (no
|
|
523
|
-
// per-route loader fetch in a unit test), so every seeded loader stays
|
|
524
|
-
// available after navigate() — unlike a real navigation, which would fetch
|
|
525
|
-
// the target route's own loaders. This is a deliberate test-isolation design.
|
|
475
|
+
let warnedNavLifecycle = false;
|
|
526
476
|
const navigate = async (target: string): Promise<void> => {
|
|
477
|
+
// renderRoute commits navigations synchronously (no server fetch, no Flight
|
|
478
|
+
// stream), so it never drives the navigation lifecycle. The transition state
|
|
479
|
+
// useNavigation()/useLinkStatus()/useAction() read stays "idle" — asserting a
|
|
480
|
+
// pending/loading/submitting state here proves nothing. Warn once (per render)
|
|
481
|
+
// under the test runner so that false-confidence trap is loud, not silent.
|
|
482
|
+
if (isUnderTestRunner() && !warnedNavLifecycle) {
|
|
483
|
+
warnedNavLifecycle = true;
|
|
484
|
+
console.warn(
|
|
485
|
+
"renderRoute: navigate()/useRouter().push commit synchronously and do " +
|
|
486
|
+
"NOT drive the navigation lifecycle. useNavigation().state, " +
|
|
487
|
+
'useLinkStatus().pending, and useAction().state stay "idle" here. ' +
|
|
488
|
+
"Assert params/pathname/content after navigate(); use renderServerTree " +
|
|
489
|
+
"or e2e to assert pending/loading/submitting transition states.",
|
|
490
|
+
);
|
|
491
|
+
}
|
|
527
492
|
const nextUrl = new URL(target, TEST_ORIGIN);
|
|
528
493
|
const match = resolve(nextUrl.pathname);
|
|
529
494
|
const segments = buildSegments(routes, match.params, loaderData, mount);
|
|
@@ -554,18 +519,26 @@ export async function renderRoute(
|
|
|
554
519
|
);
|
|
555
520
|
const initialTree = await renderSegments(initialSegments);
|
|
556
521
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
522
|
+
// Wrap render in an awaited async act so a tree that suspends (async loaders,
|
|
523
|
+
// loading states, deferred handle entries that arrive as a Promise) settles its
|
|
524
|
+
// Suspense within act — otherwise React orphans the resolution ("a component
|
|
525
|
+
// suspended inside an act scope, but the act call was not awaited") and the
|
|
526
|
+
// resolved content never reaches the asserted DOM.
|
|
527
|
+
let result!: Awaited<ReturnType<typeof render>>;
|
|
528
|
+
await act(async () => {
|
|
529
|
+
result = render(
|
|
530
|
+
<NavigationProvider
|
|
531
|
+
store={store}
|
|
532
|
+
eventController={eventController}
|
|
533
|
+
initialPayload={{ root: initialTree, metadata: initialMetadata }}
|
|
534
|
+
bridge={bridge}
|
|
535
|
+
basename={normalizeBasename(options.basename)}
|
|
536
|
+
themeConfig={
|
|
537
|
+
options.theme === undefined ? null : resolveThemeConfig(options.theme)
|
|
538
|
+
}
|
|
539
|
+
/>,
|
|
540
|
+
);
|
|
541
|
+
});
|
|
569
542
|
|
|
570
543
|
const router: TestRouterHandle = {
|
|
571
544
|
navigate,
|
|
@@ -578,7 +551,6 @@ export async function renderRoute(
|
|
|
578
551
|
return Object.assign(result, { router });
|
|
579
552
|
}
|
|
580
553
|
|
|
581
|
-
/** Minimal RscMetadata for client-side re-renders (no server-only fields). */
|
|
582
554
|
function makeMetadata(
|
|
583
555
|
pathname: string,
|
|
584
556
|
segments: ResolvedSegment[],
|
|
@@ -177,10 +177,6 @@ export interface RunLoaderOptions<TEnv = any> {
|
|
|
177
177
|
handles?: ReadonlyArray<readonly [Handle<any, any>, unknown]>;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
/**
|
|
181
|
-
* Merge `search` into a request's URL, returning a value `toRequest` can build.
|
|
182
|
-
* Keeps the original method/headers/body when a Request was passed.
|
|
183
|
-
*/
|
|
184
180
|
function withSearch(
|
|
185
181
|
request: Request | string | undefined,
|
|
186
182
|
search: Record<string, string> | undefined,
|
|
@@ -206,16 +202,6 @@ export type RunnableLoader<T> =
|
|
|
206
202
|
| ((ctx: TestLoaderContext) => Promise<T> | T)
|
|
207
203
|
| LoaderDefinition<T, any>;
|
|
208
204
|
|
|
209
|
-
/**
|
|
210
|
-
* Resolve the function to run from either a raw body or a `createLoader()` handle.
|
|
211
|
-
*
|
|
212
|
-
* A handle carries no inline body (`createLoader` registers it in the fetchable
|
|
213
|
-
* registry by `$$id`), so recover it from there — `def.fn` first (a hand-built
|
|
214
|
-
* def), then the registry. This works when the handle resolves through the
|
|
215
|
-
* SERVER build (the consumer's `@rangojs/router` under `rangoTestConfig`, which
|
|
216
|
-
* registers the fn); the CLIENT stub drops the body, so a handle imported that
|
|
217
|
-
* way is unrecoverable and we say so explicitly.
|
|
218
|
-
*/
|
|
219
205
|
function resolveLoaderFn<T>(
|
|
220
206
|
loader: RunnableLoader<T>,
|
|
221
207
|
): (ctx: TestLoaderContext) => Promise<T> | T {
|
|
@@ -237,31 +223,11 @@ function resolveLoaderFn<T>(
|
|
|
237
223
|
return fn as (ctx: TestLoaderContext) => Promise<T> | T;
|
|
238
224
|
}
|
|
239
225
|
|
|
240
|
-
/**
|
|
241
|
-
* Run a loader and return its resolved data. Pass the RAW loader body, or a
|
|
242
|
-
* registered `createLoader()` handle (its fn is recovered from the registry).
|
|
243
|
-
*
|
|
244
|
-
* @example
|
|
245
|
-
* ```ts
|
|
246
|
-
* // raw body
|
|
247
|
-
* const a = await runLoader(
|
|
248
|
-
* async (ctx) => ({ id: ctx.params.id, user: ctx.get("user") }),
|
|
249
|
-
* { params: { id: "42" }, vars: { user: { name: "Ada" } } },
|
|
250
|
-
* );
|
|
251
|
-
* // registered createLoader() handle (recovered from the registry)
|
|
252
|
-
* const b = await runLoader(ProductLoader, { params: { id: "42" } });
|
|
253
|
-
* ```
|
|
254
|
-
*/
|
|
255
|
-
// Build the createTestRequestContext options from runLoader's options. Shared by
|
|
256
|
-
// runLoader (returns the loader data) and runLoaderResult (also snapshots effects).
|
|
257
226
|
function buildLoaderCtxOpts(
|
|
258
227
|
opts: RunLoaderOptions,
|
|
259
228
|
): CreateTestContextOptions<any> {
|
|
260
229
|
return {
|
|
261
230
|
env: opts.env,
|
|
262
|
-
// Bake opts.search into the request URL itself so ctx.request.url, ctx.url,
|
|
263
|
-
// and ctx.searchParams all agree (production carries the query string on the
|
|
264
|
-
// real request — a loader reading ctx.request.url must see it too).
|
|
265
231
|
request: withSearch(opts.request, opts.search),
|
|
266
232
|
requestInit: opts.method ? { method: opts.method } : undefined,
|
|
267
233
|
vars: opts.vars,
|
|
@@ -276,33 +242,19 @@ function buildLoaderCtxOpts(
|
|
|
276
242
|
};
|
|
277
243
|
}
|
|
278
244
|
|
|
279
|
-
// Enter `reqCtx` and run `fn` with a seeded TestLoaderContext (the same ctx shape
|
|
280
|
-
// a real loader receives). The single place the loader context is built, so
|
|
281
|
-
// runLoader and runLoaderResult share identical loader-context semantics.
|
|
282
245
|
function runWithLoaderContext<R>(
|
|
283
246
|
reqCtx: RequestContext<any>,
|
|
284
247
|
opts: RunLoaderOptions,
|
|
285
248
|
fn: (ctx: TestLoaderContext) => R,
|
|
286
249
|
): R {
|
|
287
|
-
// Seed values for ctx.use(SomeHandle), matched by handle reference (so a real
|
|
288
|
-
// handle resolves regardless of its build-injected $$id).
|
|
289
250
|
const handleSeeds = new Map<unknown, unknown>(opts.handles ?? []);
|
|
290
|
-
|
|
291
|
-
// Seed values for ctx.use(OtherLoader), matched by loader reference (same model
|
|
292
|
-
// as renderHandler/renderRoute). Checked before the `use` resolver.
|
|
293
251
|
const loaderSeeds = new Map<unknown, unknown>(opts.loaders ?? []);
|
|
294
|
-
|
|
295
|
-
// Tracks whether the mocked render barrier has settled. ctx.use(handle)
|
|
296
|
-
// reads are gated on this, matching production (loader-resolution.ts).
|
|
297
252
|
let renderedResolved = false;
|
|
298
253
|
|
|
299
254
|
return runWithRequestContext(reqCtx, () => {
|
|
300
255
|
const reverse = opts.routeMap
|
|
301
256
|
? createReverseFunction(opts.routeMap, opts.routeName, opts.params ?? {})
|
|
302
257
|
: ((() => {
|
|
303
|
-
// Documented contract: reverse requires routeMap. Do NOT fall back to
|
|
304
|
-
// reqCtx.reverse (the global route map) — that leaks whichever routes
|
|
305
|
-
// another test registered and contradicts the documented behavior.
|
|
306
258
|
throw new Error(
|
|
307
259
|
"ctx.reverse() requires the `routeMap` option in runLoader(). " +
|
|
308
260
|
"Pass { routeMap: { name: pattern, ... } } to enable reverse().",
|
|
@@ -323,10 +275,6 @@ function runWithLoaderContext<R>(
|
|
|
323
275
|
executionContext: reqCtx.executionContext,
|
|
324
276
|
get: reqCtx.get as TestLoaderContext["get"],
|
|
325
277
|
use: ((dep: LoaderDefinition<any, any> | Handle<any, any>) => {
|
|
326
|
-
// Match production (loader-resolution.ts): reading a handle in a loader
|
|
327
|
-
// requires the render barrier to have settled. Gate BEFORE returning a
|
|
328
|
-
// seed, so a loader that forgets `await ctx.rendered()` fails in the
|
|
329
|
-
// test exactly as it would at runtime.
|
|
330
278
|
if (isHandle(dep) && !renderedResolved) {
|
|
331
279
|
throw new Error(
|
|
332
280
|
`ctx.use(handle) in a loader requires "await ctx.rendered()" first. ` +
|
|
@@ -334,16 +282,8 @@ function runWithLoaderContext<R>(
|
|
|
334
282
|
`the render tree has settled.`,
|
|
335
283
|
);
|
|
336
284
|
}
|
|
337
|
-
// Handle reads (ctx.use(SomeHandle)) resolve from the seeded map first.
|
|
338
285
|
if (handleSeeds.has(dep)) return handleSeeds.get(dep);
|
|
339
|
-
// Post-barrier, an UNSEEDED handle must match production
|
|
340
|
-
// (loader-resolution.ts -> collectHandleData), which runs the handle's
|
|
341
|
-
// registered collect over empty segments (collect([])) rather than
|
|
342
|
-
// throwing or leaking into the loader resolver. Resolve it via
|
|
343
|
-
// collectHandle, which recovers and runs that same collect.
|
|
344
286
|
if (isHandle(dep)) return collectHandle(dep, []);
|
|
345
|
-
// Loader reads (ctx.use(OtherLoader)) resolve from the seeded map next,
|
|
346
|
-
// then the dynamic `use` resolver, then the real request-context use().
|
|
347
287
|
if (loaderSeeds.has(dep)) return loaderSeeds.get(dep);
|
|
348
288
|
if (opts.use) return opts.use(dep as LoaderDefinition<any, any>);
|
|
349
289
|
return reqCtx.use(dep as LoaderDefinition<any, any>);
|
|
@@ -358,7 +298,6 @@ function runWithLoaderContext<R>(
|
|
|
358
298
|
if (typeof opts.rendered === "function") {
|
|
359
299
|
await opts.rendered();
|
|
360
300
|
}
|
|
361
|
-
// Barrier has settled: subsequent ctx.use(handle) reads resolve.
|
|
362
301
|
renderedResolved = true;
|
|
363
302
|
}
|
|
364
303
|
: () => {
|
|
@@ -377,13 +316,6 @@ function runWithLoaderContext<R>(
|
|
|
377
316
|
});
|
|
378
317
|
}
|
|
379
318
|
|
|
380
|
-
/**
|
|
381
|
-
* Run a loader and return its resolved data.
|
|
382
|
-
*
|
|
383
|
-
* Effects the loader sets (cookies, response headers, a thrown redirect) are NOT
|
|
384
|
-
* observable here — use {@link runLoaderResult} for an auth-style loader that
|
|
385
|
-
* sets a `Set-Cookie` and/or `throw redirect(...)`.
|
|
386
|
-
*/
|
|
387
319
|
export async function runLoader<T>(
|
|
388
320
|
loader: RunnableLoader<T>,
|
|
389
321
|
opts: RunLoaderOptions = {},
|
|
@@ -395,12 +327,6 @@ export async function runLoader<T>(
|
|
|
395
327
|
);
|
|
396
328
|
}
|
|
397
329
|
|
|
398
|
-
/**
|
|
399
|
-
* What a loader run accumulated: its data PLUS the response effects it produced,
|
|
400
|
-
* surfaced as PUBLIC values (parity with `runMiddleware`/`runInRequestContext`)
|
|
401
|
-
* so an effect-setting loader is assertable without casting through the
|
|
402
|
-
* `@internal` request context.
|
|
403
|
-
*/
|
|
404
330
|
export interface RunLoaderResult<T> {
|
|
405
331
|
/**
|
|
406
332
|
* The loader's resolved data (the value bare `runLoader` returns), or
|
|
@@ -430,25 +356,6 @@ export interface RunLoaderResult<T> {
|
|
|
430
356
|
stateCookieName: string;
|
|
431
357
|
}
|
|
432
358
|
|
|
433
|
-
/**
|
|
434
|
-
* Run a loader AND surface the response effects it produced. The richer sibling
|
|
435
|
-
* of {@link runLoader} (which returns the bare data): use this when the loader
|
|
436
|
-
* sets a cookie / response header / location-state, or `throw redirect(...)`, and
|
|
437
|
-
* the test must assert that output.
|
|
438
|
-
*
|
|
439
|
-
* @example
|
|
440
|
-
* ```ts
|
|
441
|
-
* // AuthLoader: validates, sets a `session` cookie, then `throw redirect("/")`.
|
|
442
|
-
* const { thrown, response, cookies } = await runLoaderResult(AuthLoader, {
|
|
443
|
-
* request: new Request("https://app.test/login?token=ok"),
|
|
444
|
-
* });
|
|
445
|
-
* expect((thrown as Response).headers.get("Location")).toBe("/");
|
|
446
|
-
* expect(cookies.session).toBeDefined();
|
|
447
|
-
* expect(
|
|
448
|
-
* response.headers.getSetCookie().some((c) => c.startsWith("session=")),
|
|
449
|
-
* ).toBe(true);
|
|
450
|
-
* ```
|
|
451
|
-
*/
|
|
452
359
|
export async function runLoaderResult<T>(
|
|
453
360
|
loader: RunnableLoader<T>,
|
|
454
361
|
opts: RunLoaderOptions = {},
|
|
@@ -465,9 +372,6 @@ export async function runLoaderResult<T>(
|
|
|
465
372
|
Promise.resolve(loaderFn(loaderCtx)),
|
|
466
373
|
);
|
|
467
374
|
} catch (error) {
|
|
468
|
-
// Capture (do NOT re-throw): a loader's success path is often
|
|
469
|
-
// `throw redirect(...)`, and the cookie/flash it set before the throw must
|
|
470
|
-
// stay observable (parity with runInRequestContext).
|
|
471
375
|
thrown = error;
|
|
472
376
|
}
|
|
473
377
|
return { result, ...buildRunSnapshot(reqCtx, thrown, stateCookieName) };
|
|
@@ -133,21 +133,6 @@ export interface RunMiddlewareResult<TEnv = any> {
|
|
|
133
133
|
stateCookieName: string;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
/**
|
|
137
|
-
* Run a middleware chain and return the response plus observable context.
|
|
138
|
-
*
|
|
139
|
-
* @example
|
|
140
|
-
* ```ts
|
|
141
|
-
* const { response, ctx, nextCalled } = await runMiddleware(
|
|
142
|
-
* async (ctx, next) => {
|
|
143
|
-
* if (!ctx.get("user")) return new Response(null, { status: 401 });
|
|
144
|
-
* return next();
|
|
145
|
-
* },
|
|
146
|
-
* { request: "/dashboard", vars: [["user", { id: 1 }]] },
|
|
147
|
-
* );
|
|
148
|
-
* // nextCalled === 1, response.status === 200
|
|
149
|
-
* ```
|
|
150
|
-
*/
|
|
151
136
|
export async function runMiddleware<TEnv = any>(
|
|
152
137
|
mw: MiddlewareFn<TEnv> | MiddlewareFn<TEnv>[],
|
|
153
138
|
opts: RunMiddlewareOptions<TEnv>,
|
|
@@ -181,11 +166,6 @@ export async function runMiddleware<TEnv = any>(
|
|
|
181
166
|
return opts.next?.() ?? new Response(null, { status: 200 });
|
|
182
167
|
};
|
|
183
168
|
|
|
184
|
-
// Match production: app/response middleware receive ctx.reverse built from the
|
|
185
|
-
// route map ALONE (no matched route name or current params), so reversing a
|
|
186
|
-
// parameterized route without explicit params does NOT auto-fill from the
|
|
187
|
-
// current request. Passing routeName/params here would recreate the
|
|
188
|
-
// false-confidence class fixed in dispatch.
|
|
189
169
|
const reverse = opts.routeMap
|
|
190
170
|
? (createReverseFunction(opts.routeMap) as (
|
|
191
171
|
name: string,
|
|
@@ -194,12 +174,6 @@ export async function runMiddleware<TEnv = any>(
|
|
|
194
174
|
) => string)
|
|
195
175
|
: undefined;
|
|
196
176
|
|
|
197
|
-
// Keep the RETURNED ctx.reverse consistent with the map-only reverse the
|
|
198
|
-
// chain receives. createTestRequestContext installs an auto-fill reverse
|
|
199
|
-
// (correct for the loader phase) when routeName/params are passed, but
|
|
200
|
-
// production app/response middleware see a map-only reverse. Without this,
|
|
201
|
-
// a middleware reading getRequestContext().reverse — or a consumer asserting
|
|
202
|
-
// on result.ctx.reverse — would observe auto-fill that production never does.
|
|
203
177
|
if (reverse) {
|
|
204
178
|
(ctx as RequestContext<TEnv>).reverse =
|
|
205
179
|
reverse as RequestContext<TEnv>["reverse"];
|