@rangojs/router 0.32.0 → 0.33.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 -10173
- package/dist/bin/runtime-EPUDEHO4.js +596 -0
- package/dist/bin/runtime-discovery-3HH6BP3K.js +352 -0
- package/dist/bin/state-ACD2MGKJ.js +16 -0
- package/dist/bin/temp-rsc-server-JALTIWLT.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/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 +15 -6
- 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/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/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 +5 -2
- package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
- package/dist/types/rsc/request-stages.d.ts +2 -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 +5 -2
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +43 -7
- package/dist/types/testing/e2e/server.d.ts +16 -1
- package/dist/types/testing/render-route.d.ts +66 -2
- package/dist/types/testing/serve-shell-request.d.ts +6 -3
- package/dist/types/types/handler-context.d.ts +7 -5
- package/dist/types/types/loader-types.d.ts +7 -4
- package/dist/types/types/segments.d.ts +15 -4
- package/dist/types/urls/path-helper-types.d.ts +7 -5
- package/dist/types/vite/temp-rsc-server.d.ts +2 -2
- package/dist/vite/index.js +449 -189
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +81 -0
- package/skills/app-structure/e-commerce.md +10 -9
- package/skills/app-structure/large-app.md +18 -2
- package/skills/caching/SKILL.md +10 -5
- package/skills/client-urls/SKILL.md +29 -11
- package/skills/comparison/references/framework-comparison.md +13 -8
- package/skills/document-cache/SKILL.md +8 -0
- package/skills/hooks/navigation.md +8 -8
- package/skills/hooks/outlets.md +4 -0
- package/skills/hooks/state.md +24 -8
- package/skills/intercept/SKILL.md +33 -12
- package/skills/layout/SKILL.md +60 -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 +26 -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 +41 -8
- package/skills/testing/client-components.md +29 -13
- package/skills/testing/serve-shell-request.md +7 -7
- package/skills/testing/server-actions.md +8 -0
- package/skills/ux-architecture/SKILL.md +70 -19
- 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/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 +20 -9
- package/src/browser/react/use-navigation.ts +1 -1
- package/src/browser/rsc-router.tsx +8 -1
- package/src/browser/server-action-bridge.ts +16 -11
- 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/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 +27 -0
- package/src/cache/document-cache.ts +5 -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/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 +21 -13
- package/src/router/match-result.ts +39 -87
- package/src/router/navigation-snapshot.ts +18 -3
- package/src/router/prefetch-cache-ttl.ts +15 -3
- package/src/router/revalidation.ts +53 -23
- 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 +8 -3
- package/src/rsc/prefetch-render-error.ts +7 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/request-stages.ts +59 -0
- package/src/rsc/rsc-rendering.ts +49 -33
- 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 +23 -2
- package/src/server/handle-store.ts +2 -0
- package/src/server/request-context.ts +113 -28
- package/src/testing/dispatch.ts +3 -0
- package/src/testing/e2e/fixture.ts +4 -12
- package/src/testing/e2e/server.ts +91 -4
- package/src/testing/render-route.tsx +269 -4
- package/src/testing/serve-shell-request.ts +6 -3
- package/src/types/handler-context.ts +7 -5
- package/src/types/loader-types.ts +7 -4
- package/src/types/segments.ts +15 -4
- package/src/urls/path-helper-types.ts +7 -5
- package/src/vite/temp-rsc-server.ts +2 -2
|
@@ -71,6 +71,7 @@ export function shouldCacheResponse(
|
|
|
71
71
|
RequestContext,
|
|
72
72
|
| "_payloadVisitorTheme"
|
|
73
73
|
| "_payloadDeferred"
|
|
74
|
+
| "_payloadSourceScoped"
|
|
74
75
|
| "_payloadVisitorLocation"
|
|
75
76
|
| "_documentLocationState"
|
|
76
77
|
>,
|
|
@@ -118,10 +119,36 @@ export function shouldCacheResponse(
|
|
|
118
119
|
return null;
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
// A partial scoped to its source page: this cache keys it by
|
|
123
|
+
// URL and the ids the client holds, so it would answer another page with
|
|
124
|
+
// the same ids. rsc-rendering.ts sends it NOT_REUSED; a middleware can
|
|
125
|
+
// replace that after next().
|
|
126
|
+
if (requestCtx?._payloadSourceScoped) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
121
130
|
const cacheControl = response.headers.get("Cache-Control");
|
|
122
131
|
return parseCacheControl(cacheControl);
|
|
123
132
|
}
|
|
124
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Whether a response carries one client's state, which a stored copy would
|
|
136
|
+
* replay to that client's later requests: a per-client signal header (a state
|
|
137
|
+
* cookie rotation, `x-rango-keep-cache`) or location state. The stored
|
|
138
|
+
* prefetch Cache-Control (rsc/request-stages.ts) is not sent on
|
|
139
|
+
* one.
|
|
140
|
+
*/
|
|
141
|
+
export function carriesClientState(
|
|
142
|
+
response: Response,
|
|
143
|
+
requestCtx: Pick<RequestContext, "_locationState" | "_documentLocationState">,
|
|
144
|
+
): boolean {
|
|
145
|
+
return (
|
|
146
|
+
hasPerClientSignal(response.headers) ||
|
|
147
|
+
(requestCtx._locationState?.length ?? 0) > 0 ||
|
|
148
|
+
requestCtx._documentLocationState === true
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
125
152
|
/**
|
|
126
153
|
* Whether this cache will store the response the current render produces, as
|
|
127
154
|
* far as the render can tell: the middleware runs it (a MISS or a stale
|
|
@@ -357,6 +357,11 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
357
357
|
if (!cached.shouldRevalidate) {
|
|
358
358
|
// Fresh hit - return immediately
|
|
359
359
|
log(`[DocumentCache] HIT ${typeLabel}: ${url.pathname}`);
|
|
360
|
+
// Nothing in it runs: its loaders' output is replayed, and no
|
|
361
|
+
// failed render is ever stored (throwIfRenderErrored). A prefetch
|
|
362
|
+
// answered with it leaves with the router's prefetch Cache-Control
|
|
363
|
+
// (rsc/request-stages.ts).
|
|
364
|
+
requestCtx._servedFromStorage = true;
|
|
360
365
|
return drainOnResponseCallbacks(
|
|
361
366
|
addCacheStatusHeader(cached.response, "HIT"),
|
|
362
367
|
requestCtx,
|
|
@@ -120,13 +120,14 @@ export interface MemorySegmentCacheStoreOptions<TEnv = unknown> {
|
|
|
120
120
|
|
|
121
121
|
/**
|
|
122
122
|
* Custom key generator applied to all cache operations.
|
|
123
|
-
* Receives the full RequestContext and the default-generated key.
|
|
123
|
+
* Receives the full RequestContext and the default-generated key. A route
|
|
124
|
+
* record keeps its request kind (see SegmentCacheStore.keyGenerator).
|
|
124
125
|
*
|
|
125
126
|
* @example
|
|
126
127
|
* ```typescript
|
|
127
128
|
* keyGenerator: (ctx, defaultKey) => {
|
|
128
129
|
* const locale = cookies().get('locale')?.value || 'en';
|
|
129
|
-
* return `${
|
|
130
|
+
* return `${defaultKey}|${locale}`;
|
|
130
131
|
* }
|
|
131
132
|
* ```
|
|
132
133
|
*/
|
|
@@ -309,8 +309,8 @@ export async function serializeSegments(
|
|
|
309
309
|
* cached with the route record).
|
|
310
310
|
*
|
|
311
311
|
* The loading "null" sentinel decodes here (not on the consumer): loading:null
|
|
312
|
-
* must survive as null, and shipping the raw sentinel would
|
|
313
|
-
*
|
|
312
|
+
* must survive as null, and shipping the raw sentinel would hand renderSegments
|
|
313
|
+
* a truthy `loading`, the fallback tree shape (docs/tree-structure.md).
|
|
314
314
|
*/
|
|
315
315
|
export async function fragmentSegments(
|
|
316
316
|
data: SerializedSegmentData[],
|
|
@@ -407,7 +407,7 @@ const PIN_KEY_PREFIX = /^[^D.]*D\d+\./;
|
|
|
407
407
|
* Deserializations run in parallel; a record that fails to decode is skipped
|
|
408
408
|
* and reported to `onUndecodable`: its loader runs instead, which a document
|
|
409
409
|
* HIT has already committed a prelude against, so serveShellHit retires the
|
|
410
|
-
* entry
|
|
410
|
+
* entry. Returns undefined when the snapshot carries no loader
|
|
411
411
|
* records, without touching the Flight codec (kept lazy for cold paths and
|
|
412
412
|
* non-RSC configs).
|
|
413
413
|
*/
|
package/src/cache/types.ts
CHANGED
|
@@ -93,16 +93,21 @@ export interface SegmentCacheStore<TEnv = unknown> {
|
|
|
93
93
|
/**
|
|
94
94
|
* Custom key generator applied to all cache operations using this store.
|
|
95
95
|
* Receives the full RequestContext and the default-generated key.
|
|
96
|
-
*
|
|
96
|
+
* The return value is the cache key (unless route overrides with `key`
|
|
97
|
+
* option), with a route record's request kind appended when it does not
|
|
98
|
+
* start with that kind (below).
|
|
97
99
|
*
|
|
98
100
|
* Resolution priority:
|
|
99
101
|
* 1. Route-level `key` function (replaces the URL part; the request kind stays)
|
|
100
102
|
* 2. Store-level `keyGenerator` (modifies default key)
|
|
101
103
|
* 3. Default key generation (prefix:pathname:params)
|
|
102
104
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
105
|
+
* A route record keeps its request kind (a document, a navigation or an
|
|
106
|
+
* intercept navigation), whose records hold different segments: the kind
|
|
107
|
+
* is `defaultKey`'s prefix (`doc:`, `partial:`, `intercept:<intercept>:`),
|
|
108
|
+
* and a result that does not start with it gets the kind appended as a
|
|
109
|
+
* key part of its own. Build on `defaultKey` (`${defaultKey}|${segment}`)
|
|
110
|
+
* and the result is the record key byte for byte.
|
|
106
111
|
*
|
|
107
112
|
* Return `defaultKey` unchanged to leave a request unpartitioned. An empty
|
|
108
113
|
* string is not a key: where the result partitions a nested cache() on
|
|
@@ -114,7 +119,7 @@ export interface SegmentCacheStore<TEnv = unknown> {
|
|
|
114
119
|
* ```typescript
|
|
115
120
|
* keyGenerator: (ctx, defaultKey) => {
|
|
116
121
|
* const segment = ctx.request.headers.get('x-user-segment') || 'default';
|
|
117
|
-
* return `${
|
|
122
|
+
* return `${defaultKey}|${segment}`;
|
|
118
123
|
* }
|
|
119
124
|
* ```
|
|
120
125
|
*
|
|
@@ -122,7 +127,7 @@ export interface SegmentCacheStore<TEnv = unknown> {
|
|
|
122
127
|
* ```typescript
|
|
123
128
|
* keyGenerator: (ctx, defaultKey) => {
|
|
124
129
|
* const region = ctx.env.REGION || 'us';
|
|
125
|
-
* return `${
|
|
130
|
+
* return `${defaultKey}|${region}`;
|
|
126
131
|
* }
|
|
127
132
|
* ```
|
|
128
133
|
*
|
|
@@ -130,7 +135,7 @@ export interface SegmentCacheStore<TEnv = unknown> {
|
|
|
130
135
|
* ```typescript
|
|
131
136
|
* keyGenerator: (ctx, defaultKey) => {
|
|
132
137
|
* const locale = cookies().get('locale')?.value || 'en';
|
|
133
|
-
* return `${
|
|
138
|
+
* return `${defaultKey}|${locale}`;
|
|
134
139
|
* }
|
|
135
140
|
* ```
|
|
136
141
|
*/
|
|
@@ -356,7 +356,8 @@ export interface VercelCacheStoreOptions<TEnv = unknown> {
|
|
|
356
356
|
|
|
357
357
|
/**
|
|
358
358
|
* Custom key generator applied to all cache operations (region/locale/user
|
|
359
|
-
* segmentation). Receives the request context and the default key.
|
|
359
|
+
* segmentation). Receives the request context and the default key. A route
|
|
360
|
+
* record keeps its request kind (see SegmentCacheStore.keyGenerator).
|
|
360
361
|
*/
|
|
361
362
|
keyGenerator?: (
|
|
362
363
|
ctx: RequestContext<TEnv>,
|
package/src/client-urls/types.ts
CHANGED
|
@@ -85,8 +85,9 @@ export interface ClientUrlLoaderRecord {
|
|
|
85
85
|
*/
|
|
86
86
|
readonly ssr?: false;
|
|
87
87
|
/**
|
|
88
|
-
* loader(Def, { prefetch: false }): a
|
|
89
|
-
* intercept's target from its source, defers this loader
|
|
88
|
+
* loader(Def, { prefetch: false }): a viewport or render prefetch of the
|
|
89
|
+
* route, or of an intercept's target from its source, defers this loader
|
|
90
|
+
* (a hover prefetch runs it) and the click
|
|
90
91
|
* that adopts it runs it in its fill request (see {@link LoaderOptions}).
|
|
91
92
|
* Projected onto the materialized stub.
|
|
92
93
|
*/
|
|
@@ -169,12 +170,13 @@ export interface ClientUrlHelpers {
|
|
|
169
170
|
* holes).
|
|
170
171
|
*
|
|
171
172
|
* Pass `{ prefetch: false }` to keep this loader out of the route's
|
|
172
|
-
*
|
|
173
|
+
* viewport and render prefetches; a hover prefetch runs it (see
|
|
174
|
+
* {@link LoaderOptions}). An in-group click still shows
|
|
173
175
|
* the destination at once, commits the adopted prefetch with the other
|
|
174
176
|
* loaders' data, and runs only the flagged loaders in its one fill
|
|
175
177
|
* request. A `layout()` loader with the flag is flagged on every route
|
|
176
|
-
* below it. Inside intercept() it defers the loader in a
|
|
177
|
-
* intercept from its source, as in urls().
|
|
178
|
+
* below it. Inside intercept() it defers the loader in a viewport or
|
|
179
|
+
* render prefetch of the intercept from its source, as in urls().
|
|
178
180
|
*/
|
|
179
181
|
readonly loader: <TData>(
|
|
180
182
|
definition: LoaderDefinition<TData>,
|
package/src/prefetch-markers.ts
CHANGED
|
@@ -9,6 +9,16 @@
|
|
|
9
9
|
/** Request header of a `<Link>` prefetch (browser/prefetch/fetch.ts). */
|
|
10
10
|
export const PREFETCH_HEADER = "X-Rango-Prefetch";
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* PREFETCH_HEADER's value on an intent prefetch: a hover or an explicit
|
|
14
|
+
* `prefetch()` call (browser/prefetch/fetch.ts prefetchDirect). The server
|
|
15
|
+
* defers nothing in it: `prefetch: false` loaders and the work behind a
|
|
16
|
+
* flagged `loading()` run, as in the navigation it starts early. A
|
|
17
|
+
* speculative prefetch (viewport, render) sends `1`, and any value other than
|
|
18
|
+
* this one reads as speculative.
|
|
19
|
+
*/
|
|
20
|
+
export const PREFETCH_INTENT = "intent";
|
|
21
|
+
|
|
12
22
|
/**
|
|
13
23
|
* The last row of a prefetch whose render reported an error
|
|
14
24
|
* (rsc/prefetch-render-error.ts): a Flight hint with a code React does not
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The source group of a prefetch answer a revalidate() rule kept from the
|
|
3
|
+
* page being left (#1331). Shared by the server, which reports
|
|
4
|
+
* the group in `x-rsc-prefetch-scope: params; <label>` (rsc/rsc-rendering.ts),
|
|
5
|
+
* and the browser, which stores the answer under that label and rebuilds it
|
|
6
|
+
* from the page on screen to look it up (browser/prefetch/cache.ts).
|
|
7
|
+
* Dependency free, like prefetch-markers.ts: the browser must not import the
|
|
8
|
+
* server modules that record the reads (router/source-reads.ts).
|
|
9
|
+
*
|
|
10
|
+
* Why a label is enough: a rule's decision depends on the source page only
|
|
11
|
+
* through what it read. Two pages with equal values of everything the kept
|
|
12
|
+
* rules read run every rule the same way, so one answer serves both.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** What the revalidate() rules that kept a segment read of the source page. */
|
|
16
|
+
export interface SourceReads {
|
|
17
|
+
/**
|
|
18
|
+
* `currentParams` was read. Its key set is then part of the group: a rule
|
|
19
|
+
* can test a key's presence (`in`, `Object.keys`) without reading a value.
|
|
20
|
+
*/
|
|
21
|
+
keys: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Each `currentParams` value read; a spread or JSON.stringify reads all.
|
|
24
|
+
* Non-empty only with `keys`: a value is read through `currentParams`.
|
|
25
|
+
*/
|
|
26
|
+
params: Set<string>;
|
|
27
|
+
/** `fromRouteName` was read. */
|
|
28
|
+
route: boolean;
|
|
29
|
+
/** `currentUrl` was read: the answer is the exact page's. */
|
|
30
|
+
url: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The page a label describes: the source on the server, the screen in the browser. */
|
|
34
|
+
export interface SourcePage {
|
|
35
|
+
params: Readonly<Record<string, string | undefined>>;
|
|
36
|
+
/** The public route name (route-name.ts publicRouteName). */
|
|
37
|
+
routeName: string | undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** What a label reads, without the values: one per kind of rule set. */
|
|
41
|
+
export interface SourceShape {
|
|
42
|
+
keys: boolean;
|
|
43
|
+
route: boolean;
|
|
44
|
+
/** Sorted. */
|
|
45
|
+
names: readonly string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** `x-rsc-prefetch-scope` value prefix of a params-grouped answer. */
|
|
49
|
+
export const PARAMS_SCOPE_PREFIX = "params; ";
|
|
50
|
+
|
|
51
|
+
export function createSourceReads(): SourceReads {
|
|
52
|
+
return { keys: false, params: new Set(), route: false, url: false };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function hasSourceReads(reads: SourceReads): boolean {
|
|
56
|
+
return reads.keys || reads.route || reads.url || reads.params.size > 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Union `from` into `into`. */
|
|
60
|
+
export function mergeSourceReads(into: SourceReads, from: SourceReads): void {
|
|
61
|
+
into.keys ||= from.keys;
|
|
62
|
+
into.route ||= from.route;
|
|
63
|
+
into.url ||= from.url;
|
|
64
|
+
for (const name of from.params) into.params.add(name);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function shapeOfReads(reads: SourceReads): SourceShape {
|
|
68
|
+
return {
|
|
69
|
+
keys: reads.keys,
|
|
70
|
+
route: reads.route,
|
|
71
|
+
names: [...reads.params].sort(),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The label of `page` under `shape`: `&`-joined, `$k=<keys>` first, then
|
|
77
|
+
* `$r[=<route>]`, then `<name>[=<value>]` per read param, sorted. A missing
|
|
78
|
+
* value has no `=`. Names and values are URI-encoded, so a `$` entry never
|
|
79
|
+
* collides with a param (encodeURIComponent encodes `$`).
|
|
80
|
+
*/
|
|
81
|
+
export function formatSourceLabel(
|
|
82
|
+
shape: SourceShape,
|
|
83
|
+
page: SourcePage,
|
|
84
|
+
): string {
|
|
85
|
+
const parts: string[] = [];
|
|
86
|
+
if (shape.keys) {
|
|
87
|
+
parts.push(
|
|
88
|
+
"$k=" + Object.keys(page.params).sort().map(encodeURIComponent).join(","),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (shape.route) parts.push(entry("$r", page.routeName));
|
|
92
|
+
for (const name of shape.names) {
|
|
93
|
+
parts.push(entry(encodeURIComponent(name), page.params[name]));
|
|
94
|
+
}
|
|
95
|
+
return parts.join("&");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function entry(name: string, value: string | undefined): string {
|
|
99
|
+
return value === undefined ? name : name + "=" + encodeURIComponent(value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The label of an `x-rsc-prefetch-scope` value, or undefined when the value
|
|
104
|
+
* is not a params scope.
|
|
105
|
+
*/
|
|
106
|
+
export function paramsScopeLabel(
|
|
107
|
+
header: string | null | undefined,
|
|
108
|
+
): string | undefined {
|
|
109
|
+
if (!header?.startsWith(PARAMS_SCOPE_PREFIX)) return undefined;
|
|
110
|
+
return header.slice(PARAMS_SCOPE_PREFIX.length);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The shape of a label formatSourceLabel wrote. */
|
|
114
|
+
export function parseSourceShape(label: string): SourceShape {
|
|
115
|
+
let keys = false;
|
|
116
|
+
let route = false;
|
|
117
|
+
const names: string[] = [];
|
|
118
|
+
for (const part of label.split("&")) {
|
|
119
|
+
if (part === "") continue;
|
|
120
|
+
const eq = part.indexOf("=");
|
|
121
|
+
const name = eq === -1 ? part : part.slice(0, eq);
|
|
122
|
+
if (name === "$k") keys = true;
|
|
123
|
+
else if (name === "$r") route = true;
|
|
124
|
+
else names.push(decodeURIComponent(name));
|
|
125
|
+
}
|
|
126
|
+
return { keys, route, names: names.sort() };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A stable id of a shape, for indexing. */
|
|
130
|
+
export function sourceShapeId(shape: SourceShape): string {
|
|
131
|
+
return (
|
|
132
|
+
(shape.keys ? "k" : "") +
|
|
133
|
+
(shape.route ? "r" : "") +
|
|
134
|
+
":" +
|
|
135
|
+
shape.names.map(encodeURIComponent).join(",")
|
|
136
|
+
);
|
|
137
|
+
}
|
|
@@ -333,9 +333,10 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
333
333
|
* loader(RecommendationsLoader), // still streams behind loading()
|
|
334
334
|
* ```
|
|
335
335
|
*
|
|
336
|
-
* Pass `{ prefetch: false }` to keep the loader out of
|
|
337
|
-
* prefetch skips it and the click
|
|
338
|
-
* one follow-up request
|
|
336
|
+
* Pass `{ prefetch: false }` to keep the loader out of speculative
|
|
337
|
+
* prefetches (viewport, render): such a prefetch skips it and the click
|
|
338
|
+
* that adopts it fetches it in one follow-up request; a hover prefetch runs
|
|
339
|
+
* it (see {@link LoaderOptions}). Only on a segment the
|
|
339
340
|
* client does not have yet: a loader the client already holds runs in the
|
|
340
341
|
* prefetch when it revalidates.
|
|
341
342
|
*
|
|
@@ -361,14 +362,16 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
361
362
|
* // Skip loading on SSR, only show on client navigation
|
|
362
363
|
* loading(<Skeleton />, { ssr: false })
|
|
363
364
|
*
|
|
364
|
-
* // Keep everything behind this fallback out of
|
|
365
|
+
* // Keep everything behind this fallback out of viewport and render
|
|
366
|
+
* // prefetches (a hover prefetch runs it)
|
|
365
367
|
* loading(<Skeleton />, { prefetch: false })
|
|
366
368
|
* ```
|
|
367
369
|
* @param component - The loading UI to show during navigation
|
|
368
370
|
* @param options - Configuration options
|
|
369
371
|
* @param options.ssr - If false, skip showing loading on document requests (SSR)
|
|
370
372
|
* @param options.prefetch - If false, the flag applies when this entry's
|
|
371
|
-
* segment is new to the client. A prefetch then skips
|
|
373
|
+
* segment is new to the client. A viewport or render prefetch then skips
|
|
374
|
+
* the whole unit (a hover prefetch runs it): the
|
|
372
375
|
* handler, its loaders, its orphan layouts and parallel slots and, for a
|
|
373
376
|
* layout, every deeper entry; the click that adopts the prefetch shows
|
|
374
377
|
* this fallback and fetches them in one follow-up request, which reads
|
|
@@ -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(
|