@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.80

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 (312) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +942 -4
  3. package/dist/bin/rango.js +1689 -0
  4. package/dist/vite/index.js +4960 -935
  5. package/package.json +70 -60
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +294 -0
  8. package/skills/caching/SKILL.md +93 -23
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +12 -8
  11. package/skills/document-cache/SKILL.md +18 -16
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/handler-use/SKILL.md +362 -0
  14. package/skills/hooks/SKILL.md +334 -72
  15. package/skills/host-router/SKILL.md +218 -0
  16. package/skills/intercept/SKILL.md +151 -8
  17. package/skills/layout/SKILL.md +122 -3
  18. package/skills/links/SKILL.md +92 -31
  19. package/skills/loader/SKILL.md +404 -44
  20. package/skills/middleware/SKILL.md +205 -37
  21. package/skills/migrate-nextjs/SKILL.md +560 -0
  22. package/skills/migrate-react-router/SKILL.md +764 -0
  23. package/skills/mime-routes/SKILL.md +128 -0
  24. package/skills/parallel/SKILL.md +263 -1
  25. package/skills/prerender/SKILL.md +685 -0
  26. package/skills/rango/SKILL.md +87 -16
  27. package/skills/response-routes/SKILL.md +411 -0
  28. package/skills/route/SKILL.md +281 -14
  29. package/skills/router-setup/SKILL.md +210 -32
  30. package/skills/tailwind/SKILL.md +129 -0
  31. package/skills/theme/SKILL.md +9 -8
  32. package/skills/typesafety/SKILL.md +328 -89
  33. package/skills/use-cache/SKILL.md +324 -0
  34. package/src/__internal.ts +102 -4
  35. package/src/bin/rango.ts +321 -0
  36. package/src/browser/action-coordinator.ts +97 -0
  37. package/src/browser/action-response-classifier.ts +99 -0
  38. package/src/browser/app-version.ts +14 -0
  39. package/src/browser/event-controller.ts +92 -64
  40. package/src/browser/history-state.ts +80 -0
  41. package/src/browser/intercept-utils.ts +52 -0
  42. package/src/browser/link-interceptor.ts +24 -4
  43. package/src/browser/logging.ts +55 -0
  44. package/src/browser/merge-segment-loaders.ts +20 -12
  45. package/src/browser/navigation-bridge.ts +317 -560
  46. package/src/browser/navigation-client.ts +206 -68
  47. package/src/browser/navigation-store.ts +73 -55
  48. package/src/browser/navigation-transaction.ts +297 -0
  49. package/src/browser/network-error-handler.ts +61 -0
  50. package/src/browser/partial-update.ts +343 -316
  51. package/src/browser/prefetch/cache.ts +216 -0
  52. package/src/browser/prefetch/fetch.ts +206 -0
  53. package/src/browser/prefetch/observer.ts +65 -0
  54. package/src/browser/prefetch/policy.ts +48 -0
  55. package/src/browser/prefetch/queue.ts +160 -0
  56. package/src/browser/prefetch/resource-ready.ts +77 -0
  57. package/src/browser/rango-state.ts +112 -0
  58. package/src/browser/react/Link.tsx +253 -74
  59. package/src/browser/react/NavigationProvider.tsx +87 -11
  60. package/src/browser/react/context.ts +11 -0
  61. package/src/browser/react/filter-segment-order.ts +11 -0
  62. package/src/browser/react/index.ts +12 -12
  63. package/src/browser/react/location-state-shared.ts +95 -53
  64. package/src/browser/react/location-state.ts +60 -15
  65. package/src/browser/react/mount-context.ts +6 -1
  66. package/src/browser/react/nonce-context.ts +23 -0
  67. package/src/browser/react/shallow-equal.ts +27 -0
  68. package/src/browser/react/use-action.ts +29 -51
  69. package/src/browser/react/use-client-cache.ts +5 -3
  70. package/src/browser/react/use-handle.ts +30 -126
  71. package/src/browser/react/use-href.tsx +2 -2
  72. package/src/browser/react/use-link-status.ts +6 -5
  73. package/src/browser/react/use-navigation.ts +44 -65
  74. package/src/browser/react/use-params.ts +65 -0
  75. package/src/browser/react/use-pathname.ts +47 -0
  76. package/src/browser/react/use-router.ts +76 -0
  77. package/src/browser/react/use-search-params.ts +56 -0
  78. package/src/browser/react/use-segments.ts +80 -97
  79. package/src/browser/response-adapter.ts +73 -0
  80. package/src/browser/rsc-router.tsx +214 -58
  81. package/src/browser/scroll-restoration.ts +127 -52
  82. package/src/browser/segment-reconciler.ts +243 -0
  83. package/src/browser/segment-structure-assert.ts +16 -0
  84. package/src/browser/server-action-bridge.ts +510 -603
  85. package/src/browser/shallow.ts +6 -1
  86. package/src/browser/types.ts +141 -48
  87. package/src/browser/validate-redirect-origin.ts +29 -0
  88. package/src/build/generate-manifest.ts +235 -24
  89. package/src/build/generate-route-types.ts +39 -0
  90. package/src/build/index.ts +13 -0
  91. package/src/build/route-trie.ts +291 -0
  92. package/src/build/route-types/ast-helpers.ts +25 -0
  93. package/src/build/route-types/ast-route-extraction.ts +98 -0
  94. package/src/build/route-types/codegen.ts +102 -0
  95. package/src/build/route-types/include-resolution.ts +418 -0
  96. package/src/build/route-types/param-extraction.ts +48 -0
  97. package/src/build/route-types/per-module-writer.ts +128 -0
  98. package/src/build/route-types/router-processing.ts +618 -0
  99. package/src/build/route-types/scan-filter.ts +85 -0
  100. package/src/build/runtime-discovery.ts +231 -0
  101. package/src/cache/background-task.ts +34 -0
  102. package/src/cache/cache-key-utils.ts +44 -0
  103. package/src/cache/cache-policy.ts +125 -0
  104. package/src/cache/cache-runtime.ts +342 -0
  105. package/src/cache/cache-scope.ts +167 -309
  106. package/src/cache/cf/cf-cache-store.ts +571 -17
  107. package/src/cache/cf/index.ts +13 -3
  108. package/src/cache/document-cache.ts +116 -77
  109. package/src/cache/handle-capture.ts +81 -0
  110. package/src/cache/handle-snapshot.ts +41 -0
  111. package/src/cache/index.ts +1 -15
  112. package/src/cache/memory-segment-store.ts +191 -13
  113. package/src/cache/profile-registry.ts +73 -0
  114. package/src/cache/read-through-swr.ts +134 -0
  115. package/src/cache/segment-codec.ts +256 -0
  116. package/src/cache/taint.ts +153 -0
  117. package/src/cache/types.ts +72 -122
  118. package/src/client.rsc.tsx +3 -1
  119. package/src/client.tsx +135 -301
  120. package/src/component-utils.ts +4 -4
  121. package/src/components/DefaultDocument.tsx +5 -1
  122. package/src/context-var.ts +156 -0
  123. package/src/debug.ts +19 -9
  124. package/src/errors.ts +108 -2
  125. package/src/handle.ts +55 -29
  126. package/src/handles/MetaTags.tsx +73 -20
  127. package/src/handles/breadcrumbs.ts +66 -0
  128. package/src/handles/index.ts +1 -0
  129. package/src/handles/meta.ts +30 -13
  130. package/src/host/cookie-handler.ts +21 -15
  131. package/src/host/errors.ts +8 -8
  132. package/src/host/index.ts +4 -7
  133. package/src/host/pattern-matcher.ts +27 -27
  134. package/src/host/router.ts +61 -39
  135. package/src/host/testing.ts +8 -8
  136. package/src/host/types.ts +15 -7
  137. package/src/host/utils.ts +1 -1
  138. package/src/href-client.ts +119 -29
  139. package/src/index.rsc.ts +155 -19
  140. package/src/index.ts +251 -30
  141. package/src/internal-debug.ts +11 -0
  142. package/src/loader.rsc.ts +26 -157
  143. package/src/loader.ts +27 -10
  144. package/src/network-error-thrower.tsx +3 -1
  145. package/src/outlet-provider.tsx +45 -0
  146. package/src/prerender/param-hash.ts +37 -0
  147. package/src/prerender/store.ts +186 -0
  148. package/src/prerender.ts +524 -0
  149. package/src/reverse.ts +354 -0
  150. package/src/root-error-boundary.tsx +41 -29
  151. package/src/route-content-wrapper.tsx +7 -4
  152. package/src/route-definition/dsl-helpers.ts +1121 -0
  153. package/src/route-definition/helper-factories.ts +200 -0
  154. package/src/route-definition/helpers-types.ts +478 -0
  155. package/src/route-definition/index.ts +55 -0
  156. package/src/route-definition/redirect.ts +101 -0
  157. package/src/route-definition/resolve-handler-use.ts +149 -0
  158. package/src/route-definition.ts +1 -1428
  159. package/src/route-map-builder.ts +217 -123
  160. package/src/route-name.ts +53 -0
  161. package/src/route-types.ts +77 -8
  162. package/src/router/content-negotiation.ts +215 -0
  163. package/src/router/debug-manifest.ts +72 -0
  164. package/src/router/error-handling.ts +9 -9
  165. package/src/router/find-match.ts +160 -0
  166. package/src/router/handler-context.ts +438 -86
  167. package/src/router/intercept-resolution.ts +402 -0
  168. package/src/router/lazy-includes.ts +237 -0
  169. package/src/router/loader-resolution.ts +356 -128
  170. package/src/router/logging.ts +251 -0
  171. package/src/router/manifest.ts +163 -35
  172. package/src/router/match-api.ts +555 -0
  173. package/src/router/match-context.ts +5 -3
  174. package/src/router/match-handlers.ts +440 -0
  175. package/src/router/match-middleware/background-revalidation.ts +108 -93
  176. package/src/router/match-middleware/cache-lookup.ts +460 -10
  177. package/src/router/match-middleware/cache-store.ts +98 -26
  178. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  179. package/src/router/match-middleware/segment-resolution.ts +80 -6
  180. package/src/router/match-pipelines.ts +10 -45
  181. package/src/router/match-result.ts +135 -35
  182. package/src/router/metrics.ts +240 -15
  183. package/src/router/middleware-cookies.ts +55 -0
  184. package/src/router/middleware-types.ts +220 -0
  185. package/src/router/middleware.ts +324 -369
  186. package/src/router/navigation-snapshot.ts +182 -0
  187. package/src/router/pattern-matching.ts +211 -43
  188. package/src/router/prerender-match.ts +502 -0
  189. package/src/router/preview-match.ts +98 -0
  190. package/src/router/request-classification.ts +310 -0
  191. package/src/router/revalidation.ts +137 -38
  192. package/src/router/route-snapshot.ts +245 -0
  193. package/src/router/router-context.ts +41 -21
  194. package/src/router/router-interfaces.ts +484 -0
  195. package/src/router/router-options.ts +618 -0
  196. package/src/router/router-registry.ts +24 -0
  197. package/src/router/segment-resolution/fresh.ts +748 -0
  198. package/src/router/segment-resolution/helpers.ts +268 -0
  199. package/src/router/segment-resolution/loader-cache.ts +199 -0
  200. package/src/router/segment-resolution/revalidation.ts +1379 -0
  201. package/src/router/segment-resolution/static-store.ts +67 -0
  202. package/src/router/segment-resolution.ts +21 -0
  203. package/src/router/segment-wrappers.ts +291 -0
  204. package/src/router/telemetry-otel.ts +299 -0
  205. package/src/router/telemetry.ts +300 -0
  206. package/src/router/timeout.ts +148 -0
  207. package/src/router/trie-matching.ts +239 -0
  208. package/src/router/types.ts +78 -3
  209. package/src/router.ts +740 -4252
  210. package/src/rsc/handler-context.ts +45 -0
  211. package/src/rsc/handler.ts +907 -797
  212. package/src/rsc/helpers.ts +140 -6
  213. package/src/rsc/index.ts +0 -20
  214. package/src/rsc/loader-fetch.ts +229 -0
  215. package/src/rsc/manifest-init.ts +90 -0
  216. package/src/rsc/nonce.ts +14 -0
  217. package/src/rsc/origin-guard.ts +141 -0
  218. package/src/rsc/progressive-enhancement.ts +391 -0
  219. package/src/rsc/response-error.ts +37 -0
  220. package/src/rsc/response-route-handler.ts +347 -0
  221. package/src/rsc/rsc-rendering.ts +246 -0
  222. package/src/rsc/runtime-warnings.ts +42 -0
  223. package/src/rsc/server-action.ts +356 -0
  224. package/src/rsc/ssr-setup.ts +128 -0
  225. package/src/rsc/types.ts +46 -11
  226. package/src/search-params.ts +230 -0
  227. package/src/segment-content-promise.ts +67 -0
  228. package/src/segment-loader-promise.ts +122 -0
  229. package/src/segment-system.tsx +134 -36
  230. package/src/server/context.ts +341 -61
  231. package/src/server/cookie-store.ts +190 -0
  232. package/src/server/fetchable-loader-store.ts +37 -0
  233. package/src/server/handle-store.ts +113 -15
  234. package/src/server/loader-registry.ts +24 -64
  235. package/src/server/request-context.ts +607 -81
  236. package/src/server.ts +35 -130
  237. package/src/ssr/index.tsx +103 -30
  238. package/src/static-handler.ts +126 -0
  239. package/src/theme/ThemeProvider.tsx +21 -15
  240. package/src/theme/ThemeScript.tsx +5 -5
  241. package/src/theme/constants.ts +5 -2
  242. package/src/theme/index.ts +4 -14
  243. package/src/theme/theme-context.ts +4 -30
  244. package/src/theme/theme-script.ts +21 -18
  245. package/src/types/boundaries.ts +158 -0
  246. package/src/types/cache-types.ts +198 -0
  247. package/src/types/error-types.ts +192 -0
  248. package/src/types/global-namespace.ts +100 -0
  249. package/src/types/handler-context.ts +791 -0
  250. package/src/types/index.ts +88 -0
  251. package/src/types/loader-types.ts +210 -0
  252. package/src/types/route-config.ts +170 -0
  253. package/src/types/route-entry.ts +120 -0
  254. package/src/types/segments.ts +150 -0
  255. package/src/types.ts +1 -1623
  256. package/src/urls/include-helper.ts +207 -0
  257. package/src/urls/index.ts +53 -0
  258. package/src/urls/path-helper-types.ts +372 -0
  259. package/src/urls/path-helper.ts +364 -0
  260. package/src/urls/pattern-types.ts +107 -0
  261. package/src/urls/response-types.ts +116 -0
  262. package/src/urls/type-extraction.ts +372 -0
  263. package/src/urls/urls-function.ts +98 -0
  264. package/src/urls.ts +1 -802
  265. package/src/use-loader.tsx +161 -81
  266. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  267. package/src/vite/discovery/discover-routers.ts +348 -0
  268. package/src/vite/discovery/prerender-collection.ts +439 -0
  269. package/src/vite/discovery/route-types-writer.ts +258 -0
  270. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  271. package/src/vite/discovery/state.ts +117 -0
  272. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  273. package/src/vite/index.ts +15 -1133
  274. package/src/vite/plugin-types.ts +103 -0
  275. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  276. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  277. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  278. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  279. package/src/vite/plugins/expose-id-utils.ts +299 -0
  280. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  281. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  282. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  283. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  284. package/src/vite/plugins/expose-ids/types.ts +45 -0
  285. package/src/vite/plugins/expose-internal-ids.ts +786 -0
  286. package/src/vite/plugins/performance-tracks.ts +88 -0
  287. package/src/vite/plugins/refresh-cmd.ts +127 -0
  288. package/src/vite/plugins/use-cache-transform.ts +323 -0
  289. package/src/vite/plugins/version-injector.ts +83 -0
  290. package/src/vite/plugins/version-plugin.ts +266 -0
  291. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  292. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  293. package/src/vite/rango.ts +462 -0
  294. package/src/vite/router-discovery.ts +918 -0
  295. package/src/vite/utils/ast-handler-extract.ts +517 -0
  296. package/src/vite/utils/banner.ts +36 -0
  297. package/src/vite/utils/bundle-analysis.ts +137 -0
  298. package/src/vite/utils/manifest-utils.ts +70 -0
  299. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  300. package/src/vite/utils/prerender-utils.ts +221 -0
  301. package/src/vite/utils/shared-utils.ts +170 -0
  302. package/CLAUDE.md +0 -43
  303. package/src/browser/lru-cache.ts +0 -69
  304. package/src/browser/request-controller.ts +0 -164
  305. package/src/cache/memory-store.ts +0 -253
  306. package/src/href-context.ts +0 -33
  307. package/src/href.ts +0 -255
  308. package/src/server/route-manifest-cache.ts +0 -173
  309. package/src/vite/expose-handle-id.ts +0 -209
  310. package/src/vite/expose-loader-id.ts +0 -426
  311. package/src/vite/expose-location-state-id.ts +0 -177
  312. /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
