@rangojs/router 0.0.0-experimental.39 → 0.0.0-experimental.3b1deca8

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 (89) hide show
  1. package/dist/bin/rango.js +8 -3
  2. package/dist/vite/index.js +292 -204
  3. package/package.json +1 -1
  4. package/skills/cache-guide/SKILL.md +32 -0
  5. package/skills/caching/SKILL.md +45 -4
  6. package/skills/loader/SKILL.md +53 -43
  7. package/skills/parallel/SKILL.md +126 -0
  8. package/skills/route/SKILL.md +31 -0
  9. package/skills/router-setup/SKILL.md +52 -2
  10. package/skills/typesafety/SKILL.md +10 -0
  11. package/src/browser/debug-channel.ts +93 -0
  12. package/src/browser/event-controller.ts +5 -0
  13. package/src/browser/navigation-bridge.ts +1 -5
  14. package/src/browser/navigation-client.ts +84 -27
  15. package/src/browser/navigation-transaction.ts +11 -9
  16. package/src/browser/partial-update.ts +50 -9
  17. package/src/browser/prefetch/cache.ts +57 -5
  18. package/src/browser/prefetch/fetch.ts +30 -21
  19. package/src/browser/prefetch/queue.ts +92 -20
  20. package/src/browser/prefetch/resource-ready.ts +77 -0
  21. package/src/browser/react/Link.tsx +9 -1
  22. package/src/browser/react/NavigationProvider.tsx +32 -3
  23. package/src/browser/rsc-router.tsx +109 -57
  24. package/src/browser/scroll-restoration.ts +31 -34
  25. package/src/browser/segment-reconciler.ts +6 -1
  26. package/src/browser/server-action-bridge.ts +12 -0
  27. package/src/browser/types.ts +17 -1
  28. package/src/build/route-types/router-processing.ts +12 -2
  29. package/src/cache/cache-runtime.ts +15 -11
  30. package/src/cache/cache-scope.ts +48 -7
  31. package/src/cache/cf/cf-cache-store.ts +453 -11
  32. package/src/cache/cf/index.ts +5 -1
  33. package/src/cache/document-cache.ts +17 -7
  34. package/src/cache/index.ts +1 -0
  35. package/src/cache/taint.ts +55 -0
  36. package/src/context-var.ts +72 -2
  37. package/src/debug.ts +2 -2
  38. package/src/deps/browser.ts +1 -0
  39. package/src/route-definition/dsl-helpers.ts +32 -7
  40. package/src/route-definition/helpers-types.ts +6 -5
  41. package/src/route-definition/redirect.ts +2 -2
  42. package/src/route-map-builder.ts +7 -1
  43. package/src/router/find-match.ts +4 -2
  44. package/src/router/handler-context.ts +31 -8
  45. package/src/router/intercept-resolution.ts +2 -0
  46. package/src/router/lazy-includes.ts +4 -1
  47. package/src/router/loader-resolution.ts +7 -1
  48. package/src/router/logging.ts +5 -2
  49. package/src/router/manifest.ts +9 -3
  50. package/src/router/match-middleware/background-revalidation.ts +30 -2
  51. package/src/router/match-middleware/cache-lookup.ts +66 -9
  52. package/src/router/match-middleware/cache-store.ts +53 -10
  53. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  54. package/src/router/match-middleware/segment-resolution.ts +8 -5
  55. package/src/router/match-result.ts +22 -6
  56. package/src/router/metrics.ts +6 -1
  57. package/src/router/middleware-types.ts +6 -2
  58. package/src/router/middleware.ts +4 -3
  59. package/src/router/router-context.ts +6 -1
  60. package/src/router/segment-resolution/fresh.ts +130 -17
  61. package/src/router/segment-resolution/helpers.ts +29 -24
  62. package/src/router/segment-resolution/loader-cache.ts +1 -0
  63. package/src/router/segment-resolution/revalidation.ts +352 -290
  64. package/src/router/segment-wrappers.ts +2 -0
  65. package/src/router/types.ts +1 -0
  66. package/src/router.ts +6 -1
  67. package/src/rsc/handler.ts +28 -2
  68. package/src/rsc/loader-fetch.ts +7 -2
  69. package/src/rsc/progressive-enhancement.ts +4 -1
  70. package/src/rsc/rsc-rendering.ts +4 -1
  71. package/src/rsc/server-action.ts +2 -0
  72. package/src/rsc/types.ts +7 -1
  73. package/src/segment-system.tsx +140 -4
  74. package/src/server/context.ts +102 -13
  75. package/src/server/request-context.ts +59 -12
  76. package/src/ssr/index.tsx +1 -0
  77. package/src/types/handler-context.ts +120 -22
  78. package/src/types/loader-types.ts +4 -4
  79. package/src/types/route-entry.ts +7 -0
  80. package/src/types/segments.ts +2 -0
  81. package/src/urls/path-helper.ts +1 -1
  82. package/src/vite/discovery/state.ts +0 -2
  83. package/src/vite/plugin-types.ts +0 -83
  84. package/src/vite/plugins/expose-action-id.ts +1 -3
  85. package/src/vite/plugins/performance-tracks.ts +235 -0
  86. package/src/vite/plugins/version-plugin.ts +13 -1
  87. package/src/vite/rango.ts +148 -209
  88. package/src/vite/router-discovery.ts +0 -8
  89. package/src/vite/utils/banner.ts +3 -3
