@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1b930379

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 (84) hide show
  1. package/README.md +46 -12
  2. package/dist/bin/rango.js +109 -15
  3. package/dist/vite/index.js +323 -121
  4. package/package.json +15 -16
  5. package/skills/breadcrumbs/SKILL.md +250 -0
  6. package/skills/caching/SKILL.md +4 -4
  7. package/skills/document-cache/SKILL.md +2 -2
  8. package/skills/hooks/SKILL.md +33 -31
  9. package/skills/host-router/SKILL.md +218 -0
  10. package/skills/loader/SKILL.md +55 -15
  11. package/skills/prerender/SKILL.md +2 -2
  12. package/skills/rango/SKILL.md +0 -1
  13. package/skills/route/SKILL.md +3 -4
  14. package/skills/router-setup/SKILL.md +8 -3
  15. package/skills/typesafety/SKILL.md +25 -23
  16. package/src/__internal.ts +92 -0
  17. package/src/bin/rango.ts +18 -0
  18. package/src/browser/link-interceptor.ts +4 -0
  19. package/src/browser/navigation-bridge.ts +95 -5
  20. package/src/browser/navigation-client.ts +97 -72
  21. package/src/browser/prefetch/cache.ts +112 -25
  22. package/src/browser/prefetch/fetch.ts +28 -30
  23. package/src/browser/prefetch/policy.ts +6 -0
  24. package/src/browser/react/Link.tsx +19 -7
  25. package/src/browser/rsc-router.tsx +11 -2
  26. package/src/browser/server-action-bridge.ts +448 -432
  27. package/src/browser/types.ts +24 -0
  28. package/src/build/generate-route-types.ts +2 -0
  29. package/src/build/route-trie.ts +19 -3
  30. package/src/build/route-types/router-processing.ts +125 -15
  31. package/src/client.rsc.tsx +2 -1
  32. package/src/client.tsx +1 -46
  33. package/src/handles/breadcrumbs.ts +66 -0
  34. package/src/handles/index.ts +1 -0
  35. package/src/host/index.ts +0 -3
  36. package/src/index.rsc.ts +5 -36
  37. package/src/index.ts +32 -66
  38. package/src/prerender/store.ts +56 -15
  39. package/src/route-definition/index.ts +0 -3
  40. package/src/router/handler-context.ts +30 -3
  41. package/src/router/loader-resolution.ts +1 -1
  42. package/src/router/match-api.ts +1 -1
  43. package/src/router/match-result.ts +0 -9
  44. package/src/router/metrics.ts +233 -13
  45. package/src/router/middleware-types.ts +53 -10
  46. package/src/router/middleware.ts +170 -81
  47. package/src/router/pattern-matching.ts +20 -5
  48. package/src/router/prerender-match.ts +4 -0
  49. package/src/router/revalidation.ts +27 -7
  50. package/src/router/router-interfaces.ts +14 -1
  51. package/src/router/router-options.ts +13 -8
  52. package/src/router/segment-resolution/fresh.ts +18 -0
  53. package/src/router/segment-resolution/helpers.ts +1 -1
  54. package/src/router/segment-resolution/revalidation.ts +22 -9
  55. package/src/router/trie-matching.ts +20 -2
  56. package/src/router.ts +29 -9
  57. package/src/rsc/handler.ts +106 -11
  58. package/src/rsc/index.ts +0 -20
  59. package/src/rsc/progressive-enhancement.ts +21 -8
  60. package/src/rsc/rsc-rendering.ts +30 -43
  61. package/src/rsc/server-action.ts +14 -10
  62. package/src/rsc/ssr-setup.ts +128 -0
  63. package/src/rsc/types.ts +2 -0
  64. package/src/search-params.ts +16 -13
  65. package/src/server/context.ts +8 -2
  66. package/src/server/request-context.ts +38 -16
  67. package/src/server.ts +6 -0
  68. package/src/theme/index.ts +4 -13
  69. package/src/types/handler-context.ts +12 -16
  70. package/src/types/route-config.ts +17 -8
  71. package/src/types/segments.ts +0 -5
  72. package/src/vite/discovery/bundle-postprocess.ts +31 -56
  73. package/src/vite/discovery/discover-routers.ts +18 -4
  74. package/src/vite/discovery/prerender-collection.ts +34 -14
  75. package/src/vite/discovery/state.ts +4 -7
  76. package/src/vite/index.ts +4 -3
  77. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  78. package/src/vite/plugins/refresh-cmd.ts +65 -0
  79. package/src/vite/rango.ts +11 -0
  80. package/src/vite/router-discovery.ts +16 -0
  81. package/src/vite/utils/prerender-utils.ts +60 -0
  82. package/skills/testing/SKILL.md +0 -226
  83. package/src/route-definition/route-function.ts +0 -119
  84. /package/{CLAUDE.md → AGENTS.md} +0 -0