- * - (Client already has this segment's UI)
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 { generateServerTiming, logMetrics } from "./metrics.js";
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) {
@@ -123,23 +125,99 @@ export async function collectSegments(
123
125
  return segments;
124
126
  }
125
127
 
128
+ /**
129
+ * Deduplicate inherited loader segments by loaderId.
130
+ *
131
+ * When a route has loaders and a child layout has parallel slots, the same
132
+ * loader is resolved twice: once for the route and once inherited into the
133
+ * layout (tagged with `_inherited`). The inherited copy is only needed when
134
+ * the route uses `loading()` — in that case, the loader data is inside a
135
+ * LoaderBoundary/Suspense that parallel slots can't reach through. Without
136
+ * loading(), useLoader() traverses parent contexts and finds the data.
137
+ */
138
+ function deduplicateLoaderSegments(
139
+ segments: ResolvedSegment[],
140
+ logPrefix: string,
141
+ ): ResolvedSegment[] {
142
+ // First pass: collect loaderIds of original (non-inherited) segments
143
+ // and whether their parent entry uses loading()
144
+ const originalLoaders = new Set<string>();
145
+ const loadersWithLoading = new Set<string>();
146
+ for (const s of segments) {
147
+ if (s.type === "loader" && s.loaderId && !s._inherited) {
148
+ originalLoaders.add(s.loaderId);
149
+ // If the segment has a sibling with loading, the parent uses loading()
150
+ // We detect this by checking if any non-loader segment in the same
151
+ // namespace has loading defined
152
+ }
153
+ }
154
+ // Check if any layout/route segment has loading — if a loader's namespace
155
+ // matches a segment with loading, the inherited copy is needed
156
+ for (const s of segments) {
157
+ if (s.type !== "loader" && s.loading !== undefined && s.loading !== false) {
158
+ // Find loaders in this namespace
159
+ for (const l of segments) {
160
+ if (l.type === "loader" && l.namespace === s.namespace && l.loaderId) {
161
+ loadersWithLoading.add(l.loaderId);
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ const result: ResolvedSegment[] = [];
168
+ let dedupCount = 0;
169
+
170
+ for (const s of segments) {
171
+ if (
172
+ s.type === "loader" &&
173
+ s.loaderId &&
174
+ s._inherited &&
175
+ originalLoaders.has(s.loaderId) &&
176
+ !loadersWithLoading.has(s.loaderId)
177
+ ) {
178
+ dedupCount++;
179
+ continue;
180
+ }
181
+ result.push(s);
182
+ }
183
+
184
+ if (dedupCount > 0) {
185
+ debugLog(logPrefix, `deduped ${dedupCount} inherited loader segment(s)`);
186
+ }
187
+
188
+ return result;
189
+ }
190
+
126
191
  /**
127
192
  * Build the final MatchResult from collected segments and context
128
193
  */
129
194
  export function buildMatchResult<TEnv>(
130
195
  allSegments: ResolvedSegment[],
131
196
  ctx: MatchContext<TEnv>,
132
- state: MatchPipelineState
197
+ state: MatchPipelineState,
133
198
  ): MatchResult {
134
- const logPrefix = ctx.isFullMatch ? "[Router.match]" : "[Router.matchPartial]";
199
+ const logPrefix = ctx.isFullMatch
200
+ ? "[Router.match]"
201
+ : "[Router.matchPartial]";
135
202
 
136
203
  let allIds: string[];
137
204
  let segmentsToRender: ResolvedSegment[];
138
205
 
139
206
  if (ctx.isFullMatch) {
140
207
  // Full match (document request) - all segments are rendered
141
- allIds = allSegments.map((s) => s.id);
142
- segmentsToRender = allSegments;
208
+ // Deduplicate by segment ID (defense-in-depth). The primary dedup is in
209
+ // resolveAllSegments, but this guards against any path that bypasses it.
210
+ // include() scopes can produce entries that resolve the same shared layout,
211
+ // and duplicate IDs change the client's React tree depth causing remounts.
212
+ const seen = new Set<string>();
213
+ segmentsToRender = [];
214
+ for (const s of allSegments) {
215
+ if (!seen.has(s.id)) {
216
+ seen.add(s.id);
217
+ segmentsToRender.push(s);
218
+ }
219
+ }
220
+ allIds = segmentsToRender.map((s) => s.id);
143
221
  } else {
144
222
  // Partial match (navigation) - filter and handle intercepts
145
223
  // When intercepting, tell browser to keep its current segments + add modal
@@ -151,40 +229,53 @@ export function buildMatchResult<TEnv>(
151
229
  : allSegments.map((s) => s.id) // Use actual segments, not matchedIds
152
230
  : [...state.matchedIds, ...state.interceptSegments.map((s) => s.id)];
153
231
 
154
- // Filter out segments with null components (client already has them)
155
- // BUT always include loader segments - they carry data even with null component
232
+ // Deduplicate allIds (defense-in-depth for partial match path)
233
+ allIds = [...new Set(allIds)];
234
+
235
+ // Filter out null-component segments only when the client already has
236
+ // them cached (revalidation skip). If the client doesn't have the segment,
237
+ // it must be included even with null component — it's structurally required
238
+ // as a parent node for child layouts/parallels to reconcile against.
239
+ // Loader segments are always included as they carry data.
240
+ const clientIdSet = new Set(ctx.clientSegmentIds);
156
241
  segmentsToRender = allSegments.filter(
157
- (s) => s.component !== null || s.type === "loader"
242
+ (s) =>
243
+ s.component !== null || s.type === "loader" || !clientIdSet.has(s.id),
158
244
  );
159
245
  }
160
246
 
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
- }
247
+ const dedupedSegments = deduplicateLoaderSegments(
248
+ segmentsToRender,
249
+ logPrefix,
250
+ );
173
251
 
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
- }
252
+ debugLog(logPrefix, "all segments", {
253
+ segments: allSegments.map((s) => ({
254
+ id: s.id,
255
+ type: s.type,
256
+ hasComponent: s.component !== null,
257
+ })),
258
+ });
259
+ debugLog(logPrefix, "segments to render", {
260
+ segmentIds: dedupedSegments.map((s) => s.id),
261
+ });
262
+
263
+ // Remove deduped loader IDs from matched so the client doesn't treat
264
+ // them as missing segments and trigger a fallback refetch.
265
+ const removedIds = new Set(
266
+ segmentsToRender
267
+ .filter((s) => !dedupedSegments.includes(s))
268
+ .map((s) => s.id),
269
+ );
270
+ const matchedIds =
271
+ removedIds.size > 0 ? allIds.filter((id) => !removedIds.has(id)) : allIds;
180
272
 
181
273
  return {
182
- segments: segmentsToRender,
183
- matched: allIds,
184
- diff: segmentsToRender.map((s) => s.id),
274
+ segments: dedupedSegments,
275
+ matched: matchedIds,
276
+ diff: dedupedSegments.map((s) => s.id),
185
277
  params: ctx.matched.params,
186
278
  routeName: ctx.routeKey,
187
- serverTiming,
188
279
  slots: Object.keys(state.slots).length > 0 ? state.slots : undefined,
189
280
  routeMiddleware:
190
281
  ctx.routeMiddleware.length > 0 ? ctx.routeMiddleware : undefined,
@@ -200,14 +291,23 @@ export function buildMatchResult<TEnv>(
200
291
  export async function collectMatchResult<TEnv>(
201
292
  pipeline: AsyncGenerator<ResolvedSegment>,
202
293
  ctx: MatchContext<TEnv>,
203
- state: MatchPipelineState
294
+ state: MatchPipelineState,
204
295
  ): Promise<MatchResult> {
205
296
  const allSegments = await collectSegments(pipeline);
206
297
 
298
+ const buildStart = performance.now();
299
+
207
300
  // Update state with collected segments if not already set
208
301
  if (state.segments.length === 0) {
209
302
  state.segments = allSegments;
210
303
  }
211
304
 
212
- return buildMatchResult(allSegments, ctx, state);
305
+ const result = buildMatchResult(allSegments, ctx, state);
306
+ appendMetric(
307
+ ctx.metricsStore,
308
+ "collect-result",
309
+ buildStart,
310
+ performance.now() - buildStart,
311
+ );
312
+ return result;
213
313
  }
@@ -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
- * Log metrics to console in a formatted way
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
- // Find max label length for alignment
34
- const maxLabelLen = Math.max(
35
- ...metricsStore.metrics.map((m) => m.label.length),
36
- 20
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
- console.log(`[RSC Perf] ${method} ${pathname} (${total.toFixed(1)}ms)`);
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
- for (const m of metricsStore.metrics) {
42
- const paddedLabel = m.label.padEnd(maxLabelLen);
43
- console.log(` ${paddedLabel} ${m.duration.toFixed(1)}ms`);
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 name = m.label
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
+ }