@rangojs/router 0.0.0-experimental.9 → 0.0.0-experimental.a5f27bd5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -0
- package/README.md +884 -4
- package/dist/bin/rango.js +1531 -155
- package/dist/vite/index.js +4440 -2170
- package/package.json +60 -54
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +50 -21
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +18 -16
- package/skills/fonts/SKILL.md +6 -4
- package/skills/hooks/SKILL.md +333 -71
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +131 -8
- package/skills/layout/SKILL.md +100 -3
- package/skills/links/SKILL.md +74 -15
- package/skills/loader/SKILL.md +388 -38
- package/skills/middleware/SKILL.md +171 -34
- package/skills/mime-routes/SKILL.md +15 -11
- package/skills/parallel/SKILL.md +78 -1
- package/skills/prerender/SKILL.md +405 -45
- package/skills/rango/SKILL.md +85 -21
- package/skills/response-routes/SKILL.md +144 -91
- package/skills/route/SKILL.md +226 -14
- package/skills/router-setup/SKILL.md +123 -30
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +316 -87
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +102 -4
- package/src/bin/rango.ts +312 -15
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +87 -64
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +285 -553
- package/src/browser/navigation-client.ts +123 -73
- package/src/browser/navigation-store.ts +33 -50
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +261 -309
- package/src/browser/prefetch/cache.ts +154 -0
- package/src/browser/prefetch/fetch.ts +135 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +182 -70
- package/src/browser/react/NavigationProvider.tsx +51 -11
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +12 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- package/src/browser/react/mount-context.ts +6 -1
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +29 -70
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +22 -63
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +63 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +80 -97
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +106 -27
- package/src/browser/scroll-restoration.ts +92 -16
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +504 -599
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +107 -47
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +82 -21
- package/src/build/generate-route-types.ts +36 -752
- package/src/build/index.ts +6 -5
- package/src/build/route-trie.ts +39 -13
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +411 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +469 -0
- package/src/build/route-types/scan-filter.ts +78 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +338 -0
- package/src/cache/cache-scope.ts +120 -301
- package/src/cache/cf/cf-cache-store.ts +119 -7
- package/src/cache/cf/index.ts +8 -2
- package/src/cache/document-cache.ts +101 -72
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +0 -15
- package/src/cache/memory-segment-store.ts +191 -13
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +98 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +3 -1
- package/src/client.tsx +84 -126
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +86 -0
- package/src/debug.ts +17 -7
- package/src/errors.ts +77 -7
- package/src/handle.ts +15 -10
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +21 -15
- package/src/host/errors.ts +8 -8
- package/src/host/index.ts +4 -7
- package/src/host/pattern-matcher.ts +27 -27
- package/src/host/router.ts +61 -39
- package/src/host/testing.ts +8 -8
- package/src/host/types.ts +15 -7
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +65 -45
- package/src/index.rsc.ts +133 -21
- package/src/index.ts +164 -52
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +25 -143
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +4 -2
- package/src/prerender/store.ts +158 -13
- package/src/prerender.ts +333 -26
- package/src/reverse.ts +184 -121
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -1431
- package/src/route-map-builder.ts +156 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +48 -9
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +158 -0
- package/src/router/handler-context.ts +374 -81
- package/src/router/intercept-resolution.ts +24 -16
- package/src/router/lazy-includes.ts +234 -0
- package/src/router/loader-resolution.ts +215 -122
- package/src/router/logging.ts +248 -0
- package/src/router/manifest.ts +83 -32
- package/src/router/match-api.ts +118 -119
- package/src/router/match-context.ts +4 -2
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +80 -93
- package/src/router/match-middleware/cache-lookup.ts +336 -84
- package/src/router/match-middleware/cache-store.ts +43 -24
- package/src/router/match-middleware/intercept-resolution.ts +45 -20
- package/src/router/match-middleware/segment-resolution.ts +16 -8
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +34 -28
- package/src/router/metrics.ts +235 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +324 -367
- package/src/router/pattern-matching.ts +197 -41
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +137 -38
- package/src/router/router-context.ts +36 -21
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1239 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -1315
- package/src/router/segment-wrappers.ts +289 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +96 -29
- package/src/router/types.ts +16 -9
- package/src/router.ts +590 -1983
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +661 -1015
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +237 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +38 -11
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +25 -13
- package/src/server/context.ts +173 -48
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +94 -15
- package/src/server/loader-registry.ts +15 -56
- package/src/server/request-context.ts +430 -70
- package/src/server.ts +35 -155
- package/src/ssr/index.tsx +100 -31
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +102 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -1757
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -1282
- package/src/use-loader.tsx +85 -77
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +110 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +11 -1963
- package/src/vite/plugin-types.ts +131 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +254 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +510 -0
- package/src/vite/router-discovery.ts +785 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
- package/CLAUDE.md +0 -43
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/router.gen.ts +0 -6
- package/src/urls.gen.ts +0 -8
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -426
- package/src/vite/expose-location-state-id.ts +0 -177
- package/src/vite/expose-prerender-handler-id.ts +0 -429
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -105,6 +105,7 @@ import type { ResolvedSegment } from "../../types.js";
|
|
|
105
105
|
import type { MatchContext, MatchPipelineState } from "../match-context.js";
|
|
106
106
|
import { getRouterContext } from "../router-context.js";
|
|
107
107
|
import type { GeneratorMiddleware } from "./cache-lookup.js";
|
|
108
|
+
import { debugLog } from "../logging.js";
|
|
108
109
|
|
|
109
110
|
/**
|
|
110
111
|
* Creates intercept resolution middleware
|
|
@@ -117,10 +118,10 @@ import type { GeneratorMiddleware } from "./cache-lookup.js";
|
|
|
117
118
|
*/
|
|
118
119
|
export function withInterceptResolution<TEnv>(
|
|
119
120
|
ctx: MatchContext<TEnv>,
|
|
120
|
-
state: MatchPipelineState
|
|
121
|
+
state: MatchPipelineState,
|
|
121
122
|
): GeneratorMiddleware<ResolvedSegment> {
|
|
122
123
|
return async function* (
|
|
123
|
-
source: AsyncGenerator<ResolvedSegment
|
|
124
|
+
source: AsyncGenerator<ResolvedSegment>,
|
|
124
125
|
): AsyncGenerator<ResolvedSegment> {
|
|
125
126
|
const pipelineStart = performance.now();
|
|
126
127
|
const ms = ctx.metricsStore;
|
|
@@ -135,7 +136,11 @@ export function withInterceptResolution<TEnv>(
|
|
|
135
136
|
// Skip intercept resolution for full match (document requests don't have intercepts)
|
|
136
137
|
if (ctx.isFullMatch) {
|
|
137
138
|
if (ms) {
|
|
138
|
-
ms.metrics.push({
|
|
139
|
+
ms.metrics.push({
|
|
140
|
+
label: "pipeline:intercept",
|
|
141
|
+
duration: performance.now() - pipelineStart,
|
|
142
|
+
startTime: pipelineStart - ms.requestStart,
|
|
143
|
+
});
|
|
139
144
|
}
|
|
140
145
|
return;
|
|
141
146
|
}
|
|
@@ -156,7 +161,11 @@ export function withInterceptResolution<TEnv>(
|
|
|
156
161
|
await handleCacheHitIntercept(ctx, state, segments);
|
|
157
162
|
}
|
|
158
163
|
if (ms) {
|
|
159
|
-
ms.metrics.push({
|
|
164
|
+
ms.metrics.push({
|
|
165
|
+
label: "pipeline:intercept",
|
|
166
|
+
duration: performance.now() - pipelineStart,
|
|
167
|
+
startTime: pipelineStart - ms.requestStart,
|
|
168
|
+
});
|
|
160
169
|
}
|
|
161
170
|
return;
|
|
162
171
|
}
|
|
@@ -165,9 +174,10 @@ export function withInterceptResolution<TEnv>(
|
|
|
165
174
|
const { resolveInterceptEntry } = getRouterContext<TEnv>();
|
|
166
175
|
|
|
167
176
|
const slotName = ctx.interceptResult!.intercept.slotName;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
177
|
+
debugLog("matchPartial.intercept", "intercept resolved", {
|
|
178
|
+
routeName: ctx.localRouteName,
|
|
179
|
+
slotName,
|
|
180
|
+
});
|
|
171
181
|
|
|
172
182
|
// Resolve intercept entry (middleware, loaders, handler)
|
|
173
183
|
const Store = ctx.Store;
|
|
@@ -187,8 +197,8 @@ export function withInterceptResolution<TEnv>(
|
|
|
187
197
|
routeKey: ctx.routeKey,
|
|
188
198
|
actionContext: ctx.actionContext,
|
|
189
199
|
stale: ctx.stale,
|
|
190
|
-
}
|
|
191
|
-
)
|
|
200
|
+
},
|
|
201
|
+
),
|
|
192
202
|
);
|
|
193
203
|
|
|
194
204
|
// Update state
|
|
@@ -204,7 +214,11 @@ export function withInterceptResolution<TEnv>(
|
|
|
204
214
|
}
|
|
205
215
|
|
|
206
216
|
if (ms) {
|
|
207
|
-
ms.metrics.push({
|
|
217
|
+
ms.metrics.push({
|
|
218
|
+
label: "pipeline:intercept",
|
|
219
|
+
duration: performance.now() - pipelineStart,
|
|
220
|
+
startTime: pipelineStart - ms.requestStart,
|
|
221
|
+
});
|
|
208
222
|
}
|
|
209
223
|
};
|
|
210
224
|
}
|
|
@@ -217,7 +231,7 @@ export function withInterceptResolution<TEnv>(
|
|
|
217
231
|
async function handleCacheHitIntercept<TEnv>(
|
|
218
232
|
ctx: MatchContext<TEnv>,
|
|
219
233
|
state: MatchPipelineState,
|
|
220
|
-
segments: ResolvedSegment[]
|
|
234
|
+
segments: ResolvedSegment[],
|
|
221
235
|
): Promise<void> {
|
|
222
236
|
if (!ctx.interceptResult) return;
|
|
223
237
|
|
|
@@ -227,7 +241,7 @@ async function handleCacheHitIntercept<TEnv>(
|
|
|
227
241
|
|
|
228
242
|
// Find intercept segments from cached segments (namespace starts with "intercept:")
|
|
229
243
|
const interceptSegments = segments.filter((s) =>
|
|
230
|
-
s.namespace?.startsWith("intercept:")
|
|
244
|
+
s.namespace?.startsWith("intercept:"),
|
|
231
245
|
);
|
|
232
246
|
state.interceptSegments = interceptSegments;
|
|
233
247
|
|
|
@@ -251,25 +265,36 @@ async function handleCacheHitIntercept<TEnv>(
|
|
|
251
265
|
routeKey: ctx.routeKey,
|
|
252
266
|
actionContext: ctx.actionContext,
|
|
253
267
|
stale: ctx.stale,
|
|
254
|
-
}
|
|
255
|
-
)
|
|
268
|
+
},
|
|
269
|
+
),
|
|
256
270
|
);
|
|
257
271
|
|
|
258
272
|
// Update intercept segment's loaderDataPromise with fresh data
|
|
259
273
|
if (freshLoaderResult) {
|
|
260
274
|
const interceptMainSegment = interceptSegments.find(
|
|
261
|
-
(s) => s.type === "parallel" && s.slot
|
|
275
|
+
(s) => s.type === "parallel" && s.slot,
|
|
262
276
|
);
|
|
263
277
|
if (interceptMainSegment) {
|
|
264
|
-
interceptMainSegment.loaderDataPromise =
|
|
278
|
+
interceptMainSegment.loaderDataPromise =
|
|
279
|
+
freshLoaderResult.loaderDataPromise;
|
|
265
280
|
interceptMainSegment.loaderIds = freshLoaderResult.loaderIds;
|
|
266
|
-
|
|
267
|
-
|
|
281
|
+
debugLog(
|
|
282
|
+
"matchPartial.intercept",
|
|
283
|
+
"cache hit with fresh intercept loaders",
|
|
284
|
+
{
|
|
285
|
+
routeName: ctx.localRouteName,
|
|
286
|
+
slotName,
|
|
287
|
+
},
|
|
268
288
|
);
|
|
269
289
|
}
|
|
270
290
|
} else {
|
|
271
|
-
|
|
272
|
-
|
|
291
|
+
debugLog(
|
|
292
|
+
"matchPartial.intercept",
|
|
293
|
+
"cache hit without intercept loader revalidation",
|
|
294
|
+
{
|
|
295
|
+
routeName: ctx.localRouteName,
|
|
296
|
+
slotName,
|
|
297
|
+
},
|
|
273
298
|
);
|
|
274
299
|
}
|
|
275
300
|
}
|
|
@@ -99,10 +99,10 @@ import type { GeneratorMiddleware } from "./cache-lookup.js";
|
|
|
99
99
|
*/
|
|
100
100
|
export function withSegmentResolution<TEnv>(
|
|
101
101
|
ctx: MatchContext<TEnv>,
|
|
102
|
-
state: MatchPipelineState
|
|
102
|
+
state: MatchPipelineState,
|
|
103
103
|
): GeneratorMiddleware<ResolvedSegment> {
|
|
104
104
|
return async function* (
|
|
105
|
-
source: AsyncGenerator<ResolvedSegment
|
|
105
|
+
source: AsyncGenerator<ResolvedSegment>,
|
|
106
106
|
): AsyncGenerator<ResolvedSegment> {
|
|
107
107
|
const pipelineStart = performance.now();
|
|
108
108
|
const ms = ctx.metricsStore;
|
|
@@ -116,7 +116,11 @@ export function withSegmentResolution<TEnv>(
|
|
|
116
116
|
// If cache hit, segments were already yielded by cache lookup
|
|
117
117
|
if (state.cacheHit) {
|
|
118
118
|
if (ms) {
|
|
119
|
-
ms.metrics.push({
|
|
119
|
+
ms.metrics.push({
|
|
120
|
+
label: "pipeline:segment-resolve",
|
|
121
|
+
duration: performance.now() - pipelineStart,
|
|
122
|
+
startTime: pipelineStart - ms.requestStart,
|
|
123
|
+
});
|
|
120
124
|
}
|
|
121
125
|
return;
|
|
122
126
|
}
|
|
@@ -134,8 +138,8 @@ export function withSegmentResolution<TEnv>(
|
|
|
134
138
|
ctx.routeKey,
|
|
135
139
|
ctx.matched.params,
|
|
136
140
|
ctx.handlerContext,
|
|
137
|
-
ctx.loaderPromises
|
|
138
|
-
)
|
|
141
|
+
ctx.loaderPromises,
|
|
142
|
+
),
|
|
139
143
|
);
|
|
140
144
|
|
|
141
145
|
// Update state with resolved segments
|
|
@@ -163,8 +167,8 @@ export function withSegmentResolution<TEnv>(
|
|
|
163
167
|
ctx.actionContext,
|
|
164
168
|
ctx.interceptResult,
|
|
165
169
|
ctx.localRouteName,
|
|
166
|
-
ctx.pathname
|
|
167
|
-
)
|
|
170
|
+
ctx.pathname,
|
|
171
|
+
),
|
|
168
172
|
);
|
|
169
173
|
|
|
170
174
|
// Update state with resolved segments
|
|
@@ -178,7 +182,11 @@ export function withSegmentResolution<TEnv>(
|
|
|
178
182
|
}
|
|
179
183
|
|
|
180
184
|
if (ms) {
|
|
181
|
-
ms.metrics.push({
|
|
185
|
+
ms.metrics.push({
|
|
186
|
+
label: "pipeline:segment-resolve",
|
|
187
|
+
duration: performance.now() - pipelineStart,
|
|
188
|
+
startTime: pipelineStart - ms.requestStart,
|
|
189
|
+
});
|
|
182
190
|
}
|
|
183
191
|
};
|
|
184
192
|
}
|
|
@@ -86,19 +86,14 @@
|
|
|
86
86
|
* -> output: cached segments + fresh loader data
|
|
87
87
|
*
|
|
88
88
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* 2. createMatchPartialPipeline (Partial Match)
|
|
98
|
-
* - Used for client-side navigation
|
|
99
|
-
* - Includes revalidation for SWR
|
|
100
|
-
* - Compares with previous params/URL
|
|
101
|
-
* - Supports intercepts (soft navigation modals)
|
|
89
|
+
* PIPELINE VARIANT
|
|
90
|
+
* ================
|
|
91
|
+
*
|
|
92
|
+
* createMatchPartialPipeline handles both full (document) and partial
|
|
93
|
+
* (navigation) requests. The middleware steps adapt based on ctx.isFullMatch:
|
|
94
|
+
* - cache-lookup/store work for both
|
|
95
|
+
* - background-revalidation is a no-op for full matches (no stale state)
|
|
96
|
+
* - intercept-resolution is a no-op for full matches (no previous navigation)
|
|
102
97
|
*/
|
|
103
98
|
import type { ResolvedSegment } from "../types.js";
|
|
104
99
|
import type { MatchContext, MatchPipelineState } from "./match-context.js";
|
|
@@ -163,7 +158,7 @@ export async function* empty<T>(): AsyncGenerator<T> {
|
|
|
163
158
|
*/
|
|
164
159
|
export function createMatchPartialPipeline<TEnv>(
|
|
165
160
|
ctx: MatchContext<TEnv>,
|
|
166
|
-
state: MatchPipelineState
|
|
161
|
+
state: MatchPipelineState,
|
|
167
162
|
): AsyncGenerator<ResolvedSegment> {
|
|
168
163
|
// Build the middleware chain
|
|
169
164
|
const pipeline = compose<ResolvedSegment>(
|
|
@@ -176,39 +171,9 @@ export function createMatchPartialPipeline<TEnv>(
|
|
|
176
171
|
// Resolves segments on cache miss
|
|
177
172
|
withSegmentResolution(ctx, state),
|
|
178
173
|
// Innermost - checks cache first
|
|
179
|
-
withCacheLookup(ctx, state)
|
|
174
|
+
withCacheLookup(ctx, state),
|
|
180
175
|
);
|
|
181
176
|
|
|
182
177
|
// Start with empty source - cache lookup or segment resolution will produce segments
|
|
183
178
|
return pipeline(empty());
|
|
184
179
|
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Create the full match pipeline (simpler, no revalidation)
|
|
188
|
-
*
|
|
189
|
-
* Used for document requests (initial page load) where we don't need
|
|
190
|
-
* revalidation logic since there's no previous state to compare against.
|
|
191
|
-
*/
|
|
192
|
-
export function createMatchPipeline<TEnv>(
|
|
193
|
-
ctx: MatchContext<TEnv>,
|
|
194
|
-
state: MatchPipelineState
|
|
195
|
-
): AsyncGenerator<ResolvedSegment> {
|
|
196
|
-
// For full match, we only need:
|
|
197
|
-
// 1. Cache lookup
|
|
198
|
-
// 2. Segment resolution (without revalidation)
|
|
199
|
-
// 3. Intercept resolution
|
|
200
|
-
// 4. Cache store
|
|
201
|
-
|
|
202
|
-
// Note: Full match uses different resolution logic (resolveAllSegments instead of
|
|
203
|
-
// resolveAllSegmentsWithRevalidation). This will be handled by the segment resolution
|
|
204
|
-
// middleware checking ctx.isFullMatch or similar flag.
|
|
205
|
-
|
|
206
|
-
const pipeline = compose<ResolvedSegment>(
|
|
207
|
-
withCacheStore(ctx, state),
|
|
208
|
-
withInterceptResolution(ctx, state),
|
|
209
|
-
withSegmentResolution(ctx, state),
|
|
210
|
-
withCacheLookup(ctx, state)
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
return pipeline(empty());
|
|
214
|
-
}
|
|
@@ -108,13 +108,13 @@
|
|
|
108
108
|
*/
|
|
109
109
|
import type { MatchResult, ResolvedSegment } from "../types.js";
|
|
110
110
|
import type { MatchContext, MatchPipelineState } from "./match-context.js";
|
|
111
|
-
import {
|
|
111
|
+
import { debugLog } from "./logging.js";
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
114
|
* Collect all segments from an async generator
|
|
115
115
|
*/
|
|
116
116
|
export async function collectSegments(
|
|
117
|
-
generator: AsyncGenerator<ResolvedSegment
|
|
117
|
+
generator: AsyncGenerator<ResolvedSegment>,
|
|
118
118
|
): Promise<ResolvedSegment[]> {
|
|
119
119
|
const segments: ResolvedSegment[] = [];
|
|
120
120
|
for await (const segment of generator) {
|
|
@@ -129,17 +129,30 @@ export async function collectSegments(
|
|
|
129
129
|
export function buildMatchResult<TEnv>(
|
|
130
130
|
allSegments: ResolvedSegment[],
|
|
131
131
|
ctx: MatchContext<TEnv>,
|
|
132
|
-
state: MatchPipelineState
|
|
132
|
+
state: MatchPipelineState,
|
|
133
133
|
): MatchResult {
|
|
134
|
-
const logPrefix = ctx.isFullMatch
|
|
134
|
+
const logPrefix = ctx.isFullMatch
|
|
135
|
+
? "[Router.match]"
|
|
136
|
+
: "[Router.matchPartial]";
|
|
135
137
|
|
|
136
138
|
let allIds: string[];
|
|
137
139
|
let segmentsToRender: ResolvedSegment[];
|
|
138
140
|
|
|
139
141
|
if (ctx.isFullMatch) {
|
|
140
142
|
// Full match (document request) - all segments are rendered
|
|
141
|
-
|
|
142
|
-
|
|
143
|
+
// Deduplicate by segment ID (defense-in-depth). The primary dedup is in
|
|
144
|
+
// resolveAllSegments, but this guards against any path that bypasses it.
|
|
145
|
+
// include() scopes can produce entries that resolve the same shared layout,
|
|
146
|
+
// and duplicate IDs change the client's React tree depth causing remounts.
|
|
147
|
+
const seen = new Set<string>();
|
|
148
|
+
segmentsToRender = [];
|
|
149
|
+
for (const s of allSegments) {
|
|
150
|
+
if (!seen.has(s.id)) {
|
|
151
|
+
seen.add(s.id);
|
|
152
|
+
segmentsToRender.push(s);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
allIds = segmentsToRender.map((s) => s.id);
|
|
143
156
|
} else {
|
|
144
157
|
// Partial match (navigation) - filter and handle intercepts
|
|
145
158
|
// When intercepting, tell browser to keep its current segments + add modal
|
|
@@ -151,32 +164,26 @@ export function buildMatchResult<TEnv>(
|
|
|
151
164
|
: allSegments.map((s) => s.id) // Use actual segments, not matchedIds
|
|
152
165
|
: [...state.matchedIds, ...state.interceptSegments.map((s) => s.id)];
|
|
153
166
|
|
|
167
|
+
// Deduplicate allIds (defense-in-depth for partial match path)
|
|
168
|
+
allIds = [...new Set(allIds)];
|
|
169
|
+
|
|
154
170
|
// Filter out segments with null components (client already has them)
|
|
155
171
|
// BUT always include loader segments - they carry data even with null component
|
|
156
172
|
segmentsToRender = allSegments.filter(
|
|
157
|
-
(s) => s.component !== null || s.type === "loader"
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (process.env.NODE_ENV === "development") {
|
|
162
|
-
console.log(
|
|
163
|
-
`${logPrefix} All segments:`,
|
|
164
|
-
allSegments
|
|
165
|
-
.map((s) => `${s.id}(${s.type}, component=${s.component !== null})`)
|
|
166
|
-
.join(", ")
|
|
167
|
-
);
|
|
168
|
-
console.log(
|
|
169
|
-
`${logPrefix} Segments to render:`,
|
|
170
|
-
segmentsToRender.map((s) => s.id).join(", ")
|
|
173
|
+
(s) => s.component !== null || s.type === "loader",
|
|
171
174
|
);
|
|
172
175
|
}
|
|
173
176
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
debugLog(logPrefix, "all segments", {
|
|
178
|
+
segments: allSegments.map((s) => ({
|
|
179
|
+
id: s.id,
|
|
180
|
+
type: s.type,
|
|
181
|
+
hasComponent: s.component !== null,
|
|
182
|
+
})),
|
|
183
|
+
});
|
|
184
|
+
debugLog(logPrefix, "segments to render", {
|
|
185
|
+
segmentIds: segmentsToRender.map((s) => s.id),
|
|
186
|
+
});
|
|
180
187
|
|
|
181
188
|
return {
|
|
182
189
|
segments: segmentsToRender,
|
|
@@ -184,7 +191,6 @@ export function buildMatchResult<TEnv>(
|
|
|
184
191
|
diff: segmentsToRender.map((s) => s.id),
|
|
185
192
|
params: ctx.matched.params,
|
|
186
193
|
routeName: ctx.routeKey,
|
|
187
|
-
serverTiming,
|
|
188
194
|
slots: Object.keys(state.slots).length > 0 ? state.slots : undefined,
|
|
189
195
|
routeMiddleware:
|
|
190
196
|
ctx.routeMiddleware.length > 0 ? ctx.routeMiddleware : undefined,
|
|
@@ -200,7 +206,7 @@ export function buildMatchResult<TEnv>(
|
|
|
200
206
|
export async function collectMatchResult<TEnv>(
|
|
201
207
|
pipeline: AsyncGenerator<ResolvedSegment>,
|
|
202
208
|
ctx: MatchContext<TEnv>,
|
|
203
|
-
state: MatchPipelineState
|
|
209
|
+
state: MatchPipelineState,
|
|
204
210
|
): Promise<MatchResult> {
|
|
205
211
|
const allSegments = await collectSegments(pipeline);
|
|
206
212
|
|
package/src/router/metrics.ts
CHANGED
|
@@ -4,58 +4,278 @@
|
|
|
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) => a.startTime - b.startTime);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface Span {
|
|
22
|
+
startTime: number;
|
|
23
|
+
duration: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function renderTimeline(spans: Span[], total: number): string {
|
|
27
|
+
if (TIMELINE_WIDTH <= 0) {
|
|
28
|
+
return "||";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const cells = Array(TIMELINE_WIDTH).fill(".");
|
|
32
|
+
|
|
33
|
+
if (!(total > 0)) {
|
|
34
|
+
cells[0] = "#";
|
|
35
|
+
return `|${cells.join("")}|`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const span of spans) {
|
|
39
|
+
const start = Math.max(0, span.startTime);
|
|
40
|
+
const end = Math.max(start, span.startTime + span.duration);
|
|
41
|
+
const startColumn = Math.min(
|
|
42
|
+
TIMELINE_WIDTH - 1,
|
|
43
|
+
Math.floor((start / total) * TIMELINE_WIDTH),
|
|
44
|
+
);
|
|
45
|
+
const endColumn = Math.max(
|
|
46
|
+
startColumn + 1,
|
|
47
|
+
Math.min(
|
|
48
|
+
TIMELINE_WIDTH,
|
|
49
|
+
Math.ceil((Math.min(total, end) / total) * TIMELINE_WIDTH),
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
cells.fill("#", startColumn, endColumn);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return `|${cells.join("")}|`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createTimelineAxis(total: number): string {
|
|
60
|
+
const totalLabel = formatMs(total);
|
|
61
|
+
return `0ms${" ".repeat(
|
|
62
|
+
Math.max(1, TIMELINE_WIDTH - "0ms".length - totalLabel.length),
|
|
63
|
+
)}${totalLabel}`;
|
|
64
|
+
}
|
|
8
65
|
|
|
9
66
|
/**
|
|
10
|
-
* Create a metrics store for the request if debugPerformance is enabled
|
|
67
|
+
* Create a metrics store for the request if debugPerformance is enabled.
|
|
68
|
+
* An optional `requestStart` timestamp can anchor the store to an earlier
|
|
69
|
+
* point (e.g. handler start) so that handler:total has startTime=0.
|
|
11
70
|
*/
|
|
12
71
|
export function createMetricsStore(
|
|
13
|
-
debugPerformance: boolean
|
|
72
|
+
debugPerformance: boolean,
|
|
73
|
+
requestStart?: number,
|
|
14
74
|
): MetricsStore | undefined {
|
|
15
75
|
if (!debugPerformance) return undefined;
|
|
16
76
|
return {
|
|
17
77
|
enabled: true,
|
|
18
|
-
requestStart: performance.now(),
|
|
78
|
+
requestStart: requestStart ?? performance.now(),
|
|
19
79
|
metrics: [],
|
|
20
80
|
};
|
|
21
81
|
}
|
|
22
82
|
|
|
23
83
|
/**
|
|
24
|
-
*
|
|
84
|
+
* Append a metric to the request store using an absolute start timestamp.
|
|
85
|
+
*/
|
|
86
|
+
export function appendMetric(
|
|
87
|
+
metricsStore: MetricsStore | undefined,
|
|
88
|
+
label: string,
|
|
89
|
+
start: number,
|
|
90
|
+
duration: number,
|
|
91
|
+
depth?: number,
|
|
92
|
+
): void {
|
|
93
|
+
if (!metricsStore) return;
|
|
94
|
+
metricsStore.metrics.push({
|
|
95
|
+
label,
|
|
96
|
+
duration,
|
|
97
|
+
startTime: start - metricsStore.requestStart,
|
|
98
|
+
depth,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Log the current request metrics and return the corresponding Server-Timing value.
|
|
104
|
+
*/
|
|
105
|
+
export function buildMetricsTiming(
|
|
106
|
+
method: string,
|
|
107
|
+
pathname: string,
|
|
108
|
+
metricsStore: MetricsStore | undefined,
|
|
109
|
+
): string | undefined {
|
|
110
|
+
if (!metricsStore) return undefined;
|
|
111
|
+
logMetrics(method, pathname, metricsStore);
|
|
112
|
+
return generateServerTiming(metricsStore) || undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Display row produced by merging :pre/:post metric pairs. */
|
|
116
|
+
interface DisplayRow {
|
|
117
|
+
label: string;
|
|
118
|
+
startTime: number;
|
|
119
|
+
duration: number;
|
|
120
|
+
depth: number | undefined;
|
|
121
|
+
spans: Span[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Build display rows from sorted metrics, merging :pre/:post pairs into
|
|
126
|
+
* a single row with disjoint timeline segments.
|
|
127
|
+
*/
|
|
128
|
+
function buildDisplayRows(sorted: PerformanceMetric[]): DisplayRow[] {
|
|
129
|
+
// Index :pre and :post metrics by their base label
|
|
130
|
+
const preMap = new Map<string, PerformanceMetric>();
|
|
131
|
+
const postMap = new Map<string, PerformanceMetric>();
|
|
132
|
+
const consumed = new Set<PerformanceMetric>();
|
|
133
|
+
|
|
134
|
+
for (const m of sorted) {
|
|
135
|
+
if (m.label.endsWith(":pre")) {
|
|
136
|
+
preMap.set(m.label.slice(0, -4), m);
|
|
137
|
+
} else if (m.label.endsWith(":post")) {
|
|
138
|
+
postMap.set(m.label.slice(0, -5), m);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const rows: DisplayRow[] = [];
|
|
143
|
+
|
|
144
|
+
for (const m of sorted) {
|
|
145
|
+
if (consumed.has(m)) continue;
|
|
146
|
+
|
|
147
|
+
if (m.label.endsWith(":pre")) {
|
|
148
|
+
const base = m.label.slice(0, -4);
|
|
149
|
+
const post = postMap.get(base);
|
|
150
|
+
if (post) {
|
|
151
|
+
// Merge into a single row with two disjoint spans
|
|
152
|
+
consumed.add(m);
|
|
153
|
+
consumed.add(post);
|
|
154
|
+
rows.push({
|
|
155
|
+
label: base,
|
|
156
|
+
startTime: m.startTime,
|
|
157
|
+
duration: m.duration + post.duration,
|
|
158
|
+
depth: m.depth,
|
|
159
|
+
spans: [
|
|
160
|
+
{ startTime: m.startTime, duration: m.duration },
|
|
161
|
+
{ startTime: post.startTime, duration: post.duration },
|
|
162
|
+
],
|
|
163
|
+
});
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
// Lone :pre — display with base label
|
|
167
|
+
consumed.add(m);
|
|
168
|
+
rows.push({
|
|
169
|
+
label: base,
|
|
170
|
+
startTime: m.startTime,
|
|
171
|
+
duration: m.duration,
|
|
172
|
+
depth: m.depth,
|
|
173
|
+
spans: [{ startTime: m.startTime, duration: m.duration }],
|
|
174
|
+
});
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (m.label.endsWith(":post")) {
|
|
179
|
+
const base = m.label.slice(0, -5);
|
|
180
|
+
if (preMap.has(base)) {
|
|
181
|
+
// Already consumed as part of the pair above
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
// Lone :post — display with base label
|
|
185
|
+
consumed.add(m);
|
|
186
|
+
rows.push({
|
|
187
|
+
label: base,
|
|
188
|
+
startTime: m.startTime,
|
|
189
|
+
duration: m.duration,
|
|
190
|
+
depth: m.depth,
|
|
191
|
+
spans: [{ startTime: m.startTime, duration: m.duration }],
|
|
192
|
+
});
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Regular metric
|
|
197
|
+
rows.push({
|
|
198
|
+
label: m.label,
|
|
199
|
+
startTime: m.startTime,
|
|
200
|
+
duration: m.duration,
|
|
201
|
+
depth: m.depth,
|
|
202
|
+
spans: [{ startTime: m.startTime, duration: m.duration }],
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return rows;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Log metrics to console in a formatted way.
|
|
211
|
+
* Uses a shared-axis timeline so overlapping work stays visible.
|
|
212
|
+
* Merges :pre/:post pairs onto one row with disjoint timeline segments.
|
|
25
213
|
*/
|
|
26
214
|
export function logMetrics(
|
|
27
215
|
method: string,
|
|
28
216
|
pathname: string,
|
|
29
|
-
metricsStore: MetricsStore
|
|
217
|
+
metricsStore: MetricsStore,
|
|
30
218
|
): void {
|
|
31
219
|
const total = performance.now() - metricsStore.requestStart;
|
|
32
220
|
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
221
|
+
const sorted = sortMetrics(metricsStore.metrics);
|
|
222
|
+
const displayRows = buildDisplayRows(sorted);
|
|
223
|
+
|
|
224
|
+
const labels = displayRows.map(
|
|
225
|
+
(r) =>
|
|
226
|
+
`${" ".repeat(BASE_INDENT + (r.depth ?? 0) * DEPTH_INDENT)}${r.label}`,
|
|
227
|
+
);
|
|
228
|
+
const startValues = displayRows.map((r) => formatMs(r.startTime));
|
|
229
|
+
const durationValues = displayRows.map((r) => formatMs(r.duration));
|
|
230
|
+
const startWidth = Math.max(
|
|
231
|
+
"start".length,
|
|
232
|
+
...startValues.map((v) => v.length),
|
|
233
|
+
);
|
|
234
|
+
const durationWidth = Math.max(
|
|
235
|
+
"dur".length,
|
|
236
|
+
...durationValues.map((v) => v.length),
|
|
237
|
+
);
|
|
238
|
+
const spanWidth = Math.max(
|
|
239
|
+
"span".length,
|
|
240
|
+
...labels.map((label) => label.length),
|
|
241
|
+
22,
|
|
242
|
+
);
|
|
243
|
+
const timelinePadding = " ".repeat(
|
|
244
|
+
startWidth + 2 + durationWidth + 2 + spanWidth + 2,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
console.log(`[RSC Perf] ${method} ${pathname} (${total.toFixed(2)}ms)`);
|
|
248
|
+
console.log(
|
|
249
|
+
`${"start".padStart(startWidth)} ${"dur".padStart(durationWidth)} ${"span".padEnd(spanWidth)} timeline`,
|
|
37
250
|
);
|
|
251
|
+
console.log(`${timelinePadding}${createTimelineAxis(total)}`);
|
|
38
252
|
|
|
39
|
-
|
|
253
|
+
for (let index = 0; index < displayRows.length; index++) {
|
|
254
|
+
const row = displayRows[index];
|
|
255
|
+
const label = labels[index].padEnd(spanWidth);
|
|
256
|
+
const start = formatMs(row.startTime).padStart(startWidth);
|
|
257
|
+
const duration = formatMs(row.duration).padStart(durationWidth);
|
|
40
258
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
259
|
+
console.log(
|
|
260
|
+
`${start} ${duration} ${label} ${renderTimeline(row.spans, total)}`,
|
|
261
|
+
);
|
|
44
262
|
}
|
|
45
263
|
}
|
|
46
264
|
|
|
47
265
|
/**
|
|
48
266
|
* Generate Server-Timing header value from metrics
|
|
49
267
|
* Format: metric-name;dur=X.XX
|
|
268
|
+
* Depth is encoded as a "d{N}-" prefix for nested metrics.
|
|
50
269
|
*/
|
|
51
270
|
export function generateServerTiming(metricsStore: MetricsStore): string {
|
|
52
271
|
return metricsStore.metrics
|
|
53
272
|
.map((m) => {
|
|
54
273
|
// Convert label to valid Server-Timing name (alphanumeric and hyphens)
|
|
55
|
-
const
|
|
274
|
+
const base = m.label
|
|
56
275
|
.replace(/:/g, "-")
|
|
57
276
|
.replace(/[^a-zA-Z0-9-]/g, "")
|
|
58
277
|
.toLowerCase();
|
|
278
|
+
const name = m.depth ? `d${m.depth}-${base}` : base;
|
|
59
279
|
return `${name};dur=${m.duration.toFixed(2)}`;
|
|
60
280
|
})
|
|
61
281
|
.join(", ");
|