@rangojs/router 0.0.0-experimental.122 → 0.0.0-experimental.125
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/dist/bin/rango.js +10 -6
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +55 -48
- package/package.json +61 -21
- package/skills/caching/SKILL.md +2 -1
- package/skills/hooks/SKILL.md +40 -29
- package/skills/host-router/SKILL.md +16 -2
- package/skills/intercept/SKILL.md +4 -2
- package/skills/layout/SKILL.md +11 -6
- package/skills/loader/SKILL.md +6 -2
- package/skills/middleware/SKILL.md +4 -2
- package/skills/migrate-nextjs/SKILL.md +3 -1
- package/skills/parallel/SKILL.md +9 -4
- package/skills/rango/SKILL.md +12 -0
- package/skills/route/SKILL.md +10 -2
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +98 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +89 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +118 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +1 -83
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +14 -1
- package/src/browser/navigation-client.ts +14 -1
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +26 -51
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/partial-update.ts +1 -83
- package/src/browser/prefetch/cache.ts +6 -45
- package/src/browser/prefetch/fetch.ts +7 -0
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +157 -99
- 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 -51
- 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-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/rsc-router.tsx +12 -4
- package/src/browser/server-action-bridge.ts +77 -15
- package/src/browser/types.ts +7 -2
- package/src/browser/validate-redirect-origin.ts +4 -5
- package/src/build/route-trie.ts +3 -0
- 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 +27 -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 +94 -46
- package/src/cache/cf/index.ts +0 -24
- package/src/cache/document-cache.ts +11 -36
- package/src/cache/handle-snapshot.ts +0 -40
- package/src/cache/index.ts +0 -27
- package/src/cache/memory-segment-store.ts +2 -48
- package/src/cache/profile-registry.ts +7 -3
- 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 +1 -22
- package/src/client.tsx +14 -38
- package/src/component-utils.ts +19 -0
- package/src/deps/ssr.ts +0 -1
- package/src/handle.ts +28 -18
- package/src/handles/MetaTags.tsx +0 -14
- 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 +40 -27
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +0 -4
- package/src/index.rsc.ts +42 -3
- package/src/index.ts +31 -1
- package/src/internal-debug.ts +2 -4
- package/src/loader.rsc.ts +19 -9
- package/src/loader.ts +12 -4
- 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 +58 -3
- 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 +11 -1
- package/src/route-map-builder.ts +0 -16
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +0 -13
- package/src/router/error-handling.ts +12 -16
- package/src/router/find-match.ts +4 -30
- package/src/router/intercept-resolution.ts +10 -1
- package/src/router/lazy-includes.ts +1 -57
- package/src/router/loader-resolution.ts +3 -2
- 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 +57 -58
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +1 -54
- 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 -21
- 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-cookies.ts +0 -13
- package/src/router/middleware-types.ts +0 -115
- package/src/router/middleware.ts +7 -30
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +1 -33
- package/src/router/prerender-match.ts +33 -45
- package/src/router/request-classification.ts +1 -38
- package/src/router/revalidation.ts +5 -58
- package/src/router/router-context.ts +0 -26
- package/src/router/router-interfaces.ts +7 -0
- package/src/router/router-options.ts +30 -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 +10 -13
- package/src/router/segment-resolution/revalidation.ts +5 -42
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +63 -40
- package/src/router/types.ts +1 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +40 -9
- package/src/rsc/handler.ts +14 -2
- package/src/rsc/helpers.ts +34 -0
- package/src/rsc/origin-guard.ts +0 -12
- package/src/rsc/progressive-enhancement.ts +4 -1
- package/src/rsc/rsc-rendering.ts +4 -7
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +30 -28
- package/src/rsc/types.ts +2 -1
- package/src/runtime-env.ts +18 -0
- 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/cookie-store.ts +52 -1
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +74 -77
- package/src/ssr/index.tsx +14 -14
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +119 -0
- package/src/testing/collect-handle.ts +40 -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 +127 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +186 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +98 -0
- package/src/testing/internal/context.ts +348 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +311 -0
- package/src/testing/render-route.tsx +504 -0
- package/src/testing/run-loader.ts +378 -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 +305 -0
- 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 +15 -15
- package/src/types/handler-context.ts +16 -13
- 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 +6 -7
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/plugin-types.ts +3 -1
- 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/use-cache-transform.ts +0 -36
- 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 +1 -108
- package/src/vite/router-discovery.ts +2 -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/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
package/src/rsc/handler.ts
CHANGED
|
@@ -453,6 +453,8 @@ export function createRSCHandler<
|
|
|
453
453
|
cacheProfiles: router.cacheProfiles,
|
|
454
454
|
executionContext: executionCtx,
|
|
455
455
|
themeConfig: router.themeConfig,
|
|
456
|
+
stateCookieName: router.resolvedStateCookieName,
|
|
457
|
+
version,
|
|
456
458
|
});
|
|
457
459
|
if (earlyMetricsStore) {
|
|
458
460
|
requestContext._debugPerformance = true;
|
|
@@ -1015,10 +1017,19 @@ export function createRSCHandler<
|
|
|
1015
1017
|
} catch (error) {
|
|
1016
1018
|
// Check if middleware/handler returned Response
|
|
1017
1019
|
if (error instanceof Response) {
|
|
1020
|
+
// An action revalidation render is delivered to the client over the
|
|
1021
|
+
// same Flight-parsing path as a partial navigation, so a Response
|
|
1022
|
+
// thrown during it must be converted exactly like a partial one
|
|
1023
|
+
// (raw 200 -> hard-nav hint, 3xx -> Flight redirect). Without this,
|
|
1024
|
+
// the no-middleware path returns the raw Response (the with-middleware
|
|
1025
|
+
// path is already covered by the isPartial || actionContinuation
|
|
1026
|
+
// guard below).
|
|
1027
|
+
const treatAsPartial = isPartial || actionContinuation != null;
|
|
1028
|
+
|
|
1018
1029
|
// During partial (client-side navigation), a 200 Response from a handler
|
|
1019
1030
|
// means the route serves raw content (JSON, text, etc.), not JSX.
|
|
1020
1031
|
// Signal the browser to hard-navigate so it renders the raw response.
|
|
1021
|
-
if (
|
|
1032
|
+
if (treatAsPartial && error.status === 200) {
|
|
1022
1033
|
console.warn(
|
|
1023
1034
|
`[RSC] Route handler at ${url.pathname} returned a Response during client-side navigation. ` +
|
|
1024
1035
|
`Falling back to hard navigation. Use data-external on the <Link> to avoid the extra round-trip.`,
|
|
@@ -1032,7 +1043,7 @@ export function createRSCHandler<
|
|
|
1032
1043
|
});
|
|
1033
1044
|
}
|
|
1034
1045
|
|
|
1035
|
-
if (
|
|
1046
|
+
if (treatAsPartial) {
|
|
1036
1047
|
const intercepted = interceptRedirectForPartial(
|
|
1037
1048
|
error,
|
|
1038
1049
|
createRedirectFlightResponse,
|
|
@@ -1079,6 +1090,7 @@ export function createRSCHandler<
|
|
|
1079
1090
|
rootLayout: router.rootLayout,
|
|
1080
1091
|
handles: handleStore.stream(),
|
|
1081
1092
|
version,
|
|
1093
|
+
stateCookieName: router.resolvedStateCookieName,
|
|
1082
1094
|
themeConfig: router.themeConfig,
|
|
1083
1095
|
warmupEnabled: router.warmupEnabled,
|
|
1084
1096
|
initialTheme: requireRequestContext().theme,
|
package/src/rsc/helpers.ts
CHANGED
|
@@ -12,6 +12,25 @@ import type { RequestContext } from "../server/request-context.js";
|
|
|
12
12
|
import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
|
|
13
13
|
import { isRedirectResponse } from "../response-utils.js";
|
|
14
14
|
import type { MiddlewareEntry, MiddlewareFn } from "../router/middleware.js";
|
|
15
|
+
import { formatCacheSignalHeader } from "../router/telemetry.js";
|
|
16
|
+
import type { RscPayload } from "./types.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* DEVELOPMENT/TEST ONLY. When the debug cache signal gate is on,
|
|
20
|
+
* match/matchPartial populate ctx._cacheSignal. Emit it as the X-Rango-Cache
|
|
21
|
+
* header. When the gate is off, ctx._cacheSignal is undefined and NOTHING is
|
|
22
|
+
* attached — output is byte-identical to the default. Header mutation failures
|
|
23
|
+
* are swallowed so immutable Response headers (e.g. protocol-switch) are safe.
|
|
24
|
+
*/
|
|
25
|
+
function applyCacheSignalHeader(target: Headers, ctx: RequestContext): void {
|
|
26
|
+
const signal = ctx._cacheSignal;
|
|
27
|
+
if (!signal || signal.length === 0) return;
|
|
28
|
+
try {
|
|
29
|
+
target.set("X-Rango-Cache", formatCacheSignalHeader(signal));
|
|
30
|
+
} catch {
|
|
31
|
+
// Headers immutable — skip.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
15
34
|
|
|
16
35
|
/**
|
|
17
36
|
* Copy stub headers from the request context onto a target Headers instance:
|
|
@@ -85,6 +104,7 @@ export function createResponseWithMergedHeaders(
|
|
|
85
104
|
const mergedHeaders = new Headers(init.headers);
|
|
86
105
|
applyStubHeaders(mergedHeaders, ctx.res.headers);
|
|
87
106
|
ctx.res.headers.delete("set-cookie");
|
|
107
|
+
applyCacheSignalHeader(mergedHeaders, ctx);
|
|
88
108
|
|
|
89
109
|
// ctx.res.status overrides init.status when explicitly set (e.g. 404 for
|
|
90
110
|
// notFound, 500 for error). Default ctx.res.status is 200.
|
|
@@ -166,6 +186,20 @@ export function interceptRedirectForPartial(
|
|
|
166
186
|
return intercepted;
|
|
167
187
|
}
|
|
168
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Attach location state set during a request to a payload's metadata.
|
|
191
|
+
* No-op if no location state was set. Callers must ensure payload.metadata
|
|
192
|
+
* is populated (the non-null assertion holds for the partial/action payloads
|
|
193
|
+
* that reach this helper).
|
|
194
|
+
*/
|
|
195
|
+
export function attachLocationStateIfPresent(payload: RscPayload): void {
|
|
196
|
+
const locationState = getLocationState();
|
|
197
|
+
if (locationState) {
|
|
198
|
+
payload.metadata!.locationState =
|
|
199
|
+
resolveLocationStateEntries(locationState);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
169
203
|
/**
|
|
170
204
|
* Only cache successful responses. Non-200 statuses (errors, redirects) are
|
|
171
205
|
* not cached -- notFound() produces 500 in response routes, and explicit
|
package/src/rsc/origin-guard.ts
CHANGED
|
@@ -69,11 +69,8 @@ export type OriginCheckConfig<TEnv = any> =
|
|
|
69
69
|
* Returns true to allow, false to reject.
|
|
70
70
|
*/
|
|
71
71
|
export function defaultOriginCheck(request: Request, url: URL): boolean {
|
|
72
|
-
// 1. Read Origin header (present on all cross-origin requests and
|
|
73
|
-
// same-origin POST/PUT/PATCH/DELETE in modern browsers)
|
|
74
72
|
let requestOrigin = request.headers.get("origin");
|
|
75
73
|
|
|
76
|
-
// 2. Fallback to Referer if Origin is absent (some proxies strip it)
|
|
77
74
|
if (!requestOrigin) {
|
|
78
75
|
const referer = request.headers.get("referer");
|
|
79
76
|
if (referer) {
|
|
@@ -85,22 +82,13 @@ export function defaultOriginCheck(request: Request, url: URL): boolean {
|
|
|
85
82
|
}
|
|
86
83
|
}
|
|
87
84
|
|
|
88
|
-
// 3. No Origin or Referer — allow (can't be browser-initiated CSRF)
|
|
89
85
|
if (!requestOrigin) return true;
|
|
90
86
|
|
|
91
|
-
// "null" origin comes from privacy-sensitive contexts (data: URLs,
|
|
92
|
-
// sandboxed iframes, cross-origin redirects). Reject it.
|
|
93
87
|
if (requestOrigin === "null") return false;
|
|
94
88
|
|
|
95
|
-
// 4. Determine expected host from Host header or URL.
|
|
96
|
-
// X-Forwarded-Host/Proto are NOT used — they are client-controllable
|
|
97
|
-
// unless a trusted proxy strips them. On standard deployments (Cloudflare
|
|
98
|
-
// Workers, Node behind nginx/caddy) the Host header is already correct.
|
|
99
|
-
// For non-standard setups, use the custom function escape hatch.
|
|
100
89
|
const expectedHost = request.headers.get("host") || url.host;
|
|
101
90
|
const expectedProtocol = url.protocol;
|
|
102
91
|
|
|
103
|
-
// 5. Build expected origin and compare (case-insensitive)
|
|
104
92
|
const expectedOrigin = `${expectedProtocol}//${expectedHost}`;
|
|
105
93
|
|
|
106
94
|
return requestOrigin.toLowerCase() === expectedOrigin.toLowerCase();
|
|
@@ -254,11 +254,11 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
254
254
|
rootLayout: ctx.router.rootLayout,
|
|
255
255
|
handles: handleStore.stream(),
|
|
256
256
|
version: ctx.version,
|
|
257
|
+
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
257
258
|
themeConfig: ctx.router.themeConfig,
|
|
258
259
|
warmupEnabled: ctx.router.warmupEnabled,
|
|
259
260
|
initialTheme: requireRequestContext().theme,
|
|
260
261
|
},
|
|
261
|
-
formState: actionResult,
|
|
262
262
|
};
|
|
263
263
|
|
|
264
264
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
@@ -276,6 +276,8 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
276
276
|
url,
|
|
277
277
|
undefined,
|
|
278
278
|
);
|
|
279
|
+
// reactFormState carries the useActionState payload via the SSR-option path
|
|
280
|
+
// (renderToReadableStream({ formState })); it does NOT travel on RscPayload.
|
|
279
281
|
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
280
282
|
formState: reactFormState,
|
|
281
283
|
nonce,
|
|
@@ -362,6 +364,7 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
362
364
|
rootLayout: ctx.router.rootLayout,
|
|
363
365
|
handles: handleStore.stream(),
|
|
364
366
|
version: ctx.version,
|
|
367
|
+
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
365
368
|
themeConfig: ctx.router.themeConfig,
|
|
366
369
|
warmupEnabled: ctx.router.warmupEnabled,
|
|
367
370
|
initialTheme: requireRequestContext().theme,
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -9,9 +9,7 @@
|
|
|
9
9
|
import {
|
|
10
10
|
requireRequestContext,
|
|
11
11
|
setRequestContextParams,
|
|
12
|
-
getLocationState,
|
|
13
12
|
} from "../server/request-context.js";
|
|
14
|
-
import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
|
|
15
13
|
import { appendMetric } from "../router/metrics.js";
|
|
16
14
|
import { getSSRSetup, isRscRequest } from "./ssr-setup.js";
|
|
17
15
|
import type { RscPayload } from "./types.js";
|
|
@@ -19,6 +17,7 @@ import type { MatchResult } from "../types.js";
|
|
|
19
17
|
import {
|
|
20
18
|
createResponseWithMergedHeaders,
|
|
21
19
|
createSimpleRedirectResponse,
|
|
20
|
+
attachLocationStateIfPresent,
|
|
22
21
|
} from "./helpers.js";
|
|
23
22
|
import type { HandlerContext } from "./handler-context.js";
|
|
24
23
|
|
|
@@ -53,6 +52,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
53
52
|
handles: handleStore.stream(),
|
|
54
53
|
version: ctx.version,
|
|
55
54
|
prefetchCacheTTL: ctx.router.prefetchCacheTTL,
|
|
55
|
+
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
56
56
|
themeConfig: ctx.router.themeConfig,
|
|
57
57
|
initialTheme: reqCtx.theme,
|
|
58
58
|
},
|
|
@@ -99,6 +99,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
99
99
|
handles: handleStore.stream(),
|
|
100
100
|
version: ctx.version,
|
|
101
101
|
prefetchCacheTTL: ctx.router.prefetchCacheTTL,
|
|
102
|
+
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
102
103
|
},
|
|
103
104
|
};
|
|
104
105
|
}
|
|
@@ -153,11 +154,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
153
154
|
// SSR (full page) requests ignore location state since there's no history.state
|
|
154
155
|
// to write to on a fresh page load.
|
|
155
156
|
if (isPartial && payload.metadata) {
|
|
156
|
-
|
|
157
|
-
if (locationState) {
|
|
158
|
-
payload.metadata.locationState =
|
|
159
|
-
resolveLocationStateEntries(locationState);
|
|
160
|
-
}
|
|
157
|
+
attachLocationStateIfPresent(payload);
|
|
161
158
|
}
|
|
162
159
|
|
|
163
160
|
const metricsStore = reqCtx._metricsStore;
|
|
@@ -39,3 +39,17 @@ export function warnNonRedirectPeResponse(): void {
|
|
|
39
39
|
`ignored — the page will re-render at the current URL instead.`,
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Warn when a non-redirect Response is returned (not thrown) from an action
|
|
45
|
+
* on the JS (fetch) path. A raw Response cannot be serialized into Flight, so
|
|
46
|
+
* it is discarded — mirroring the PE path. Use `throw redirect('/path')` for
|
|
47
|
+
* redirects.
|
|
48
|
+
*/
|
|
49
|
+
export function warnNonRedirectActionResponse(actionId: string): void {
|
|
50
|
+
console.warn(
|
|
51
|
+
`[@rangojs/router] Server action "${actionId}" returned a Response ` +
|
|
52
|
+
`that is not a redirect. Non-redirect Responses cannot be serialized ` +
|
|
53
|
+
`and are ignored. Use \`throw redirect('/path')\` for redirects.`,
|
|
54
|
+
);
|
|
55
|
+
}
|
package/src/rsc/server-action.ts
CHANGED
|
@@ -18,9 +18,7 @@
|
|
|
18
18
|
import {
|
|
19
19
|
requireRequestContext,
|
|
20
20
|
setRequestContextParams,
|
|
21
|
-
getLocationState,
|
|
22
21
|
} from "../server/request-context.js";
|
|
23
|
-
import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
|
|
24
22
|
import { appendMetric } from "../router/metrics.js";
|
|
25
23
|
import type { RscPayload } from "./types.js";
|
|
26
24
|
import {
|
|
@@ -28,21 +26,11 @@ import {
|
|
|
28
26
|
createResponseWithMergedHeaders,
|
|
29
27
|
createSimpleRedirectResponse,
|
|
30
28
|
interceptRedirectForPartial,
|
|
29
|
+
attachLocationStateIfPresent,
|
|
31
30
|
} from "./helpers.js";
|
|
31
|
+
import { warnNonRedirectActionResponse } from "./runtime-warnings.js";
|
|
32
32
|
import type { HandlerContext } from "./handler-context.js";
|
|
33
33
|
|
|
34
|
-
/**
|
|
35
|
-
* Attach location state set during the action to a payload's metadata.
|
|
36
|
-
* No-op if no location state was set.
|
|
37
|
-
*/
|
|
38
|
-
function attachLocationState(payload: RscPayload): void {
|
|
39
|
-
const locationState = getLocationState();
|
|
40
|
-
if (locationState) {
|
|
41
|
-
payload.metadata!.locationState =
|
|
42
|
-
resolveLocationStateEntries(locationState);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
34
|
/**
|
|
47
35
|
* Data flowing from action execution to the revalidation phase.
|
|
48
36
|
* When the action completes without redirect/error-boundary, the handler
|
|
@@ -109,7 +97,7 @@ export async function executeServerAction<TEnv>(
|
|
|
109
97
|
|
|
110
98
|
try {
|
|
111
99
|
loadedAction = await ctx.loadServerAction(actionId);
|
|
112
|
-
|
|
100
|
+
let data = await loadedAction!.apply(null, args);
|
|
113
101
|
|
|
114
102
|
// Intercept redirect Responses: serializing one as the action returnValue
|
|
115
103
|
// would fail, and revalidation would run needlessly.
|
|
@@ -119,6 +107,14 @@ export async function executeServerAction<TEnv>(
|
|
|
119
107
|
ctx.createRedirectFlightResponse,
|
|
120
108
|
);
|
|
121
109
|
if (intercepted) return intercepted;
|
|
110
|
+
|
|
111
|
+
// Non-redirect Response returned (not thrown): a raw Response cannot be
|
|
112
|
+
// serialized into Flight. Discard it and re-render — mirroring the PE
|
|
113
|
+
// path (progressive-enhancement.ts) so JS and no-JS behave identically.
|
|
114
|
+
if (process.env.NODE_ENV !== "production") {
|
|
115
|
+
warnNonRedirectActionResponse(actionId);
|
|
116
|
+
}
|
|
117
|
+
data = undefined;
|
|
122
118
|
}
|
|
123
119
|
|
|
124
120
|
returnValue = { ok: true, data };
|
|
@@ -224,18 +220,21 @@ export async function executeServerAction<TEnv>(
|
|
|
224
220
|
}
|
|
225
221
|
|
|
226
222
|
// Build continuation for the revalidation phase
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
223
|
+
const actionMeta = loadedAction as
|
|
224
|
+
| { $id?: string; $$id?: string }
|
|
225
|
+
| undefined;
|
|
226
|
+
const resolvedActionId = actionMeta?.$id ?? actionMeta?.$$id ?? actionId;
|
|
231
227
|
|
|
232
228
|
return {
|
|
233
229
|
returnValue,
|
|
234
230
|
actionStatus,
|
|
235
231
|
temporaryReferences,
|
|
236
232
|
actionContext: {
|
|
233
|
+
// Defensive copy of the already-parsed url (avoids re-parsing
|
|
234
|
+
// request.url). actionUrl is persisted into the continuation and later
|
|
235
|
+
// flows into matchPartial, so it must not alias the handler's live url.
|
|
237
236
|
actionId: resolvedActionId,
|
|
238
|
-
actionUrl: new URL(
|
|
237
|
+
actionUrl: new URL(url),
|
|
239
238
|
actionResult: returnValue.data,
|
|
240
239
|
formData: actionFormData,
|
|
241
240
|
},
|
|
@@ -274,8 +273,8 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
274
273
|
);
|
|
275
274
|
|
|
276
275
|
if (!matchResult) {
|
|
277
|
-
// matchPartial returns null when the route is a redirect or
|
|
278
|
-
//
|
|
276
|
+
// matchPartial returns null when the route is a redirect or no previous-URL
|
|
277
|
+
// context could be resolved. Check for redirect first.
|
|
279
278
|
const fullMatch = await ctx.router.match(request, { env });
|
|
280
279
|
setRequestContextParams(fullMatch.params, fullMatch.routeName);
|
|
281
280
|
|
|
@@ -286,14 +285,17 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
286
285
|
return createSimpleRedirectResponse(fullMatch.redirect);
|
|
287
286
|
}
|
|
288
287
|
|
|
289
|
-
// Non-redirect: this branch is only reachable when
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
288
|
+
// Non-redirect: this branch is only reachable when no previous URL could
|
|
289
|
+
// be resolved (neither X-RSC-Router-Client-Path nor a usable Referer), or
|
|
290
|
+
// the previous URL was unparseable (defensive). The client requires
|
|
291
|
+
// isPartial for action responses, so producing a full payload here would
|
|
292
|
+
// be rejected. Return 500 instead.
|
|
293
293
|
throw new Error(
|
|
294
294
|
`[RSC] matchPartial returned null for a non-redirect route ` +
|
|
295
295
|
`during action revalidation (${url.pathname}). This indicates ` +
|
|
296
|
-
`a malformed action request
|
|
296
|
+
`a malformed action request: no previous-URL context could be ` +
|
|
297
|
+
`resolved (neither X-RSC-Router-Client-Path nor a usable Referer), ` +
|
|
298
|
+
`or the previous URL was unparseable.`,
|
|
297
299
|
);
|
|
298
300
|
}
|
|
299
301
|
|
|
@@ -319,7 +321,7 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
319
321
|
returnValue,
|
|
320
322
|
};
|
|
321
323
|
|
|
322
|
-
|
|
324
|
+
attachLocationStateIfPresent(payload);
|
|
323
325
|
|
|
324
326
|
const renderStart = performance.now();
|
|
325
327
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
package/src/rsc/types.ts
CHANGED
|
@@ -43,6 +43,8 @@ export interface RscPayload {
|
|
|
43
43
|
version?: string;
|
|
44
44
|
/** TTL in milliseconds for the client-side in-memory prefetch cache */
|
|
45
45
|
prefetchCacheTTL?: number;
|
|
46
|
+
/** Server-resolved rango state cookie name; the client reads it verbatim. */
|
|
47
|
+
stateCookieName?: string;
|
|
46
48
|
/** Theme configuration for FOUC prevention */
|
|
47
49
|
themeConfig?: ResolvedThemeConfig | null;
|
|
48
50
|
/** Initial theme from cookie (for SSR hydration) */
|
|
@@ -57,7 +59,6 @@ export interface RscPayload {
|
|
|
57
59
|
locationState?: Record<string, unknown>;
|
|
58
60
|
};
|
|
59
61
|
returnValue?: { ok: boolean; data: unknown };
|
|
60
|
-
formState?: unknown;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
/**
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Runtime-safe detection of a test runner (Vitest), used to decide whether a
|
|
2
|
+
// create*() call with no plugin-injected $$id may fall back to a synthetic id (a
|
|
3
|
+
// bare test) or must fail loud (dev / a real build).
|
|
4
|
+
//
|
|
5
|
+
// `process` is absent in some target runtimes (the browser, certain edge/worker
|
|
6
|
+
// RSC environments), so probe it through `globalThis` with optional chaining —
|
|
7
|
+
// NEVER a bare `process.env.VITEST`, which would ReferenceError before the
|
|
8
|
+
// intended error is thrown. Unlike `process.env.NODE_ENV` (folded by the app's
|
|
9
|
+
// build `define`), `VITEST` is not folded, so this stays a small runtime check;
|
|
10
|
+
// it lives only on the create*() error path (id missing), which never runs in a
|
|
11
|
+
// correct production build.
|
|
12
|
+
//
|
|
13
|
+
// Vitest sets `VITEST` in every test process — the node project and the
|
|
14
|
+
// react-server forks alike (the RSC project forces NODE_ENV=production, so NODE_ENV
|
|
15
|
+
// cannot distinguish it from a real build; `VITEST` can). A real build never sets it.
|
|
16
|
+
export function isUnderTestRunner(): boolean {
|
|
17
|
+
return !!globalThis.process?.env?.VITEST;
|
|
18
|
+
}
|
package/src/search-params.ts
CHANGED
|
@@ -7,10 +7,6 @@
|
|
|
7
7
|
* URLSearchParams instance.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
// ============================================================================
|
|
11
|
-
// Schema Types
|
|
12
|
-
// ============================================================================
|
|
13
|
-
|
|
14
10
|
/** Supported scalar types for search params (append ? for optional). */
|
|
15
11
|
export type SearchSchemaValue =
|
|
16
12
|
| "string"
|
|
@@ -23,10 +19,6 @@ export type SearchSchemaValue =
|
|
|
23
19
|
/** A search schema maps param names to their type descriptors. */
|
|
24
20
|
export type SearchSchema = Record<string, SearchSchemaValue>;
|
|
25
21
|
|
|
26
|
-
// ============================================================================
|
|
27
|
-
// Type-Level Schema Resolution
|
|
28
|
-
// ============================================================================
|
|
29
|
-
|
|
30
22
|
/** Strip trailing `?` from a schema value to get the base type. */
|
|
31
23
|
type BaseType<T extends string> = T extends `${infer B}?` ? B : T;
|
|
32
24
|
|
|
@@ -163,10 +155,6 @@ type ExtractParamsFromPattern<T extends string> =
|
|
|
163
155
|
: { [K in Param]: string }
|
|
164
156
|
: {};
|
|
165
157
|
|
|
166
|
-
// ============================================================================
|
|
167
|
-
// Runtime Parser
|
|
168
|
-
// ============================================================================
|
|
169
|
-
|
|
170
158
|
/**
|
|
171
159
|
* Parse URLSearchParams into a typed object using the given schema.
|
|
172
160
|
*
|
|
@@ -210,10 +198,6 @@ export function parseSearchParams<T extends SearchSchema>(
|
|
|
210
198
|
return result as ResolveSearchSchema<T>;
|
|
211
199
|
}
|
|
212
200
|
|
|
213
|
-
// ============================================================================
|
|
214
|
-
// Runtime Serializer
|
|
215
|
-
// ============================================================================
|
|
216
|
-
|
|
217
201
|
/**
|
|
218
202
|
* Serialize a typed search params object to a query string (without leading `?`).
|
|
219
203
|
* Skips `undefined` and `null` values.
|
|
@@ -26,7 +26,10 @@ const IS_BROWSER = typeof window !== "undefined";
|
|
|
26
26
|
|
|
27
27
|
interface LoaderCacheEntry {
|
|
28
28
|
sources: any[];
|
|
29
|
-
|
|
29
|
+
// buildLoaderPromise always returns a Promise, so the cached value is never a
|
|
30
|
+
// bare array. The public getMemoizedLoaderPromise return type stays broader
|
|
31
|
+
// (Promise<any[]> | any[]) to mirror its siblings.
|
|
32
|
+
promise: Promise<any[]>;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
const objectLoaderCache = IS_BROWSER
|
|
@@ -56,7 +59,16 @@ function hasSameReferences(a: any[], b: any[]): boolean {
|
|
|
56
59
|
return true;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Build a fresh aggregate Promise.all over the loaders' resolved data refs.
|
|
64
|
+
* Unlike getMemoizedLoaderPromise this never caches, so each call yields a new
|
|
65
|
+
* Promise — correct for sites that await the result immediately (a shared,
|
|
66
|
+
* already-resolved promise would leak React's `.status` across server requests
|
|
67
|
+
* and skip the Suspense fallback).
|
|
68
|
+
*
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
export function buildLoaderPromise(loaders: ResolvedSegment[]): Promise<any[]> {
|
|
60
72
|
if (loaders.length === 0) {
|
|
61
73
|
return Promise.resolve([]);
|
|
62
74
|
}
|