@rangojs/router 0.20.0 → 0.21.0
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/dist/bin/rango.js +26 -14
- package/dist/types/browser/event-controller.d.ts +32 -0
- package/dist/types/browser/history-state.d.ts +16 -4
- package/dist/types/browser/navigation-transaction.d.ts +7 -0
- package/dist/types/browser/rango-state.d.ts +3 -1
- package/dist/types/browser/react/context.d.ts +11 -0
- package/dist/types/browser/react/location-state-shared.d.ts +84 -10
- package/dist/types/browser/react/location-state.d.ts +7 -2
- package/dist/types/browser/react/subscribe-to-nothing.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +4 -5
- package/dist/types/cache/cf/cf-cache-store.d.ts +27 -6
- package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
- package/dist/types/cache/handle-snapshot.d.ts +36 -50
- package/dist/types/cache/index.d.ts +2 -0
- package/dist/types/cache/shell-snapshot.d.ts +19 -3
- package/dist/types/cache/types.d.ts +21 -10
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +28 -16
- package/dist/types/client-urls/navigation.d.ts +7 -1
- package/dist/types/client-urls/optimistic-location.d.ts +10 -4
- package/dist/types/handles/deferred-resolution.d.ts +11 -1
- package/dist/types/prerender/build-shell-capture.d.ts +8 -6
- package/dist/types/router/match-middleware/cache-lookup.d.ts +1 -1
- package/dist/types/router/navigation-snapshot.d.ts +8 -1
- package/dist/types/router/router-interfaces.d.ts +2 -1
- package/dist/types/router/router-options.d.ts +17 -7
- package/dist/types/router/segment-resolution/loader-cache.d.ts +21 -9
- package/dist/types/router-versions.d.ts +39 -0
- package/dist/types/rsc/handler-context.d.ts +5 -0
- package/dist/types/rsc/shell-build-manifest.d.ts +3 -2
- package/dist/types/rsc/types.d.ts +4 -6
- package/dist/types/server/build-version-table.d.ts +44 -0
- package/dist/types/server/handle-store.d.ts +87 -46
- package/dist/types/server/request-context.d.ts +22 -8
- package/dist/types/testing/build-versions.d.ts +32 -0
- package/dist/types/testing/dom.entry.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +2 -0
- package/dist/types/testing/internal/seed-vars.d.ts +7 -2
- package/dist/types/testing/location-state-key.d.ts +5 -1
- package/dist/types/testing/render-route.d.ts +77 -6
- package/dist/types/testing/serve-shell-request.d.ts +21 -0
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/types/handler-context.d.ts +8 -5
- package/dist/types/vite/discovery/build-versions.d.ts +393 -0
- package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
- package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
- package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
- package/dist/types/vite/discovery/state.d.ts +14 -0
- package/dist/types/vite/encryption-key.d.ts +18 -0
- package/dist/types/vite/plugin-types.d.ts +27 -0
- package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
- package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
- package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
- package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
- package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
- package/dist/types/vite/plugins/virtual-entries.d.ts +18 -1
- package/dist/types/vite/router-discovery.d.ts +6 -0
- package/dist/types/vite/utils/client-chunks.d.ts +2 -0
- package/dist/types/vite/utils/compare-strings.d.ts +7 -0
- package/dist/types/vite/utils/package-resolution.d.ts +8 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
- package/dist/vite/index.js +1537 -444
- package/package.json +1 -2
- package/skills/cache-guide/SKILL.md +84 -20
- package/skills/caching/SKILL.md +16 -5
- package/skills/client-urls/SKILL.md +16 -0
- package/skills/cloudflare/SKILL.md +2 -1
- package/skills/comparison/references/framework-comparison.md +16 -12
- package/skills/hooks/SKILL.md +3 -0
- package/skills/hooks/handle-and-actions.md +6 -0
- package/skills/hooks/navigation.md +28 -0
- package/skills/hooks/state.md +209 -8
- package/skills/hooks/urls.md +40 -1
- package/skills/host-router/SKILL.md +13 -0
- package/skills/intercept/SKILL.md +6 -0
- package/skills/loader/SKILL.md +19 -1
- package/skills/middleware/SKILL.md +2 -0
- package/skills/ppr/SKILL.md +119 -40
- package/skills/prerender/SKILL.md +7 -3
- package/skills/rango/SKILL.md +8 -7
- package/skills/router-setup/SKILL.md +15 -1
- package/skills/testing/SKILL.md +30 -26
- package/skills/testing/client-components.md +193 -8
- package/skills/testing/handles.md +2 -0
- package/skills/testing/loader.md +1 -1
- package/skills/testing/middleware.md +1 -1
- package/skills/testing/render-handler.md +18 -18
- package/skills/testing/serve-shell-request.md +63 -14
- package/skills/testing/server-actions.md +17 -17
- package/skills/testing/server-tree.md +1 -1
- package/skills/typesafety/generated-files-and-cli.md +1 -1
- package/skills/use-cache/SKILL.md +16 -5
- package/skills/vercel/SKILL.md +3 -3
- package/src/bin/rango.ts +1 -1
- package/src/browser/event-controller.ts +75 -2
- package/src/browser/history-state.ts +39 -17
- package/src/browser/navigation-bridge.ts +41 -18
- package/src/browser/navigation-transaction.ts +15 -10
- package/src/browser/partial-update.ts +10 -2
- package/src/browser/rango-state.ts +3 -1
- package/src/browser/react/Link.tsx +2 -3
- package/src/browser/react/NavigationProvider.tsx +25 -1
- package/src/browser/react/context.ts +13 -0
- package/src/browser/react/location-state-shared.ts +200 -34
- package/src/browser/react/location-state.ts +40 -75
- package/src/browser/react/subscribe-to-nothing.ts +7 -0
- package/src/browser/react/use-handle.ts +20 -1
- package/src/browser/rsc-router.tsx +33 -11
- package/src/browser/server-action-bridge.ts +17 -1
- package/src/cache/cache-scope.ts +18 -8
- package/src/cache/cf/cf-cache-store.ts +75 -55
- package/src/cache/cf/cf-cache-types.ts +12 -4
- package/src/cache/handle-snapshot.ts +69 -87
- package/src/cache/index.ts +4 -0
- package/src/cache/shell-snapshot.ts +35 -5
- package/src/cache/types.ts +21 -10
- package/src/cache/vercel/vercel-cache-store.ts +48 -20
- package/src/client-urls/client-root.tsx +1 -0
- package/src/client-urls/navigation.ts +10 -0
- package/src/client-urls/optimistic-location.ts +10 -4
- package/src/handles/deferred-resolution.ts +31 -0
- package/src/prerender/build-shell-capture.ts +11 -9
- package/src/router/manifest.ts +2 -0
- package/src/router/match-middleware/cache-lookup.ts +11 -18
- package/src/router/navigation-snapshot.ts +25 -6
- package/src/router/request-classification.ts +15 -12
- package/src/router/router-interfaces.ts +2 -1
- package/src/router/router-options.ts +17 -7
- package/src/router/segment-resolution/loader-cache.ts +95 -60
- package/src/router-versions.ts +60 -0
- package/src/rsc/full-payload.ts +6 -2
- package/src/rsc/handler-context.ts +5 -0
- package/src/rsc/handler.ts +14 -4
- package/src/rsc/rsc-rendering.ts +12 -1
- package/src/rsc/shell-build-manifest.ts +10 -7
- package/src/rsc/shell-capture.ts +36 -26
- package/src/rsc/types.ts +4 -6
- package/src/server/build-version-table.ts +115 -0
- package/src/server/handle-store.ts +344 -127
- package/src/server/request-context.ts +62 -11
- package/src/ssr/ssr-root.tsx +16 -4
- package/src/testing/build-versions.ts +64 -0
- package/src/testing/dispatch.ts +6 -0
- package/src/testing/dom.entry.ts +1 -0
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +3 -0
- package/src/testing/internal/context.ts +2 -1
- package/src/testing/internal/seed-vars.ts +14 -2
- package/src/testing/location-state-key.ts +18 -8
- package/src/testing/render-handler.ts +2 -1
- package/src/testing/render-route.tsx +277 -43
- package/src/testing/serve-shell-request.ts +80 -11
- package/src/testing/vitest-stubs/version.ts +4 -1
- package/src/types/handler-context.ts +8 -5
- package/src/vite/discovery/build-versions.ts +1281 -0
- package/src/vite/discovery/installed-packages.ts +159 -0
- package/src/vite/discovery/prerender-collection.ts +38 -1
- package/src/vite/discovery/router-versions-phase.ts +336 -0
- package/src/vite/discovery/shell-prerender-phase.ts +28 -38
- package/src/vite/discovery/state.ts +19 -0
- package/src/vite/discovery/virtual-module-codegen.ts +7 -2
- package/src/vite/encryption-key.ts +101 -9
- package/src/vite/plugin-types.ts +28 -0
- package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
- package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
- package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
- package/src/vite/plugins/expose-internal-ids.ts +12 -3
- package/src/vite/plugins/stable-rsc-output.ts +147 -0
- package/src/vite/plugins/version-plugin.ts +19 -24
- package/src/vite/plugins/version.d.ts +17 -2
- package/src/vite/plugins/virtual-entries.ts +31 -5
- package/src/vite/rango.ts +28 -3
- package/src/vite/router-discovery.ts +82 -13
- package/src/vite/utils/client-chunks.ts +5 -1
- package/src/vite/utils/compare-strings.ts +9 -0
- package/src/vite/utils/package-resolution.ts +9 -0
- package/src/vite/utils/prerender-utils.ts +4 -1
- package/src/vite/utils/shared-utils.ts +1 -1
|
@@ -47,6 +47,7 @@ import { type MetricsStore } from "./context.js";
|
|
|
47
47
|
import { observePhase, PHASES } from "../router/instrument.js";
|
|
48
48
|
import { getFetchableLoader } from "./fetchable-loader-store.js";
|
|
49
49
|
import type { SegmentCacheStore } from "../cache/types.js";
|
|
50
|
+
import type { RouterVersions } from "../router-versions.js";
|
|
50
51
|
import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
|
|
51
52
|
import type { ExecutionContext, RequestScope } from "../types/request-scope.js";
|
|
52
53
|
import type { TransitionWhenRecord } from "../router/transition-when.js";
|
|
@@ -377,7 +378,7 @@ export interface RequestContext<
|
|
|
377
378
|
|
|
378
379
|
/**
|
|
379
380
|
* @internal Loader-family snapshot seed for a shell HIT's tail render:
|
|
380
|
-
*
|
|
381
|
+
* loader id -> the capture's elided container (already Flight-deserialized
|
|
381
382
|
* by serveShellHit) plus its capture-computed hole bit. resolveLoaderData
|
|
382
383
|
* overlays it onto the fresh run's container (recorded paths pinned,
|
|
383
384
|
* hole-marker paths keep the fresh nested promises) so the payload's baked
|
|
@@ -385,7 +386,9 @@ export interface RequestContext<
|
|
|
385
386
|
* and does not run the loader. Two owners: serveShellHit sets it on the
|
|
386
387
|
* HIT tail's derived context, and matchPartialWithPprReplay sets it on the
|
|
387
388
|
* request context of a PPR partial replay once its doc record hits, and
|
|
388
|
-
* restores the previous value after the match.
|
|
389
|
+
* restores the previous value after the match. Read through servedPins
|
|
390
|
+
* (loader-cache.ts) only: it also decides which of the record's
|
|
391
|
+
* loader-owned handle pushes stand (loaderPins).
|
|
389
392
|
*/
|
|
390
393
|
_shellLoaderSeed?: Map<
|
|
391
394
|
string,
|
|
@@ -446,9 +449,7 @@ export interface RequestContext<
|
|
|
446
449
|
* scope replaces any route-derived cache() scope for the tail's match
|
|
447
450
|
* (resolveShellImplicitCacheScope), and a lookup that does not hit throws
|
|
448
451
|
* ShellRecordUnavailableError instead of resolving segments
|
|
449
|
-
* (withCacheLookup): a HIT never runs a handler.
|
|
450
|
-
* loader-owned handle pushes restore as authoritative
|
|
451
|
-
* (HandleStore.pushRestored): the prelude rendered them.
|
|
452
|
+
* (withCacheLookup): a HIT never runs a handler.
|
|
452
453
|
*/
|
|
453
454
|
docTail?: true;
|
|
454
455
|
/**
|
|
@@ -857,6 +858,15 @@ export interface RequestContext<
|
|
|
857
858
|
* lookups per router (route-map-builder.ts) */
|
|
858
859
|
_routerId?: string;
|
|
859
860
|
|
|
861
|
+
/**
|
|
862
|
+
* @internal Cache versions of the router serving this request. The cache
|
|
863
|
+
* factory builds a store per request without knowing the router, so a store
|
|
864
|
+
* reads its key versions from here, lazily, the same way CFCacheStore
|
|
865
|
+
* resolves its base URL (getCacheVersions, server/build-version-table.ts).
|
|
866
|
+
* Unset on a context no router created; a store then uses the build version.
|
|
867
|
+
*/
|
|
868
|
+
_versions?: RouterVersions;
|
|
869
|
+
|
|
860
870
|
/**
|
|
861
871
|
* @internal RouteSnapshot from classifyRequest, reused by match/matchPartial
|
|
862
872
|
* to avoid a second resolveRoute call. Cleared on HMR invalidation.
|
|
@@ -966,6 +976,7 @@ export type PublicRequestContext<
|
|
|
966
976
|
| "_tracing"
|
|
967
977
|
| "_basename"
|
|
968
978
|
| "_routerId"
|
|
979
|
+
| "_versions"
|
|
969
980
|
| "_setStatus"
|
|
970
981
|
| "_rotateStateCookie"
|
|
971
982
|
| "_setKeepCacheDirective"
|
|
@@ -1130,8 +1141,13 @@ export interface CreateRequestContextOptions<TEnv> {
|
|
|
1130
1141
|
themeConfig?: ResolvedThemeConfig | null;
|
|
1131
1142
|
/** Resolved rango state cookie name, for the server seat of invalidateClientCache(). */
|
|
1132
1143
|
stateCookieName?: string;
|
|
1133
|
-
/**
|
|
1134
|
-
|
|
1144
|
+
/**
|
|
1145
|
+
* The versions of the router serving this request (resolveRouterVersions).
|
|
1146
|
+
* Stored as `_versions` for the cache stores; `document` is also the prefix
|
|
1147
|
+
* of a server-rotated rango state value, which the browser compares with the
|
|
1148
|
+
* payload's `metadata.version`.
|
|
1149
|
+
*/
|
|
1150
|
+
versions?: RouterVersions;
|
|
1135
1151
|
}
|
|
1136
1152
|
|
|
1137
1153
|
/**
|
|
@@ -1159,7 +1175,7 @@ export function createRequestContext<TEnv>(
|
|
|
1159
1175
|
build = false,
|
|
1160
1176
|
themeConfig,
|
|
1161
1177
|
stateCookieName,
|
|
1162
|
-
|
|
1178
|
+
versions,
|
|
1163
1179
|
} = options;
|
|
1164
1180
|
// ctx.request.headers is guarded like headers() (#976); every router read
|
|
1165
1181
|
// of this request's headers goes through requestHeaders().
|
|
@@ -1194,6 +1210,12 @@ export function createRequestContext<TEnv>(
|
|
|
1194
1210
|
// Set-Cookie drain (rsc/helpers.ts) run outside any latched funnel scope,
|
|
1195
1211
|
// where assertCachedHeaderWriteAllowed is a no-op.
|
|
1196
1212
|
let rawStubHeaders = stubResponse.headers;
|
|
1213
|
+
// Node's util.inspect unwraps this Proxy and calls undici's Headers hook
|
|
1214
|
+
// with the proxy as `this`. That reads `#headersList` and throws (#1000).
|
|
1215
|
+
// Inspect does not consult the get trap, so the hook is an own property of
|
|
1216
|
+
// the target and formats `rawStubHeaders`. Read on each call: setStatus
|
|
1217
|
+
// rebinds that let to a new Headers.
|
|
1218
|
+
const NODE_INSPECT = Symbol.for("nodejs.util.inspect.custom");
|
|
1197
1219
|
const guardedStubHeaders: Headers = new Proxy(new Headers(), {
|
|
1198
1220
|
get(_target, prop) {
|
|
1199
1221
|
const raw = rawStubHeaders;
|
|
@@ -1208,6 +1230,22 @@ export function createRequestContext<TEnv>(
|
|
|
1208
1230
|
return (value as (...a: unknown[]) => unknown).bind(raw);
|
|
1209
1231
|
},
|
|
1210
1232
|
});
|
|
1233
|
+
Object.defineProperty(guardedStubHeaders, NODE_INSPECT, {
|
|
1234
|
+
value(...args: unknown[]): unknown {
|
|
1235
|
+
const hook = (rawStubHeaders as unknown as Record<symbol, unknown>)[
|
|
1236
|
+
NODE_INSPECT
|
|
1237
|
+
];
|
|
1238
|
+
if (typeof hook === "function") return hook.apply(rawStubHeaders, args);
|
|
1239
|
+
// DOM Headers has forEach and not entries. Node always has the hook above.
|
|
1240
|
+
const listed: Record<string, string> = {};
|
|
1241
|
+
rawStubHeaders.forEach((value, key) => {
|
|
1242
|
+
listed[key] = value;
|
|
1243
|
+
});
|
|
1244
|
+
return listed;
|
|
1245
|
+
},
|
|
1246
|
+
configurable: true,
|
|
1247
|
+
writable: true,
|
|
1248
|
+
});
|
|
1211
1249
|
const shadowStubHeaders = (res: Response): void => {
|
|
1212
1250
|
Object.defineProperty(res, "headers", {
|
|
1213
1251
|
value: guardedStubHeaders,
|
|
@@ -1429,7 +1467,7 @@ export function createRequestContext<TEnv>(
|
|
|
1429
1467
|
const prevRaw =
|
|
1430
1468
|
(requestHeaders(request).get("x-rango-state") || null) ??
|
|
1431
1469
|
getRawCookieValue(cookieHeader, stateCookieName);
|
|
1432
|
-
const value = mintStateValue(
|
|
1470
|
+
const value = mintStateValue(versions?.document ?? "0", prevRaw);
|
|
1433
1471
|
// rawStubHeaders: guard-exempt internal writer — invalidateClientCache()
|
|
1434
1472
|
// is documented callable from loaders and during shell capture.
|
|
1435
1473
|
rawStubHeaders.append(
|
|
@@ -1505,6 +1543,7 @@ export function createRequestContext<TEnv>(
|
|
|
1505
1543
|
|
|
1506
1544
|
_handleStore: handleStore,
|
|
1507
1545
|
_cacheStore: cacheStore,
|
|
1546
|
+
_versions: versions,
|
|
1508
1547
|
_searchParamsFilter: searchParamsFilter,
|
|
1509
1548
|
_explicitTaggedStores: explicitTaggedStores,
|
|
1510
1549
|
_requestTags: new Set<string>(),
|
|
@@ -1515,11 +1554,23 @@ export function createRequestContext<TEnv>(
|
|
|
1515
1554
|
|
|
1516
1555
|
waitUntil(fn: () => Promise<void>): void {
|
|
1517
1556
|
if (ctx.build) return;
|
|
1518
|
-
//
|
|
1557
|
+
// A deferred task does not rely on the platform carrying the request's
|
|
1558
|
+
// async context into it. Deployed workerd lost it once ("use cache"
|
|
1559
|
+
// background revalidation, cache-runtime.ts); Node and miniflare never
|
|
1560
|
+
// do, so no local run shows a loss. A cache store builds its key inside
|
|
1561
|
+
// the task and reads the serving router's versions and the request host
|
|
1562
|
+
// from the context: without it the write lands under the whole-build
|
|
1563
|
+
// pair, which no router with its own version reads. So the task
|
|
1564
|
+
// re-enters the context it was scheduled under: the store active then
|
|
1565
|
+
// (a derived context), else this one.
|
|
1566
|
+
const scheduledUnder = requestContextStorage.getStore() ?? ctx;
|
|
1567
|
+
// Wrap in Promise.resolve().then(...) so a SYNCHRONOUS throw in a
|
|
1519
1568
|
// non-async callback becomes a rejected promise handed to the host's
|
|
1520
1569
|
// waitUntil (logged as a background failure), instead of escaping into
|
|
1521
1570
|
// the request flow. Mirrors fireAndForgetWaitUntil's deferral.
|
|
1522
|
-
const task = Promise.resolve().then(
|
|
1571
|
+
const task = Promise.resolve().then(() =>
|
|
1572
|
+
requestContextStorage.run(scheduledUnder, fn),
|
|
1573
|
+
);
|
|
1523
1574
|
// Track the task promise so the PPR shell capture can settle the
|
|
1524
1575
|
// foreground's deferred cache writes before its own match/render (the
|
|
1525
1576
|
// ordering edge; see _pendingBackgroundTasks). The capture task itself
|
package/src/ssr/ssr-root.tsx
CHANGED
|
@@ -7,7 +7,10 @@ import {
|
|
|
7
7
|
} from "../browser/react/filter-segment-order.js";
|
|
8
8
|
import { ThemeProvider } from "../theme/ThemeProvider.js";
|
|
9
9
|
import { NonceContext } from "../browser/react/nonce-context.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
LocationStateContext,
|
|
12
|
+
NavigationStoreContext,
|
|
13
|
+
} from "../browser/react/context.js";
|
|
11
14
|
import type { NavigationStoreContextValue } from "../browser/react/context.js";
|
|
12
15
|
import type { HandleData } from "../browser/types.js";
|
|
13
16
|
import type { ResolvedSegment } from "../types.js";
|
|
@@ -119,11 +122,17 @@ function createSsrEventController(opts: {
|
|
|
119
122
|
flushRouteState: () => {},
|
|
120
123
|
setHandleData: () => {},
|
|
121
124
|
getHandleState: () => handleState,
|
|
125
|
+
freezeHydrationHandleState: () => {},
|
|
126
|
+
// The server render is the state the document is rendered with.
|
|
127
|
+
getHydrationHandleState: () => undefined,
|
|
122
128
|
setRouteSegmentIds: () => {},
|
|
123
129
|
setParams: (nextParams) => {
|
|
124
130
|
params = nextParams;
|
|
125
131
|
},
|
|
126
132
|
getParams: () => params,
|
|
133
|
+
commitLocationState: () => {},
|
|
134
|
+
getLocationState: () => undefined,
|
|
135
|
+
takeTreelessLocationState: () => false,
|
|
127
136
|
setLocation: () => {},
|
|
128
137
|
startNavigation: () => {
|
|
129
138
|
throw new Error("Navigation not supported during SSR");
|
|
@@ -294,7 +303,7 @@ export function createSsrRootComponent(opts: SsrRootOptions): React.FC {
|
|
|
294
303
|
};
|
|
295
304
|
|
|
296
305
|
// Build content tree from segments.
|
|
297
|
-
// Order must match NavigationProvider: NavigationStoreContext > NonceContext > ThemeProvider > content
|
|
306
|
+
// Order must match NavigationProvider: NavigationStoreContext > LocationStateContext > NonceContext > ThemeProvider > content
|
|
298
307
|
// Memoize like payload/handles above: renderSegments is async, so
|
|
299
308
|
// React.use() on a fresh promise suspends and replays SsrRoot, which
|
|
300
309
|
// would re-run the entire segment-tree build on every initial render.
|
|
@@ -324,10 +333,13 @@ export function createSsrRootComponent(opts: SsrRootOptions): React.FC {
|
|
|
324
333
|
<NonceContext.Provider value={nonce}>{content}</NonceContext.Provider>
|
|
325
334
|
);
|
|
326
335
|
|
|
327
|
-
// Wrap with NavigationStoreContext for useNavigation hook
|
|
336
|
+
// Wrap with NavigationStoreContext for useNavigation hook. The server has
|
|
337
|
+
// no history entry: location state is undefined, as readers hydrate.
|
|
328
338
|
return (
|
|
329
339
|
<NavigationStoreContext.Provider value={ssrContextValue!}>
|
|
330
|
-
{
|
|
340
|
+
<LocationStateContext.Provider value={undefined}>
|
|
341
|
+
{content}
|
|
342
|
+
</LocationStateContext.Provider>
|
|
331
343
|
</NavigationStoreContext.Provider>
|
|
332
344
|
);
|
|
333
345
|
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Testing primitive for the per-router cache versions.
|
|
3
|
+
*
|
|
4
|
+
* A production build gives every `createRouter()` a data version and a
|
|
5
|
+
* document version (docs/design/per-app-cache-version.md) and ships them as a
|
|
6
|
+
* table in the `@rangojs/router:version` module. A test has no build, so the
|
|
7
|
+
* module is a stub and every router serves unversioned. `setBuildVersions`
|
|
8
|
+
* installs a table in its place; the request handler, `dispatch` and the cache
|
|
9
|
+
* stores then resolve versions through the same code path the built app uses.
|
|
10
|
+
* Calling it again is a deploy: the next request is served by a handler that
|
|
11
|
+
* resolved the new versions.
|
|
12
|
+
*/
|
|
13
|
+
import { installRouterVersionsTable } from "../server/build-version-table.js";
|
|
14
|
+
import {
|
|
15
|
+
DEFAULT_ROUTER_VERSIONS_KEY,
|
|
16
|
+
type RouterVersions,
|
|
17
|
+
type RouterVersionsTable,
|
|
18
|
+
} from "../router-versions.js";
|
|
19
|
+
|
|
20
|
+
export type { RouterVersions };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The versions a simulated build ships.
|
|
24
|
+
*
|
|
25
|
+
* - `data` and `document`: the versions of every router the test does not list
|
|
26
|
+
* under `routers` (the build's whole-build pair).
|
|
27
|
+
* - `routers`: versions per router id (`router.id`), as a host-router build
|
|
28
|
+
* computes them.
|
|
29
|
+
*/
|
|
30
|
+
export interface BuildVersions extends RouterVersions {
|
|
31
|
+
routers?: Record<string, RouterVersions>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Install the cache versions of a simulated build, or remove them with no
|
|
36
|
+
* argument (routers serve unversioned again, the default in tests).
|
|
37
|
+
*
|
|
38
|
+
* A router that sets `createRouter({ version })` keeps that value for both
|
|
39
|
+
* versions, exactly as in production.
|
|
40
|
+
*
|
|
41
|
+
* @example A deploy that changes the client only keeps cached data
|
|
42
|
+
* ```ts
|
|
43
|
+
* setBuildVersions({ data: "d1", document: "h1" });
|
|
44
|
+
* await serveShellRequest(router, "/product/1", { cacheStore });
|
|
45
|
+
* setBuildVersions({ data: "d1", document: "h2" });
|
|
46
|
+
* const { shellStatus } = await serveShellRequest(router, "/product/1", {
|
|
47
|
+
* cacheStore,
|
|
48
|
+
* });
|
|
49
|
+
* // shellStatus is "MISS": the stored HTML was keyed by document version h1.
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export function setBuildVersions(versions?: BuildVersions): void {
|
|
53
|
+
if (!versions) {
|
|
54
|
+
installRouterVersionsTable(undefined);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const table: Record<string, readonly [string, string]> = {
|
|
58
|
+
[DEFAULT_ROUTER_VERSIONS_KEY]: [versions.data, versions.document],
|
|
59
|
+
};
|
|
60
|
+
for (const [routerId, pair] of Object.entries(versions.routers ?? {})) {
|
|
61
|
+
table[routerId] = [pair.data, pair.document];
|
|
62
|
+
}
|
|
63
|
+
installRouterVersionsTable(table satisfies RouterVersionsTable);
|
|
64
|
+
}
|
package/src/testing/dispatch.ts
CHANGED
|
@@ -106,6 +106,7 @@ import {
|
|
|
106
106
|
runWithRequestContext,
|
|
107
107
|
setRequestContextParams,
|
|
108
108
|
} from "../server/request-context.js";
|
|
109
|
+
import { resolveRouterVersions } from "../server/build-version-table.js";
|
|
109
110
|
import { executeMiddleware, matchMiddleware } from "../router/middleware.js";
|
|
110
111
|
import type {
|
|
111
112
|
MiddlewareEntry,
|
|
@@ -166,6 +167,8 @@ import { applyStreamIdleTimeout } from "../rsc/stream-idle.js";
|
|
|
166
167
|
interface DispatchableRouter<TEnv> {
|
|
167
168
|
id?: string;
|
|
168
169
|
routerId?: string;
|
|
170
|
+
/** createRouter({ version }), the consumer-set version. */
|
|
171
|
+
version?: string;
|
|
169
172
|
routeMap: Record<string, unknown>;
|
|
170
173
|
middleware: MiddlewareEntry<TEnv>[];
|
|
171
174
|
onError?: OnErrorCallback<TEnv>;
|
|
@@ -411,6 +414,9 @@ export async function dispatch<TEnv = any>(
|
|
|
411
414
|
cacheStore,
|
|
412
415
|
searchParamsFilter,
|
|
413
416
|
cacheProfiles: router.cacheProfiles,
|
|
417
|
+
// The store keys with the router's versions, resolved as the production
|
|
418
|
+
// handler resolves them (rsc/handler.ts).
|
|
419
|
+
versions: resolveRouterVersions(routerId, router.version),
|
|
414
420
|
});
|
|
415
421
|
// Wire background error reporting so cache degradation (reportCacheError ->
|
|
416
422
|
// _reportBackgroundError) reaches the router's onError, mirroring the production
|
package/src/testing/dom.entry.ts
CHANGED
package/src/testing/index.ts
CHANGED
|
@@ -98,6 +98,9 @@ export type {
|
|
|
98
98
|
ShellStatusTarget,
|
|
99
99
|
} from "./shell-status.js";
|
|
100
100
|
|
|
101
|
+
export { setBuildVersions } from "./build-versions.js";
|
|
102
|
+
export type { BuildVersions, RouterVersions } from "./build-versions.js";
|
|
103
|
+
|
|
101
104
|
export { collectHandle } from "./collect-handle.js";
|
|
102
105
|
|
|
103
106
|
export { withLocationStateKey } from "./location-state-key.js";
|
|
@@ -19,6 +19,7 @@ import { normalizeBasename } from "../../router/basename.js";
|
|
|
19
19
|
import {
|
|
20
20
|
seedVariables,
|
|
21
21
|
resolveSeededStateCookieName,
|
|
22
|
+
seededVersions,
|
|
22
23
|
type VarsInit,
|
|
23
24
|
type StateCookieSeed,
|
|
24
25
|
} from "./seed-vars.js";
|
|
@@ -164,7 +165,7 @@ export function createTestRequestContext<TEnv>(
|
|
|
164
165
|
cacheStore: opts.cacheStore,
|
|
165
166
|
cacheProfiles: opts.cacheProfiles,
|
|
166
167
|
stateCookieName,
|
|
167
|
-
|
|
168
|
+
versions: seededVersions(opts.stateCookie),
|
|
168
169
|
});
|
|
169
170
|
if (opts.basename !== undefined)
|
|
170
171
|
ctx._basename = normalizeBasename(opts.basename);
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { contextSet, type ContextVar } from "../../context-var.js";
|
|
10
10
|
import { resolveStateCookieName } from "../../router/state-cookie-name.js";
|
|
11
|
+
import { uniformVersions, type RouterVersions } from "../../router-versions.js";
|
|
11
12
|
|
|
12
13
|
export interface StateCookieSeed {
|
|
13
14
|
/**
|
|
@@ -22,8 +23,10 @@ export interface StateCookieSeed {
|
|
|
22
23
|
*/
|
|
23
24
|
routerId?: string;
|
|
24
25
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
26
|
+
* The app's version, as `createRouter({ version })` would set it: the
|
|
27
|
+
* rotated value's prefix (`{version}:{timestamp}`) and the version a cache
|
|
28
|
+
* store keys with. Defaults to `"0"` for the prefix (resolved inside
|
|
29
|
+
* createRequestContext) and to the build versions for the store.
|
|
27
30
|
*/
|
|
28
31
|
version?: string;
|
|
29
32
|
}
|
|
@@ -32,6 +35,15 @@ export function resolveSeededStateCookieName(seed?: StateCookieSeed): string {
|
|
|
32
35
|
return resolveStateCookieName(seed?.prefix, seed?.routerId ?? "router_0");
|
|
33
36
|
}
|
|
34
37
|
|
|
38
|
+
/** The request context's `versions` for a seeded `stateCookie.version`. */
|
|
39
|
+
export function seededVersions(
|
|
40
|
+
seed?: StateCookieSeed,
|
|
41
|
+
): RouterVersions | undefined {
|
|
42
|
+
return seed?.version === undefined
|
|
43
|
+
? undefined
|
|
44
|
+
: uniformVersions(seed.version);
|
|
45
|
+
}
|
|
46
|
+
|
|
35
47
|
export type VarsInit =
|
|
36
48
|
| Record<string, unknown>
|
|
37
49
|
| ReadonlyArray<readonly [ContextVar<unknown> | string, unknown]>;
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
+
isLocationStateKey,
|
|
3
|
+
LOCATION_STATE_KEY_PREFIX,
|
|
4
|
+
LOCATION_STATE_KEY_SUFFIX_SEPARATOR,
|
|
2
5
|
peekLocationStateKey,
|
|
3
6
|
type LocationStateDefinition,
|
|
4
7
|
} from "../browser/react/location-state-shared.js";
|
|
5
8
|
|
|
6
|
-
// Typed history.state slots must carry this prefix: buildHistoryState spreads
|
|
7
|
-
// only `__rsc_ls_*` keys onto history.state (history-state.ts).
|
|
8
|
-
const KEY_PREFIX = "__rsc_ls_";
|
|
9
|
-
|
|
10
9
|
let syntheticKeyCounter = 0;
|
|
11
10
|
|
|
12
11
|
/**
|
|
@@ -16,11 +15,15 @@ let syntheticKeyCounter = 0;
|
|
|
16
15
|
* use (`MyState(value)`, `useLocationState(MyState)`, `.read()`).
|
|
17
16
|
*
|
|
18
17
|
* - `withLocationStateKey(GridState, "GridState")` sets `__rsc_ls_GridState`
|
|
19
|
-
* (a name that already starts with `__rsc_ls_` is used as-is).
|
|
18
|
+
* (a name that already starts with `__rsc_ls_` is used as-is). A definition
|
|
19
|
+
* with `clearOnReload` appends its suffix, so read the stored key back from
|
|
20
|
+
* `GridState.__rsc_ls_key` (`__rsc_ls_GridState~r`).
|
|
20
21
|
* - Without a name, a key already set (by the plugin or an earlier call) is
|
|
21
22
|
* kept; otherwise the definition gets a synthetic `__rsc_ls_test_<n>` key,
|
|
22
23
|
* which then stays on it.
|
|
23
24
|
*
|
|
25
|
+
* A name may not contain "~": that separator is reserved for the suffix.
|
|
26
|
+
*
|
|
24
27
|
* Returns the definition.
|
|
25
28
|
*
|
|
26
29
|
* @example
|
|
@@ -35,11 +38,18 @@ export function withLocationStateKey<
|
|
|
35
38
|
TDefinition extends LocationStateDefinition<any, any>,
|
|
36
39
|
>(definition: TDefinition, name?: string): TDefinition {
|
|
37
40
|
if (name !== undefined) {
|
|
38
|
-
|
|
41
|
+
if (name.includes(LOCATION_STATE_KEY_SUFFIX_SEPARATOR)) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`withLocationStateKey: "${name}" contains "${LOCATION_STATE_KEY_SUFFIX_SEPARATOR}", ` +
|
|
44
|
+
"which is reserved for the key suffix of the `clearOnReload` option. " +
|
|
45
|
+
"Pass the name without a suffix; the definition appends its own.",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
definition.__rsc_ls_key = isLocationStateKey(name)
|
|
39
49
|
? name
|
|
40
|
-
:
|
|
50
|
+
: LOCATION_STATE_KEY_PREFIX + name;
|
|
41
51
|
} else if (!peekLocationStateKey(definition)) {
|
|
42
|
-
definition.__rsc_ls_key = `${
|
|
52
|
+
definition.__rsc_ls_key = `${LOCATION_STATE_KEY_PREFIX}test_${syntheticKeyCounter++}`;
|
|
43
53
|
}
|
|
44
54
|
return definition;
|
|
45
55
|
}
|
|
@@ -37,6 +37,7 @@ import type { LoaderDefinition } from "../types.js";
|
|
|
37
37
|
import {
|
|
38
38
|
seedVariables,
|
|
39
39
|
resolveSeededStateCookieName,
|
|
40
|
+
seededVersions,
|
|
40
41
|
type VarsInit,
|
|
41
42
|
type StateCookieSeed,
|
|
42
43
|
} from "./internal/seed-vars.js";
|
|
@@ -253,7 +254,7 @@ export async function renderHandler<TEnv = any>(
|
|
|
253
254
|
variables: seedVariables({}, opts.vars),
|
|
254
255
|
build: opts.build,
|
|
255
256
|
stateCookieName,
|
|
256
|
-
|
|
257
|
+
versions: seededVersions(opts.stateCookie),
|
|
257
258
|
cacheStore: opts.cacheStore,
|
|
258
259
|
cacheProfiles: opts.cacheProfiles,
|
|
259
260
|
themeConfig:
|