@rangojs/router 0.31.0 → 0.33.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/README.md +3 -2
- package/dist/bin/browser-7KIJ5HSV.js +15 -0
- package/dist/bin/chunk-3B7CKBRO.js +463 -0
- package/dist/bin/chunk-524VACCG.js +71 -0
- package/dist/bin/chunk-LD7UBHT4.js +196 -0
- package/dist/bin/chunk-TO6CRWLN.js +1429 -0
- package/dist/bin/chunk-TTXC2U3G.js +2477 -0
- package/dist/bin/client-urls-projection-DQLWPDAT.js +28 -0
- package/dist/bin/forward-user-plugins-URBZKJP4.js +62 -0
- package/dist/bin/rango.js +42 -10143
- package/dist/bin/runtime-EPUDEHO4.js +596 -0
- package/dist/bin/runtime-discovery-CZ2QHT4Q.js +352 -0
- package/dist/bin/state-ACD2MGKJ.js +16 -0
- package/dist/bin/temp-rsc-server-QRR6HD6T.js +3190 -0
- package/dist/types/bin/rango.d.ts +1 -0
- package/dist/types/browser/action-fence.d.ts +20 -0
- package/dist/types/browser/cookie-name.d.ts +51 -10
- package/dist/types/browser/history-state.d.ts +13 -2
- package/dist/types/browser/invalidate-client-cache.d.ts +8 -6
- package/dist/types/browser/navigation-bridge.d.ts +36 -1
- package/dist/types/browser/navigation-client.d.ts +19 -0
- package/dist/types/browser/navigation-store-handle.d.ts +37 -4
- package/dist/types/browser/navigation-store.d.ts +2 -2
- package/dist/types/browser/navigation-transaction.d.ts +0 -1
- package/dist/types/browser/partial-update.d.ts +13 -0
- package/dist/types/browser/prefetch/cache.d.ts +96 -8
- package/dist/types/browser/prefetch/fetch.d.ts +26 -2
- package/dist/types/browser/prefetch/queue.d.ts +3 -2
- package/dist/types/browser/rango-state.d.ts +34 -5
- package/dist/types/browser/react/Link.d.ts +19 -13
- package/dist/types/browser/react/location-state-shared.d.ts +2 -1
- package/dist/types/browser/types.d.ts +25 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/include-resolution.d.ts +9 -1
- package/dist/types/build/route-types/router-processing.d.ts +3 -1
- package/dist/types/cache/cache-key-utils.d.ts +12 -8
- package/dist/types/cache/cache-scope.d.ts +4 -2
- package/dist/types/cache/cf/cf-cache-types.d.ts +9 -5
- package/dist/types/cache/document-cache-policy.d.ts +9 -1
- package/dist/types/cache/memory-segment-store.d.ts +3 -2
- package/dist/types/cache/segment-codec.d.ts +2 -2
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/types.d.ts +12 -7
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -1
- package/dist/types/client-urls/types.d.ts +7 -5
- package/dist/types/prefetch-markers.d.ts +9 -0
- package/dist/types/prefetch-source-scope.d.ts +66 -0
- package/dist/types/route-definition/helpers-types.d.ts +8 -5
- package/dist/types/route-definition/redirect.d.ts +5 -0
- package/dist/types/router/intercept-resolution.d.ts +25 -11
- package/dist/types/router/match-context.d.ts +1 -0
- package/dist/types/router/match-handlers.d.ts +1 -1
- package/dist/types/router/middleware-types.d.ts +6 -0
- package/dist/types/router/prefetch-cache-ttl.d.ts +11 -3
- package/dist/types/router/router-context.d.ts +1 -1
- package/dist/types/router/router-options.d.ts +6 -3
- package/dist/types/router/segment-resolution/loader-cache.d.ts +8 -0
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +23 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +19 -0
- package/dist/types/router/source-reads.d.ts +53 -0
- package/dist/types/router/types.d.ts +1 -1
- package/dist/types/rsc/helpers.d.ts +8 -3
- package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
- package/dist/types/rsc/request-stages.d.ts +4 -0
- package/dist/types/rsc/shell-serve.d.ts +6 -1
- package/dist/types/server/context.d.ts +23 -6
- package/dist/types/server/cookie-store.d.ts +15 -1
- package/dist/types/server/document-location-state.d.ts +59 -0
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +65 -12
- package/dist/types/testing/e2e/server.d.ts +16 -1
- package/dist/types/testing/render-route.d.ts +83 -2
- package/dist/types/testing/serve-shell-request.d.ts +15 -3
- package/dist/types/types/handler-context.d.ts +21 -4
- package/dist/types/types/loader-types.d.ts +7 -4
- package/dist/types/types/segments.d.ts +18 -1
- package/dist/types/urls/path-helper-types.d.ts +7 -5
- package/dist/types/vite/temp-rsc-server.d.ts +2 -2
- package/dist/types/vite/utils/forward-user-plugins.d.ts +32 -2
- package/dist/vite/index.js +451 -191
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +74 -0
- package/skills/app-structure/e-commerce.md +10 -9
- package/skills/app-structure/large-app.md +15 -2
- package/skills/caching/SKILL.md +10 -5
- package/skills/client-urls/SKILL.md +18 -11
- package/skills/comparison/references/framework-comparison.md +16 -10
- package/skills/document-cache/SKILL.md +13 -0
- package/skills/hooks/navigation.md +8 -8
- package/skills/hooks/state.md +132 -12
- package/skills/intercept/SKILL.md +33 -11
- package/skills/layout/SKILL.md +11 -0
- package/skills/links/SKILL.md +101 -4
- package/skills/loader/SKILL.md +49 -22
- package/skills/migrate-nextjs/SKILL.md +8 -2
- package/skills/migrate-react-router/data-and-actions.md +5 -0
- package/skills/ppr/SKILL.md +38 -5
- package/skills/prerender/SKILL.md +4 -1
- package/skills/route/SKILL.md +6 -6
- package/skills/router-setup/SKILL.md +2 -1
- package/skills/server-actions/SKILL.md +49 -8
- package/skills/testing/client-components.md +73 -32
- package/skills/testing/serve-shell-request.md +45 -20
- package/skills/testing/server-actions.md +8 -0
- package/skills/ux-architecture/SKILL.md +67 -15
- package/src/bin/rango.ts +39 -13
- package/src/browser/action-fence.ts +72 -0
- package/src/browser/cookie-name.ts +82 -14
- package/src/browser/history-state.ts +24 -2
- package/src/browser/invalidate-client-cache.ts +9 -7
- package/src/browser/navigation-bridge.ts +164 -32
- package/src/browser/navigation-client.ts +95 -20
- package/src/browser/navigation-store-handle.ts +67 -5
- package/src/browser/navigation-store.ts +86 -17
- package/src/browser/navigation-transaction.ts +0 -6
- package/src/browser/partial-update.ts +31 -12
- package/src/browser/prefetch/cache.ts +188 -11
- package/src/browser/prefetch/fetch.ts +211 -51
- package/src/browser/prefetch/queue.ts +3 -2
- package/src/browser/rango-state.ts +50 -8
- package/src/browser/react/Link.tsx +24 -16
- package/src/browser/react/location-state-shared.ts +8 -1
- package/src/browser/react/use-navigation.ts +1 -1
- package/src/browser/rsc-router.tsx +13 -3
- package/src/browser/server-action-bridge.ts +3 -3
- package/src/browser/types.ts +25 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/include-resolution.ts +40 -3
- package/src/build/route-types/router-processing.ts +170 -25
- package/src/build/route-types/scan-filter.ts +1 -0
- package/src/build/runtime-discovery.ts +46 -8
- package/src/cache/cache-key-utils.ts +12 -8
- package/src/cache/cache-runtime.ts +5 -1
- package/src/cache/cache-scope.ts +28 -7
- package/src/cache/cf/cf-cache-types.ts +9 -5
- package/src/cache/document-cache-policy.ts +37 -1
- package/src/cache/document-cache.ts +12 -0
- package/src/cache/memory-segment-store.ts +3 -2
- package/src/cache/segment-codec.ts +2 -2
- package/src/cache/shell-snapshot.ts +1 -1
- package/src/cache/types.ts +12 -7
- package/src/cache/vercel/vercel-cache-store.ts +2 -1
- package/src/client-urls/types.ts +7 -5
- package/src/prefetch-markers.ts +10 -0
- package/src/prefetch-source-scope.ts +137 -0
- package/src/route-definition/helpers-types.ts +8 -5
- package/src/route-definition/redirect.ts +5 -0
- package/src/router/intercept-resolution.ts +94 -50
- package/src/router/loader-resolution.ts +8 -6
- package/src/router/match-api.ts +33 -29
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +2 -1
- package/src/router/match-middleware/cache-lookup.ts +39 -14
- package/src/router/match-result.ts +45 -85
- package/src/router/middleware-types.ts +6 -0
- package/src/router/navigation-snapshot.ts +18 -3
- package/src/router/prefetch-cache-ttl.ts +15 -3
- package/src/router/revalidation.ts +63 -13
- package/src/router/router-context.ts +2 -1
- package/src/router/router-options.ts +6 -3
- package/src/router/segment-resolution/loader-cache.ts +21 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +38 -31
- package/src/router/segment-resolution/revalidation.ts +40 -12
- package/src/router/source-reads.ts +122 -0
- package/src/router/types.ts +2 -1
- package/src/router.ts +11 -12
- package/src/rsc/helpers.ts +20 -6
- package/src/rsc/prefetch-render-error.ts +7 -2
- package/src/rsc/progressive-enhancement.ts +8 -2
- package/src/rsc/request-stages.ts +87 -1
- package/src/rsc/rsc-rendering.ts +78 -32
- package/src/rsc/server-action.ts +2 -1
- package/src/rsc/shell-capture.ts +4 -5
- package/src/rsc/shell-serve.ts +12 -1
- package/src/segment-system.tsx +1 -1
- package/src/server/context.ts +37 -6
- package/src/server/cookie-store.ts +22 -1
- package/src/server/document-location-state.ts +334 -0
- package/src/server/handle-store.ts +2 -0
- package/src/server/request-context.ts +137 -33
- package/src/testing/dispatch.ts +6 -0
- package/src/testing/e2e/fixture.ts +4 -12
- package/src/testing/e2e/server.ts +91 -4
- package/src/testing/render-route.tsx +309 -10
- package/src/testing/run-revalidate.ts +11 -1
- package/src/testing/serve-shell-request.ts +22 -3
- package/src/types/handler-context.ts +21 -4
- package/src/types/loader-types.ts +7 -4
- package/src/types/segments.ts +18 -1
- package/src/urls/path-helper-types.ts +7 -5
- package/src/vite/temp-rsc-server.ts +2 -2
- package/src/vite/utils/forward-user-plugins.ts +13 -5
|
@@ -26,11 +26,12 @@ import {
|
|
|
26
26
|
} from "./segment-resolution/revalidation.js";
|
|
27
27
|
import { fillSends } from "./segment-resolution/prefetch-deferral.js";
|
|
28
28
|
import type { ResolutionPass } from "./segment-resolution/resolution-pass.js";
|
|
29
|
-
import { getRouterContext } from "./router-context.js";
|
|
29
|
+
import { getRouterContext, type InterceptResult } from "./router-context.js";
|
|
30
30
|
import { runInSegmentTagScope } from "../cache/cache-tag.js";
|
|
31
31
|
import {
|
|
32
32
|
RangoContext,
|
|
33
33
|
latchPprHeaderScopeForEntries,
|
|
34
|
+
runAsSegment,
|
|
34
35
|
} from "../server/context.js";
|
|
35
36
|
import { _getRequestContext } from "../server/request-context.js";
|
|
36
37
|
|
|
@@ -60,18 +61,11 @@ function latchPprHeaderScopeForInterceptTarget(
|
|
|
60
61
|
* If no when() conditions are defined, the intercept always activates.
|
|
61
62
|
* A selector that throws yields the conservative default, no intercept (the
|
|
62
63
|
* full page renders), and is logged; it never fails the request.
|
|
63
|
-
*
|
|
64
|
-
* During action revalidation, when() is NOT evaluated.
|
|
65
64
|
*/
|
|
66
65
|
export function evaluateInterceptWhen(
|
|
67
66
|
intercept: InterceptEntry,
|
|
68
67
|
selectorContext: InterceptSelectorContext | null,
|
|
69
|
-
isAction: boolean,
|
|
70
68
|
): boolean {
|
|
71
|
-
if (isAction) {
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
69
|
if (!intercept.when || intercept.when.length === 0) {
|
|
76
70
|
return true;
|
|
77
71
|
}
|
|
@@ -92,9 +86,34 @@ export function evaluateInterceptWhen(
|
|
|
92
86
|
}
|
|
93
87
|
|
|
94
88
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
89
|
+
* Walk the intercepts declared along `fromEntry`'s chain in selection order,
|
|
90
|
+
* an entry's own and then its orphans' (nested ones included) in render
|
|
91
|
+
* order, from `fromEntry` up, until `pick` returns a value. Tolerates partial
|
|
92
|
+
* entries (unit-test mocks omit the arrays); real registration always
|
|
93
|
+
* populates layout/intercept.
|
|
97
94
|
*/
|
|
95
|
+
function findOnChain<T>(
|
|
96
|
+
fromEntry: EntryData | null,
|
|
97
|
+
pick: (intercept: InterceptEntry, entry: EntryData) => T | undefined,
|
|
98
|
+
): T | undefined {
|
|
99
|
+
const visit = (entry: EntryData): T | undefined => {
|
|
100
|
+
for (const intercept of entry.intercept ?? []) {
|
|
101
|
+
const found = pick(intercept, entry);
|
|
102
|
+
if (found !== undefined) return found;
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
};
|
|
106
|
+
let current: EntryData | null = fromEntry;
|
|
107
|
+
let prev: EntryData | null = null;
|
|
108
|
+
while (current) {
|
|
109
|
+
const found = visit(current) ?? findInOrphans(current, visit, prev);
|
|
110
|
+
if (found !== undefined) return found;
|
|
111
|
+
prev = current;
|
|
112
|
+
current = current.parent;
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
98
117
|
/**
|
|
99
118
|
* Collect every intercept TARGET route name reachable from an origin entry —
|
|
100
119
|
* the same chain walk findInterceptForRoute() performs when this location is
|
|
@@ -110,55 +129,78 @@ export function collectInterceptTargetNames(
|
|
|
110
129
|
fromEntry: EntryData | null,
|
|
111
130
|
): string[] {
|
|
112
131
|
const names = new Set<string>();
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const add = (source: EntryData): undefined => {
|
|
116
|
-
for (const intercept of source.intercept ?? []) {
|
|
117
|
-
names.add(intercept.routeName);
|
|
118
|
-
}
|
|
132
|
+
findOnChain(fromEntry, (intercept) => {
|
|
133
|
+
names.add(intercept.routeName);
|
|
119
134
|
return undefined;
|
|
120
|
-
};
|
|
121
|
-
let current: EntryData | null = fromEntry;
|
|
122
|
-
let prev: EntryData | null = null;
|
|
123
|
-
while (current) {
|
|
124
|
-
add(current);
|
|
125
|
-
findInOrphans(current, add, prev);
|
|
126
|
-
prev = current;
|
|
127
|
-
current = current.parent;
|
|
128
|
-
}
|
|
135
|
+
});
|
|
129
136
|
return [...names];
|
|
130
137
|
}
|
|
131
138
|
|
|
139
|
+
/**
|
|
140
|
+
* The intercept for the target route, with the entry that declares it.
|
|
141
|
+
* `targetRouteKeys` are the names it may be declared for, in precedence
|
|
142
|
+
* order: the route's own name, then its local name inside include()
|
|
143
|
+
* (match-api.ts). A navigation, or an action with no `held`, takes the first
|
|
144
|
+
* along the chain from `fromEntry` up under the first name that has one: on a
|
|
145
|
+
* navigation, the first whose when() passes; on an action, the first, when()
|
|
146
|
+
* not evaluated.
|
|
147
|
+
*
|
|
148
|
+
* An action given `held` (the request's segment ids) re-renders the intercept
|
|
149
|
+
* on screen: one whose slot id the client holds, null when it holds none. Its
|
|
150
|
+
* candidates are collected under every name in one walk, the route's own
|
|
151
|
+
* name's first: a held id under the own name (a parallel() default of the
|
|
152
|
+
* slot's name) must not win before the local name is looked at. One held:
|
|
153
|
+
* that one, when() not evaluated, so an open intercept stays open when its
|
|
154
|
+
* when() would now reject. Several held (intercepts on one entry that fill one
|
|
155
|
+
* slot share its id, as does a parallel() slot of that name): the ones whose
|
|
156
|
+
* slot has no parallel() default on their entry, if any (a default holds its
|
|
157
|
+
* slot id whether or not an intercept is open there). One left: that one,
|
|
158
|
+
* when() not evaluated. Else the first whose when() passes against the
|
|
159
|
+
* action's intercept source, else the first.
|
|
160
|
+
*/
|
|
132
161
|
export function findInterceptForRoute(
|
|
133
|
-
|
|
162
|
+
targetRouteKeys: string | readonly string[],
|
|
134
163
|
fromEntry: EntryData | null,
|
|
135
164
|
selectorContext: InterceptSelectorContext | null = null,
|
|
136
165
|
isAction: boolean = false,
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
|
|
166
|
+
held?: ReadonlySet<string>,
|
|
167
|
+
): InterceptResult | null {
|
|
168
|
+
const names =
|
|
169
|
+
typeof targetRouteKeys === "string" ? [targetRouteKeys] : targetRouteKeys;
|
|
170
|
+
if (!isAction || !held) {
|
|
171
|
+
for (const name of names) {
|
|
172
|
+
const found = findOnChain(fromEntry, (intercept, entry) =>
|
|
173
|
+
intercept.routeName === name &&
|
|
174
|
+
(isAction || evaluateInterceptWhen(intercept, selectorContext))
|
|
175
|
+
? { intercept, entry }
|
|
176
|
+
: undefined,
|
|
177
|
+
);
|
|
178
|
+
if (found) return found;
|
|
146
179
|
}
|
|
147
|
-
return
|
|
148
|
-
};
|
|
149
|
-
let current: EntryData | null = fromEntry;
|
|
150
|
-
let prev: EntryData | null = null;
|
|
151
|
-
|
|
152
|
-
while (current) {
|
|
153
|
-
// current first, then its orphans (nested ones included) in render order.
|
|
154
|
-
const found = match(current) ?? findInOrphans(current, match, prev);
|
|
155
|
-
if (found) return found;
|
|
156
|
-
|
|
157
|
-
prev = current;
|
|
158
|
-
current = current.parent;
|
|
180
|
+
return null;
|
|
159
181
|
}
|
|
160
|
-
|
|
161
|
-
|
|
182
|
+
const byName: InterceptResult[][] = names.map(() => []);
|
|
183
|
+
findOnChain(fromEntry, (intercept, entry) => {
|
|
184
|
+
const rank = names.indexOf(intercept.routeName);
|
|
185
|
+
if (rank !== -1 && held.has(interceptSlotId(entry, intercept))) {
|
|
186
|
+
byName[rank]!.push({ intercept, entry });
|
|
187
|
+
}
|
|
188
|
+
return undefined;
|
|
189
|
+
});
|
|
190
|
+
const open = byName.flat();
|
|
191
|
+
if (open.length < 2) return open[0] ?? null;
|
|
192
|
+
// A parallel() default of the slot's name holds its slot id whichever
|
|
193
|
+
// intercept is open, so a candidate that has one proves nothing about what
|
|
194
|
+
// is on screen: among several, the ones without a default are the open ones.
|
|
195
|
+
const unshadowed = open.filter(
|
|
196
|
+
(c) => !c.entry.parallel?.[c.intercept.slotName],
|
|
197
|
+
);
|
|
198
|
+
const pool = unshadowed.length > 0 ? unshadowed : open;
|
|
199
|
+
if (pool.length === 1) return pool[0]!;
|
|
200
|
+
return (
|
|
201
|
+
pool.find((c) => evaluateInterceptWhen(c.intercept, selectorContext)) ??
|
|
202
|
+
pool[0]!
|
|
203
|
+
);
|
|
162
204
|
}
|
|
163
205
|
|
|
164
206
|
/**
|
|
@@ -357,7 +399,9 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
357
399
|
handlerResult =
|
|
358
400
|
typeof handler === "function"
|
|
359
401
|
? handleHandlerResult(
|
|
360
|
-
|
|
402
|
+
runAsSegment(slotSegmentId, () =>
|
|
403
|
+
runInSegmentTagScope(slotSegmentId, handler, context),
|
|
404
|
+
),
|
|
361
405
|
)
|
|
362
406
|
: handler;
|
|
363
407
|
} catch (error) {
|
|
@@ -70,6 +70,8 @@ import {
|
|
|
70
70
|
scopeReverse,
|
|
71
71
|
runInsideLoaderBodyScope,
|
|
72
72
|
runInsideLoaderRun,
|
|
73
|
+
runAsSegment,
|
|
74
|
+
segmentPinOf,
|
|
73
75
|
isInsidePushCallbackScope,
|
|
74
76
|
runInsidePushCallbackScope,
|
|
75
77
|
type LoaderRunIdentity,
|
|
@@ -871,8 +873,10 @@ function createLoaderExecutor<TEnv>(
|
|
|
871
873
|
Promise.resolve(
|
|
872
874
|
runInsideLoaderBodyScope(
|
|
873
875
|
() =>
|
|
874
|
-
|
|
875
|
-
|
|
876
|
+
runAsSegment(owningSegmentId, () =>
|
|
877
|
+
runInsideLoaderRun(identity, () =>
|
|
878
|
+
loaderFn(loaderCtx as LoaderContext<any, TEnv>),
|
|
879
|
+
),
|
|
876
880
|
),
|
|
877
881
|
loader.$$id,
|
|
878
882
|
recordedTags,
|
|
@@ -982,8 +986,7 @@ export function setupLoaderAccess<TEnv>(
|
|
|
982
986
|
if (isHandle(item)) {
|
|
983
987
|
const handle = item;
|
|
984
988
|
const store = handleStoreRef;
|
|
985
|
-
const segmentId = (ctx as InternalHandlerContext<any, TEnv>)
|
|
986
|
-
._currentSegmentId;
|
|
989
|
+
const segmentId = segmentPinOf(ctx as InternalHandlerContext<any, TEnv>);
|
|
987
990
|
|
|
988
991
|
if (!segmentId) {
|
|
989
992
|
throw new Error(
|
|
@@ -1057,8 +1060,7 @@ export function setupBuildUse<TEnv>(ctx: HandlerContext<any, TEnv>): void {
|
|
|
1057
1060
|
if (isHandle(item)) {
|
|
1058
1061
|
const handle = item;
|
|
1059
1062
|
const store = handleStoreRef;
|
|
1060
|
-
const segmentId = (ctx as InternalHandlerContext<any, TEnv>)
|
|
1061
|
-
._currentSegmentId;
|
|
1063
|
+
const segmentId = segmentPinOf(ctx as InternalHandlerContext<any, TEnv>);
|
|
1062
1064
|
|
|
1063
1065
|
if (!segmentId) {
|
|
1064
1066
|
throw new Error(
|
package/src/router/match-api.ts
CHANGED
|
@@ -410,23 +410,29 @@ export async function createMatchContextForPartial<TEnv>(
|
|
|
410
410
|
id.includes(".@"),
|
|
411
411
|
);
|
|
412
412
|
const skipInterceptForAction = isAction && !clientHasInterceptSegments;
|
|
413
|
+
// The names an intercept may target this route by, in precedence order:
|
|
414
|
+
// its own name, then its local name inside include().
|
|
415
|
+
const interceptNames =
|
|
416
|
+
snapshot.localRouteName !== matched.routeKey
|
|
417
|
+
? [matched.routeKey, snapshot.localRouteName]
|
|
418
|
+
: [matched.routeKey];
|
|
419
|
+
const findIntercept = (held?: ReadonlySet<string>) =>
|
|
420
|
+
findInterceptForRoute(
|
|
421
|
+
interceptNames,
|
|
422
|
+
snapshot.manifestEntry.parent,
|
|
423
|
+
interceptSelectorContext,
|
|
424
|
+
isAction,
|
|
425
|
+
held,
|
|
426
|
+
);
|
|
427
|
+
// An action re-renders the intercept the client holds, under either name
|
|
428
|
+
// (one walk over both, findInterceptForRoute); holding none, the first
|
|
429
|
+
// declared.
|
|
413
430
|
const interceptResult =
|
|
414
431
|
nav.isSameRouteNavigation || skipInterceptForAction
|
|
415
432
|
? null
|
|
416
|
-
:
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
interceptSelectorContext,
|
|
420
|
-
isAction,
|
|
421
|
-
) ||
|
|
422
|
-
(snapshot.localRouteName !== matched.routeKey
|
|
423
|
-
? findInterceptForRoute(
|
|
424
|
-
snapshot.localRouteName,
|
|
425
|
-
snapshot.manifestEntry.parent,
|
|
426
|
-
interceptSelectorContext,
|
|
427
|
-
isAction,
|
|
428
|
-
)
|
|
429
|
-
: null);
|
|
433
|
+
: isAction
|
|
434
|
+
? findIntercept(nav.clientSegmentSet) || findIntercept()
|
|
435
|
+
: findIntercept();
|
|
430
436
|
|
|
431
437
|
// The page's copy of a clientUrls() layout() loader under the route it
|
|
432
438
|
// leaves holds the destination route's (client-urls/layout-loader-copies.ts).
|
|
@@ -451,24 +457,21 @@ export async function createMatchContextForPartial<TEnv>(
|
|
|
451
457
|
}
|
|
452
458
|
|
|
453
459
|
// Whether an intercept targets this route at all: the same walk as above
|
|
454
|
-
// with isAction=true, which skips when(). If one does, the
|
|
455
|
-
// on the source even when none applied (a when() that
|
|
456
|
-
// same-route navigation), so it must not share the
|
|
457
|
-
// slot (#1007). The walk is over the TARGET's
|
|
458
|
-
// intercept targets never depends on the source this
|
|
459
|
-
|
|
460
|
-
findInterceptForRoute(
|
|
461
|
-
routeKey,
|
|
462
|
-
snapshot.manifestEntry.parent,
|
|
463
|
-
null,
|
|
464
|
-
true,
|
|
465
|
-
) !== null;
|
|
460
|
+
// with isAction=true and nothing held, which skips when(). If one does, the
|
|
461
|
+
// response depends on the source even when none applied (a when() that
|
|
462
|
+
// rejected it, a same-route navigation), so it must not share the
|
|
463
|
+
// source-agnostic prefetch slot (#1007). The walk is over the TARGET's
|
|
464
|
+
// chain, so a route no intercept targets never depends on the source this
|
|
465
|
+
// way.
|
|
466
466
|
const interceptTargeted =
|
|
467
467
|
!!interceptResult ||
|
|
468
468
|
(!skipInterceptForAction &&
|
|
469
|
-
(
|
|
470
|
-
|
|
471
|
-
|
|
469
|
+
findInterceptForRoute(
|
|
470
|
+
interceptNames,
|
|
471
|
+
snapshot.manifestEntry.parent,
|
|
472
|
+
null,
|
|
473
|
+
true,
|
|
474
|
+
) !== null);
|
|
472
475
|
|
|
473
476
|
const isIntercept = !!interceptResult;
|
|
474
477
|
// A normal-route navigation may replay a PPR shell's canonical segment
|
|
@@ -489,6 +492,7 @@ export async function createMatchContextForPartial<TEnv>(
|
|
|
489
492
|
snapshot.entries,
|
|
490
493
|
partialDeferralMode({
|
|
491
494
|
fill: kind === "fill",
|
|
495
|
+
// Speculative only: an intent prefetch defers nothing.
|
|
492
496
|
prefetch: kind === "prefetch",
|
|
493
497
|
method: request.method,
|
|
494
498
|
isAction,
|
|
@@ -239,6 +239,11 @@ export interface MatchPipelineState {
|
|
|
239
239
|
// Whether cache should be revalidated (SWR)
|
|
240
240
|
shouldRevalidate?: boolean;
|
|
241
241
|
|
|
242
|
+
// Whether the prerender entry that answered is past its soft staleAt: it
|
|
243
|
+
// serves while a refresh is due, so it is not what the page will hold once
|
|
244
|
+
// that lands (MatchResult.fromStorage counts it live)
|
|
245
|
+
storedStale?: boolean;
|
|
246
|
+
|
|
242
247
|
// Source of cache hit ("runtime" or "prerender")
|
|
243
248
|
cacheSource?: "runtime" | "prerender";
|
|
244
249
|
|
|
@@ -155,10 +155,11 @@ export interface MatchHandlerDeps<TEnv = any> {
|
|
|
155
155
|
defaultErrorBoundary: ReactNode | ErrorBoundaryHandler | undefined;
|
|
156
156
|
findMatch: (pathname: string, ms?: any) => any;
|
|
157
157
|
findInterceptForRoute: (
|
|
158
|
-
|
|
158
|
+
routeKeys: string | readonly string[],
|
|
159
159
|
parentEntry: EntryData | null,
|
|
160
160
|
selectorContext: InterceptSelectorContext | null,
|
|
161
161
|
isAction: boolean,
|
|
162
|
+
held?: ReadonlySet<string>,
|
|
162
163
|
) => { intercept: InterceptEntry; entry: EntryData } | null;
|
|
163
164
|
telemetry?: TelemetrySink;
|
|
164
165
|
/**
|
|
@@ -83,6 +83,7 @@ import {
|
|
|
83
83
|
import { ShellRecordUnavailableError } from "../../cache/shell-snapshot.js";
|
|
84
84
|
import { prerenderStoreShortCircuits } from "../navigation-snapshot.js";
|
|
85
85
|
import { paramsEqual } from "../params-util.js";
|
|
86
|
+
import { markSourceHeld, readsOfAllParams } from "../source-reads.js";
|
|
86
87
|
import { requestHeaders } from "../../server/request-headers.js";
|
|
87
88
|
import { isInterceptSegment } from "../../browser/intercept-utils.js";
|
|
88
89
|
import {
|
|
@@ -144,7 +145,8 @@ async function ensurePrerenderDeps(): Promise<void> {
|
|
|
144
145
|
* - `prerender-build`: the bundled build manifest (immutable).
|
|
145
146
|
* - `prerender-overlay`: the writable on-demand overlay. Its entry may be
|
|
146
147
|
* newer than the client's copy: one that a refresh (possibly inside this
|
|
147
|
-
* very action) replaced.
|
|
148
|
+
* very action) replaced. `stale`: it is past its soft `staleAt` (serves
|
|
149
|
+
* while a refresh is due).
|
|
148
150
|
*/
|
|
149
151
|
type StoredSegmentSource =
|
|
150
152
|
| {
|
|
@@ -154,7 +156,8 @@ type StoredSegmentSource =
|
|
|
154
156
|
scope: CacheScope;
|
|
155
157
|
restoreHandles?: (sentIds: ReadonlySet<string>) => Promise<void>;
|
|
156
158
|
}
|
|
157
|
-
| { kind: "prerender-build"
|
|
159
|
+
| { kind: "prerender-build"; entry: PrerenderEntry }
|
|
160
|
+
| { kind: "prerender-overlay"; entry: PrerenderEntry; stale: boolean };
|
|
158
161
|
|
|
159
162
|
/**
|
|
160
163
|
* Decode a prerender entry's segments and replay its handle data into the
|
|
@@ -313,12 +316,15 @@ function cachedSegmentSends<TEnv>(
|
|
|
313
316
|
* Whether a prerender entry's segment is sent. On a partial navigation the
|
|
314
317
|
* client keeps the segments it holds. The route's own segments (an
|
|
315
318
|
* intercept's included) are sent again when the params changed (e.g., a
|
|
316
|
-
* different guide slug)
|
|
317
|
-
*
|
|
319
|
+
* different guide slug), and kept on equal params whatever the search: a
|
|
320
|
+
* prerender does not vary by search. An overlay entry may be newer than the
|
|
321
|
+
* client's copy: on an action or a stale request they are re-sent too, as a live
|
|
318
322
|
* action re-render re-sends them (revalidation.ts "action:route-segment");
|
|
319
323
|
* the immutable bundled manifest keeps the client's copy. A held segment
|
|
320
324
|
* above the route stays held, as a live walk keeps it ("nav:non-route-skip"),
|
|
321
|
-
* unless its revalidate() rules ask for it when the params changed.
|
|
325
|
+
* unless its revalidate() rules ask for it when the params changed. A route
|
|
326
|
+
* segment, or one with rules, kept on equal params marks the match
|
|
327
|
+
* source-held: the decision hangs on the source's params.
|
|
322
328
|
*
|
|
323
329
|
* Decided once per decoded segment, by object: an intercept variant can hold
|
|
324
330
|
* a layout's parallel and the intercept's slot under one id. decodeStoredEntry
|
|
@@ -333,9 +339,20 @@ function storedEntrySends<TEnv>(
|
|
|
333
339
|
const paramsChanged = !paramsEqual(ctx.matched.params, ctx.prevParams);
|
|
334
340
|
const resendRouteSegments = replaceable && (ctx.isAction || ctx.stale);
|
|
335
341
|
const entryRevalidateMap =
|
|
336
|
-
|
|
337
|
-
? router.buildEntryRevalidateMap(ctx.entries)
|
|
342
|
+
ctx.clientSegmentSet.size > 0
|
|
343
|
+
? router.buildEntryRevalidateMap?.(ctx.entries)
|
|
338
344
|
: undefined;
|
|
345
|
+
// A keep that hangs on the source's params is right for the pages with
|
|
346
|
+
// the same params only: scope a prefetch of the answer to them, as a
|
|
347
|
+
// predicate that compared every param would (#1331).
|
|
348
|
+
const heldForSource = (): false => {
|
|
349
|
+
markSourceHeld(
|
|
350
|
+
ctx.handlerContext as InternalHandlerContext<any, TEnv>,
|
|
351
|
+
readsOfAllParams(ctx.prevParams),
|
|
352
|
+
{ params: ctx.prevParams, routeName: undefined },
|
|
353
|
+
);
|
|
354
|
+
return false;
|
|
355
|
+
};
|
|
339
356
|
const decide = (segment: ResolvedSegment): boolean | Promise<boolean> => {
|
|
340
357
|
if (!ctx.clientSegmentSet.has(segment.id)) return true;
|
|
341
358
|
// A fill keeps it (fillSends).
|
|
@@ -346,10 +363,16 @@ function storedEntrySends<TEnv>(
|
|
|
346
363
|
);
|
|
347
364
|
if (fill !== undefined) return fill;
|
|
348
365
|
if (segment.type === "route" || segment.belongsToRoute === true) {
|
|
349
|
-
|
|
366
|
+
if (paramsChanged || resendRouteSegments) return true;
|
|
367
|
+
// Kept on equal params, whatever the search: a source with other
|
|
368
|
+
// params is sent it.
|
|
369
|
+
return heldForSource();
|
|
350
370
|
}
|
|
351
371
|
const revalidate = entryRevalidateMap?.get(segment.id)?.revalidate;
|
|
352
372
|
if (!revalidate?.length) return false;
|
|
373
|
+
// Kept on equal params: from another source the rules run and may send
|
|
374
|
+
// it.
|
|
375
|
+
if (!paramsChanged) return heldForSource();
|
|
353
376
|
return router.evaluateRevalidation({
|
|
354
377
|
segment,
|
|
355
378
|
prevParams: ctx.prevParams,
|
|
@@ -546,6 +569,11 @@ async function* serveStoredSegments<TEnv>(
|
|
|
546
569
|
state.cacheHit = true;
|
|
547
570
|
state.cacheSource = fromCache ? "runtime" : "prerender";
|
|
548
571
|
if (fromCache) state.shouldRevalidate = source.shouldRevalidate;
|
|
572
|
+
// Not shouldRevalidate: that one starts the cache() scope's background
|
|
573
|
+
// revalidation.
|
|
574
|
+
if (source.kind === "prerender-overlay" && source.stale) {
|
|
575
|
+
state.storedStale = true;
|
|
576
|
+
}
|
|
549
577
|
state.cachedSegments = segments;
|
|
550
578
|
const cachedMatchedIds = [...liveMatchedIds, ...segments.map((s) => s.id)];
|
|
551
579
|
state.cachedMatchedIds = cachedMatchedIds;
|
|
@@ -790,11 +818,8 @@ async function readPrerenderSource<TEnv>(
|
|
|
790
818
|
// A stale "removed" marker is rechecked like a stale page: only a
|
|
791
819
|
// refresh that hit notFound() (or declined) stamps one with a ttl or
|
|
792
820
|
// tags. A prerender.remove() marker has neither and never gets here.
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
reqCtx &&
|
|
796
|
-
isStoredEntryStale(stored, Date.now())
|
|
797
|
-
) {
|
|
821
|
+
const stale = isStoredEntryStale(stored, Date.now());
|
|
822
|
+
if (overlay.config.onRevalidate && reqCtx && stale) {
|
|
798
823
|
scheduleOverlayRevalidation(
|
|
799
824
|
serializePrerenderKey(key),
|
|
800
825
|
overlay.config.onRevalidate,
|
|
@@ -816,7 +841,7 @@ async function readPrerenderSource<TEnv>(
|
|
|
816
841
|
)._prerenderRemoved = true;
|
|
817
842
|
return null;
|
|
818
843
|
}
|
|
819
|
-
return { kind: "prerender-overlay", entry: stored.entry };
|
|
844
|
+
return { kind: "prerender-overlay", entry: stored.entry, stale };
|
|
820
845
|
}
|
|
821
846
|
}
|
|
822
847
|
|
|
@@ -107,11 +107,14 @@
|
|
|
107
107
|
* 3. Diff against previous state with diff[]
|
|
108
108
|
* 4. Render slot content if slots present
|
|
109
109
|
*/
|
|
110
|
-
import type {
|
|
110
|
+
import type {
|
|
111
|
+
InternalHandlerContext,
|
|
112
|
+
MatchResult,
|
|
113
|
+
ResolvedSegment,
|
|
114
|
+
} from "../types.js";
|
|
111
115
|
import { isPlaceholder } from "../types/segments.js";
|
|
112
116
|
import { interceptMatched } from "../browser/intercept-utils.js";
|
|
113
117
|
import type { MatchContext, MatchPipelineState } from "./match-context.js";
|
|
114
|
-
import { debugLog } from "./logging.js";
|
|
115
118
|
import { appendMetric } from "./metrics.js";
|
|
116
119
|
import {
|
|
117
120
|
collectInterceptTargetNames,
|
|
@@ -128,83 +131,11 @@ export async function collectSegments(
|
|
|
128
131
|
return segments;
|
|
129
132
|
}
|
|
130
133
|
|
|
131
|
-
/**
|
|
132
|
-
* Deduplicate inherited loader segments by loaderId.
|
|
133
|
-
*
|
|
134
|
-
* When a route has loaders and a child layout has parallel slots, the same
|
|
135
|
-
* loader is resolved twice: once for the route and once inherited into the
|
|
136
|
-
* layout (tagged with `_inherited`). The inherited copy is only needed when
|
|
137
|
-
* the route uses `loading()` — in that case, the loader data is inside a
|
|
138
|
-
* LoaderBoundary/Suspense that parallel slots can't reach through. Without
|
|
139
|
-
* loading(), useLoader() traverses parent contexts and finds the data.
|
|
140
|
-
*/
|
|
141
|
-
function deduplicateLoaderSegments(
|
|
142
|
-
segments: ResolvedSegment[],
|
|
143
|
-
logPrefix: string,
|
|
144
|
-
): { segments: ResolvedSegment[]; removedIds: Set<string> } {
|
|
145
|
-
// Single pass: original (non-inherited) loaderIds, all loaderIds grouped by
|
|
146
|
-
// namespace, and namespaces of segments that declare loading().
|
|
147
|
-
const originalLoaders = new Set<string>();
|
|
148
|
-
const loaderIdsByNamespace = new Map<string, string[]>();
|
|
149
|
-
const namespacesWithLoading = new Set<string>();
|
|
150
|
-
for (const s of segments) {
|
|
151
|
-
if (s.type === "loader" && s.loaderId) {
|
|
152
|
-
if (!s._inherited) originalLoaders.add(s.loaderId);
|
|
153
|
-
const ids = loaderIdsByNamespace.get(s.namespace);
|
|
154
|
-
if (ids) ids.push(s.loaderId);
|
|
155
|
-
else loaderIdsByNamespace.set(s.namespace, [s.loaderId]);
|
|
156
|
-
} else if (
|
|
157
|
-
s.type !== "loader" &&
|
|
158
|
-
s.loading !== undefined &&
|
|
159
|
-
s.loading !== false
|
|
160
|
-
) {
|
|
161
|
-
namespacesWithLoading.add(s.namespace);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const loadersWithLoading = new Set<string>();
|
|
166
|
-
for (const ns of namespacesWithLoading) {
|
|
167
|
-
for (const id of loaderIdsByNamespace.get(ns) ?? []) {
|
|
168
|
-
loadersWithLoading.add(id);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const result: ResolvedSegment[] = [];
|
|
173
|
-
const removedIds = new Set<string>();
|
|
174
|
-
|
|
175
|
-
for (const s of segments) {
|
|
176
|
-
if (
|
|
177
|
-
s.type === "loader" &&
|
|
178
|
-
s.loaderId &&
|
|
179
|
-
s._inherited &&
|
|
180
|
-
originalLoaders.has(s.loaderId) &&
|
|
181
|
-
!loadersWithLoading.has(s.loaderId)
|
|
182
|
-
) {
|
|
183
|
-
removedIds.add(s.id);
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
result.push(s);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (removedIds.size > 0) {
|
|
190
|
-
debugLog(
|
|
191
|
-
logPrefix,
|
|
192
|
-
`deduped ${removedIds.size} inherited loader segment(s)`,
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return { segments: result, removedIds };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
134
|
export function buildMatchResult<TEnv>(
|
|
200
135
|
allSegments: ResolvedSegment[],
|
|
201
136
|
ctx: MatchContext<TEnv>,
|
|
202
137
|
state: MatchPipelineState,
|
|
203
138
|
): MatchResult {
|
|
204
|
-
const logPrefix = ctx.isFullMatch
|
|
205
|
-
? "[Router.match]"
|
|
206
|
-
: "[Router.matchPartial]";
|
|
207
|
-
|
|
208
139
|
let allIds: string[];
|
|
209
140
|
let segmentsToRender: ResolvedSegment[];
|
|
210
141
|
let resolvedIds: string[];
|
|
@@ -267,19 +198,11 @@ export function buildMatchResult<TEnv>(
|
|
|
267
198
|
}
|
|
268
199
|
}
|
|
269
200
|
|
|
270
|
-
|
|
271
|
-
segmentsToRender,
|
|
272
|
-
logPrefix,
|
|
273
|
-
);
|
|
274
|
-
|
|
275
|
-
const matchedIds =
|
|
276
|
-
removedIds.size > 0 ? allIds.filter((id) => !removedIds.has(id)) : allIds;
|
|
277
|
-
|
|
278
|
-
// One pass over dedupedSegments: strip the internal _handlerRan marker and
|
|
201
|
+
// One pass over segmentsToRender: strip the internal _handlerRan marker and
|
|
279
202
|
// collect the diff ids (id is unchanged by the strip) together.
|
|
280
203
|
const cleanedSegments: ResolvedSegment[] = [];
|
|
281
204
|
const diff: string[] = [];
|
|
282
|
-
for (const s of
|
|
205
|
+
for (const s of segmentsToRender) {
|
|
283
206
|
if (s._handlerRan === undefined) {
|
|
284
207
|
cleanedSegments.push(s);
|
|
285
208
|
} else {
|
|
@@ -293,10 +216,13 @@ export function buildMatchResult<TEnv>(
|
|
|
293
216
|
// origin — the browser-local clientUrls matcher declines its optimistic
|
|
294
217
|
// presentation for these (see collectInterceptTargetNames).
|
|
295
218
|
const interceptTargets = collectInterceptTargetNames(ctx.manifestEntry);
|
|
219
|
+
const sourceHeld = (
|
|
220
|
+
ctx.handlerContext as InternalHandlerContext<any, TEnv> | undefined
|
|
221
|
+
)?._sourceHeld;
|
|
296
222
|
|
|
297
223
|
return {
|
|
298
224
|
segments: cleanedSegments,
|
|
299
|
-
matched:
|
|
225
|
+
matched: allIds,
|
|
300
226
|
diff,
|
|
301
227
|
resolvedIds,
|
|
302
228
|
params: ctx.matched.params,
|
|
@@ -304,11 +230,45 @@ export function buildMatchResult<TEnv>(
|
|
|
304
230
|
slots: Object.keys(state.slots).length > 0 ? state.slots : undefined,
|
|
305
231
|
...(interceptTargets.length > 0 ? { interceptTargets } : {}),
|
|
306
232
|
...(ctx.interceptTargeted ? { interceptTargeted: true } : {}),
|
|
233
|
+
...(sourceHeld ? { sourceHeld } : {}),
|
|
234
|
+
...(!ctx.isFullMatch && sendsOnlyStored(segmentsToRender, state)
|
|
235
|
+
? { fromStorage: true as const }
|
|
236
|
+
: {}),
|
|
307
237
|
routeMiddleware:
|
|
308
238
|
ctx.routeMiddleware.length > 0 ? ctx.routeMiddleware : undefined,
|
|
309
239
|
};
|
|
310
240
|
}
|
|
311
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Whether a partial answer sends only what the cache lookup decoded from
|
|
244
|
+
* storage (MatchResult.fromStorage). By object, not by id: a
|
|
245
|
+
* segment the live walk resolved under a stored one's id (a live layout above
|
|
246
|
+
* a cache() boundary, an intercept resolved live) is not the stored copy.
|
|
247
|
+
* Any loader segment ran in this request, so it is live. Defaults to live:
|
|
248
|
+
* counted stored by mistake, a segment brings back the cached failures #1170
|
|
249
|
+
* removed. A stale cache() record (state.shouldRevalidate, refreshed in the
|
|
250
|
+
* background), a stale on-demand Prerender overlay entry (state.storedStale,
|
|
251
|
+
* which only the overlay can be: the build manifest never goes stale) and an
|
|
252
|
+
* answer that sends nothing count as live too: the browser would keep the
|
|
253
|
+
* first two past their refresh, and the third is a source page's answer.
|
|
254
|
+
*/
|
|
255
|
+
function sendsOnlyStored(
|
|
256
|
+
sent: readonly ResolvedSegment[],
|
|
257
|
+
state: MatchPipelineState,
|
|
258
|
+
): boolean {
|
|
259
|
+
if (
|
|
260
|
+
!state.cacheHit ||
|
|
261
|
+
!state.cachedSegments ||
|
|
262
|
+
state.shouldRevalidate ||
|
|
263
|
+
state.storedStale ||
|
|
264
|
+
sent.length === 0
|
|
265
|
+
) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
const stored = new Set(state.cachedSegments);
|
|
269
|
+
return sent.every((s) => s.type !== "loader" && stored.has(s));
|
|
270
|
+
}
|
|
271
|
+
|
|
312
272
|
export async function collectMatchResult<TEnv>(
|
|
313
273
|
pipeline: AsyncGenerator<ResolvedSegment>,
|
|
314
274
|
ctx: MatchContext<TEnv>,
|
|
@@ -78,6 +78,12 @@ export interface MiddlewareContext<
|
|
|
78
78
|
|
|
79
79
|
setTheme?: (theme: Theme) => void;
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Attach location state entries to this response (see the handler
|
|
83
|
+
* context's `setLocationState`). Entries accumulate across definitions;
|
|
84
|
+
* for one definition the last write wins. Set it here rather than in a
|
|
85
|
+
* handler on a `ppr` route: a shell HIT runs no handler.
|
|
86
|
+
*/
|
|
81
87
|
setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void;
|
|
82
88
|
|
|
83
89
|
/** Global route names only; see {@link GlobalReverseFunction}. */
|