@rangojs/router 0.5.2 → 0.6.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 +343 -125
- package/dist/types/browser/react/use-router.d.ts +10 -3
- package/dist/types/browser/react/use-search-params.d.ts +57 -10
- package/dist/types/browser/types.d.ts +22 -0
- package/dist/types/build/merge-full-manifests.d.ts +3 -0
- package/dist/types/build/route-trie.d.ts +4 -73
- package/dist/types/build/route-types/per-module-writer.d.ts +6 -4
- package/dist/types/build/route-types/router-processing.d.ts +2 -3
- package/dist/types/cache/cache-exec-scope.d.ts +31 -0
- package/dist/types/cache/taint.d.ts +12 -6
- package/dist/types/client-urls/client-root.d.ts +38 -0
- package/dist/types/client-urls/client-urls.d.ts +5 -0
- package/dist/types/client-urls/navigation.d.ts +38 -0
- package/dist/types/client-urls/revalidation-protocol.d.ts +25 -0
- package/dist/types/client-urls/server-projection.d.ts +62 -0
- package/dist/types/client-urls/types.d.ts +144 -0
- package/dist/types/client.d.ts +12 -4
- package/dist/types/client.rsc.d.ts +4 -1
- package/dist/types/decode-loader-results.d.ts +37 -0
- package/dist/types/errors.d.ts +1 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/loader-redirect.d.ts +27 -0
- package/dist/types/outlet-context.d.ts +12 -0
- package/dist/types/outlet-provider.d.ts +3 -1
- package/dist/types/redirect-origin.d.ts +4 -0
- package/dist/types/route-content-wrapper.d.ts +42 -1
- package/dist/types/route-definition/helpers-types.d.ts +13 -2
- package/dist/types/router/error-handling.d.ts +35 -1
- package/dist/types/router/intercept-resolution.d.ts +12 -0
- package/dist/types/router/loader-resolution.d.ts +24 -2
- package/dist/types/router/revalidation.d.ts +7 -0
- package/dist/types/router/route-trie-builder.d.ts +77 -0
- package/dist/types/router/router-interfaces.d.ts +20 -0
- package/dist/types/router/segment-resolution/helpers.d.ts +1 -1
- package/dist/types/router/trie-matching.d.ts +1 -1
- package/dist/types/rsc/manifest-init.d.ts +5 -5
- package/dist/types/rsc/shell-capture.d.ts +9 -0
- package/dist/types/rsc/shell-serve.d.ts +11 -0
- package/dist/types/rsc/types.d.ts +30 -0
- package/dist/types/segment-system.d.ts +2 -0
- package/dist/types/server/context.d.ts +10 -0
- package/dist/types/server/handle-store.d.ts +34 -3
- package/dist/types/server/request-context.d.ts +11 -1
- package/dist/types/server.d.ts +1 -0
- package/dist/types/ssr/index.d.ts +22 -0
- package/dist/types/ssr/ssr-root.d.ts +10 -0
- package/dist/types/testing/dom.entry.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +16 -6
- package/dist/types/testing/run-loader.d.ts +9 -0
- package/dist/types/types/boundaries.d.ts +22 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/loader-types.d.ts +57 -5
- package/dist/types/types/segments.d.ts +7 -0
- package/dist/types/urls/path-helper-types.d.ts +10 -4
- package/dist/types/vite/discovery/client-urls-projection.d.ts +53 -0
- package/dist/types/vite/discovery/discover-routers.d.ts +1 -1
- package/dist/types/vite/discovery/state.d.ts +8 -1
- package/dist/vite/index.js +5313 -2365
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +39 -9
- package/skills/catalog.json +7 -1
- package/skills/client-urls/SKILL.md +338 -0
- package/skills/comparison/references/framework-comparison.md +23 -9
- package/skills/hooks/SKILL.md +2 -2
- package/skills/hooks/data.md +11 -2
- package/skills/hooks/handle-and-actions.md +7 -0
- package/skills/hooks/outlets.md +26 -5
- package/skills/hooks/urls.md +40 -3
- package/skills/loader/SKILL.md +132 -20
- package/skills/migrate-nextjs/SKILL.md +70 -10
- package/skills/migrate-react-router/SKILL.md +49 -13
- package/skills/migrate-react-router/component-migration.md +18 -13
- package/skills/migrate-react-router/data-and-actions.md +14 -3
- package/skills/migrate-react-router/route-mapping.md +15 -2
- package/skills/parallel/SKILL.md +32 -1
- package/skills/ppr/SKILL.md +16 -6
- package/skills/prerender/SKILL.md +8 -4
- package/skills/rango/SKILL.md +21 -17
- package/skills/react-compiler/SKILL.md +3 -3
- package/skills/route/SKILL.md +5 -2
- package/skills/router-setup/SKILL.md +16 -2
- package/skills/scripts/SKILL.md +16 -6
- package/skills/shell-manifest/SKILL.md +16 -7
- package/skills/testing/SKILL.md +2 -2
- package/skills/testing/client-components.md +6 -0
- package/skills/testing/handles.md +30 -8
- package/skills/testing/loader.md +51 -49
- package/skills/testing/middleware.md +1 -1
- package/skills/theme/SKILL.md +8 -5
- package/src/bin/rango.ts +7 -3
- package/src/browser/navigation-bridge.ts +6 -0
- package/src/browser/navigation-client.ts +5 -0
- package/src/browser/partial-update.ts +65 -13
- package/src/browser/react/use-router.ts +40 -11
- package/src/browser/react/use-search-params.ts +140 -17
- package/src/browser/rsc-router.tsx +59 -0
- package/src/browser/server-action-bridge.ts +26 -0
- package/src/browser/types.ts +22 -0
- package/src/build/merge-full-manifests.ts +161 -0
- package/src/build/route-trie.ts +9 -332
- package/src/build/route-types/include-resolution.ts +66 -11
- package/src/build/route-types/per-module-writer.ts +11 -6
- package/src/build/route-types/router-processing.ts +184 -153
- package/src/build/runtime-discovery.ts +23 -12
- package/src/cache/cache-exec-scope.ts +47 -0
- package/src/cache/cache-runtime.ts +24 -25
- package/src/cache/taint.ts +28 -9
- package/src/client-urls/client-root.tsx +168 -0
- package/src/client-urls/client-urls.ts +698 -0
- package/src/client-urls/navigation.ts +237 -0
- package/src/client-urls/revalidation-protocol.ts +56 -0
- package/src/client-urls/server-projection.ts +579 -0
- package/src/client-urls/types.ts +195 -0
- package/src/client.rsc.tsx +12 -0
- package/src/client.tsx +49 -6
- package/src/decode-loader-results.ts +113 -0
- package/src/errors.ts +14 -0
- package/src/handles/deferred-resolution.ts +14 -7
- package/src/index.ts +1 -0
- package/src/loader-redirect.tsx +64 -0
- package/src/outlet-context.ts +12 -0
- package/src/outlet-provider.tsx +15 -1
- package/src/redirect-origin.ts +29 -0
- package/src/route-content-wrapper.tsx +96 -3
- package/src/route-definition/dsl-helpers.ts +28 -3
- package/src/route-definition/helpers-types.ts +13 -0
- package/src/route-definition/redirect.ts +17 -18
- package/src/router/error-handling.ts +65 -11
- package/src/router/intercept-resolution.ts +29 -0
- package/src/router/loader-resolution.ts +261 -28
- package/src/router/match-result.ts +7 -0
- package/src/router/revalidation.ts +24 -11
- package/src/router/route-trie-builder.ts +334 -0
- package/src/router/router-interfaces.ts +38 -0
- package/src/router/segment-resolution/fresh.ts +47 -0
- package/src/router/segment-resolution/helpers.ts +9 -11
- package/src/router/segment-resolution/loader-cache.ts +14 -24
- package/src/router/segment-resolution/revalidation.ts +20 -1
- package/src/router/trie-matching.ts +3 -3
- package/src/router.ts +46 -1
- package/src/rsc/full-payload.ts +6 -0
- package/src/rsc/handler.ts +10 -7
- package/src/rsc/loader-fetch.ts +2 -2
- package/src/rsc/manifest-init.ts +28 -9
- package/src/rsc/rsc-rendering.ts +15 -1
- package/src/rsc/shell-capture.ts +12 -0
- package/src/rsc/shell-serve.ts +15 -2
- package/src/rsc/ssr-setup.ts +10 -1
- package/src/rsc/types.ts +31 -2
- package/src/segment-system.tsx +83 -26
- package/src/server/context.ts +10 -0
- package/src/server/cookie-store.ts +19 -19
- package/src/server/handle-store.ts +185 -48
- package/src/server/request-context.ts +30 -6
- package/src/server.ts +7 -0
- package/src/ssr/index.tsx +37 -2
- package/src/ssr/ssr-root.tsx +29 -2
- package/src/testing/dom.entry.ts +1 -1
- package/src/testing/render-route.tsx +22 -8
- package/src/testing/run-loader.ts +51 -13
- package/src/types/boundaries.ts +19 -0
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +60 -5
- package/src/types/segments.ts +7 -0
- package/src/urls/include-helper.ts +22 -4
- package/src/urls/path-helper-types.ts +14 -1
- package/src/use-loader.tsx +67 -6
- package/src/vite/discovery/client-urls-projection.ts +322 -0
- package/src/vite/discovery/discover-routers.ts +43 -17
- package/src/vite/discovery/state.ts +11 -1
- package/src/vite/discovery/virtual-module-codegen.ts +20 -0
- package/src/vite/plugins/virtual-entries.ts +12 -3
- package/src/vite/router-discovery.ts +163 -12
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { startTransition } from "react";
|
|
4
|
+
import { encodeClientRevalidationDecisions } from "./revalidation-protocol.js";
|
|
5
|
+
import type { ClientRevalidateArgs, ClientUrlPatterns } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export interface ClientUrlNavigationIntent {
|
|
8
|
+
readonly routeId: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ActiveClientUrlGroup {
|
|
12
|
+
readonly definition: ClientUrlPatterns;
|
|
13
|
+
/** include() mount prefix the group is rendered under ("/" at root). */
|
|
14
|
+
readonly mount: string;
|
|
15
|
+
/** include() route-name prefix ("" at root) for canonical name composition. */
|
|
16
|
+
readonly namePrefix: string;
|
|
17
|
+
readonly setIntent: (intent: ClientUrlNavigationIntent | null) => void;
|
|
18
|
+
intent: ClientUrlNavigationIntent | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Intercept TARGET route names reachable from the CURRENT location as a
|
|
23
|
+
* navigation origin, shipped in payload metadata (MatchResult.interceptTargets)
|
|
24
|
+
* and refreshed on every commit. When a local match's canonical route name is
|
|
25
|
+
* in this set, the optimistic presentation DECLINES: the canonical response
|
|
26
|
+
* will commit the intercept over the ORIGIN page, so destination loading would
|
|
27
|
+
* flash and revert. Conservative for `when`-conditional intercepts (their
|
|
28
|
+
* selectors need live navigation context): a non-intercepted navigation may
|
|
29
|
+
* lose its optimistic loading, never the reverse.
|
|
30
|
+
*/
|
|
31
|
+
let activeInterceptTargets: ReadonlySet<string> = new Set();
|
|
32
|
+
|
|
33
|
+
export function setActiveInterceptTargets(
|
|
34
|
+
targets: readonly string[] | undefined,
|
|
35
|
+
): void {
|
|
36
|
+
activeInterceptTargets = new Set(targets ?? []);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ClientUrlNavigationPresentation {
|
|
40
|
+
readonly routeId: string;
|
|
41
|
+
clear(): void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let activeGroup: ActiveClientUrlGroup | null = null;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Strip the include mount prefix from an absolute pathname, yielding the
|
|
48
|
+
* definition-local pathname the module trie was built from. Returns null when
|
|
49
|
+
* the pathname lies outside the mount — the navigation then targets a server
|
|
50
|
+
* route and gets no optimistic presentation. Mirrors joinMount() in
|
|
51
|
+
* use-reverse.ts: the bare mount maps to the module index "/".
|
|
52
|
+
*/
|
|
53
|
+
function stripMountPrefix(pathname: string, mount: string): string | null {
|
|
54
|
+
if (mount === "" || mount === "/") return pathname;
|
|
55
|
+
const normalized = mount.endsWith("/") ? mount.slice(0, -1) : mount;
|
|
56
|
+
if (pathname === normalized) return "/";
|
|
57
|
+
if (pathname.startsWith(`${normalized}/`)) {
|
|
58
|
+
return pathname.slice(normalized.length);
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function registerClientUrlGroup(
|
|
64
|
+
definition: ClientUrlPatterns,
|
|
65
|
+
mount: string,
|
|
66
|
+
namePrefix: string,
|
|
67
|
+
setIntent: (intent: ClientUrlNavigationIntent | null) => void,
|
|
68
|
+
): () => void {
|
|
69
|
+
const group: ActiveClientUrlGroup = {
|
|
70
|
+
definition,
|
|
71
|
+
mount,
|
|
72
|
+
namePrefix,
|
|
73
|
+
setIntent,
|
|
74
|
+
intent: null,
|
|
75
|
+
};
|
|
76
|
+
activeGroup = group;
|
|
77
|
+
|
|
78
|
+
return () => {
|
|
79
|
+
if (activeGroup === group) activeGroup = null;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function beginClientUrlNavigation(
|
|
84
|
+
targetUrl: URL,
|
|
85
|
+
signal: AbortSignal,
|
|
86
|
+
): ClientUrlNavigationPresentation | null {
|
|
87
|
+
const group = activeGroup;
|
|
88
|
+
if (!group) return null;
|
|
89
|
+
|
|
90
|
+
const localPathname = stripMountPrefix(targetUrl.pathname, group.mount);
|
|
91
|
+
if (localPathname === null) return null;
|
|
92
|
+
|
|
93
|
+
const match = group.definition.match(localPathname);
|
|
94
|
+
if (!match || match.redirectTo) return null;
|
|
95
|
+
|
|
96
|
+
// Decline when an intercept would claim this target: compose the matched
|
|
97
|
+
// record's canonical route name (include namePrefix + local name) and check
|
|
98
|
+
// it against the current location's intercept target set. Unnamed records
|
|
99
|
+
// cannot be intercept targets (intercepts target route NAMES).
|
|
100
|
+
const record = group.definition.routes.find(
|
|
101
|
+
(candidate) => candidate.id === match.routeKey,
|
|
102
|
+
);
|
|
103
|
+
if (record?.name) {
|
|
104
|
+
const canonicalName = group.namePrefix
|
|
105
|
+
? `${group.namePrefix}.${record.name}`
|
|
106
|
+
: record.name;
|
|
107
|
+
if (activeInterceptTargets.has(canonicalName)) return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const intent: ClientUrlNavigationIntent = { routeId: match.routeKey };
|
|
111
|
+
group.intent = intent;
|
|
112
|
+
group.setIntent(intent);
|
|
113
|
+
|
|
114
|
+
const clear = (): void => {
|
|
115
|
+
if (group.intent !== intent) return;
|
|
116
|
+
group.intent = null;
|
|
117
|
+
// The canonical commit may be held in a startTransition (explicit
|
|
118
|
+
// transition() routes, view transitions, action revalidations in
|
|
119
|
+
// partial-update.ts). An urgent clear would flush against the
|
|
120
|
+
// pre-transition tree first — loading UI → ORIGIN content → destination.
|
|
121
|
+
// Clearing inside a transition keeps the optimistic presentation until
|
|
122
|
+
// the destination commit lands; on the plain urgent commit path both
|
|
123
|
+
// updates land in order, so this changes nothing there.
|
|
124
|
+
startTransition(() => group.setIntent(null));
|
|
125
|
+
};
|
|
126
|
+
signal.addEventListener("abort", clear, { once: true });
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
routeId: match.routeKey,
|
|
130
|
+
clear() {
|
|
131
|
+
signal.removeEventListener("abort", clear);
|
|
132
|
+
clear();
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Run the held clientUrls route's per-loader revalidate() predicates and
|
|
139
|
+
* encode their decisions for the request header. Predicates are CLIENT code
|
|
140
|
+
* (declared in the "use client" definition module); only decisions cross the
|
|
141
|
+
* wire — the synthesized per-loader revalidate() on every materialized stub
|
|
142
|
+
* (server-projection.ts) reads them back by loader $$id.
|
|
143
|
+
*
|
|
144
|
+
* Decisions exist only for the loaders the client currently HOLDS: the local
|
|
145
|
+
* match of currentUrl. They matter when the server would re-evaluate those
|
|
146
|
+
* held loader segments — same-route param/search navs, actions, and stale
|
|
147
|
+
* restores. Cross-route targets render new segments unconditionally, so a
|
|
148
|
+
* decision would be ignored; we still compute against the current route since
|
|
149
|
+
* only its held segments are addressable.
|
|
150
|
+
*
|
|
151
|
+
* Returns null when no group is active, the current location is not a client
|
|
152
|
+
* route, or no predicate produced a decision that differs from the locked
|
|
153
|
+
* default — the server then applies defaults, which is also the behavior for
|
|
154
|
+
* requests that cannot carry decisions (no-JS, PE, prefetch, document loads).
|
|
155
|
+
*/
|
|
156
|
+
export function collectClientRevalidationDecisions(options: {
|
|
157
|
+
currentUrl: URL;
|
|
158
|
+
nextUrl: URL;
|
|
159
|
+
isAction: boolean;
|
|
160
|
+
actionId?: string;
|
|
161
|
+
stale: boolean;
|
|
162
|
+
}): string | null {
|
|
163
|
+
const group = activeGroup;
|
|
164
|
+
if (!group) return null;
|
|
165
|
+
|
|
166
|
+
const { currentUrl, nextUrl, isAction, actionId, stale } = options;
|
|
167
|
+
const currentLocal = stripMountPrefix(currentUrl.pathname, group.mount);
|
|
168
|
+
if (currentLocal === null) return null;
|
|
169
|
+
const currentMatch = group.definition.match(currentLocal);
|
|
170
|
+
if (!currentMatch || currentMatch.redirectTo) return null;
|
|
171
|
+
const record = group.definition.routes.find(
|
|
172
|
+
(candidate) => candidate.id === currentMatch.routeKey,
|
|
173
|
+
);
|
|
174
|
+
if (!record) return null;
|
|
175
|
+
|
|
176
|
+
const nextLocal = stripMountPrefix(nextUrl.pathname, group.mount);
|
|
177
|
+
const nextMatch =
|
|
178
|
+
nextLocal === null ? null : group.definition.match(nextLocal);
|
|
179
|
+
const nextParams = nextMatch?.params ?? {};
|
|
180
|
+
|
|
181
|
+
// Locked default, mirrored client-side: actions re-run route-owned loaders;
|
|
182
|
+
// navigations re-run them when params or search changed.
|
|
183
|
+
const paramsEqual = (
|
|
184
|
+
a: Record<string, string>,
|
|
185
|
+
b: Record<string, string>,
|
|
186
|
+
): boolean => {
|
|
187
|
+
const aKeys = Object.keys(a);
|
|
188
|
+
return (
|
|
189
|
+
aKeys.length === Object.keys(b).length &&
|
|
190
|
+
aKeys.every((key) => a[key] === b[key])
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
const defaultShouldRevalidate = isAction
|
|
194
|
+
? true
|
|
195
|
+
: !paramsEqual(currentMatch.params, nextParams) ||
|
|
196
|
+
currentUrl.search !== nextUrl.search;
|
|
197
|
+
|
|
198
|
+
const skip: string[] = [];
|
|
199
|
+
const force: string[] = [];
|
|
200
|
+
for (const { loader, revalidate } of record.loaders) {
|
|
201
|
+
if (revalidate.length === 0) continue;
|
|
202
|
+
const args: ClientRevalidateArgs = {
|
|
203
|
+
currentUrl,
|
|
204
|
+
nextUrl,
|
|
205
|
+
currentParams: currentMatch.params,
|
|
206
|
+
nextParams,
|
|
207
|
+
defaultShouldRevalidate,
|
|
208
|
+
stale,
|
|
209
|
+
isAction,
|
|
210
|
+
...(actionId !== undefined ? { actionId } : {}),
|
|
211
|
+
};
|
|
212
|
+
// Same iteration contract as the server: every predicate runs, the last
|
|
213
|
+
// boolean verdict wins. A throwing predicate fails open to the default
|
|
214
|
+
// (mirrors evaluateRevalidation's fail-open).
|
|
215
|
+
let decision = defaultShouldRevalidate;
|
|
216
|
+
for (const fn of revalidate) {
|
|
217
|
+
try {
|
|
218
|
+
const verdict = fn(args);
|
|
219
|
+
if (typeof verdict === "boolean") decision = verdict;
|
|
220
|
+
} catch (error) {
|
|
221
|
+
console.error(
|
|
222
|
+
`[@rangojs/router] clientUrls revalidate() threw for loader "${loader.$$id}"; using default decision:`,
|
|
223
|
+
error,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (decision === defaultShouldRevalidate) continue;
|
|
228
|
+
(decision ? force : skip).push(loader.$$id);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return encodeClientRevalidationDecisions({ skip, force });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function clearClientUrlNavigationRegistry(): void {
|
|
235
|
+
activeGroup = null;
|
|
236
|
+
activeInterceptTargets = new Set();
|
|
237
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire protocol for CLIENT-RUN per-loader revalidation decisions.
|
|
3
|
+
*
|
|
4
|
+
* clientUrls() revalidate() predicates are declared in a "use client" module,
|
|
5
|
+
* so they cannot cross the projection boundary as functions — instead they
|
|
6
|
+
* EXECUTE in the browser and only their DECISIONS cross: the browser attaches
|
|
7
|
+
* this header to partial-navigation and action requests, and the synthesized
|
|
8
|
+
* per-loader revalidate() on every materialized loader stub reads it back
|
|
9
|
+
* (see materializeRouteItems in server-projection.ts).
|
|
10
|
+
*
|
|
11
|
+
* Trust model: same class as `_rsc_segments` — a decision can only make the
|
|
12
|
+
* CLIENT's own view staler (skip) or fresher (force), and the synthesized
|
|
13
|
+
* predicates exist only on client-urls loader stubs, so the header can never
|
|
14
|
+
* influence server-tree loaders. Requests without the header (no-JS, PE,
|
|
15
|
+
* prefetch, document loads) fall back to the locked server defaults.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export const CLIENT_REVALIDATION_HEADER = "X-Rango-Client-Reval";
|
|
19
|
+
|
|
20
|
+
export interface ClientRevalidationDecisions {
|
|
21
|
+
/** Loader $$ids whose client predicate said false (keep held data). */
|
|
22
|
+
readonly skip: readonly string[];
|
|
23
|
+
/** Loader $$ids whose client predicate said true against a false default. */
|
|
24
|
+
readonly force: readonly string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function encodeClientRevalidationDecisions(
|
|
28
|
+
decisions: ClientRevalidationDecisions,
|
|
29
|
+
): string | null {
|
|
30
|
+
if (decisions.skip.length === 0 && decisions.force.length === 0) return null;
|
|
31
|
+
// encodeURIComponent keeps the header value ISO-8859-1-safe regardless of
|
|
32
|
+
// what characters loader module ids contain.
|
|
33
|
+
return encodeURIComponent(
|
|
34
|
+
JSON.stringify({ skip: decisions.skip, force: decisions.force }),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function decodeClientRevalidationDecisions(
|
|
39
|
+
headerValue: string | null,
|
|
40
|
+
): ClientRevalidationDecisions | null {
|
|
41
|
+
if (!headerValue) return null;
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(decodeURIComponent(headerValue)) as {
|
|
44
|
+
skip?: unknown;
|
|
45
|
+
force?: unknown;
|
|
46
|
+
};
|
|
47
|
+
const toIds = (value: unknown): readonly string[] =>
|
|
48
|
+
Array.isArray(value)
|
|
49
|
+
? value.filter((entry): entry is string => typeof entry === "string")
|
|
50
|
+
: [];
|
|
51
|
+
return { skip: toIds(parsed.skip), force: toIds(parsed.force) };
|
|
52
|
+
} catch {
|
|
53
|
+
// A malformed header is treated as absent: locked defaults apply.
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|