@@ -7,7 +7,11 @@
7
7
 
8
8
  import type { ReactNode } from "react";
9
9
  import { invariant } from "../../errors";
10
- import type { EntryData } from "../../server/context";
10
+ import {
11
+ getParallelEntries,
12
+ getParallelSlotEntries,
13
+ type EntryData,
14
+ } from "../../server/context";
11
15
  import type {
12
16
  HandlerContext,
13
17
  InternalHandlerContext,
@@ -15,6 +19,8 @@ import type {
15
19
  } from "../../types";
16
20
  import type { SegmentResolutionDeps } from "../types.js";
17
21
  import { resolveLoaderData } from "./loader-cache.js";
22
+ import { _getRequestContext } from "../../server/request-context.js";
23
+ import { appendMetric } from "../metrics.js";
18
24
  import {
19
25
  handleHandlerResult,
20
26
  tryStaticHandler,
@@ -24,7 +30,7 @@ import {
24
30
  } from "./helpers.js";
25
31
  import { getRouterContext } from "../router-context.js";
26
32
  import { resolveSink, safeEmit } from "../telemetry.js";
27
- import { track } from "../../server/context.js";
33
+ import { track, RSCRouterContext } from "../../server/context.js";
28
34
 
29
35
  // ---------------------------------------------------------------------------
30
36
  // Streamed handler telemetry
@@ -90,9 +96,11 @@ export async function resolveLoaders<TEnv>(
90
96
  const shortCode = shortCodeOverride ?? entry.shortCode;
91
97
  const hasLoading = "loading" in entry && entry.loading !== undefined;
92
98
  const loadingDisabled = hasLoading && entry.loading === false;
99
+ const ms = _getRequestContext()?._metricsStore;
93
100
 
94
101
  if (!loadingDisabled) {
95
- return loaderEntries.map((loaderEntry, i) => {
102
+ // Streaming loaders: promises kick off now, settle during RSC serialization.
103
+ const segments = loaderEntries.map((loaderEntry, i) => {
96
104
  const { loader } = loaderEntry;
97
105
  const segmentId = `${shortCode}D${i}.${loader.$$id}`;
98
106
  return {
@@ -112,18 +120,36 @@ export async function resolveLoaders<TEnv>(
112
120
  belongsToRoute,
113
121
  };
114
122
  });
123
+
124
+ return segments;
115
125
  }
116
126
 
117
127
  // Loading disabled: still start all loaders in parallel, but only emit
118
128
  // settled promises so handlers don't stream loading placeholders.
119
- const pendingLoaderData = loaderEntries.map((loaderEntry) =>
120
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
121
- );
122
- await Promise.all(pendingLoaderData);
129
+ const pendingLoaderData = loaderEntries.map((loaderEntry) => {
130
+ const start = performance.now();
131
+ const promise = resolveLoaderData(loaderEntry, ctx, ctx.pathname);
132
+ return { promise, start, loaderId: loaderEntry.loader.$$id };
133
+ });
134
+ await Promise.all(pendingLoaderData.map((p) => p.promise));
123
135
 
124
136
  return loaderEntries.map((loaderEntry, i) => {
125
137
  const { loader } = loaderEntry;
126
138
  const segmentId = `${shortCode}D${i}.${loader.$$id}`;
139
+ const pending = pendingLoaderData[i]!;
140
+ if (ms && !ms.metrics.some((m) => m.label === `loader:${loader.$$id}`)) {
141
+ // All loaders ran in parallel via Promise.all — each span covers
142
+ // from its own kickoff to the batch settlement, giving a ceiling
143
+ // on that loader's contribution to the overall wait.
144
+ const batchEnd = performance.now();
145
+ appendMetric(
146
+ ms,
147
+ `loader:${loader.$$id}`,
148
+ pending.start,
149
+ batchEnd - pending.start,
150
+ 2,
151
+ );
152
+ }
127
153
  return {
128
154
  id: segmentId,
129
155
  namespace: entry.id,
@@ -133,7 +159,7 @@ export async function resolveLoaders<TEnv>(
133
159
  params: ctx.params,
134
160
  loaderId: loader.$$id,
135
161
  loaderData: deps.wrapLoaderPromise(
136
- pendingLoaderData[i]!,
162
+ pending.promise,
137
163
  entry,
138
164
  segmentId,
139
165
  ctx.pathname,
@@ -197,7 +223,10 @@ export async function resolveSegment<TEnv>(
197
223
  ...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
198
224
  });
199
225
 
200
- for (const parallelEntry of entry.parallel) {
226
+ const resolvedParallelEntries = new Set<string>();
227
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
228
+ entry.parallel,
229
+ )) {
201
230
  const parallelSegments = await resolveParallelEntry(
202
231
  parallelEntry,
203
232
  params,
@@ -207,8 +236,11 @@ export async function resolveSegment<TEnv>(
207
236
  deps,
208
237
  options,
209
238
  routeKey,
239
+ [slot],
240
+ !resolvedParallelEntries.has(parallelEntry.id),
210
241
  );
211
242
  segments.push(...parallelSegments);
243
+ resolvedParallelEntries.add(parallelEntry.id);
212
244
  }
213
245
 
214
246
  for (const orphan of entry.layout) {
@@ -286,7 +318,10 @@ export async function resolveSegment<TEnv>(
286
318
  segments.push(...orphanSegments);
287
319
  }
288
320
 
289
- for (const parallelEntry of entry.parallel) {
321
+ const resolvedParallelEntries = new Set<string>();
322
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
323
+ entry.parallel,
324
+ )) {
290
325
  const parallelSegments = await resolveParallelEntry(
291
326
  parallelEntry,
292
327
  params,
@@ -296,8 +331,11 @@ export async function resolveSegment<TEnv>(
296
331
  deps,
297
332
  options,
298
333
  routeKey,
334
+ [slot],
335
+ !resolvedParallelEntries.has(parallelEntry.id),
299
336
  );
300
337
  segments.push(...parallelSegments);
338
+ resolvedParallelEntries.add(parallelEntry.id);
301
339
  }
302
340
 
303
341
  segments.push({
@@ -305,7 +343,7 @@ export async function resolveSegment<TEnv>(
305
343
  namespace: entry.id,
306
344
  type: "route",
307
345
  index: 0,
308
- component,
346
+ component: component ?? null,
309
347
  loading: entry.loading === false ? null : entry.loading,
310
348
  transition: entry.transition,
311
349
  params,
@@ -368,7 +406,10 @@ export async function resolveOrphanLayout<TEnv>(
368
406
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
369
407
  });
370
408
 
371
- for (const parallelEntry of orphan.parallel) {
409
+ const resolvedParallelEntries = new Set<string>();
410
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
411
+ orphan.parallel,
412
+ )) {
372
413
  const parallelSegments = await resolveParallelEntry(
373
414
  parallelEntry,
374
415
  params,
@@ -378,8 +419,11 @@ export async function resolveOrphanLayout<TEnv>(
378
419
  deps,
379
420
  options,
380
421
  routeKey,
422
+ [slot],
423
+ !resolvedParallelEntries.has(parallelEntry.id),
381
424
  );
382
425
  segments.push(...parallelSegments);
426
+ resolvedParallelEntries.add(parallelEntry.id);
383
427
  }
384
428
 
385
429
  return segments;
@@ -397,6 +441,8 @@ export async function resolveParallelEntry<TEnv>(
397
441
  deps: SegmentResolutionDeps<TEnv>,
398
442
  options?: ResolveSegmentOptions,
399
443
  routeKey?: string,
444
+ slotNames?: `@${string}`[],
445
+ includeLoaders: boolean = true,
400
446
  ): Promise<ResolvedSegment[]> {
401
447
  invariant(
402
448
  parallelEntry.type === "parallel",
@@ -411,7 +457,12 @@ export async function resolveParallelEntry<TEnv>(
411
457
  | ReactNode
412
458
  >;
413
459
 
414
- for (const [slot, handler] of Object.entries(slots)) {
460
+ const slotsToResolve = slotNames ?? (Object.keys(slots) as `@${string}`[]);
461
+
462
+ for (const slot of slotsToResolve) {
463
+ // Try static lookup first — in production, handler bodies are evicted
464
+ // and replaced with stubs that have no .handler property (undefined).
465
+ // The static store holds the pre-rendered component for these slots.
415
466
  let component: ReactNode | undefined = await tryStaticSlot(
416
467
  parallelEntry,
417
468
  slot,
@@ -419,6 +470,10 @@ export async function resolveParallelEntry<TEnv>(
419
470
  );
420
471
 
421
472
  if (component === undefined) {
473
+ const handler = slots[slot];
474
+ if (handler === undefined) {
475
+ continue;
476
+ }
422
477
  const doneParallelHandler = track(
423
478
  `handler:${parallelEntry.id}.${slot}`,
424
479
  2,
@@ -472,7 +527,7 @@ export async function resolveParallelEntry<TEnv>(
472
527
  });
473
528
  }
474
529
 
475
- if (!parallelEntry.loading && !options?.skipLoaders) {
530
+ if (!options?.skipLoaders && includeLoaders) {
476
531
  const loaderSegments = await resolveLoaders(
477
532
  parallelEntry,
478
533
  context,
@@ -480,6 +535,15 @@ export async function resolveParallelEntry<TEnv>(
480
535
  deps,
481
536
  parentShortCode,
482
537
  );
538
+ // Tag parallel-owned loaders so renderSegments can stream them
539
+ // using the parallel's loading() instead of awaiting on the layout
540
+ const parallelLoading =
541
+ parallelEntry.loading === false ? undefined : parallelEntry.loading;
542
+ if (parallelLoading) {
543
+ for (const seg of loaderSegments) {
544
+ seg.parallelLoading = parallelLoading;
545
+ }
546
+ }
483
547
  segments.push(...loaderSegments);
484
548
  }
485
549
 
@@ -515,6 +579,13 @@ export async function resolveAllSegments<TEnv>(
515
579
  } catch {}
516
580
 
517
581
  for (const entry of entries) {
582
+ // Set ALS flag when entering a cache() boundary so that ctx.get()
583
+ // can guard non-cacheable variable reads. Also guards response-level
584
+ // side effects (headers.set). Persists for all descendant entries.
585
+ if (entry.type === "cache") {
586
+ const store = RSCRouterContext.getStore();
587
+ if (store) store.insideCacheScope = true;
588
+ }
518
589
  const doneEntry = track(`segment:${entry.id}`, 1);
519
590
  const resolvedSegments = await resolveWithErrorBoundary(
520
591
  entry,
@@ -559,11 +630,53 @@ export async function resolveLoadersOnly<TEnv>(
559
630
  deps: SegmentResolutionDeps<TEnv>,
560
631
  ): Promise<ResolvedSegment[]> {
561
632
  const loaderSegments: ResolvedSegment[] = [];
633
+ const seenIds = new Set<string>();
634
+
635
+ async function collectEntryLoaders(
636
+ entry: EntryData,
637
+ belongsToRoute: boolean,
638
+ shortCodeOverride?: string,
639
+ ): Promise<void> {
640
+ // Skip if all loaders from this entry have already been resolved
641
+ // via a parent (e.g., cache boundary wrapping a layout with shared loaders).
642
+ const entryLoaders = entry.loader ?? [];
643
+ const sc = shortCodeOverride ?? entry.shortCode;
644
+ const allAlreadySeen =
645
+ entryLoaders.length > 0 &&
646
+ entryLoaders.every((le, i) =>
647
+ seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
648
+ );
649
+ if (!allAlreadySeen) {
650
+ const segments = await resolveLoaders(
651
+ entry,
652
+ context,
653
+ belongsToRoute,
654
+ deps,
655
+ shortCodeOverride,
656
+ );
657
+ for (const seg of segments) {
658
+ if (!seenIds.has(seg.id)) {
659
+ seenIds.add(seg.id);
660
+ loaderSegments.push(seg);
661
+ }
662
+ }
663
+ }
664
+
665
+ const seenParallelEntryIds = new Set<string>();
666
+ for (const parallelEntry of getParallelEntries(entry.parallel)) {
667
+ if (seenParallelEntryIds.has(parallelEntry.id)) continue;
668
+ seenParallelEntryIds.add(parallelEntry.id);
669
+ await collectEntryLoaders(parallelEntry, belongsToRoute, entry.shortCode);
670
+ }
671
+
672
+ const childBelongsToRoute = belongsToRoute || entry.type === "route";
673
+ for (const layoutEntry of entry.layout) {
674
+ await collectEntryLoaders(layoutEntry, childBelongsToRoute);
675
+ }
676
+ }
562
677
 
563
678
  for (const entry of entries) {
564
- const belongsToRoute = entry.type === "route";
565
- const segments = await resolveLoaders(entry, context, belongsToRoute, deps);
566
- loaderSegments.push(...segments);
679
+ await collectEntryLoaders(entry, entry.type === "route");
567
680
  }
568
681
 
569
682
  return loaderSegments;
@@ -8,7 +8,7 @@
8
8
  * - Error boundary segment creation
9
9
  */
10
10
 
11
- import type { ReactNode } from "react";
11
+ import { createElement, type ReactNode } from "react";
12
12
  import { DataNotFoundError } from "../../errors";
13
13
  import {
14
14
  createErrorInfo,
@@ -180,34 +180,39 @@ export function catchSegmentError<TEnv>(
180
180
 
181
181
  if (error instanceof DataNotFoundError) {
182
182
  const notFoundFallback = deps.findNearestNotFoundBoundary(entry);
183
+ // Fall back to router's notFound component, then a plain default
184
+ const notFoundOption = deps.notFoundComponent;
185
+ const defaultFallback =
186
+ typeof notFoundOption === "function"
187
+ ? notFoundOption({ pathname: pathname ?? "" })
188
+ : (notFoundOption ?? createElement("h1", null, "Not Found"));
189
+ const effectiveNotFoundFallback = notFoundFallback ?? defaultFallback;
183
190
 
184
- if (notFoundFallback) {
185
- const notFoundInfo = createNotFoundInfo(
186
- error,
187
- entry.shortCode,
188
- entry.type,
189
- pathname,
190
- );
191
+ const notFoundInfo = createNotFoundInfo(
192
+ error,
193
+ entry.shortCode,
194
+ entry.type,
195
+ pathname,
196
+ );
191
197
 
192
- reportError(true, {
193
- notFound: true,
194
- message: notFoundInfo.message,
195
- });
198
+ reportError(true, {
199
+ notFound: true,
200
+ message: notFoundInfo.message,
201
+ });
196
202
 
197
- debugLog("segment", "notFound boundary handled error", {
198
- segmentId: entry.shortCode,
199
- message: notFoundInfo.message,
200
- });
203
+ debugLog("segment", "notFound boundary handled error", {
204
+ segmentId: entry.shortCode,
205
+ message: notFoundInfo.message,
206
+ });
201
207
 
202
- setResponseStatus(404);
208
+ setResponseStatus(404);
203
209
 
204
- return createNotFoundSegment(
205
- notFoundInfo,
206
- notFoundFallback,
207
- entry,
208
- params,
209
- );
210
- }
210
+ return createNotFoundSegment(
211
+ notFoundInfo,
212
+ effectiveNotFoundFallback,
213
+ entry,
214
+ params,
215
+ );
211
216
  }
212
217
 
213
218
  const fallback = deps.findNearestErrorBoundary(entry);
@@ -147,6 +147,7 @@ export function resolveLoaderData<TEnv>(
147
147
  }
148
148
 
149
149
  const loaderId = loaderEntry.loader.$$id;
150
+
150
151
  const ttl = resolveTtl(options.ttl, store.defaults, DEFAULT_ROUTE_TTL);
151
152
  const swrWindow = resolveSwrWindow(options.swr, store.defaults);
152
153
  const swr = swrWindow || undefined;