@rangojs/router 0.0.0-experimental.0f44aca1
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/AGENTS.md +5 -0
- package/README.md +899 -0
- package/dist/bin/rango.js +1601 -0
- package/dist/vite/index.js +5214 -0
- package/package.json +176 -0
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +220 -0
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +112 -0
- package/skills/document-cache/SKILL.md +182 -0
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +704 -0
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +313 -0
- package/skills/layout/SKILL.md +310 -0
- package/skills/links/SKILL.md +239 -0
- package/skills/loader/SKILL.md +596 -0
- package/skills/middleware/SKILL.md +339 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +305 -0
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +118 -0
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +385 -0
- package/skills/router-setup/SKILL.md +439 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +79 -0
- package/skills/typesafety/SKILL.md +623 -0
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +273 -0
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +899 -0
- package/src/browser/history-state.ts +80 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +141 -0
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +134 -0
- package/src/browser/navigation-bridge.ts +645 -0
- package/src/browser/navigation-client.ts +215 -0
- package/src/browser/navigation-store.ts +806 -0
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +550 -0
- package/src/browser/prefetch/cache.ts +146 -0
- package/src/browser/prefetch/fetch.ts +135 -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 +360 -0
- package/src/browser/react/NavigationProvider.tsx +386 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +59 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +162 -0
- package/src/browser/react/location-state.ts +107 -0
- package/src/browser/react/mount-context.ts +37 -0
- 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 +218 -0
- package/src/browser/react/use-client-cache.ts +58 -0
- package/src/browser/react/use-handle.ts +162 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +135 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +99 -0
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +63 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +171 -0
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +431 -0
- package/src/browser/scroll-restoration.ts +400 -0
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +667 -0
- package/src/browser/shallow.ts +40 -0
- package/src/browser/types.ts +538 -0
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +438 -0
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +35 -0
- package/src/build/route-trie.ts +265 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +411 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +469 -0
- package/src/build/route-types/scan-filter.ts +78 -0
- package/src/build/runtime-discovery.ts +231 -0
- 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 +338 -0
- package/src/cache/cache-scope.ts +382 -0
- package/src/cache/cf/cf-cache-store.ts +540 -0
- package/src/cache/cf/index.ts +25 -0
- package/src/cache/document-cache.ts +369 -0
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +43 -0
- package/src/cache/memory-segment-store.ts +328 -0
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +98 -0
- package/src/cache/types.ts +342 -0
- package/src/client.rsc.tsx +85 -0
- package/src/client.tsx +601 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +27 -0
- package/src/context-var.ts +86 -0
- package/src/debug.ts +243 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +365 -0
- package/src/handle.ts +135 -0
- package/src/handles/MetaTags.tsx +246 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +7 -0
- package/src/handles/meta.ts +264 -0
- package/src/host/cookie-handler.ts +165 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +53 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +352 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +146 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +222 -0
- package/src/index.rsc.ts +233 -0
- package/src/index.ts +277 -0
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +89 -0
- package/src/loader.ts +64 -0
- package/src/network-error-thrower.tsx +23 -0
- package/src/outlet-context.ts +15 -0
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +185 -0
- package/src/prerender.ts +463 -0
- package/src/reverse.ts +330 -0
- package/src/root-error-boundary.tsx +289 -0
- package/src/route-content-wrapper.tsx +196 -0
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -0
- package/src/route-map-builder.ts +275 -0
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +259 -0
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/find-match.ts +158 -0
- package/src/router/handler-context.ts +451 -0
- package/src/router/intercept-resolution.ts +395 -0
- package/src/router/lazy-includes.ts +234 -0
- package/src/router/loader-resolution.ts +420 -0
- package/src/router/logging.ts +248 -0
- package/src/router/manifest.ts +267 -0
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +266 -0
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +223 -0
- package/src/router/match-middleware/cache-lookup.ts +634 -0
- package/src/router/match-middleware/cache-store.ts +295 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +306 -0
- package/src/router/match-middleware/segment-resolution.ts +192 -0
- package/src/router/match-pipelines.ts +179 -0
- package/src/router/match-result.ts +219 -0
- package/src/router/metrics.ts +282 -0
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +748 -0
- package/src/router/pattern-matching.ts +563 -0
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +289 -0
- package/src/router/router-context.ts +316 -0
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1239 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +289 -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 +239 -0
- package/src/router/types.ts +170 -0
- package/src/router.ts +1002 -0
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +1089 -0
- package/src/rsc/helpers.ts +198 -0
- package/src/rsc/index.ts +36 -0
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +32 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +235 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +263 -0
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +454 -0
- package/src/server/context.ts +591 -0
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +308 -0
- package/src/server/loader-registry.ts +133 -0
- package/src/server/request-context.ts +914 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +51 -0
- package/src/ssr/index.tsx +365 -0
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +297 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +62 -0
- package/src/theme/index.ts +48 -0
- package/src/theme/theme-context.ts +44 -0
- package/src/theme/theme-script.ts +155 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +102 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -0
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -0
- package/src/use-loader.tsx +354 -0
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +110 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +16 -0
- package/src/vite/plugin-types.ts +131 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/plugins/expose-action-id.ts +365 -0
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +254 -0
- package/src/vite/plugins/version.d.ts +12 -0
- package/src/vite/plugins/virtual-entries.ts +123 -0
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +510 -0
- package/src/vite/router-discovery.ts +785 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/utils/package-resolution.ts +121 -0
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Action Handler
|
|
3
|
+
*
|
|
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.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
requireRequestContext,
|
|
20
|
+
setRequestContextParams,
|
|
21
|
+
getLocationState,
|
|
22
|
+
} from "../server/request-context.js";
|
|
23
|
+
import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
|
|
24
|
+
import { appendMetric } from "../router/metrics.js";
|
|
25
|
+
import type { RscPayload } from "./types.js";
|
|
26
|
+
import {
|
|
27
|
+
hasBodyContent,
|
|
28
|
+
createResponseWithMergedHeaders,
|
|
29
|
+
createSimpleRedirectResponse,
|
|
30
|
+
carryOverRedirectHeaders,
|
|
31
|
+
} from "./helpers.js";
|
|
32
|
+
import type { HandlerContext } from "./handler-context.js";
|
|
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
|
+
/**
|
|
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>(
|
|
73
|
+
ctx: HandlerContext<TEnv>,
|
|
74
|
+
request: Request,
|
|
75
|
+
env: TEnv,
|
|
76
|
+
url: URL,
|
|
77
|
+
actionId: string,
|
|
78
|
+
handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
79
|
+
): Promise<Response | ActionContinuation> {
|
|
80
|
+
const temporaryReferences = ctx.createTemporaryReferenceSet();
|
|
81
|
+
|
|
82
|
+
// Decode action arguments from request body
|
|
83
|
+
const contentType = request.headers.get("content-type") || "";
|
|
84
|
+
let args: unknown[] = [];
|
|
85
|
+
let actionFormData: FormData | undefined;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const body = contentType.includes("multipart/form-data")
|
|
89
|
+
? await request.formData()
|
|
90
|
+
: await request.text();
|
|
91
|
+
|
|
92
|
+
if (body instanceof FormData) {
|
|
93
|
+
actionFormData = body;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (hasBodyContent(body)) {
|
|
97
|
+
args = await ctx.decodeReply(body, { temporaryReferences });
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
throw new Error(`Failed to decode action arguments: ${error}`, {
|
|
101
|
+
cause: error,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Execute the server action
|
|
106
|
+
let returnValue: { ok: boolean; data: unknown };
|
|
107
|
+
let actionStatus = 200;
|
|
108
|
+
let loadedAction: Function | undefined;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
loadedAction = await ctx.loadServerAction(actionId);
|
|
112
|
+
const data = await loadedAction!.apply(null, args);
|
|
113
|
+
|
|
114
|
+
// Intercept redirect responses from actions. Without this, the redirect
|
|
115
|
+
// Response would be serialized as the action returnValue (which fails)
|
|
116
|
+
// and the revalidation step would run unnecessarily.
|
|
117
|
+
if (data instanceof Response) {
|
|
118
|
+
const redirectUrl = data.headers.get("Location");
|
|
119
|
+
const isRedirect = data.status >= 300 && data.status < 400 && redirectUrl;
|
|
120
|
+
if (isRedirect) {
|
|
121
|
+
const locationState = getLocationState();
|
|
122
|
+
let redirect: Response;
|
|
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
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
returnValue = { ok: true, data };
|
|
137
|
+
} catch (error) {
|
|
138
|
+
// Handle thrown redirect (e.g., throw redirect('/path'))
|
|
139
|
+
if (error instanceof Response) {
|
|
140
|
+
const redirectUrl = error.headers.get("Location");
|
|
141
|
+
const isRedirect =
|
|
142
|
+
error.status >= 300 && error.status < 400 && redirectUrl;
|
|
143
|
+
if (isRedirect) {
|
|
144
|
+
const locationState = getLocationState();
|
|
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
|
+
}
|
|
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
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
returnValue = { ok: false, data: error };
|
|
172
|
+
actionStatus = 500;
|
|
173
|
+
|
|
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
|
+
}
|
|
196
|
+
|
|
197
|
+
ctx.callOnError(error, "action", {
|
|
198
|
+
request,
|
|
199
|
+
url,
|
|
200
|
+
env,
|
|
201
|
+
actionId,
|
|
202
|
+
handledByBoundary: !!errorResult,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (errorResult) {
|
|
206
|
+
setRequestContextParams(errorResult.params, errorResult.routeName);
|
|
207
|
+
|
|
208
|
+
const payload: RscPayload = {
|
|
209
|
+
metadata: {
|
|
210
|
+
pathname: url.pathname,
|
|
211
|
+
segments: errorResult.segments,
|
|
212
|
+
isPartial: true,
|
|
213
|
+
matched: errorResult.matched,
|
|
214
|
+
diff: errorResult.diff,
|
|
215
|
+
isError: true,
|
|
216
|
+
handles: handleStore.stream(),
|
|
217
|
+
version: ctx.version,
|
|
218
|
+
},
|
|
219
|
+
returnValue,
|
|
220
|
+
};
|
|
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
|
+
|
|
226
|
+
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
227
|
+
temporaryReferences,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
return createResponseWithMergedHeaders(rscStream, {
|
|
231
|
+
status: actionStatus,
|
|
232
|
+
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Build continuation for the revalidation phase
|
|
238
|
+
const resolvedActionId =
|
|
239
|
+
(loadedAction as { $id?: string; $$id?: string } | undefined)?.$id ??
|
|
240
|
+
(loadedAction as { $$id?: string } | undefined)?.$$id ??
|
|
241
|
+
actionId;
|
|
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
|
+
},
|
|
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;
|
|
280
|
+
|
|
281
|
+
const matchResult = await ctx.router.matchPartial(
|
|
282
|
+
request,
|
|
283
|
+
{ env },
|
|
284
|
+
actionContext,
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
if (!matchResult) {
|
|
288
|
+
// matchPartial returns null when the route is a redirect or the request
|
|
289
|
+
// is missing required headers (previousUrl). Check for redirect first.
|
|
290
|
+
const fullMatch = await ctx.router.match(request, { env });
|
|
291
|
+
setRequestContextParams(fullMatch.params, fullMatch.routeName);
|
|
292
|
+
|
|
293
|
+
if (fullMatch.redirect) {
|
|
294
|
+
// Action context is always partial — use X-RSC-Redirect header so
|
|
295
|
+
// the client can perform SPA navigation instead of fetch auto-following
|
|
296
|
+
// a raw 308 to a URL that would render full HTML.
|
|
297
|
+
return createSimpleRedirectResponse(fullMatch.redirect);
|
|
298
|
+
}
|
|
299
|
+
|
|
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
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Return updated segments
|
|
312
|
+
setRequestContextParams(matchResult.params, matchResult.routeName);
|
|
313
|
+
|
|
314
|
+
const payload: RscPayload = {
|
|
315
|
+
metadata: {
|
|
316
|
+
pathname: url.pathname,
|
|
317
|
+
segments: matchResult.segments,
|
|
318
|
+
isPartial: true,
|
|
319
|
+
matched: matchResult.matched,
|
|
320
|
+
diff: matchResult.diff,
|
|
321
|
+
slots: matchResult.slots,
|
|
322
|
+
handles: handleStore.stream(),
|
|
323
|
+
version: ctx.version,
|
|
324
|
+
},
|
|
325
|
+
returnValue,
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
attachLocationState(payload);
|
|
329
|
+
|
|
330
|
+
const renderStart = performance.now();
|
|
331
|
+
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
332
|
+
temporaryReferences,
|
|
333
|
+
});
|
|
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
|
+
);
|
|
343
|
+
|
|
344
|
+
return createResponseWithMergedHeaders(rscStream, {
|
|
345
|
+
status: actionStatus,
|
|
346
|
+
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
347
|
+
});
|
|
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
|
+
}
|