@rangojs/router 0.0.0-experimental.79 → 0.0.0-experimental.7d061845
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 +2138 -841
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +68 -21
- 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 +3 -1
- 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 +26 -4
- package/skills/layout/SKILL.md +6 -7
- package/skills/links/SKILL.md +247 -17
- package/skills/loader/SKILL.md +219 -9
- package/skills/middleware/SKILL.md +15 -9
- package/skills/migrate-nextjs/SKILL.md +4 -2
- package/skills/migrate-react-router/SKILL.md +5 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +12 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +242 -24
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- package/skills/route/SKILL.md +33 -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 +816 -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 +117 -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 +65 -9
- package/src/browser/navigation-client.ts +45 -25
- package/src/browser/navigation-store.ts +32 -9
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +52 -26
- package/src/browser/prefetch/cache.ts +124 -26
- package/src/browser/prefetch/fetch.ts +114 -38
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +18 -13
- 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 +25 -0
- package/src/browser/rsc-router.tsx +64 -22
- package/src/browser/scroll-restoration.ts +22 -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 +2 -0
- package/src/build/route-trie.ts +2 -1
- 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 +10 -8
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +26 -13
- 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 -20
- package/src/index.rsc.ts +9 -4
- package/src/index.ts +16 -6
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +21 -6
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender.ts +4 -4
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -39
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +253 -265
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +43 -15
- 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 +19 -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/handler-context.ts +21 -41
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +3 -3
- package/src/router/loader-resolution.ts +19 -2
- 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 +101 -17
- 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 +18 -13
- package/src/router/types.ts +8 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +38 -23
- 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/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-route-handler.ts +46 -53
- 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/search-params.ts +4 -4
- package/src/segment-system.tsx +122 -56
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +118 -51
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +20 -42
- package/src/ssr/index.tsx +5 -1
- package/src/static-handler.ts +1 -1
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -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 +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +332 -0
- package/src/testing/flight.entry.ts +46 -0
- package/src/testing/flight.ts +224 -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 +267 -0
- package/src/testing/render-route.tsx +565 -0
- package/src/testing/run-loader.ts +341 -0
- package/src/testing/run-middleware.ts +188 -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 +270 -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 +5 -6
- package/src/types/request-scope.ts +126 -0
- package/src/types/segments.ts +35 -1
- package/src/urls/include-helper.ts +10 -53
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +11 -3
- 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 +26 -116
- 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 +101 -51
- 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 +21 -5
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +5 -4
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
package/src/rsc/index.ts
CHANGED
package/src/rsc/origin-guard.ts
CHANGED
|
@@ -9,11 +9,29 @@
|
|
|
9
9
|
* navigations, bookmarks, and non-browser clients don't send Origin.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import type { RequestPlan } from "../router/request-classification.js";
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* Request phase that triggered the origin check.
|
|
14
16
|
*/
|
|
15
17
|
export type OriginCheckPhase = "action" | "loader" | "pe-form";
|
|
16
18
|
|
|
19
|
+
// Exhaustive over RequestPlan modes so a new mode must be classified here (the
|
|
20
|
+
// security gate) instead of silently falling through to no origin check.
|
|
21
|
+
export const ORIGIN_CHECK_PHASE_BY_MODE: Record<
|
|
22
|
+
RequestPlan["mode"],
|
|
23
|
+
OriginCheckPhase | null
|
|
24
|
+
> = {
|
|
25
|
+
action: "action",
|
|
26
|
+
loader: "loader",
|
|
27
|
+
"pe-render": "pe-form",
|
|
28
|
+
"full-render": null,
|
|
29
|
+
"partial-render": null,
|
|
30
|
+
response: null,
|
|
31
|
+
redirect: null,
|
|
32
|
+
"version-mismatch": null,
|
|
33
|
+
};
|
|
34
|
+
|
|
17
35
|
/**
|
|
18
36
|
* Context passed to the originCheck callback.
|
|
19
37
|
*/
|
|
@@ -116,14 +134,15 @@ export async function checkRequestOrigin<TEnv = any>(
|
|
|
116
134
|
// Disabled by explicit opt-out
|
|
117
135
|
if (config === false) return null;
|
|
118
136
|
|
|
119
|
-
// Default
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
137
|
+
// Default (true/undefined) becomes a callback returning boolean, so the
|
|
138
|
+
// Response|true|reject resolution below is written once.
|
|
139
|
+
const check: (
|
|
140
|
+
ctx: OriginCheckContext<TEnv>,
|
|
141
|
+
) => boolean | Response | Promise<boolean | Response> =
|
|
142
|
+
config === true || config === undefined
|
|
143
|
+
? () => defaultOriginCheck(request, url)
|
|
144
|
+
: config;
|
|
125
145
|
|
|
126
|
-
// Custom function — build context and call
|
|
127
146
|
const ctx: OriginCheckContext<TEnv> = {
|
|
128
147
|
request,
|
|
129
148
|
url,
|
|
@@ -133,9 +152,8 @@ export async function checkRequestOrigin<TEnv = any>(
|
|
|
133
152
|
defaultCheck: () => defaultOriginCheck(request, url),
|
|
134
153
|
};
|
|
135
154
|
|
|
136
|
-
const result = await
|
|
155
|
+
const result = await check(ctx);
|
|
137
156
|
|
|
138
157
|
if (result instanceof Response) return result;
|
|
139
|
-
|
|
140
|
-
return createForbiddenResponse(request);
|
|
158
|
+
return result === true ? null : createForbiddenResponse(request);
|
|
141
159
|
}
|
|
@@ -248,6 +248,8 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
248
248
|
segments: match.segments,
|
|
249
249
|
matched: match.matched,
|
|
250
250
|
diff: match.diff,
|
|
251
|
+
resolvedIds: match.resolvedIds,
|
|
252
|
+
params: match.params,
|
|
251
253
|
isPartial: false,
|
|
252
254
|
rootLayout: ctx.router.rootLayout,
|
|
253
255
|
handles: handleStore.stream(),
|
|
@@ -353,6 +355,8 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
353
355
|
segments: errorResult.segments,
|
|
354
356
|
matched: errorResult.matched,
|
|
355
357
|
diff: errorResult.diff,
|
|
358
|
+
resolvedIds: errorResult.resolvedIds,
|
|
359
|
+
params: errorResult.params,
|
|
356
360
|
isPartial: false,
|
|
357
361
|
isError: true,
|
|
358
362
|
rootLayout: ctx.router.rootLayout,
|
|
@@ -11,6 +11,7 @@ import { requireRequestContext } from "../server/request-context.js";
|
|
|
11
11
|
import { contextGet } from "../context-var.js";
|
|
12
12
|
import { NOCACHE_SYMBOL } from "../cache/taint.js";
|
|
13
13
|
import { traverseBack } from "../router/pattern-matching.js";
|
|
14
|
+
import { RESPONSE_TYPE_MIME } from "../router/content-negotiation.js";
|
|
14
15
|
import { createCacheScope } from "../cache/cache-scope.js";
|
|
15
16
|
import { executeMiddleware } from "../router/middleware.js";
|
|
16
17
|
import {
|
|
@@ -26,7 +27,9 @@ import {
|
|
|
26
27
|
finalizeResponse,
|
|
27
28
|
isCacheableStatus,
|
|
28
29
|
buildRouteMiddlewareEntries,
|
|
30
|
+
mergeStubHeadersAndFinalize,
|
|
29
31
|
} from "./helpers.js";
|
|
32
|
+
import { isWebSocketUpgradeResponse } from "../response-utils.js";
|
|
30
33
|
|
|
31
34
|
export interface ResponseRouteMatch {
|
|
32
35
|
responseType: string;
|
|
@@ -78,10 +81,13 @@ export async function handleResponseRoute<TEnv>(
|
|
|
78
81
|
env,
|
|
79
82
|
searchParams: cleanUrl.searchParams,
|
|
80
83
|
url: cleanUrl,
|
|
84
|
+
originalUrl: reqCtx.originalUrl,
|
|
81
85
|
pathname: url.pathname,
|
|
82
86
|
reverse: createReverseFunction(handlerCtx.getRequiredRouteMap()),
|
|
83
87
|
get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
|
|
84
88
|
header: (name: string, value: string) => reqCtx.header(name, value),
|
|
89
|
+
waitUntil: reqCtx.waitUntil.bind(reqCtx),
|
|
90
|
+
executionContext: reqCtx.executionContext,
|
|
85
91
|
_responseType: preview.responseType,
|
|
86
92
|
};
|
|
87
93
|
// Brand with taint symbol so "use cache" detects it as request-scoped
|
|
@@ -96,6 +102,12 @@ export async function handleResponseRoute<TEnv>(
|
|
|
96
102
|
// so that stub headers (cookies, custom headers set via ctx.header()) are included.
|
|
97
103
|
// Use Headers (not Record<string, string>) to preserve duplicate entries like Set-Cookie.
|
|
98
104
|
const rewrapResponse = (result: Response) => {
|
|
105
|
+
// 204/205/304 are NOT short-circuited — they're valid for the Response
|
|
106
|
+
// constructor and must honor ctx.setStatus() overrides. Only upgrade
|
|
107
|
+
// responses (status 101 / `webSocket` property) bypass reconstruction.
|
|
108
|
+
if (isWebSocketUpgradeResponse(result)) {
|
|
109
|
+
return mergeStubHeadersAndFinalize(result);
|
|
110
|
+
}
|
|
99
111
|
const headers = new Headers();
|
|
100
112
|
result.headers.forEach((value, key) => {
|
|
101
113
|
if (key.toLowerCase() === "set-cookie") {
|
|
@@ -110,13 +122,15 @@ export async function handleResponseRoute<TEnv>(
|
|
|
110
122
|
});
|
|
111
123
|
};
|
|
112
124
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
try {
|
|
126
|
+
const result = await (preview.handler as Function)(responseHandlerCtx);
|
|
127
|
+
|
|
128
|
+
if (result instanceof Response) {
|
|
129
|
+
return rewrapResponse(result);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Handled before the MIME lookup (json is also a RESPONSE_TYPE_MIME key).
|
|
133
|
+
if (preview.responseType === "json") {
|
|
120
134
|
return createResponseWithMergedHeaders(
|
|
121
135
|
JSON.stringify({ data: result }),
|
|
122
136
|
{
|
|
@@ -124,10 +138,28 @@ export async function handleResponseRoute<TEnv>(
|
|
|
124
138
|
headers: { "content-type": "application/json;charset=utf-8" },
|
|
125
139
|
},
|
|
126
140
|
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Object.hasOwn (not truthiness) so prototype names like "toString" are not
|
|
144
|
+
// matched; image/stream/any are absent and fall through to the throw.
|
|
145
|
+
if (Object.hasOwn(RESPONSE_TYPE_MIME, preview.responseType)) {
|
|
146
|
+
return createResponseWithMergedHeaders(String(result), {
|
|
147
|
+
status: 200,
|
|
148
|
+
headers: {
|
|
149
|
+
"content-type": `${RESPONSE_TYPE_MIME[preview.responseType]};charset=utf-8`,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Response route handler for "${preview.responseType}" must return a Response object, got ${typeof result}`,
|
|
156
|
+
);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
handlerCtx.callOnError(error, "handler", errorCtx);
|
|
159
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
160
|
+
const status = error instanceof RouterError ? error.status : 500;
|
|
161
|
+
|
|
162
|
+
if (preview.responseType === "json") {
|
|
131
163
|
return createResponseWithMergedHeaders(
|
|
132
164
|
JSON.stringify({
|
|
133
165
|
error: createResponseErrorPayload(error, isDev),
|
|
@@ -138,48 +170,7 @@ export async function handleResponseRoute<TEnv>(
|
|
|
138
170
|
},
|
|
139
171
|
);
|
|
140
172
|
}
|
|
141
|
-
}
|
|
142
173
|
|
|
143
|
-
// Non-JSON response routes: catch errors and return plain Response
|
|
144
|
-
try {
|
|
145
|
-
const result = await (preview.handler as Function)(responseHandlerCtx);
|
|
146
|
-
|
|
147
|
-
if (result instanceof Response) {
|
|
148
|
-
return rewrapResponse(result);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Auto-wrap based on response type tag
|
|
152
|
-
switch (preview.responseType) {
|
|
153
|
-
case "text":
|
|
154
|
-
return createResponseWithMergedHeaders(String(result), {
|
|
155
|
-
status: 200,
|
|
156
|
-
headers: { "content-type": "text/plain;charset=utf-8" },
|
|
157
|
-
});
|
|
158
|
-
case "html":
|
|
159
|
-
return createResponseWithMergedHeaders(String(result), {
|
|
160
|
-
status: 200,
|
|
161
|
-
headers: { "content-type": "text/html;charset=utf-8" },
|
|
162
|
-
});
|
|
163
|
-
case "xml":
|
|
164
|
-
return createResponseWithMergedHeaders(String(result), {
|
|
165
|
-
status: 200,
|
|
166
|
-
headers: { "content-type": "application/xml;charset=utf-8" },
|
|
167
|
-
});
|
|
168
|
-
case "md":
|
|
169
|
-
return createResponseWithMergedHeaders(String(result), {
|
|
170
|
-
status: 200,
|
|
171
|
-
headers: { "content-type": "text/markdown;charset=utf-8" },
|
|
172
|
-
});
|
|
173
|
-
default:
|
|
174
|
-
// image, stream, any -- must return Response
|
|
175
|
-
throw new Error(
|
|
176
|
-
`Response route handler for "${preview.responseType}" must return a Response object, got ${typeof result}`,
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
} catch (error) {
|
|
180
|
-
handlerCtx.callOnError(error, "handler", errorCtx);
|
|
181
|
-
const isDev = process.env.NODE_ENV !== "production";
|
|
182
|
-
const status = error instanceof RouterError ? error.status : 500;
|
|
183
174
|
const message =
|
|
184
175
|
error instanceof RouterError
|
|
185
176
|
? error.message
|
|
@@ -196,7 +187,9 @@ export async function handleResponseRoute<TEnv>(
|
|
|
196
187
|
// Wrap callHandler to append Vary: Accept on content-negotiated responses
|
|
197
188
|
const callHandlerWithVary = async () => {
|
|
198
189
|
const response = await callHandler();
|
|
199
|
-
if (preview.negotiated) {
|
|
190
|
+
if (preview.negotiated && !isWebSocketUpgradeResponse(response)) {
|
|
191
|
+
// Skip Vary on upgrade responses: headers are semantically immutable
|
|
192
|
+
// on some runtimes, and Vary is meaningless for a 101 response.
|
|
200
193
|
response.headers.append("Vary", "Accept");
|
|
201
194
|
}
|
|
202
195
|
return response;
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -13,8 +13,9 @@ import {
|
|
|
13
13
|
} from "../server/request-context.js";
|
|
14
14
|
import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
|
|
15
15
|
import { appendMetric } from "../router/metrics.js";
|
|
16
|
-
import { getSSRSetup } from "./ssr-setup.js";
|
|
16
|
+
import { getSSRSetup, isRscRequest } from "./ssr-setup.js";
|
|
17
17
|
import type { RscPayload } from "./types.js";
|
|
18
|
+
import type { MatchResult } from "../types.js";
|
|
18
19
|
import {
|
|
19
20
|
createResponseWithMergedHeaders,
|
|
20
21
|
createSimpleRedirectResponse,
|
|
@@ -35,6 +36,28 @@ export async function handleRscRendering<TEnv>(
|
|
|
35
36
|
let payload: RscPayload;
|
|
36
37
|
let hasInterceptSlots = false;
|
|
37
38
|
|
|
39
|
+
// Shared by the partial-fallback and full-render paths. The partial-success
|
|
40
|
+
// payload below is intentionally different (omits rootLayout/theme, adds slots).
|
|
41
|
+
const buildFullPayload = (m: MatchResult): RscPayload => ({
|
|
42
|
+
metadata: {
|
|
43
|
+
pathname: url.pathname,
|
|
44
|
+
routerId: ctx.router.id,
|
|
45
|
+
basename: ctx.router.basename,
|
|
46
|
+
segments: m.segments,
|
|
47
|
+
matched: m.matched,
|
|
48
|
+
diff: m.diff,
|
|
49
|
+
resolvedIds: m.resolvedIds,
|
|
50
|
+
params: m.params,
|
|
51
|
+
isPartial: false,
|
|
52
|
+
rootLayout: ctx.router.rootLayout,
|
|
53
|
+
handles: handleStore.stream(),
|
|
54
|
+
version: ctx.version,
|
|
55
|
+
prefetchCacheTTL: ctx.router.prefetchCacheTTL,
|
|
56
|
+
themeConfig: ctx.router.themeConfig,
|
|
57
|
+
initialTheme: reqCtx.theme,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
38
61
|
if (isPartial) {
|
|
39
62
|
// Partial render (navigation)
|
|
40
63
|
const result = await ctx.router.matchPartial(request, { env });
|
|
@@ -51,24 +74,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
51
74
|
return createSimpleRedirectResponse(match.redirect);
|
|
52
75
|
}
|
|
53
76
|
|
|
54
|
-
payload =
|
|
55
|
-
metadata: {
|
|
56
|
-
pathname: url.pathname,
|
|
57
|
-
routerId: ctx.router.id,
|
|
58
|
-
basename: ctx.router.basename,
|
|
59
|
-
segments: match.segments,
|
|
60
|
-
matched: match.matched,
|
|
61
|
-
diff: match.diff,
|
|
62
|
-
params: match.params,
|
|
63
|
-
isPartial: false,
|
|
64
|
-
rootLayout: ctx.router.rootLayout,
|
|
65
|
-
handles: handleStore.stream(),
|
|
66
|
-
version: ctx.version,
|
|
67
|
-
prefetchCacheTTL: ctx.router.prefetchCacheTTL,
|
|
68
|
-
themeConfig: ctx.router.themeConfig,
|
|
69
|
-
initialTheme: reqCtx.theme,
|
|
70
|
-
},
|
|
71
|
-
};
|
|
77
|
+
payload = buildFullPayload(match);
|
|
72
78
|
} else {
|
|
73
79
|
setRequestContextParams(result.params, result.routeName);
|
|
74
80
|
|
|
@@ -81,6 +87,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
81
87
|
segments: result.segments,
|
|
82
88
|
matched: result.matched,
|
|
83
89
|
diff: result.diff,
|
|
90
|
+
resolvedIds: result.resolvedIds,
|
|
84
91
|
params: result.params,
|
|
85
92
|
isPartial: true,
|
|
86
93
|
slots: result.slots,
|
|
@@ -133,27 +140,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
133
140
|
{ headers: { "Content-Type": "application/json" } },
|
|
134
141
|
);
|
|
135
142
|
} else {
|
|
136
|
-
payload =
|
|
137
|
-
// Initial SSR can reconstruct the tree from segments + rootLayout,
|
|
138
|
-
// so we omit root to avoid sending the same structure twice.
|
|
139
|
-
|
|
140
|
-
metadata: {
|
|
141
|
-
pathname: url.pathname,
|
|
142
|
-
routerId: ctx.router.id,
|
|
143
|
-
basename: ctx.router.basename,
|
|
144
|
-
segments: match.segments,
|
|
145
|
-
matched: match.matched,
|
|
146
|
-
diff: match.diff,
|
|
147
|
-
params: match.params,
|
|
148
|
-
isPartial: false,
|
|
149
|
-
rootLayout: ctx.router.rootLayout,
|
|
150
|
-
handles: handleStore.stream(),
|
|
151
|
-
version: ctx.version,
|
|
152
|
-
prefetchCacheTTL: ctx.router.prefetchCacheTTL,
|
|
153
|
-
themeConfig: ctx.router.themeConfig,
|
|
154
|
-
initialTheme: reqCtx.theme,
|
|
155
|
-
},
|
|
156
|
-
};
|
|
143
|
+
payload = buildFullPayload(match);
|
|
157
144
|
}
|
|
158
145
|
}
|
|
159
146
|
|
|
@@ -187,23 +174,20 @@ export async function handleRscRendering<TEnv>(
|
|
|
187
174
|
rscSerializeDur,
|
|
188
175
|
);
|
|
189
176
|
|
|
190
|
-
|
|
191
|
-
// Partial requests (_rsc_partial) are always RSC -- they come from client-side
|
|
192
|
-
// navigation or prefetch fetch(). We cannot rely on Accept alone since some
|
|
193
|
-
// browsers may send Accept: text/html for non-HTML requests.
|
|
194
|
-
const isRscRequest =
|
|
195
|
-
isPartial ||
|
|
196
|
-
(!request.headers.get("accept")?.includes("text/html") &&
|
|
197
|
-
!url.searchParams.has("__html")) ||
|
|
198
|
-
url.searchParams.has("__rsc");
|
|
199
|
-
|
|
200
|
-
if (isRscRequest) {
|
|
177
|
+
if (isRscRequest(request, url, isPartial)) {
|
|
201
178
|
const renderDur = performance.now() - renderStart;
|
|
202
179
|
appendMetric(metricsStore, "render:total", renderStart, renderDur);
|
|
203
180
|
const rscHeaders: Record<string, string> = {
|
|
204
181
|
"content-type": "text/x-component;charset=utf-8",
|
|
205
182
|
vary: "accept, X-Rango-State, X-RSC-Router-Client-Path",
|
|
206
183
|
};
|
|
184
|
+
// Tell the client's prefetch cache to scope this response to its source
|
|
185
|
+
// URL (instead of the default source-agnostic wildcard). Intercept
|
|
186
|
+
// responses depend on the source page matching an intercept rule, so
|
|
187
|
+
// they must not be reused for navigations from other sources.
|
|
188
|
+
if (hasInterceptSlots) {
|
|
189
|
+
rscHeaders["x-rsc-prefetch-scope"] = "source";
|
|
190
|
+
}
|
|
207
191
|
// Enable browser HTTP caching for prefetch responses only.
|
|
208
192
|
// Requires X-Rango-Prefetch header (sent by Link prefetch fetch),
|
|
209
193
|
// non-intercept context (intercept responses depend on source page),
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
createResponseWithMergedHeaders,
|
|
9
9
|
carryOverRedirectHeaders,
|
|
10
10
|
} from "./helpers.js";
|
|
11
|
+
import { isRedirectResponse } from "../response-utils.js";
|
|
11
12
|
|
|
12
13
|
// W3 -----------------------------------------------------------------------
|
|
13
14
|
|
|
@@ -18,16 +19,14 @@ import {
|
|
|
18
19
|
*/
|
|
19
20
|
export function extractRedirectResponse(value: unknown): Response | null {
|
|
20
21
|
if (!(value instanceof Response)) return null;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
return null;
|
|
22
|
+
if (!isRedirectResponse(value)) return null;
|
|
23
|
+
const location = value.headers.get("Location")!;
|
|
24
|
+
const redirect = createResponseWithMergedHeaders(null, {
|
|
25
|
+
status: value.status,
|
|
26
|
+
headers: { Location: location },
|
|
27
|
+
});
|
|
28
|
+
carryOverRedirectHeaders(value, redirect);
|
|
29
|
+
return redirect;
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
/**
|
package/src/rsc/server-action.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
hasBodyContent,
|
|
28
28
|
createResponseWithMergedHeaders,
|
|
29
29
|
createSimpleRedirectResponse,
|
|
30
|
-
|
|
30
|
+
interceptRedirectForPartial,
|
|
31
31
|
} from "./helpers.js";
|
|
32
32
|
import type { HandlerContext } from "./handler-context.js";
|
|
33
33
|
|
|
@@ -111,49 +111,25 @@ export async function executeServerAction<TEnv>(
|
|
|
111
111
|
loadedAction = await ctx.loadServerAction(actionId);
|
|
112
112
|
const data = await loadedAction!.apply(null, args);
|
|
113
113
|
|
|
114
|
-
// Intercept redirect
|
|
115
|
-
//
|
|
116
|
-
// and the revalidation step would run unnecessarily.
|
|
114
|
+
// Intercept redirect Responses: serializing one as the action returnValue
|
|
115
|
+
// would fail, and revalidation would run needlessly.
|
|
117
116
|
if (data instanceof Response) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (locationState) {
|
|
124
|
-
redirect = ctx.createRedirectFlightResponse(
|
|
125
|
-
redirectUrl,
|
|
126
|
-
resolveLocationStateEntries(locationState),
|
|
127
|
-
);
|
|
128
|
-
} else {
|
|
129
|
-
redirect = createSimpleRedirectResponse(redirectUrl);
|
|
130
|
-
}
|
|
131
|
-
carryOverRedirectHeaders(data, redirect);
|
|
132
|
-
return redirect;
|
|
133
|
-
}
|
|
117
|
+
const intercepted = interceptRedirectForPartial(
|
|
118
|
+
data,
|
|
119
|
+
ctx.createRedirectFlightResponse,
|
|
120
|
+
);
|
|
121
|
+
if (intercepted) return intercepted;
|
|
134
122
|
}
|
|
135
123
|
|
|
136
124
|
returnValue = { ok: true, data };
|
|
137
125
|
} catch (error) {
|
|
138
126
|
// Handle thrown redirect (e.g., throw redirect('/path'))
|
|
139
127
|
if (error instanceof Response) {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
let redirect: Response;
|
|
146
|
-
if (locationState) {
|
|
147
|
-
redirect = ctx.createRedirectFlightResponse(
|
|
148
|
-
redirectUrl,
|
|
149
|
-
resolveLocationStateEntries(locationState),
|
|
150
|
-
);
|
|
151
|
-
} else {
|
|
152
|
-
redirect = createSimpleRedirectResponse(redirectUrl);
|
|
153
|
-
}
|
|
154
|
-
carryOverRedirectHeaders(error, redirect);
|
|
155
|
-
return redirect;
|
|
156
|
-
}
|
|
128
|
+
const intercepted = interceptRedirectForPartial(
|
|
129
|
+
error,
|
|
130
|
+
ctx.createRedirectFlightResponse,
|
|
131
|
+
);
|
|
132
|
+
if (intercepted) return intercepted;
|
|
157
133
|
|
|
158
134
|
// Non-redirect Response thrown from action — this will be treated
|
|
159
135
|
// as a regular error and routed to the error boundary. Warn in dev
|
|
@@ -213,6 +189,8 @@ export async function executeServerAction<TEnv>(
|
|
|
213
189
|
isPartial: true,
|
|
214
190
|
matched: errorResult.matched,
|
|
215
191
|
diff: errorResult.diff,
|
|
192
|
+
resolvedIds: errorResult.resolvedIds,
|
|
193
|
+
params: errorResult.params,
|
|
216
194
|
isError: true,
|
|
217
195
|
handles: handleStore.stream(),
|
|
218
196
|
version: ctx.version,
|
|
@@ -323,6 +301,8 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
323
301
|
isPartial: true,
|
|
324
302
|
matched: matchResult.matched,
|
|
325
303
|
diff: matchResult.diff,
|
|
304
|
+
resolvedIds: matchResult.resolvedIds,
|
|
305
|
+
params: matchResult.params,
|
|
326
306
|
slots: matchResult.slots,
|
|
327
307
|
handles: handleStore.stream(),
|
|
328
308
|
version: ctx.version,
|
package/src/rsc/ssr-setup.ts
CHANGED
|
@@ -126,3 +126,19 @@ export function mayNeedSSR(request: Request, url: URL): boolean {
|
|
|
126
126
|
|
|
127
127
|
return true;
|
|
128
128
|
}
|
|
129
|
+
|
|
130
|
+
// Final render-time decision: is the response an RSC stream (vs HTML)? Distinct
|
|
131
|
+
// from mayNeedSSR, which is a conservative pre-classifier (it treats a missing
|
|
132
|
+
// Accept header as needing SSR; this treats it as RSC).
|
|
133
|
+
export function isRscRequest(
|
|
134
|
+
request: Request,
|
|
135
|
+
url: URL,
|
|
136
|
+
isPartial: boolean,
|
|
137
|
+
): boolean {
|
|
138
|
+
return (
|
|
139
|
+
isPartial ||
|
|
140
|
+
(!request.headers.get("accept")?.includes("text/html") &&
|
|
141
|
+
!url.searchParams.has("__html")) ||
|
|
142
|
+
url.searchParams.has("__rsc")
|
|
143
|
+
);
|
|
144
|
+
}
|
package/src/rsc/types.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { ResolvedSegment, SlotState } from "../types.js";
|
|
9
9
|
import type { HandleData } from "../server/handle-store.js";
|
|
10
|
-
import type {
|
|
10
|
+
import type { RangoInternal } from "../router/router-interfaces.js";
|
|
11
11
|
import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -26,6 +26,12 @@ export interface RscPayload {
|
|
|
26
26
|
isError?: boolean;
|
|
27
27
|
matched?: string[];
|
|
28
28
|
diff?: string[];
|
|
29
|
+
/**
|
|
30
|
+
* All segment ids re-resolved on the server, including null-component
|
|
31
|
+
* ones excluded from `segments`/`diff`. Drives client-side handle-bucket
|
|
32
|
+
* cleanup. Superset of `diff`. See MatchResult.resolvedIds.
|
|
33
|
+
*/
|
|
34
|
+
resolvedIds?: string[];
|
|
29
35
|
/** Merged route params from the matched route */
|
|
30
36
|
params?: Record<string, string>;
|
|
31
37
|
slots?: Record<string, SlotState>;
|
|
@@ -179,7 +185,7 @@ export interface CreateRSCHandlerOptions<
|
|
|
179
185
|
/**
|
|
180
186
|
* The RSC router instance
|
|
181
187
|
*/
|
|
182
|
-
router:
|
|
188
|
+
router: RangoInternal<TEnv, TRoutes>;
|
|
183
189
|
|
|
184
190
|
/**
|
|
185
191
|
* RSC dependencies from @vitejs/plugin-rsc/rsc.
|
package/src/search-params.ts
CHANGED
|
@@ -81,11 +81,11 @@ export type ResolveSearchSchema<T extends SearchSchema> = Simplify<
|
|
|
81
81
|
// ============================================================================
|
|
82
82
|
|
|
83
83
|
/** Resolve the global route map from RegisteredRoutes or GeneratedRouteMap. */
|
|
84
|
-
type GlobalRouteMap = keyof
|
|
85
|
-
? keyof
|
|
84
|
+
type GlobalRouteMap = keyof Rango.RegisteredRoutes extends never
|
|
85
|
+
? keyof Rango.GeneratedRouteMap extends never
|
|
86
86
|
? Record<string, string>
|
|
87
|
-
:
|
|
88
|
-
:
|
|
87
|
+
: Rango.GeneratedRouteMap
|
|
88
|
+
: Rango.RegisteredRoutes;
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
91
|
* Extract the resolved search params type for a named route.
|