@rangojs/router 0.21.0 → 0.22.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/dist/bin/rango.js +13 -1
- package/dist/types/browser/event-controller.d.ts +30 -13
- package/dist/types/browser/navigation-store.d.ts +9 -0
- package/dist/types/browser/partial-update.d.ts +2 -1
- package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
- package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
- package/dist/types/browser/react/context.d.ts +14 -0
- package/dist/types/browser/rsc-router.d.ts +13 -2
- package/dist/types/browser/types.d.ts +10 -0
- package/dist/types/build/generate-manifest.d.ts +2 -0
- package/dist/types/cache/background-task.d.ts +22 -0
- package/dist/types/cache/cache-key-utils.d.ts +50 -5
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +1 -1
- package/dist/types/cache/cf/cf-cache-store.d.ts +7 -1
- package/dist/types/cache/document-cache.d.ts +8 -5
- package/dist/types/cache/handle-snapshot.d.ts +23 -0
- package/dist/types/cache/index.d.ts +1 -1
- package/dist/types/cache/invalidation-order.d.ts +14 -0
- package/dist/types/cache/memory-segment-store.d.ts +33 -1
- package/dist/types/cache/shell-snapshot.d.ts +10 -1
- package/dist/types/cache/store-scope.d.ts +22 -0
- package/dist/types/cache/tag-invalidation.d.ts +7 -6
- package/dist/types/cache/types.d.ts +63 -14
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +7 -1
- package/dist/types/errors.d.ts +6 -0
- package/dist/types/handles/deferred-resolution.d.ts +7 -0
- package/dist/types/internal-suspense-audit.d.ts +1 -0
- package/dist/types/loader-store.d.ts +4 -8
- package/dist/types/outlet-provider.d.ts +2 -1
- package/dist/types/prerender/cloudflare.d.ts +49 -0
- package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
- package/dist/types/prerender/index.d.ts +12 -0
- package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
- package/dist/types/prerender/on-demand.d.ts +272 -0
- package/dist/types/prerender/producer-guard.d.ts +33 -0
- package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
- package/dist/types/prerender/warm-request.d.ts +65 -0
- package/dist/types/prerender/warm.d.ts +65 -0
- package/dist/types/prerender/writable-store.d.ts +170 -0
- package/dist/types/prerender.d.ts +27 -0
- package/dist/types/reverse.d.ts +1 -1
- package/dist/types/route-content-wrapper.d.ts +3 -1
- package/dist/types/router/error-handling.d.ts +2 -0
- package/dist/types/router/handler-context.d.ts +1 -1
- package/dist/types/router/match-middleware/cache-lookup.d.ts +14 -0
- package/dist/types/router/pattern-matching.d.ts +2 -0
- package/dist/types/router/prefetch-default.d.ts +4 -4
- package/dist/types/router/prerender-match.d.ts +23 -1
- package/dist/types/router/route-trie-builder.d.ts +8 -1
- package/dist/types/router/router-context.d.ts +1 -0
- package/dist/types/router/router-interfaces.d.ts +30 -0
- package/dist/types/router/router-options.d.ts +24 -0
- package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +2 -1
- package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
- package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
- package/dist/types/router/trie-matching.d.ts +2 -0
- package/dist/types/rsc/shell-build-manifest.d.ts +6 -4
- package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
- package/dist/types/rsc/shell-capture.d.ts +14 -1
- package/dist/types/rsc/shell-serve.d.ts +2 -2
- package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
- package/dist/types/rsc/types.d.ts +8 -0
- package/dist/types/segment-boundary-content.d.ts +17 -0
- package/dist/types/segment-loader-promise.d.ts +2 -6
- package/dist/types/server/build-version-table.d.ts +9 -0
- package/dist/types/server/context.d.ts +12 -4
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +63 -2
- package/dist/types/suspense-audit-react.d.ts +26 -0
- package/dist/types/suspense-audit.d.ts +157 -0
- package/dist/types/testing/action-form.d.ts +34 -0
- package/dist/types/testing/flight.entry.d.ts +2 -0
- package/dist/types/testing/index.d.ts +4 -0
- package/dist/types/testing/internal/server-modules.d.ts +8 -0
- package/dist/types/testing/serve-shell-request.d.ts +34 -4
- package/dist/types/testing/shell-status.d.ts +14 -6
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
- package/dist/types/thenable-status.d.ts +6 -0
- package/dist/types/types/handler-context.d.ts +7 -0
- package/dist/types/types/route-entry.d.ts +5 -0
- package/dist/types/urls/path-helper.d.ts +18 -0
- package/dist/types/vite/discovery/state.d.ts +11 -0
- package/dist/types/vite/inject-client-debug.d.ts +6 -25
- package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
- package/dist/vite/index.js +180 -497
- package/package.json +13 -3
- package/skills/cache-guide/SKILL.md +18 -17
- package/skills/caching/SKILL.md +40 -4
- package/skills/catalog.json +1 -1
- package/skills/cloudflare/SKILL.md +131 -0
- package/skills/comparison/references/framework-comparison.md +1 -1
- package/skills/deployment-caching/SKILL.md +33 -12
- package/skills/document-cache/SKILL.md +16 -14
- package/skills/hooks/SKILL.md +22 -0
- package/skills/hooks/handle-and-actions.md +3 -4
- package/skills/hooks/navigation.md +10 -0
- package/skills/hooks/state.md +5 -4
- package/skills/host-router/SKILL.md +18 -0
- package/skills/loader/SKILL.md +6 -4
- package/skills/middleware/SKILL.md +1 -1
- package/skills/migrate-nextjs/SKILL.md +82 -9
- package/skills/ppr/SKILL.md +31 -8
- package/skills/prerender/SKILL.md +645 -8
- package/skills/rango/SKILL.md +1 -0
- package/skills/response-routes/SKILL.md +5 -5
- package/skills/route/SKILL.md +29 -0
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/cache-prerender.md +18 -17
- package/skills/testing/serve-shell-request.md +104 -8
- package/skills/use-cache/SKILL.md +9 -5
- package/skills/vercel/SKILL.md +2 -0
- package/skills/view-transitions/SKILL.md +9 -4
- package/src/browser/event-controller.ts +69 -25
- package/src/browser/link-interceptor.ts +2 -2
- package/src/browser/navigation-bridge.ts +75 -35
- package/src/browser/navigation-store.ts +26 -0
- package/src/browser/navigation-transaction.ts +9 -2
- package/src/browser/network-error-handler.ts +2 -0
- package/src/browser/partial-update.ts +48 -12
- package/src/browser/prefetch/default-strategy.ts +22 -42
- package/src/browser/react/Link.tsx +37 -42
- package/src/browser/react/NavigationProvider.tsx +63 -3
- package/src/browser/react/context.ts +17 -0
- package/src/browser/react/location-state.ts +5 -19
- package/src/browser/react/use-action.ts +4 -1
- package/src/browser/react/use-handle.ts +4 -20
- package/src/browser/react/use-link-status.ts +4 -3
- package/src/browser/react/use-navigation.ts +4 -1
- package/src/browser/react/use-params.ts +4 -1
- package/src/browser/react/use-pathname.ts +4 -1
- package/src/browser/react/use-search-params.ts +5 -3
- package/src/browser/react/use-segments.ts +36 -54
- package/src/browser/rsc-router.tsx +79 -39
- package/src/browser/segment-reconciler.ts +3 -3
- package/src/browser/server-action-bridge.ts +26 -6
- package/src/browser/types.ts +11 -0
- package/src/build/generate-manifest.ts +17 -0
- package/src/build/merge-full-manifests.ts +5 -0
- package/src/build/route-trie.ts +1 -0
- package/src/cache/background-task.ts +61 -0
- package/src/cache/cache-key-utils.ts +91 -12
- package/src/cache/cache-policy.ts +11 -2
- package/src/cache/cache-runtime.ts +23 -23
- package/src/cache/cache-scope.ts +38 -36
- package/src/cache/cf/cf-cache-store.ts +16 -11
- package/src/cache/document-cache.ts +35 -14
- package/src/cache/handle-snapshot.ts +50 -0
- package/src/cache/index.ts +1 -0
- package/src/cache/invalidation-order.ts +53 -0
- package/src/cache/memory-segment-store.ts +52 -1
- package/src/cache/shell-snapshot.ts +17 -0
- package/src/cache/store-scope.ts +36 -0
- package/src/cache/tag-invalidation.ts +7 -5
- package/src/cache/types.ts +69 -14
- package/src/cache/vercel/vercel-cache-store.ts +20 -6
- package/src/client.tsx +40 -3
- package/src/errors.ts +13 -6
- package/src/handles/deferred-resolution.ts +7 -2
- package/src/internal-suspense-audit.ts +12 -0
- package/src/loader-store.ts +4 -9
- package/src/outlet-provider.tsx +25 -1
- package/src/prerender/cloudflare.ts +130 -0
- package/src/prerender/create-prerender-trigger.ts +780 -0
- package/src/prerender/index.ts +39 -0
- package/src/prerender/memory-prerender-store.ts +115 -0
- package/src/prerender/on-demand.ts +339 -0
- package/src/prerender/producer-guard.ts +66 -0
- package/src/prerender/shell-manifest-key.ts +12 -17
- package/src/prerender/warm-request.ts +154 -0
- package/src/prerender/warm.ts +193 -0
- package/src/prerender/writable-store.ts +278 -0
- package/src/prerender.ts +29 -0
- package/src/reverse.ts +1 -1
- package/src/route-content-wrapper.tsx +107 -13
- package/src/router/error-handling.ts +12 -12
- package/src/router/find-match.ts +1 -0
- package/src/router/handler-context.ts +2 -0
- package/src/router/intercept-resolution.ts +15 -8
- package/src/router/loader-resolution.ts +2 -10
- package/src/router/match-middleware/background-revalidation.ts +2 -0
- package/src/router/match-middleware/cache-lookup.ts +230 -38
- package/src/router/pattern-matching.ts +14 -0
- package/src/router/prefetch-default.ts +4 -4
- package/src/router/prerender-match.ts +460 -352
- package/src/router/route-trie-builder.ts +9 -0
- package/src/router/router-context.ts +1 -0
- package/src/router/router-interfaces.ts +33 -0
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +20 -19
- package/src/router/segment-resolution/loader-cache.ts +15 -17
- package/src/router/segment-resolution/revalidation.ts +34 -24
- package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
- package/src/router/trie-matching.ts +3 -0
- package/src/router.ts +107 -1
- package/src/rsc/handler.ts +51 -4
- package/src/rsc/progressive-enhancement.ts +5 -1
- package/src/rsc/response-cache-serve.ts +10 -4
- package/src/rsc/rsc-rendering.ts +53 -11
- package/src/rsc/shell-build-manifest.ts +10 -8
- package/src/rsc/shell-capture-constants.ts +39 -7
- package/src/rsc/shell-capture.ts +108 -19
- package/src/rsc/shell-serve.ts +12 -6
- package/src/rsc/ssr-module-loader.ts +26 -0
- package/src/rsc/types.ts +8 -0
- package/src/segment-boundary-content.ts +25 -0
- package/src/segment-loader-promise.ts +28 -38
- package/src/segment-system.tsx +117 -57
- package/src/server/build-version-table.ts +15 -0
- package/src/server/context.ts +40 -17
- package/src/server/cookie-store.ts +5 -0
- package/src/server/handle-store.ts +13 -0
- package/src/server/loader-registry.ts +6 -1
- package/src/server/request-context.ts +87 -1
- package/src/ssr/ssr-root.tsx +20 -13
- package/src/suspense-audit-react.tsx +98 -0
- package/src/suspense-audit.ts +1030 -0
- package/src/testing/action-form.ts +66 -0
- package/src/testing/flight-runtime.d.ts +27 -0
- package/src/testing/flight.entry.ts +3 -0
- package/src/testing/index.ts +19 -0
- package/src/testing/internal/server-modules.ts +32 -0
- package/src/testing/render-route.tsx +25 -12
- package/src/testing/serve-shell-request.ts +190 -34
- package/src/testing/shell-status.ts +17 -6
- package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
- package/src/thenable-status.ts +10 -0
- package/src/types/handler-context.ts +7 -0
- package/src/types/route-entry.ts +6 -0
- package/src/urls/path-helper.ts +69 -9
- package/src/use-loader.tsx +57 -30
- package/src/vite/discovery/bundle-postprocess.ts +4 -0
- package/src/vite/discovery/discover-routers.ts +15 -0
- package/src/vite/discovery/shell-prerender-phase.ts +5 -5
- package/src/vite/discovery/state.ts +17 -1
- package/src/vite/inject-client-debug.ts +42 -20
- package/src/vite/plugins/virtual-entries.ts +8 -1
- package/src/vite/router-discovery.ts +14 -0
- package/src/vite/utils/bundle-analysis.ts +39 -7
- package/dist/types/browser/react/subscribe-to-nothing.d.ts +0 -7
- package/dist/types/segment-content-promise.d.ts +0 -13
- package/src/browser/react/subscribe-to-nothing.ts +0 -7
- package/src/segment-content-promise.ts +0 -67
package/src/router.ts
CHANGED
|
@@ -14,6 +14,14 @@ import {
|
|
|
14
14
|
isValidDevDiscoveryEpoch,
|
|
15
15
|
} from "./dev-discovery-protocol.js";
|
|
16
16
|
import { createReverse, type ReverseFunction } from "./reverse.js";
|
|
17
|
+
import {
|
|
18
|
+
resolvePrerenderVersion,
|
|
19
|
+
resolveRouterVersions,
|
|
20
|
+
} from "./server/build-version-table.js";
|
|
21
|
+
import { isDevEnvironment } from "./errors.js";
|
|
22
|
+
import { isViteDevServer } from "./cache/store-scope.js";
|
|
23
|
+
import { createPrerenderTrigger } from "./prerender/create-prerender-trigger.js";
|
|
24
|
+
import { createMemoryPrerenderStore } from "./prerender/memory-prerender-store.js";
|
|
17
25
|
import {
|
|
18
26
|
registerRouteMap,
|
|
19
27
|
getRouterManifest,
|
|
@@ -155,6 +163,7 @@ export function createRouter<TEnv = any>(
|
|
|
155
163
|
notFound,
|
|
156
164
|
onError,
|
|
157
165
|
cache,
|
|
166
|
+
prerender: prerenderConfigOption,
|
|
158
167
|
cacheProfiles: cacheProfilesOption,
|
|
159
168
|
theme: themeOption,
|
|
160
169
|
urls: urlsOption,
|
|
@@ -181,6 +190,16 @@ export function createRouter<TEnv = any>(
|
|
|
181
190
|
strictMode: strictModeOption = true,
|
|
182
191
|
} = options;
|
|
183
192
|
|
|
193
|
+
const defaultDevPrerenderStore = prerenderConfigOption
|
|
194
|
+
? undefined
|
|
195
|
+
: createMemoryPrerenderStore();
|
|
196
|
+
const effectivePrerenderConfigOption =
|
|
197
|
+
prerenderConfigOption ??
|
|
198
|
+
((() =>
|
|
199
|
+
isViteDevServer() && defaultDevPrerenderStore
|
|
200
|
+
? { store: defaultDevPrerenderStore }
|
|
201
|
+
: undefined) as RangoOptions<TEnv>["prerender"]);
|
|
202
|
+
|
|
184
203
|
// Debug cache signal gate (DEVELOPMENT/TEST ONLY). Enabled by the
|
|
185
204
|
// debugCacheSignal option OR the RANGO_TEST_SIGNALS=1 env flag. When off,
|
|
186
205
|
// no X-Rango-Cache header is emitted and output is byte-identical.
|
|
@@ -744,6 +763,77 @@ export function createRouter<TEnv = any>(
|
|
|
744
763
|
);
|
|
745
764
|
}
|
|
746
765
|
|
|
766
|
+
// Shared reverse function (also the value behind router.reverse).
|
|
767
|
+
const reverseFn = createReverse(mergedRouteMap);
|
|
768
|
+
const reverseLoose = reverseFn as unknown as (
|
|
769
|
+
name: string,
|
|
770
|
+
params?: Record<string, string>,
|
|
771
|
+
) => string;
|
|
772
|
+
|
|
773
|
+
// On-demand prerender binder (router.prerender({ env, ctx }) -> runner with .many / .markStale).
|
|
774
|
+
// Requestless: runProducer runs matchForPrerender with onDemand=true (arming
|
|
775
|
+
// the personalization guard); the store/config resolve per call from {env,ctx}.
|
|
776
|
+
// The key version is the router's data version, resolved by the same call the
|
|
777
|
+
// handler's overlay read makes (resolvePrerenderVersion), so the keys agree.
|
|
778
|
+
const prerenderTrigger = createPrerenderTrigger<TEnv, {}>({
|
|
779
|
+
routerId,
|
|
780
|
+
resolveVersion: () => resolvePrerenderVersion(routerId, version),
|
|
781
|
+
isDev: isDevEnvironment,
|
|
782
|
+
isViteDevServer,
|
|
783
|
+
ensureManifest: () => ensureRouterManifest(routerId),
|
|
784
|
+
resolveConfig: (env, ctx) => {
|
|
785
|
+
const opt = effectivePrerenderConfigOption;
|
|
786
|
+
if (!opt) return undefined;
|
|
787
|
+
return typeof opt === "function" ? opt(env, ctx) : opt;
|
|
788
|
+
},
|
|
789
|
+
reverse: (route, params) => {
|
|
790
|
+
try {
|
|
791
|
+
return reverseLoose(route, params);
|
|
792
|
+
} catch {
|
|
793
|
+
return undefined;
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
matchRoute: async (pathname) => {
|
|
797
|
+
const m = await findMatch(pathname);
|
|
798
|
+
if (!m) return null;
|
|
799
|
+
return {
|
|
800
|
+
routeName: m.routeKey,
|
|
801
|
+
params: m.params,
|
|
802
|
+
isOnDemand: m.od === true,
|
|
803
|
+
isPassthrough: m.pt === true,
|
|
804
|
+
};
|
|
805
|
+
},
|
|
806
|
+
runProducer: ({ pathname, isPassthrough, env, dev }) =>
|
|
807
|
+
_matchForPrerender<TEnv>(
|
|
808
|
+
pathname,
|
|
809
|
+
{},
|
|
810
|
+
{
|
|
811
|
+
...prerenderDeps,
|
|
812
|
+
versions: resolveRouterVersions(routerId, version),
|
|
813
|
+
},
|
|
814
|
+
undefined,
|
|
815
|
+
isPassthrough,
|
|
816
|
+
env,
|
|
817
|
+
dev,
|
|
818
|
+
true,
|
|
819
|
+
),
|
|
820
|
+
// A warm goes through router.fetch's handler: the one createRouter's own
|
|
821
|
+
// cache, nonce and version configure. An entry that passes them to
|
|
822
|
+
// createRSCHandler itself is not what a warm runs.
|
|
823
|
+
resolveCacheConfig: (env, ctx) =>
|
|
824
|
+
typeof cache === "function" ? cache(env, ctx) : cache,
|
|
825
|
+
// Called from inside a request, the warm leaves that request's scopes:
|
|
826
|
+
// it runs in the context the request entered the handler with
|
|
827
|
+
// (RequestContext._runAtRequestEntry).
|
|
828
|
+
fetch: (request, input) => {
|
|
829
|
+
const atRequestEntry = _getRequestContext()?._runAtRequestEntry;
|
|
830
|
+
return atRequestEntry
|
|
831
|
+
? atRequestEntry(() => router.fetch(request, input))
|
|
832
|
+
: router.fetch(request, input);
|
|
833
|
+
},
|
|
834
|
+
ambientOrigin: () => _getRequestContext()?.url.origin,
|
|
835
|
+
});
|
|
836
|
+
|
|
747
837
|
// Create match handler functions bound to router state
|
|
748
838
|
const matchHandlers = createMatchHandlers<TEnv>({
|
|
749
839
|
buildRouterContext,
|
|
@@ -866,6 +956,7 @@ export function createRouter<TEnv = any>(
|
|
|
866
956
|
// Collect route keys that have prerender handlers (for non-trie match path)
|
|
867
957
|
let prerenderRouteKeys: Set<string> | undefined;
|
|
868
958
|
let passthroughRouteKeys: Set<string> | undefined;
|
|
959
|
+
let onDemandRouteKeys: Set<string> | undefined;
|
|
869
960
|
for (const [name, entry] of manifest.entries()) {
|
|
870
961
|
if (entry.type === "route" && entry.isPrerender) {
|
|
871
962
|
if (!prerenderRouteKeys) prerenderRouteKeys = new Set();
|
|
@@ -874,6 +965,10 @@ export function createRouter<TEnv = any>(
|
|
|
874
965
|
if (!passthroughRouteKeys) passthroughRouteKeys = new Set();
|
|
875
966
|
passthroughRouteKeys.add(name);
|
|
876
967
|
}
|
|
968
|
+
if (entry.isOnDemand === true) {
|
|
969
|
+
if (!onDemandRouteKeys) onDemandRouteKeys = new Set();
|
|
970
|
+
onDemandRouteKeys.add(name);
|
|
971
|
+
}
|
|
877
972
|
}
|
|
878
973
|
}
|
|
879
974
|
|
|
@@ -900,6 +995,7 @@ export function createRouter<TEnv = any>(
|
|
|
900
995
|
cacheProfiles: resolvedCacheProfiles,
|
|
901
996
|
...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
|
|
902
997
|
...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
|
|
998
|
+
...(onDemandRouteKeys ? { onDemandRouteKeys } : {}),
|
|
903
999
|
});
|
|
904
1000
|
}
|
|
905
1001
|
} else {
|
|
@@ -920,6 +1016,7 @@ export function createRouter<TEnv = any>(
|
|
|
920
1016
|
cacheProfiles: resolvedCacheProfiles,
|
|
921
1017
|
...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
|
|
922
1018
|
...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
|
|
1019
|
+
...(onDemandRouteKeys ? { onDemandRouteKeys } : {}),
|
|
923
1020
|
});
|
|
924
1021
|
}
|
|
925
1022
|
|
|
@@ -1020,7 +1117,7 @@ export function createRouter<TEnv = any>(
|
|
|
1020
1117
|
// Type-safe URL builder using merged route map
|
|
1021
1118
|
// Types are tracked through the builder chain via TRoutes parameter
|
|
1022
1119
|
// Seeded with static route names from the generated file (injected by Vite)
|
|
1023
|
-
reverse:
|
|
1120
|
+
reverse: reverseFn,
|
|
1024
1121
|
|
|
1025
1122
|
// Expose accumulated route map for typeof extraction
|
|
1026
1123
|
// Returns {} initially, but builder chain accumulates specific route types
|
|
@@ -1037,6 +1134,15 @@ export function createRouter<TEnv = any>(
|
|
|
1037
1134
|
// Expose cache configuration for RSC handler
|
|
1038
1135
|
cache,
|
|
1039
1136
|
|
|
1137
|
+
// Expose the prerender store config (durable overlay) for the RSC handler to
|
|
1138
|
+
// resolve per request. Stored under _prerenderConfig because `prerender` on
|
|
1139
|
+
// the instance is the trigger method (router.prerender()).
|
|
1140
|
+
_prerenderConfig: effectivePrerenderConfigOption,
|
|
1141
|
+
|
|
1142
|
+
// On-demand prerender trigger: router.prerender({ env, ctx })(target),
|
|
1143
|
+
// plus .many() and .markStale() on the runner.
|
|
1144
|
+
prerender: prerenderTrigger,
|
|
1145
|
+
|
|
1040
1146
|
// Expose notFound component for RSC handler
|
|
1041
1147
|
notFound,
|
|
1042
1148
|
|
package/src/rsc/handler.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
getRequestContext,
|
|
16
16
|
_getRequestContext,
|
|
17
17
|
createRequestContext,
|
|
18
|
+
captureRequestEntryContext,
|
|
18
19
|
} from "../server/request-context.js";
|
|
19
20
|
import * as rscDeps from "@vitejs/plugin-rsc/rsc/server";
|
|
20
21
|
import type {
|
|
@@ -46,7 +47,10 @@ import {
|
|
|
46
47
|
type ResponseRouteMatch,
|
|
47
48
|
} from "./response-route-handler.js";
|
|
48
49
|
import { nonce as nonceToken, resolveProviderNonce } from "./nonce.js";
|
|
49
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
resolvePrerenderVersion,
|
|
52
|
+
resolveRouterVersions,
|
|
53
|
+
} from "../server/build-version-table.js";
|
|
50
54
|
import type { ErrorPhase } from "../types.js";
|
|
51
55
|
import type { RouterRequestInput } from "../router/router-interfaces.js";
|
|
52
56
|
import {
|
|
@@ -86,6 +90,8 @@ import {
|
|
|
86
90
|
} from "./origin-guard.js";
|
|
87
91
|
import { handleRscRendering } from "./rsc-rendering.js";
|
|
88
92
|
import { withoutShellMissMarker } from "./shell-serve.js";
|
|
93
|
+
import { readWarmMark } from "../prerender/warm-request.js";
|
|
94
|
+
import { defaultSSRModuleLoaderForTests } from "./ssr-module-loader.js";
|
|
89
95
|
import {
|
|
90
96
|
withTimeout,
|
|
91
97
|
isTimeoutEnabled,
|
|
@@ -205,7 +211,13 @@ export function createRSCHandler<
|
|
|
205
211
|
// In dev mode Vite may hot-reload the module, so skip memoization.
|
|
206
212
|
const rawLoadSSRModule: LoadSSRModule =
|
|
207
213
|
options.loadSSRModule ??
|
|
208
|
-
(() =>
|
|
214
|
+
(() =>
|
|
215
|
+
// The test seam (ssr-module-loader.ts). Gated on the build mode, not
|
|
216
|
+
// NODE_ENV: the rsc Vitest project forces NODE_ENV=production, its mode
|
|
217
|
+
// is "test", and a production build folds the whole branch away.
|
|
218
|
+
((import.meta as any).env?.MODE !== "production" &&
|
|
219
|
+
defaultSSRModuleLoaderForTests()?.()) ||
|
|
220
|
+
import.meta.viteRsc.loadModule("ssr", "index"));
|
|
209
221
|
let _ssrModulePromise: Promise<SSRModule> | undefined;
|
|
210
222
|
const loadSSRModule: LoadSSRModule =
|
|
211
223
|
process.env.NODE_ENV === "production"
|
|
@@ -452,12 +464,17 @@ export function createRSCHandler<
|
|
|
452
464
|
input: RouterRequestInput<TEnv> = {},
|
|
453
465
|
): Promise<Response> {
|
|
454
466
|
const handlerStart = performance.now();
|
|
467
|
+
// Before any router scope: see captureRequestEntryContext.
|
|
468
|
+
const runAtRequestEntry = captureRequestEntryContext();
|
|
455
469
|
// A degraded PPR HIT's reload carries the forced-MISS marker: keep only
|
|
456
470
|
// the flag (set on the request context below) and drop the marker before
|
|
457
471
|
// anything reads the request, so middleware, handlers, loaders, cache
|
|
458
472
|
// keys and the SSR search seed see the URL the visitor asked for.
|
|
459
473
|
const unmarkedRequest = withoutShellMissMarker(incomingRequest);
|
|
460
474
|
const request = unmarkedRequest ?? incomingRequest;
|
|
475
|
+
// A router.prerender() warm: the mark is the incoming Request object
|
|
476
|
+
// itself (prerender/warm-request.ts), read before anything rebuilds it.
|
|
477
|
+
const warm = readWarmMark(incomingRequest);
|
|
461
478
|
// Create the metrics store at handler start so handler:total has startTime=0
|
|
462
479
|
// and all metrics are relative to the request entry point.
|
|
463
480
|
const earlyMetricsStore = router.debugPerformance
|
|
@@ -509,10 +526,12 @@ export function createRSCHandler<
|
|
|
509
526
|
let searchParamsFilter: SearchParamsFilter | undefined;
|
|
510
527
|
const cacheOption = options.cache ?? router.cache;
|
|
511
528
|
if (cacheOption && !url.searchParams.has("__no_cache")) {
|
|
529
|
+
// A warm writes to the store the trigger checked the scope of.
|
|
512
530
|
const cacheConfig =
|
|
513
|
-
|
|
531
|
+
warm?.cacheConfig ??
|
|
532
|
+
(typeof cacheOption === "function"
|
|
514
533
|
? cacheOption(env, executionCtx)
|
|
515
|
-
: cacheOption;
|
|
534
|
+
: cacheOption);
|
|
516
535
|
|
|
517
536
|
if (cacheConfig.enabled !== false) {
|
|
518
537
|
cacheStore = cacheConfig.store;
|
|
@@ -522,6 +541,28 @@ export function createRSCHandler<
|
|
|
522
541
|
}
|
|
523
542
|
}
|
|
524
543
|
|
|
544
|
+
// Resolve the writable prerender store (durable overlay) for the serve path.
|
|
545
|
+
// Same factory-or-object shape as cache, resolved per request from env/ctx
|
|
546
|
+
// (never memoized). The version resolves per request, as router.prerender()
|
|
547
|
+
// resolves it per refresh, so both sides key off the same table state.
|
|
548
|
+
let resolvedPrerender:
|
|
549
|
+
| import("../prerender/on-demand.js").ResolvedPrerender<any>
|
|
550
|
+
| undefined;
|
|
551
|
+
const prerenderOption = router._prerenderConfig;
|
|
552
|
+
if (prerenderOption) {
|
|
553
|
+
const prerenderConfig =
|
|
554
|
+
typeof prerenderOption === "function"
|
|
555
|
+
? prerenderOption(env, executionCtx)
|
|
556
|
+
: prerenderOption;
|
|
557
|
+
if (prerenderConfig?.store) {
|
|
558
|
+
resolvedPrerender = {
|
|
559
|
+
config: prerenderConfig,
|
|
560
|
+
routerId: router.id,
|
|
561
|
+
version: resolvePrerenderVersion(router.id, router.version),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
525
566
|
// Route manifest is populated at startup via the virtual module
|
|
526
567
|
// (virtual:rsc-router/routes-manifest). In build/production, it's inlined
|
|
527
568
|
// into the bundle. In dev mode (Node), the discovery plugin populates it
|
|
@@ -576,6 +617,7 @@ export function createRSCHandler<
|
|
|
576
617
|
url,
|
|
577
618
|
variables,
|
|
578
619
|
cacheStore,
|
|
620
|
+
prerender: resolvedPrerender,
|
|
579
621
|
searchParamsFilter,
|
|
580
622
|
explicitTaggedStores,
|
|
581
623
|
cacheProfiles: router.cacheProfiles,
|
|
@@ -585,6 +627,11 @@ export function createRSCHandler<
|
|
|
585
627
|
versions,
|
|
586
628
|
});
|
|
587
629
|
if (unmarkedRequest) requestContext._shellForcedMiss = true;
|
|
630
|
+
requestContext._runAtRequestEntry = runAtRequestEntry;
|
|
631
|
+
if (warm) {
|
|
632
|
+
requestContext._prerenderWarm = warm;
|
|
633
|
+
requestContext._renderErrors = warm.renderErrors;
|
|
634
|
+
}
|
|
588
635
|
// Gate on the SAME enabled-semantics withTimeout uses (isTimeoutEnabled):
|
|
589
636
|
// a `renderStartMs: 0` / negative opt-out disables the timeout, so the
|
|
590
637
|
// driver's cursor bookkeeping (which only the timeout reads) must be off too.
|
|
@@ -438,6 +438,11 @@ function renderPeResponse<TEnv>(
|
|
|
438
438
|
strictMode: ctx.router.strictMode,
|
|
439
439
|
initialTheme: payloadInitialTheme(getRequestContext()),
|
|
440
440
|
},
|
|
441
|
+
// The same decoded value the SSR render receives, never a second decode:
|
|
442
|
+
// hydrateRoot needs it on the client or useActionState resets. A promise,
|
|
443
|
+
// so a state Flight cannot serialize (a class instance) rejects only this
|
|
444
|
+
// slot instead of failing the payload root and the whole document.
|
|
445
|
+
...(reactFormState ? { formState: Promise.resolve(reactFormState) } : {}),
|
|
441
446
|
};
|
|
442
447
|
|
|
443
448
|
const trace = getRequestContext()._activeRoutine;
|
|
@@ -470,7 +475,6 @@ function renderPeResponse<TEnv>(
|
|
|
470
475
|
{
|
|
471
476
|
// metricsStore=undefined is safe: the handler already stashed the early
|
|
472
477
|
// SSR setup promise, so this reuses it instead of starting setup again.
|
|
473
|
-
// reactFormState travels through the SSR option, not RscPayload.
|
|
474
478
|
html: createSsrHtmlStage({
|
|
475
479
|
ctx,
|
|
476
480
|
request,
|
|
@@ -33,9 +33,11 @@ import {
|
|
|
33
33
|
finalizeResponse,
|
|
34
34
|
} from "./helpers.js";
|
|
35
35
|
import { reportCacheError } from "../cache/cache-error.js";
|
|
36
|
-
import {
|
|
36
|
+
import { requestKeyBase } from "../cache/cache-key-utils.js";
|
|
37
|
+
import { isWarmReplace, noteWarmWrite } from "../prerender/warm-request.js";
|
|
37
38
|
import {
|
|
38
39
|
executionStart,
|
|
40
|
+
markResponseStart,
|
|
39
41
|
predatesInvalidation,
|
|
40
42
|
type ExecutionStart,
|
|
41
43
|
} from "../cache/tag-invalidation.js";
|
|
@@ -104,7 +106,7 @@ export async function serveResponseRouteWithCache(
|
|
|
104
106
|
// Default key: response:{type}: + host-namespaced base (sorted search, reserved
|
|
105
107
|
// _rsc*/__* params excluded). Same composition as document/segment tiers so
|
|
106
108
|
// the host-namespacing and search-normalization rules cannot drift.
|
|
107
|
-
let cacheKey = `response:${responseType}:${
|
|
109
|
+
let cacheKey = `response:${responseType}:${requestKeyBase(reqCtx, url.host, url.pathname, url.searchParams, undefined, reqCtx._searchParamsFilter)}`;
|
|
108
110
|
|
|
109
111
|
// Keyed like the segment record (CacheScope.resolveKeyFrom): with key()
|
|
110
112
|
// results on the cache() chain (a full override, namespaced, #975) or an
|
|
@@ -180,15 +182,19 @@ export async function serveResponseRouteWithCache(
|
|
|
180
182
|
}
|
|
181
183
|
await store2.putResponse!(
|
|
182
184
|
cacheKey,
|
|
183
|
-
fresh,
|
|
185
|
+
markResponseStart(fresh, start),
|
|
184
186
|
cacheScope!.ttl,
|
|
185
187
|
cacheScope!.swr,
|
|
186
188
|
responseTags,
|
|
187
189
|
);
|
|
190
|
+
noteWarmWrite(reqCtx, "response");
|
|
188
191
|
};
|
|
189
192
|
|
|
190
193
|
try {
|
|
191
|
-
|
|
194
|
+
// A router.prerender() warm takes the miss branch and replaces the entry.
|
|
195
|
+
const cached = isWarmReplace(reqCtx)
|
|
196
|
+
? null
|
|
197
|
+
: await store.getResponse(cacheKey);
|
|
192
198
|
if (cached && canServeCached(cached.response)) {
|
|
193
199
|
if (!cached.shouldRevalidate) {
|
|
194
200
|
return applyPreHandlerCallbacks(cached.response);
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
buildShellLoaderSeed,
|
|
19
19
|
countSnapshotFamilies,
|
|
20
20
|
hasDocRecord,
|
|
21
|
+
shellPrerenderHandles,
|
|
21
22
|
} from "../cache/shell-snapshot.js";
|
|
22
23
|
import { appendMetric } from "../router/metrics.js";
|
|
23
24
|
import type { MetricsStore } from "../server/context.js";
|
|
@@ -93,6 +94,10 @@ import {
|
|
|
93
94
|
type DevShellLookup,
|
|
94
95
|
} from "./shell-build-manifest.js";
|
|
95
96
|
import { contextGet } from "../context-var.js";
|
|
97
|
+
import {
|
|
98
|
+
isWarmReplace,
|
|
99
|
+
noteWarmShellEvent,
|
|
100
|
+
} from "../prerender/warm-request.js";
|
|
96
101
|
import {
|
|
97
102
|
resolveSwrWindow,
|
|
98
103
|
staleShellRecaptureDue,
|
|
@@ -465,6 +470,13 @@ function* shellServePlan<TEnv>(
|
|
|
465
470
|
): RoutinePlan<ShellServeOutcome> {
|
|
466
471
|
const { ctx, request, env, url, isPartial, handleStore, nonce, reqCtx } =
|
|
467
472
|
input;
|
|
473
|
+
const manifestEntry = reqCtx._classifiedRoute?.manifestEntry;
|
|
474
|
+
|
|
475
|
+
// A router.prerender() warm (prerender/warm-request.ts) reports the shell of
|
|
476
|
+
// a ppr route: "not-eligible" stands for every gate below that passes, until
|
|
477
|
+
// a HIT or a capture event overwrites it.
|
|
478
|
+
const warm = reqCtx._prerenderWarm;
|
|
479
|
+
if (warm && resolvePprConfig(manifestEntry)) warm.shell ??= "not-eligible";
|
|
468
480
|
|
|
469
481
|
if (
|
|
470
482
|
isPartial ||
|
|
@@ -474,9 +486,11 @@ function* shellServePlan<TEnv>(
|
|
|
474
486
|
) {
|
|
475
487
|
return { kind: "pass" };
|
|
476
488
|
}
|
|
477
|
-
const manifestEntry = reqCtx._classifiedRoute?.manifestEntry;
|
|
478
489
|
const pprConfig = resolvePprConfig(manifestEntry);
|
|
479
490
|
if (!pprConfig) return { kind: "pass" };
|
|
491
|
+
// In replace mode the request is a MISS without reading a shell, and its
|
|
492
|
+
// capture is forced (ShellCaptureDescriptor.force).
|
|
493
|
+
const warmReplace = isWarmReplace(reqCtx);
|
|
480
494
|
// A degraded HIT's reload (shellReloadScript) renders like a cache miss:
|
|
481
495
|
// no shell read, no capture, so it can never degrade again. The handler
|
|
482
496
|
// stripped the marker from the request and left this flag.
|
|
@@ -487,7 +501,7 @@ function* shellServePlan<TEnv>(
|
|
|
487
501
|
// other visitor. Every step below, a HIT included, therefore runs without
|
|
488
502
|
// one.
|
|
489
503
|
const store = reqCtx._cacheStore;
|
|
490
|
-
const baseKey = buildShellKey(url, reqCtx._searchParamsFilter);
|
|
504
|
+
const baseKey = buildShellKey(ctx.router.id, url, reqCtx._searchParamsFilter);
|
|
491
505
|
if (activeRequestNonce(nonce, reqCtx) !== undefined) {
|
|
492
506
|
// Declared intent that cannot be honored deserves a diagnostic (unlike an
|
|
493
507
|
// undeclared route, which is silent): a ppr route gated off by an active
|
|
@@ -551,12 +565,16 @@ function* shellServePlan<TEnv>(
|
|
|
551
565
|
// A MISS schedules its capture after rendering: taken before the read, this
|
|
552
566
|
// lets that capture skip itself when another request's capture stored the
|
|
553
567
|
// shell in between (ShellCaptureDescriptor.storedSeqAtRead).
|
|
554
|
-
|
|
568
|
+
// A warm's capture is its own: one another request stored meanwhile does
|
|
569
|
+
// not cancel it, so it takes no sequence.
|
|
570
|
+
const storedSeqAtRead = warmReplace ? undefined : lastStoredCaptureSeq(key);
|
|
555
571
|
// SSR setup starts before route handling, so read the shell before joining it
|
|
556
572
|
// to overlap the remaining setup work with cache I/O.
|
|
557
|
-
const cached =
|
|
558
|
-
|
|
559
|
-
|
|
573
|
+
const cached = warmReplace
|
|
574
|
+
? null
|
|
575
|
+
: yield* step("shell-read", () =>
|
|
576
|
+
readShellEntry(store, key, reqCtx, pprConfig.tags),
|
|
577
|
+
);
|
|
560
578
|
|
|
561
579
|
// allReady (ssr.resolveStreaming) bypasses PPR entirely: buffering defeats
|
|
562
580
|
// streaming, so bots/SEO crawlers get one complete axis-1 document. These
|
|
@@ -579,6 +597,15 @@ function* shellServePlan<TEnv>(
|
|
|
579
597
|
pprConfig,
|
|
580
598
|
store,
|
|
581
599
|
);
|
|
600
|
+
if (warm) {
|
|
601
|
+
// The warm learns its capture's outcome from the capture's own events,
|
|
602
|
+
// ahead of the router's debugShellCapture sink.
|
|
603
|
+
const routerSink = descriptor.debugSink;
|
|
604
|
+
descriptor.debugSink = (event) => {
|
|
605
|
+
noteWarmShellEvent(warm, event);
|
|
606
|
+
routerSink?.(event);
|
|
607
|
+
};
|
|
608
|
+
}
|
|
582
609
|
// MISS (no entry, invalid reactVersion, a tombstone, or store read
|
|
583
610
|
// failure): axis 1 + a background capture scheduled once the response is
|
|
584
611
|
// known servable.
|
|
@@ -587,6 +614,10 @@ function* shellServePlan<TEnv>(
|
|
|
587
614
|
descriptor: { ...descriptor, storedSeqAtRead },
|
|
588
615
|
ssrModule,
|
|
589
616
|
};
|
|
617
|
+
// Neither the stored shell nor the build shell is read: a warm renders.
|
|
618
|
+
if (warmReplace) {
|
|
619
|
+
return { ...miss, descriptor: { ...descriptor, force: true } };
|
|
620
|
+
}
|
|
590
621
|
|
|
591
622
|
// A HIT replays the handler layer from the entry's doc record (`docKey`),
|
|
592
623
|
// or, for a Prerender route, from the prerender store. An entry that has
|
|
@@ -612,6 +643,9 @@ function* shellServePlan<TEnv>(
|
|
|
612
643
|
"[ShellServe] getShell",
|
|
613
644
|
);
|
|
614
645
|
} else {
|
|
646
|
+
// An onlyIfStale warm found a servable shell; a recapture scheduled
|
|
647
|
+
// below overwrites this through the capture's events.
|
|
648
|
+
if (warm) warm.shell = "fresh";
|
|
615
649
|
// Stale (SWR) hit: serve the stale shell now, recapture in the
|
|
616
650
|
// background (stampede-guarded + backoff inside scheduleShellCapture),
|
|
617
651
|
// at most once per SHELL_MIN_RECAPTURE_INTERVAL_MS of the shell's age.
|
|
@@ -680,17 +714,23 @@ function* shellServePlan<TEnv>(
|
|
|
680
714
|
// route warned. A search-bearing URL never reads one: nothing to probe.
|
|
681
715
|
const routeName = reqCtx._classifiedRoute?.routeKey;
|
|
682
716
|
if (
|
|
683
|
-
!partitionBuildShellCheckDone(url.pathname, routeName) &&
|
|
717
|
+
!partitionBuildShellCheckDone(ctx.router.id, url.pathname, routeName) &&
|
|
684
718
|
shellSearchSeed(url, reqCtx._searchParamsFilter) === ""
|
|
685
719
|
) {
|
|
686
720
|
const found = yield* step("build-shell-check", () =>
|
|
687
|
-
hasBuildShell(url.pathname, devShellLookup),
|
|
721
|
+
hasBuildShell(ctx.router.id, url.pathname, devShellLookup),
|
|
722
|
+
);
|
|
723
|
+
notePartitionBuildShellCheck(
|
|
724
|
+
ctx.router.id,
|
|
725
|
+
url.pathname,
|
|
726
|
+
routeName,
|
|
727
|
+
found,
|
|
688
728
|
);
|
|
689
|
-
notePartitionBuildShellCheck(url.pathname, routeName, found);
|
|
690
729
|
}
|
|
691
730
|
} else {
|
|
692
731
|
buildHit = yield* step("build-shell-lookup", () =>
|
|
693
732
|
lookupBuildShell(
|
|
733
|
+
ctx.router.id,
|
|
694
734
|
url,
|
|
695
735
|
ctx.version,
|
|
696
736
|
store,
|
|
@@ -705,6 +745,7 @@ function* shellServePlan<TEnv>(
|
|
|
705
745
|
? openShellDocumentMetered(reqCtx, { entry: buildHit.entry })
|
|
706
746
|
: null;
|
|
707
747
|
if (buildHit && buildDocument) {
|
|
748
|
+
if (warm) warm.shell = "fresh";
|
|
708
749
|
// Past ppr.ttl: still serve the baked entry, recapture upgrades it.
|
|
709
750
|
if (buildHit.stale) {
|
|
710
751
|
yield* handoff("shell-recapture", () =>
|
|
@@ -1398,7 +1439,7 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1398
1439
|
// The visitor's own partition (resolveShellPartition): replay never
|
|
1399
1440
|
// crosses into another partition's shell.
|
|
1400
1441
|
const key = await resolveRequestShellKey(
|
|
1401
|
-
buildShellKey(url, reqCtx._searchParamsFilter),
|
|
1442
|
+
buildShellKey(ctx.router.id, url, reqCtx._searchParamsFilter),
|
|
1402
1443
|
routeSnapshot,
|
|
1403
1444
|
store,
|
|
1404
1445
|
reqCtx,
|
|
@@ -1896,7 +1937,7 @@ function serveShellHit(
|
|
|
1896
1937
|
// fresh; a promise-free bake-lane loader is served from its pin
|
|
1897
1938
|
// (_shellLoaderSeed, loader-cache.ts). A Prerender route's tail takes
|
|
1898
1939
|
// the handler layer from the prerender store before this scope is
|
|
1899
|
-
// consulted
|
|
1940
|
+
// consulted, and the loader pushes from `prerenderHandles`.
|
|
1900
1941
|
seededCtx._shellImplicitCache = {
|
|
1901
1942
|
ttl: descriptor.ttl,
|
|
1902
1943
|
swr: descriptor.swr,
|
|
@@ -1904,6 +1945,7 @@ function serveShellHit(
|
|
|
1904
1945
|
keyPrefix: "doc",
|
|
1905
1946
|
docTail: true,
|
|
1906
1947
|
fixedDocKey: entry.docKey,
|
|
1948
|
+
prerenderHandles: shellPrerenderHandles(records),
|
|
1907
1949
|
};
|
|
1908
1950
|
// Fragment splice (issue #700): store hits in THIS tail emit their stored
|
|
1909
1951
|
// segment fragments verbatim (expanded by segment-fragments.ts). The flag
|
|
@@ -55,7 +55,7 @@ export interface BuildShellEntry {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
interface ShellManifestModule {
|
|
58
|
-
/** Manifest key (
|
|
58
|
+
/** Manifest key (router id + pathname, shell-manifest-key.ts) -> asset specifier. */
|
|
59
59
|
default: Record<string, string>;
|
|
60
60
|
loadShellAsset: (spec: string) => Promise<{ default: BuildShellEntry }>;
|
|
61
61
|
}
|
|
@@ -92,9 +92,7 @@ function loadManifest(): Promise<ShellManifestModule | null> {
|
|
|
92
92
|
* the document HIT decodes the prelude once per serve (openShellDocument),
|
|
93
93
|
* partial replay never does. `null` memoizes a failed verdict —
|
|
94
94
|
* deterministically invalid. Keyed by the asking router's document version as
|
|
95
|
-
* well as the spec: the
|
|
96
|
-
* so under a host router two routers with different versions can ask about one
|
|
97
|
-
* record.
|
|
95
|
+
* well as the spec: the verdict is that version's.
|
|
98
96
|
*/
|
|
99
97
|
const validatedSpecs = new Map<string, ValidatedBuildShellEntry | null>();
|
|
100
98
|
|
|
@@ -270,9 +268,11 @@ async function fetchDevShellEntry(
|
|
|
270
268
|
* request is search-less, like the entries lookupBuildShell serves): a
|
|
271
269
|
* manifest entry for the path in production, a Prerender route in dev. No
|
|
272
270
|
* asset load and no dev capture: it only decides whether a request that
|
|
273
|
-
* cannot read the build shell deserves a warning.
|
|
271
|
+
* cannot read the build shell deserves a warning. The manifest entry is the
|
|
272
|
+
* one staged for the router `routerId` (shell-manifest-key.ts).
|
|
274
273
|
*/
|
|
275
274
|
export async function hasBuildShell(
|
|
275
|
+
routerId: string,
|
|
276
276
|
pathname: string,
|
|
277
277
|
dev?: DevShellLookup,
|
|
278
278
|
): Promise<boolean> {
|
|
@@ -280,11 +280,12 @@ export async function hasBuildShell(
|
|
|
280
280
|
return dev?.isPrerenderRoute === true;
|
|
281
281
|
}
|
|
282
282
|
const mod = await loadManifest();
|
|
283
|
-
return mod?.default[buildShellManifestKey(pathname)] !== undefined;
|
|
283
|
+
return mod?.default[buildShellManifestKey(routerId, pathname)] !== undefined;
|
|
284
284
|
}
|
|
285
285
|
|
|
286
286
|
/**
|
|
287
287
|
* Look up the baked shell entry for a request, applying every serve gate:
|
|
288
|
+
* the serving router's own entry only (`routerId`, shell-manifest-key.ts),
|
|
288
289
|
* search-less requests only (the build captured the bare pathname; a
|
|
289
290
|
* search-bearing URL has its own shell identity owned by runtime capture),
|
|
290
291
|
* version validity, payload integrity, and tag-invalidation markers. Returns
|
|
@@ -297,6 +298,7 @@ export async function hasBuildShell(
|
|
|
297
298
|
* traffic the shell was prerendered for.
|
|
298
299
|
*/
|
|
299
300
|
export async function lookupBuildShell(
|
|
301
|
+
routerId: string,
|
|
300
302
|
url: URL,
|
|
301
303
|
buildVersion: string,
|
|
302
304
|
store: SegmentCacheStore,
|
|
@@ -314,7 +316,7 @@ export async function lookupBuildShell(
|
|
|
314
316
|
if (hasManifest) {
|
|
315
317
|
const mod = await loadManifest();
|
|
316
318
|
if (!mod) return null;
|
|
317
|
-
const spec = mod.default[buildShellManifestKey(url.pathname)];
|
|
319
|
+
const spec = mod.default[buildShellManifestKey(routerId, url.pathname)];
|
|
318
320
|
if (!spec) return null;
|
|
319
321
|
record = await validatedManifestRecord(mod, spec, buildVersion);
|
|
320
322
|
} else if (dev) {
|
|
@@ -341,7 +343,7 @@ export async function lookupBuildShell(
|
|
|
341
343
|
// diagnostic; the route keeps runtime-capture semantics.
|
|
342
344
|
warnOnce(
|
|
343
345
|
"build-shell-tag-check",
|
|
344
|
-
buildShellManifestKey(url.pathname),
|
|
346
|
+
buildShellManifestKey(routerId, url.pathname),
|
|
345
347
|
() =>
|
|
346
348
|
`[rango] Build-time shell for "${url.pathname}" carries cache tags, but ` +
|
|
347
349
|
"the app cache store cannot answer tag-invalidation history durably " +
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
* pulling the capture orchestration module or shell-serve.ts (React).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
routerKeyPrefix,
|
|
11
|
+
sortedSearchString,
|
|
12
|
+
} from "../cache/cache-key-utils.js";
|
|
10
13
|
import type { SearchParamsFilter } from "../cache/search-params-filter.js";
|
|
11
14
|
|
|
12
15
|
/**
|
|
@@ -85,13 +88,14 @@ export function shellSearchSeed(url: URL, filter?: SearchParamsFilter): string {
|
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
/**
|
|
88
|
-
* Shell cache key: host + pathname + sorted search + a `:shell`
|
|
89
|
-
* (so it can never collide with a document-cache key; the
|
|
90
|
-
* the shell family internally).
|
|
91
|
+
* Shell cache key: router id + host + pathname + sorted search + a `:shell`
|
|
92
|
+
* namespace suffix (so it can never collide with a document-cache key; the
|
|
93
|
+
* store further isolates the shell family internally).
|
|
91
94
|
*
|
|
92
95
|
* The key includes the request HOST: in a multi-tenant host-router deployment
|
|
93
96
|
* (one worker, one shared KV/runtime-cache store) a host-less key would serve
|
|
94
|
-
* tenant A's captured shell to tenant B's users.
|
|
97
|
+
* tenant A's captured shell to tenant B's users. It starts with the serving
|
|
98
|
+
* ROUTER for the same reason (cache-key-utils.ts, the router rule).
|
|
95
99
|
*
|
|
96
100
|
* `filter` is the request's compiled `cache.searchParams` config
|
|
97
101
|
* (ctx._searchParamsFilter): excluded params collapse onto one shell slot.
|
|
@@ -99,8 +103,12 @@ export function shellSearchSeed(url: URL, filter?: SearchParamsFilter): string {
|
|
|
99
103
|
* and lookup makes every shell request a permanent miss. The testing helper
|
|
100
104
|
* `shellCacheKey` (testing/shell-status.ts) builds its key with it too.
|
|
101
105
|
*/
|
|
102
|
-
export function buildShellKey(
|
|
103
|
-
|
|
106
|
+
export function buildShellKey(
|
|
107
|
+
routerId: string,
|
|
108
|
+
url: URL,
|
|
109
|
+
filter?: SearchParamsFilter,
|
|
110
|
+
): string {
|
|
111
|
+
return `${routerKeyPrefix(routerId)}${url.host}${url.pathname}${shellSearchSeed(url, filter)}:shell`;
|
|
104
112
|
}
|
|
105
113
|
|
|
106
114
|
/**
|
|
@@ -173,6 +181,30 @@ export const POST_QUIESCE_TASK_HOPS = 16;
|
|
|
173
181
|
*/
|
|
174
182
|
export const SHELL_CAPTURE_TASK_HARD_CAP_MS = 25_000;
|
|
175
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Why a capture attempt refused to store its shell
|
|
186
|
+
* (ShellCaptureDebugEvent.refusal, `caches.refusal` on a `router.prerender()`
|
|
187
|
+
* result): `identity` (a request-scoped read: cookies(), headers(), the
|
|
188
|
+
* theme, a `{ cache: false }` variable), `dynamic` (`ctx.dynamic()`),
|
|
189
|
+
* `loader` (a `{ ssr: false }` loader rejected, or settled with
|
|
190
|
+
* redirect()/notFound()), `no-record` (no doc record: the route's cache()
|
|
191
|
+
* refused the write, or the handler output failed), `handles` (a pushed
|
|
192
|
+
* handle value did not finish encoding), `size` (over the store's value
|
|
193
|
+
* limit), `record-expired` (the route cache() record the capture wrote ran
|
|
194
|
+
* out first), `invalidated` (a tag of the shell was invalidated after the
|
|
195
|
+
* capture started), `uncacheable` (the store cannot hold the entry).
|
|
196
|
+
*/
|
|
197
|
+
export type ShellCaptureRefusal =
|
|
198
|
+
| "identity"
|
|
199
|
+
| "dynamic"
|
|
200
|
+
| "loader"
|
|
201
|
+
| "no-record"
|
|
202
|
+
| "handles"
|
|
203
|
+
| "size"
|
|
204
|
+
| "record-expired"
|
|
205
|
+
| "invalidated"
|
|
206
|
+
| "uncacheable";
|
|
207
|
+
|
|
176
208
|
/**
|
|
177
209
|
* Shared PPR loader-lane hint, appended to every capture warning or error
|
|
178
210
|
* whose fix depends on the lane (shell-capture.ts warnings, the cookie-store.ts
|