@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
|
@@ -67,10 +67,11 @@
|
|
|
67
67
|
* Keep if:
|
|
68
68
|
* - component !== null (needs rendering)
|
|
69
69
|
* - type === "loader" (carries data even with null component)
|
|
70
|
+
* - client doesn't have the segment (structurally required parent node)
|
|
70
71
|
*
|
|
71
72
|
* Skip if:
|
|
72
|
-
* - component === null AND type !== "loader"
|
|
73
|
-
* - (
|
|
73
|
+
* - component === null AND type !== "loader" AND client has it cached
|
|
74
|
+
* - (Revalidation skip — client already has this segment's UI)
|
|
74
75
|
*
|
|
75
76
|
*
|
|
76
77
|
* INTERCEPT HANDLING
|
|
@@ -108,8 +109,8 @@
|
|
|
108
109
|
*/
|
|
109
110
|
import type { MatchResult, ResolvedSegment } from "../types.js";
|
|
110
111
|
import type { MatchContext, MatchPipelineState } from "./match-context.js";
|
|
111
|
-
import { generateServerTiming, logMetrics } from "./metrics.js";
|
|
112
112
|
import { debugLog } from "./logging.js";
|
|
113
|
+
import { appendMetric } from "./metrics.js";
|
|
113
114
|
|
|
114
115
|
/**
|
|
115
116
|
* Collect all segments from an async generator
|
|
@@ -124,6 +125,69 @@ export async function collectSegments(
|
|
|
124
125
|
return segments;
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Deduplicate inherited loader segments by loaderId.
|
|
130
|
+
*
|
|
131
|
+
* When a route has loaders and a child layout has parallel slots, the same
|
|
132
|
+
* loader is resolved twice: once for the route and once inherited into the
|
|
133
|
+
* layout (tagged with `_inherited`). The inherited copy is only needed when
|
|
134
|
+
* the route uses `loading()` — in that case, the loader data is inside a
|
|
135
|
+
* LoaderBoundary/Suspense that parallel slots can't reach through. Without
|
|
136
|
+
* loading(), useLoader() traverses parent contexts and finds the data.
|
|
137
|
+
*/
|
|
138
|
+
function deduplicateLoaderSegments(
|
|
139
|
+
segments: ResolvedSegment[],
|
|
140
|
+
logPrefix: string,
|
|
141
|
+
): ResolvedSegment[] {
|
|
142
|
+
// First pass: collect loaderIds of original (non-inherited) segments
|
|
143
|
+
// and whether their parent entry uses loading()
|
|
144
|
+
const originalLoaders = new Set<string>();
|
|
145
|
+
const loadersWithLoading = new Set<string>();
|
|
146
|
+
for (const s of segments) {
|
|
147
|
+
if (s.type === "loader" && s.loaderId && !s._inherited) {
|
|
148
|
+
originalLoaders.add(s.loaderId);
|
|
149
|
+
// If the segment has a sibling with loading, the parent uses loading()
|
|
150
|
+
// We detect this by checking if any non-loader segment in the same
|
|
151
|
+
// namespace has loading defined
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Check if any layout/route segment has loading — if a loader's namespace
|
|
155
|
+
// matches a segment with loading, the inherited copy is needed
|
|
156
|
+
for (const s of segments) {
|
|
157
|
+
if (s.type !== "loader" && s.loading !== undefined && s.loading !== false) {
|
|
158
|
+
// Find loaders in this namespace
|
|
159
|
+
for (const l of segments) {
|
|
160
|
+
if (l.type === "loader" && l.namespace === s.namespace && l.loaderId) {
|
|
161
|
+
loadersWithLoading.add(l.loaderId);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const result: ResolvedSegment[] = [];
|
|
168
|
+
let dedupCount = 0;
|
|
169
|
+
|
|
170
|
+
for (const s of segments) {
|
|
171
|
+
if (
|
|
172
|
+
s.type === "loader" &&
|
|
173
|
+
s.loaderId &&
|
|
174
|
+
s._inherited &&
|
|
175
|
+
originalLoaders.has(s.loaderId) &&
|
|
176
|
+
!loadersWithLoading.has(s.loaderId)
|
|
177
|
+
) {
|
|
178
|
+
dedupCount++;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
result.push(s);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (dedupCount > 0) {
|
|
185
|
+
debugLog(logPrefix, `deduped ${dedupCount} inherited loader segment(s)`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
|
|
127
191
|
/**
|
|
128
192
|
* Build the final MatchResult from collected segments and context
|
|
129
193
|
*/
|
|
@@ -168,13 +232,23 @@ export function buildMatchResult<TEnv>(
|
|
|
168
232
|
// Deduplicate allIds (defense-in-depth for partial match path)
|
|
169
233
|
allIds = [...new Set(allIds)];
|
|
170
234
|
|
|
171
|
-
// Filter out segments
|
|
172
|
-
//
|
|
235
|
+
// Filter out null-component segments only when the client already has
|
|
236
|
+
// them cached (revalidation skip). If the client doesn't have the segment,
|
|
237
|
+
// it must be included even with null component — it's structurally required
|
|
238
|
+
// as a parent node for child layouts/parallels to reconcile against.
|
|
239
|
+
// Loader segments are always included as they carry data.
|
|
240
|
+
const clientIdSet = new Set(ctx.clientSegmentIds);
|
|
173
241
|
segmentsToRender = allSegments.filter(
|
|
174
|
-
(s) =>
|
|
242
|
+
(s) =>
|
|
243
|
+
s.component !== null || s.type === "loader" || !clientIdSet.has(s.id),
|
|
175
244
|
);
|
|
176
245
|
}
|
|
177
246
|
|
|
247
|
+
const dedupedSegments = deduplicateLoaderSegments(
|
|
248
|
+
segmentsToRender,
|
|
249
|
+
logPrefix,
|
|
250
|
+
);
|
|
251
|
+
|
|
178
252
|
debugLog(logPrefix, "all segments", {
|
|
179
253
|
segments: allSegments.map((s) => ({
|
|
180
254
|
id: s.id,
|
|
@@ -183,23 +257,25 @@ export function buildMatchResult<TEnv>(
|
|
|
183
257
|
})),
|
|
184
258
|
});
|
|
185
259
|
debugLog(logPrefix, "segments to render", {
|
|
186
|
-
segmentIds:
|
|
260
|
+
segmentIds: dedupedSegments.map((s) => s.id),
|
|
187
261
|
});
|
|
188
262
|
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
263
|
+
// Remove deduped loader IDs from matched so the client doesn't treat
|
|
264
|
+
// them as missing segments and trigger a fallback refetch.
|
|
265
|
+
const removedIds = new Set(
|
|
266
|
+
segmentsToRender
|
|
267
|
+
.filter((s) => !dedupedSegments.includes(s))
|
|
268
|
+
.map((s) => s.id),
|
|
269
|
+
);
|
|
270
|
+
const matchedIds =
|
|
271
|
+
removedIds.size > 0 ? allIds.filter((id) => !removedIds.has(id)) : allIds;
|
|
195
272
|
|
|
196
273
|
return {
|
|
197
|
-
segments:
|
|
198
|
-
matched:
|
|
199
|
-
diff:
|
|
274
|
+
segments: dedupedSegments,
|
|
275
|
+
matched: matchedIds,
|
|
276
|
+
diff: dedupedSegments.map((s) => s.id),
|
|
200
277
|
params: ctx.matched.params,
|
|
201
278
|
routeName: ctx.routeKey,
|
|
202
|
-
serverTiming,
|
|
203
279
|
slots: Object.keys(state.slots).length > 0 ? state.slots : undefined,
|
|
204
280
|
routeMiddleware:
|
|
205
281
|
ctx.routeMiddleware.length > 0 ? ctx.routeMiddleware : undefined,
|
|
@@ -219,10 +295,19 @@ export async function collectMatchResult<TEnv>(
|
|
|
219
295
|
): Promise<MatchResult> {
|
|
220
296
|
const allSegments = await collectSegments(pipeline);
|
|
221
297
|
|
|
298
|
+
const buildStart = performance.now();
|
|
299
|
+
|
|
222
300
|
// Update state with collected segments if not already set
|
|
223
301
|
if (state.segments.length === 0) {
|
|
224
302
|
state.segments = allSegments;
|
|
225
303
|
}
|
|
226
304
|
|
|
227
|
-
|
|
305
|
+
const result = buildMatchResult(allSegments, ctx, state);
|
|
306
|
+
appendMetric(
|
|
307
|
+
ctx.metricsStore,
|
|
308
|
+
"collect-result",
|
|
309
|
+
buildStart,
|
|
310
|
+
performance.now() - buildStart,
|
|
311
|
+
);
|
|
312
|
+
return result;
|
|
228
313
|
}
|
package/src/router/metrics.ts
CHANGED
|
@@ -4,24 +4,217 @@
|
|
|
4
4
|
* Performance metrics collection and reporting for RSC Router.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { MetricsStore } from "../server/context";
|
|
7
|
+
import type { MetricsStore, PerformanceMetric } from "../server/context";
|
|
8
|
+
|
|
9
|
+
const BASE_INDENT = 2;
|
|
10
|
+
const DEPTH_INDENT = 2;
|
|
11
|
+
const TIMELINE_WIDTH = 40;
|
|
12
|
+
|
|
13
|
+
function formatMs(value: number): string {
|
|
14
|
+
return `${value.toFixed(2)}ms`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sortMetrics(metrics: PerformanceMetric[]): PerformanceMetric[] {
|
|
18
|
+
return [...metrics].sort((a, b) => {
|
|
19
|
+
// handler:total always goes last (it wraps everything)
|
|
20
|
+
if (a.label === "handler:total") return 1;
|
|
21
|
+
if (b.label === "handler:total") return -1;
|
|
22
|
+
return a.startTime - b.startTime;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface Span {
|
|
27
|
+
startTime: number;
|
|
28
|
+
duration: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function renderTimeline(spans: Span[], total: number): string {
|
|
32
|
+
if (TIMELINE_WIDTH <= 0) {
|
|
33
|
+
return "||";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const cells = Array(TIMELINE_WIDTH).fill(".");
|
|
37
|
+
|
|
38
|
+
if (!(total > 0)) {
|
|
39
|
+
cells[0] = "#";
|
|
40
|
+
return `|${cells.join("")}|`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const span of spans) {
|
|
44
|
+
const start = Math.max(0, span.startTime);
|
|
45
|
+
const end = Math.max(start, span.startTime + span.duration);
|
|
46
|
+
const startColumn = Math.min(
|
|
47
|
+
TIMELINE_WIDTH - 1,
|
|
48
|
+
Math.floor((start / total) * TIMELINE_WIDTH),
|
|
49
|
+
);
|
|
50
|
+
const endColumn = Math.max(
|
|
51
|
+
startColumn + 1,
|
|
52
|
+
Math.min(
|
|
53
|
+
TIMELINE_WIDTH,
|
|
54
|
+
Math.ceil((Math.min(total, end) / total) * TIMELINE_WIDTH),
|
|
55
|
+
),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
cells.fill("#", startColumn, endColumn);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return `|${cells.join("")}|`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function createTimelineAxis(total: number): string {
|
|
65
|
+
const totalLabel = formatMs(total);
|
|
66
|
+
return `0ms${" ".repeat(
|
|
67
|
+
Math.max(1, TIMELINE_WIDTH - "0ms".length - totalLabel.length),
|
|
68
|
+
)}${totalLabel}`;
|
|
69
|
+
}
|
|
8
70
|
|
|
9
71
|
/**
|
|
10
|
-
* Create a metrics store for the request if debugPerformance is enabled
|
|
72
|
+
* Create a metrics store for the request if debugPerformance is enabled.
|
|
73
|
+
* An optional `requestStart` timestamp can anchor the store to an earlier
|
|
74
|
+
* point (e.g. handler start) so that handler:total has startTime=0.
|
|
11
75
|
*/
|
|
12
76
|
export function createMetricsStore(
|
|
13
77
|
debugPerformance: boolean,
|
|
78
|
+
requestStart?: number,
|
|
14
79
|
): MetricsStore | undefined {
|
|
15
80
|
if (!debugPerformance) return undefined;
|
|
16
81
|
return {
|
|
17
82
|
enabled: true,
|
|
18
|
-
requestStart: performance.now(),
|
|
83
|
+
requestStart: requestStart ?? performance.now(),
|
|
19
84
|
metrics: [],
|
|
20
85
|
};
|
|
21
86
|
}
|
|
22
87
|
|
|
23
88
|
/**
|
|
24
|
-
*
|
|
89
|
+
* Append a metric to the request store using an absolute start timestamp.
|
|
90
|
+
*/
|
|
91
|
+
export function appendMetric(
|
|
92
|
+
metricsStore: MetricsStore | undefined,
|
|
93
|
+
label: string,
|
|
94
|
+
start: number,
|
|
95
|
+
duration: number,
|
|
96
|
+
depth?: number,
|
|
97
|
+
): void {
|
|
98
|
+
if (!metricsStore) return;
|
|
99
|
+
metricsStore.metrics.push({
|
|
100
|
+
label,
|
|
101
|
+
duration,
|
|
102
|
+
startTime: start - metricsStore.requestStart,
|
|
103
|
+
depth,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Log the current request metrics and return the corresponding Server-Timing value.
|
|
109
|
+
*/
|
|
110
|
+
export function buildMetricsTiming(
|
|
111
|
+
method: string,
|
|
112
|
+
pathname: string,
|
|
113
|
+
metricsStore: MetricsStore | undefined,
|
|
114
|
+
): string | undefined {
|
|
115
|
+
if (!metricsStore) return undefined;
|
|
116
|
+
logMetrics(method, pathname, metricsStore);
|
|
117
|
+
return generateServerTiming(metricsStore) || undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Display row produced by merging :pre/:post metric pairs. */
|
|
121
|
+
interface DisplayRow {
|
|
122
|
+
label: string;
|
|
123
|
+
startTime: number;
|
|
124
|
+
duration: number;
|
|
125
|
+
depth: number | undefined;
|
|
126
|
+
spans: Span[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Build display rows from sorted metrics, merging :pre/:post pairs into
|
|
131
|
+
* a single row with disjoint timeline segments.
|
|
132
|
+
*/
|
|
133
|
+
function buildDisplayRows(sorted: PerformanceMetric[]): DisplayRow[] {
|
|
134
|
+
// Index :pre and :post metrics by their base label
|
|
135
|
+
const preMap = new Map<string, PerformanceMetric>();
|
|
136
|
+
const postMap = new Map<string, PerformanceMetric>();
|
|
137
|
+
const consumed = new Set<PerformanceMetric>();
|
|
138
|
+
|
|
139
|
+
for (const m of sorted) {
|
|
140
|
+
if (m.label.endsWith(":pre")) {
|
|
141
|
+
preMap.set(m.label.slice(0, -4), m);
|
|
142
|
+
} else if (m.label.endsWith(":post")) {
|
|
143
|
+
postMap.set(m.label.slice(0, -5), m);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const rows: DisplayRow[] = [];
|
|
148
|
+
|
|
149
|
+
for (const m of sorted) {
|
|
150
|
+
if (consumed.has(m)) continue;
|
|
151
|
+
|
|
152
|
+
if (m.label.endsWith(":pre")) {
|
|
153
|
+
const base = m.label.slice(0, -4);
|
|
154
|
+
const post = postMap.get(base);
|
|
155
|
+
if (post) {
|
|
156
|
+
// Merge into a single row with two disjoint spans
|
|
157
|
+
consumed.add(m);
|
|
158
|
+
consumed.add(post);
|
|
159
|
+
rows.push({
|
|
160
|
+
label: base,
|
|
161
|
+
startTime: m.startTime,
|
|
162
|
+
duration: m.duration + post.duration,
|
|
163
|
+
depth: m.depth,
|
|
164
|
+
spans: [
|
|
165
|
+
{ startTime: m.startTime, duration: m.duration },
|
|
166
|
+
{ startTime: post.startTime, duration: post.duration },
|
|
167
|
+
],
|
|
168
|
+
});
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
// Lone :pre — display with base label
|
|
172
|
+
consumed.add(m);
|
|
173
|
+
rows.push({
|
|
174
|
+
label: base,
|
|
175
|
+
startTime: m.startTime,
|
|
176
|
+
duration: m.duration,
|
|
177
|
+
depth: m.depth,
|
|
178
|
+
spans: [{ startTime: m.startTime, duration: m.duration }],
|
|
179
|
+
});
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (m.label.endsWith(":post")) {
|
|
184
|
+
const base = m.label.slice(0, -5);
|
|
185
|
+
if (preMap.has(base)) {
|
|
186
|
+
// Already consumed as part of the pair above
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
// Lone :post — display with base label
|
|
190
|
+
consumed.add(m);
|
|
191
|
+
rows.push({
|
|
192
|
+
label: base,
|
|
193
|
+
startTime: m.startTime,
|
|
194
|
+
duration: m.duration,
|
|
195
|
+
depth: m.depth,
|
|
196
|
+
spans: [{ startTime: m.startTime, duration: m.duration }],
|
|
197
|
+
});
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Regular metric
|
|
202
|
+
rows.push({
|
|
203
|
+
label: m.label,
|
|
204
|
+
startTime: m.startTime,
|
|
205
|
+
duration: m.duration,
|
|
206
|
+
depth: m.depth,
|
|
207
|
+
spans: [{ startTime: m.startTime, duration: m.duration }],
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return rows;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Log metrics to console in a formatted way.
|
|
216
|
+
* Uses a shared-axis timeline so overlapping work stays visible.
|
|
217
|
+
* Merges :pre/:post pairs onto one row with disjoint timeline segments.
|
|
25
218
|
*/
|
|
26
219
|
export function logMetrics(
|
|
27
220
|
method: string,
|
|
@@ -30,32 +223,64 @@ export function logMetrics(
|
|
|
30
223
|
): void {
|
|
31
224
|
const total = performance.now() - metricsStore.requestStart;
|
|
32
225
|
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
226
|
+
const sorted = sortMetrics(metricsStore.metrics);
|
|
227
|
+
const displayRows = buildDisplayRows(sorted);
|
|
228
|
+
|
|
229
|
+
const labels = displayRows.map(
|
|
230
|
+
(r) =>
|
|
231
|
+
`${" ".repeat(BASE_INDENT + (r.depth ?? 0) * DEPTH_INDENT)}${r.label}`,
|
|
232
|
+
);
|
|
233
|
+
const startValues = displayRows.map((r) => formatMs(r.startTime));
|
|
234
|
+
const durationValues = displayRows.map((r) => formatMs(r.duration));
|
|
235
|
+
const startWidth = Math.max(
|
|
236
|
+
"start".length,
|
|
237
|
+
...startValues.map((v) => v.length),
|
|
238
|
+
);
|
|
239
|
+
const durationWidth = Math.max(
|
|
240
|
+
"dur".length,
|
|
241
|
+
...durationValues.map((v) => v.length),
|
|
242
|
+
);
|
|
243
|
+
const spanWidth = Math.max(
|
|
244
|
+
"span".length,
|
|
245
|
+
...labels.map((label) => label.length),
|
|
246
|
+
22,
|
|
247
|
+
);
|
|
248
|
+
const timelinePadding = " ".repeat(
|
|
249
|
+
startWidth + 2 + durationWidth + 2 + spanWidth + 2,
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
console.log(`[RSC Perf] ${method} ${pathname} (${total.toFixed(2)}ms)`);
|
|
253
|
+
console.log(
|
|
254
|
+
`${"start".padStart(startWidth)} ${"dur".padStart(durationWidth)} ${"span".padEnd(spanWidth)} timeline`,
|
|
37
255
|
);
|
|
256
|
+
console.log(`${timelinePadding}${createTimelineAxis(total)}`);
|
|
38
257
|
|
|
39
|
-
|
|
258
|
+
for (let index = 0; index < displayRows.length; index++) {
|
|
259
|
+
const row = displayRows[index];
|
|
260
|
+
const label = labels[index].padEnd(spanWidth);
|
|
261
|
+
const start = formatMs(row.startTime).padStart(startWidth);
|
|
262
|
+
const duration = formatMs(row.duration).padStart(durationWidth);
|
|
40
263
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
264
|
+
console.log(
|
|
265
|
+
`${start} ${duration} ${label} ${renderTimeline(row.spans, total)}`,
|
|
266
|
+
);
|
|
44
267
|
}
|
|
45
268
|
}
|
|
46
269
|
|
|
47
270
|
/**
|
|
48
271
|
* Generate Server-Timing header value from metrics
|
|
49
272
|
* Format: metric-name;dur=X.XX
|
|
273
|
+
* Depth is encoded as a "d{N}-" prefix for nested metrics.
|
|
50
274
|
*/
|
|
51
275
|
export function generateServerTiming(metricsStore: MetricsStore): string {
|
|
52
276
|
return metricsStore.metrics
|
|
53
277
|
.map((m) => {
|
|
54
278
|
// Convert label to valid Server-Timing name (alphanumeric and hyphens)
|
|
55
|
-
const
|
|
279
|
+
const base = m.label
|
|
56
280
|
.replace(/:/g, "-")
|
|
57
281
|
.replace(/[^a-zA-Z0-9-]/g, "")
|
|
58
282
|
.toLowerCase();
|
|
283
|
+
const name = m.depth ? `d${m.depth}-${base}` : base;
|
|
59
284
|
return `${name};dur=${m.duration.toFixed(2)}`;
|
|
60
285
|
})
|
|
61
286
|
.join(", ");
|
|
@@ -12,6 +12,9 @@ import type {
|
|
|
12
12
|
DefaultVars,
|
|
13
13
|
} from "../types/global-namespace.js";
|
|
14
14
|
import type { ScopedReverseFunction } from "../reverse.js";
|
|
15
|
+
import type { Theme } from "../theme/types.js";
|
|
16
|
+
import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
|
|
17
|
+
import type { RequestScope } from "../types/request-scope.js";
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* Get variable function type
|
|
@@ -25,8 +28,12 @@ type GetVariableFn = {
|
|
|
25
28
|
* Set variable function type
|
|
26
29
|
*/
|
|
27
30
|
type SetVariableFn = {
|
|
28
|
-
<T>(contextVar: ContextVar<T>, value: T): void;
|
|
29
|
-
<K extends keyof DefaultVars>(
|
|
31
|
+
<T>(contextVar: ContextVar<T>, value: T, options?: { cache?: boolean }): void;
|
|
32
|
+
<K extends keyof DefaultVars>(
|
|
33
|
+
key: K,
|
|
34
|
+
value: DefaultVars[K],
|
|
35
|
+
options?: { cache?: boolean },
|
|
36
|
+
): void;
|
|
30
37
|
};
|
|
31
38
|
|
|
32
39
|
/**
|
|
@@ -51,33 +58,16 @@ export interface CookieOptions {
|
|
|
51
58
|
export interface MiddlewareContext<
|
|
52
59
|
TEnv = any,
|
|
53
60
|
TParams = Record<string, string>,
|
|
54
|
-
> {
|
|
55
|
-
/** Original request */
|
|
56
|
-
request: Request;
|
|
57
|
-
|
|
58
|
-
/** Parsed URL */
|
|
59
|
-
url: URL;
|
|
60
|
-
|
|
61
|
-
/** URL pathname */
|
|
62
|
-
pathname: string;
|
|
63
|
-
|
|
64
|
-
/** URL search params */
|
|
65
|
-
searchParams: URLSearchParams;
|
|
66
|
-
|
|
67
|
-
/** Platform bindings (Cloudflare, etc.) */
|
|
68
|
-
env: TEnv;
|
|
69
|
-
|
|
61
|
+
> extends RequestScope<TEnv> {
|
|
70
62
|
/** URL params extracted from route/middleware pattern */
|
|
71
63
|
params: TParams;
|
|
72
64
|
|
|
73
65
|
/**
|
|
74
|
-
* Response
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* Use `ctx.header()` to set response headers, or `cookies()` for cookie mutations.
|
|
78
|
-
* To replace the response entirely, return a new `Response` from the middleware.
|
|
66
|
+
* Response headers.
|
|
67
|
+
* Before `next()`, returns headers from the shared response stub.
|
|
68
|
+
* After `next()`, returns headers from the downstream response.
|
|
79
69
|
*/
|
|
80
|
-
readonly
|
|
70
|
+
readonly headers: Headers;
|
|
81
71
|
|
|
82
72
|
/** Get a context variable (shared with route handlers) */
|
|
83
73
|
get: GetVariableFn;
|
|
@@ -86,11 +76,10 @@ export interface MiddlewareContext<
|
|
|
86
76
|
set: SetVariableFn;
|
|
87
77
|
|
|
88
78
|
/**
|
|
89
|
-
* Set a response header - can be called before or after `next()
|
|
79
|
+
* Set a response header - can be called before or after `next()`.
|
|
90
80
|
*
|
|
91
81
|
* When called before `next()`, headers are queued and merged into the final response.
|
|
92
82
|
* When called after `next()`, headers are set directly on the response.
|
|
93
|
-
* Shorthand for `ctx.res.headers.set()`.
|
|
94
83
|
*/
|
|
95
84
|
header(name: string, value: string): void;
|
|
96
85
|
|
|
@@ -100,6 +89,38 @@ export interface MiddlewareContext<
|
|
|
100
89
|
*/
|
|
101
90
|
routeName?: DefaultRouteName;
|
|
102
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Enable performance metrics for this request.
|
|
94
|
+
* When called, granular timing breakdown is logged to console and
|
|
95
|
+
* included in the Server-Timing response header, regardless of the
|
|
96
|
+
* router-level `debugPerformance` option.
|
|
97
|
+
*
|
|
98
|
+
* Call **before** `await next()` so the metrics store exists when
|
|
99
|
+
* downstream phases (route matching, rendering, SSR) record their
|
|
100
|
+
* spans. Calling after `next()` returns still emits `handler:total`
|
|
101
|
+
* but misses all upstream metrics.
|
|
102
|
+
*/
|
|
103
|
+
debugPerformance(): void;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Current theme (from cookie or default).
|
|
107
|
+
* Only available when theme is enabled in router config.
|
|
108
|
+
*/
|
|
109
|
+
theme?: Theme;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Set the theme (only available when theme is enabled in router config).
|
|
113
|
+
* Sets a cookie with the new theme value.
|
|
114
|
+
*/
|
|
115
|
+
setTheme?: (theme: Theme) => void;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Attach location state entries to this response.
|
|
119
|
+
* State is delivered to the client via history.pushState and accessible
|
|
120
|
+
* through the useLocationState() hook.
|
|
121
|
+
*/
|
|
122
|
+
setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void;
|
|
123
|
+
|
|
103
124
|
/**
|
|
104
125
|
* Generate URLs from route names.
|
|
105
126
|
* - `name` — global route, from the named-routes definition
|
|
@@ -155,7 +176,7 @@ export interface MiddlewareEntry<TEnv = any> {
|
|
|
155
176
|
}
|
|
156
177
|
|
|
157
178
|
/**
|
|
158
|
-
* Mutable response holder -
|
|
179
|
+
* Mutable response holder - tracks the current response through the middleware chain.
|
|
159
180
|
*/
|
|
160
181
|
export interface ResponseHolder {
|
|
161
182
|
response: Response | null;
|