@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26
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 +294 -28
- package/dist/bin/rango.js +355 -47
- package/dist/vite/index.js +1658 -1239
- package/package.json +3 -3
- package/skills/cache-guide/SKILL.md +9 -5
- package/skills/caching/SKILL.md +4 -4
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +40 -29
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +229 -15
- package/skills/middleware/SKILL.md +109 -30
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +189 -19
- package/skills/rango/SKILL.md +1 -2
- package/skills/response-routes/SKILL.md +3 -3
- package/skills/route/SKILL.md +44 -3
- package/skills/router-setup/SKILL.md +80 -3
- package/skills/theme/SKILL.md +5 -4
- package/skills/typesafety/SKILL.md +59 -16
- package/skills/use-cache/SKILL.md +16 -2
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +56 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +29 -48
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +19 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +66 -443
- package/src/browser/navigation-client.ts +34 -62
- package/src/browser/navigation-store.ts +4 -33
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/partial-update.ts +103 -151
- package/src/browser/prefetch/cache.ts +67 -0
- package/src/browser/prefetch/fetch.ts +137 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +154 -44
- package/src/browser/react/NavigationProvider.tsx +32 -0
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +29 -11
- package/src/browser/react/location-state.ts +6 -4
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +23 -45
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +21 -64
- package/src/browser/react/use-navigation.ts +7 -32
- package/src/browser/react/use-params.ts +5 -34
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +3 -6
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +75 -114
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +46 -22
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +458 -405
- package/src/browser/types.ts +21 -35
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +38 -13
- package/src/build/generate-route-types.ts +4 -0
- package/src/build/index.ts +1 -0
- package/src/build/route-trie.ts +19 -3
- package/src/build/route-types/codegen.ts +13 -4
- package/src/build/route-types/include-resolution.ts +13 -0
- package/src/build/route-types/per-module-writer.ts +15 -3
- package/src/build/route-types/router-processing.ts +170 -18
- package/src/build/runtime-discovery.ts +13 -1
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +136 -123
- package/src/cache/cache-scope.ts +76 -83
- package/src/cache/cf/cf-cache-store.ts +12 -7
- package/src/cache/document-cache.ts +93 -69
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/index.ts +0 -15
- package/src/cache/memory-segment-store.ts +43 -69
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +140 -117
- package/src/cache/taint.ts +30 -3
- package/src/cache/types.ts +1 -115
- package/src/client.rsc.tsx +0 -1
- package/src/client.tsx +53 -76
- package/src/errors.ts +6 -1
- package/src/handle.ts +1 -1
- package/src/handles/MetaTags.tsx +5 -2
- package/src/host/cookie-handler.ts +8 -3
- package/src/host/index.ts +0 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +53 -10
- package/src/index.ts +73 -43
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +60 -18
- package/src/prerender.ts +76 -18
- package/src/reverse.ts +11 -7
- package/src/root-error-boundary.tsx +30 -26
- package/src/route-definition/dsl-helpers.ts +9 -6
- package/src/route-definition/index.ts +0 -3
- package/src/route-definition/redirect.ts +15 -3
- package/src/route-map-builder.ts +38 -2
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +7 -0
- package/src/router/content-negotiation.ts +1 -1
- package/src/router/debug-manifest.ts +16 -3
- package/src/router/handler-context.ts +96 -17
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +6 -11
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +62 -54
- package/src/router/match-context.ts +3 -0
- package/src/router/match-handlers.ts +185 -11
- package/src/router/match-middleware/background-revalidation.ts +65 -85
- package/src/router/match-middleware/cache-lookup.ts +78 -10
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/match-result.ts +0 -9
- package/src/router/metrics.ts +233 -13
- package/src/router/middleware-types.ts +34 -39
- package/src/router/middleware.ts +290 -130
- package/src/router/pattern-matching.ts +61 -10
- package/src/router/prerender-match.ts +36 -6
- package/src/router/preview-match.ts +7 -1
- package/src/router/revalidation.ts +61 -2
- package/src/router/router-context.ts +15 -0
- package/src/router/router-interfaces.ts +158 -40
- package/src/router/router-options.ts +223 -1
- package/src/router/router-registry.ts +5 -2
- package/src/router/segment-resolution/fresh.ts +165 -242
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +102 -98
- package/src/router/segment-resolution/revalidation.ts +394 -272
- package/src/router/segment-resolution/static-store.ts +2 -2
- package/src/router/segment-resolution.ts +1 -3
- package/src/router/segment-wrappers.ts +3 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +20 -2
- package/src/router/types.ts +7 -1
- package/src/router.ts +203 -18
- package/src/rsc/handler-context.ts +13 -2
- package/src/rsc/handler.ts +489 -438
- package/src/rsc/helpers.ts +125 -5
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/manifest-init.ts +3 -2
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +245 -19
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +47 -43
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +166 -66
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +20 -2
- package/src/search-params.ts +38 -23
- package/src/server/context.ts +61 -7
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +84 -12
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +275 -49
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +67 -28
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +4 -18
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +6 -1
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +22 -0
- package/src/types/handler-context.ts +103 -16
- package/src/types/index.ts +1 -1
- package/src/types/loader-types.ts +9 -6
- package/src/types/route-config.ts +17 -26
- package/src/types/route-entry.ts +28 -0
- package/src/types/segments.ts +0 -5
- package/src/urls/include-helper.ts +49 -8
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +30 -12
- package/src/urls/path-helper.ts +17 -2
- package/src/urls/pattern-types.ts +21 -1
- package/src/urls/response-types.ts +29 -7
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +27 -9
- package/src/vite/discovery/bundle-postprocess.ts +32 -52
- package/src/vite/discovery/discover-routers.ts +52 -26
- package/src/vite/discovery/prerender-collection.ts +58 -41
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/state.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/index.ts +10 -51
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/expose-internal-ids.ts +4 -3
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/plugins/version-plugin.ts +188 -18
- package/src/vite/rango.ts +61 -36
- package/src/vite/router-discovery.ts +173 -100
- package/src/vite/utils/prerender-utils.ts +81 -0
- package/src/vite/utils/shared-utils.ts +19 -9
- package/skills/testing/SKILL.md +0 -226
- package/src/browser/lru-cache.ts +0 -61
- package/src/browser/react/prefetch.ts +0 -27
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/route-definition/route-function.ts +0 -119
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/{CLAUDE.md → AGENTS.md} +0 -0
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
getLocationState,
|
|
13
13
|
} from "../server/request-context.js";
|
|
14
14
|
import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
|
|
15
|
+
import { appendMetric } from "../router/metrics.js";
|
|
16
|
+
import { getSSRSetup } from "./ssr-setup.js";
|
|
15
17
|
import type { RscPayload } from "./types.js";
|
|
16
18
|
import {
|
|
17
19
|
createResponseWithMergedHeaders,
|
|
@@ -28,13 +30,10 @@ export async function handleRscRendering<TEnv>(
|
|
|
28
30
|
handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
29
31
|
nonce: string | undefined,
|
|
30
32
|
): Promise<Response> {
|
|
31
|
-
// Retrieve handler-level timing from variables
|
|
32
33
|
const reqCtx = requireRequestContext();
|
|
33
|
-
const handlerTimingArr: string[] = reqCtx.var.__handlerTiming || [];
|
|
34
|
-
const handlerStart: number = reqCtx.var.__handlerStart || 0;
|
|
35
34
|
|
|
36
35
|
let payload: RscPayload;
|
|
37
|
-
let
|
|
36
|
+
let hasInterceptSlots = false;
|
|
38
37
|
|
|
39
38
|
if (isPartial) {
|
|
40
39
|
// Partial render (navigation)
|
|
@@ -52,8 +51,6 @@ export async function handleRscRendering<TEnv>(
|
|
|
52
51
|
return createSimpleRedirectResponse(match.redirect);
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
serverTiming = match.serverTiming;
|
|
56
|
-
|
|
57
54
|
payload = {
|
|
58
55
|
metadata: {
|
|
59
56
|
pathname: url.pathname,
|
|
@@ -71,7 +68,8 @@ export async function handleRscRendering<TEnv>(
|
|
|
71
68
|
};
|
|
72
69
|
} else {
|
|
73
70
|
setRequestContextParams(result.params, result.routeName);
|
|
74
|
-
|
|
71
|
+
|
|
72
|
+
hasInterceptSlots = !!result.slots;
|
|
75
73
|
|
|
76
74
|
payload = {
|
|
77
75
|
metadata: {
|
|
@@ -109,6 +107,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
109
107
|
const nonLoaderSegments = match.segments.filter(
|
|
110
108
|
(s) => s.type !== "loader",
|
|
111
109
|
);
|
|
110
|
+
handleStore.seal();
|
|
112
111
|
await handleStore.settled;
|
|
113
112
|
const { serializeSegments } = await import("../cache/segment-codec.js");
|
|
114
113
|
const serializedSegments = await serializeSegments(nonLoaderSegments);
|
|
@@ -129,8 +128,6 @@ export async function handleRscRendering<TEnv>(
|
|
|
129
128
|
{ headers: { "Content-Type": "application/json" } },
|
|
130
129
|
);
|
|
131
130
|
} else {
|
|
132
|
-
serverTiming = match.serverTiming;
|
|
133
|
-
|
|
134
131
|
payload = {
|
|
135
132
|
// Initial SSR can reconstruct the tree from segments + rootLayout,
|
|
136
133
|
// so we omit root to avoid sending the same structure twice.
|
|
@@ -163,68 +160,75 @@ export async function handleRscRendering<TEnv>(
|
|
|
163
160
|
}
|
|
164
161
|
}
|
|
165
162
|
|
|
163
|
+
const metricsStore = reqCtx._metricsStore;
|
|
164
|
+
const renderStart = performance.now();
|
|
165
|
+
|
|
166
166
|
// Serialize to RSC stream
|
|
167
167
|
const rscSerializeStart = performance.now();
|
|
168
168
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload);
|
|
169
169
|
const rscSerializeDur = performance.now() - rscSerializeStart;
|
|
170
|
+
// This measures synchronous stream creation, not end-to-end stream consumption.
|
|
171
|
+
appendMetric(
|
|
172
|
+
metricsStore,
|
|
173
|
+
"rsc-serialize",
|
|
174
|
+
rscSerializeStart,
|
|
175
|
+
rscSerializeDur,
|
|
176
|
+
);
|
|
170
177
|
|
|
171
178
|
// Determine if this is an RSC request or HTML request.
|
|
172
179
|
// Partial requests (_rsc_partial) are always RSC -- they come from client-side
|
|
173
|
-
// navigation or
|
|
174
|
-
//
|
|
180
|
+
// navigation or prefetch fetch(). We cannot rely on Accept alone since some
|
|
181
|
+
// browsers may send Accept: text/html for non-HTML requests.
|
|
175
182
|
const isRscRequest =
|
|
176
183
|
isPartial ||
|
|
177
184
|
(!request.headers.get("accept")?.includes("text/html") &&
|
|
178
185
|
!url.searchParams.has("__html")) ||
|
|
179
186
|
url.searchParams.has("__rsc");
|
|
180
187
|
|
|
181
|
-
// Build complete Server-Timing: handler phases + match/manifest + RSC serialize
|
|
182
|
-
const timingParts: string[] = [...handlerTimingArr];
|
|
183
|
-
if (serverTiming) {
|
|
184
|
-
timingParts.push(serverTiming);
|
|
185
|
-
}
|
|
186
|
-
timingParts.push(`rsc-serialize;dur=${rscSerializeDur.toFixed(2)}`);
|
|
187
|
-
|
|
188
188
|
if (isRscRequest) {
|
|
189
|
-
const
|
|
189
|
+
const renderDur = performance.now() - renderStart;
|
|
190
|
+
appendMetric(metricsStore, "render:total", renderStart, renderDur);
|
|
190
191
|
const rscHeaders: Record<string, string> = {
|
|
191
192
|
"content-type": "text/x-component;charset=utf-8",
|
|
192
|
-
vary: "accept",
|
|
193
|
+
vary: "accept, X-Rango-State, X-RSC-Router-Client-Path",
|
|
193
194
|
};
|
|
194
|
-
|
|
195
|
-
|
|
195
|
+
// Enable browser HTTP caching for prefetch responses only.
|
|
196
|
+
// Requires X-Rango-Prefetch header (sent by Link prefetch fetch),
|
|
197
|
+
// non-intercept context (intercept responses depend on source page),
|
|
198
|
+
// and a configured cache-control value (false disables caching).
|
|
199
|
+
const isPrefetch = request.headers.has("X-Rango-Prefetch");
|
|
200
|
+
if (isPrefetch && isPartial && !hasInterceptSlots) {
|
|
201
|
+
const cc = ctx.router.prefetchCacheControl;
|
|
202
|
+
if (cc) {
|
|
203
|
+
rscHeaders["cache-control"] = cc;
|
|
204
|
+
}
|
|
196
205
|
}
|
|
197
206
|
return createResponseWithMergedHeaders(rscStream, {
|
|
198
207
|
headers: rscHeaders,
|
|
199
208
|
});
|
|
200
209
|
}
|
|
201
210
|
|
|
202
|
-
// Delegate to SSR for HTML response
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
211
|
+
// Delegate to SSR for HTML response (reuse early setup if available)
|
|
212
|
+
const [ssrModule, streamMode] = await getSSRSetup(
|
|
213
|
+
ctx,
|
|
214
|
+
request,
|
|
215
|
+
env,
|
|
216
|
+
url,
|
|
217
|
+
metricsStore,
|
|
218
|
+
);
|
|
207
219
|
|
|
208
220
|
const ssrRenderStart = performance.now();
|
|
209
|
-
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
221
|
+
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
222
|
+
nonce,
|
|
223
|
+
streamMode,
|
|
224
|
+
});
|
|
210
225
|
const ssrRenderDur = performance.now() - ssrRenderStart;
|
|
211
|
-
|
|
226
|
+
appendMetric(metricsStore, "ssr-render-html", ssrRenderStart, ssrRenderDur);
|
|
212
227
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const totalHandler = performance.now() - handlerStart;
|
|
216
|
-
timingParts.push(`handler-total;dur=${totalHandler.toFixed(2)}`);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const fullTiming = timingParts.join(", ");
|
|
220
|
-
const htmlHeaders: Record<string, string> = {
|
|
221
|
-
"content-type": "text/html;charset=utf-8",
|
|
222
|
-
};
|
|
223
|
-
if (fullTiming) {
|
|
224
|
-
htmlHeaders["Server-Timing"] = fullTiming;
|
|
225
|
-
}
|
|
228
|
+
const renderDur = performance.now() - renderStart;
|
|
229
|
+
appendMetric(metricsStore, "render:total", renderStart, renderDur);
|
|
226
230
|
|
|
227
231
|
return createResponseWithMergedHeaders(htmlStream, {
|
|
228
|
-
headers:
|
|
232
|
+
headers: { "content-type": "text/html;charset=utf-8" },
|
|
229
233
|
});
|
|
230
234
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime guardrail warnings (dev-only).
|
|
3
|
+
*
|
|
4
|
+
* W3: PE action redirect / Response handling.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
createResponseWithMergedHeaders,
|
|
9
|
+
carryOverRedirectHeaders,
|
|
10
|
+
} from "./helpers.js";
|
|
11
|
+
|
|
12
|
+
// W3 -----------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extract a redirect Response from a thrown or returned value.
|
|
16
|
+
* Returns a redirect Response to send to the client, or null if the value
|
|
17
|
+
* is not a redirect Response.
|
|
18
|
+
*/
|
|
19
|
+
export function extractRedirectResponse(value: unknown): Response | null {
|
|
20
|
+
if (!(value instanceof Response)) return null;
|
|
21
|
+
const location = value.headers.get("Location");
|
|
22
|
+
if (value.status >= 300 && value.status < 400 && location) {
|
|
23
|
+
const redirect = createResponseWithMergedHeaders(null, {
|
|
24
|
+
status: value.status,
|
|
25
|
+
headers: { Location: location },
|
|
26
|
+
});
|
|
27
|
+
carryOverRedirectHeaders(value, redirect);
|
|
28
|
+
return redirect;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Warn when a non-redirect Response is returned from an action during PE.
|
|
35
|
+
*/
|
|
36
|
+
export function warnNonRedirectPeResponse(): void {
|
|
37
|
+
console.warn(
|
|
38
|
+
`[rango] Server action returned a non-redirect Response during ` +
|
|
39
|
+
`progressive enhancement (no-JS) request. The Response will be ` +
|
|
40
|
+
`ignored — the page will re-render at the current URL instead.`,
|
|
41
|
+
);
|
|
42
|
+
}
|
package/src/rsc/server-action.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Server Action Handler
|
|
3
3
|
*
|
|
4
|
-
* Handles server action execution and post-action revalidation
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Handles server action execution and post-action revalidation as two
|
|
5
|
+
* separate phases:
|
|
6
|
+
*
|
|
7
|
+
* 1. executeServerAction — decodes args, runs the action, handles redirects
|
|
8
|
+
* and error boundaries. Returns either a final Response (redirect/error)
|
|
9
|
+
* or an ActionContinuation for the revalidation phase.
|
|
10
|
+
*
|
|
11
|
+
* 2. revalidateAfterAction — takes the continuation, matches affected
|
|
12
|
+
* segments, builds the RSC payload, and returns the Flight response.
|
|
13
|
+
*
|
|
14
|
+
* The handler (handler.ts) runs the action BEFORE route middleware, then
|
|
15
|
+
* wraps revalidation inside route middleware — identical to a normal render.
|
|
7
16
|
*/
|
|
8
17
|
|
|
9
18
|
import {
|
|
@@ -12,22 +21,62 @@ import {
|
|
|
12
21
|
getLocationState,
|
|
13
22
|
} from "../server/request-context.js";
|
|
14
23
|
import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
|
|
24
|
+
import { appendMetric } from "../router/metrics.js";
|
|
15
25
|
import type { RscPayload } from "./types.js";
|
|
16
26
|
import {
|
|
17
27
|
hasBodyContent,
|
|
18
28
|
createResponseWithMergedHeaders,
|
|
19
29
|
createSimpleRedirectResponse,
|
|
30
|
+
carryOverRedirectHeaders,
|
|
20
31
|
} from "./helpers.js";
|
|
21
32
|
import type { HandlerContext } from "./handler-context.js";
|
|
22
33
|
|
|
23
|
-
|
|
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
|
+
/**
|
|
47
|
+
* Data flowing from action execution to the revalidation phase.
|
|
48
|
+
* When the action completes without redirect/error-boundary, the handler
|
|
49
|
+
* passes this to route middleware → revalidateAfterAction.
|
|
50
|
+
*/
|
|
51
|
+
export interface ActionContinuation {
|
|
52
|
+
returnValue: { ok: boolean; data: unknown };
|
|
53
|
+
actionStatus: number;
|
|
54
|
+
temporaryReferences: ReturnType<
|
|
55
|
+
HandlerContext["createTemporaryReferenceSet"]
|
|
56
|
+
>;
|
|
57
|
+
actionContext: {
|
|
58
|
+
actionId: string;
|
|
59
|
+
actionUrl: URL;
|
|
60
|
+
actionResult: unknown;
|
|
61
|
+
formData?: FormData;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Phase 1: Execute the server action.
|
|
67
|
+
*
|
|
68
|
+
* Decodes arguments, runs the action, handles redirects and error
|
|
69
|
+
* boundaries. Returns a final Response (redirect, error boundary render)
|
|
70
|
+
* or an ActionContinuation for the revalidation phase.
|
|
71
|
+
*/
|
|
72
|
+
export async function executeServerAction<TEnv>(
|
|
24
73
|
ctx: HandlerContext<TEnv>,
|
|
25
74
|
request: Request,
|
|
26
75
|
env: TEnv,
|
|
27
76
|
url: URL,
|
|
28
77
|
actionId: string,
|
|
29
78
|
handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
30
|
-
): Promise<Response> {
|
|
79
|
+
): Promise<Response | ActionContinuation> {
|
|
31
80
|
const temporaryReferences = ctx.createTemporaryReferenceSet();
|
|
32
81
|
|
|
33
82
|
// Decode action arguments from request body
|
|
@@ -70,15 +119,17 @@ export async function handleServerAction<TEnv>(
|
|
|
70
119
|
const isRedirect = data.status >= 300 && data.status < 400 && redirectUrl;
|
|
71
120
|
if (isRedirect) {
|
|
72
121
|
const locationState = getLocationState();
|
|
122
|
+
let redirect: Response;
|
|
73
123
|
if (locationState) {
|
|
74
|
-
|
|
75
|
-
return ctx.createRedirectFlightResponse(
|
|
124
|
+
redirect = ctx.createRedirectFlightResponse(
|
|
76
125
|
redirectUrl,
|
|
77
126
|
resolveLocationStateEntries(locationState),
|
|
78
127
|
);
|
|
128
|
+
} else {
|
|
129
|
+
redirect = createSimpleRedirectResponse(redirectUrl);
|
|
79
130
|
}
|
|
80
|
-
|
|
81
|
-
return
|
|
131
|
+
carryOverRedirectHeaders(data, redirect);
|
|
132
|
+
return redirect;
|
|
82
133
|
}
|
|
83
134
|
}
|
|
84
135
|
|
|
@@ -91,28 +142,58 @@ export async function handleServerAction<TEnv>(
|
|
|
91
142
|
error.status >= 300 && error.status < 400 && redirectUrl;
|
|
92
143
|
if (isRedirect) {
|
|
93
144
|
const locationState = getLocationState();
|
|
145
|
+
let redirect: Response;
|
|
94
146
|
if (locationState) {
|
|
95
|
-
|
|
147
|
+
redirect = ctx.createRedirectFlightResponse(
|
|
96
148
|
redirectUrl,
|
|
97
149
|
resolveLocationStateEntries(locationState),
|
|
98
150
|
);
|
|
151
|
+
} else {
|
|
152
|
+
redirect = createSimpleRedirectResponse(redirectUrl);
|
|
99
153
|
}
|
|
100
|
-
|
|
154
|
+
carryOverRedirectHeaders(error, redirect);
|
|
155
|
+
return redirect;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Non-redirect Response thrown from action — this will be treated
|
|
159
|
+
// as a regular error and routed to the error boundary. Warn in dev
|
|
160
|
+
// since the intent is likely a redirect with a missing Location header.
|
|
161
|
+
if (process.env.NODE_ENV !== "production") {
|
|
162
|
+
console.warn(
|
|
163
|
+
`[@rangojs/router] Server action "${actionId}" threw a Response ` +
|
|
164
|
+
`(status ${error.status}) that is not a redirect. ` +
|
|
165
|
+
`Non-redirect Responses are treated as errors. ` +
|
|
166
|
+
`Use \`throw redirect('/path')\` for redirects.`,
|
|
167
|
+
);
|
|
101
168
|
}
|
|
102
169
|
}
|
|
103
170
|
|
|
104
171
|
returnValue = { ok: false, data: error };
|
|
105
172
|
actionStatus = 500;
|
|
106
173
|
|
|
107
|
-
// Try to render error boundary
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
174
|
+
// Try to render error boundary.
|
|
175
|
+
// Report the action error first so it is not lost if matchError throws.
|
|
176
|
+
let errorResult;
|
|
177
|
+
try {
|
|
178
|
+
errorResult = await ctx.router.matchError(
|
|
179
|
+
request,
|
|
180
|
+
{ env },
|
|
181
|
+
error,
|
|
182
|
+
"route",
|
|
183
|
+
);
|
|
184
|
+
} catch (matchErr) {
|
|
185
|
+
// matchError failed — report the original action error as unhandled,
|
|
186
|
+
// then let the matchError failure propagate.
|
|
187
|
+
ctx.callOnError(error, "action", {
|
|
188
|
+
request,
|
|
189
|
+
url,
|
|
190
|
+
env,
|
|
191
|
+
actionId,
|
|
192
|
+
handledByBoundary: false,
|
|
193
|
+
});
|
|
194
|
+
throw matchErr;
|
|
195
|
+
}
|
|
114
196
|
|
|
115
|
-
// Report the action error (handledByBoundary indicates if error boundary will render)
|
|
116
197
|
ctx.callOnError(error, "action", {
|
|
117
198
|
request,
|
|
118
199
|
url,
|
|
@@ -138,6 +219,10 @@ export async function handleServerAction<TEnv>(
|
|
|
138
219
|
returnValue,
|
|
139
220
|
};
|
|
140
221
|
|
|
222
|
+
// Intentionally omit attachLocationState for error payloads:
|
|
223
|
+
// location state is a success-only semantic. Error boundary responses
|
|
224
|
+
// update the error UI but should not mutate browser history state.
|
|
225
|
+
|
|
141
226
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
142
227
|
temporaryReferences,
|
|
143
228
|
});
|
|
@@ -149,17 +234,49 @@ export async function handleServerAction<TEnv>(
|
|
|
149
234
|
}
|
|
150
235
|
}
|
|
151
236
|
|
|
152
|
-
//
|
|
237
|
+
// Build continuation for the revalidation phase
|
|
153
238
|
const resolvedActionId =
|
|
154
239
|
(loadedAction as { $id?: string; $$id?: string } | undefined)?.$id ??
|
|
155
240
|
(loadedAction as { $$id?: string } | undefined)?.$$id ??
|
|
156
241
|
actionId;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
returnValue,
|
|
245
|
+
actionStatus,
|
|
246
|
+
temporaryReferences,
|
|
247
|
+
actionContext: {
|
|
248
|
+
actionId: resolvedActionId,
|
|
249
|
+
actionUrl: new URL(request.url),
|
|
250
|
+
actionResult: returnValue.data,
|
|
251
|
+
formData: actionFormData,
|
|
252
|
+
},
|
|
162
253
|
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Phase 2: Revalidate after action.
|
|
258
|
+
*
|
|
259
|
+
* Matches affected segments, builds the RSC payload, and returns the
|
|
260
|
+
* Flight response. Called inside route middleware (same as a normal render).
|
|
261
|
+
*
|
|
262
|
+
* Invariant: the response payload MUST have isPartial: true. The client
|
|
263
|
+
* (server-action-bridge) rejects non-partial payloads because partial
|
|
264
|
+
* reconciliation requires matched/diff semantics that full renders don't
|
|
265
|
+
* provide. Redirects are the only non-partial outcome and are handled via
|
|
266
|
+
* X-RSC-Redirect headers before Flight deserialization.
|
|
267
|
+
*/
|
|
268
|
+
export async function revalidateAfterAction<TEnv>(
|
|
269
|
+
ctx: HandlerContext<TEnv>,
|
|
270
|
+
request: Request,
|
|
271
|
+
env: TEnv,
|
|
272
|
+
url: URL,
|
|
273
|
+
handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
274
|
+
continuation: ActionContinuation,
|
|
275
|
+
): Promise<Response> {
|
|
276
|
+
const { returnValue, actionStatus, temporaryReferences, actionContext } =
|
|
277
|
+
continuation;
|
|
278
|
+
const reqCtx = requireRequestContext();
|
|
279
|
+
const metricsStore = reqCtx._metricsStore;
|
|
163
280
|
|
|
164
281
|
const matchResult = await ctx.router.matchPartial(
|
|
165
282
|
request,
|
|
@@ -168,7 +285,8 @@ export async function handleServerAction<TEnv>(
|
|
|
168
285
|
);
|
|
169
286
|
|
|
170
287
|
if (!matchResult) {
|
|
171
|
-
//
|
|
288
|
+
// matchPartial returns null when the route is a redirect or the request
|
|
289
|
+
// is missing required headers (previousUrl). Check for redirect first.
|
|
172
290
|
const fullMatch = await ctx.router.match(request, { env });
|
|
173
291
|
setRequestContextParams(fullMatch.params, fullMatch.routeName);
|
|
174
292
|
|
|
@@ -179,43 +297,20 @@ export async function handleServerAction<TEnv>(
|
|
|
179
297
|
return createSimpleRedirectResponse(fullMatch.redirect);
|
|
180
298
|
}
|
|
181
299
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
handles: handleStore.stream(),
|
|
192
|
-
version: ctx.version,
|
|
193
|
-
},
|
|
194
|
-
returnValue,
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
198
|
-
temporaryReferences,
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
const headers: Record<string, string> = {
|
|
202
|
-
"content-type": "text/x-component;charset=utf-8",
|
|
203
|
-
};
|
|
204
|
-
if (serverTiming) {
|
|
205
|
-
headers["Server-Timing"] = serverTiming;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return createResponseWithMergedHeaders(rscStream, {
|
|
209
|
-
status: actionStatus,
|
|
210
|
-
headers,
|
|
211
|
-
});
|
|
300
|
+
// Non-redirect: this branch is only reachable when the action request
|
|
301
|
+
// is missing the X-RSC-Router-Client-Path header (defensive). The
|
|
302
|
+
// client requires isPartial for action responses, so producing a full
|
|
303
|
+
// payload here would be rejected. Return 500 instead.
|
|
304
|
+
throw new Error(
|
|
305
|
+
`[RSC] matchPartial returned null for a non-redirect route ` +
|
|
306
|
+
`during action revalidation (${url.pathname}). This indicates ` +
|
|
307
|
+
`a malformed action request (missing X-RSC-Router-Client-Path header).`,
|
|
308
|
+
);
|
|
212
309
|
}
|
|
213
310
|
|
|
214
311
|
// Return updated segments
|
|
215
312
|
setRequestContextParams(matchResult.params, matchResult.routeName);
|
|
216
313
|
|
|
217
|
-
const serverTiming = matchResult.serverTiming;
|
|
218
|
-
|
|
219
314
|
const payload: RscPayload = {
|
|
220
315
|
metadata: {
|
|
221
316
|
pathname: url.pathname,
|
|
@@ -230,19 +325,24 @@ export async function handleServerAction<TEnv>(
|
|
|
230
325
|
returnValue,
|
|
231
326
|
};
|
|
232
327
|
|
|
328
|
+
attachLocationState(payload);
|
|
329
|
+
|
|
330
|
+
const renderStart = performance.now();
|
|
233
331
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
234
332
|
temporaryReferences,
|
|
235
333
|
});
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
334
|
+
const rscSerializeDur = performance.now() - renderStart;
|
|
335
|
+
// This measures synchronous stream creation, not end-to-end stream consumption.
|
|
336
|
+
appendMetric(metricsStore, "rsc-serialize", renderStart, rscSerializeDur);
|
|
337
|
+
appendMetric(
|
|
338
|
+
metricsStore,
|
|
339
|
+
"render:total",
|
|
340
|
+
renderStart,
|
|
341
|
+
performance.now() - renderStart,
|
|
342
|
+
);
|
|
243
343
|
|
|
244
344
|
return createResponseWithMergedHeaders(rscStream, {
|
|
245
345
|
status: actionStatus,
|
|
246
|
-
headers:
|
|
346
|
+
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
247
347
|
});
|
|
248
348
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSR Setup Utilities
|
|
3
|
+
*
|
|
4
|
+
* Manages early kickoff and retrieval of SSR module loading and stream mode
|
|
5
|
+
* resolution. Both operations are request-scoped but independent of route
|
|
6
|
+
* matching, so they can run in parallel with segment resolution.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { HandlerContext } from "./handler-context.js";
|
|
10
|
+
import type { SSRModule } from "./types.js";
|
|
11
|
+
import type { SSRStreamMode } from "../router/router-options.js";
|
|
12
|
+
import type { MetricsStore } from "../server/context.js";
|
|
13
|
+
import { appendMetric } from "../router/metrics.js";
|
|
14
|
+
import { _getRequestContext } from "../server/request-context.js";
|
|
15
|
+
|
|
16
|
+
export type SSRSetup = readonly [SSRModule, SSRStreamMode];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Key used to stash the early SSR setup promise on request variables.
|
|
20
|
+
* Read back via `getSSRSetup`.
|
|
21
|
+
*/
|
|
22
|
+
export const SSR_SETUP_VAR = "__ssrSetup";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Start loading the SSR module and resolving the stream mode in parallel.
|
|
26
|
+
* When a `getMetricsStore` getter is provided, records individual
|
|
27
|
+
* `ssr:module-load` and `ssr:stream-mode` metrics (the getter is called
|
|
28
|
+
* lazily so stores created after kickoff are still captured). Without a
|
|
29
|
+
* getter the promises run bare — no `.then()` microtasks, no
|
|
30
|
+
* `performance.now()` calls — keeping the non-debug hot path lean.
|
|
31
|
+
*/
|
|
32
|
+
export function startSSRSetup<TEnv>(
|
|
33
|
+
ctx: HandlerContext<TEnv>,
|
|
34
|
+
request: Request,
|
|
35
|
+
env: TEnv,
|
|
36
|
+
url: URL,
|
|
37
|
+
getMetricsStore?: () => MetricsStore | undefined,
|
|
38
|
+
): Promise<SSRSetup> {
|
|
39
|
+
if (!getMetricsStore) {
|
|
40
|
+
return Promise.all([
|
|
41
|
+
ctx.loadSSRModule(),
|
|
42
|
+
ctx.resolveStreamMode(request, env, url),
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
const start = performance.now();
|
|
46
|
+
return Promise.all([
|
|
47
|
+
ctx.loadSSRModule().then((mod) => {
|
|
48
|
+
appendMetric(
|
|
49
|
+
getMetricsStore(),
|
|
50
|
+
"ssr:module-load",
|
|
51
|
+
start,
|
|
52
|
+
performance.now() - start,
|
|
53
|
+
);
|
|
54
|
+
return mod;
|
|
55
|
+
}),
|
|
56
|
+
ctx.resolveStreamMode(request, env, url).then((mode) => {
|
|
57
|
+
appendMetric(
|
|
58
|
+
getMetricsStore(),
|
|
59
|
+
"ssr:stream-mode",
|
|
60
|
+
start,
|
|
61
|
+
performance.now() - start,
|
|
62
|
+
);
|
|
63
|
+
return mode;
|
|
64
|
+
}),
|
|
65
|
+
]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Retrieve the SSR setup result. Returns the early-kicked-off promise
|
|
70
|
+
* when available (stashed on request variables), otherwise starts a
|
|
71
|
+
* fresh setup.
|
|
72
|
+
*/
|
|
73
|
+
export function getSSRSetup<TEnv>(
|
|
74
|
+
ctx: HandlerContext<TEnv>,
|
|
75
|
+
request: Request,
|
|
76
|
+
env: TEnv,
|
|
77
|
+
url: URL,
|
|
78
|
+
metricsStore: MetricsStore | undefined,
|
|
79
|
+
): Promise<SSRSetup> {
|
|
80
|
+
const early = _getRequestContext()?.var?.[SSR_SETUP_VAR] as
|
|
81
|
+
| Promise<SSRSetup>
|
|
82
|
+
| undefined;
|
|
83
|
+
if (early) return early;
|
|
84
|
+
return startSSRSetup(
|
|
85
|
+
ctx,
|
|
86
|
+
request,
|
|
87
|
+
env,
|
|
88
|
+
url,
|
|
89
|
+
metricsStore ? () => metricsStore : undefined,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Classify whether a request may require SSR (HTML rendering).
|
|
95
|
+
*
|
|
96
|
+
* Returns false for requests that are definitively RSC-only, loader fetches,
|
|
97
|
+
* prerender collection, or Accept-based RSC (no text/html). This mirrors
|
|
98
|
+
* the isRscRequest decision in rsc-rendering.ts.
|
|
99
|
+
*
|
|
100
|
+
* Note: response/mime routes are excluded by the caller — this function
|
|
101
|
+
* runs after previewMatch() classifies the route type.
|
|
102
|
+
*/
|
|
103
|
+
export function mayNeedSSR(request: Request, url: URL): boolean {
|
|
104
|
+
if (
|
|
105
|
+
url.searchParams.has("_rsc_partial") ||
|
|
106
|
+
url.searchParams.has("_rsc_action") ||
|
|
107
|
+
request.headers.has("rsc-action") ||
|
|
108
|
+
url.searchParams.has("_rsc_loader") ||
|
|
109
|
+
url.searchParams.has("__rsc") ||
|
|
110
|
+
url.searchParams.has("__prerender_collect")
|
|
111
|
+
) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Mirror the Accept-based RSC decision from rsc-rendering.ts:
|
|
116
|
+
// if Accept is present and does not include text/html (and no __html override),
|
|
117
|
+
// the response will be RSC, not HTML.
|
|
118
|
+
const accept = request.headers.get("accept");
|
|
119
|
+
if (
|
|
120
|
+
accept &&
|
|
121
|
+
!accept.includes("text/html") &&
|
|
122
|
+
!url.searchParams.has("__html")
|
|
123
|
+
) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return true;
|
|
128
|
+
}
|