@rangojs/router 0.0.0-experimental.13 → 0.0.0-experimental.13221847

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 (298) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +884 -4
  3. package/dist/bin/rango.js +1531 -212
  4. package/dist/vite/index.js +3995 -2489
  5. package/package.json +57 -52
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +85 -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 +6 -4
  13. package/skills/hooks/SKILL.md +328 -70
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +131 -8
  16. package/skills/layout/SKILL.md +100 -3
  17. package/skills/links/SKILL.md +62 -15
  18. package/skills/loader/SKILL.md +368 -42
  19. package/skills/middleware/SKILL.md +171 -34
  20. package/skills/mime-routes/SKILL.md +14 -10
  21. package/skills/parallel/SKILL.md +137 -1
  22. package/skills/prerender/SKILL.md +366 -28
  23. package/skills/rango/SKILL.md +85 -21
  24. package/skills/response-routes/SKILL.md +136 -83
  25. package/skills/route/SKILL.md +195 -21
  26. package/skills/router-setup/SKILL.md +123 -30
  27. package/skills/theme/SKILL.md +9 -8
  28. package/skills/typesafety/SKILL.md +240 -102
  29. package/skills/use-cache/SKILL.md +324 -0
  30. package/src/__internal.ts +102 -4
  31. package/src/bin/rango.ts +312 -15
  32. package/src/browser/action-coordinator.ts +97 -0
  33. package/src/browser/action-response-classifier.ts +99 -0
  34. package/src/browser/event-controller.ts +92 -64
  35. package/src/browser/history-state.ts +80 -0
  36. package/src/browser/intercept-utils.ts +52 -0
  37. package/src/browser/link-interceptor.ts +24 -4
  38. package/src/browser/logging.ts +11 -0
  39. package/src/browser/merge-segment-loaders.ts +20 -12
  40. package/src/browser/navigation-bridge.ts +266 -558
  41. package/src/browser/navigation-client.ts +132 -75
  42. package/src/browser/navigation-store.ts +33 -50
  43. package/src/browser/navigation-transaction.ts +297 -0
  44. package/src/browser/network-error-handler.ts +61 -0
  45. package/src/browser/partial-update.ts +303 -309
  46. package/src/browser/prefetch/cache.ts +206 -0
  47. package/src/browser/prefetch/fetch.ts +144 -0
  48. package/src/browser/prefetch/observer.ts +65 -0
  49. package/src/browser/prefetch/policy.ts +48 -0
  50. package/src/browser/prefetch/queue.ts +128 -0
  51. package/src/browser/rango-state.ts +112 -0
  52. package/src/browser/react/Link.tsx +190 -70
  53. package/src/browser/react/NavigationProvider.tsx +78 -11
  54. package/src/browser/react/context.ts +6 -0
  55. package/src/browser/react/filter-segment-order.ts +11 -0
  56. package/src/browser/react/index.ts +12 -12
  57. package/src/browser/react/location-state-shared.ts +95 -53
  58. package/src/browser/react/location-state.ts +60 -15
  59. package/src/browser/react/mount-context.ts +6 -1
  60. package/src/browser/react/nonce-context.ts +23 -0
  61. package/src/browser/react/shallow-equal.ts +27 -0
  62. package/src/browser/react/use-action.ts +29 -51
  63. package/src/browser/react/use-client-cache.ts +5 -3
  64. package/src/browser/react/use-handle.ts +29 -70
  65. package/src/browser/react/use-link-status.ts +6 -5
  66. package/src/browser/react/use-navigation.ts +22 -63
  67. package/src/browser/react/use-params.ts +65 -0
  68. package/src/browser/react/use-pathname.ts +47 -0
  69. package/src/browser/react/use-router.ts +63 -0
  70. package/src/browser/react/use-search-params.ts +56 -0
  71. package/src/browser/react/use-segments.ts +80 -97
  72. package/src/browser/response-adapter.ts +73 -0
  73. package/src/browser/rsc-router.tsx +188 -57
  74. package/src/browser/scroll-restoration.ts +117 -44
  75. package/src/browser/segment-reconciler.ts +221 -0
  76. package/src/browser/segment-structure-assert.ts +16 -0
  77. package/src/browser/server-action-bridge.ts +488 -606
  78. package/src/browser/shallow.ts +6 -1
  79. package/src/browser/types.ts +116 -47
  80. package/src/browser/validate-redirect-origin.ts +29 -0
  81. package/src/build/generate-manifest.ts +63 -21
  82. package/src/build/generate-route-types.ts +36 -1038
  83. package/src/build/index.ts +2 -5
  84. package/src/build/route-trie.ts +38 -12
  85. package/src/build/route-types/ast-helpers.ts +25 -0
  86. package/src/build/route-types/ast-route-extraction.ts +98 -0
  87. package/src/build/route-types/codegen.ts +102 -0
  88. package/src/build/route-types/include-resolution.ts +411 -0
  89. package/src/build/route-types/param-extraction.ts +48 -0
  90. package/src/build/route-types/per-module-writer.ts +128 -0
  91. package/src/build/route-types/router-processing.ts +479 -0
  92. package/src/build/route-types/scan-filter.ts +78 -0
  93. package/src/build/runtime-discovery.ts +231 -0
  94. package/src/cache/background-task.ts +34 -0
  95. package/src/cache/cache-key-utils.ts +44 -0
  96. package/src/cache/cache-policy.ts +125 -0
  97. package/src/cache/cache-runtime.ts +342 -0
  98. package/src/cache/cache-scope.ts +122 -303
  99. package/src/cache/cf/cf-cache-store.ts +571 -17
  100. package/src/cache/cf/index.ts +13 -3
  101. package/src/cache/document-cache.ts +116 -77
  102. package/src/cache/handle-capture.ts +81 -0
  103. package/src/cache/handle-snapshot.ts +41 -0
  104. package/src/cache/index.ts +1 -15
  105. package/src/cache/memory-segment-store.ts +191 -13
  106. package/src/cache/profile-registry.ts +73 -0
  107. package/src/cache/read-through-swr.ts +134 -0
  108. package/src/cache/segment-codec.ts +256 -0
  109. package/src/cache/taint.ts +98 -0
  110. package/src/cache/types.ts +72 -122
  111. package/src/client.rsc.tsx +3 -1
  112. package/src/client.tsx +84 -126
  113. package/src/component-utils.ts +4 -4
  114. package/src/components/DefaultDocument.tsx +5 -1
  115. package/src/context-var.ts +86 -0
  116. package/src/debug.ts +19 -9
  117. package/src/errors.ts +77 -7
  118. package/src/handle.ts +12 -7
  119. package/src/handles/MetaTags.tsx +73 -20
  120. package/src/handles/breadcrumbs.ts +66 -0
  121. package/src/handles/index.ts +1 -0
  122. package/src/handles/meta.ts +30 -13
  123. package/src/host/cookie-handler.ts +21 -15
  124. package/src/host/errors.ts +8 -8
  125. package/src/host/index.ts +4 -7
  126. package/src/host/pattern-matcher.ts +27 -27
  127. package/src/host/router.ts +61 -39
  128. package/src/host/testing.ts +8 -8
  129. package/src/host/types.ts +15 -7
  130. package/src/host/utils.ts +1 -1
  131. package/src/href-client.ts +65 -45
  132. package/src/index.rsc.ts +104 -40
  133. package/src/index.ts +122 -67
  134. package/src/internal-debug.ts +9 -3
  135. package/src/loader.rsc.ts +18 -93
  136. package/src/loader.ts +26 -9
  137. package/src/network-error-thrower.tsx +3 -1
  138. package/src/outlet-provider.tsx +45 -0
  139. package/src/prerender/param-hash.ts +4 -2
  140. package/src/prerender/store.ts +121 -17
  141. package/src/prerender.ts +325 -20
  142. package/src/reverse.ts +144 -124
  143. package/src/root-error-boundary.tsx +41 -29
  144. package/src/route-content-wrapper.tsx +7 -4
  145. package/src/route-definition/dsl-helpers.ts +959 -0
  146. package/src/route-definition/helper-factories.ts +200 -0
  147. package/src/route-definition/helpers-types.ts +430 -0
  148. package/src/route-definition/index.ts +52 -0
  149. package/src/route-definition/redirect.ts +93 -0
  150. package/src/route-definition.ts +1 -1450
  151. package/src/route-map-builder.ts +87 -133
  152. package/src/route-name.ts +53 -0
  153. package/src/route-types.ts +41 -6
  154. package/src/router/content-negotiation.ts +116 -0
  155. package/src/router/debug-manifest.ts +72 -0
  156. package/src/router/error-handling.ts +9 -9
  157. package/src/router/find-match.ts +160 -0
  158. package/src/router/handler-context.ts +324 -116
  159. package/src/router/intercept-resolution.ts +11 -4
  160. package/src/router/lazy-includes.ts +237 -0
  161. package/src/router/loader-resolution.ts +179 -133
  162. package/src/router/logging.ts +112 -6
  163. package/src/router/manifest.ts +58 -19
  164. package/src/router/match-api.ts +89 -88
  165. package/src/router/match-context.ts +4 -2
  166. package/src/router/match-handlers.ts +440 -0
  167. package/src/router/match-middleware/background-revalidation.ts +86 -89
  168. package/src/router/match-middleware/cache-lookup.ts +295 -49
  169. package/src/router/match-middleware/cache-store.ts +56 -13
  170. package/src/router/match-middleware/intercept-resolution.ts +45 -22
  171. package/src/router/match-middleware/segment-resolution.ts +20 -9
  172. package/src/router/match-pipelines.ts +10 -45
  173. package/src/router/match-result.ts +44 -21
  174. package/src/router/metrics.ts +240 -15
  175. package/src/router/middleware-cookies.ts +55 -0
  176. package/src/router/middleware-types.ts +222 -0
  177. package/src/router/middleware.ts +327 -369
  178. package/src/router/pattern-matching.ts +169 -31
  179. package/src/router/prerender-match.ts +402 -0
  180. package/src/router/preview-match.ts +170 -0
  181. package/src/router/revalidation.ts +105 -14
  182. package/src/router/router-context.ts +40 -21
  183. package/src/router/router-interfaces.ts +452 -0
  184. package/src/router/router-options.ts +592 -0
  185. package/src/router/router-registry.ts +24 -0
  186. package/src/router/segment-resolution/fresh.ts +677 -0
  187. package/src/router/segment-resolution/helpers.ts +263 -0
  188. package/src/router/segment-resolution/loader-cache.ts +199 -0
  189. package/src/router/segment-resolution/revalidation.ts +1296 -0
  190. package/src/router/segment-resolution/static-store.ts +67 -0
  191. package/src/router/segment-resolution.ts +21 -1354
  192. package/src/router/segment-wrappers.ts +291 -0
  193. package/src/router/telemetry-otel.ts +299 -0
  194. package/src/router/telemetry.ts +300 -0
  195. package/src/router/timeout.ts +148 -0
  196. package/src/router/trie-matching.ts +96 -29
  197. package/src/router/types.ts +15 -9
  198. package/src/router.ts +642 -2366
  199. package/src/rsc/handler-context.ts +45 -0
  200. package/src/rsc/handler.ts +639 -1027
  201. package/src/rsc/helpers.ts +140 -6
  202. package/src/rsc/index.ts +0 -20
  203. package/src/rsc/loader-fetch.ts +209 -0
  204. package/src/rsc/manifest-init.ts +86 -0
  205. package/src/rsc/nonce.ts +14 -0
  206. package/src/rsc/origin-guard.ts +141 -0
  207. package/src/rsc/progressive-enhancement.ts +379 -0
  208. package/src/rsc/response-error.ts +37 -0
  209. package/src/rsc/response-route-handler.ts +347 -0
  210. package/src/rsc/rsc-rendering.ts +237 -0
  211. package/src/rsc/runtime-warnings.ts +42 -0
  212. package/src/rsc/server-action.ts +348 -0
  213. package/src/rsc/ssr-setup.ts +128 -0
  214. package/src/rsc/types.ts +38 -11
  215. package/src/search-params.ts +66 -54
  216. package/src/segment-system.tsx +165 -17
  217. package/src/server/context.ts +237 -54
  218. package/src/server/cookie-store.ts +190 -0
  219. package/src/server/fetchable-loader-store.ts +11 -6
  220. package/src/server/handle-store.ts +94 -15
  221. package/src/server/loader-registry.ts +15 -56
  222. package/src/server/request-context.ts +438 -71
  223. package/src/server.ts +26 -164
  224. package/src/ssr/index.tsx +101 -31
  225. package/src/static-handler.ts +22 -4
  226. package/src/theme/ThemeProvider.tsx +21 -15
  227. package/src/theme/ThemeScript.tsx +5 -5
  228. package/src/theme/constants.ts +5 -2
  229. package/src/theme/index.ts +4 -14
  230. package/src/theme/theme-context.ts +4 -30
  231. package/src/theme/theme-script.ts +21 -18
  232. package/src/types/boundaries.ts +158 -0
  233. package/src/types/cache-types.ts +198 -0
  234. package/src/types/error-types.ts +192 -0
  235. package/src/types/global-namespace.ts +100 -0
  236. package/src/types/handler-context.ts +773 -0
  237. package/src/types/index.ts +88 -0
  238. package/src/types/loader-types.ts +183 -0
  239. package/src/types/route-config.ts +170 -0
  240. package/src/types/route-entry.ts +109 -0
  241. package/src/types/segments.ts +150 -0
  242. package/src/types.ts +1 -1795
  243. package/src/urls/include-helper.ts +197 -0
  244. package/src/urls/index.ts +53 -0
  245. package/src/urls/path-helper-types.ts +339 -0
  246. package/src/urls/path-helper.ts +329 -0
  247. package/src/urls/pattern-types.ts +95 -0
  248. package/src/urls/response-types.ts +106 -0
  249. package/src/urls/type-extraction.ts +372 -0
  250. package/src/urls/urls-function.ts +98 -0
  251. package/src/urls.ts +1 -1323
  252. package/src/use-loader.tsx +85 -77
  253. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  254. package/src/vite/discovery/discover-routers.ts +344 -0
  255. package/src/vite/discovery/prerender-collection.ts +385 -0
  256. package/src/vite/discovery/route-types-writer.ts +258 -0
  257. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  258. package/src/vite/discovery/state.ts +108 -0
  259. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  260. package/src/vite/index.ts +11 -2259
  261. package/src/vite/plugin-types.ts +48 -0
  262. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  263. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  264. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  265. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -47
  266. package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +8 -43
  267. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  268. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  269. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  270. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  271. package/src/vite/plugins/expose-ids/types.ts +45 -0
  272. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  273. package/src/vite/plugins/refresh-cmd.ts +65 -0
  274. package/src/vite/plugins/use-cache-transform.ts +323 -0
  275. package/src/vite/plugins/version-injector.ts +83 -0
  276. package/src/vite/plugins/version-plugin.ts +266 -0
  277. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  278. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  279. package/src/vite/rango.ts +445 -0
  280. package/src/vite/router-discovery.ts +777 -0
  281. package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +181 -9
  282. package/src/vite/utils/banner.ts +36 -0
  283. package/src/vite/utils/bundle-analysis.ts +137 -0
  284. package/src/vite/utils/manifest-utils.ts +70 -0
  285. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  286. package/src/vite/utils/prerender-utils.ts +189 -0
  287. package/src/vite/utils/shared-utils.ts +169 -0
  288. package/CLAUDE.md +0 -43
  289. package/dist/vite/index.named-routes.gen.ts +0 -103
  290. package/src/browser/lru-cache.ts +0 -69
  291. package/src/browser/request-controller.ts +0 -164
  292. package/src/cache/memory-store.ts +0 -253
  293. package/src/href-context.ts +0 -33
  294. package/src/router.gen.ts +0 -6
  295. package/src/static-handler.gen.ts +0 -5
  296. package/src/urls.gen.ts +0 -8
  297. package/src/vite/expose-internal-ids.ts +0 -1167
  298. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -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
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Middleware Types
3
+ *
4
+ * Type definitions and interfaces for the middleware system.
5
+ * Separated from execution logic for cleaner imports.
6
+ */
7
+
8
+ import type { ContextVar } from "../context-var.js";
9
+ import type {
10
+ DefaultReverseRouteMap,
11
+ DefaultRouteName,
12
+ DefaultVars,
13
+ } from "../types/global-namespace.js";
14
+ import type { ScopedReverseFunction } from "../reverse.js";
15
+ import type { Theme } from "../theme/types.js";
16
+ import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
17
+
18
+ /**
19
+ * Get variable function type
20
+ */
21
+ type GetVariableFn = {
22
+ <T>(contextVar: ContextVar<T>): T | undefined;
23
+ <K extends keyof DefaultVars>(key: K): DefaultVars[K];
24
+ };
25
+
26
+ /**
27
+ * Set variable function type
28
+ */
29
+ type SetVariableFn = {
30
+ <T>(contextVar: ContextVar<T>, value: T): void;
31
+ <K extends keyof DefaultVars>(key: K, value: DefaultVars[K]): void;
32
+ };
33
+
34
+ /**
35
+ * Cookie options for setting cookies
36
+ */
37
+ export interface CookieOptions {
38
+ domain?: string;
39
+ path?: string;
40
+ maxAge?: number;
41
+ expires?: Date;
42
+ httpOnly?: boolean;
43
+ secure?: boolean;
44
+ sameSite?: "strict" | "lax" | "none";
45
+ }
46
+
47
+ /**
48
+ * Context passed to middleware
49
+ *
50
+ * @template TEnv - Environment type (bindings, variables) - defaults to any for internal flexibility
51
+ * @template TParams - URL params type (typed for route middleware, Record<string, string> for global middleware)
52
+ */
53
+ export interface MiddlewareContext<
54
+ TEnv = any,
55
+ TParams = Record<string, string>,
56
+ > {
57
+ /** Original request */
58
+ request: Request;
59
+
60
+ /** Parsed URL (with internal `_rsc*` params stripped) */
61
+ url: URL;
62
+
63
+ /**
64
+ * The original request URL with all parameters intact, including
65
+ * internal `_rsc*` transport params.
66
+ */
67
+ originalUrl: URL;
68
+
69
+ /** URL pathname */
70
+ pathname: string;
71
+
72
+ /** URL search params */
73
+ searchParams: URLSearchParams;
74
+
75
+ /** Platform bindings (Cloudflare, etc.) */
76
+ env: TEnv;
77
+
78
+ /** URL params extracted from route/middleware pattern */
79
+ params: TParams;
80
+
81
+ /**
82
+ * Response headers.
83
+ * Before `next()`, returns headers from the shared response stub.
84
+ * After `next()`, returns headers from the downstream response.
85
+ */
86
+ readonly headers: Headers;
87
+
88
+ /** Get a context variable (shared with route handlers) */
89
+ get: GetVariableFn;
90
+
91
+ /** Set a context variable (shared with route handlers) */
92
+ set: SetVariableFn;
93
+
94
+ /**
95
+ * Middleware-injected variables.
96
+ * Same shared dictionary as `ctx.get()`/`ctx.set()`.
97
+ */
98
+ var: DefaultVars;
99
+
100
+ /**
101
+ * Set a response header - can be called before or after `next()`.
102
+ *
103
+ * When called before `next()`, headers are queued and merged into the final response.
104
+ * When called after `next()`, headers are set directly on the response.
105
+ */
106
+ header(name: string, value: string): void;
107
+
108
+ /**
109
+ * The matched route name, if available and the route has an explicit name.
110
+ * Undefined for global middleware (runs before route matching) or unnamed routes.
111
+ */
112
+ routeName?: DefaultRouteName;
113
+
114
+ /**
115
+ * Enable performance metrics for this request.
116
+ * When called, granular timing breakdown is logged to console and
117
+ * included in the Server-Timing response header, regardless of the
118
+ * router-level `debugPerformance` option.
119
+ *
120
+ * Call **before** `await next()` so the metrics store exists when
121
+ * downstream phases (route matching, rendering, SSR) record their
122
+ * spans. Calling after `next()` returns still emits `handler:total`
123
+ * but misses all upstream metrics.
124
+ */
125
+ debugPerformance(): void;
126
+
127
+ /**
128
+ * Current theme (from cookie or default).
129
+ * Only available when theme is enabled in router config.
130
+ */
131
+ theme?: Theme;
132
+
133
+ /**
134
+ * Set the theme (only available when theme is enabled in router config).
135
+ * Sets a cookie with the new theme value.
136
+ */
137
+ setTheme?: (theme: Theme) => void;
138
+
139
+ /**
140
+ * Attach location state entries to this response.
141
+ * State is delivered to the client via history.pushState and accessible
142
+ * through the useLocationState() hook.
143
+ */
144
+ setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void;
145
+
146
+ /**
147
+ * Generate URLs from route names.
148
+ * - `name` — global route, from the named-routes definition
149
+ */
150
+ reverse: ScopedReverseFunction<
151
+ Record<string, string>,
152
+ DefaultReverseRouteMap
153
+ >;
154
+ }
155
+
156
+ /**
157
+ * Middleware function signature
158
+ *
159
+ * @template TEnv - Environment type - defaults to any for internal flexibility
160
+ * @template TParams - URL params type (typed for route middleware)
161
+ *
162
+ * When using middleware with global augmentation (RSCRouter.Env), explicitly
163
+ * annotate your middleware functions, or the types will be inferred from context:
164
+ *
165
+ * @example
166
+ * ```typescript
167
+ * // With explicit annotation (recommended for reusable middleware)
168
+ * const authMiddleware: MiddlewareFn<AppEnv> = async (ctx, next) => {...}
169
+ *
170
+ * // Types inferred from router.use() call
171
+ * router.use((ctx, next) => {...}) // ctx is typed from router's TEnv
172
+ * ```
173
+ */
174
+ export type MiddlewareFn<TEnv = any, TParams = Record<string, string>> = (
175
+ ctx: MiddlewareContext<TEnv, TParams>,
176
+ next: () => Promise<Response>,
177
+ ) => Response | void | Promise<Response | void>;
178
+
179
+ /**
180
+ * Stored middleware entry with pattern matching info
181
+ * @internal - uses any for internal flexibility
182
+ */
183
+ export interface MiddlewareEntry<TEnv = any> {
184
+ /** Original pattern string */
185
+ pattern: string | null;
186
+
187
+ /** Compiled regex for matching */
188
+ regex: RegExp | null;
189
+
190
+ /** Param names extracted from pattern */
191
+ paramNames: string[];
192
+
193
+ /** The middleware function */
194
+ handler: MiddlewareFn<TEnv>;
195
+
196
+ /** Mount prefix this middleware is scoped to (null = global) */
197
+ mountPrefix: string | null;
198
+ }
199
+
200
+ /**
201
+ * Mutable response holder - tracks the current response through the middleware chain.
202
+ */
203
+ export interface ResponseHolder {
204
+ response: Response | null;
205
+ }
206
+
207
+ /**
208
+ * Entry type for middleware collection
209
+ * Matches the shape of EntryData used in router.ts
210
+ */
211
+ export interface MiddlewareCollectableEntry {
212
+ middleware?: MiddlewareFn<any, any>[];
213
+ layout?: MiddlewareCollectableEntry[];
214
+ }
215
+
216
+ /**
217
+ * Collected route middleware with params
218
+ */
219
+ export interface CollectedMiddleware {
220
+ handler: MiddlewareFn<any, any>;
221
+ params: Record<string, string>;
222
+ }