@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,590 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* renderRoute — a React-Testing-Library-style helper for unit-testing CLIENT
|
|
3
|
+
* components that read @rangojs/router client context (useParams, useReverse,
|
|
4
|
+
* Outlet, useNavigation, useLoader).
|
|
5
|
+
*
|
|
6
|
+
* Peer of React Router's createRoutesStub and Expo Router's renderRouter. It
|
|
7
|
+
* mounts the router's NavigationProvider plus a synthetic segment tree so that
|
|
8
|
+
* a component under test sees real router context, without spinning up a
|
|
9
|
+
* server, a Vite build, or a real Flight round-trip.
|
|
10
|
+
*
|
|
11
|
+
* FIDELITY CONTRACT — read before relying on this helper:
|
|
12
|
+
* This renders the CLIENT tree ONLY. The segment tree is built synthetically
|
|
13
|
+
* from the `routes` you pass; there is no server render and no Flight
|
|
14
|
+
* (de)serialization. Consequences:
|
|
15
|
+
* - It will NOT catch server/client boundary reference-identity remount bugs
|
|
16
|
+
* (a server-serialized component reference differing from the client
|
|
17
|
+
* reference). Use renderServerTree / e2e for those.
|
|
18
|
+
* - It will NOT catch real Flight serialization errors (non-serializable
|
|
19
|
+
* props crossing the RSC boundary), loader execution on the server,
|
|
20
|
+
* middleware, or handler ordering. Those are renderServerTree / renderHandler
|
|
21
|
+
* / e2e territory.
|
|
22
|
+
* - Loader data, location state, and handle output are SEEDED directly into
|
|
23
|
+
* client context (see the `loaders` / `locationState` / `handles` options) —
|
|
24
|
+
* nothing is executed on the server. This exercises the read path
|
|
25
|
+
* (useLoader / useLocationState / useHandle from context), not the run path.
|
|
26
|
+
* What it DOES cover: client hooks that read NavigationProvider /
|
|
27
|
+
* OutletContext — useParams, useReverse, useHref, useMount, useNavigation,
|
|
28
|
+
* useRouter, usePathname, useSearchParams, Outlet nesting, useLoader /
|
|
29
|
+
* useFetchLoader (seeded data), useLocationState (seeded), and useHandle (seeded).
|
|
30
|
+
* Basename-mounted apps: pass the `basename` option so useRouter().basename,
|
|
31
|
+
* <Link> prefixing, and useMount/useHref resolve against the mount prefix
|
|
32
|
+
* (without it they resolve at the root "/"). For an include("/shop", ...)
|
|
33
|
+
* subtree, pass the `mount` option so useMount() returns the mounted prefix
|
|
34
|
+
* (the segment chain is wrapped in a MountContext exactly as in production).
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import type { ReactNode, ComponentType } from "react";
|
|
38
|
+
import type { RenderResult } from "@testing-library/react";
|
|
39
|
+
import { renderSegments } from "../segment-system.js";
|
|
40
|
+
import {
|
|
41
|
+
createNavigationStore,
|
|
42
|
+
generateHistoryKey,
|
|
43
|
+
} from "../browser/navigation-store.js";
|
|
44
|
+
import { createEventController } from "../browser/event-controller.js";
|
|
45
|
+
import type { NavigationStore, NavigationBridge } from "../browser/types.js";
|
|
46
|
+
import type { EventController } from "../browser/event-controller.js";
|
|
47
|
+
import type { ResolvedSegment, RscMetadata } from "../browser/types.js";
|
|
48
|
+
import { NavigationProvider } from "../browser/react/NavigationProvider.js";
|
|
49
|
+
import { compilePattern } from "../router/pattern-matching.js";
|
|
50
|
+
import { normalizeBasename } from "../router/basename.js";
|
|
51
|
+
import type { LoaderDefinition } from "../types.js";
|
|
52
|
+
import type { LocationStateDefinition } from "../browser/react/location-state-shared.js";
|
|
53
|
+
import type { Handle } from "../handle.js";
|
|
54
|
+
import type { ThemeConfig } from "../theme/types.js";
|
|
55
|
+
import { resolveThemeConfig } from "../theme/constants.js";
|
|
56
|
+
|
|
57
|
+
const TEST_ORIGIN = "http://localhost";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Seed shape for `options.handle`, matching the handle wire format:
|
|
61
|
+
* `{ [handleName]: { [segmentId]: pushedValues[] } }` (each segment accumulates
|
|
62
|
+
* an array of values pushed for that handle).
|
|
63
|
+
*/
|
|
64
|
+
export type HandleDataSeed = Record<string, Record<string, unknown[]>>;
|
|
65
|
+
|
|
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
|
+
const syntheticIds = new WeakMap<object, string>();
|
|
72
|
+
let syntheticIdCounter = 0;
|
|
73
|
+
|
|
74
|
+
function ensureSyntheticId(
|
|
75
|
+
handle: object,
|
|
76
|
+
field: "$$id" | "__rsc_ls_key",
|
|
77
|
+
): string {
|
|
78
|
+
const existing = (handle as Record<string, string>)[field];
|
|
79
|
+
if (existing) return existing;
|
|
80
|
+
let id = syntheticIds.get(handle);
|
|
81
|
+
if (!id) {
|
|
82
|
+
id = `__rango_test_id_${syntheticIdCounter++}`;
|
|
83
|
+
syntheticIds.set(handle, id);
|
|
84
|
+
}
|
|
85
|
+
(handle as Record<string, string>)[field] = id;
|
|
86
|
+
return id;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** One-level clone of a raw handle seed so we don't mutate the caller's object. */
|
|
90
|
+
function cloneHandleSeed(seed?: HandleDataSeed): HandleDataSeed {
|
|
91
|
+
const out: HandleDataSeed = {};
|
|
92
|
+
for (const [name, segMap] of Object.entries(seed ?? {})) {
|
|
93
|
+
out[name] = { ...segMap };
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
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
|
+
export interface RenderRouteSpec {
|
|
105
|
+
/**
|
|
106
|
+
* The route pattern this node matches, e.g. "/products/:productId". The LAST
|
|
107
|
+
* spec in the array is treated as the leaf route; earlier specs are layouts
|
|
108
|
+
* wrapping it. Only the leaf pattern is matched against the `request` URL to
|
|
109
|
+
* extract params; layout patterns are informational.
|
|
110
|
+
*/
|
|
111
|
+
path: string;
|
|
112
|
+
/** The component rendered for this node (the leaf route or a layout body). */
|
|
113
|
+
Component: ComponentType;
|
|
114
|
+
/**
|
|
115
|
+
* Optional layout component. When set on the LEAF spec it wraps the route in
|
|
116
|
+
* its own layout segment (useful for a route that owns a layout). Prefer
|
|
117
|
+
* expressing layouts as their own array entries instead.
|
|
118
|
+
*/
|
|
119
|
+
layout?: ComponentType;
|
|
120
|
+
/**
|
|
121
|
+
* Loader ids ($$id) whose seeded data (from `options.loaderData`) should be
|
|
122
|
+
* attached to THIS node's segment so useLoader/useFetchLoader resolve it from
|
|
123
|
+
* context. When omitted, every key in `options.loaderData` is attached to the
|
|
124
|
+
* leaf route segment.
|
|
125
|
+
*/
|
|
126
|
+
loaderIds?: string[];
|
|
127
|
+
/** Optional route name (informational; not used for matching). */
|
|
128
|
+
name?: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Options for renderRoute.
|
|
133
|
+
*/
|
|
134
|
+
export interface RenderRouteOptions {
|
|
135
|
+
/**
|
|
136
|
+
* The initial location to render at: a `Request`, or a URL string (absolute or
|
|
137
|
+
* path). Only the URL is read (this is a client render — headers/method are
|
|
138
|
+
* ignored); named `request` for parity with the other primitives. Defaults to
|
|
139
|
+
* the leaf spec's static prefix or "/".
|
|
140
|
+
*/
|
|
141
|
+
request?: Request | string;
|
|
142
|
+
/**
|
|
143
|
+
* Loader data to seed into client context, keyed by loader id ($$id). A
|
|
144
|
+
* component calling useLoader(SomeLoader) reads `loaderData[SomeLoader.$$id]`.
|
|
145
|
+
* Seeded values are placed in the route segment's OutletProvider context, so
|
|
146
|
+
* the read path is exercised without executing any loader.
|
|
147
|
+
*/
|
|
148
|
+
loaderData?: Record<string, unknown>;
|
|
149
|
+
/**
|
|
150
|
+
* Loaders to seed by REFERENCE — the robust way to test a component that calls
|
|
151
|
+
* `useLoader(loader)`. A real `createLoader()` handle has an empty `$$id` in a
|
|
152
|
+
* bare test (the id is injected by the Vite plugin at build time), so keying
|
|
153
|
+
* `loaderData` by `$$id` collides under `""` and `useLoader` resolves nothing.
|
|
154
|
+
* Passing `[loader, data]` pairs lets renderRoute assign a synthetic stable id
|
|
155
|
+
* and wire `useLoader` to it. Prefer this over `loaderData` for real handles.
|
|
156
|
+
*
|
|
157
|
+
* NOTE: when a real handle has no `$$id`, renderRoute MUTATES it to assign a
|
|
158
|
+
* synthetic stable id (so repeat renders key consistently). This is a side
|
|
159
|
+
* effect on your input object; a handle reused across tests keeps that id.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* renderRoute([{ path: "/cart", Component: CartBadge }], {
|
|
163
|
+
* loaders: [[CartLoader, { itemCount: 3, total: 89.97 }]],
|
|
164
|
+
* });
|
|
165
|
+
*/
|
|
166
|
+
loaders?: ReadonlyArray<readonly [LoaderDefinition<any>, unknown]>;
|
|
167
|
+
/**
|
|
168
|
+
* Explicit params. Merged over (and overriding) params extracted from the
|
|
169
|
+
* `request` URL. Use this when the URL alone cannot express the params, or to
|
|
170
|
+
* avoid relying on URL parsing.
|
|
171
|
+
*/
|
|
172
|
+
params?: Record<string, string>;
|
|
173
|
+
/**
|
|
174
|
+
* Location-state values to seed by REFERENCE, for components that call
|
|
175
|
+
* `useLocationState(StateDef)`. Like loaders, a real `createLocationState()`
|
|
176
|
+
* handle has an empty injected key in a bare test, so pass `[def, value]`
|
|
177
|
+
* pairs; renderRoute assigns a synthetic key and writes it to `history.state`.
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* renderRoute([{ path: "/", Component: FlashBanner }], {
|
|
181
|
+
* locationState: [[FlashMessage, { text: "Saved" }]],
|
|
182
|
+
* });
|
|
183
|
+
*/
|
|
184
|
+
locationState?: ReadonlyArray<
|
|
185
|
+
readonly [LocationStateDefinition<any, any>, unknown]
|
|
186
|
+
>;
|
|
187
|
+
/**
|
|
188
|
+
* Handles to seed by REFERENCE, for components that read handle output via
|
|
189
|
+
* `useHandle(SomeHandle)` (e.g. a client `Breadcrumbs` trail). Each entry is
|
|
190
|
+
* `[handle, pushedValues[]]` — the values a route's handlers would have pushed;
|
|
191
|
+
* renderRoute attaches them to the leaf route segment under the handle's id.
|
|
192
|
+
* Built-in handles (Breadcrumbs/Meta) have stable ids and work directly.
|
|
193
|
+
*
|
|
194
|
+
* Handle data is accumulated GLOBALLY on the event controller, not scoped per
|
|
195
|
+
* segment like loaders — so ANY component in the chain reads the seeded values,
|
|
196
|
+
* a LAYOUT (e.g. a DetailLayout/ActionToolbar reading a handle) just as much as
|
|
197
|
+
* the leaf route. Most handle usage is server-side (`ctx.use(...)`) and is
|
|
198
|
+
* better covered by `renderToFlightString`/e2e; this seeds the client read path
|
|
199
|
+
* only.
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* renderRoute([{ path: "/p", Component: BreadcrumbTrail }], {
|
|
203
|
+
* handles: [[Breadcrumbs, [{ label: "Home", href: "/" }, { label: "P", href: "/p" }]]],
|
|
204
|
+
* });
|
|
205
|
+
*/
|
|
206
|
+
handles?: ReadonlyArray<readonly [Handle<any, any>, unknown[]]>;
|
|
207
|
+
/**
|
|
208
|
+
* Advanced: raw handle data in wire format
|
|
209
|
+
* `{ [handleId]: { [segmentId]: pushedValues[] } }`. Prefer `handles` (which
|
|
210
|
+
* computes the segment id for you). Merged with `handles`.
|
|
211
|
+
*/
|
|
212
|
+
handle?: HandleDataSeed;
|
|
213
|
+
/**
|
|
214
|
+
* Route name -> pattern map. Informational for parity with the server test
|
|
215
|
+
* context; client useReverse takes its map directly as an argument, so this
|
|
216
|
+
* is not consumed by the client hooks.
|
|
217
|
+
*/
|
|
218
|
+
routeMap?: Record<string, string>;
|
|
219
|
+
/**
|
|
220
|
+
* Router basename (the `createRouter({ basename })` value). Wired into
|
|
221
|
+
* NavigationProvider so `useRouter().basename`, `<Link>` href prefixing, and
|
|
222
|
+
* `useMount`/`useHref` resolve against the mounted prefix instead of the root.
|
|
223
|
+
* Normalized exactly like createRouter (leading slash forced, trailing
|
|
224
|
+
* stripped, bare "/" -> undefined). Defaults to undefined (root mount).
|
|
225
|
+
*/
|
|
226
|
+
basename?: string;
|
|
227
|
+
/**
|
|
228
|
+
* include() mount prefix, to model an `include("/shop", ...)` subtree so a
|
|
229
|
+
* component (route OR layout in the chain) calling `useMount()` returns the
|
|
230
|
+
* mounted prefix instead of "/". Wraps the segment chain in a MountContext
|
|
231
|
+
* exactly as `renderSegments` does in production (a segment whose `mountPath`
|
|
232
|
+
* is set is wrapped in a MountContextProvider). Normalized like a path prefix
|
|
233
|
+
* (leading slash forced, trailing stripped, bare "/" -> root). Defaults to "/".
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* renderRoute([{ path: "/c/wine", Component: ProductPage }], { mount: "/shop" });
|
|
237
|
+
* // useMount() inside ProductPage returns "/shop"
|
|
238
|
+
*/
|
|
239
|
+
mount?: string;
|
|
240
|
+
/**
|
|
241
|
+
* Theme config in the `createRouter({ theme })` shape (resolved internally) to
|
|
242
|
+
* wrap the tree in a ThemeProvider. Defaults to no provider. Note: a component
|
|
243
|
+
* that calls `useTheme()` REQUIRES a provider — it throws "used outside
|
|
244
|
+
* ThemeProvider" without one — so pass a config (e.g. `true`) to test such a
|
|
245
|
+
* component.
|
|
246
|
+
*/
|
|
247
|
+
theme?: ThemeConfig | true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Imperative handle returned alongside the RTL result.
|
|
252
|
+
*/
|
|
253
|
+
export interface TestRouterHandle {
|
|
254
|
+
/**
|
|
255
|
+
* Navigate to a new URL. Re-resolves the URL against the supplied `routes`,
|
|
256
|
+
* updates params + location, and re-renders the segment tree. This is a
|
|
257
|
+
* client-only navigation: no server fetch occurs, so only the components in
|
|
258
|
+
* `routes` can be reached.
|
|
259
|
+
*/
|
|
260
|
+
navigate(url: string): Promise<void>;
|
|
261
|
+
/** The current committed pathname. */
|
|
262
|
+
pathname(): string;
|
|
263
|
+
/** The current committed params. */
|
|
264
|
+
params(): Record<string, string>;
|
|
265
|
+
/** The underlying navigation store (advanced use). */
|
|
266
|
+
store: NavigationStore;
|
|
267
|
+
/** The underlying event controller (advanced use). */
|
|
268
|
+
eventController: EventController;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Result of renderRoute: RTL's render result plus the router handle. */
|
|
272
|
+
export type RenderRouteResult = RenderResult & { router: TestRouterHandle };
|
|
273
|
+
|
|
274
|
+
interface ResolvedMatch {
|
|
275
|
+
params: Record<string, string>;
|
|
276
|
+
pathname: string;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Match a pathname against the leaf spec's pattern and extract params.
|
|
281
|
+
* Returns null when the pattern does not match (params then fall back to the
|
|
282
|
+
* caller-provided `options.params`).
|
|
283
|
+
*/
|
|
284
|
+
function matchLeaf(
|
|
285
|
+
pattern: string,
|
|
286
|
+
pathname: string,
|
|
287
|
+
): Record<string, string> | null {
|
|
288
|
+
const compiled = compilePattern(pattern);
|
|
289
|
+
const match = compiled.regex.exec(pathname);
|
|
290
|
+
if (!match) return null;
|
|
291
|
+
const params: Record<string, string> = {};
|
|
292
|
+
compiled.paramNames.forEach((name, index) => {
|
|
293
|
+
const value = match[index + 1];
|
|
294
|
+
if (value !== undefined) {
|
|
295
|
+
params[name] = decodeURIComponent(value);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
return params;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Derive a usable initial pathname from a leaf pattern when none is given. */
|
|
302
|
+
function staticPrefix(pattern: string): string {
|
|
303
|
+
const out: string[] = [];
|
|
304
|
+
for (const part of pattern.split("/")) {
|
|
305
|
+
if (part === "") continue;
|
|
306
|
+
if (part.startsWith(":") || part === "*") break;
|
|
307
|
+
out.push(part);
|
|
308
|
+
}
|
|
309
|
+
return "/" + out.join("/");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Build the synthetic ResolvedSegment[] for a matched route. Produces, in
|
|
314
|
+
* root-to-leaf order: one layout segment per non-leaf spec, then the leaf route
|
|
315
|
+
* segment, plus a loader segment for each seeded loader id attached to the
|
|
316
|
+
* owning spec. Segment ids follow the real convention (L0, L0L1, ..., the leaf
|
|
317
|
+
* route as L0...R{n}; loaders as {parentId}D{i}.{loaderId}).
|
|
318
|
+
*/
|
|
319
|
+
function buildSegments(
|
|
320
|
+
routes: RenderRouteSpec[],
|
|
321
|
+
params: Record<string, string>,
|
|
322
|
+
loaderData: Record<string, unknown>,
|
|
323
|
+
mount?: string,
|
|
324
|
+
): ResolvedSegment[] {
|
|
325
|
+
const segments: ResolvedSegment[] = [];
|
|
326
|
+
const leafIndex = routes.length - 1;
|
|
327
|
+
let idPath = "";
|
|
328
|
+
|
|
329
|
+
const seededIds = Object.keys(loaderData);
|
|
330
|
+
const explicitlyOwned = new Set<string>();
|
|
331
|
+
for (const spec of routes) {
|
|
332
|
+
for (const id of spec.loaderIds ?? []) explicitlyOwned.add(id);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
routes.forEach((spec, i) => {
|
|
336
|
+
const isLeaf = i === leafIndex;
|
|
337
|
+
const tag = isLeaf ? `R${i}` : `L${i}`;
|
|
338
|
+
idPath = idPath + tag;
|
|
339
|
+
const segmentId = idPath;
|
|
340
|
+
|
|
341
|
+
const Component = spec.Component;
|
|
342
|
+
const node: ResolvedSegment = {
|
|
343
|
+
id: segmentId,
|
|
344
|
+
namespace: "",
|
|
345
|
+
type: isLeaf ? "route" : "layout",
|
|
346
|
+
index: i,
|
|
347
|
+
component: <Component />,
|
|
348
|
+
params,
|
|
349
|
+
belongsToRoute: true,
|
|
350
|
+
};
|
|
351
|
+
// Model an include() mount: every component segment in the chain shares the
|
|
352
|
+
// same prefix, so renderSegments wraps each in a MountContextProvider and
|
|
353
|
+
// useMount() resolves the mounted prefix (production sets mountPath on every
|
|
354
|
+
// segment of an included subtree). Must be applied identically at both
|
|
355
|
+
// buildSegments call sites or segment-structure-assert flags a remount.
|
|
356
|
+
if (mount) node.mountPath = mount;
|
|
357
|
+
// A leaf-owned layout component wraps the route via its own layout element.
|
|
358
|
+
if (isLeaf && spec.layout) {
|
|
359
|
+
const Layout = spec.layout;
|
|
360
|
+
node.layout = <Layout />;
|
|
361
|
+
}
|
|
362
|
+
segments.push(node);
|
|
363
|
+
|
|
364
|
+
// Determine which seeded loader ids this spec owns.
|
|
365
|
+
const ownedIds = spec.loaderIds
|
|
366
|
+
? spec.loaderIds.filter((id) => id in loaderData)
|
|
367
|
+
: isLeaf
|
|
368
|
+
? seededIds.filter((id) => !explicitlyOwned.has(id))
|
|
369
|
+
: [];
|
|
370
|
+
|
|
371
|
+
ownedIds.forEach((loaderId, li) => {
|
|
372
|
+
segments.push({
|
|
373
|
+
id: `${segmentId}D${li}.${loaderId}`,
|
|
374
|
+
namespace: "",
|
|
375
|
+
type: "loader",
|
|
376
|
+
index: li,
|
|
377
|
+
component: null,
|
|
378
|
+
loaderId,
|
|
379
|
+
loaderData: loaderData[loaderId],
|
|
380
|
+
params,
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
return segments;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Render a CLIENT component (and its layout chain) inside the router's
|
|
390
|
+
* NavigationProvider for unit testing. Exported from `@rangojs/router/testing/dom`
|
|
391
|
+
* (its own entry, kept out of the main `@rangojs/router/testing` barrel so that
|
|
392
|
+
* barrel never references React/@testing-library/react). Async so the heavy
|
|
393
|
+
* @testing-library/react dependency is loaded only at call time.
|
|
394
|
+
*
|
|
395
|
+
* @example
|
|
396
|
+
* ```tsx
|
|
397
|
+
* // @vitest-environment happy-dom
|
|
398
|
+
* import { renderRoute } from "@rangojs/router/testing/dom";
|
|
399
|
+
*
|
|
400
|
+
* function Product() {
|
|
401
|
+
* const { productId } = useParams<{ productId: string }>();
|
|
402
|
+
* const reverse = useReverse({ product: "/products/:productId" });
|
|
403
|
+
* return <a href={reverse("product", { productId: "2" })}>{productId}</a>;
|
|
404
|
+
* }
|
|
405
|
+
*
|
|
406
|
+
* const { getByText, router } = await renderRoute(
|
|
407
|
+
* [{ path: "/products/:productId", Component: Product }],
|
|
408
|
+
* { request: "/products/1" },
|
|
409
|
+
* );
|
|
410
|
+
* ```
|
|
411
|
+
*/
|
|
412
|
+
export async function renderRoute(
|
|
413
|
+
routes: RenderRouteSpec[],
|
|
414
|
+
options: RenderRouteOptions = {},
|
|
415
|
+
): Promise<RenderRouteResult> {
|
|
416
|
+
if (routes.length === 0) {
|
|
417
|
+
throw new Error("renderRoute: `routes` must contain at least one entry");
|
|
418
|
+
}
|
|
419
|
+
// The pre-rename `initialUrl` option was renamed to `request`. A plain-JS or
|
|
420
|
+
// spread-defeated caller still passing it would otherwise be silently ignored;
|
|
421
|
+
// fail loud with the migration name instead.
|
|
422
|
+
if ("initialUrl" in options) {
|
|
423
|
+
throw new Error(
|
|
424
|
+
"renderRoute: the `initialUrl` option was renamed to `request`. " +
|
|
425
|
+
"Pass { request: <Request | url> } instead.",
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const { render, act } = await import("@testing-library/react");
|
|
430
|
+
|
|
431
|
+
const leaf = routes[routes.length - 1];
|
|
432
|
+
const requestUrl =
|
|
433
|
+
options.request instanceof Request ? options.request.url : options.request;
|
|
434
|
+
const initialUrl = requestUrl ?? staticPrefix(leaf.path) ?? "/";
|
|
435
|
+
const url = new URL(initialUrl, TEST_ORIGIN);
|
|
436
|
+
|
|
437
|
+
// Seed loader data: explicit-id entries from `loaderData`, plus by-reference
|
|
438
|
+
// entries from `loaders` (assigning synthetic ids to real handles whose `$$id`
|
|
439
|
+
// is empty in a bare test).
|
|
440
|
+
const loaderData: Record<string, unknown> = { ...(options.loaderData ?? {}) };
|
|
441
|
+
for (const [loader, data] of options.loaders ?? []) {
|
|
442
|
+
loaderData[ensureSyntheticId(loader as object, "$$id")] = data;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Seed location state into history.state so useLocationState(def) resolves.
|
|
446
|
+
// Keyed defs read history.state[def.__rsc_ls_key]; assign a synthetic key when
|
|
447
|
+
// the injected one is empty (bare test). RESET history.state to only this
|
|
448
|
+
// call's seeds (not a merge) so a previous render's seeded state does not leak
|
|
449
|
+
// into a later render in the same DOM environment.
|
|
450
|
+
if (typeof window !== "undefined") {
|
|
451
|
+
const stateObj: Record<string, unknown> = {};
|
|
452
|
+
for (const [def, value] of options.locationState ?? []) {
|
|
453
|
+
stateObj[ensureSyntheticId(def as object, "__rsc_ls_key")] = value;
|
|
454
|
+
}
|
|
455
|
+
// No URL arg: useLocationState reads history.state (not the URL), and passing
|
|
456
|
+
// a TEST_ORIGIN URL would trip the DOM env's same-origin check.
|
|
457
|
+
window.history.replaceState(stateObj, "");
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Resolve params: URL-extracted params first, explicit params override.
|
|
461
|
+
const resolve = (pathname: string): ResolvedMatch => {
|
|
462
|
+
const matched = matchLeaf(leaf.path, pathname) ?? {};
|
|
463
|
+
return {
|
|
464
|
+
params: { ...matched, ...(options.params ?? {}) },
|
|
465
|
+
pathname,
|
|
466
|
+
};
|
|
467
|
+
};
|
|
468
|
+
const initialMatch = resolve(url.pathname);
|
|
469
|
+
|
|
470
|
+
// Reuse the real browser primitives so context shape matches production.
|
|
471
|
+
const historyKey = generateHistoryKey(url.href);
|
|
472
|
+
// Normalize the include() mount prefix once and apply it at BOTH buildSegments
|
|
473
|
+
// call sites (initial + navigate) so mountPath is consistent across renders.
|
|
474
|
+
const mount = normalizeBasename(options.mount);
|
|
475
|
+
const initialSegments = buildSegments(
|
|
476
|
+
routes,
|
|
477
|
+
initialMatch.params,
|
|
478
|
+
loaderData,
|
|
479
|
+
mount,
|
|
480
|
+
);
|
|
481
|
+
const store = createNavigationStore({
|
|
482
|
+
initialLocation: { href: url.href },
|
|
483
|
+
initialSegmentIds: initialSegments.map((s) => s.id),
|
|
484
|
+
initialHistoryKey: historyKey,
|
|
485
|
+
initialSegments,
|
|
486
|
+
crossTabSync: false,
|
|
487
|
+
});
|
|
488
|
+
// Seed handle data: raw `handle` entries plus by-reference `handles` attached
|
|
489
|
+
// to the leaf route segment under each handle's id (so useHandle(handle)
|
|
490
|
+
// resolves the pushed values).
|
|
491
|
+
const leafRouteSegmentId =
|
|
492
|
+
[...initialSegments].reverse().find((s) => s.type === "route")?.id ??
|
|
493
|
+
initialSegments[initialSegments.length - 1]?.id;
|
|
494
|
+
const handleSeed: HandleDataSeed = cloneHandleSeed(options.handle);
|
|
495
|
+
for (const [handle, values] of options.handles ?? []) {
|
|
496
|
+
if (leafRouteSegmentId === undefined) continue;
|
|
497
|
+
// createHandle always has a non-empty $$id (the Vite plugin injects one, and
|
|
498
|
+
// createHandle assigns a runtime fallback otherwise) with its REAL collect
|
|
499
|
+
// registered — so seeding under handle.$$id makes useHandle(handle) run the
|
|
500
|
+
// handle's actual collect/accumulator (custom collects included), not just a
|
|
501
|
+
// default flatten.
|
|
502
|
+
const id = (handle as unknown as { $$id: string }).$$id;
|
|
503
|
+
(handleSeed[id] ??= {})[leafRouteSegmentId] = values;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const eventController = createEventController({ initialLocation: url });
|
|
507
|
+
eventController.setParams(initialMatch.params);
|
|
508
|
+
eventController.setHandleData(
|
|
509
|
+
handleSeed,
|
|
510
|
+
initialSegments.map((s) => s.id),
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
// Client-only navigation: re-resolve against the in-memory routes and emit a
|
|
514
|
+
// re-render. No server fetch — only routes passed to renderRoute exist. The
|
|
515
|
+
// store update is flushed inside act() so React commits before callers
|
|
516
|
+
// assert, mirroring how a real navigation lands a single payload swap.
|
|
517
|
+
// NOTE: the seeded `loaderData` is reused for the target route too (no
|
|
518
|
+
// per-route loader fetch in a unit test), so every seeded loader stays
|
|
519
|
+
// available after navigate() — unlike a real navigation, which would fetch
|
|
520
|
+
// the target route's own loaders. This is a deliberate test-isolation design.
|
|
521
|
+
const navigate = async (target: string): Promise<void> => {
|
|
522
|
+
const nextUrl = new URL(target, TEST_ORIGIN);
|
|
523
|
+
const match = resolve(nextUrl.pathname);
|
|
524
|
+
const segments = buildSegments(routes, match.params, loaderData, mount);
|
|
525
|
+
const metadata = makeMetadata(nextUrl.pathname, segments, match.params);
|
|
526
|
+
const root = await renderSegments(segments);
|
|
527
|
+
eventController.setLocation(nextUrl);
|
|
528
|
+
eventController.setParams(match.params);
|
|
529
|
+
store.setCurrentUrl(nextUrl.href);
|
|
530
|
+
store.setSegmentIds(segments.map((s) => s.id));
|
|
531
|
+
await act(async () => {
|
|
532
|
+
store.emitUpdate({ root, metadata });
|
|
533
|
+
});
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const bridge: NavigationBridge = {
|
|
537
|
+
navigate: (target) => navigate(target),
|
|
538
|
+
refresh: () => navigate(url.pathname + url.search),
|
|
539
|
+
handlePopstate: async () => {},
|
|
540
|
+
registerLinkInterception: () => () => {},
|
|
541
|
+
getVersion: () => undefined,
|
|
542
|
+
updateVersion: () => {},
|
|
543
|
+
updateAppShell: () => {},
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const initialMetadata = makeMetadata(
|
|
547
|
+
url.pathname,
|
|
548
|
+
initialSegments,
|
|
549
|
+
initialMatch.params,
|
|
550
|
+
);
|
|
551
|
+
const initialTree = await renderSegments(initialSegments);
|
|
552
|
+
|
|
553
|
+
const result = render(
|
|
554
|
+
<NavigationProvider
|
|
555
|
+
store={store}
|
|
556
|
+
eventController={eventController}
|
|
557
|
+
initialPayload={{ root: initialTree, metadata: initialMetadata }}
|
|
558
|
+
bridge={bridge}
|
|
559
|
+
basename={normalizeBasename(options.basename)}
|
|
560
|
+
themeConfig={
|
|
561
|
+
options.theme === undefined ? null : resolveThemeConfig(options.theme)
|
|
562
|
+
}
|
|
563
|
+
/>,
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
const router: TestRouterHandle = {
|
|
567
|
+
navigate,
|
|
568
|
+
pathname: () => new URL(eventController.getLocation().href).pathname,
|
|
569
|
+
params: () => eventController.getParams(),
|
|
570
|
+
store,
|
|
571
|
+
eventController,
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
return Object.assign(result, { router });
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** Minimal RscMetadata for client-side re-renders (no server-only fields). */
|
|
578
|
+
function makeMetadata(
|
|
579
|
+
pathname: string,
|
|
580
|
+
segments: ResolvedSegment[],
|
|
581
|
+
params: Record<string, string>,
|
|
582
|
+
): RscMetadata {
|
|
583
|
+
return {
|
|
584
|
+
pathname,
|
|
585
|
+
segments,
|
|
586
|
+
params,
|
|
587
|
+
matched: segments.map((s) => s.id),
|
|
588
|
+
isPartial: false,
|
|
589
|
+
};
|
|
590
|
+
}
|