@rangojs/router 0.2.0 → 0.4.1

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 (105) hide show
  1. package/README.md +19 -1
  2. package/dist/types/browser/event-controller.d.ts +4 -0
  3. package/dist/types/browser/link-interceptor.d.ts +17 -7
  4. package/dist/types/browser/navigation-bridge.d.ts +13 -1
  5. package/dist/types/browser/notify-listeners.d.ts +2 -0
  6. package/dist/types/browser/prefetch/cache.d.ts +1 -1
  7. package/dist/types/browser/prefetch/default-strategy.d.ts +31 -0
  8. package/dist/types/browser/prefetch/invalidation.d.ts +6 -0
  9. package/dist/types/browser/prefetch/loader.d.ts +3 -1
  10. package/dist/types/browser/prefetch/observer.d.ts +3 -6
  11. package/dist/types/browser/prefetch/runtime.d.ts +0 -1
  12. package/dist/types/browser/rango-state.d.ts +4 -0
  13. package/dist/types/browser/react/Link.d.ts +11 -19
  14. package/dist/types/browser/react/context.d.ts +3 -0
  15. package/dist/types/browser/rsc-router.d.ts +7 -2
  16. package/dist/types/browser/types.d.ts +6 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +11 -3
  18. package/dist/types/cache/cache-scope.d.ts +82 -3
  19. package/dist/types/cache/cf/cf-cache-store.d.ts +2 -2
  20. package/dist/types/cache/index.d.ts +3 -1
  21. package/dist/types/cache/search-params-filter.d.ts +64 -0
  22. package/dist/types/cache/shell-snapshot.d.ts +4 -4
  23. package/dist/types/cache/types.d.ts +36 -2
  24. package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -2
  25. package/dist/types/index.d.ts +1 -0
  26. package/dist/types/index.rsc.d.ts +1 -0
  27. package/dist/types/router/match-middleware/cache-lookup.d.ts +13 -0
  28. package/dist/types/router/navigation-snapshot.d.ts +29 -0
  29. package/dist/types/router/prefetch-default.d.ts +28 -0
  30. package/dist/types/router/router-interfaces.d.ts +7 -0
  31. package/dist/types/router/router-options.d.ts +54 -0
  32. package/dist/types/rsc/capture-queue.d.ts +6 -0
  33. package/dist/types/rsc/shell-build-manifest.d.ts +7 -1
  34. package/dist/types/rsc/shell-capture.d.ts +15 -7
  35. package/dist/types/rsc/shell-serve.d.ts +11 -4
  36. package/dist/types/rsc/types.d.ts +19 -0
  37. package/dist/types/server/request-context.d.ts +57 -4
  38. package/dist/types/testing/e2e/index.d.ts +2 -2
  39. package/dist/types/testing/e2e/page-helpers.d.ts +24 -0
  40. package/dist/types/testing/render-route.d.ts +10 -1
  41. package/dist/types/testing/shell-status.d.ts +6 -3
  42. package/dist/types/vite/plugin-types.d.ts +1 -1
  43. package/dist/vite/index.js +9 -6
  44. package/package.json +23 -22
  45. package/skills/caching/SKILL.md +39 -0
  46. package/skills/comparison/references/framework-comparison.md +9 -2
  47. package/skills/links/SKILL.md +24 -0
  48. package/skills/ppr/SKILL.md +80 -16
  49. package/skills/router-setup/SKILL.md +9 -0
  50. package/skills/testing/client-components.md +15 -14
  51. package/skills/vercel/SKILL.md +1 -1
  52. package/src/browser/event-controller.ts +110 -11
  53. package/src/browser/link-interceptor.ts +469 -20
  54. package/src/browser/navigation-bridge.ts +71 -2
  55. package/src/browser/navigation-store.ts +24 -1
  56. package/src/browser/notify-listeners.ts +22 -0
  57. package/src/browser/prefetch/cache.ts +4 -2
  58. package/src/browser/prefetch/default-strategy.ts +74 -0
  59. package/src/browser/prefetch/invalidation.ts +30 -0
  60. package/src/browser/prefetch/loader.ts +18 -17
  61. package/src/browser/prefetch/observer.ts +50 -22
  62. package/src/browser/prefetch/runtime.ts +0 -1
  63. package/src/browser/rango-state.ts +21 -0
  64. package/src/browser/react/Link.tsx +111 -101
  65. package/src/browser/react/NavigationProvider.tsx +1 -0
  66. package/src/browser/react/context.ts +4 -0
  67. package/src/browser/rsc-router.tsx +30 -9
  68. package/src/browser/types.ts +6 -0
  69. package/src/cache/cache-key-utils.ts +18 -4
  70. package/src/cache/cache-runtime.ts +7 -2
  71. package/src/cache/cache-scope.ts +152 -23
  72. package/src/cache/cf/cf-cache-store.ts +55 -16
  73. package/src/cache/document-cache.ts +7 -1
  74. package/src/cache/index.ts +7 -0
  75. package/src/cache/search-params-filter.ts +118 -0
  76. package/src/cache/shell-snapshot.ts +8 -4
  77. package/src/cache/types.ts +39 -2
  78. package/src/cache/vercel/vercel-cache-store.ts +22 -3
  79. package/src/index.rsc.ts +4 -0
  80. package/src/index.ts +6 -0
  81. package/src/router/match-middleware/cache-lookup.ts +146 -33
  82. package/src/router/match-middleware/cache-store.ts +70 -0
  83. package/src/router/navigation-snapshot.ts +46 -6
  84. package/src/router/prefetch-default.ts +59 -0
  85. package/src/router/router-interfaces.ts +8 -0
  86. package/src/router/router-options.ts +59 -1
  87. package/src/router.ts +7 -0
  88. package/src/rsc/capture-queue.ts +24 -1
  89. package/src/rsc/full-payload.ts +1 -0
  90. package/src/rsc/handler.ts +10 -0
  91. package/src/rsc/response-cache-serve.ts +1 -1
  92. package/src/rsc/rsc-rendering.ts +367 -40
  93. package/src/rsc/shell-build-manifest.ts +8 -1
  94. package/src/rsc/shell-capture.ts +99 -50
  95. package/src/rsc/shell-serve.ts +14 -6
  96. package/src/rsc/types.ts +19 -0
  97. package/src/server/request-context.ts +62 -3
  98. package/src/testing/dispatch.ts +21 -3
  99. package/src/testing/e2e/index.ts +6 -0
  100. package/src/testing/e2e/page-helpers.ts +47 -0
  101. package/src/testing/e2e/parity.ts +13 -0
  102. package/src/testing/render-route.tsx +86 -17
  103. package/src/testing/shell-status.ts +20 -3
  104. package/src/vite/plugin-types.ts +1 -1
  105. package/src/vite/plugins/vercel-output.ts +2 -2
