@rangojs/router 0.21.0 → 0.22.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 +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 +179 -496
- package/package.json +12 -2
- 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/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
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public on-demand prerender surface (`@rangojs/router/prerender`).
|
|
3
|
+
*
|
|
4
|
+
* Store contracts, config/result/target types, and the in-memory store (usable
|
|
5
|
+
* as a durable overlay in single-process node/Vercel functions, and as a test
|
|
6
|
+
* fake). The Cloudflare KV adapter lives in `@rangojs/router/prerender/cloudflare`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { PrerenderError } from "./create-prerender-trigger.js";
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
createMemoryPrerenderStore,
|
|
13
|
+
type MemoryPrerenderStore,
|
|
14
|
+
type MemoryPrerenderStoreOptions,
|
|
15
|
+
} from "./memory-prerender-store.js";
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
serializePrerenderKey,
|
|
19
|
+
type PrerenderKey,
|
|
20
|
+
type PrerenderStoredEntry,
|
|
21
|
+
type WritablePrerenderStore,
|
|
22
|
+
} from "./writable-store.js";
|
|
23
|
+
|
|
24
|
+
export type {
|
|
25
|
+
OnDemandOption,
|
|
26
|
+
OnDemandRouteConfig,
|
|
27
|
+
PrerenderConfig,
|
|
28
|
+
PrerenderFn,
|
|
29
|
+
PrerenderResult,
|
|
30
|
+
PrerenderWarmCaches,
|
|
31
|
+
PrerenderManyOptions,
|
|
32
|
+
PrerenderRunner,
|
|
33
|
+
PrerenderRunOptions,
|
|
34
|
+
PrerenderRuntime,
|
|
35
|
+
PrerenderTarget,
|
|
36
|
+
PrerenderTargetObject,
|
|
37
|
+
} from "./on-demand.js";
|
|
38
|
+
|
|
39
|
+
export type { PrerenderEntry } from "./store.js";
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory writable prerender store.
|
|
3
|
+
*
|
|
4
|
+
* Backs the zero-config dev overlay and the `@rangojs/router/testing` fake.
|
|
5
|
+
* A plain Map — no per-isolate null memoization (a later set() must be visible
|
|
6
|
+
* after an earlier miss), no React/RSC deps.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
lowerStoredEntryStaleAt,
|
|
11
|
+
serializePrerenderKey,
|
|
12
|
+
type PrerenderKey,
|
|
13
|
+
type PrerenderStoredEntry,
|
|
14
|
+
type WritablePrerenderStore,
|
|
15
|
+
} from "./writable-store.js";
|
|
16
|
+
|
|
17
|
+
export interface MemoryPrerenderStore extends WritablePrerenderStore {
|
|
18
|
+
delete(key: PrerenderKey): Promise<void>;
|
|
19
|
+
markStale(routerId: string, tags: string[]): Promise<void>;
|
|
20
|
+
/** Synchronous read by structured key (tests). */
|
|
21
|
+
peek(key: PrerenderKey): PrerenderStoredEntry | null;
|
|
22
|
+
/** All stored [serializedKey, entry] pairs (tests). */
|
|
23
|
+
entries(): [string, PrerenderStoredEntry][];
|
|
24
|
+
/** Drop every entry. */
|
|
25
|
+
clear(): void;
|
|
26
|
+
readonly size: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MemoryPrerenderStoreOptions {
|
|
30
|
+
/** Injectable clock for `markStale` in deterministic tests. Defaults to Date.now. */
|
|
31
|
+
now?: () => number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create an in-memory {@link WritablePrerenderStore} with test-inspection
|
|
36
|
+
* helpers. Invalidation is mark-stale (sets `staleAt`), never delete — matching
|
|
37
|
+
* the durable v1 contract so the entry keeps serving until refreshed.
|
|
38
|
+
*/
|
|
39
|
+
export function createMemoryPrerenderStore(
|
|
40
|
+
options: MemoryPrerenderStoreOptions = {},
|
|
41
|
+
): MemoryPrerenderStore {
|
|
42
|
+
const now = options.now ?? (() => Date.now());
|
|
43
|
+
// The owning router per entry: the serialized key cannot be split back.
|
|
44
|
+
const map = new Map<
|
|
45
|
+
string,
|
|
46
|
+
{ routerId: string; stored: PrerenderStoredEntry }
|
|
47
|
+
>();
|
|
48
|
+
|
|
49
|
+
// routerId -> tag -> when markStale() last named it. A held entry is marked
|
|
50
|
+
// in place; this covers a write that was rendered before the mark and set
|
|
51
|
+
// after it.
|
|
52
|
+
const marks = new Map<string, Map<string, number>>();
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
async get(key: PrerenderKey): Promise<PrerenderStoredEntry | null> {
|
|
56
|
+
return map.get(serializePrerenderKey(key))?.stored ?? null;
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
async set(key: PrerenderKey, stored: PrerenderStoredEntry): Promise<void> {
|
|
60
|
+
// Strictly after storedAt, unlike the KV read (cloudflare.ts, `>=`): in
|
|
61
|
+
// one process a mark and a render start can share a millisecond with the
|
|
62
|
+
// mark first, which `>=` would read as invalidating that render. KV's
|
|
63
|
+
// marker write is I/O, so a same-millisecond marker there is ambiguous.
|
|
64
|
+
// Marks, never refuses: a stale entry keeps serving.
|
|
65
|
+
const routerMarks = marks.get(key.routerId);
|
|
66
|
+
if (routerMarks) {
|
|
67
|
+
for (const tag of stored.meta.tags) {
|
|
68
|
+
const at = routerMarks.get(tag);
|
|
69
|
+
if (at != null && at > stored.meta.storedAt) {
|
|
70
|
+
lowerStoredEntryStaleAt(stored, at);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
map.set(serializePrerenderKey(key), { routerId: key.routerId, stored });
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
async delete(key: PrerenderKey): Promise<void> {
|
|
78
|
+
map.delete(serializePrerenderKey(key));
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
async markStale(routerId: string, tags: string[]): Promise<void> {
|
|
82
|
+
if (tags.length === 0) return;
|
|
83
|
+
const tagSet = new Set(tags);
|
|
84
|
+
const at = now();
|
|
85
|
+
let routerMarks = marks.get(routerId);
|
|
86
|
+
if (!routerMarks) marks.set(routerId, (routerMarks = new Map()));
|
|
87
|
+
for (const tag of tags) routerMarks.set(tag, at);
|
|
88
|
+
for (const entry of map.values()) {
|
|
89
|
+
if (
|
|
90
|
+
entry.routerId === routerId &&
|
|
91
|
+
entry.stored.meta.tags.some((t) => tagSet.has(t))
|
|
92
|
+
) {
|
|
93
|
+
lowerStoredEntryStaleAt(entry.stored, at);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
peek(key: PrerenderKey): PrerenderStoredEntry | null {
|
|
99
|
+
return map.get(serializePrerenderKey(key))?.stored ?? null;
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
entries(): [string, PrerenderStoredEntry][] {
|
|
103
|
+
return [...map].map(([k, v]) => [k, v.stored]);
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
clear(): void {
|
|
107
|
+
map.clear();
|
|
108
|
+
marks.clear();
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
get size(): number {
|
|
112
|
+
return map.size;
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public type surface for on-demand (ISR-style) prerender: the router
|
|
3
|
+
* `prerender` config option, the `router.prerender({ env, ctx })` runner, its typed
|
|
4
|
+
* target, and the inspectable result union.
|
|
5
|
+
*
|
|
6
|
+
* Types only — safe to import from router options, the public interfaces, the
|
|
7
|
+
* testing barrel, and platform adapters without pulling RSC deps.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ExecutionContext } from "../types/request-scope.js";
|
|
11
|
+
import type { IsEmptyObject, ParamsFor } from "../reverse.js";
|
|
12
|
+
import type { WritablePrerenderStore } from "./writable-store.js";
|
|
13
|
+
import type { ShellCaptureRefusal } from "../rsc/shell-capture-constants.js";
|
|
14
|
+
|
|
15
|
+
declare const prerenderTargetBrand: unique symbol;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The target `onRevalidate` receives: a plain `{ route, params }` that goes
|
|
19
|
+
* straight into a queue message. The type-only brand (never present at
|
|
20
|
+
* runtime) lets the runner (`router.prerender({ env })(target)` / `.many()`) accepts it with no cast on a
|
|
21
|
+
* router typed with named routes, while a hand-written `{ route: "typo" }`
|
|
22
|
+
* stays a type error there. After a JSON round trip, assert it back:
|
|
23
|
+
* `JSON.parse(raw) as PrerenderTargetObject`.
|
|
24
|
+
*/
|
|
25
|
+
export interface PrerenderTargetObject {
|
|
26
|
+
route: string;
|
|
27
|
+
params: Record<string, string>;
|
|
28
|
+
readonly [prerenderTargetBrand]: true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Route map used to type the object target. When the fluent `TRoutes` phantom is
|
|
33
|
+
* empty (the common `createRouter().routes(...)` case leaves it `{}`), fall back
|
|
34
|
+
* to the generated global route map — the same fallback `Prerender<"name">` and
|
|
35
|
+
* `reverse` use — so `router.prerender({ route, params })` stays typed without an
|
|
36
|
+
* explicit route-type parameter.
|
|
37
|
+
*/
|
|
38
|
+
type PrerenderRouteMap<TRoutes> = keyof TRoutes extends never
|
|
39
|
+
? keyof Rango.GeneratedRouteMap extends never
|
|
40
|
+
? {}
|
|
41
|
+
: Rango.GeneratedRouteMap
|
|
42
|
+
: TRoutes;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Typed object target, derived from the router's route map the same way
|
|
46
|
+
* `reverse` is. No-param routes may omit `params`.
|
|
47
|
+
*/
|
|
48
|
+
type PrerenderRouteTarget<TRoutes> = {
|
|
49
|
+
[TName in keyof PrerenderRouteMap<TRoutes> & string]: IsEmptyObject<
|
|
50
|
+
ParamsFor<PrerenderRouteMap<TRoutes>, TName>
|
|
51
|
+
> extends true
|
|
52
|
+
? { route: TName; params?: Record<string, never> }
|
|
53
|
+
: { route: TName; params: ParamsFor<PrerenderRouteMap<TRoutes>, TName> };
|
|
54
|
+
}[keyof PrerenderRouteMap<TRoutes> & string];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A prerender target: a typed named-route object, a path-like string/URL
|
|
58
|
+
* (`router.reverse()` output composes for free), or the `onRevalidate` target.
|
|
59
|
+
*/
|
|
60
|
+
export type PrerenderTarget<TRoutes = {}> =
|
|
61
|
+
| string
|
|
62
|
+
| URL
|
|
63
|
+
| PrerenderRouteTarget<TRoutes>
|
|
64
|
+
| PrerenderTargetObject;
|
|
65
|
+
|
|
66
|
+
/** Env + platform capabilities bound once by `router.prerender({ env, ctx })`. */
|
|
67
|
+
export interface PrerenderRuntime<TEnv = any> {
|
|
68
|
+
/** The live env: the producer's `ctx.env` (not the build's `buildEnv`). */
|
|
69
|
+
env: TEnv;
|
|
70
|
+
/** Cloudflare `ExecutionContext`; absent on node/Vercel. */
|
|
71
|
+
ctx?: ExecutionContext;
|
|
72
|
+
/**
|
|
73
|
+
* The origin a warm requests a path or `{ route, params }` target on, for
|
|
74
|
+
* example `"https://shop.example"`. Cache keys carry the host, so a warm
|
|
75
|
+
* must request the host visitors use. A full-URL target uses its own
|
|
76
|
+
* origin; without this option the runner uses the origin of the request it
|
|
77
|
+
* is called from (an action, a route handler, `onRevalidate`), and with
|
|
78
|
+
* neither a warm returns `skipped-no-origin`. An on-demand route's
|
|
79
|
+
* requestless render never needs it.
|
|
80
|
+
*/
|
|
81
|
+
origin?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Per-call options of the bound runner. */
|
|
85
|
+
export interface PrerenderRunOptions {
|
|
86
|
+
/** Throw on failure instead of returning an `{ ok: false }` result. */
|
|
87
|
+
throwOnError?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Only refresh what is cold or stale (cron-sweep opt-in), and the only
|
|
90
|
+
* path that can return `already-fresh`. On an on-demand route the stored
|
|
91
|
+
* entry is rendered only when stale. On any other route the warm reads its
|
|
92
|
+
* caches normally instead of forcing misses, so a fresh entry is left
|
|
93
|
+
* alone, a stale one refreshes and a missing one is written.
|
|
94
|
+
*/
|
|
95
|
+
onlyIfStale?: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Per-call options of the bound runner's `.many()`. */
|
|
99
|
+
export interface PrerenderManyOptions extends PrerenderRunOptions {
|
|
100
|
+
/** Max concurrent renders in a batch (default 1; any invalid value is 1). */
|
|
101
|
+
concurrency?: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* What a warm request wrote, by cache. On a result of the warm path, and on
|
|
106
|
+
* an on-demand result whose route was also warmed.
|
|
107
|
+
*/
|
|
108
|
+
export interface PrerenderWarmCaches {
|
|
109
|
+
/**
|
|
110
|
+
* Store writes that landed, by store family: `record` (route `cache()`),
|
|
111
|
+
* `item` (`"use cache"` and loader `cache()`), `response` (the document
|
|
112
|
+
* cache and response routes), `shell` (the `ppr` shell).
|
|
113
|
+
*/
|
|
114
|
+
writes: { record: number; item: number; response: number; shell: number };
|
|
115
|
+
/**
|
|
116
|
+
* The `ppr` shell; absent on a route without `ppr`. `fresh` is
|
|
117
|
+
* `onlyIfStale` finding a servable shell; `not-eligible` is a request the
|
|
118
|
+
* shell path passed on (an active nonce, a store without shells, the
|
|
119
|
+
* route's own `cache()` opt-out, a buffered response).
|
|
120
|
+
*/
|
|
121
|
+
shell?:
|
|
122
|
+
| "stored"
|
|
123
|
+
| "fresh"
|
|
124
|
+
| "refused"
|
|
125
|
+
| "no-shell"
|
|
126
|
+
| "not-eligible"
|
|
127
|
+
| "skipped-capacity"
|
|
128
|
+
| "skipped-queue-timeout"
|
|
129
|
+
| "error";
|
|
130
|
+
/** Why the capture refused to store the shell. */
|
|
131
|
+
refusal?: ShellCaptureRefusal;
|
|
132
|
+
/** The document cache, when `createDocumentCacheMiddleware` ran. */
|
|
133
|
+
document?: "stored" | "not-cacheable";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Inspectable result, one per target. The trigger does not force callers
|
|
138
|
+
* into try/catch; `throwOnError: true` opts into throwing for admin/CI
|
|
139
|
+
* endpoints. `path` says what ran: `on-demand` is the requestless render of a
|
|
140
|
+
* `Prerender(..., { onDemand })` route into the prerender store, `warm` is a
|
|
141
|
+
* request through the router's handler that rewrites the route's runtime
|
|
142
|
+
* caches.
|
|
143
|
+
*/
|
|
144
|
+
export type PrerenderResult =
|
|
145
|
+
| {
|
|
146
|
+
ok: true;
|
|
147
|
+
path: "on-demand";
|
|
148
|
+
/**
|
|
149
|
+
* `already-fresh` only occurs with `onlyIfStale: true`.
|
|
150
|
+
*
|
|
151
|
+
* `removed`: a "removed" marker is stored in place of the page, and
|
|
152
|
+
* requests no longer get the page or its build-time entry. It is
|
|
153
|
+
* `ok: true`, so count it in sweep results. Written by
|
|
154
|
+
* `prerender.remove()` (permanent until a refresh without
|
|
155
|
+
* `onlyIfStale`; `tags` empty, no `ttl`), or by a refresh in which a
|
|
156
|
+
* `notFound()` was the only thing that went wrong, anywhere in the
|
|
157
|
+
* render: the route handler, a layout or parallel slot handler, any
|
|
158
|
+
* server component. That marker has the route's `tags` and `ttl` and is
|
|
159
|
+
* rechecked like a page. `onlyIfStale` finding a fresh marker reports
|
|
160
|
+
* `removed` too. When the data source may be failing, throw anything
|
|
161
|
+
* else: that is `render-failed` and keeps the page.
|
|
162
|
+
*/
|
|
163
|
+
status: "rendered" | "already-fresh" | "removed";
|
|
164
|
+
target: string;
|
|
165
|
+
routeName: string;
|
|
166
|
+
/** Opaque, for debugging/logs only — not a stable format. */
|
|
167
|
+
key: string;
|
|
168
|
+
tags: string[];
|
|
169
|
+
/** Absent = never stale. */
|
|
170
|
+
ttl?: number;
|
|
171
|
+
/**
|
|
172
|
+
* The warm request that followed the store write. Absent when the app
|
|
173
|
+
* store is not shared, no origin resolved, or nothing was rendered.
|
|
174
|
+
*/
|
|
175
|
+
caches?: PrerenderWarmCaches;
|
|
176
|
+
}
|
|
177
|
+
| {
|
|
178
|
+
ok: true;
|
|
179
|
+
path: "warm";
|
|
180
|
+
/** `already-fresh` only occurs with `onlyIfStale: true`. */
|
|
181
|
+
status: "warmed" | "already-fresh";
|
|
182
|
+
/** The absolute URL that was requested. */
|
|
183
|
+
target: string;
|
|
184
|
+
routeName: string;
|
|
185
|
+
/** The warm response's HTTP status. */
|
|
186
|
+
responseStatus: number;
|
|
187
|
+
caches: PrerenderWarmCaches;
|
|
188
|
+
}
|
|
189
|
+
| {
|
|
190
|
+
ok: false;
|
|
191
|
+
/** Absent when no route matched: every other result is past the match. */
|
|
192
|
+
path?: "on-demand" | "warm";
|
|
193
|
+
status:
|
|
194
|
+
| "no-match"
|
|
195
|
+
// No prerender store (on-demand), or no `createRouter({ cache })` (warm).
|
|
196
|
+
| "no-store"
|
|
197
|
+
| "skipped-personalized"
|
|
198
|
+
| "skipped-unsupported-target"
|
|
199
|
+
// The route (a Passthrough + onDemand route) returned ctx.passthrough()
|
|
200
|
+
// for this param set: no shared payload to persist. The "removed"
|
|
201
|
+
// marker is stored instead, so the live handler serves it and not a
|
|
202
|
+
// page stored or baked earlier.
|
|
203
|
+
| "skipped-passthrough"
|
|
204
|
+
// prerender.remove() on a route that is not on-demand: it has no page
|
|
205
|
+
// in the prerender store. (A refresh of such a route is a warm.)
|
|
206
|
+
| "skipped-not-on-demand"
|
|
207
|
+
// The app store's entries are not readable beyond the place the call
|
|
208
|
+
// runs (SegmentCacheStore.scope), so a warm would serve no one else.
|
|
209
|
+
| "skipped-store-not-shared"
|
|
210
|
+
// A path or { route, params } target with no origin to request it on.
|
|
211
|
+
| "skipped-no-origin"
|
|
212
|
+
// A ppr route whose shell was not stored (caches.shell says why).
|
|
213
|
+
| "shell-not-stored"
|
|
214
|
+
// The request rendered and no cache wrote anything.
|
|
215
|
+
| "skipped-uncached"
|
|
216
|
+
| "render-failed"
|
|
217
|
+
| "store-failed";
|
|
218
|
+
target: string;
|
|
219
|
+
routeName?: string;
|
|
220
|
+
/** The warm response's HTTP status, when a warm request ran. */
|
|
221
|
+
responseStatus?: number;
|
|
222
|
+
caches?: PrerenderWarmCaches;
|
|
223
|
+
error?: unknown;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Per-route on-demand opt-in, carried inside `PrerenderOptions.onDemand`. Any
|
|
228
|
+
* truthy spelling works (literal, spread, imported const): producer retention
|
|
229
|
+
* is driven by the evaluated route manifest, not a textual scan of the call.
|
|
230
|
+
*/
|
|
231
|
+
export interface OnDemandRouteConfig {
|
|
232
|
+
/** Soft TTL (seconds) for entries refreshed for this route. Overrides the router `ttl`. */
|
|
233
|
+
ttl?: number;
|
|
234
|
+
/**
|
|
235
|
+
* Tags stamped on the stored entry, addressable via
|
|
236
|
+
* the runner's `markStale()`. A separate namespace from `cacheTag()` /
|
|
237
|
+
* `updateTag()` / `revalidateTag()`, which never reach the prerender store.
|
|
238
|
+
*/
|
|
239
|
+
tags?: string[] | ((target: { params: Record<string, string> }) => string[]);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export type OnDemandOption = boolean | OnDemandRouteConfig;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Env-resolved prerender store configuration. Same union shape as the `cache`
|
|
246
|
+
* option: a plain object or a factory resolved per request/per call, never at
|
|
247
|
+
* `createRouter()` time.
|
|
248
|
+
*/
|
|
249
|
+
export interface PrerenderConfig<TEnv = any> {
|
|
250
|
+
store: WritablePrerenderStore;
|
|
251
|
+
/**
|
|
252
|
+
* Default soft TTL (seconds) for a route whose `onDemand` sets none. Absent
|
|
253
|
+
* = never stale. Soft: a stale entry keeps serving; staleness only decides
|
|
254
|
+
* whether `onRevalidate` is scheduled. Entries never expire.
|
|
255
|
+
*/
|
|
256
|
+
ttl?: number;
|
|
257
|
+
/**
|
|
258
|
+
* Stale-while-revalidate: its presence is the opt-in. Scheduled through
|
|
259
|
+
* `waitUntil` on a stale overlay hit (the stale entry still serves) with
|
|
260
|
+
* the JSON-serializable target and the live env. Runs at most once per
|
|
261
|
+
* stale key per isolate while one is in flight (one running longer than
|
|
262
|
+
* 15 s counts as finished, so a hung call cannot block the key), so
|
|
263
|
+
* `(target, env, ctx) => router.prerender({ env, ctx })(target, { onlyIfStale: true })`
|
|
264
|
+
* is safe in a single process; across isolates, point it at a queue, which
|
|
265
|
+
* owns dedup. Pass `onlyIfStale` in the call that runs later: a job queued
|
|
266
|
+
* for a page that `prerender.remove()` then removed finds the marker and
|
|
267
|
+
* renders nothing, where a plain refresh would bring the page back.
|
|
268
|
+
*/
|
|
269
|
+
onRevalidate?: (
|
|
270
|
+
target: PrerenderTargetObject,
|
|
271
|
+
env: TEnv,
|
|
272
|
+
/** The stale request's execution context; absent where none exists. */
|
|
273
|
+
ctx?: ExecutionContext,
|
|
274
|
+
) => void | Promise<void>;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @internal Per-request resolution of the prerender config plus the key
|
|
279
|
+
* version. Threaded onto the request context by the RSC handler and read by
|
|
280
|
+
* the serve-path overlay lookup.
|
|
281
|
+
*/
|
|
282
|
+
export interface ResolvedPrerender<TEnv = any> {
|
|
283
|
+
/** The resolved config; the store is `config.store`. */
|
|
284
|
+
config: PrerenderConfig<TEnv>;
|
|
285
|
+
routerId: string;
|
|
286
|
+
/** The router's cache version for overlay keys (resolvePrerenderVersion). */
|
|
287
|
+
version: string;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The runner `router.prerender({ env, ctx })` returns: callable per target,
|
|
292
|
+
* plus `.many` / `.remove` / `.markStale`. Typing rides the phantom `TRoutes`
|
|
293
|
+
* accumulator, like `reverse`.
|
|
294
|
+
*/
|
|
295
|
+
export interface PrerenderRunner<TRoutes = {}> {
|
|
296
|
+
(
|
|
297
|
+
target: PrerenderTarget<TRoutes>,
|
|
298
|
+
options?: PrerenderRunOptions,
|
|
299
|
+
): Promise<PrerenderResult>;
|
|
300
|
+
many(
|
|
301
|
+
targets: ReadonlyArray<PrerenderTarget<TRoutes>>,
|
|
302
|
+
options?: PrerenderManyOptions,
|
|
303
|
+
): Promise<PrerenderResult[]>;
|
|
304
|
+
/**
|
|
305
|
+
* Remove an on-demand route's page without rendering: a "removed" marker is
|
|
306
|
+
* stored in place of it. Requests then get a 404 (a `Passthrough` route: its
|
|
307
|
+
* live handler), never the build-time entry. Permanent: no `ttl`, no
|
|
308
|
+
* `onRevalidate`, left by `onlyIfStale`, until a `prerender(target)`
|
|
309
|
+
* without `onlyIfStale` renders the page again. It is a write like a
|
|
310
|
+
* refresh's, and the last write wins. `{ ok: true, status: "removed" }`; a
|
|
311
|
+
* route that is not on-demand is `skipped-not-on-demand`. `.many` is the
|
|
312
|
+
* batch form.
|
|
313
|
+
*/
|
|
314
|
+
remove: {
|
|
315
|
+
(
|
|
316
|
+
target: PrerenderTarget<TRoutes>,
|
|
317
|
+
options?: Pick<PrerenderRunOptions, "throwOnError">,
|
|
318
|
+
): Promise<PrerenderResult>;
|
|
319
|
+
many(
|
|
320
|
+
targets: ReadonlyArray<PrerenderTarget<TRoutes>>,
|
|
321
|
+
options?: Omit<PrerenderManyOptions, "onlyIfStale">,
|
|
322
|
+
): Promise<PrerenderResult[]>;
|
|
323
|
+
};
|
|
324
|
+
/**
|
|
325
|
+
* Mark this router's stored entries carrying one of `tags` stale. Marking only: the
|
|
326
|
+
* entries keep serving, and a stale hit schedules `onRevalidate` when one is
|
|
327
|
+
* configured. Nothing is deleted or re-rendered here.
|
|
328
|
+
*/
|
|
329
|
+
markStale(tags: string[]): Promise<void>;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* The `router.prerender` function: binds the runtime (`env`, `ctx`) and returns
|
|
334
|
+
* the {@link PrerenderRunner}. Binding does no work; the config factory is
|
|
335
|
+
* resolved per call.
|
|
336
|
+
*/
|
|
337
|
+
export type PrerenderFn<TEnv = any, TRoutes = {}> = (
|
|
338
|
+
runtime: PrerenderRuntime<TEnv>,
|
|
339
|
+
) => PrerenderRunner<TRoutes>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Requestless-prerender guard.
|
|
3
|
+
*
|
|
4
|
+
* The on-demand producer renders a route with only route params + env — never
|
|
5
|
+
* the triggering request's cookies, headers, geo, or session (see the design's
|
|
6
|
+
* Producer Semantics). Personalized reads during a producer render would bake
|
|
7
|
+
* one visitor's data into a payload shared with everyone for that route.
|
|
8
|
+
*
|
|
9
|
+
* The producer marks its own request context with `_onDemandProducer: true`. The
|
|
10
|
+
* standalone request-scoped helpers a handler reaches for — `cookies()`,
|
|
11
|
+
* `headers()`, `invalidateClientCache()`, `keepClientCache()` — call
|
|
12
|
+
* {@link assertNotInsidePrerenderProducer}, which throws
|
|
13
|
+
* {@link PrerenderPersonalizationError}; the producer catches it and maps to a
|
|
14
|
+
* `skipped-personalized` result, leaving any existing entry untouched. (The
|
|
15
|
+
* producer context's own `ctx.cookie`/`header`/etc. remain the pre-existing
|
|
16
|
+
* build-time no-op stubs over empty synthetic state — the guard covers the
|
|
17
|
+
* standalone helpers, which are the reads that would otherwise see real request
|
|
18
|
+
* data.)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Thrown when a producer render touches request-specific state. */
|
|
22
|
+
export class PrerenderPersonalizationError extends Error {
|
|
23
|
+
/** Cross-realm-safe brand (instanceof can fail across bundle/realm boundaries). */
|
|
24
|
+
readonly isPrerenderPersonalizationError = true as const;
|
|
25
|
+
|
|
26
|
+
constructor(api: string) {
|
|
27
|
+
super(
|
|
28
|
+
`${api} cannot be used during on-demand prerender. The prerender producer ` +
|
|
29
|
+
`is requestless: it renders with route params and env only, and its output ` +
|
|
30
|
+
`is a shared payload keyed by route + params. Reading request-scoped state ` +
|
|
31
|
+
`(cookies, headers) or mutating the response here would leak one visitor's ` +
|
|
32
|
+
`data to every later visitor. This route was skipped as personalized.`,
|
|
33
|
+
);
|
|
34
|
+
this.name = "PrerenderPersonalizationError";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isPrerenderPersonalizationError(
|
|
39
|
+
value: unknown,
|
|
40
|
+
): value is PrerenderPersonalizationError {
|
|
41
|
+
return (
|
|
42
|
+
value != null &&
|
|
43
|
+
typeof value === "object" &&
|
|
44
|
+
(value as PrerenderPersonalizationError).isPrerenderPersonalizationError ===
|
|
45
|
+
true
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Throw {@link PrerenderPersonalizationError} when `ctx` is a requestless
|
|
51
|
+
* on-demand producer context. A no-op for the live request path and the
|
|
52
|
+
* build/dev prerender paths (which do not set `_onDemandProducer`), so existing
|
|
53
|
+
* callers are unaffected.
|
|
54
|
+
*/
|
|
55
|
+
export function assertNotInsidePrerenderProducer(
|
|
56
|
+
ctx: unknown,
|
|
57
|
+
api: string,
|
|
58
|
+
): void {
|
|
59
|
+
if (
|
|
60
|
+
ctx != null &&
|
|
61
|
+
typeof ctx === "object" &&
|
|
62
|
+
(ctx as { _onDemandProducer?: boolean })._onDemandProducer === true
|
|
63
|
+
) {
|
|
64
|
+
throw new PrerenderPersonalizationError(api);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Build shell manifest key,
|
|
3
|
-
*
|
|
4
|
-
* (
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* PATHNAME-ONLY by design. Host-free because the build knows no request
|
|
9
|
-
* host. Router-free because the router id ($$id) is a hash of
|
|
10
|
-
* filePath:lineNumber of the TRANSFORMED source, which differs between the
|
|
11
|
-
* discovery temp server's dev-style transform chain and the main build's —
|
|
12
|
-
* a temp-realm router id can never be looked up by the shipped worker. The
|
|
13
|
-
* producer instead detects pathname collisions across routers at build time
|
|
14
|
-
* and declines both entries (loudly), keeping the key unambiguous. This is
|
|
15
|
-
* a manifest namespace, never a store keyspace: the runtime shell key
|
|
16
|
-
* (host + pathname + search + ":shell") stays untouched.
|
|
2
|
+
* Build-time shell manifest key, `{routerId}@{pathname}`: the ONE derivation
|
|
3
|
+
* shared by the producer (vite/discovery/shell-prerender-phase.ts) and the
|
|
4
|
+
* consumer (rsc/shell-build-manifest.ts) so the two sides cannot drift. The
|
|
5
|
+
* router part follows the router rule (cache-key-utils.ts). Host-free (the
|
|
6
|
+
* build knows no request host); a manifest namespace, never a store keyspace.
|
|
17
7
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
8
|
+
import { routerKeyPrefix } from "../cache/cache-key-utils.js";
|
|
9
|
+
|
|
10
|
+
export function buildShellManifestKey(
|
|
11
|
+
routerId: string,
|
|
12
|
+
pathname: string,
|
|
13
|
+
): string {
|
|
14
|
+
return `${routerKeyPrefix(routerId)}${pathname}`;
|
|
20
15
|
}
|