@@ -4,24 +4,212 @@
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) => a.startTime - b.startTime);
19
+ }
20
+
21
+ interface Span {
22
+ startTime: number;
23
+ duration: number;
24
+ }
25
+
26
+ function renderTimeline(spans: Span[], total: number): string {
27
+ if (TIMELINE_WIDTH <= 0) {
28
+ return "||";
29
+ }
30
+
31
+ const cells = Array(TIMELINE_WIDTH).fill(".");
32
+
33
+ if (!(total > 0)) {
34
+ cells[0] = "#";
35
+ return `|${cells.join("")}|`;
36
+ }
37
+
38
+ for (const span of spans) {
39
+ const start = Math.max(0, span.startTime);
40
+ const end = Math.max(start, span.startTime + span.duration);
41
+ const startColumn = Math.min(
42
+ TIMELINE_WIDTH - 1,
43
+ Math.floor((start / total) * TIMELINE_WIDTH),
44
+ );
45
+ const endColumn = Math.max(
46
+ startColumn + 1,
47
+ Math.min(
48
+ TIMELINE_WIDTH,
49
+ Math.ceil((Math.min(total, end) / total) * TIMELINE_WIDTH),
50
+ ),
51
+ );
52
+
53
+ cells.fill("#", startColumn, endColumn);
54
+ }
55
+
56
+ return `|${cells.join("")}|`;
57
+ }
58
+
59
+ function createTimelineAxis(total: number): string {
60
+ const totalLabel = formatMs(total);
61
+ return `0ms${" ".repeat(
62
+ Math.max(1, TIMELINE_WIDTH - "0ms".length - totalLabel.length),
63
+ )}${totalLabel}`;
64
+ }
8
65
 
9
66
  /**
10
- * Create a metrics store for the request if debugPerformance is enabled
67
+ * Create a metrics store for the request if debugPerformance is enabled.
68
+ * An optional `requestStart` timestamp can anchor the store to an earlier
69
+ * point (e.g. handler start) so that handler:total has startTime=0.
11
70
  */
12
71
  export function createMetricsStore(
13
72
  debugPerformance: boolean,
73
+ requestStart?: number,
14
74
  ): MetricsStore | undefined {
15
75
  if (!debugPerformance) return undefined;
16
76
  return {
17
77
  enabled: true,
18
- requestStart: performance.now(),
78
+ requestStart: requestStart ?? performance.now(),
19
79
  metrics: [],
20
80
  };
21
81
  }
22
82
 
