@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1b930379
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 +46 -12
- package/dist/bin/rango.js +109 -15
- package/dist/vite/index.js +323 -121
- package/package.json +15 -16
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/caching/SKILL.md +4 -4
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +33 -31
- package/skills/host-router/SKILL.md +218 -0
- package/skills/loader/SKILL.md +55 -15
- package/skills/prerender/SKILL.md +2 -2
- package/skills/rango/SKILL.md +0 -1
- package/skills/route/SKILL.md +3 -4
- package/skills/router-setup/SKILL.md +8 -3
- package/skills/typesafety/SKILL.md +25 -23
- package/src/__internal.ts +92 -0
- package/src/bin/rango.ts +18 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +95 -5
- package/src/browser/navigation-client.ts +97 -72
- package/src/browser/prefetch/cache.ts +112 -25
- package/src/browser/prefetch/fetch.ts +28 -30
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/react/Link.tsx +19 -7
- package/src/browser/rsc-router.tsx +11 -2
- package/src/browser/server-action-bridge.ts +448 -432
- package/src/browser/types.ts +24 -0
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/route-trie.ts +19 -3
- package/src/build/route-types/router-processing.ts +125 -15
- package/src/client.rsc.tsx +2 -1
- package/src/client.tsx +1 -46
- 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 +5 -36
- package/src/index.ts +32 -66
- package/src/prerender/store.ts +56 -15
- package/src/route-definition/index.ts +0 -3
- package/src/router/handler-context.ts +30 -3
- package/src/router/loader-resolution.ts +1 -1
- package/src/router/match-api.ts +1 -1
- package/src/router/match-result.ts +0 -9
- package/src/router/metrics.ts +233 -13
- package/src/router/middleware-types.ts +53 -10
- package/src/router/middleware.ts +170 -81
- package/src/router/pattern-matching.ts +20 -5
- package/src/router/prerender-match.ts +4 -0
- package/src/router/revalidation.ts +27 -7
- package/src/router/router-interfaces.ts +14 -1
- package/src/router/router-options.ts +13 -8
- package/src/router/segment-resolution/fresh.ts +18 -0
- package/src/router/segment-resolution/helpers.ts +1 -1
- package/src/router/segment-resolution/revalidation.ts +22 -9
- package/src/router/trie-matching.ts +20 -2
- package/src/router.ts +29 -9
- package/src/rsc/handler.ts +106 -11
- package/src/rsc/index.ts +0 -20
- package/src/rsc/progressive-enhancement.ts +21 -8
- package/src/rsc/rsc-rendering.ts +30 -43
- package/src/rsc/server-action.ts +14 -10
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +2 -0
- package/src/search-params.ts +16 -13
- package/src/server/context.ts +8 -2
- package/src/server/request-context.ts +38 -16
- package/src/server.ts +6 -0
- package/src/theme/index.ts +4 -13
- package/src/types/handler-context.ts +12 -16
- package/src/types/route-config.ts +17 -8
- package/src/types/segments.ts +0 -5
- package/src/vite/discovery/bundle-postprocess.ts +31 -56
- package/src/vite/discovery/discover-routers.ts +18 -4
- package/src/vite/discovery/prerender-collection.ts +34 -14
- package/src/vite/discovery/state.ts +4 -7
- package/src/vite/index.ts +4 -3
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/rango.ts +11 -0
- package/src/vite/router-discovery.ts +16 -0
- package/src/vite/utils/prerender-utils.ts +60 -0
- package/skills/testing/SKILL.md +0 -226
- package/src/route-definition/route-function.ts +0 -119
- /package/{CLAUDE.md → AGENTS.md} +0 -0
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "./helpers.js";
|
|
25
25
|
import { getRouterContext } from "../router-context.js";
|
|
26
26
|
import { resolveSink, safeEmit } from "../telemetry.js";
|
|
27
|
+
import { track } from "../../server/context.js";
|
|
27
28
|
|
|
28
29
|
// ---------------------------------------------------------------------------
|
|
29
30
|
// Streamed handler telemetry
|
|
@@ -178,7 +179,9 @@ export async function resolveSegment<TEnv>(
|
|
|
178
179
|
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
179
180
|
entry.shortCode;
|
|
180
181
|
|
|
182
|
+
const doneLayoutHandler = track(`handler:${entry.id}`, 2);
|
|
181
183
|
const component = await resolveLayoutComponent(entry, context);
|
|
184
|
+
doneLayoutHandler();
|
|
182
185
|
|
|
183
186
|
segments.push({
|
|
184
187
|
id: entry.shortCode,
|
|
@@ -241,9 +244,11 @@ export async function resolveSegment<TEnv>(
|
|
|
241
244
|
entry.shortCode,
|
|
242
245
|
);
|
|
243
246
|
if (component === undefined) {
|
|
247
|
+
const doneRouteHandler = track(`handler:${entry.id}`, 2);
|
|
244
248
|
if (entry.loading) {
|
|
245
249
|
const result = handleHandlerResult(entry.handler(context));
|
|
246
250
|
if (result instanceof Promise) {
|
|
251
|
+
result.finally(doneRouteHandler).catch(() => {});
|
|
247
252
|
const tracked = deps.trackHandler(result, {
|
|
248
253
|
segmentId: entry.shortCode,
|
|
249
254
|
segmentType: entry.type,
|
|
@@ -258,10 +263,12 @@ export async function resolveSegment<TEnv>(
|
|
|
258
263
|
);
|
|
259
264
|
component = tracked;
|
|
260
265
|
} else {
|
|
266
|
+
doneRouteHandler();
|
|
261
267
|
component = result;
|
|
262
268
|
}
|
|
263
269
|
} else {
|
|
264
270
|
component = handleHandlerResult(await entry.handler(context));
|
|
271
|
+
doneRouteHandler();
|
|
265
272
|
}
|
|
266
273
|
}
|
|
267
274
|
|
|
@@ -343,7 +350,9 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
343
350
|
|
|
344
351
|
// Handler-first: orphan layout handler executes before its parallels
|
|
345
352
|
// so that ctx.set() values are visible to parallel children.
|
|
353
|
+
const doneOrphanHandler = track(`handler:${orphan.id}`, 2);
|
|
346
354
|
const component = await resolveLayoutComponent(orphan, context);
|
|
355
|
+
doneOrphanHandler();
|
|
347
356
|
|
|
348
357
|
segments.push({
|
|
349
358
|
id: orphan.shortCode,
|
|
@@ -410,12 +419,17 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
410
419
|
);
|
|
411
420
|
|
|
412
421
|
if (component === undefined) {
|
|
422
|
+
const doneParallelHandler = track(
|
|
423
|
+
`handler:${parallelEntry.id}.${slot}`,
|
|
424
|
+
2,
|
|
425
|
+
);
|
|
413
426
|
const hasLoadingFallback =
|
|
414
427
|
parallelEntry.loading !== undefined && parallelEntry.loading !== false;
|
|
415
428
|
if (hasLoadingFallback) {
|
|
416
429
|
const result =
|
|
417
430
|
typeof handler === "function" ? handler(context) : handler;
|
|
418
431
|
if (result instanceof Promise) {
|
|
432
|
+
result.finally(doneParallelHandler).catch(() => {});
|
|
419
433
|
const tracked = deps.trackHandler(result, {
|
|
420
434
|
segmentId: `${parentShortCode}.${slot}`,
|
|
421
435
|
segmentType: "parallel",
|
|
@@ -430,11 +444,13 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
430
444
|
);
|
|
431
445
|
component = tracked as ReactNode;
|
|
432
446
|
} else {
|
|
447
|
+
doneParallelHandler();
|
|
433
448
|
component = result as ReactNode;
|
|
434
449
|
}
|
|
435
450
|
} else {
|
|
436
451
|
component =
|
|
437
452
|
typeof handler === "function" ? await handler(context) : handler;
|
|
453
|
+
doneParallelHandler();
|
|
438
454
|
}
|
|
439
455
|
}
|
|
440
456
|
|
|
@@ -499,6 +515,7 @@ export async function resolveAllSegments<TEnv>(
|
|
|
499
515
|
} catch {}
|
|
500
516
|
|
|
501
517
|
for (const entry of entries) {
|
|
518
|
+
const doneEntry = track(`segment:${entry.id}`, 1);
|
|
502
519
|
const resolvedSegments = await resolveWithErrorBoundary(
|
|
503
520
|
entry,
|
|
504
521
|
params,
|
|
@@ -518,6 +535,7 @@ export async function resolveAllSegments<TEnv>(
|
|
|
518
535
|
{ request: safeRequest, url: context.url, routeKey, telemetry },
|
|
519
536
|
context.pathname,
|
|
520
537
|
);
|
|
538
|
+
doneEntry();
|
|
521
539
|
// Deduplicate by segment ID. include() scopes can produce entries that
|
|
522
540
|
// resolve the same shared layout/loader segment. Duplicates in the segment
|
|
523
541
|
// array propagate to the client's matched[] and change the React tree depth.
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
} from "./helpers.js";
|
|
38
38
|
import { getRouterContext } from "../router-context.js";
|
|
39
39
|
import { resolveSink, safeEmit } from "../telemetry.js";
|
|
40
|
+
import { track } from "../../server/context.js";
|
|
40
41
|
|
|
41
42
|
// ---------------------------------------------------------------------------
|
|
42
43
|
// Telemetry helpers
|
|
@@ -621,20 +622,29 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
621
622
|
return shouldRevalidate;
|
|
622
623
|
},
|
|
623
624
|
async () => {
|
|
625
|
+
const doneHandler = track(`handler:${entry.id}`, 2);
|
|
624
626
|
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
625
627
|
entry.shortCode;
|
|
626
628
|
if (entry.type === "layout" || entry.type === "cache") {
|
|
627
|
-
|
|
629
|
+
const layoutComponent = await resolveLayoutComponent(entry, context);
|
|
630
|
+
doneHandler();
|
|
631
|
+
return layoutComponent;
|
|
628
632
|
}
|
|
629
633
|
const staticComponent = await tryStaticHandler(entry, entry.shortCode);
|
|
630
|
-
if (staticComponent !== undefined)
|
|
634
|
+
if (staticComponent !== undefined) {
|
|
635
|
+
doneHandler();
|
|
636
|
+
return staticComponent;
|
|
637
|
+
}
|
|
631
638
|
const routeEntry = entry as Extract<EntryData, { type: "route" }>;
|
|
632
639
|
if (!routeEntry.loading) {
|
|
633
|
-
|
|
640
|
+
const result = handleHandlerResult(await routeEntry.handler(context));
|
|
641
|
+
doneHandler();
|
|
642
|
+
return result;
|
|
634
643
|
}
|
|
635
644
|
if (!actionContext) {
|
|
636
645
|
const result = handleHandlerResult(routeEntry.handler(context));
|
|
637
646
|
if (result instanceof Promise) {
|
|
647
|
+
result.finally(doneHandler).catch(() => {});
|
|
638
648
|
const tracked = deps.trackHandler(result, {
|
|
639
649
|
segmentId: entry.shortCode,
|
|
640
650
|
segmentType: entry.type,
|
|
@@ -649,15 +659,18 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
649
659
|
);
|
|
650
660
|
return { content: tracked };
|
|
651
661
|
}
|
|
662
|
+
doneHandler();
|
|
652
663
|
return { content: result };
|
|
653
664
|
}
|
|
654
665
|
debugLog("segment.action", "resolving action route with awaited value", {
|
|
655
666
|
entryId: entry.id,
|
|
656
667
|
});
|
|
668
|
+
const actionResult = handleHandlerResult(
|
|
669
|
+
await routeEntry.handler(context),
|
|
670
|
+
);
|
|
671
|
+
doneHandler();
|
|
657
672
|
return {
|
|
658
|
-
content: Promise.resolve(
|
|
659
|
-
handleHandlerResult(await routeEntry.handler(context)),
|
|
660
|
-
),
|
|
673
|
+
content: Promise.resolve(actionResult),
|
|
661
674
|
};
|
|
662
675
|
},
|
|
663
676
|
() => null,
|
|
@@ -1178,6 +1191,7 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
|
|
|
1178
1191
|
}
|
|
1179
1192
|
|
|
1180
1193
|
const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
|
|
1194
|
+
const doneEntry = track(`segment:${entry.id}`, 1);
|
|
1181
1195
|
const resolved = await resolveWithErrorBoundary(
|
|
1182
1196
|
nonParallelEntry,
|
|
1183
1197
|
params,
|
|
@@ -1199,11 +1213,10 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
|
|
|
1199
1213
|
),
|
|
1200
1214
|
(seg) => ({ segments: [seg], matchedIds: [seg.id] }),
|
|
1201
1215
|
deps,
|
|
1202
|
-
telemetry
|
|
1203
|
-
? { request, url: context.url, routeKey, isPartial: true, telemetry }
|
|
1204
|
-
: undefined,
|
|
1216
|
+
{ request, url: context.url, routeKey, isPartial: true, telemetry },
|
|
1205
1217
|
pathname,
|
|
1206
1218
|
);
|
|
1219
|
+
doneEntry();
|
|
1207
1220
|
|
|
1208
1221
|
// Deduplicate segments and matchedIds by ID, matching resolveAllSegments.
|
|
1209
1222
|
// include() scopes can produce entries that resolve the same shared
|
|
@@ -114,7 +114,25 @@ function walkTrie(
|
|
|
114
114
|
if (result) return result;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
// Priority 2:
|
|
117
|
+
// Priority 2: Suffix-param match (e.g., :productId.html)
|
|
118
|
+
if (node.xp) {
|
|
119
|
+
for (const suffix in node.xp) {
|
|
120
|
+
if (segment.endsWith(suffix) && segment.length > suffix.length) {
|
|
121
|
+
const paramValue = segment.slice(0, -suffix.length);
|
|
122
|
+
paramValues.push(paramValue);
|
|
123
|
+
const result = walkTrie(
|
|
124
|
+
node.xp[suffix].c,
|
|
125
|
+
segments,
|
|
126
|
+
index + 1,
|
|
127
|
+
paramValues,
|
|
128
|
+
);
|
|
129
|
+
paramValues.pop();
|
|
130
|
+
if (result) return result;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Priority 3: Param match
|
|
118
136
|
if (node.p) {
|
|
119
137
|
paramValues.push(segment);
|
|
120
138
|
const result = walkTrie(node.p.c, segments, index + 1, paramValues);
|
|
@@ -122,7 +140,7 @@ function walkTrie(
|
|
|
122
140
|
if (result) return result;
|
|
123
141
|
}
|
|
124
142
|
|
|
125
|
-
// Priority
|
|
143
|
+
// Priority 4: Wildcard match (consumes rest)
|
|
126
144
|
if (node.w) {
|
|
127
145
|
const rest = joinRemainingSegments(segments, index);
|
|
128
146
|
return {
|
package/src/router.ts
CHANGED
|
@@ -147,7 +147,7 @@ export function createRouter<TEnv = any>(
|
|
|
147
147
|
$$sourceFile: injectedSourceFile,
|
|
148
148
|
nonce,
|
|
149
149
|
version,
|
|
150
|
-
|
|
150
|
+
prefetchCacheTTL: prefetchCacheTTLOption,
|
|
151
151
|
warmup: warmupOption,
|
|
152
152
|
allowDebugManifest: allowDebugManifestOption = false,
|
|
153
153
|
telemetry: telemetrySink,
|
|
@@ -200,11 +200,17 @@ export function createRouter<TEnv = any>(
|
|
|
200
200
|
const routerId =
|
|
201
201
|
userProvidedId ?? injectedId ?? `router_${nextRouterAutoId()}`;
|
|
202
202
|
|
|
203
|
-
// Resolve prefetch cache
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
203
|
+
// Resolve prefetch cache TTL (default: 300 seconds / 5 minutes)
|
|
204
|
+
// Clamp to a non-negative integer for valid Cache-Control max-age.
|
|
205
|
+
const rawTTL =
|
|
206
|
+
prefetchCacheTTLOption !== undefined ? prefetchCacheTTLOption : 300;
|
|
207
|
+
const prefetchCacheTTLSeconds =
|
|
208
|
+
rawTTL === false ? 0 : Math.max(0, Math.floor(rawTTL));
|
|
209
|
+
const prefetchCacheTTL = prefetchCacheTTLSeconds * 1000;
|
|
210
|
+
const prefetchCacheControl: string | false =
|
|
211
|
+
prefetchCacheTTLSeconds === 0
|
|
212
|
+
? false
|
|
213
|
+
: `private, max-age=${prefetchCacheTTLSeconds}`;
|
|
208
214
|
|
|
209
215
|
// Resolve warmup enabled flag (default: true)
|
|
210
216
|
const warmupEnabled = warmupOption !== false;
|
|
@@ -357,8 +363,18 @@ export function createRouter<TEnv = any>(
|
|
|
357
363
|
return precomputedByPrefix;
|
|
358
364
|
}
|
|
359
365
|
|
|
360
|
-
// Wrapper to pass debugPerformance to external createMetricsStore
|
|
361
|
-
|
|
366
|
+
// Wrapper to pass debugPerformance to external createMetricsStore.
|
|
367
|
+
// Also checks per-request flag set by ctx.debugPerformance() in middleware.
|
|
368
|
+
const getMetricsStore = () => {
|
|
369
|
+
const reqCtx = _getRequestContext();
|
|
370
|
+
const enabled = debugPerformance || !!reqCtx?._debugPerformance;
|
|
371
|
+
if (!enabled) return undefined;
|
|
372
|
+
if (!reqCtx) {
|
|
373
|
+
return createMetricsStore(true);
|
|
374
|
+
}
|
|
375
|
+
reqCtx._metricsStore ??= createMetricsStore(true);
|
|
376
|
+
return reqCtx._metricsStore;
|
|
377
|
+
};
|
|
362
378
|
|
|
363
379
|
// Wrapper to pass defaults to error/notFound boundary finders
|
|
364
380
|
const findNearestErrorBoundary = (entry: EntryData | null) =>
|
|
@@ -869,12 +885,16 @@ export function createRouter<TEnv = any>(
|
|
|
869
885
|
// Expose resolved cache profiles for per-request resolution
|
|
870
886
|
cacheProfiles: resolvedCacheProfiles,
|
|
871
887
|
|
|
872
|
-
// Expose prefetch cache
|
|
888
|
+
// Expose prefetch cache settings
|
|
873
889
|
prefetchCacheControl,
|
|
890
|
+
prefetchCacheTTL,
|
|
874
891
|
|
|
875
892
|
// Expose warmup enabled flag for handler and client
|
|
876
893
|
warmupEnabled,
|
|
877
894
|
|
|
895
|
+
// Expose router-wide performance debugging for request-level metrics setup
|
|
896
|
+
debugPerformance,
|
|
897
|
+
|
|
878
898
|
// Expose debug manifest flag for handler
|
|
879
899
|
allowDebugManifest: allowDebugManifestOption,
|
|
880
900
|
|
package/src/rsc/handler.ts
CHANGED
|
@@ -18,7 +18,12 @@ import {
|
|
|
18
18
|
} from "../server/request-context.js";
|
|
19
19
|
import * as rscDeps from "@vitejs/plugin-rsc/rsc";
|
|
20
20
|
|
|
21
|
-
import type {
|
|
21
|
+
import type {
|
|
22
|
+
RscPayload,
|
|
23
|
+
CreateRSCHandlerOptions,
|
|
24
|
+
LoadSSRModule,
|
|
25
|
+
SSRModule,
|
|
26
|
+
} from "./types.js";
|
|
22
27
|
import {
|
|
23
28
|
createResponseWithMergedHeaders,
|
|
24
29
|
finalizeResponse,
|
|
@@ -66,6 +71,17 @@ import {
|
|
|
66
71
|
createDefaultTimeoutResponse,
|
|
67
72
|
type TimeoutPhase,
|
|
68
73
|
} from "../router/timeout.js";
|
|
74
|
+
import {
|
|
75
|
+
createMetricsStore,
|
|
76
|
+
appendMetric,
|
|
77
|
+
buildMetricsTiming,
|
|
78
|
+
} from "../router/metrics.js";
|
|
79
|
+
import {
|
|
80
|
+
startSSRSetup,
|
|
81
|
+
getSSRSetup,
|
|
82
|
+
mayNeedSSR,
|
|
83
|
+
SSR_SETUP_VAR,
|
|
84
|
+
} from "./ssr-setup.js";
|
|
69
85
|
|
|
70
86
|
/**
|
|
71
87
|
* Create an RSC request handler.
|
|
@@ -117,10 +133,22 @@ export function createRSCHandler<
|
|
|
117
133
|
decodeFormState,
|
|
118
134
|
} = deps;
|
|
119
135
|
|
|
120
|
-
// Use provided loadSSRModule or default to vite RSC module loader
|
|
121
|
-
|
|
136
|
+
// Use provided loadSSRModule or default to vite RSC module loader.
|
|
137
|
+
// In production the SSR module is stable across requests, so memoize
|
|
138
|
+
// the dynamic import to avoid repeated module resolution overhead.
|
|
139
|
+
// In dev mode Vite may hot-reload the module, so skip memoization.
|
|
140
|
+
const rawLoadSSRModule: LoadSSRModule =
|
|
122
141
|
options.loadSSRModule ??
|
|
123
142
|
(() => import.meta.viteRsc.loadModule("ssr", "index"));
|
|
143
|
+
let _ssrModulePromise: Promise<SSRModule> | undefined;
|
|
144
|
+
const loadSSRModule: LoadSSRModule =
|
|
145
|
+
process.env.NODE_ENV === "production"
|
|
146
|
+
? () =>
|
|
147
|
+
(_ssrModulePromise ??= rawLoadSSRModule().catch((err) => {
|
|
148
|
+
_ssrModulePromise = undefined;
|
|
149
|
+
throw err;
|
|
150
|
+
}))
|
|
151
|
+
: rawLoadSSRModule;
|
|
124
152
|
|
|
125
153
|
/**
|
|
126
154
|
* Per-request error reporter that deduplicates via the ALS request context.
|
|
@@ -268,6 +296,11 @@ export function createRSCHandler<
|
|
|
268
296
|
input: RouterRequestInput<TEnv> = {},
|
|
269
297
|
): Promise<Response> {
|
|
270
298
|
const handlerStart = performance.now();
|
|
299
|
+
// Create the metrics store at handler start so handler:total has startTime=0
|
|
300
|
+
// and all metrics are relative to the request entry point.
|
|
301
|
+
const earlyMetricsStore = router.debugPerformance
|
|
302
|
+
? createMetricsStore(true, handlerStart)
|
|
303
|
+
: undefined;
|
|
271
304
|
|
|
272
305
|
const { env = {} as TEnv, vars: initialVars, ctx: executionCtx } = input;
|
|
273
306
|
|
|
@@ -381,6 +414,10 @@ export function createRSCHandler<
|
|
|
381
414
|
executionContext: executionCtx,
|
|
382
415
|
themeConfig: router.themeConfig,
|
|
383
416
|
});
|
|
417
|
+
if (earlyMetricsStore) {
|
|
418
|
+
requestContext._debugPerformance = true;
|
|
419
|
+
requestContext._metricsStore = earlyMetricsStore;
|
|
420
|
+
}
|
|
384
421
|
// Wire background error reporting so "use cache" and other subsystems
|
|
385
422
|
// can surface non-fatal errors through the router's onError callback.
|
|
386
423
|
requestContext._reportBackgroundError = (
|
|
@@ -422,6 +459,7 @@ export function createRSCHandler<
|
|
|
422
459
|
};
|
|
423
460
|
|
|
424
461
|
// Execute middleware chain if any, otherwise call core handler directly
|
|
462
|
+
let response: Response;
|
|
425
463
|
if (matchedMiddleware.length > 0) {
|
|
426
464
|
const mwResponse = await executeMiddleware(
|
|
427
465
|
matchedMiddleware,
|
|
@@ -440,13 +478,52 @@ export function createRSCHandler<
|
|
|
440
478
|
mwResponse,
|
|
441
479
|
createRedirectFlightResponse,
|
|
442
480
|
);
|
|
443
|
-
|
|
481
|
+
response = intercepted ?? finalizeResponse(mwResponse);
|
|
482
|
+
} else {
|
|
483
|
+
response = finalizeResponse(mwResponse);
|
|
444
484
|
}
|
|
485
|
+
} else {
|
|
486
|
+
response = await coreHandler();
|
|
487
|
+
}
|
|
445
488
|
|
|
446
|
-
|
|
489
|
+
// Finalize metrics after all middleware (including post-next work)
|
|
490
|
+
// has completed so :post spans are captured in the timeline.
|
|
491
|
+
// Handler timing parts are always emitted (even without debug metrics)
|
|
492
|
+
// so non-debug requests still get bootstrap Server-Timing entries.
|
|
493
|
+
const handlerTimingArr: string[] = variables.__handlerTiming || [];
|
|
494
|
+
// Preserve any existing Server-Timing set by response routes or middleware
|
|
495
|
+
const existingTiming = response.headers.get("Server-Timing");
|
|
496
|
+
const timingParts = existingTiming
|
|
497
|
+
? [existingTiming, ...handlerTimingArr]
|
|
498
|
+
: [...handlerTimingArr];
|
|
499
|
+
|
|
500
|
+
const metricsStore = requestContext._metricsStore;
|
|
501
|
+
if (metricsStore) {
|
|
502
|
+
// When the store was created at handler start (earlyMetricsStore),
|
|
503
|
+
// handler:total covers the full request. When ctx.debugPerformance()
|
|
504
|
+
// created the store mid-request, use its requestStart to avoid a
|
|
505
|
+
// negative startTime offset.
|
|
506
|
+
const totalStart = earlyMetricsStore
|
|
507
|
+
? handlerStart
|
|
508
|
+
: metricsStore.requestStart;
|
|
509
|
+
appendMetric(
|
|
510
|
+
metricsStore,
|
|
511
|
+
"handler:total",
|
|
512
|
+
totalStart,
|
|
513
|
+
performance.now() - totalStart,
|
|
514
|
+
);
|
|
515
|
+
const metricsTiming = buildMetricsTiming(
|
|
516
|
+
request.method,
|
|
517
|
+
url.pathname,
|
|
518
|
+
metricsStore,
|
|
519
|
+
);
|
|
520
|
+
if (metricsTiming) timingParts.push(metricsTiming);
|
|
447
521
|
}
|
|
448
522
|
|
|
449
|
-
|
|
523
|
+
const fullTiming = timingParts.join(", ");
|
|
524
|
+
if (fullTiming) response.headers.set("Server-Timing", fullTiming);
|
|
525
|
+
|
|
526
|
+
return response;
|
|
450
527
|
});
|
|
451
528
|
};
|
|
452
529
|
|
|
@@ -490,6 +567,21 @@ export function createRSCHandler<
|
|
|
490
567
|
return responseOutcome.result;
|
|
491
568
|
}
|
|
492
569
|
|
|
570
|
+
// Kick off SSR module loading + stream mode resolution in parallel with
|
|
571
|
+
// segment resolution. Placed after the response-route short-circuit so
|
|
572
|
+
// response/mime routes never pay for SSR work.
|
|
573
|
+
if (mayNeedSSR(request, url)) {
|
|
574
|
+
variables[SSR_SETUP_VAR] = startSSRSetup(
|
|
575
|
+
handlerCtx,
|
|
576
|
+
request,
|
|
577
|
+
env,
|
|
578
|
+
url,
|
|
579
|
+
router.debugPerformance
|
|
580
|
+
? () => requireRequestContext()._metricsStore
|
|
581
|
+
: undefined,
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
493
585
|
const routeReverse = createReverseFunction(getRequiredRouteMap());
|
|
494
586
|
|
|
495
587
|
const isAction =
|
|
@@ -964,11 +1056,14 @@ export function createRSCHandler<
|
|
|
964
1056
|
});
|
|
965
1057
|
}
|
|
966
1058
|
|
|
967
|
-
// Delegate to SSR for HTML response
|
|
968
|
-
const [ssrModule, streamMode] = await
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1059
|
+
// Delegate to SSR for HTML response (reuse early setup if available)
|
|
1060
|
+
const [ssrModule, streamMode] = await getSSRSetup(
|
|
1061
|
+
handlerCtx,
|
|
1062
|
+
request,
|
|
1063
|
+
env,
|
|
1064
|
+
url,
|
|
1065
|
+
requireRequestContext()._metricsStore,
|
|
1066
|
+
);
|
|
972
1067
|
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
973
1068
|
nonce,
|
|
974
1069
|
streamMode,
|
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";
|
|
@@ -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";
|
|
@@ -257,10 +258,16 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
257
258
|
};
|
|
258
259
|
|
|
259
260
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload);
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
]
|
|
261
|
+
// metricsStore=undefined is safe: the handler already stashed the early
|
|
262
|
+
// SSR setup promise on request variables, so getSSRSetup returns it
|
|
263
|
+
// without falling back to a fresh startSSRSetup.
|
|
264
|
+
const [ssrModule, streamMode] = await getSSRSetup(
|
|
265
|
+
ctx,
|
|
266
|
+
request,
|
|
267
|
+
env,
|
|
268
|
+
url,
|
|
269
|
+
undefined,
|
|
270
|
+
);
|
|
264
271
|
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
265
272
|
formState: reactFormState,
|
|
266
273
|
nonce,
|
|
@@ -350,10 +357,16 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
350
357
|
};
|
|
351
358
|
|
|
352
359
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload);
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
]
|
|
360
|
+
// metricsStore=undefined is safe: the handler already stashed the early
|
|
361
|
+
// SSR setup promise on request variables, so getSSRSetup returns it
|
|
362
|
+
// without falling back to a fresh startSSRSetup.
|
|
363
|
+
const [ssrModule, streamMode] = await getSSRSetup(
|
|
364
|
+
ctx,
|
|
365
|
+
request,
|
|
366
|
+
env,
|
|
367
|
+
url,
|
|
368
|
+
undefined,
|
|
369
|
+
);
|
|
357
370
|
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
358
371
|
nonce,
|
|
359
372
|
streamMode,
|