@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.
- package/README.md +19 -1
- package/dist/types/browser/event-controller.d.ts +4 -0
- package/dist/types/browser/link-interceptor.d.ts +17 -7
- package/dist/types/browser/navigation-bridge.d.ts +13 -1
- package/dist/types/browser/notify-listeners.d.ts +2 -0
- package/dist/types/browser/prefetch/cache.d.ts +1 -1
- package/dist/types/browser/prefetch/default-strategy.d.ts +31 -0
- package/dist/types/browser/prefetch/invalidation.d.ts +6 -0
- package/dist/types/browser/prefetch/loader.d.ts +3 -1
- package/dist/types/browser/prefetch/observer.d.ts +3 -6
- package/dist/types/browser/prefetch/runtime.d.ts +0 -1
- package/dist/types/browser/rango-state.d.ts +4 -0
- package/dist/types/browser/react/Link.d.ts +11 -19
- package/dist/types/browser/react/context.d.ts +3 -0
- package/dist/types/browser/rsc-router.d.ts +7 -2
- package/dist/types/browser/types.d.ts +6 -0
- package/dist/types/cache/cache-key-utils.d.ts +11 -3
- package/dist/types/cache/cache-scope.d.ts +82 -3
- package/dist/types/cache/cf/cf-cache-store.d.ts +2 -2
- package/dist/types/cache/index.d.ts +3 -1
- package/dist/types/cache/search-params-filter.d.ts +64 -0
- package/dist/types/cache/shell-snapshot.d.ts +4 -4
- package/dist/types/cache/types.d.ts +36 -2
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -2
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.rsc.d.ts +1 -0
- package/dist/types/router/match-middleware/cache-lookup.d.ts +13 -0
- package/dist/types/router/navigation-snapshot.d.ts +29 -0
- package/dist/types/router/prefetch-default.d.ts +28 -0
- package/dist/types/router/router-interfaces.d.ts +7 -0
- package/dist/types/router/router-options.d.ts +54 -0
- package/dist/types/rsc/capture-queue.d.ts +6 -0
- package/dist/types/rsc/shell-build-manifest.d.ts +7 -1
- package/dist/types/rsc/shell-capture.d.ts +15 -7
- package/dist/types/rsc/shell-serve.d.ts +11 -4
- package/dist/types/rsc/types.d.ts +19 -0
- package/dist/types/server/request-context.d.ts +57 -4
- package/dist/types/testing/e2e/index.d.ts +2 -2
- package/dist/types/testing/e2e/page-helpers.d.ts +24 -0
- package/dist/types/testing/render-route.d.ts +10 -1
- package/dist/types/testing/shell-status.d.ts +6 -3
- package/dist/types/vite/plugin-types.d.ts +1 -1
- package/dist/vite/index.js +9 -6
- package/package.json +23 -22
- package/skills/caching/SKILL.md +39 -0
- package/skills/comparison/references/framework-comparison.md +9 -2
- package/skills/links/SKILL.md +24 -0
- package/skills/ppr/SKILL.md +80 -16
- package/skills/router-setup/SKILL.md +9 -0
- package/skills/testing/client-components.md +15 -14
- package/skills/vercel/SKILL.md +1 -1
- package/src/browser/event-controller.ts +110 -11
- package/src/browser/link-interceptor.ts +469 -20
- package/src/browser/navigation-bridge.ts +71 -2
- package/src/browser/navigation-store.ts +24 -1
- package/src/browser/notify-listeners.ts +22 -0
- package/src/browser/prefetch/cache.ts +4 -2
- package/src/browser/prefetch/default-strategy.ts +74 -0
- package/src/browser/prefetch/invalidation.ts +30 -0
- package/src/browser/prefetch/loader.ts +18 -17
- package/src/browser/prefetch/observer.ts +50 -22
- package/src/browser/prefetch/runtime.ts +0 -1
- package/src/browser/rango-state.ts +21 -0
- package/src/browser/react/Link.tsx +111 -101
- package/src/browser/react/NavigationProvider.tsx +1 -0
- package/src/browser/react/context.ts +4 -0
- package/src/browser/rsc-router.tsx +30 -9
- package/src/browser/types.ts +6 -0
- package/src/cache/cache-key-utils.ts +18 -4
- package/src/cache/cache-runtime.ts +7 -2
- package/src/cache/cache-scope.ts +152 -23
- package/src/cache/cf/cf-cache-store.ts +55 -16
- package/src/cache/document-cache.ts +7 -1
- package/src/cache/index.ts +7 -0
- package/src/cache/search-params-filter.ts +118 -0
- package/src/cache/shell-snapshot.ts +8 -4
- package/src/cache/types.ts +39 -2
- package/src/cache/vercel/vercel-cache-store.ts +22 -3
- package/src/index.rsc.ts +4 -0
- package/src/index.ts +6 -0
- package/src/router/match-middleware/cache-lookup.ts +146 -33
- package/src/router/match-middleware/cache-store.ts +70 -0
- package/src/router/navigation-snapshot.ts +46 -6
- package/src/router/prefetch-default.ts +59 -0
- package/src/router/router-interfaces.ts +8 -0
- package/src/router/router-options.ts +59 -1
- package/src/router.ts +7 -0
- package/src/rsc/capture-queue.ts +24 -1
- package/src/rsc/full-payload.ts +1 -0
- package/src/rsc/handler.ts +10 -0
- package/src/rsc/response-cache-serve.ts +1 -1
- package/src/rsc/rsc-rendering.ts +367 -40
- package/src/rsc/shell-build-manifest.ts +8 -1
- package/src/rsc/shell-capture.ts +99 -50
- package/src/rsc/shell-serve.ts +14 -6
- package/src/rsc/types.ts +19 -0
- package/src/server/request-context.ts +62 -3
- package/src/testing/dispatch.ts +21 -3
- package/src/testing/e2e/index.ts +6 -0
- package/src/testing/e2e/page-helpers.ts +47 -0
- package/src/testing/e2e/parity.ts +13 -0
- package/src/testing/render-route.tsx +86 -17
- package/src/testing/shell-status.ts +20 -3
- package/src/vite/plugin-types.ts +1 -1
- 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
|
|
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 =
|
|
533
|
-
url.pathname,
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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(
|
|
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(
|
|
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
|
}
|
package/src/vite/plugin-types.ts
CHANGED
|
@@ -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 "
|
|
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 "
|
|
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 ?? "
|
|
128
|
+
runtime: vercel.runtime ?? "nodejs24.x",
|
|
129
129
|
handler: "index.mjs",
|
|
130
130
|
launcherType: "Nodejs",
|
|
131
131
|
shouldAddHelpers: false,
|