@rangojs/router 0.0.0-experimental.7 → 0.0.0-experimental.70
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 +9 -0
- package/README.md +942 -4
- package/dist/bin/rango.js +1689 -0
- package/dist/vite/index.js +4951 -930
- package/package.json +70 -60
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +294 -0
- package/skills/caching/SKILL.md +93 -23
- 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 +167 -0
- package/skills/hooks/SKILL.md +334 -72
- 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 +92 -31
- package/skills/loader/SKILL.md +404 -44
- package/skills/middleware/SKILL.md +173 -34
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +204 -1
- package/skills/prerender/SKILL.md +685 -0
- package/skills/rango/SKILL.md +85 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +257 -14
- package/skills/router-setup/SKILL.md +210 -32
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +328 -89
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +102 -4
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +92 -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 +296 -558
- package/src/browser/navigation-client.ts +179 -69
- package/src/browser/navigation-store.ts +73 -55
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +328 -313
- package/src/browser/prefetch/cache.ts +206 -0
- package/src/browser/prefetch/fetch.ts +150 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +160 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +230 -74
- package/src/browser/react/NavigationProvider.tsx +87 -11
- package/src/browser/react/context.ts +11 -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 +30 -126
- package/src/browser/react/use-href.tsx +2 -2
- 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 +76 -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 +214 -58
- package/src/browser/scroll-restoration.ts +127 -52
- package/src/browser/segment-reconciler.ts +221 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +510 -603
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +141 -48
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +235 -24
- package/src/build/generate-route-types.ts +39 -0
- package/src/build/index.ts +13 -0
- package/src/build/route-trie.ts +265 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +418 -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 +618 -0
- package/src/build/route-types/scan-filter.ts +85 -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 +342 -0
- package/src/cache/cache-scope.ts +167 -309
- package/src/cache/cf/cf-cache-store.ts +571 -17
- package/src/cache/cf/index.ts +13 -3
- package/src/cache/document-cache.ts +116 -77
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +1 -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 +153 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +3 -1
- package/src/client.tsx +105 -179
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +156 -0
- package/src/debug.ts +19 -9
- package/src/errors.ts +108 -2
- package/src/handle.ts +55 -29
- 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 +119 -29
- package/src/index.rsc.ts +155 -19
- package/src/index.ts +223 -30
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +26 -157
- 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 +37 -0
- package/src/prerender/store.ts +186 -0
- package/src/prerender.ts +524 -0
- package/src/reverse.ts +351 -0
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +982 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +434 -0
- package/src/route-definition/index.ts +55 -0
- package/src/route-definition/redirect.ts +101 -0
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-definition.ts +1 -1428
- package/src/route-map-builder.ts +217 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +70 -8
- package/src/router/content-negotiation.ts +215 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +435 -86
- package/src/router/intercept-resolution.ts +402 -0
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +356 -128
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +154 -35
- package/src/router/match-api.ts +555 -0
- package/src/router/match-context.ts +5 -3
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +108 -93
- package/src/router/match-middleware/cache-lookup.ts +459 -10
- package/src/router/match-middleware/cache-store.ts +98 -26
- package/src/router/match-middleware/intercept-resolution.ts +57 -17
- package/src/router/match-middleware/segment-resolution.ts +80 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +55 -33
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +220 -0
- package/src/router/middleware.ts +324 -369
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +211 -43
- package/src/router/prerender-match.ts +502 -0
- package/src/router/preview-match.ts +98 -0
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +137 -38
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +41 -21
- package/src/router/router-interfaces.ts +484 -0
- package/src/router/router-options.ts +618 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +743 -0
- package/src/router/segment-resolution/helpers.ts +268 -0
- package/src/router/segment-resolution/loader-cache.ts +199 -0
- package/src/router/segment-resolution/revalidation.ts +1373 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +239 -0
- package/src/router/types.ts +78 -3
- package/src/router.ts +740 -4252
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +907 -797
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +229 -0
- package/src/rsc/manifest-init.ts +90 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +391 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +246 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +356 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +46 -11
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +165 -17
- package/src/server/context.ts +315 -58
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +113 -15
- package/src/server/loader-registry.ts +24 -64
- package/src/server/request-context.ts +607 -81
- package/src/server.ts +35 -130
- package/src/ssr/index.tsx +103 -30
- package/src/static-handler.ts +126 -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 +791 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +210 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +109 -0
- package/src/types/segments.ts +150 -0
- package/src/types.ts +1 -1623
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +346 -0
- package/src/urls/path-helper.ts +364 -0
- package/src/urls/pattern-types.ts +107 -0
- package/src/urls/response-types.ts +116 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -802
- package/src/use-loader.tsx +161 -81
- package/src/vite/discovery/bundle-postprocess.ts +181 -0
- package/src/vite/discovery/discover-routers.ts +348 -0
- package/src/vite/discovery/prerender-collection.ts +439 -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 +117 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +15 -1129
- package/src/vite/plugin-types.ts +103 -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 -53
- package/src/vite/plugins/expose-id-utils.ts +299 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +209 -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 +786 -0
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -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 +266 -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 +462 -0
- package/src/vite/router-discovery.ts +918 -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 +207 -0
- package/src/vite/utils/shared-utils.ts +170 -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/href.ts +0 -255
- package/src/server/route-manifest-cache.ts +0 -173
- 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/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -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,13 +109,14 @@
|
|
|
108
109
|
*/
|
|
109
110
|
import type { MatchResult, ResolvedSegment } from "../types.js";
|
|
110
111
|
import type { MatchContext, MatchPipelineState } from "./match-context.js";
|
|
111
|
-
import {
|
|
112
|
+
import { debugLog } from "./logging.js";
|
|
113
|
+
import { appendMetric } from "./metrics.js";
|
|
112
114
|
|
|
113
115
|
/**
|
|
114
116
|
* Collect all segments from an async generator
|
|
115
117
|
*/
|
|
116
118
|
export async function collectSegments(
|
|
117
|
-
generator: AsyncGenerator<ResolvedSegment
|
|
119
|
+
generator: AsyncGenerator<ResolvedSegment>,
|
|
118
120
|
): Promise<ResolvedSegment[]> {
|
|
119
121
|
const segments: ResolvedSegment[] = [];
|
|
120
122
|
for await (const segment of generator) {
|
|
@@ -129,17 +131,30 @@ export async function collectSegments(
|
|
|
129
131
|
export function buildMatchResult<TEnv>(
|
|
130
132
|
allSegments: ResolvedSegment[],
|
|
131
133
|
ctx: MatchContext<TEnv>,
|
|
132
|
-
state: MatchPipelineState
|
|
134
|
+
state: MatchPipelineState,
|
|
133
135
|
): MatchResult {
|
|
134
|
-
const logPrefix = ctx.isFullMatch
|
|
136
|
+
const logPrefix = ctx.isFullMatch
|
|
137
|
+
? "[Router.match]"
|
|
138
|
+
: "[Router.matchPartial]";
|
|
135
139
|
|
|
136
140
|
let allIds: string[];
|
|
137
141
|
let segmentsToRender: ResolvedSegment[];
|
|
138
142
|
|
|
139
143
|
if (ctx.isFullMatch) {
|
|
140
144
|
// Full match (document request) - all segments are rendered
|
|
141
|
-
|
|
142
|
-
|
|
145
|
+
// Deduplicate by segment ID (defense-in-depth). The primary dedup is in
|
|
146
|
+
// resolveAllSegments, but this guards against any path that bypasses it.
|
|
147
|
+
// include() scopes can produce entries that resolve the same shared layout,
|
|
148
|
+
// and duplicate IDs change the client's React tree depth causing remounts.
|
|
149
|
+
const seen = new Set<string>();
|
|
150
|
+
segmentsToRender = [];
|
|
151
|
+
for (const s of allSegments) {
|
|
152
|
+
if (!seen.has(s.id)) {
|
|
153
|
+
seen.add(s.id);
|
|
154
|
+
segmentsToRender.push(s);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
allIds = segmentsToRender.map((s) => s.id);
|
|
143
158
|
} else {
|
|
144
159
|
// Partial match (navigation) - filter and handle intercepts
|
|
145
160
|
// When intercepting, tell browser to keep its current segments + add modal
|
|
@@ -151,32 +166,31 @@ export function buildMatchResult<TEnv>(
|
|
|
151
166
|
: allSegments.map((s) => s.id) // Use actual segments, not matchedIds
|
|
152
167
|
: [...state.matchedIds, ...state.interceptSegments.map((s) => s.id)];
|
|
153
168
|
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
segmentsToRender = allSegments.filter(
|
|
157
|
-
(s) => s.component !== null || s.type === "loader"
|
|
158
|
-
);
|
|
159
|
-
}
|
|
169
|
+
// Deduplicate allIds (defense-in-depth for partial match path)
|
|
170
|
+
allIds = [...new Set(allIds)];
|
|
160
171
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
segmentsToRender.map((s) => s.id).join(", ")
|
|
172
|
+
// Filter out null-component segments only when the client already has
|
|
173
|
+
// them cached (revalidation skip). If the client doesn't have the segment,
|
|
174
|
+
// it must be included even with null component — it's structurally required
|
|
175
|
+
// as a parent node for child layouts/parallels to reconcile against.
|
|
176
|
+
// Loader segments are always included as they carry data.
|
|
177
|
+
const clientIdSet = new Set(ctx.clientSegmentIds);
|
|
178
|
+
segmentsToRender = allSegments.filter(
|
|
179
|
+
(s) =>
|
|
180
|
+
s.component !== null || s.type === "loader" || !clientIdSet.has(s.id),
|
|
171
181
|
);
|
|
172
182
|
}
|
|
173
183
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
184
|
+
debugLog(logPrefix, "all segments", {
|
|
185
|
+
segments: allSegments.map((s) => ({
|
|
186
|
+
id: s.id,
|
|
187
|
+
type: s.type,
|
|
188
|
+
hasComponent: s.component !== null,
|
|
189
|
+
})),
|
|
190
|
+
});
|
|
191
|
+
debugLog(logPrefix, "segments to render", {
|
|
192
|
+
segmentIds: segmentsToRender.map((s) => s.id),
|
|
193
|
+
});
|
|
180
194
|
|
|
181
195
|
return {
|
|
182
196
|
segments: segmentsToRender,
|
|
@@ -184,7 +198,6 @@ export function buildMatchResult<TEnv>(
|
|
|
184
198
|
diff: segmentsToRender.map((s) => s.id),
|
|
185
199
|
params: ctx.matched.params,
|
|
186
200
|
routeName: ctx.routeKey,
|
|
187
|
-
serverTiming,
|
|
188
201
|
slots: Object.keys(state.slots).length > 0 ? state.slots : undefined,
|
|
189
202
|
routeMiddleware:
|
|
190
203
|
ctx.routeMiddleware.length > 0 ? ctx.routeMiddleware : undefined,
|
|
@@ -200,14 +213,23 @@ export function buildMatchResult<TEnv>(
|
|
|
200
213
|
export async function collectMatchResult<TEnv>(
|
|
201
214
|
pipeline: AsyncGenerator<ResolvedSegment>,
|
|
202
215
|
ctx: MatchContext<TEnv>,
|
|
203
|
-
state: MatchPipelineState
|
|
216
|
+
state: MatchPipelineState,
|
|
204
217
|
): Promise<MatchResult> {
|
|
205
218
|
const allSegments = await collectSegments(pipeline);
|
|
206
219
|
|
|
220
|
+
const buildStart = performance.now();
|
|
221
|
+
|
|
207
222
|
// Update state with collected segments if not already set
|
|
208
223
|
if (state.segments.length === 0) {
|
|
209
224
|
state.segments = allSegments;
|
|
210
225
|
}
|
|
211
226
|
|
|
212
|
-
|
|
227
|
+
const result = buildMatchResult(allSegments, ctx, state);
|
|
228
|
+
appendMetric(
|
|
229
|
+
ctx.metricsStore,
|
|
230
|
+
"collect-result",
|
|
231
|
+
buildStart,
|
|
232
|
+
performance.now() - buildStart,
|
|
233
|
+
);
|
|
234
|
+
return result;
|
|
213
235
|
}
|
package/src/router/metrics.ts
CHANGED
|
@@ -4,58 +4,283 @@
|
|
|
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
|
-
debugPerformance: boolean
|
|
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,
|
|
28
221
|
pathname: string,
|
|
29
|
-
metricsStore: MetricsStore
|
|
222
|
+
metricsStore: MetricsStore,
|
|
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(", ");
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cookie Utilities
|
|
3
|
+
*
|
|
4
|
+
* Parsing and serialization for HTTP cookies used by middleware context.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { CookieOptions } from "./middleware-types.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parse cookies from Cookie header
|
|
11
|
+
*/
|
|
12
|
+
export function parseCookies(
|
|
13
|
+
cookieHeader: string | null,
|
|
14
|
+
): Record<string, string> {
|
|
15
|
+
if (!cookieHeader) return {};
|
|
16
|
+
|
|
17
|
+
const cookies: Record<string, string> = {};
|
|
18
|
+
const pairs = cookieHeader.split(";");
|
|
19
|
+
|
|
20
|
+
for (const pair of pairs) {
|
|
21
|
+
const [name, ...rest] = pair.trim().split("=");
|
|
22
|
+
if (name) {
|
|
23
|
+
const raw = rest.join("=");
|
|
24
|
+
try {
|
|
25
|
+
cookies[name] = decodeURIComponent(raw);
|
|
26
|
+
} catch {
|
|
27
|
+
// Malformed percent-encoded value (e.g. %zz) - fall back to raw value
|
|
28
|
+
cookies[name] = raw;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return cookies;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Serialize a cookie for Set-Cookie header
|
|
38
|
+
*/
|
|
39
|
+
export function serializeCookie(
|
|
40
|
+
name: string,
|
|
41
|
+
value: string,
|
|
42
|
+
options: CookieOptions = {},
|
|
43
|
+
): string {
|
|
44
|
+
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
|
45
|
+
|
|
46
|
+
if (options.domain) cookie += `; Domain=${options.domain}`;
|
|
47
|
+
if (options.path) cookie += `; Path=${options.path}`;
|
|
48
|
+
if (options.maxAge !== undefined) cookie += `; Max-Age=${options.maxAge}`;
|
|
49
|
+
if (options.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
|
|
50
|
+
if (options.httpOnly) cookie += "; HttpOnly";
|
|
51
|
+
if (options.secure) cookie += "; Secure";
|
|
52
|
+
if (options.sameSite) cookie += `; SameSite=${options.sameSite}`;
|
|
53
|
+
|
|
54
|
+
return cookie;
|
|
55
|
+
}
|