@rangojs/router 0.0.0-experimental.20 → 0.0.0-experimental.204030a9
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 +242 -55
- package/dist/bin/rango.js +277 -99
- package/dist/vite/index.js +2929 -1132
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +68 -21
- package/skills/breadcrumbs/SKILL.md +252 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +243 -21
- package/skills/caching/SKILL.md +159 -10
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +262 -51
- package/skills/host-router/SKILL.md +243 -0
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +46 -4
- package/skills/layout/SKILL.md +28 -7
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +291 -31
- package/skills/middleware/SKILL.md +49 -12
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +197 -6
- package/skills/prerender/SKILL.md +125 -102
- package/skills/rango/SKILL.md +242 -23
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- package/skills/route/SKILL.md +91 -8
- package/skills/router-setup/SKILL.md +98 -8
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +511 -188
- package/skills/typesafety/SKILL.md +354 -50
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/__internal.ts +92 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +91 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +183 -18
- package/src/browser/navigation-client.ts +187 -57
- package/src/browser/navigation-store.ts +75 -17
- package/src/browser/navigation-transaction.ts +21 -37
- package/src/browser/partial-update.ts +143 -40
- 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 +110 -33
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +23 -64
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +43 -10
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +200 -75
- package/src/browser/scroll-restoration.ts +46 -39
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +31 -36
- package/src/browser/types.ts +81 -5
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-trie.ts +69 -26
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +278 -88
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +76 -49
- package/src/cache/cf/cf-cache-store.ts +501 -18
- 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 +5 -1
- package/src/client.tsx +95 -284
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +65 -12
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/host/index.ts +2 -5
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +15 -40
- package/src/index.ts +92 -76
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +57 -15
- package/src/prerender.ts +141 -80
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +435 -260
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +110 -34
- 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-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +113 -1
- package/src/router/error-handling.ts +1 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +105 -39
- package/src/router/intercept-resolution.ts +15 -22
- package/src/router/lazy-includes.ts +12 -9
- package/src/router/loader-resolution.ts +175 -23
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +31 -16
- package/src/router/match-api.ts +129 -193
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +136 -106
- package/src/router/match-middleware/cache-store.ts +54 -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 +124 -18
- package/src/router/metrics.ts +239 -14
- package/src/router/middleware-types.ts +61 -31
- package/src/router/middleware.ts +226 -124
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +118 -19
- package/src/router/prerender-match.ts +114 -10
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +286 -0
- package/src/router/revalidation.ts +85 -9
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +91 -29
- package/src/router/router-options.ts +89 -19
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +240 -23
- 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 +483 -289
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +38 -15
- package/src/router/types.ts +9 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +120 -32
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +524 -370
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -21
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +39 -10
- package/src/rsc/response-route-handler.ts +46 -53
- package/src/rsc/rsc-rendering.ts +69 -89
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +39 -47
- package/src/rsc/ssr-setup.ts +144 -0
- package/src/rsc/types.ts +19 -3
- package/src/search-params.ts +20 -17
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +219 -67
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +285 -63
- package/src/server/cookie-store.ts +28 -4
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +228 -65
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +9 -1
- package/src/static-handler.ts +19 -7
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +98 -0
- package/src/testing/internal/context.ts +151 -0
- package/src/testing/render-route.tsx +536 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +170 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +112 -0
- package/src/theme/index.ts +4 -13
- package/src/types/cache-types.ts +4 -4
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +197 -79
- package/src/types/index.ts +1 -0
- 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 +37 -6
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +50 -9
- package/src/urls/path-helper.ts +63 -63
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +487 -44
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +63 -91
- package/src/vite/discovery/discover-routers.ts +106 -53
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +222 -107
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +50 -13
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +10 -3
- package/src/vite/plugin-types.ts +111 -72
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- 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 +55 -33
- package/src/vite/plugins/expose-id-utils.ts +24 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +12 -35
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +544 -317
- package/src/vite/plugins/performance-tracks.ts +92 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +72 -3
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +265 -226
- package/src/vite/router-discovery.ts +924 -137
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +98 -5
- package/src/vite/utils/shared-utils.ts +109 -27
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/route-definition/route-function.ts +0 -119
|
@@ -87,10 +87,49 @@
|
|
|
87
87
|
* if (state.cacheHit) return; // Now we can check
|
|
88
88
|
*/
|
|
89
89
|
import type { ResolvedSegment } from "../../types.js";
|
|
90
|
+
import type { EntryData } from "../../server/context.js";
|
|
91
|
+
import { _getRequestContext } from "../../server/request-context.js";
|
|
90
92
|
import type { MatchContext, MatchPipelineState } from "../match-context.js";
|
|
91
93
|
import { getRouterContext } from "../router-context.js";
|
|
92
94
|
import type { GeneratorMiddleware } from "./cache-lookup.js";
|
|
93
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Check whether any entry in the tree uses loading() (streaming).
|
|
98
|
+
* Matches the router's streaming semantics in fresh.ts: streaming is
|
|
99
|
+
* enabled when `loading` is defined AND not `false`. `loading: false`
|
|
100
|
+
* explicitly disables streaming; `undefined` means no loading at all.
|
|
101
|
+
*/
|
|
102
|
+
export function treeHasStreaming(entries: EntryData[]): boolean {
|
|
103
|
+
for (const entry of entries) {
|
|
104
|
+
if (
|
|
105
|
+
"loading" in entry &&
|
|
106
|
+
entry.loading !== undefined &&
|
|
107
|
+
entry.loading !== false
|
|
108
|
+
)
|
|
109
|
+
return true;
|
|
110
|
+
if (entry.layout) {
|
|
111
|
+
if (treeHasStreaming(entry.layout)) return true;
|
|
112
|
+
}
|
|
113
|
+
if (entry.parallel) {
|
|
114
|
+
for (const key in entry.parallel) {
|
|
115
|
+
const parallelEntry = entry.parallel[key as `@${string}`];
|
|
116
|
+
if (parallelEntry) {
|
|
117
|
+
if (
|
|
118
|
+
"loading" in parallelEntry &&
|
|
119
|
+
parallelEntry.loading !== undefined &&
|
|
120
|
+
parallelEntry.loading !== false
|
|
121
|
+
)
|
|
122
|
+
return true;
|
|
123
|
+
if (parallelEntry.layout) {
|
|
124
|
+
if (treeHasStreaming(parallelEntry.layout)) return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
94
133
|
/**
|
|
95
134
|
* Creates segment resolution middleware
|
|
96
135
|
*
|
|
@@ -104,7 +143,6 @@ export function withSegmentResolution<TEnv>(
|
|
|
104
143
|
return async function* (
|
|
105
144
|
source: AsyncGenerator<ResolvedSegment>,
|
|
106
145
|
): AsyncGenerator<ResolvedSegment> {
|
|
107
|
-
const pipelineStart = performance.now();
|
|
108
146
|
const ms = ctx.metricsStore;
|
|
109
147
|
|
|
110
148
|
// IMPORTANT: Always iterate source first to give cache-lookup a chance
|
|
@@ -113,18 +151,27 @@ export function withSegmentResolution<TEnv>(
|
|
|
113
151
|
yield segment;
|
|
114
152
|
}
|
|
115
153
|
|
|
154
|
+
// Measure own work only (after source iteration completes)
|
|
155
|
+
const ownStart = performance.now();
|
|
156
|
+
|
|
116
157
|
// If cache hit, segments were already yielded by cache lookup
|
|
158
|
+
// (render barrier is resolved on the cache-hit path)
|
|
117
159
|
if (state.cacheHit) {
|
|
118
160
|
if (ms) {
|
|
119
161
|
ms.metrics.push({
|
|
120
162
|
label: "pipeline:segment-resolve",
|
|
121
|
-
duration: performance.now() -
|
|
122
|
-
startTime:
|
|
163
|
+
duration: performance.now() - ownStart,
|
|
164
|
+
startTime: ownStart - ms.requestStart,
|
|
123
165
|
});
|
|
124
166
|
}
|
|
125
167
|
return;
|
|
126
168
|
}
|
|
127
169
|
|
|
170
|
+
const reqCtx = _getRequestContext();
|
|
171
|
+
if (reqCtx && reqCtx._treeHasStreaming === undefined) {
|
|
172
|
+
reqCtx._treeHasStreaming = treeHasStreaming(ctx.entries);
|
|
173
|
+
}
|
|
174
|
+
|
|
128
175
|
const { resolveAllSegmentsWithRevalidation, resolveAllSegments } =
|
|
129
176
|
getRouterContext<TEnv>();
|
|
130
177
|
|
|
@@ -146,6 +193,10 @@ export function withSegmentResolution<TEnv>(
|
|
|
146
193
|
state.segments = segments;
|
|
147
194
|
state.matchedIds = segments.map((s: { id: string }) => s.id);
|
|
148
195
|
|
|
196
|
+
if (reqCtx) {
|
|
197
|
+
reqCtx._resolveRenderBarrier(segments);
|
|
198
|
+
}
|
|
199
|
+
|
|
149
200
|
// Yield all resolved segments
|
|
150
201
|
for (const segment of segments) {
|
|
151
202
|
yield segment;
|
|
@@ -168,6 +219,7 @@ export function withSegmentResolution<TEnv>(
|
|
|
168
219
|
ctx.interceptResult,
|
|
169
220
|
ctx.localRouteName,
|
|
170
221
|
ctx.pathname,
|
|
222
|
+
ctx.stale,
|
|
171
223
|
),
|
|
172
224
|
);
|
|
173
225
|
|
|
@@ -175,6 +227,10 @@ export function withSegmentResolution<TEnv>(
|
|
|
175
227
|
state.segments = result.segments;
|
|
176
228
|
state.matchedIds = result.matchedIds;
|
|
177
229
|
|
|
230
|
+
if (reqCtx) {
|
|
231
|
+
reqCtx._resolveRenderBarrier(result.segments);
|
|
232
|
+
}
|
|
233
|
+
|
|
178
234
|
// Yield all resolved segments
|
|
179
235
|
for (const segment of result.segments) {
|
|
180
236
|
yield segment;
|
|
@@ -184,8 +240,8 @@ export function withSegmentResolution<TEnv>(
|
|
|
184
240
|
if (ms) {
|
|
185
241
|
ms.metrics.push({
|
|
186
242
|
label: "pipeline:segment-resolve",
|
|
187
|
-
duration: performance.now() -
|
|
188
|
-
startTime:
|
|
243
|
+
duration: performance.now() - ownStart,
|
|
244
|
+
startTime: ownStart - ms.requestStart,
|
|
189
245
|
});
|
|
190
246
|
}
|
|
191
247
|
};
|
|
@@ -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,76 @@ 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
|
+
): { segments: ResolvedSegment[]; removedIds: Set<string> } {
|
|
142
|
+
// Single pass: original (non-inherited) loaderIds, all loaderIds grouped by
|
|
143
|
+
// namespace, and namespaces of segments that declare loading().
|
|
144
|
+
const originalLoaders = new Set<string>();
|
|
145
|
+
const loaderIdsByNamespace = new Map<string, string[]>();
|
|
146
|
+
const namespacesWithLoading = new Set<string>();
|
|
147
|
+
for (const s of segments) {
|
|
148
|
+
if (s.type === "loader" && s.loaderId) {
|
|
149
|
+
if (!s._inherited) originalLoaders.add(s.loaderId);
|
|
150
|
+
const ids = loaderIdsByNamespace.get(s.namespace);
|
|
151
|
+
if (ids) ids.push(s.loaderId);
|
|
152
|
+
else loaderIdsByNamespace.set(s.namespace, [s.loaderId]);
|
|
153
|
+
} else if (
|
|
154
|
+
s.type !== "loader" &&
|
|
155
|
+
s.loading !== undefined &&
|
|
156
|
+
s.loading !== false
|
|
157
|
+
) {
|
|
158
|
+
namespacesWithLoading.add(s.namespace);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// An inherited loader is needed when it shares a namespace with a
|
|
163
|
+
// loading-bearing segment (its data sits behind that LoaderBoundary).
|
|
164
|
+
const loadersWithLoading = new Set<string>();
|
|
165
|
+
for (const ns of namespacesWithLoading) {
|
|
166
|
+
for (const id of loaderIdsByNamespace.get(ns) ?? []) {
|
|
167
|
+
loadersWithLoading.add(id);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const result: ResolvedSegment[] = [];
|
|
172
|
+
const removedIds = new Set<string>();
|
|
173
|
+
|
|
174
|
+
for (const s of segments) {
|
|
175
|
+
if (
|
|
176
|
+
s.type === "loader" &&
|
|
177
|
+
s.loaderId &&
|
|
178
|
+
s._inherited &&
|
|
179
|
+
originalLoaders.has(s.loaderId) &&
|
|
180
|
+
!loadersWithLoading.has(s.loaderId)
|
|
181
|
+
) {
|
|
182
|
+
removedIds.add(s.id);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
result.push(s);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (removedIds.size > 0) {
|
|
189
|
+
debugLog(
|
|
190
|
+
logPrefix,
|
|
191
|
+
`deduped ${removedIds.size} inherited loader segment(s)`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { segments: result, removedIds };
|
|
196
|
+
}
|
|
197
|
+
|
|
127
198
|
/**
|
|
128
199
|
* Build the final MatchResult from collected segments and context
|
|
129
200
|
*/
|
|
@@ -168,13 +239,23 @@ export function buildMatchResult<TEnv>(
|
|
|
168
239
|
// Deduplicate allIds (defense-in-depth for partial match path)
|
|
169
240
|
allIds = [...new Set(allIds)];
|
|
170
241
|
|
|
171
|
-
// Filter out segments
|
|
172
|
-
//
|
|
242
|
+
// Filter out null-component segments only when the client already has
|
|
243
|
+
// them cached (revalidation skip). If the client doesn't have the segment,
|
|
244
|
+
// it must be included even with null component — it's structurally required
|
|
245
|
+
// as a parent node for child layouts/parallels to reconcile against.
|
|
246
|
+
// Loader segments are always included as they carry data.
|
|
247
|
+
const clientIdSet = new Set(ctx.clientSegmentIds);
|
|
173
248
|
segmentsToRender = allSegments.filter(
|
|
174
|
-
(s) =>
|
|
249
|
+
(s) =>
|
|
250
|
+
s.component !== null || s.type === "loader" || !clientIdSet.has(s.id),
|
|
175
251
|
);
|
|
176
252
|
}
|
|
177
253
|
|
|
254
|
+
const { segments: dedupedSegments, removedIds } = deduplicateLoaderSegments(
|
|
255
|
+
segmentsToRender,
|
|
256
|
+
logPrefix,
|
|
257
|
+
);
|
|
258
|
+
|
|
178
259
|
debugLog(logPrefix, "all segments", {
|
|
179
260
|
segments: allSegments.map((s) => ({
|
|
180
261
|
id: s.id,
|
|
@@ -183,23 +264,39 @@ export function buildMatchResult<TEnv>(
|
|
|
183
264
|
})),
|
|
184
265
|
});
|
|
185
266
|
debugLog(logPrefix, "segments to render", {
|
|
186
|
-
segmentIds:
|
|
267
|
+
segmentIds: dedupedSegments.map((s) => s.id),
|
|
187
268
|
});
|
|
188
269
|
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
270
|
+
// Remove deduped loader IDs from matched so the client doesn't treat
|
|
271
|
+
// them as missing segments and trigger a fallback refetch.
|
|
272
|
+
const matchedIds =
|
|
273
|
+
removedIds.size > 0 ? allIds.filter((id) => !removedIds.has(id)) : allIds;
|
|
274
|
+
|
|
275
|
+
// resolvedIds: every segment whose handler actually ran this request.
|
|
276
|
+
// For full-match every segment is fresh; for partial-match we filter by
|
|
277
|
+
// the internal `_handlerRan` flag set in revalidation.ts. Drives the
|
|
278
|
+
// client's handle-bucket cleanup — a slot that re-resolved and pushed
|
|
279
|
+
// nothing must have its previous handle data cleared, but `diff` won't
|
|
280
|
+
// carry it because the segment payload skips null-component cached
|
|
281
|
+
// segments to save bytes.
|
|
282
|
+
const resolvedIds = ctx.isFullMatch
|
|
283
|
+
? allSegments.map((s) => s.id)
|
|
284
|
+
: allSegments.filter((s) => s._handlerRan).map((s) => s.id);
|
|
285
|
+
|
|
286
|
+
// Strip internal-only fields from the segments going on the wire.
|
|
287
|
+
const cleanedSegments = dedupedSegments.map((s) => {
|
|
288
|
+
if (s._handlerRan === undefined) return s;
|
|
289
|
+
const { _handlerRan: _drop, ...rest } = s;
|
|
290
|
+
return rest as ResolvedSegment;
|
|
291
|
+
});
|
|
195
292
|
|
|
196
293
|
return {
|
|
197
|
-
segments:
|
|
198
|
-
matched:
|
|
199
|
-
diff:
|
|
294
|
+
segments: cleanedSegments,
|
|
295
|
+
matched: matchedIds,
|
|
296
|
+
diff: cleanedSegments.map((s) => s.id),
|
|
297
|
+
resolvedIds,
|
|
200
298
|
params: ctx.matched.params,
|
|
201
299
|
routeName: ctx.routeKey,
|
|
202
|
-
serverTiming,
|
|
203
300
|
slots: Object.keys(state.slots).length > 0 ? state.slots : undefined,
|
|
204
301
|
routeMiddleware:
|
|
205
302
|
ctx.routeMiddleware.length > 0 ? ctx.routeMiddleware : undefined,
|
|
@@ -219,10 +316,19 @@ export async function collectMatchResult<TEnv>(
|
|
|
219
316
|
): Promise<MatchResult> {
|
|
220
317
|
const allSegments = await collectSegments(pipeline);
|
|
221
318
|
|
|
319
|
+
const buildStart = performance.now();
|
|
320
|
+
|
|
222
321
|
// Update state with collected segments if not already set
|
|
223
322
|
if (state.segments.length === 0) {
|
|
224
323
|
state.segments = allSegments;
|
|
225
324
|
}
|
|
226
325
|
|
|
227
|
-
|
|
326
|
+
const result = buildMatchResult(allSegments, ctx, state);
|
|
327
|
+
appendMetric(
|
|
328
|
+
ctx.metricsStore,
|
|
329
|
+
"collect-result",
|
|
330
|
+
buildStart,
|
|
331
|
+
performance.now() - buildStart,
|
|
332
|
+
);
|
|
333
|
+
return result;
|
|
228
334
|
}
|
package/src/router/metrics.ts
CHANGED
|
@@ -1,27 +1,220 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Router Metrics Utilities
|
|
3
3
|
*
|
|
4
|
-
* Performance metrics collection and reporting for
|
|
4
|
+
* Performance metrics collection and reporting for Rango.
|
|
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(", ");
|