@rangojs/router 0.0.0-experimental.20 → 0.0.0-experimental.20dbba0c
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 +4 -0
- package/README.md +172 -50
- package/dist/bin/rango.js +138 -50
- package/dist/vite/index.js +1160 -508
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +17 -16
- package/skills/breadcrumbs/SKILL.md +252 -0
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +49 -8
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +61 -51
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +20 -0
- package/skills/layout/SKILL.md +22 -0
- package/skills/links/SKILL.md +91 -17
- package/skills/loader/SKILL.md +107 -24
- package/skills/middleware/SKILL.md +34 -3
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +765 -0
- package/skills/parallel/SKILL.md +185 -0
- package/skills/prerender/SKILL.md +112 -70
- package/skills/rango/SKILL.md +24 -23
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/route/SKILL.md +58 -4
- package/skills/router-setup/SKILL.md +95 -5
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +38 -24
- package/src/__internal.ts +92 -0
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +175 -17
- package/src/browser/navigation-client.ts +177 -44
- package/src/browser/navigation-store.ts +68 -9
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +113 -17
- package/src/browser/prefetch/cache.ts +275 -28
- package/src/browser/prefetch/fetch.ts +191 -46
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +123 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +98 -14
- package/src/browser/react/NavigationProvider.tsx +89 -14
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +11 -1
- package/src/browser/react/use-router.ts +29 -9
- package/src/browser/rsc-router.tsx +177 -66
- package/src/browser/scroll-restoration.ts +41 -42
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +73 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-trie.ts +67 -25
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +223 -74
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +48 -7
- package/src/cache/cf/cf-cache-store.ts +455 -15
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.rsc.tsx +2 -1
- package/src/client.tsx +85 -276
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/handle.ts +40 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/host/index.ts +0 -3
- package/src/index.rsc.ts +9 -36
- package/src/index.ts +79 -70
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +57 -15
- package/src/prerender.ts +138 -77
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +27 -2
- package/src/route-definition/dsl-helpers.ts +240 -40
- package/src/route-definition/helpers-types.ts +67 -19
- package/src/route-definition/index.ts +3 -3
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +18 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +129 -26
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +10 -7
- package/src/router/loader-resolution.ts +160 -22
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +31 -16
- package/src/router/match-api.ts +128 -193
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +94 -17
- package/src/router/match-middleware/cache-store.ts +53 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +61 -5
- package/src/router/match-result.ts +103 -18
- package/src/router/metrics.ts +238 -13
- package/src/router/middleware-types.ts +48 -27
- package/src/router/middleware.ts +201 -86
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +77 -11
- package/src/router/prerender-match.ts +114 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +27 -7
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +50 -5
- package/src/router/router-options.ts +50 -19
- package/src/router/segment-resolution/fresh.ts +215 -19
- package/src/router/segment-resolution/helpers.ts +30 -25
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +454 -301
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/trie-matching.ts +30 -6
- package/src/router/types.ts +1 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +89 -17
- package/src/rsc/handler.ts +563 -364
- package/src/rsc/helpers.ts +69 -41
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +37 -10
- package/src/rsc/response-route-handler.ts +14 -1
- package/src/rsc/rsc-rendering.ts +47 -44
- package/src/rsc/server-action.ts +24 -10
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +11 -1
- package/src/search-params.ts +16 -13
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +109 -23
- package/src/server/context.ts +174 -19
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +218 -65
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +4 -0
- package/src/static-handler.ts +18 -6
- package/src/theme/index.ts +4 -13
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +140 -72
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-config.ts +17 -8
- package/src/types/route-entry.ts +19 -1
- package/src/types/segments.ts +2 -5
- package/src/urls/include-helper.ts +24 -14
- package/src/urls/path-helper-types.ts +39 -6
- package/src/urls/path-helper.ts +48 -13
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +18 -16
- package/src/use-loader.tsx +77 -5
- package/src/vite/discovery/bundle-postprocess.ts +61 -89
- package/src/vite/discovery/discover-routers.ts +7 -4
- package/src/vite/discovery/prerender-collection.ts +162 -88
- package/src/vite/discovery/state.ts +17 -13
- package/src/vite/index.ts +8 -3
- package/src/vite/plugin-types.ts +51 -79
- 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 +1 -3
- package/src/vite/plugins/expose-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
- package/src/vite/plugins/expose-internal-ids.ts +257 -40
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +190 -217
- package/src/vite/router-discovery.ts +241 -45
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/package-resolution.ts +34 -1
- package/src/vite/utils/prerender-utils.ts +97 -5
- package/src/vite/utils/shared-utils.ts +3 -2
- package/skills/testing/SKILL.md +0 -226
- package/src/route-definition/route-function.ts +0 -119
package/src/rsc/helpers.ts
CHANGED
|
@@ -8,9 +8,49 @@ import {
|
|
|
8
8
|
_getRequestContext,
|
|
9
9
|
getLocationState,
|
|
10
10
|
} from "../server/request-context.js";
|
|
11
|
+
import type { RequestContext } from "../server/request-context.js";
|
|
11
12
|
import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
|
|
12
13
|
import type { MiddlewareEntry, MiddlewareFn } from "../router/middleware.js";
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Copy stub headers from the request context onto a target Headers instance:
|
|
17
|
+
* append Set-Cookie entries, set everything else only if absent. Header
|
|
18
|
+
* mutation failures are swallowed so the same logic works against Response
|
|
19
|
+
* headers that may be immutable (e.g. Cloudflare protocol-switch responses).
|
|
20
|
+
*/
|
|
21
|
+
function applyStubHeaders(target: Headers, stub: Headers): void {
|
|
22
|
+
stub.forEach((value, name) => {
|
|
23
|
+
try {
|
|
24
|
+
if (name.toLowerCase() === "set-cookie") {
|
|
25
|
+
target.append(name, value);
|
|
26
|
+
} else if (!target.has(name)) {
|
|
27
|
+
target.set(name, value);
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// Headers immutable — skip.
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Drain ctx._onResponseCallbacks onto a response. Swapping the array before
|
|
37
|
+
* iteration prevents re-entrant registrations from double-firing and matches
|
|
38
|
+
* the contract that each callback runs at most once per request.
|
|
39
|
+
*/
|
|
40
|
+
function drainOnResponseCallbacks(
|
|
41
|
+
ctx: RequestContext,
|
|
42
|
+
response: Response,
|
|
43
|
+
): Response {
|
|
44
|
+
const callbacks = ctx._onResponseCallbacks;
|
|
45
|
+
if (callbacks.length === 0) return response;
|
|
46
|
+
ctx._onResponseCallbacks = [];
|
|
47
|
+
let result = response;
|
|
48
|
+
for (const callback of callbacks) {
|
|
49
|
+
result = callback(result) ?? result;
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
14
54
|
/**
|
|
15
55
|
* Check if a request body has content to decode
|
|
16
56
|
*/
|
|
@@ -39,40 +79,23 @@ export function createResponseWithMergedHeaders(
|
|
|
39
79
|
return new Response(body, init);
|
|
40
80
|
}
|
|
41
81
|
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
// merge points (e.g. executeMiddleware) do not duplicate them.
|
|
82
|
+
// Delete Set-Cookie from the stub after consuming so downstream merge
|
|
83
|
+
// points (e.g. executeMiddleware) don't duplicate them.
|
|
45
84
|
const mergedHeaders = new Headers(init.headers);
|
|
46
|
-
ctx.res.headers
|
|
47
|
-
if (name.toLowerCase() === "set-cookie") {
|
|
48
|
-
mergedHeaders.append(name, value);
|
|
49
|
-
} else if (!mergedHeaders.has(name)) {
|
|
50
|
-
// Only set if not already present in init.headers
|
|
51
|
-
mergedHeaders.set(name, value);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
85
|
+
applyStubHeaders(mergedHeaders, ctx.res.headers);
|
|
54
86
|
ctx.res.headers.delete("set-cookie");
|
|
55
87
|
|
|
56
|
-
//
|
|
57
|
-
//
|
|
88
|
+
// ctx.res.status overrides init.status when explicitly set (e.g. 404 for
|
|
89
|
+
// notFound, 500 for error). Default ctx.res.status is 200.
|
|
58
90
|
const status = ctx.res.status !== 200 ? ctx.res.status : init.status;
|
|
59
91
|
|
|
60
|
-
|
|
92
|
+
const response = new Response(body, {
|
|
61
93
|
...init,
|
|
62
94
|
status,
|
|
63
95
|
headers: mergedHeaders,
|
|
64
96
|
});
|
|
65
97
|
|
|
66
|
-
|
|
67
|
-
// Drain the array so that downstream callers (e.g. finalizeResponse)
|
|
68
|
-
// do not re-execute the same callbacks on this response.
|
|
69
|
-
const callbacks = ctx._onResponseCallbacks;
|
|
70
|
-
ctx._onResponseCallbacks = [];
|
|
71
|
-
for (const callback of callbacks) {
|
|
72
|
-
response = callback(response) ?? response;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return response;
|
|
98
|
+
return drainOnResponseCallbacks(ctx, response);
|
|
76
99
|
}
|
|
77
100
|
|
|
78
101
|
/**
|
|
@@ -175,24 +198,29 @@ export function buildRouteMiddlewareEntries<TEnv>(
|
|
|
175
198
|
}
|
|
176
199
|
|
|
177
200
|
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
201
|
+
* Merge stub headers from the request context onto an existing Response in
|
|
202
|
+
* place, then drain onResponse callbacks. Used when a Response cannot flow
|
|
203
|
+
* through `new Response()` — status 101 is outside the constructor's
|
|
204
|
+
* 200-599 range, and the Cloudflare-specific `webSocket` property would be
|
|
205
|
+
* lost on reconstruction.
|
|
183
206
|
*/
|
|
184
|
-
export function
|
|
207
|
+
export function mergeStubHeadersAndFinalize(response: Response): Response {
|
|
185
208
|
const ctx = _getRequestContext();
|
|
186
|
-
if (!ctx
|
|
187
|
-
return response;
|
|
188
|
-
}
|
|
209
|
+
if (!ctx) return response;
|
|
189
210
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
211
|
+
applyStubHeaders(response.headers, ctx.res.headers);
|
|
212
|
+
ctx.res.headers.delete("set-cookie");
|
|
213
|
+
|
|
214
|
+
return drainOnResponseCallbacks(ctx, response);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Run onResponse callbacks on an existing Response. Used by code paths that
|
|
219
|
+
* bypass createResponseWithMergedHeaders (e.g. middleware short-circuits)
|
|
220
|
+
* but still need ctx.onResponse() callbacks to fire.
|
|
221
|
+
*/
|
|
222
|
+
export function finalizeResponse(response: Response): Response {
|
|
223
|
+
const ctx = _getRequestContext();
|
|
224
|
+
if (!ctx) return response;
|
|
225
|
+
return drainOnResponseCallbacks(ctx, response);
|
|
198
226
|
}
|
package/src/rsc/index.ts
CHANGED
|
@@ -29,28 +29,8 @@ export type {
|
|
|
29
29
|
NonceProvider,
|
|
30
30
|
} from "./types.js";
|
|
31
31
|
|
|
32
|
-
// Re-export HandleStore types for consumers who need custom handling
|
|
33
|
-
export {
|
|
34
|
-
createHandleStore,
|
|
35
|
-
type HandleStore,
|
|
36
|
-
type HandleData,
|
|
37
|
-
} from "../server/handle-store.js";
|
|
38
|
-
|
|
39
32
|
// Re-export request context utilities for server-side access to env/request/params
|
|
40
33
|
export {
|
|
41
34
|
getRequestContext,
|
|
42
35
|
requireRequestContext,
|
|
43
|
-
setRequestContextParams,
|
|
44
36
|
} from "../server/request-context.js";
|
|
45
|
-
|
|
46
|
-
// Re-export cache store types and implementations
|
|
47
|
-
export type {
|
|
48
|
-
SegmentCacheStore,
|
|
49
|
-
CachedEntryData,
|
|
50
|
-
CachedEntryResult,
|
|
51
|
-
SegmentCacheProvider,
|
|
52
|
-
SegmentHandleData,
|
|
53
|
-
} from "../cache/types.js";
|
|
54
|
-
|
|
55
|
-
export { MemorySegmentCacheStore } from "../cache/memory-segment-store.js";
|
|
56
|
-
export { CFCacheStore, type CFCacheStoreOptions } from "../cache/cf/index.js";
|
package/src/rsc/loader-fetch.ts
CHANGED
|
@@ -168,8 +168,19 @@ export async function handleLoaderFetch<TEnv>(
|
|
|
168
168
|
loaderResult: unknown;
|
|
169
169
|
}
|
|
170
170
|
const loaderPayload: LoaderPayload = { loaderResult: result };
|
|
171
|
-
const rscStream =
|
|
172
|
-
|
|
171
|
+
const rscStream = ctx.renderToReadableStream<LoaderPayload>(
|
|
172
|
+
loaderPayload,
|
|
173
|
+
{
|
|
174
|
+
onError: (error: unknown) => {
|
|
175
|
+
ctx.callOnError(error, "rendering", {
|
|
176
|
+
request,
|
|
177
|
+
url,
|
|
178
|
+
env,
|
|
179
|
+
loaderName: loaderId,
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
);
|
|
173
184
|
|
|
174
185
|
return createResponseWithMergedHeaders(rscStream, {
|
|
175
186
|
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
@@ -199,7 +210,16 @@ export async function handleLoaderFetch<TEnv>(
|
|
|
199
210
|
name: err.name,
|
|
200
211
|
},
|
|
201
212
|
};
|
|
202
|
-
const rscStream = ctx.renderToReadableStream(errorPayload
|
|
213
|
+
const rscStream = ctx.renderToReadableStream(errorPayload, {
|
|
214
|
+
onError: (error: unknown) => {
|
|
215
|
+
ctx.callOnError(error, "rendering", {
|
|
216
|
+
request,
|
|
217
|
+
url,
|
|
218
|
+
env,
|
|
219
|
+
loaderName: loaderId,
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
});
|
|
203
223
|
|
|
204
224
|
return createResponseWithMergedHeaders(rscStream, {
|
|
205
225
|
status: 500,
|
package/src/rsc/manifest-init.ts
CHANGED
|
@@ -31,7 +31,11 @@ export async function buildRouterTrieFromUrlpatterns(
|
|
|
31
31
|
): Promise<void> {
|
|
32
32
|
const { generateManifestFull } =
|
|
33
33
|
await import("../build/generate-manifest.js");
|
|
34
|
-
const generated = generateManifestFull(
|
|
34
|
+
const generated = generateManifestFull(
|
|
35
|
+
router.urlpatterns,
|
|
36
|
+
undefined,
|
|
37
|
+
router.basename ? { urlPrefix: router.basename } : undefined,
|
|
38
|
+
);
|
|
35
39
|
if (
|
|
36
40
|
generated._routeAncestry &&
|
|
37
41
|
Object.keys(generated._routeAncestry).length > 0
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
requireRequestContext,
|
|
11
11
|
setRequestContextParams,
|
|
12
12
|
} from "../server/request-context.js";
|
|
13
|
+
import { getSSRSetup } from "./ssr-setup.js";
|
|
13
14
|
import type { MiddlewareFn } from "../router/middleware.js";
|
|
14
15
|
import { executeMiddleware } from "../router/middleware.js";
|
|
15
16
|
import type { RscPayload, ReactFormState } from "./types.js";
|
|
@@ -242,9 +243,12 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
242
243
|
const payload: RscPayload = {
|
|
243
244
|
metadata: {
|
|
244
245
|
pathname: url.pathname,
|
|
246
|
+
routerId: ctx.router.id,
|
|
247
|
+
basename: ctx.router.basename,
|
|
245
248
|
segments: match.segments,
|
|
246
249
|
matched: match.matched,
|
|
247
250
|
diff: match.diff,
|
|
251
|
+
params: match.params,
|
|
248
252
|
isPartial: false,
|
|
249
253
|
rootLayout: ctx.router.rootLayout,
|
|
250
254
|
handles: handleStore.stream(),
|
|
@@ -256,11 +260,21 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
256
260
|
formState: actionResult,
|
|
257
261
|
};
|
|
258
262
|
|
|
259
|
-
const rscStream = ctx.renderToReadableStream<RscPayload>(payload
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
263
|
+
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
264
|
+
onError: (error: unknown) => {
|
|
265
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
// metricsStore=undefined is safe: the handler already stashed the early
|
|
269
|
+
// SSR setup promise on request variables, so getSSRSetup returns it
|
|
270
|
+
// without falling back to a fresh startSSRSetup.
|
|
271
|
+
const [ssrModule, streamMode] = await getSSRSetup(
|
|
272
|
+
ctx,
|
|
273
|
+
request,
|
|
274
|
+
env,
|
|
275
|
+
url,
|
|
276
|
+
undefined,
|
|
277
|
+
);
|
|
264
278
|
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
265
279
|
formState: reactFormState,
|
|
266
280
|
nonce,
|
|
@@ -335,9 +349,12 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
335
349
|
const payload: RscPayload = {
|
|
336
350
|
metadata: {
|
|
337
351
|
pathname: url.pathname,
|
|
352
|
+
routerId: ctx.router.id,
|
|
353
|
+
basename: ctx.router.basename,
|
|
338
354
|
segments: errorResult.segments,
|
|
339
355
|
matched: errorResult.matched,
|
|
340
356
|
diff: errorResult.diff,
|
|
357
|
+
params: errorResult.params,
|
|
341
358
|
isPartial: false,
|
|
342
359
|
isError: true,
|
|
343
360
|
rootLayout: ctx.router.rootLayout,
|
|
@@ -349,11 +366,21 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
349
366
|
},
|
|
350
367
|
};
|
|
351
368
|
|
|
352
|
-
const rscStream = ctx.renderToReadableStream<RscPayload>(payload
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
369
|
+
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
370
|
+
onError: (error: unknown) => {
|
|
371
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
// metricsStore=undefined is safe: the handler already stashed the early
|
|
375
|
+
// SSR setup promise on request variables, so getSSRSetup returns it
|
|
376
|
+
// without falling back to a fresh startSSRSetup.
|
|
377
|
+
const [ssrModule, streamMode] = await getSSRSetup(
|
|
378
|
+
ctx,
|
|
379
|
+
request,
|
|
380
|
+
env,
|
|
381
|
+
url,
|
|
382
|
+
undefined,
|
|
383
|
+
);
|
|
357
384
|
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
358
385
|
nonce,
|
|
359
386
|
streamMode,
|
|
@@ -26,7 +26,9 @@ import {
|
|
|
26
26
|
finalizeResponse,
|
|
27
27
|
isCacheableStatus,
|
|
28
28
|
buildRouteMiddlewareEntries,
|
|
29
|
+
mergeStubHeadersAndFinalize,
|
|
29
30
|
} from "./helpers.js";
|
|
31
|
+
import { isWebSocketUpgradeResponse } from "../response-utils.js";
|
|
30
32
|
|
|
31
33
|
export interface ResponseRouteMatch {
|
|
32
34
|
responseType: string;
|
|
@@ -78,10 +80,13 @@ export async function handleResponseRoute<TEnv>(
|
|
|
78
80
|
env,
|
|
79
81
|
searchParams: cleanUrl.searchParams,
|
|
80
82
|
url: cleanUrl,
|
|
83
|
+
originalUrl: reqCtx.originalUrl,
|
|
81
84
|
pathname: url.pathname,
|
|
82
85
|
reverse: createReverseFunction(handlerCtx.getRequiredRouteMap()),
|
|
83
86
|
get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
|
|
84
87
|
header: (name: string, value: string) => reqCtx.header(name, value),
|
|
88
|
+
waitUntil: reqCtx.waitUntil.bind(reqCtx),
|
|
89
|
+
executionContext: reqCtx.executionContext,
|
|
85
90
|
_responseType: preview.responseType,
|
|
86
91
|
};
|
|
87
92
|
// Brand with taint symbol so "use cache" detects it as request-scoped
|
|
@@ -96,6 +101,12 @@ export async function handleResponseRoute<TEnv>(
|
|
|
96
101
|
// so that stub headers (cookies, custom headers set via ctx.header()) are included.
|
|
97
102
|
// Use Headers (not Record<string, string>) to preserve duplicate entries like Set-Cookie.
|
|
98
103
|
const rewrapResponse = (result: Response) => {
|
|
104
|
+
// 204/205/304 are NOT short-circuited — they're valid for the Response
|
|
105
|
+
// constructor and must honor ctx.setStatus() overrides. Only upgrade
|
|
106
|
+
// responses (status 101 / `webSocket` property) bypass reconstruction.
|
|
107
|
+
if (isWebSocketUpgradeResponse(result)) {
|
|
108
|
+
return mergeStubHeadersAndFinalize(result);
|
|
109
|
+
}
|
|
99
110
|
const headers = new Headers();
|
|
100
111
|
result.headers.forEach((value, key) => {
|
|
101
112
|
if (key.toLowerCase() === "set-cookie") {
|
|
@@ -196,7 +207,9 @@ export async function handleResponseRoute<TEnv>(
|
|
|
196
207
|
// Wrap callHandler to append Vary: Accept on content-negotiated responses
|
|
197
208
|
const callHandlerWithVary = async () => {
|
|
198
209
|
const response = await callHandler();
|
|
199
|
-
if (preview.negotiated) {
|
|
210
|
+
if (preview.negotiated && !isWebSocketUpgradeResponse(response)) {
|
|
211
|
+
// Skip Vary on upgrade responses: headers are semantically immutable
|
|
212
|
+
// on some runtimes, and Vary is meaningless for a 101 response.
|
|
200
213
|
response.headers.append("Vary", "Accept");
|
|
201
214
|
}
|
|
202
215
|
return response;
|
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,9 @@ 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 serverTiming: string | undefined;
|
|
38
36
|
let hasInterceptSlots = false;
|
|
39
37
|
|
|
40
38
|
if (isPartial) {
|
|
@@ -53,11 +51,11 @@ export async function handleRscRendering<TEnv>(
|
|
|
53
51
|
return createSimpleRedirectResponse(match.redirect);
|
|
54
52
|
}
|
|
55
53
|
|
|
56
|
-
serverTiming = match.serverTiming;
|
|
57
|
-
|
|
58
54
|
payload = {
|
|
59
55
|
metadata: {
|
|
60
56
|
pathname: url.pathname,
|
|
57
|
+
routerId: ctx.router.id,
|
|
58
|
+
basename: ctx.router.basename,
|
|
61
59
|
segments: match.segments,
|
|
62
60
|
matched: match.matched,
|
|
63
61
|
diff: match.diff,
|
|
@@ -66,18 +64,20 @@ export async function handleRscRendering<TEnv>(
|
|
|
66
64
|
rootLayout: ctx.router.rootLayout,
|
|
67
65
|
handles: handleStore.stream(),
|
|
68
66
|
version: ctx.version,
|
|
67
|
+
prefetchCacheTTL: ctx.router.prefetchCacheTTL,
|
|
69
68
|
themeConfig: ctx.router.themeConfig,
|
|
70
69
|
initialTheme: reqCtx.theme,
|
|
71
70
|
},
|
|
72
71
|
};
|
|
73
72
|
} else {
|
|
74
73
|
setRequestContextParams(result.params, result.routeName);
|
|
75
|
-
|
|
74
|
+
|
|
76
75
|
hasInterceptSlots = !!result.slots;
|
|
77
76
|
|
|
78
77
|
payload = {
|
|
79
78
|
metadata: {
|
|
80
79
|
pathname: url.pathname,
|
|
80
|
+
routerId: ctx.router.id,
|
|
81
81
|
segments: result.segments,
|
|
82
82
|
matched: result.matched,
|
|
83
83
|
diff: result.diff,
|
|
@@ -86,6 +86,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
86
86
|
slots: result.slots,
|
|
87
87
|
handles: handleStore.stream(),
|
|
88
88
|
version: ctx.version,
|
|
89
|
+
prefetchCacheTTL: ctx.router.prefetchCacheTTL,
|
|
89
90
|
},
|
|
90
91
|
};
|
|
91
92
|
}
|
|
@@ -132,14 +133,14 @@ export async function handleRscRendering<TEnv>(
|
|
|
132
133
|
{ headers: { "Content-Type": "application/json" } },
|
|
133
134
|
);
|
|
134
135
|
} else {
|
|
135
|
-
serverTiming = match.serverTiming;
|
|
136
|
-
|
|
137
136
|
payload = {
|
|
138
137
|
// Initial SSR can reconstruct the tree from segments + rootLayout,
|
|
139
138
|
// so we omit root to avoid sending the same structure twice.
|
|
140
139
|
|
|
141
140
|
metadata: {
|
|
142
141
|
pathname: url.pathname,
|
|
142
|
+
routerId: ctx.router.id,
|
|
143
|
+
basename: ctx.router.basename,
|
|
143
144
|
segments: match.segments,
|
|
144
145
|
matched: match.matched,
|
|
145
146
|
diff: match.diff,
|
|
@@ -148,6 +149,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
148
149
|
rootLayout: ctx.router.rootLayout,
|
|
149
150
|
handles: handleStore.stream(),
|
|
150
151
|
version: ctx.version,
|
|
152
|
+
prefetchCacheTTL: ctx.router.prefetchCacheTTL,
|
|
151
153
|
themeConfig: ctx.router.themeConfig,
|
|
152
154
|
initialTheme: reqCtx.theme,
|
|
153
155
|
},
|
|
@@ -166,10 +168,24 @@ export async function handleRscRendering<TEnv>(
|
|
|
166
168
|
}
|
|
167
169
|
}
|
|
168
170
|
|
|
171
|
+
const metricsStore = reqCtx._metricsStore;
|
|
172
|
+
const renderStart = performance.now();
|
|
173
|
+
|
|
169
174
|
// Serialize to RSC stream
|
|
170
175
|
const rscSerializeStart = performance.now();
|
|
171
|
-
const rscStream = ctx.renderToReadableStream<RscPayload>(payload
|
|
176
|
+
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
177
|
+
onError: (error: unknown) => {
|
|
178
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
179
|
+
},
|
|
180
|
+
});
|
|
172
181
|
const rscSerializeDur = performance.now() - rscSerializeStart;
|
|
182
|
+
// This measures synchronous stream creation, not end-to-end stream consumption.
|
|
183
|
+
appendMetric(
|
|
184
|
+
metricsStore,
|
|
185
|
+
"rsc-serialize",
|
|
186
|
+
rscSerializeStart,
|
|
187
|
+
rscSerializeDur,
|
|
188
|
+
);
|
|
173
189
|
|
|
174
190
|
// Determine if this is an RSC request or HTML request.
|
|
175
191
|
// Partial requests (_rsc_partial) are always RSC -- they come from client-side
|
|
@@ -181,19 +197,20 @@ export async function handleRscRendering<TEnv>(
|
|
|
181
197
|
!url.searchParams.has("__html")) ||
|
|
182
198
|
url.searchParams.has("__rsc");
|
|
183
199
|
|
|
184
|
-
// Build complete Server-Timing: handler phases + match/manifest + RSC serialize
|
|
185
|
-
const timingParts: string[] = [...handlerTimingArr];
|
|
186
|
-
if (serverTiming) {
|
|
187
|
-
timingParts.push(serverTiming);
|
|
188
|
-
}
|
|
189
|
-
timingParts.push(`rsc-serialize;dur=${rscSerializeDur.toFixed(2)}`);
|
|
190
|
-
|
|
191
200
|
if (isRscRequest) {
|
|
192
|
-
const
|
|
201
|
+
const renderDur = performance.now() - renderStart;
|
|
202
|
+
appendMetric(metricsStore, "render:total", renderStart, renderDur);
|
|
193
203
|
const rscHeaders: Record<string, string> = {
|
|
194
204
|
"content-type": "text/x-component;charset=utf-8",
|
|
195
205
|
vary: "accept, X-Rango-State, X-RSC-Router-Client-Path",
|
|
196
206
|
};
|
|
207
|
+
// Tell the client's prefetch cache to scope this response to its source
|
|
208
|
+
// URL (instead of the default source-agnostic wildcard). Intercept
|
|
209
|
+
// responses depend on the source page matching an intercept rule, so
|
|
210
|
+
// they must not be reused for navigations from other sources.
|
|
211
|
+
if (hasInterceptSlots) {
|
|
212
|
+
rscHeaders["x-rsc-prefetch-scope"] = "source";
|
|
213
|
+
}
|
|
197
214
|
// Enable browser HTTP caching for prefetch responses only.
|
|
198
215
|
// Requires X-Rango-Prefetch header (sent by Link prefetch fetch),
|
|
199
216
|
// non-intercept context (intercept responses depend on source page),
|
|
@@ -205,22 +222,19 @@ export async function handleRscRendering<TEnv>(
|
|
|
205
222
|
rscHeaders["cache-control"] = cc;
|
|
206
223
|
}
|
|
207
224
|
}
|
|
208
|
-
if (fullTiming) {
|
|
209
|
-
rscHeaders["Server-Timing"] = fullTiming;
|
|
210
|
-
}
|
|
211
225
|
return createResponseWithMergedHeaders(rscStream, {
|
|
212
226
|
headers: rscHeaders,
|
|
213
227
|
});
|
|
214
228
|
}
|
|
215
229
|
|
|
216
|
-
// Delegate to SSR for HTML response
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
230
|
+
// Delegate to SSR for HTML response (reuse early setup if available)
|
|
231
|
+
const [ssrModule, streamMode] = await getSSRSetup(
|
|
232
|
+
ctx,
|
|
233
|
+
request,
|
|
234
|
+
env,
|
|
235
|
+
url,
|
|
236
|
+
metricsStore,
|
|
237
|
+
);
|
|
224
238
|
|
|
225
239
|
const ssrRenderStart = performance.now();
|
|
226
240
|
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
@@ -228,23 +242,12 @@ export async function handleRscRendering<TEnv>(
|
|
|
228
242
|
streamMode,
|
|
229
243
|
});
|
|
230
244
|
const ssrRenderDur = performance.now() - ssrRenderStart;
|
|
231
|
-
|
|
245
|
+
appendMetric(metricsStore, "ssr-render-html", ssrRenderStart, ssrRenderDur);
|
|
232
246
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const totalHandler = performance.now() - handlerStart;
|
|
236
|
-
timingParts.push(`handler-total;dur=${totalHandler.toFixed(2)}`);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const fullTiming = timingParts.join(", ");
|
|
240
|
-
const htmlHeaders: Record<string, string> = {
|
|
241
|
-
"content-type": "text/html;charset=utf-8",
|
|
242
|
-
};
|
|
243
|
-
if (fullTiming) {
|
|
244
|
-
htmlHeaders["Server-Timing"] = fullTiming;
|
|
245
|
-
}
|
|
247
|
+
const renderDur = performance.now() - renderStart;
|
|
248
|
+
appendMetric(metricsStore, "render:total", renderStart, renderDur);
|
|
246
249
|
|
|
247
250
|
return createResponseWithMergedHeaders(htmlStream, {
|
|
248
|
-
headers:
|
|
251
|
+
headers: { "content-type": "text/html;charset=utf-8" },
|
|
249
252
|
});
|
|
250
253
|
}
|
package/src/rsc/server-action.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
getLocationState,
|
|
22
22
|
} from "../server/request-context.js";
|
|
23
23
|
import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
|
|
24
|
+
import { appendMetric } from "../router/metrics.js";
|
|
24
25
|
import type { RscPayload } from "./types.js";
|
|
25
26
|
import {
|
|
26
27
|
hasBodyContent,
|
|
@@ -207,10 +208,12 @@ export async function executeServerAction<TEnv>(
|
|
|
207
208
|
const payload: RscPayload = {
|
|
208
209
|
metadata: {
|
|
209
210
|
pathname: url.pathname,
|
|
211
|
+
routerId: ctx.router.id,
|
|
210
212
|
segments: errorResult.segments,
|
|
211
213
|
isPartial: true,
|
|
212
214
|
matched: errorResult.matched,
|
|
213
215
|
diff: errorResult.diff,
|
|
216
|
+
params: errorResult.params,
|
|
214
217
|
isError: true,
|
|
215
218
|
handles: handleStore.stream(),
|
|
216
219
|
version: ctx.version,
|
|
@@ -224,6 +227,9 @@ export async function executeServerAction<TEnv>(
|
|
|
224
227
|
|
|
225
228
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
226
229
|
temporaryReferences,
|
|
230
|
+
onError: (error: unknown) => {
|
|
231
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
232
|
+
},
|
|
227
233
|
});
|
|
228
234
|
|
|
229
235
|
return createResponseWithMergedHeaders(rscStream, {
|
|
@@ -274,6 +280,8 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
274
280
|
): Promise<Response> {
|
|
275
281
|
const { returnValue, actionStatus, temporaryReferences, actionContext } =
|
|
276
282
|
continuation;
|
|
283
|
+
const reqCtx = requireRequestContext();
|
|
284
|
+
const metricsStore = reqCtx._metricsStore;
|
|
277
285
|
|
|
278
286
|
const matchResult = await ctx.router.matchPartial(
|
|
279
287
|
request,
|
|
@@ -308,15 +316,15 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
308
316
|
// Return updated segments
|
|
309
317
|
setRequestContextParams(matchResult.params, matchResult.routeName);
|
|
310
318
|
|
|
311
|
-
const serverTiming = matchResult.serverTiming;
|
|
312
|
-
|
|
313
319
|
const payload: RscPayload = {
|
|
314
320
|
metadata: {
|
|
315
321
|
pathname: url.pathname,
|
|
322
|
+
routerId: ctx.router.id,
|
|
316
323
|
segments: matchResult.segments,
|
|
317
324
|
isPartial: true,
|
|
318
325
|
matched: matchResult.matched,
|
|
319
326
|
diff: matchResult.diff,
|
|
327
|
+
params: matchResult.params,
|
|
320
328
|
slots: matchResult.slots,
|
|
321
329
|
handles: handleStore.stream(),
|
|
322
330
|
version: ctx.version,
|
|
@@ -326,19 +334,25 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
326
334
|
|
|
327
335
|
attachLocationState(payload);
|
|
328
336
|
|
|
337
|
+
const renderStart = performance.now();
|
|
329
338
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
330
339
|
temporaryReferences,
|
|
340
|
+
onError: (error: unknown) => {
|
|
341
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
342
|
+
},
|
|
331
343
|
});
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
344
|
+
const rscSerializeDur = performance.now() - renderStart;
|
|
345
|
+
// This measures synchronous stream creation, not end-to-end stream consumption.
|
|
346
|
+
appendMetric(metricsStore, "rsc-serialize", renderStart, rscSerializeDur);
|
|
347
|
+
appendMetric(
|
|
348
|
+
metricsStore,
|
|
349
|
+
"render:total",
|
|
350
|
+
renderStart,
|
|
351
|
+
performance.now() - renderStart,
|
|
352
|
+
);
|
|
339
353
|
|
|
340
354
|
return createResponseWithMergedHeaders(rscStream, {
|
|
341
355
|
status: actionStatus,
|
|
342
|
-
headers:
|
|
356
|
+
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
343
357
|
});
|
|
344
358
|
}
|