@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,581 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dispatch — in-process request -> Response for unit/integration tests,
|
|
3
|
+
* WITHOUT the Flight RSC runtime.
|
|
4
|
+
*
|
|
5
|
+
* dispatch runs the router's real matching and middleware execution so that
|
|
6
|
+
* redirects, 404s, response routes (path.json / path.text / path.html / ...),
|
|
7
|
+
* and middleware short-circuits behave exactly as in production. It deliberately
|
|
8
|
+
* does NOT render React Server Components: there is no Flight stream, no SSR,
|
|
9
|
+
* and no DOM. Hit an RSC (component) route and dispatch throws a clear error
|
|
10
|
+
* directing you to renderToFlightString/renderServerTree/renderHandler or an e2e test.
|
|
11
|
+
*
|
|
12
|
+
* What dispatch DOES support:
|
|
13
|
+
* - Trailing-slash and other findMatch() redirects -> 308 with Location
|
|
14
|
+
* - Unmatched paths -> 404 Response
|
|
15
|
+
* Both the 308 and the 404 are produced INSIDE the global middleware chain
|
|
16
|
+
* (mirroring production, where coreHandler runs wrapped by executeMiddleware),
|
|
17
|
+
* so a global auth middleware can 401/redirect them and middleware-set
|
|
18
|
+
* cookies/headers merge onto the 308/404 the way createResponseWithMergedHeaders
|
|
19
|
+
* merges them in production.
|
|
20
|
+
* - Response routes (non-RSC) -> serialized Response
|
|
21
|
+
* - json: JSON.stringify(result) (bare value) with application/json
|
|
22
|
+
* - text/html/xml/md: String(result) with the mapped MIME type
|
|
23
|
+
* - handler returning a Response: re-wrapped like
|
|
24
|
+
* handleResponseRoute (stub headers/cookies merged, Set-Cookie preserved,
|
|
25
|
+
* WebSocket upgrade passed through without reconstruction)
|
|
26
|
+
* - handler throwing an error: typed 500 / RouterError
|
|
27
|
+
* status, matching handleResponseRoute (RFC 9457 problem+json body with
|
|
28
|
+
* application/problem+json for json routes, text/plain message otherwise)
|
|
29
|
+
* - content-negotiated route: Vary: Accept appended
|
|
30
|
+
* - Global middleware (router.use(...)) AND route-level middleware, with full
|
|
31
|
+
* next()/short-circuit/throw-Response/header+cookie-merge fidelity.
|
|
32
|
+
* - Partial (client-navigation) requests to a RESPONSE route (?_rsc_partial):
|
|
33
|
+
* global middleware runs first (so an auth gate can still 401/redirect),
|
|
34
|
+
* then — if it passes through — an X-RSC-Reload is returned. Route-level
|
|
35
|
+
* middleware is skipped on a partial, exactly as production skips it.
|
|
36
|
+
* - A middleware redirect (3xx + Location) on a partial/action request
|
|
37
|
+
* (?_rsc_partial / ?_rsc_action): converted to a 204 + X-RSC-Redirect via the
|
|
38
|
+
* real interceptRedirectForPartial, so fetch() does not auto-follow the 3xx —
|
|
39
|
+
* identical to production's no-location-state path.
|
|
40
|
+
*
|
|
41
|
+
* What dispatch DOES NOT support (and why):
|
|
42
|
+
* - RSC component routes — rendering requires the Flight serializer + React
|
|
43
|
+
* server runtime, which is the boundary this primitive is defined to avoid.
|
|
44
|
+
* This includes partial requests that resolve to a component route.
|
|
45
|
+
* - Server actions (?_rsc_action) — RSC protocol concerns handled by
|
|
46
|
+
* router.fetch().
|
|
47
|
+
* - ctx.onError() callbacks on a thrown response-route handler error: the
|
|
48
|
+
* error is serialized into the same typed 500 / RouterError Response as
|
|
49
|
+
* production, but registered onError handlers are NOT invoked here. Cover
|
|
50
|
+
* onError side effects with an e2e test.
|
|
51
|
+
* - Location-state-carrying redirects on a partial/action request: production
|
|
52
|
+
* embeds a Flight payload (createRedirectFlightResponse) so the client can
|
|
53
|
+
* restore location state across the redirect. dispatch is RSC-free, so it
|
|
54
|
+
* cannot emit that Flight stream. It falls back to the no-state behavior — a
|
|
55
|
+
* 204 + X-RSC-Redirect via createSimpleRedirectResponse — dropping the
|
|
56
|
+
* embedded location state. The 204 status, the X-RSC-Redirect header, and the
|
|
57
|
+
* merged cookies/headers all match production; only the Flight-embedded
|
|
58
|
+
* location-state entries are absent. Cover location-state restoration across a
|
|
59
|
+
* partial redirect with an e2e test.
|
|
60
|
+
*
|
|
61
|
+
* dispatch reuses router.previewMatch(), which itself runs content negotiation
|
|
62
|
+
* and resolves route middleware from the matched entry tree, so dispatch's
|
|
63
|
+
* route-middleware collection is exactly the router's, not a re-implementation.
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
import {
|
|
67
|
+
createRequestContext,
|
|
68
|
+
runWithRequestContext,
|
|
69
|
+
setRequestContextParams,
|
|
70
|
+
} from "../server/request-context.js";
|
|
71
|
+
import { executeMiddleware, matchMiddleware } from "../router/middleware.js";
|
|
72
|
+
import type {
|
|
73
|
+
MiddlewareEntry,
|
|
74
|
+
MiddlewareFn,
|
|
75
|
+
} from "../router/middleware-types.js";
|
|
76
|
+
import {
|
|
77
|
+
createReverseFunction,
|
|
78
|
+
stripInternalParams,
|
|
79
|
+
} from "../router/handler-context.js";
|
|
80
|
+
import { NOCACHE_SYMBOL } from "../cache/taint.js";
|
|
81
|
+
import type { SegmentCacheStore } from "../cache/types.js";
|
|
82
|
+
import type { CacheProfile } from "../cache/profile-registry.js";
|
|
83
|
+
import { setRouterManifest } from "../route-map-builder.js";
|
|
84
|
+
import { RESPONSE_TYPE_MIME } from "../router/content-negotiation.js";
|
|
85
|
+
import { RouterError } from "../errors.js";
|
|
86
|
+
import { createProblemDetails } from "../rsc/response-error.js";
|
|
87
|
+
import {
|
|
88
|
+
createResponseWithMergedHeaders,
|
|
89
|
+
createSimpleRedirectResponse,
|
|
90
|
+
finalizeResponse,
|
|
91
|
+
interceptRedirectForPartial,
|
|
92
|
+
mergeStubHeadersAndFinalize,
|
|
93
|
+
} from "../rsc/helpers.js";
|
|
94
|
+
import { isWebSocketUpgradeResponse } from "../response-utils.js";
|
|
95
|
+
import type { Rango } from "../router/router-interfaces.js";
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The internal subset of the router surface dispatch depends on. The public
|
|
99
|
+
* `Rango` router carries these members at runtime (they are declared on the
|
|
100
|
+
* internal interface), so dispatch accepts a public `Rango` and reads them
|
|
101
|
+
* through this shape — the consumer never needs a cast.
|
|
102
|
+
*/
|
|
103
|
+
interface DispatchableRouter<TEnv> {
|
|
104
|
+
id?: string;
|
|
105
|
+
routerId?: string;
|
|
106
|
+
routeMap: Record<string, unknown>;
|
|
107
|
+
middleware: MiddlewareEntry<TEnv>[];
|
|
108
|
+
findMatch(pathname: string): {
|
|
109
|
+
redirectTo?: string;
|
|
110
|
+
routeKey?: string;
|
|
111
|
+
params?: Record<string, string>;
|
|
112
|
+
} | null;
|
|
113
|
+
previewMatch(
|
|
114
|
+
request: Request,
|
|
115
|
+
input?: { env?: TEnv },
|
|
116
|
+
): Promise<{
|
|
117
|
+
routeMiddleware?: Array<{
|
|
118
|
+
handler: MiddlewareFn<TEnv>;
|
|
119
|
+
params: Record<string, string>;
|
|
120
|
+
}>;
|
|
121
|
+
responseType?: string;
|
|
122
|
+
handler?: Function;
|
|
123
|
+
params?: Record<string, string>;
|
|
124
|
+
routeKey?: string;
|
|
125
|
+
negotiated?: boolean;
|
|
126
|
+
} | null>;
|
|
127
|
+
basename?: string;
|
|
128
|
+
cache?:
|
|
129
|
+
| { enabled?: boolean; store?: SegmentCacheStore }
|
|
130
|
+
| ((
|
|
131
|
+
env: TEnv,
|
|
132
|
+
executionContext: unknown,
|
|
133
|
+
) => { enabled?: boolean; store?: SegmentCacheStore });
|
|
134
|
+
cacheProfiles?: Record<string, CacheProfile>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Options for dispatch.
|
|
139
|
+
*/
|
|
140
|
+
export interface DispatchOptions<TEnv = any> {
|
|
141
|
+
/** The request to dispatch: a `Request`, or a URL string (absolute or path). */
|
|
142
|
+
request: Request | string;
|
|
143
|
+
/** Environment bindings forwarded to matching and middleware. */
|
|
144
|
+
env?: TEnv;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const DEFAULT_ORIGIN = "http://localhost/";
|
|
148
|
+
|
|
149
|
+
function toRequest(request: Request | string): Request {
|
|
150
|
+
if (request instanceof Request) return request;
|
|
151
|
+
return new Request(new URL(request, DEFAULT_ORIGIN));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Serialize a NON-Response response-route handler result, mirroring the
|
|
156
|
+
* router's handleResponseRoute() contract:
|
|
157
|
+
* - "json" serializes the value verbatim (bare) with application/json,
|
|
158
|
+
* - text/html/xml/md stringify with the mapped MIME type.
|
|
159
|
+
*
|
|
160
|
+
* A handler-returned Response is NOT routed here — callHandler re-wraps it via
|
|
161
|
+
* rewrapHandlerResponse (mirroring handleResponseRoute's rewrapResponse) so the
|
|
162
|
+
* WebSocket-upgrade bypass and Set-Cookie-preserving header merge match
|
|
163
|
+
* production.
|
|
164
|
+
*/
|
|
165
|
+
function serializeResponseRouteResult(
|
|
166
|
+
result: unknown,
|
|
167
|
+
responseType: string,
|
|
168
|
+
): Response {
|
|
169
|
+
if (responseType === "json") {
|
|
170
|
+
return new Response(JSON.stringify(result), {
|
|
171
|
+
status: 200,
|
|
172
|
+
headers: { "content-type": "application/json;charset=utf-8" },
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (Object.hasOwn(RESPONSE_TYPE_MIME, responseType)) {
|
|
177
|
+
return new Response(String(result), {
|
|
178
|
+
status: 200,
|
|
179
|
+
headers: {
|
|
180
|
+
"content-type": `${RESPONSE_TYPE_MIME[responseType]};charset=utf-8`,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
throw new Error(
|
|
186
|
+
`dispatch(): response route handler for "${responseType}" must return a ` +
|
|
187
|
+
`Response object, got ${typeof result}. Binary/streaming response types ` +
|
|
188
|
+
`(image, stream, any) must return a Response explicitly.`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Serialize a thrown handler error into the same typed Response the router's
|
|
194
|
+
* handleResponseRoute() catch block produces:
|
|
195
|
+
* - "json" routes return an RFC 9457 problem+json body (application/problem+json),
|
|
196
|
+
* - all other types return a text/plain body (the RouterError message verbatim,
|
|
197
|
+
* the Error message in dev, else "Internal Server Error").
|
|
198
|
+
*
|
|
199
|
+
* `status` is the effective HTTP status resolved by the caller (RouterError.status
|
|
200
|
+
* or 500, overridden by ctx.setStatus()); it governs both the HTTP status and the
|
|
201
|
+
* problem body's `status`/`title` members. Reuses the production
|
|
202
|
+
* createProblemDetails so the error body is byte-identical rather than re-derived.
|
|
203
|
+
*/
|
|
204
|
+
function serializeResponseRouteError(
|
|
205
|
+
error: unknown,
|
|
206
|
+
responseType: string,
|
|
207
|
+
status: number,
|
|
208
|
+
): Response {
|
|
209
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
210
|
+
|
|
211
|
+
if (responseType === "json") {
|
|
212
|
+
return new Response(
|
|
213
|
+
JSON.stringify(createProblemDetails(error, status, isDev)),
|
|
214
|
+
{
|
|
215
|
+
status,
|
|
216
|
+
headers: { "content-type": "application/problem+json;charset=utf-8" },
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const message =
|
|
222
|
+
error instanceof RouterError
|
|
223
|
+
? error.message
|
|
224
|
+
: isDev && error instanceof Error
|
|
225
|
+
? error.message
|
|
226
|
+
: "Internal Server Error";
|
|
227
|
+
return new Response(message, {
|
|
228
|
+
status,
|
|
229
|
+
headers: { "content-type": "text/plain;charset=utf-8" },
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Re-wrap a handler-returned Response, byte-identical to handleResponseRoute's
|
|
235
|
+
* rewrapResponse:
|
|
236
|
+
* - A WebSocket upgrade (status 101 or a `webSocket` property) is returned via
|
|
237
|
+
* mergeStubHeadersAndFinalize WITHOUT reconstruction — the Response
|
|
238
|
+
* constructor rejects status 101, and an upgrade response's headers/socket
|
|
239
|
+
* must not be rebuilt.
|
|
240
|
+
* - Otherwise headers are copied into a fresh Headers (Set-Cookie appended to
|
|
241
|
+
* preserve duplicates, others set) and the Response is rebuilt through
|
|
242
|
+
* createResponseWithMergedHeaders so stub headers/cookies, the ctx.setStatus
|
|
243
|
+
* override, and onResponse callbacks merge exactly as in production. statusText
|
|
244
|
+
* is intentionally dropped (production does not carry it across the re-wrap).
|
|
245
|
+
*
|
|
246
|
+
* Must run inside runWithRequestContext (reads the ambient request context via
|
|
247
|
+
* the helpers), which callHandler guarantees.
|
|
248
|
+
*/
|
|
249
|
+
function rewrapHandlerResponse(result: Response): Response {
|
|
250
|
+
if (isWebSocketUpgradeResponse(result)) {
|
|
251
|
+
return mergeStubHeadersAndFinalize(result);
|
|
252
|
+
}
|
|
253
|
+
const headers = new Headers();
|
|
254
|
+
result.headers.forEach((value, key) => {
|
|
255
|
+
if (key.toLowerCase() === "set-cookie") {
|
|
256
|
+
headers.append(key, value);
|
|
257
|
+
} else {
|
|
258
|
+
headers.set(key, value);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
return createResponseWithMergedHeaders(result.body, {
|
|
262
|
+
status: result.status,
|
|
263
|
+
headers,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Run a request through the router in-process and return the Response.
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```ts
|
|
272
|
+
* const router = createRouter<Env>({}).routes(urls(({ path }) => [
|
|
273
|
+
* path.json("/api/health", () => ({ ok: true }), { name: "health" }),
|
|
274
|
+
* ]));
|
|
275
|
+
*
|
|
276
|
+
* const res = await dispatch(router, { request: "/api/health" });
|
|
277
|
+
* expect(res.status).toBe(200);
|
|
278
|
+
* expect(await res.json()).toEqual({ ok: true });
|
|
279
|
+
* ```
|
|
280
|
+
*/
|
|
281
|
+
export async function dispatch<TEnv = any>(
|
|
282
|
+
publicRouter: Rango<TEnv, any>,
|
|
283
|
+
opts: DispatchOptions<TEnv>,
|
|
284
|
+
): Promise<Response> {
|
|
285
|
+
// The public Rango type intentionally hides the matching internals; read them
|
|
286
|
+
// through the dispatchable shape (present at runtime). Consumers pass their
|
|
287
|
+
// real router with no cast.
|
|
288
|
+
const router = publicRouter as unknown as DispatchableRouter<TEnv>;
|
|
289
|
+
const req = toRequest(opts.request);
|
|
290
|
+
const url = new URL(req.url);
|
|
291
|
+
const env = (opts.env ?? {}) as TEnv;
|
|
292
|
+
|
|
293
|
+
// Seed the per-router manifest so reverse() resolves during handler execution.
|
|
294
|
+
const routerId = router.id ?? router.routerId;
|
|
295
|
+
if (routerId) {
|
|
296
|
+
setRouterManifest(routerId, router.routeMap as Record<string, string>);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// findMatch carries trailing-slash/redirect targets and null on no match.
|
|
300
|
+
// previewMatch swallows redirects, so detect them here first.
|
|
301
|
+
const match = router.findMatch(url.pathname);
|
|
302
|
+
const redirectTo = match?.redirectTo;
|
|
303
|
+
const isUnmatched = !match;
|
|
304
|
+
|
|
305
|
+
// previewMatch resolves responseType, the response-route handler, and the
|
|
306
|
+
// route middleware from the matched entry tree (with content negotiation).
|
|
307
|
+
// Skip it for a redirect/unmatched path — there is no response route to
|
|
308
|
+
// resolve, and previewMatch would return null / a redirect marker anyway.
|
|
309
|
+
const preview =
|
|
310
|
+
redirectTo || isUnmatched ? null : await router.previewMatch(req, { env });
|
|
311
|
+
|
|
312
|
+
// A bare match with no responseType is an RSC route. The RSC-route throw is a
|
|
313
|
+
// hard boundary of this primitive (no Flight runtime), distinct from the
|
|
314
|
+
// 308/404 outcomes below, so it stays a pre-middleware guard.
|
|
315
|
+
const responseType = preview?.responseType;
|
|
316
|
+
const handler = preview?.handler;
|
|
317
|
+
const params = preview?.params ?? match?.params ?? {};
|
|
318
|
+
const routeKey = preview?.routeKey ?? match?.routeKey;
|
|
319
|
+
|
|
320
|
+
if (
|
|
321
|
+
!redirectTo &&
|
|
322
|
+
!isUnmatched &&
|
|
323
|
+
(!responseType || typeof handler !== "function")
|
|
324
|
+
) {
|
|
325
|
+
throw new Error(
|
|
326
|
+
`dispatch() does not render RSC routes — the route matched at ` +
|
|
327
|
+
`"${url.pathname}" is a React Server Component route, not a response ` +
|
|
328
|
+
`route. Use renderHandler/renderServerTree/renderToFlightString or an ` +
|
|
329
|
+
`e2e test to exercise component rendering.`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const variables: Record<string, unknown> = {};
|
|
334
|
+
|
|
335
|
+
// Resolve the router's cache store the way the production handler does, so a
|
|
336
|
+
// "use cache" inside a response-route handler reaches the request-scope
|
|
337
|
+
// (NOCACHE) detection below instead of bypassing on a missing store.
|
|
338
|
+
let cacheStore: SegmentCacheStore | undefined;
|
|
339
|
+
const cacheOption = router.cache;
|
|
340
|
+
if (cacheOption && !url.searchParams.has("__no_cache")) {
|
|
341
|
+
const cacheConfig =
|
|
342
|
+
typeof cacheOption === "function"
|
|
343
|
+
? cacheOption(env, undefined)
|
|
344
|
+
: cacheOption;
|
|
345
|
+
if (cacheConfig.enabled !== false) cacheStore = cacheConfig.store;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const requestContext = createRequestContext<TEnv>({
|
|
349
|
+
env,
|
|
350
|
+
request: req,
|
|
351
|
+
url,
|
|
352
|
+
variables,
|
|
353
|
+
cacheStore,
|
|
354
|
+
cacheProfiles: router.cacheProfiles,
|
|
355
|
+
});
|
|
356
|
+
// Match production: the RSC handler stores the router's basename on the
|
|
357
|
+
// request context (handler.ts), and redirect() prefixes root-relative URLs
|
|
358
|
+
// with it. Mirror it so basename-redirect tests behave as they do in a real
|
|
359
|
+
// mounted app instead of always seeing no prefix.
|
|
360
|
+
requestContext._basename = router.basename;
|
|
361
|
+
|
|
362
|
+
// Match production's response-route reverse EXACTLY: the real handler builds
|
|
363
|
+
// it from the route map alone (response-route-handler.ts), with NO matched
|
|
364
|
+
// routeKey or params. Passing routeKey/params here would auto-fill params from
|
|
365
|
+
// the matched route, so ctx.reverse("name") could pass in a test while the
|
|
366
|
+
// real handler throws for the missing param.
|
|
367
|
+
const reverse = createReverseFunction(
|
|
368
|
+
router.routeMap as Record<string, string>,
|
|
369
|
+
) as (
|
|
370
|
+
name: string,
|
|
371
|
+
p?: Record<string, string>,
|
|
372
|
+
search?: Record<string, unknown>,
|
|
373
|
+
) => string;
|
|
374
|
+
|
|
375
|
+
const isPartial = url.searchParams.has("_rsc_partial");
|
|
376
|
+
const isAction = url.searchParams.has("_rsc_action");
|
|
377
|
+
|
|
378
|
+
return runWithRequestContext(requestContext, async () => {
|
|
379
|
+
// Set params before middleware/handler run, so global middleware sees
|
|
380
|
+
// ctx.params (production sets them during matching, before middleware).
|
|
381
|
+
// On a redirect/unmatched path there are no route params.
|
|
382
|
+
if (routeKey !== undefined) {
|
|
383
|
+
setRequestContextParams(params, routeKey);
|
|
384
|
+
} else {
|
|
385
|
+
requestContext.params = params;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// The response-route handler (with its own route middleware) lives inside
|
|
389
|
+
// coreHandler below, mirroring production where handleResponseRoute is
|
|
390
|
+
// nested inside coreHandler. Built lazily so a redirect/404 path never
|
|
391
|
+
// touches it.
|
|
392
|
+
const callResponseRoute = (): Promise<Response> => {
|
|
393
|
+
// Match production: a partial (client-navigation) request to a response
|
|
394
|
+
// route is short-circuited to X-RSC-Reload (handleResponseRoute), BEFORE
|
|
395
|
+
// route-level middleware runs. Route-level middleware is skipped on a
|
|
396
|
+
// partial, exactly as production skips it.
|
|
397
|
+
const partialFinalHandler = async (): Promise<Response> =>
|
|
398
|
+
createResponseWithMergedHeaders(null, {
|
|
399
|
+
status: 200,
|
|
400
|
+
headers: {
|
|
401
|
+
"X-RSC-Reload": stripInternalParams(url).toString(),
|
|
402
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const cleanUrl = new URL(req.url);
|
|
407
|
+
for (const key of [...cleanUrl.searchParams.keys()]) {
|
|
408
|
+
if (key.startsWith("_rsc")) cleanUrl.searchParams.delete(key);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Lightweight response-handler context mirroring handleResponseRoute.
|
|
412
|
+
const responseHandlerCtx = {
|
|
413
|
+
request: req,
|
|
414
|
+
params,
|
|
415
|
+
env,
|
|
416
|
+
searchParams: cleanUrl.searchParams,
|
|
417
|
+
url: cleanUrl,
|
|
418
|
+
originalUrl: requestContext.originalUrl,
|
|
419
|
+
pathname: url.pathname,
|
|
420
|
+
reverse,
|
|
421
|
+
get: requestContext.get,
|
|
422
|
+
header: (name: string, value: string) =>
|
|
423
|
+
requestContext.header(name, value),
|
|
424
|
+
waitUntil: requestContext.waitUntil.bind(requestContext),
|
|
425
|
+
executionContext: requestContext.executionContext,
|
|
426
|
+
_responseType: responseType,
|
|
427
|
+
};
|
|
428
|
+
// Brand as request-scoped so a "use cache" inside a response-route handler
|
|
429
|
+
// is detected as a request-scope violation here exactly as in production
|
|
430
|
+
// (response-route-handler.ts brands the same shape).
|
|
431
|
+
(responseHandlerCtx as Record<symbol, unknown>)[NOCACHE_SYMBOL] = true;
|
|
432
|
+
|
|
433
|
+
const callHandler = async (): Promise<Response> => {
|
|
434
|
+
let merged: Response;
|
|
435
|
+
try {
|
|
436
|
+
const result = await (handler as Function)(responseHandlerCtx);
|
|
437
|
+
if (result instanceof Response) {
|
|
438
|
+
// Handler returned a Response: mirror handleResponseRoute's
|
|
439
|
+
// rewrapResponse (WebSocket-upgrade bypass + Set-Cookie-preserving
|
|
440
|
+
// header rebuild, statusText dropped) rather than the generic
|
|
441
|
+
// createResponseWithMergedHeaders re-wrap below.
|
|
442
|
+
merged = rewrapHandlerResponse(result);
|
|
443
|
+
} else {
|
|
444
|
+
// Route the serialized (json/text/...) body through the SAME
|
|
445
|
+
// production finalizer the RSC handler uses, so ctx.onResponse()
|
|
446
|
+
// callbacks fire and stub headers/cookies + the ctx.setStatus
|
|
447
|
+
// override merge identically to production. Runs inside
|
|
448
|
+
// runWithRequestContext, so _getRequestContext() resolves here.
|
|
449
|
+
const serialized = serializeResponseRouteResult(
|
|
450
|
+
result,
|
|
451
|
+
responseType as string,
|
|
452
|
+
);
|
|
453
|
+
merged = createResponseWithMergedHeaders(serialized.body, {
|
|
454
|
+
status: serialized.status,
|
|
455
|
+
headers: serialized.headers,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
} catch (error) {
|
|
459
|
+
// Mirror handleResponseRoute's catch: a genuine handler error becomes
|
|
460
|
+
// the router's typed 500 / RouterError-status Response (NOT a rejected
|
|
461
|
+
// promise). Middleware short-circuit via thrown Response is handled by
|
|
462
|
+
// executeMiddleware and never reaches here.
|
|
463
|
+
const derivedStatus =
|
|
464
|
+
error instanceof RouterError ? error.status : 500;
|
|
465
|
+
// Resolve the effective status the way createResponseWithMergedHeaders
|
|
466
|
+
// (below) will (ctx.res.status override) BEFORE building the problem
|
|
467
|
+
// body, so the body's status/title match the actual HTTP status when a
|
|
468
|
+
// handler called ctx.setStatus() before throwing — exactly as
|
|
469
|
+
// handleResponseRoute resolves it.
|
|
470
|
+
const status =
|
|
471
|
+
requestContext.res.status !== 200
|
|
472
|
+
? requestContext.res.status
|
|
473
|
+
: derivedStatus;
|
|
474
|
+
const serialized = serializeResponseRouteError(
|
|
475
|
+
error,
|
|
476
|
+
responseType as string,
|
|
477
|
+
status,
|
|
478
|
+
);
|
|
479
|
+
merged = createResponseWithMergedHeaders(serialized.body, {
|
|
480
|
+
status: serialized.status,
|
|
481
|
+
headers: serialized.headers,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Append Vary: Accept on content-negotiated responses, matching
|
|
486
|
+
// handleResponseRoute's callHandlerWithVary. Skipped on WebSocket
|
|
487
|
+
// upgrade responses (immutable headers, Vary meaningless for a 101).
|
|
488
|
+
if (preview?.negotiated && !isWebSocketUpgradeResponse(merged)) {
|
|
489
|
+
merged.headers.append("Vary", "Accept");
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return merged;
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
// On a partial request the reload IS the terminal handler and route
|
|
496
|
+
// middleware is skipped; otherwise the response-route handler is wrapped
|
|
497
|
+
// by route-level middleware (production order: route middleware runs
|
|
498
|
+
// inside handleResponseRoute, after the global chain).
|
|
499
|
+
if (isPartial) {
|
|
500
|
+
return partialFinalHandler();
|
|
501
|
+
}
|
|
502
|
+
const routeMiddlewareEntries = (preview?.routeMiddleware ?? []).map(
|
|
503
|
+
(mw) => ({
|
|
504
|
+
entry: {
|
|
505
|
+
pattern: null,
|
|
506
|
+
regex: null,
|
|
507
|
+
paramNames: [],
|
|
508
|
+
handler: mw.handler,
|
|
509
|
+
mountPrefix: null,
|
|
510
|
+
} as MiddlewareEntry<TEnv>,
|
|
511
|
+
params: mw.params,
|
|
512
|
+
}),
|
|
513
|
+
);
|
|
514
|
+
if (routeMiddlewareEntries.length === 0) {
|
|
515
|
+
return callHandler();
|
|
516
|
+
}
|
|
517
|
+
return executeMiddleware<TEnv>(
|
|
518
|
+
routeMiddlewareEntries,
|
|
519
|
+
req,
|
|
520
|
+
env,
|
|
521
|
+
variables,
|
|
522
|
+
callHandler,
|
|
523
|
+
reverse,
|
|
524
|
+
);
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
// coreHandler is the single terminal the global middleware chain wraps,
|
|
528
|
+
// mirroring production's coreHandler (handler.ts): a trailing-slash/redirect
|
|
529
|
+
// 308, an unmatched-path 404, or the response route. Both the 308 and the
|
|
530
|
+
// 404 are produced via createResponseWithMergedHeaders so middleware-set
|
|
531
|
+
// cookies/headers merge onto them, identical to production's
|
|
532
|
+
// rsc-rendering.ts redirect path — and because they sit inside the chain, a
|
|
533
|
+
// global middleware that short-circuits (e.g. an auth 401) runs first and
|
|
534
|
+
// wins, never reaching the 308/404.
|
|
535
|
+
const coreHandler = async (): Promise<Response> => {
|
|
536
|
+
if (redirectTo) {
|
|
537
|
+
return createResponseWithMergedHeaders(null, {
|
|
538
|
+
status: 308,
|
|
539
|
+
headers: { Location: redirectTo + url.search },
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
if (isUnmatched) {
|
|
543
|
+
return createResponseWithMergedHeaders("Not Found", {
|
|
544
|
+
status: 404,
|
|
545
|
+
headers: { "content-type": "text/plain;charset=utf-8" },
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
return callResponseRoute();
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
// Global (pattern-matched) middleware wraps coreHandler, exactly as
|
|
552
|
+
// production wraps coreHandler with executeMiddleware (handler.ts).
|
|
553
|
+
const globalMatches = matchMiddleware(url.pathname, router.middleware);
|
|
554
|
+
const mwResponse =
|
|
555
|
+
globalMatches.length === 0
|
|
556
|
+
? await coreHandler()
|
|
557
|
+
: await executeMiddleware<TEnv>(
|
|
558
|
+
globalMatches,
|
|
559
|
+
req,
|
|
560
|
+
env,
|
|
561
|
+
variables,
|
|
562
|
+
coreHandler,
|
|
563
|
+
reverse,
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
// Match production's global-chain exit (handler.ts): on a partial/action
|
|
567
|
+
// request a middleware 3xx redirect is converted to a Flight-safe response
|
|
568
|
+
// so fetch() does not auto-follow it; every path then drains onResponse
|
|
569
|
+
// callbacks via finalizeResponse. dispatch is RSC-free, so the
|
|
570
|
+
// createRedirectFlightResponse stand-in falls back to the no-state
|
|
571
|
+
// 204 + X-RSC-Redirect (see the location-state divergence in the header).
|
|
572
|
+
if (isPartial || isAction) {
|
|
573
|
+
const intercepted = interceptRedirectForPartial(
|
|
574
|
+
mwResponse,
|
|
575
|
+
(redirectUrl) => createSimpleRedirectResponse(redirectUrl),
|
|
576
|
+
);
|
|
577
|
+
return finalizeResponse(intercepted ?? mwResponse);
|
|
578
|
+
}
|
|
579
|
+
return finalizeResponse(mwResponse);
|
|
580
|
+
});
|
|
581
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rangojs/router/testing/dom
|
|
3
|
+
*
|
|
4
|
+
* Component-render testing: `renderRoute`, the React-Testing-Library-style stub
|
|
5
|
+
* for client components that read router context (useParams / useReverse /
|
|
6
|
+
* Outlet / useNavigation / useLoader).
|
|
7
|
+
*
|
|
8
|
+
* Separate from the main `@rangojs/router/testing` barrel so unit suites that
|
|
9
|
+
* only test loaders, middleware, or `dispatch` never reference React, the
|
|
10
|
+
* browser runtime, or `@testing-library/react` (an optional peer that
|
|
11
|
+
* `renderRoute` lazy-loads at call time). Run these tests in a DOM environment
|
|
12
|
+
* (`happy-dom` or `jsdom`).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export { renderRoute } from "./render-route.js";
|
|
16
|
+
export type {
|
|
17
|
+
RenderRouteSpec,
|
|
18
|
+
RenderRouteOptions,
|
|
19
|
+
TestRouterHandle,
|
|
20
|
+
RenderRouteResult,
|
|
21
|
+
HandleDataSeed,
|
|
22
|
+
} from "./render-route.js";
|