@@ -38,7 +38,7 @@
38
38
  * (the segment chain is wrapped in a MountContext exactly as in production).
39
39
  */
40
40
 
41
- import type { ReactNode, ComponentType } from "react";
41
+ import { useEffect, type ReactNode, type ComponentType } from "react";
42
42
  import type { RenderResult } from "@testing-library/react";
43
43
  import { renderSegments } from "../segment-system.js";
44
44
  import {
@@ -62,8 +62,14 @@ import type { Handle } from "../handle.js";
62
62
  import type { ThemeConfig } from "../theme/types.js";
63
63
  import { resolveThemeConfig } from "../theme/constants.js";
64
64
  import { isUnderTestRunner } from "../runtime-env.js";
65
+ import { setupNavigationBridgeDelegatedPrefetch } from "../browser/navigation-bridge.js";
66
+ import { resetAdaptiveStrategyForTesting } from "../browser/prefetch/default-strategy.js";
67
+ import { resetPrefetchObserverForTesting } from "../browser/prefetch/observer.js";
68
+ import type { PrefetchStrategy } from "../router/prefetch-default.js";
65
69
 
66
70
  const TEST_ORIGIN = "http://localhost";
71
+ let activePrefetchRegistrations = 0;
72
+ let pendingPrefetchReset: object | undefined;
67
73
 
68
74
  /**
69
75
  * Seed shape for `options.handle`, matching the handle wire format:
@@ -268,6 +274,14 @@ export interface RenderRouteOptions {
268
274
  * expect(getByTestId("nonce").textContent).toBe("test-nonce");
269
275
  */
270
276
  nonce?: string;
277
+ /**
278
+ * Router default prefetch strategy scoped to this rendered tree. This mirrors
279
+ * `createRouter({ defaultPrefetch })` for Links and eligible plain anchors
280
+ * inside `basename`. `data-prefetch="false"`/`"none"` opts out; `"true"`
281
+ * allows an application route with a common static-resource suffix but does
282
+ * not override a `"none"` default.
283
+ */
284
+ defaultPrefetch?: PrefetchStrategy;
271
285
  }