23
83
  /**
24
- * Log metrics to console in a formatted way
84
+ * Append a metric to the request store using an absolute start timestamp.
85
+ */
86
+ export function appendMetric(
87
+ metricsStore: MetricsStore | undefined,
88
+ label: string,
89
+ start: number,
90
+ duration: number,
91
+ depth?: number,
92
+ ): void {
93
+ if (!metricsStore) return;
94
+ metricsStore.metrics.push({
95
+ label,
96
+ duration,
97
+ startTime: start - metricsStore.requestStart,
98
+ depth,
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Log the current request metrics and return the corresponding Server-Timing value.
104
+ */
105
+ export function buildMetricsTiming(
106
+ method: string,
107
+ pathname: string,
108
+ metricsStore: MetricsStore | undefined,
109
+ ): string | undefined {
110
+ if (!metricsStore) return undefined;
111
+ logMetrics(method, pathname, metricsStore);
112
+ return generateServerTiming(metricsStore) || undefined;
113
+ }
114
+
115
+ /** Display row produced by merging :pre/:post metric pairs. */
116
+ interface DisplayRow {
117
+ label: string;
118
+ startTime: number;
119
+ duration: number;
120
+ depth: number | undefined;
121
+ spans: Span[];
122
+ }
123
+
124
+ /**
125
+ * Build display rows from sorted metrics, merging :pre/:post pairs into
126
+ * a single row with disjoint timeline segments.
127
+ */
128
+ function buildDisplayRows(sorted: PerformanceMetric[]): DisplayRow[] {
129
+ // Index :pre and :post metrics by their base label
130
+ const preMap = new Map<string, PerformanceMetric>();
131
+ const postMap = new Map<string, PerformanceMetric>();
132
+ const consumed = new Set<PerformanceMetric>();
133
+
134
+ for (const m of sorted) {
135
+ if (m.label.endsWith(":pre")) {
136
+ preMap.set(m.label.slice(0, -4), m);
137
+ } else if (m.label.endsWith(":post")) {
138
+ postMap.set(m.label.slice(0, -5), m);
139
+ }
140
+ }
141
+
142
+ const rows: DisplayRow[] = [];
143
+
144
+ for (const m of sorted) {
145
+ if (consumed.has(m)) continue;
146
+
147
+ if (m.label.endsWith(":pre")) {
148
+ const base = m.label.slice(0, -4);
149
+ const post = postMap.get(base);
150
+ if (post) {
151
+ // Merge into a single row with two disjoint spans
152
+ consumed.add(m);
153
+ consumed.add(post);
154
+ rows.push({
155
+ label: base,
156
+ startTime: m.startTime,
157
+ duration: m.duration + post.duration,
158
+ depth: m.depth,
159
+ spans: [
160
+ { startTime: m.startTime, duration: m.duration },
161
+ { startTime: post.startTime, duration: post.duration },
162
+ ],
163
+ });
164
+ continue;
165
+ }
166
+ // Lone :pre — display with base label
167
+ consumed.add(m);
168
+ rows.push({
169
+ label: base,
170
+ startTime: m.startTime,
171
+ duration: m.duration,
172
+ depth: m.depth,
173
+ spans: [{ startTime: m.startTime, duration: m.duration }],
174
+ });
175
+ continue;
176
+ }
177
+
178
+ if (m.label.endsWith(":post")) {
179
+ const base = m.label.slice(0, -5);
180
+ if (preMap.has(base)) {
181
+ // Already consumed as part of the pair above
182
+ continue;
183
+ }
184
+ // Lone :post — display with base label
185
+ consumed.add(m);
186
+ rows.push({
187
+ label: base,
188
+ startTime: m.startTime,
189
+ duration: m.duration,
190
+ depth: m.depth,
191
+ spans: [{ startTime: m.startTime, duration: m.duration }],
192
+ });
193
+ continue;
194
+ }
195
+
196
+ // Regular metric
197
+ rows.push({
198
+ label: m.label,
199
+ startTime: m.startTime,
200
+ duration: m.duration,
201
+ depth: m.depth,
202
+ spans: [{ startTime: m.startTime, duration: m.duration }],
203
+ });
204
+ }
205
+
206
+ return rows;
207
+ }
208
+
209
+ /**
210
+ * Log metrics to console in a formatted way.
211
+ * Uses a shared-axis timeline so overlapping work stays visible.
212
+ * Merges :pre/:post pairs onto one row with disjoint timeline segments.
25
213
  */
26
214
  export function logMetrics(
27
215
  method: string,
@@ -30,32 +218,64 @@ export function logMetrics(
30
218
  ): void {
31
219
  const total = performance.now() - metricsStore.requestStart;
32
220
 
33
- // Find max label length for alignment
34
- const maxLabelLen = Math.max(
35
- ...metricsStore.metrics.map((m) => m.label.length),
36
- 20,
221
+ const sorted = sortMetrics(metricsStore.metrics);
222
+ const displayRows = buildDisplayRows(sorted);
223
+
224
+ const labels = displayRows.map(
225
+ (r) =>
226
+ `${" ".repeat(BASE_INDENT + (r.depth ?? 0) * DEPTH_INDENT)}${r.label}`,
227
+ );
228
+ const startValues = displayRows.map((r) => formatMs(r.startTime));
229
+ const durationValues = displayRows.map((r) => formatMs(r.duration));
230
+ const startWidth = Math.max(
231
+ "start".length,
232
+ ...startValues.map((v) => v.length),
233
+ );
234
+ const durationWidth = Math.max(
235
+ "dur".length,
236
+ ...durationValues.map((v) => v.length),
237
+ );
238
+ const spanWidth = Math.max(
239
+ "span".length,
240
+ ...labels.map((label) => label.length),
241
+ 22,
242
+ );
243
+ const timelinePadding = " ".repeat(
244
+ startWidth + 2 + durationWidth + 2 + spanWidth + 2,
245
+ );
246
+
247
+ console.log(`[RSC Perf] ${method} ${pathname} (${total.toFixed(2)}ms)`);
248
+ console.log(
249
+ `${"start".padStart(startWidth)} ${"dur".padStart(durationWidth)} ${"span".padEnd(spanWidth)} timeline`,
37
250
  );
251
+ console.log(`${timelinePadding}${createTimelineAxis(total)}`);
38
252
 
39
- console.log(`[RSC Perf] ${method} ${pathname} (${total.toFixed(1)}ms)`);
253
+ for (let index = 0; index < displayRows.length; index++) {
254
+ const row = displayRows[index];
255
+ const label = labels[index].padEnd(spanWidth);
256
+ const start = formatMs(row.startTime).padStart(startWidth);
257
+ const duration = formatMs(row.duration).padStart(durationWidth);
40
258
 
41
- for (const m of metricsStore.metrics) {
42
- const paddedLabel = m.label.padEnd(maxLabelLen);
43
- console.log(` ${paddedLabel} ${m.duration.toFixed(1)}ms`);
259
+ console.log(
260
+ `${start} ${duration} ${label} ${renderTimeline(row.spans, total)}`,
261
+ );
44
262
  }
45
263
  }
46
264
 
47
265
  /**
48
266
  * Generate Server-Timing header value from metrics
49
267
  * Format: metric-name;dur=X.XX
268
+ * Depth is encoded as a "d{N}-" prefix for nested metrics.
50
269
  */
51
270
  export function generateServerTiming(metricsStore: MetricsStore): string {
52
271
  return metricsStore.metrics
53
272
  .map((m) => {
54
273
  // Convert label to valid Server-Timing name (alphanumeric and hyphens)
55
- const name = m.label
274
+ const base = m.label
56
275
  .replace(/:/g, "-")
57
276
  .replace(/[^a-zA-Z0-9-]/g, "")
58
277
  .toLowerCase();
278
+ const name = m.depth ? `d${m.depth}-${base}` : base;
59
279
  return `${name};dur=${m.duration.toFixed(2)}`;
60
280
  })
61
281
  .join(", ");
@@ -12,6 +12,8 @@ import type {
12
12
  DefaultVars,
13
13
  } from "../types/global-namespace.js";
14
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";
15
17
 
16
18
  /**
17
19
  * Get variable function type
@@ -55,9 +57,15 @@ export interface MiddlewareContext<
55
57
  /** Original request */
56
58
  request: Request;
57
59
 
58
- /** Parsed URL */
60
+ /** Parsed URL (with internal `_rsc*` params stripped) */
59
61
  url: URL;
60
62
 
63
+ /**
64
+ * The original request URL with all parameters intact, including
65
+ * internal `_rsc*` transport params.
66
+ */
67
+ originalUrl: URL;
68
+
61
69
  /** URL pathname */
62
70
  pathname: string;
63
71
 
@@ -71,13 +79,11 @@ export interface MiddlewareContext<
71
79
  params: TParams;
72
80
 
73
81
  /**
74
- * Response stub (read-only). Before `next()`, returns the shared response stub
75
- * where headers and cookies accumulate. After `next()`, returns the downstream response.
76
- *
77
- * Use `ctx.header()` to set response headers, or `cookies()` for cookie mutations.
78
- * To replace the response entirely, return a new `Response` from the middleware.
82
+ * Response headers.
83
+ * Before `next()`, returns headers from the shared response stub.
84
+ * After `next()`, returns headers from the downstream response.
79
85
  */
80
- readonly res: Response;
86
+ readonly headers: Headers;
81
87
 
82
88
  /** Get a context variable (shared with route handlers) */
83
89
  get: GetVariableFn;
@@ -86,11 +92,16 @@ export interface MiddlewareContext<
86
92
  set: SetVariableFn;
87
93
 
88
94
  /**
89
- * Set a response header - can be called before or after `next()`
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()`.
90
102
  *
91
103
  * When called before `next()`, headers are queued and merged into the final response.
92
104
  * When called after `next()`, headers are set directly on the response.
93
- * Shorthand for `ctx.res.headers.set()`.
94
105
  */
95
106
  header(name: string, value: string): void;
96
107
 
@@ -100,6 +111,38 @@ export interface MiddlewareContext<
100
111
  */
101
112
  routeName?: DefaultRouteName;
102
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
+
103
146
  /**
104
147
  * Generate URLs from route names.
105
148
  * - `name` — global route, from the named-routes definition
@@ -155,7 +198,7 @@ export interface MiddlewareEntry<TEnv = any> {
155
198
  }
156
199
 
157
200
  /**
158
- * Mutable response holder - allows ctx.res to be updated after next() is called
201
+ * Mutable response holder - tracks the current response through the middleware chain.
159
202
  */
160
203
  export interface ResponseHolder {
161
204
  response: Response | null;