@rangojs/router 0.0.0-experimental.10

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.
Files changed (172) hide show
  1. package/CLAUDE.md +43 -0
  2. package/README.md +19 -0
  3. package/dist/bin/rango.js +227 -0
  4. package/dist/vite/index.js +3039 -0
  5. package/package.json +171 -0
  6. package/skills/caching/SKILL.md +191 -0
  7. package/skills/debug-manifest/SKILL.md +108 -0
  8. package/skills/document-cache/SKILL.md +180 -0
  9. package/skills/fonts/SKILL.md +165 -0
  10. package/skills/hooks/SKILL.md +442 -0
  11. package/skills/intercept/SKILL.md +190 -0
  12. package/skills/layout/SKILL.md +213 -0
  13. package/skills/links/SKILL.md +180 -0
  14. package/skills/loader/SKILL.md +246 -0
  15. package/skills/middleware/SKILL.md +202 -0
  16. package/skills/mime-routes/SKILL.md +124 -0
  17. package/skills/parallel/SKILL.md +228 -0
  18. package/skills/prerender/SKILL.md +283 -0
  19. package/skills/rango/SKILL.md +54 -0
  20. package/skills/response-routes/SKILL.md +358 -0
  21. package/skills/route/SKILL.md +173 -0
  22. package/skills/router-setup/SKILL.md +346 -0
  23. package/skills/tailwind/SKILL.md +129 -0
  24. package/skills/theme/SKILL.md +78 -0
  25. package/skills/typesafety/SKILL.md +394 -0
  26. package/src/__internal.ts +175 -0
  27. package/src/bin/rango.ts +24 -0
  28. package/src/browser/event-controller.ts +876 -0
  29. package/src/browser/index.ts +18 -0
  30. package/src/browser/link-interceptor.ts +121 -0
  31. package/src/browser/lru-cache.ts +69 -0
  32. package/src/browser/merge-segment-loaders.ts +126 -0
  33. package/src/browser/navigation-bridge.ts +913 -0
  34. package/src/browser/navigation-client.ts +165 -0
  35. package/src/browser/navigation-store.ts +823 -0
  36. package/src/browser/partial-update.ts +600 -0
  37. package/src/browser/react/Link.tsx +248 -0
  38. package/src/browser/react/NavigationProvider.tsx +346 -0
  39. package/src/browser/react/ScrollRestoration.tsx +94 -0
  40. package/src/browser/react/context.ts +53 -0
  41. package/src/browser/react/index.ts +52 -0
  42. package/src/browser/react/location-state-shared.ts +120 -0
  43. package/src/browser/react/location-state.ts +62 -0
  44. package/src/browser/react/mount-context.ts +32 -0
  45. package/src/browser/react/use-action.ts +240 -0
  46. package/src/browser/react/use-client-cache.ts +56 -0
  47. package/src/browser/react/use-handle.ts +203 -0
  48. package/src/browser/react/use-href.tsx +40 -0
  49. package/src/browser/react/use-link-status.ts +134 -0
  50. package/src/browser/react/use-mount.ts +31 -0
  51. package/src/browser/react/use-navigation.ts +140 -0
  52. package/src/browser/react/use-segments.ts +188 -0
  53. package/src/browser/request-controller.ts +164 -0
  54. package/src/browser/rsc-router.tsx +352 -0
  55. package/src/browser/scroll-restoration.ts +324 -0
  56. package/src/browser/segment-structure-assert.ts +67 -0
  57. package/src/browser/server-action-bridge.ts +762 -0
  58. package/src/browser/shallow.ts +35 -0
  59. package/src/browser/types.ts +478 -0
  60. package/src/build/generate-manifest.ts +377 -0
  61. package/src/build/generate-route-types.ts +828 -0
  62. package/src/build/index.ts +36 -0
  63. package/src/build/route-trie.ts +239 -0
  64. package/src/cache/cache-scope.ts +563 -0
  65. package/src/cache/cf/cf-cache-store.ts +428 -0
  66. package/src/cache/cf/index.ts +19 -0
  67. package/src/cache/document-cache.ts +340 -0
  68. package/src/cache/index.ts +58 -0
  69. package/src/cache/memory-segment-store.ts +150 -0
  70. package/src/cache/memory-store.ts +253 -0
  71. package/src/cache/types.ts +392 -0
  72. package/src/client.rsc.tsx +83 -0
  73. package/src/client.tsx +643 -0
  74. package/src/component-utils.ts +76 -0
  75. package/src/components/DefaultDocument.tsx +23 -0
  76. package/src/debug.ts +233 -0
  77. package/src/default-error-boundary.tsx +88 -0
  78. package/src/deps/browser.ts +8 -0
  79. package/src/deps/html-stream-client.ts +2 -0
  80. package/src/deps/html-stream-server.ts +2 -0
  81. package/src/deps/rsc.ts +10 -0
  82. package/src/deps/ssr.ts +2 -0
  83. package/src/errors.ts +295 -0
  84. package/src/handle.ts +130 -0
  85. package/src/handles/MetaTags.tsx +193 -0
  86. package/src/handles/index.ts +6 -0
  87. package/src/handles/meta.ts +247 -0
  88. package/src/host/cookie-handler.ts +159 -0
  89. package/src/host/errors.ts +97 -0
  90. package/src/host/index.ts +56 -0
  91. package/src/host/pattern-matcher.ts +214 -0
  92. package/src/host/router.ts +330 -0
  93. package/src/host/testing.ts +79 -0
  94. package/src/host/types.ts +138 -0
  95. package/src/host/utils.ts +25 -0
  96. package/src/href-client.ts +202 -0
  97. package/src/href-context.ts +33 -0
  98. package/src/index.rsc.ts +121 -0
  99. package/src/index.ts +165 -0
  100. package/src/loader.rsc.ts +207 -0
  101. package/src/loader.ts +47 -0
  102. package/src/network-error-thrower.tsx +21 -0
  103. package/src/outlet-context.ts +15 -0
  104. package/src/prerender/param-hash.ts +35 -0
  105. package/src/prerender/store.ts +40 -0
  106. package/src/prerender.ts +156 -0
  107. package/src/reverse.ts +267 -0
  108. package/src/root-error-boundary.tsx +277 -0
  109. package/src/route-content-wrapper.tsx +193 -0
  110. package/src/route-definition.ts +1431 -0
  111. package/src/route-map-builder.ts +242 -0
  112. package/src/route-types.ts +220 -0
  113. package/src/router/error-handling.ts +287 -0
  114. package/src/router/handler-context.ts +158 -0
  115. package/src/router/intercept-resolution.ts +387 -0
  116. package/src/router/loader-resolution.ts +327 -0
  117. package/src/router/manifest.ts +216 -0
  118. package/src/router/match-api.ts +621 -0
  119. package/src/router/match-context.ts +264 -0
  120. package/src/router/match-middleware/background-revalidation.ts +236 -0
  121. package/src/router/match-middleware/cache-lookup.ts +382 -0
  122. package/src/router/match-middleware/cache-store.ts +276 -0
  123. package/src/router/match-middleware/index.ts +81 -0
  124. package/src/router/match-middleware/intercept-resolution.ts +281 -0
  125. package/src/router/match-middleware/segment-resolution.ts +184 -0
  126. package/src/router/match-pipelines.ts +214 -0
  127. package/src/router/match-result.ts +213 -0
  128. package/src/router/metrics.ts +62 -0
  129. package/src/router/middleware.ts +791 -0
  130. package/src/router/pattern-matching.ts +407 -0
  131. package/src/router/revalidation.ts +190 -0
  132. package/src/router/router-context.ts +301 -0
  133. package/src/router/segment-resolution.ts +1315 -0
  134. package/src/router/trie-matching.ts +172 -0
  135. package/src/router/types.ts +163 -0
  136. package/src/router.gen.ts +6 -0
  137. package/src/router.ts +2423 -0
  138. package/src/rsc/handler.ts +1443 -0
  139. package/src/rsc/helpers.ts +64 -0
  140. package/src/rsc/index.ts +56 -0
  141. package/src/rsc/nonce.ts +18 -0
  142. package/src/rsc/types.ts +236 -0
  143. package/src/segment-system.tsx +442 -0
  144. package/src/server/context.ts +466 -0
  145. package/src/server/handle-store.ts +229 -0
  146. package/src/server/loader-registry.ts +174 -0
  147. package/src/server/request-context.ts +554 -0
  148. package/src/server/root-layout.tsx +10 -0
  149. package/src/server/tsconfig.json +14 -0
  150. package/src/server.ts +171 -0
  151. package/src/ssr/index.tsx +296 -0
  152. package/src/theme/ThemeProvider.tsx +291 -0
  153. package/src/theme/ThemeScript.tsx +61 -0
  154. package/src/theme/constants.ts +59 -0
  155. package/src/theme/index.ts +58 -0
  156. package/src/theme/theme-context.ts +70 -0
  157. package/src/theme/theme-script.ts +152 -0
  158. package/src/theme/types.ts +182 -0
  159. package/src/theme/use-theme.ts +44 -0
  160. package/src/types.ts +1757 -0
  161. package/src/urls.gen.ts +8 -0
  162. package/src/urls.ts +1282 -0
  163. package/src/use-loader.tsx +346 -0
  164. package/src/vite/expose-action-id.ts +344 -0
  165. package/src/vite/expose-handle-id.ts +209 -0
  166. package/src/vite/expose-loader-id.ts +426 -0
  167. package/src/vite/expose-location-state-id.ts +177 -0
  168. package/src/vite/expose-prerender-handler-id.ts +429 -0
  169. package/src/vite/index.ts +2068 -0
  170. package/src/vite/package-resolution.ts +125 -0
  171. package/src/vite/version.d.ts +12 -0
  172. package/src/vite/virtual-entries.ts +114 -0
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Match Pipelines
3
+ *
4
+ * Composes async generator middleware into pipelines for route matching.
5
+ * The pipeline transforms navigation requests into resolved UI segments.
6
+ *
7
+ * PIPELINE ARCHITECTURE OVERVIEW
8
+ * ==============================
9
+ *
10
+ * The router uses a pipeline of async generator middleware to process requests.
11
+ * Each middleware can:
12
+ * 1. Produce segments (yield)
13
+ * 2. Transform segments from upstream
14
+ * 3. Observe segments without modifying them
15
+ * 4. Trigger side effects (caching, background revalidation)
16
+ *
17
+ * REQUEST FLOW DIAGRAM
18
+ * ====================
19
+ *
20
+ * Navigation Request
21
+ * |
22
+ * v
23
+ * +------------------+
24
+ * | Create Context | MatchContext: routes, params, client state
25
+ * +------------------+
26
+ * |
27
+ * v
28
+ * +------------------+
29
+ * | Select Pipeline | Full (document) vs Partial (navigation)
30
+ * +------------------+
31
+ * |
32
+ * v
33
+ * ==================== PIPELINE EXECUTION ====================
34
+ * | |
35
+ * | empty() ─────> [1] ─────> [2] ─────> [3] ─────> [4] ───>|───> segments
36
+ * | | | | | | |
37
+ * | | cache | segment |intercept | cache | bg |
38
+ * | | lookup | resolve | resolve | store | reval |
39
+ * | |
40
+ * ============================================================
41
+ * |
42
+ * v
43
+ * +------------------+
44
+ * | Collect Result | Filter segments, build MatchResult
45
+ * +------------------+
46
+ * |
47
+ * v
48
+ * RSC Stream Response
49
+ *
50
+ *
51
+ * MIDDLEWARE EXECUTION ORDER
52
+ * ==========================
53
+ *
54
+ * Middleware compose in reverse order (rightmost = innermost, runs first):
55
+ *
56
+ * compose(A, B, C)(source) => source -> C -> B -> A -> output
57
+ *
58
+ * For the partial match pipeline:
59
+ *
60
+ * compose(
61
+ * withBackgroundRevalidation, // [5] Outermost - triggers SWR
62
+ * withCacheStore, // [4] Stores segments in cache
63
+ * withInterceptResolution, // [3] Resolves intercept segments
64
+ * withSegmentResolution, // [2] Resolves on cache miss
65
+ * withCacheLookup // [1] Innermost - checks cache first
66
+ * )
67
+ *
68
+ * Execution flow for cache MISS:
69
+ *
70
+ * empty() yields nothing
71
+ * -> [1] cache-lookup: no cache, passes through
72
+ * -> [2] segment-resolution: resolves segments, yields them
73
+ * -> [3] intercept-resolution: resolves intercepts, yields them
74
+ * -> [4] cache-store: observes all, stores in cache
75
+ * -> [5] bg-revalidation: no-op (wasn't stale)
76
+ * -> output: all segments
77
+ *
78
+ * Execution flow for cache HIT (stale):
79
+ *
80
+ * empty() yields nothing
81
+ * -> [1] cache-lookup: HIT! yields cached segments + fresh loaders
82
+ * -> [2] segment-resolution: sees cacheHit=true, skips
83
+ * -> [3] intercept-resolution: extracts intercepts from cache
84
+ * -> [4] cache-store: sees cacheHit=true, skips
85
+ * -> [5] bg-revalidation: triggers waitUntil() to revalidate
86
+ * -> output: cached segments + fresh loader data
87
+ *
88
+ *
89
+ * TWO PIPELINE VARIANTS
90
+ * =====================
91
+ *
92
+ * 1. createMatchPipeline (Full Match)
93
+ * - Used for document requests (initial page load)
94
+ * - No revalidation logic (no previous state to compare)
95
+ * - Simpler segment resolution
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)
102
+ */
103
+ import type { ResolvedSegment } from "../types.js";
104
+ import type { MatchContext, MatchPipelineState } from "./match-context.js";
105
+ import type { GeneratorMiddleware } from "./match-middleware/index.js";
106
+ import {
107
+ withBackgroundRevalidation,
108
+ withCacheLookup,
109
+ withCacheStore,
110
+ withInterceptResolution,
111
+ withSegmentResolution,
112
+ } from "./match-middleware/index.js";
113
+
114
+ /**
115
+ * Compose multiple async generator middleware into a single middleware
116
+ *
117
+ * Middleware are applied in reverse order (rightmost runs first, innermost).
118
+ * For the pipeline:
119
+ * compose(A, B, C)(source)
120
+ *
121
+ * The flow is: source -> C -> B -> A -> output
122
+ * Where C is the innermost (runs first on input) and A is outermost (runs last).
123
+ */
124
+ export function compose<T>(
125
+ ...middleware: GeneratorMiddleware<T>[]
126
+ ): GeneratorMiddleware<T> {
127
+ if (middleware.length === 0) {
128
+ return (source) => source;
129
+ }
130
+ if (middleware.length === 1) {
131
+ return middleware[0];
132
+ }
133
+ return (source) => {
134
+ // Apply middleware in reverse order (rightmost first)
135
+ return middleware.reduceRight((prev, fn) => fn(prev), source);
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Create an empty async generator (source for pipeline)
141
+ */
142
+ export async function* empty<T>(): AsyncGenerator<T> {
143
+ // Yields nothing - used as the initial source for the pipeline
144
+ }
145
+
146
+ /**
147
+ * Create the match partial pipeline
148
+ *
149
+ * Pipeline order (innermost to outermost):
150
+ * 1. cache-lookup - Check cache first, yield cached segments if hit
151
+ * 2. segment-resolution - Resolve segments if cache miss
152
+ * 3. intercept-resolution - Resolve intercept segments
153
+ * 4. cache-store - Store segments in cache
154
+ * 5. background-revalidation - Trigger SWR if cache was stale
155
+ *
156
+ * Data flow:
157
+ * - empty() produces no segments
158
+ * - cache-lookup either yields cached segments OR passes through to segment-resolution
159
+ * - segment-resolution resolves fresh segments on cache miss
160
+ * - intercept-resolution adds intercept segments
161
+ * - cache-store observes and caches segments
162
+ * - background-revalidation triggers SWR revalidation if needed
163
+ */
164
+ export function createMatchPartialPipeline<TEnv>(
165
+ ctx: MatchContext<TEnv>,
166
+ state: MatchPipelineState
167
+ ): AsyncGenerator<ResolvedSegment> {
168
+ // Build the middleware chain
169
+ const pipeline = compose<ResolvedSegment>(
170
+ // Outermost - observes segments and triggers background revalidation
171
+ withBackgroundRevalidation(ctx, state),
172
+ // Observes and stores segments in cache
173
+ withCacheStore(ctx, state),
174
+ // Adds intercept segments after main segments
175
+ withInterceptResolution(ctx, state),
176
+ // Resolves segments on cache miss
177
+ withSegmentResolution(ctx, state),
178
+ // Innermost - checks cache first
179
+ withCacheLookup(ctx, state)
180
+ );
181
+
182
+ // Start with empty source - cache lookup or segment resolution will produce segments
183
+ return pipeline(empty());
184
+ }
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
+ }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Match Result Collection
3
+ *
4
+ * Collects segments from the pipeline and builds the final MatchResult.
5
+ * This is the final stage of the match pipeline.
6
+ *
7
+ * COLLECTION FLOW
8
+ * ===============
9
+ *
10
+ * Pipeline Generator
11
+ * |
12
+ * v
13
+ * +---------------------------+
14
+ * | collectSegments() | Drain async generator
15
+ * | for await...push |
16
+ * +---------------------------+
17
+ * |
18
+ * v
19
+ * +---------------------------+
20
+ * | buildMatchResult() | Transform to MatchResult
21
+ * +---------------------------+
22
+ * |
23
+ * |
24
+ * +-----+-----+
25
+ * | |
26
+ * Full Partial
27
+ * Match Match
28
+ * | |
29
+ * v v
30
+ * All segs Filter:
31
+ * rendered - null components out
32
+ * - keep loaders
33
+ * - handle intercepts
34
+ * | |
35
+ * +-----------+
36
+ * |
37
+ * v
38
+ * MatchResult {
39
+ * segments, // Segments to render
40
+ * matched, // All segment IDs
41
+ * diff, // Changed segment IDs
42
+ * params, // Route params
43
+ * slots, // Intercept slot data
44
+ * serverTiming // Performance metrics
45
+ * }
46
+ *
47
+ *
48
+ * FULL VS PARTIAL MATCH
49
+ * =====================
50
+ *
51
+ * Full Match (document request):
52
+ * - All segments are rendered
53
+ * - allIds = all segment IDs
54
+ * - No filtering needed
55
+ *
56
+ * Partial Match (navigation):
57
+ * - Filter out null components (client already has them)
58
+ * - BUT keep loader segments (they carry data)
59
+ * - Handle intercepts specially (preserve client page + add modal)
60
+ *
61
+ *
62
+ * SEGMENT FILTERING RULES
63
+ * =======================
64
+ *
65
+ * For partial match, segments are filtered:
66
+ *
67
+ * Keep if:
68
+ * - component !== null (needs rendering)
69
+ * - type === "loader" (carries data even with null component)
70
+ *
71
+ * Skip if:
72
+ * - component === null AND type !== "loader"
73
+ * - (Client already has this segment's UI)
74
+ *
75
+ *
76
+ * INTERCEPT HANDLING
77
+ * ==================
78
+ *
79
+ * When intercepting (modal over current page):
80
+ *
81
+ * allIds = client segments + intercept segments
82
+ *
83
+ * This tells the client:
84
+ * 1. Keep your current segments
85
+ * 2. Add these intercept segments to the modal slot
86
+ *
87
+ * The page stays visible, modal renders on top.
88
+ *
89
+ *
90
+ * MATCHRESULT STRUCTURE
91
+ * =====================
92
+ *
93
+ * {
94
+ * segments: ResolvedSegment[] // Segments to serialize and render
95
+ * matched: string[] // All segment IDs for this route
96
+ * diff: string[] // Which segments changed (for client diffing)
97
+ * params: Record<string,string> // Route parameters
98
+ * slots?: Record<string, {...}> // Named slot data for intercepts
99
+ * serverTiming?: string // Server-Timing header value
100
+ * routeMiddleware?: [...] // Route middleware results
101
+ * }
102
+ *
103
+ * The client uses this to:
104
+ * 1. Render segments[] to the UI tree
105
+ * 2. Update internal state with matched[]
106
+ * 3. Diff against previous state with diff[]
107
+ * 4. Render slot content if slots present
108
+ */
109
+ import type { MatchResult, ResolvedSegment } from "../types.js";
110
+ import type { MatchContext, MatchPipelineState } from "./match-context.js";
111
+ import { generateServerTiming, logMetrics } from "./metrics.js";
112
+
113
+ /**
114
+ * Collect all segments from an async generator
115
+ */
116
+ export async function collectSegments(
117
+ generator: AsyncGenerator<ResolvedSegment>
118
+ ): Promise<ResolvedSegment[]> {
119
+ const segments: ResolvedSegment[] = [];
120
+ for await (const segment of generator) {
121
+ segments.push(segment);
122
+ }
123
+ return segments;
124
+ }
125
+
126
+ /**
127
+ * Build the final MatchResult from collected segments and context
128
+ */
129
+ export function buildMatchResult<TEnv>(
130
+ allSegments: ResolvedSegment[],
131
+ ctx: MatchContext<TEnv>,
132
+ state: MatchPipelineState
133
+ ): MatchResult {
134
+ const logPrefix = ctx.isFullMatch ? "[Router.match]" : "[Router.matchPartial]";
135
+
136
+ let allIds: string[];
137
+ let segmentsToRender: ResolvedSegment[];
138
+
139
+ if (ctx.isFullMatch) {
140
+ // Full match (document request) - all segments are rendered
141
+ allIds = allSegments.map((s) => s.id);
142
+ segmentsToRender = allSegments;
143
+ } else {
144
+ // Partial match (navigation) - filter and handle intercepts
145
+ // When intercepting, tell browser to keep its current segments + add modal
146
+ // This prevents the browser from discarding the current page content
147
+ // If client sent empty segments (HMR recovery), use segment IDs from allSegments
148
+ allIds = ctx.interceptResult
149
+ ? ctx.clientSegmentIds.length > 0
150
+ ? [...ctx.clientSegmentIds, ...state.interceptSegments.map((s) => s.id)]
151
+ : allSegments.map((s) => s.id) // Use actual segments, not matchedIds
152
+ : [...state.matchedIds, ...state.interceptSegments.map((s) => s.id)];
153
+
154
+ // Filter out segments with null components (client already has them)
155
+ // BUT always include loader segments - they carry data even with null component
156
+ 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(", ")
171
+ );
172
+ }
173
+
174
+ // Output metrics if enabled
175
+ let serverTiming: string | undefined;
176
+ if (ctx.metricsStore) {
177
+ logMetrics(ctx.request.method, ctx.pathname, ctx.metricsStore);
178
+ serverTiming = generateServerTiming(ctx.metricsStore);
179
+ }
180
+
181
+ return {
182
+ segments: segmentsToRender,
183
+ matched: allIds,
184
+ diff: segmentsToRender.map((s) => s.id),
185
+ params: ctx.matched.params,
186
+ routeName: ctx.routeKey,
187
+ serverTiming,
188
+ slots: Object.keys(state.slots).length > 0 ? state.slots : undefined,
189
+ routeMiddleware:
190
+ ctx.routeMiddleware.length > 0 ? ctx.routeMiddleware : undefined,
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Collect segments from pipeline and build MatchResult
196
+ *
197
+ * This is the main entry point for building the final result after
198
+ * the pipeline has processed all segments.
199
+ */
200
+ export async function collectMatchResult<TEnv>(
201
+ pipeline: AsyncGenerator<ResolvedSegment>,
202
+ ctx: MatchContext<TEnv>,
203
+ state: MatchPipelineState
204
+ ): Promise<MatchResult> {
205
+ const allSegments = await collectSegments(pipeline);
206
+
207
+ // Update state with collected segments if not already set
208
+ if (state.segments.length === 0) {
209
+ state.segments = allSegments;
210
+ }
211
+
212
+ return buildMatchResult(allSegments, ctx, state);
213
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Router Metrics Utilities
3
+ *
4
+ * Performance metrics collection and reporting for RSC Router.
5
+ */
6
+
7
+ import type { MetricsStore } from "../server/context";
8
+
9
+ /**
10
+ * Create a metrics store for the request if debugPerformance is enabled
11
+ */
12
+ export function createMetricsStore(
13
+ debugPerformance: boolean
14
+ ): MetricsStore | undefined {
15
+ if (!debugPerformance) return undefined;
16
+ return {
17
+ enabled: true,
18
+ requestStart: performance.now(),
19
+ metrics: [],
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Log metrics to console in a formatted way
25
+ */
26
+ export function logMetrics(
27
+ method: string,
28
+ pathname: string,
29
+ metricsStore: MetricsStore
30
+ ): void {
31
+ const total = performance.now() - metricsStore.requestStart;
32
+
33
+ // Find max label length for alignment
34
+ const maxLabelLen = Math.max(
35
+ ...metricsStore.metrics.map((m) => m.label.length),
36
+ 20
37
+ );
38
+
39
+ console.log(`[RSC Perf] ${method} ${pathname} (${total.toFixed(1)}ms)`);
40
+
41
+ for (const m of metricsStore.metrics) {
42
+ const paddedLabel = m.label.padEnd(maxLabelLen);
43
+ console.log(` ${paddedLabel} ${m.duration.toFixed(1)}ms`);
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Generate Server-Timing header value from metrics
49
+ * Format: metric-name;dur=X.XX
50
+ */
51
+ export function generateServerTiming(metricsStore: MetricsStore): string {
52
+ return metricsStore.metrics
53
+ .map((m) => {
54
+ // Convert label to valid Server-Timing name (alphanumeric and hyphens)
55
+ const name = m.label
56
+ .replace(/:/g, "-")
57
+ .replace(/[^a-zA-Z0-9-]/g, "")
58
+ .toLowerCase();
59
+ return `${name};dur=${m.duration.toFixed(2)}`;
60
+ })
61
+ .join(", ");
62
+ }