272
286
 
273
287
  /**
@@ -299,6 +313,41 @@ interface ResolvedMatch {
299
313
  pathname: string;
300
314
  }
301
315
 
316
+ function DelegatedPrefetchRegistration({
317
+ bridge,
318
+ }: {
319
+ bridge: NavigationBridge;
320
+ }): null {
321
+ useEffect(() => {
322
+ const unregister = bridge.registerDelegatedPrefetch();
323
+ pendingPrefetchReset = undefined;
324
+ activePrefetchRegistrations++;
325
+ return () => {
326
+ try {
327
+ unregister();
328
+ } finally {
329
+ activePrefetchRegistrations--;
330
+ if (activePrefetchRegistrations === 0) {
331
+ const reset = {};
332
+ pendingPrefetchReset = reset;
333
+ queueMicrotask(() => {
334
+ if (
335
+ pendingPrefetchReset !== reset ||
336
+ activePrefetchRegistrations !== 0
337
+ ) {
338
+ return;
339
+ }
340
+ pendingPrefetchReset = undefined;
341
+ resetPrefetchObserverForTesting();
342
+ resetAdaptiveStrategyForTesting();
343
+ });
344
+ }
345
+ }
346
+ };
347
+ }, [bridge]);
348
+ return null;
349
+ }
350
+
302
351
  function matchLeaf(
303
352
  pattern: string,
304
353
  pathname: string,
@@ -429,6 +478,7 @@ export async function renderRoute(
429
478
 
430
479
  const historyKey = generateHistoryKey(url.href);
431
480
  const mount = normalizeBasename(options.mount);
481
+ const basename = normalizeBasename(options.basename);
432
482
  // Fail loud on a request that cannot resolve the leaf route (a typo, or the
433
483
  // mount-prefixed-vs-relative confusion) instead of silently rendering empty
434
484
  // params (matchLeaf -> null -> {}). renderRoute paths are include-RELATIVE and
@@ -520,20 +570,31 @@ export async function renderRoute(
520
570
  });
521
571
  };
522
572
 
573
+ let prefetchRoot: HTMLElement | undefined;
523
574
  const bridge: NavigationBridge = {
524
575
  navigate: (target) => navigate(target),
525
576
  refresh: () => navigate(url.pathname + url.search),
526
577
  handlePopstate: async () => {},
527
578
  registerLinkInterception: () => () => {},
579
+ registerDelegatedPrefetch: () =>
580
+ setupNavigationBridgeDelegatedPrefetch(
581
+ store,
582
+ eventController,
583
+ () => undefined,
584
+ {
585
+ defaultPrefetch: options.defaultPrefetch,
586
+ root: prefetchRoot,
587
+ basename,
588
+ },
589
+ ),
528
590
  getVersion: () => undefined,
529
591
  updateVersion: () => {},
530
592
  };
531
593
 
532
- const initialMetadata = makeMetadata(
533
- url.pathname,
534
- initialSegments,
535
- initialMatch.params,
536
- );
594
+ const initialMetadata = {
595
+ ...makeMetadata(url.pathname, initialSegments, initialMatch.params),
596
+ defaultPrefetch: options.defaultPrefetch,
597
+ };
537
598
  const initialTree = await renderSegments(initialSegments);
538
599
 
539
600
  // Wrap render in an awaited async act so a tree that suspends (async loaders,
@@ -542,19 +603,27 @@ export async function renderRoute(
542
603
  // suspended inside an act scope, but the act call was not awaited") and the
543
604
  // resolved content never reaches the asserted DOM.
544
605
  let result!: Awaited<ReturnType<typeof render>>;
606
+ const container = document.body.appendChild(document.createElement("div"));
607
+ prefetchRoot = container;
545
608
  await act(async () => {
546
609
  result = render(
547
- <NavigationProvider
548
- store={store}
549
- eventController={eventController}
550
- initialPayload={{ root: initialTree, metadata: initialMetadata }}
551
- bridge={bridge}
552
- basename={normalizeBasename(options.basename)}
553
- themeConfig={
554
- options.theme === undefined ? null : resolveThemeConfig(options.theme)
555
- }
556
- nonce={options.nonce}
557
- />,
610
+ <>
611
+ <NavigationProvider
612
+ store={store}
613
+ eventController={eventController}
614
+ initialPayload={{ root: initialTree, metadata: initialMetadata }}
615
+ bridge={bridge}
616
+ basename={basename}
617
+ themeConfig={
618
+ options.theme === undefined
619
+ ? null
620
+ : resolveThemeConfig(options.theme)
621
+ }
622
+ nonce={options.nonce}
623
+ />
624
+ <DelegatedPrefetchRegistration bridge={bridge} />
625
+ </>,
626
+ { baseElement: document.body, container },
558
627
  );
559
628
  });
560
629
 
@@ -29,6 +29,10 @@
29
29
  */
30
30
 
31
31
  import { sortedSearchString } from "../cache/cache-key-utils.js";
32
+ import {
33
+ compileSearchParamsFilter,
34
+ type CacheSearchParams,
35
+ } from "../cache/search-params-filter.js";
32
36
 
33
37
  /** Production header name (`rsc/shell-serve.ts` `SHELL_STATUS_HEADER`). */
34
38
  export const SHELL_STATUS_HEADER: string = "x-rango-shell";
@@ -45,6 +49,10 @@ const PPR_REPLAY_BYPASS_REASONS = [
45
49
  "nonce",
46
50
  "store-unavailable",
47
51
  "passive-read-unsupported",
52
+ "no-navigation-context",
53
+ "prerender-store",
54
+ "intercept",
55
+ "cache-disabled",
48
56
  "read-error",
49
57
  "no-entry",
50
58
  "invalid-version",
@@ -53,6 +61,7 @@ const PPR_REPLAY_BYPASS_REASONS = [
53
61
  "transition-when",
54
62
  "no-segment-snapshot",
55
63
  "snapshot-miss",
64
+ "explicit-cache-hit",
56
65
  "stale-build-entry",
57
66
  ] as const;
58
67
 
@@ -74,12 +83,20 @@ export type ShellStatusTarget = Response | { headers: Headers };
74
83
  * module (it pulls React). A parity test pins the two implementations.
75
84
  *
76
85
  * Accepts a `URL` or an absolute/relative request URL string (relative strings
77
- * resolve against `http://localhost`).
86
+ * resolve against `http://localhost`). Pass the router's `cache.searchParams`
87
+ * config as `searchParams` when the app under test sets one — the production
88
+ * key applies it, so the expected key must too.
78
89
  */
79
- export function shellCacheKey(url: URL | string): string {
90
+ export function shellCacheKey(
91
+ url: URL | string,
92
+ searchParams?: CacheSearchParams,
93
+ ): string {
80
94
  const resolved =
81
95
  typeof url === "string" ? new URL(url, "http://localhost") : url;
82
- const sorted = sortedSearchString(resolved.searchParams);
96
+ const sorted = sortedSearchString(
97
+ resolved.searchParams,
98
+ compileSearchParamsFilter(searchParams),
99
+ );
83
100
  const searchSuffix = sorted ? `?${sorted}` : "";
84
101
  return `${resolved.host}${resolved.pathname}${searchSuffix}:shell`;
85
102
  }
@@ -267,7 +267,7 @@ export interface RangoCloudflareOptions extends RangoBaseOptions {
267
267
  * `.vc-config.json` (and `config.json` for `functionName`).
268
268
  */
269
269
  export interface VercelPresetOptions {
270
- /** Node runtime for the function. @default "nodejs22.x" */
270
+ /** Node runtime for the function. @default "nodejs24.x" */
271
271
  runtime?: string;
272
272
  /** Max execution time in seconds. @default 30 */
273
273
  maxDuration?: number;
@@ -99,7 +99,7 @@ export function assertVercelNodeRuntime(runtime: string | undefined): void {
99
99
  throw new Error(
100
100
  `[rango] preset "vercel": runtime "${runtime}" is not supported. ` +
101
101
  `This preset emits a Node serverless function; use a "nodejs*" runtime ` +
102
- `(default "nodejs22.x"). The Edge runtime is not supported.`,
102
+ `(default "nodejs24.x"). The Edge runtime is not supported.`,
103
103
  );
104
104
  }
105
105
  }
@@ -125,7 +125,7 @@ export function buildVercelVcConfig(
125
125
  vercel: VercelPresetOptions,
126
126
  ): Record<string, unknown> {
127
127
  const vcConfig: Record<string, unknown> = {
128
- runtime: vercel.runtime ?? "nodejs22.x",
128
+ runtime: vercel.runtime ?? "nodejs24.x",
129
129
  handler: "index.mjs",
130
130
  launcherType: "Nodejs",
131
131
  shouldAddHelpers: false,