@rangojs/router 0.1.1 → 0.2.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 +2 -1
- package/dist/types/cache/document-cache.d.ts +3 -1
- package/dist/types/cache/types.d.ts +4 -3
- package/dist/types/route-definition/helpers-types.d.ts +6 -6
- package/dist/types/router/match-handlers.d.ts +2 -3
- package/dist/types/router/segment-resolution/view-transition-default.d.ts +3 -4
- package/dist/types/router/transition-when.d.ts +13 -0
- package/dist/types/rsc/shell-serve.d.ts +2 -0
- package/dist/types/rsc/transition-gate.d.ts +10 -14
- package/dist/types/server/request-context.d.ts +5 -1
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/index.d.ts +2 -2
- package/dist/types/testing/run-transition-when.d.ts +6 -5
- package/dist/types/testing/shell-status.d.ts +23 -3
- package/dist/types/types/segments.d.ts +27 -22
- package/dist/types/urls/path-helper-types.d.ts +4 -3
- package/dist/vite/index.js +1 -1
- package/package.json +20 -21
- package/skills/cache-guide/SKILL.md +6 -3
- package/skills/catalog.json +7 -1
- package/skills/deployment-caching/SKILL.md +176 -0
- package/skills/document-cache/SKILL.md +30 -3
- package/skills/ppr/SKILL.md +57 -25
- package/skills/prerender/SKILL.md +15 -8
- package/skills/rango/SKILL.md +20 -17
- package/skills/testing/SKILL.md +1 -1
- package/skills/testing/cache-prerender.md +5 -1
- package/skills/vercel/SKILL.md +22 -1
- package/skills/view-transitions/SKILL.md +12 -8
- package/src/cache/cache-scope.ts +6 -1
- package/src/cache/document-cache.ts +4 -2
- package/src/cache/types.ts +4 -3
- package/src/route-definition/helpers-types.ts +6 -6
- package/src/router/match-handlers.ts +44 -6
- package/src/router/match-middleware/cache-lookup.ts +10 -3
- package/src/router/segment-resolution/view-transition-default.ts +9 -5
- package/src/router/transition-when.ts +76 -0
- package/src/rsc/progressive-enhancement.ts +9 -14
- package/src/rsc/rsc-rendering.ts +142 -31
- package/src/rsc/shell-serve.ts +3 -0
- package/src/rsc/transition-gate.ts +37 -40
- package/src/server/request-context.ts +6 -0
- package/src/testing/e2e/index.ts +5 -0
- package/src/testing/index.ts +9 -1
- package/src/testing/run-transition-when.ts +42 -9
- package/src/testing/shell-status.ts +92 -3
- package/src/types/segments.ts +27 -22
- package/src/urls/path-helper-types.ts +4 -3
|
@@ -209,9 +209,8 @@ interface TransitionConfig {
|
|
|
209
209
|
default?: string | Record<string, string>; // fallback for any phase
|
|
210
210
|
name?: string; // explicit view-transition-name
|
|
211
211
|
viewTransition?: "auto" | false; // boundary opt-out (see below)
|
|
212
|
-
// Conditional
|
|
213
|
-
//
|
|
214
|
-
// loading() fallback instead of holding. See the gate section below.
|
|
212
|
+
// Conditional server-side gate. PPR routes run it before route handlers and
|
|
213
|
+
// on every replay; other routes run it after handlers on fresh resolution.
|
|
215
214
|
when?: (ctx: TransitionWhenContext) => boolean;
|
|
216
215
|
}
|
|
217
216
|
```
|
|
@@ -223,14 +222,19 @@ interface TransitionConfig {
|
|
|
223
222
|
|
|
224
223
|
## Conditional transitions (`when`)
|
|
225
224
|
|
|
226
|
-
`transition({ when })` gates the hold per request. The predicate runs
|
|
225
|
+
`transition({ when })` gates the hold per request. The predicate runs server-side and outside any cache scope; return `false` to drop this segment's transition for the request (the navigation streams its `loading()` fallback instead of holding).
|
|
227
226
|
|
|
228
|
-
|
|
227
|
+
Timing follows the route's rendering contract:
|
|
228
|
+
|
|
229
|
+
- On an ordinary route it runs after the route handler during fresh resolution, so `get()` can read handler- and middleware-set context. Cache/prerender hits replay the stored decision.
|
|
230
|
+
- On a `ppr` route it is automatically hoisted before route handlers and runs on every match, including runtime-cache, prerender, document-shell, and partial-navigation replay. It can read URL/params/action metadata, `env`, and middleware-set context, but not values set by route handlers. This is what keeps the handler-free PPR fast path available without a second API.
|
|
231
|
+
|
|
232
|
+
Its context mirrors the `revalidate()` predicate args — the same navigation/action metadata — plus `get`/`env` for request-context reads:
|
|
229
233
|
|
|
230
234
|
```ts
|
|
231
235
|
import type { TransitionWhenContext } from "@rangojs/router";
|
|
232
236
|
|
|
233
|
-
//
|
|
237
|
+
// Ordinary route: hold only when the handler marked this request:
|
|
234
238
|
transition({ when: (ctx) => ctx.get(KeepScroll) === true });
|
|
235
239
|
|
|
236
240
|
// Hold only when arriving from a specific page (the navigation SOURCE):
|
|
@@ -252,13 +256,13 @@ transition({
|
|
|
252
256
|
| `toRouteName` (and `fromRouteName`) | route **name** | when the route is named (undefined for unnamed/auto-generated) |
|
|
253
257
|
| `actionId` / `actionUrl` / `actionResult` / `formData` | the server action that triggered this render | action-triggered renders only |
|
|
254
258
|
| `method` | `"GET"` (nav) / `"POST"` (action) | always |
|
|
255
|
-
| `get` / `env` | read
|
|
259
|
+
| `get` / `env` | read request vars + app env | always; PPR timing exposes middleware vars, not handler writes |
|
|
256
260
|
|
|
257
261
|
A predicate that throws is reported to `router.onError` (phase `"rendering"`) and treated as no-hold (conservative).
|
|
258
262
|
|
|
259
263
|
**Same-route content-holds need the transition present on the FIRST render.** The same-route hold works by giving the route a param-agnostic key so a param change reconciles instead of remounting — but that key is established when the route first mounts. A source gate that returns `false` on the initial full load (where `currentUrl`/`currentParams`/`fromRouteName` are undefined) drops the transition before the route mounts, so the route mounts _outside_ a transition scope and **every** later same-route param nav remounts (flashing the skeleton) regardless of what the gate decides on those navs. Write source gates so they hold when there is no source — e.g. `({ currentParams }) => currentParams?.tab !== "raw"` (true on the initial load) rather than `=== "details"` (false on the initial load) — when the same-route content-hold must engage. This only affects same-route param navigations; action-only or cross-route gating is unaffected (no shared param key is in play).
|
|
260
264
|
|
|
261
|
-
**Prefetch / cache caveat.**
|
|
265
|
+
**Prefetch / cache caveat.** A **prefetched** navigation still decides at prefetch time — `currentUrl`/`currentParams`/`fromRouteName` reflect the page the prefetch fired from, not necessarily the click-time source. Non-PPR `cache()`/prerender hits also replay the stored transition without rerunning the predicate. PPR routes rerun it on the server for each cache/prerender/PPR match, but a completed browser prefetch still carries its earlier Flight decision. If the exact click-time source matters, source-scope the prefetch (`<Link prefetchKey=":source">`).
|
|
262
266
|
|
|
263
267
|
## Opting out of the router boundary (place your own `<ViewTransition>`)
|
|
264
268
|
|
package/src/cache/cache-scope.ts
CHANGED
|
@@ -108,6 +108,8 @@ function getDefaultRouteCacheKey(
|
|
|
108
108
|
// CacheScope
|
|
109
109
|
// ============================================================================
|
|
110
110
|
|
|
111
|
+
const CACHE_HIT_OBSERVERS = new WeakMap<CacheScope, () => void>();
|
|
112
|
+
|
|
111
113
|
/**
|
|
112
114
|
* CacheScope represents a cache boundary in the route tree.
|
|
113
115
|
*
|
|
@@ -348,6 +350,7 @@ export class CacheScope {
|
|
|
348
350
|
);
|
|
349
351
|
}
|
|
350
352
|
|
|
353
|
+
CACHE_HIT_OBSERVERS.get(this)?.();
|
|
351
354
|
return { segments, shouldRevalidate };
|
|
352
355
|
} catch (error) {
|
|
353
356
|
// Covers a store.get() failure AND a throwing consumer key()/keyGenerator
|
|
@@ -555,9 +558,11 @@ export function resolveShellImplicitCacheScope(
|
|
|
555
558
|
if (scope) return scope;
|
|
556
559
|
const marker = getRequestContext()?._shellImplicitCache;
|
|
557
560
|
if (!marker) return null;
|
|
558
|
-
|
|
561
|
+
const implicitScope = new CacheScope(
|
|
559
562
|
{ ttl: marker.ttl, swr: marker.swr, store: marker.store },
|
|
560
563
|
null,
|
|
561
564
|
marker.keyPrefix,
|
|
562
565
|
);
|
|
566
|
+
if (marker.onHit) CACHE_HIT_OBSERVERS.set(implicitScope, marker.onHit);
|
|
567
|
+
return implicitScope;
|
|
563
568
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Document-Level Cache Middleware
|
|
3
3
|
*
|
|
4
|
-
* Caches full HTTP responses
|
|
4
|
+
* Caches full HTTP responses in the configured app store based on Cache-Control
|
|
5
|
+
* headers. A deployment CDN may independently consume the same shared-cache
|
|
6
|
+
* directives; this middleware itself runs inside the worker/function.
|
|
5
7
|
* Routes opt-in to caching by setting s-maxage or stale-while-revalidate headers.
|
|
6
8
|
*
|
|
7
9
|
* Flow:
|
|
@@ -58,7 +60,7 @@ function parseCacheControl(header: string | null): CacheDirectives | null {
|
|
|
58
60
|
|
|
59
61
|
// RFC 7234: in a SHARED cache, `private` and `no-store` forbid storage and
|
|
60
62
|
// MUST win over `s-maxage` even though `private, s-maxage` is contradictory.
|
|
61
|
-
// The document cache is a shared
|
|
63
|
+
// The document cache is a shared app store, so refuse both regardless of any
|
|
62
64
|
// s-maxage / stale-while-revalidate also present. Match standalone directive
|
|
63
65
|
// tokens (start/end, whitespace, comma, semicolon, or `=` bounded), not a
|
|
64
66
|
// substring, so a value containing "private" cannot false-veto.
|
package/src/cache/types.ts
CHANGED
|
@@ -319,9 +319,10 @@ export interface ShellCacheEntry {
|
|
|
319
319
|
*/
|
|
320
320
|
handlerLiveHoles?: boolean;
|
|
321
321
|
/**
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
322
|
+
* Legacy/fallback marker for a transition({ when }) predicate that was not
|
|
323
|
+
* evaluated by the PPR pre-handler gate. Such an entry stays conservatively
|
|
324
|
+
* ineligible for handler-free replay. New PPR matches evaluate known segment
|
|
325
|
+
* predicates before the pipeline and do not set this marker.
|
|
325
326
|
*/
|
|
326
327
|
transitionWhen?: true;
|
|
327
328
|
/** Capture-generation start time; tag invalidations at or after it win. */
|
|
@@ -460,11 +460,11 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
460
460
|
* startTransition only when the router sets viewTransition: false.
|
|
461
461
|
*
|
|
462
462
|
* Conditional hold: pass `when: (ctx) => boolean` to gate the transition per
|
|
463
|
-
* request. It runs server-side
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
463
|
+
* request. It normally runs server-side after the route handler. On a `ppr`
|
|
464
|
+
* route it is automatically hoisted before route handlers and reevaluated on
|
|
465
|
+
* every replay, so it may read URL/params/action/env and middleware-set context
|
|
466
|
+
* but not handler-set context. Returning false drops this transition for the
|
|
467
|
+
* request. This is distinct from intercept()'s match-time `when` selector.
|
|
468
468
|
*
|
|
469
469
|
* ```typescript
|
|
470
470
|
* // Attach to a single route
|
|
@@ -491,7 +491,7 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
491
491
|
* @param config - ViewTransition configuration (enter, exit, update, share,
|
|
492
492
|
* default, name), `viewTransition: "auto" | false` to toggle the router
|
|
493
493
|
* boundary (createRouter({ viewTransition }) sets the app-wide default), and
|
|
494
|
-
* `when: (ctx) => boolean` to gate the transition per request
|
|
494
|
+
* `when: (ctx) => boolean` to gate the transition per request
|
|
495
495
|
* @param children - Optional callback returning child routes to wrap
|
|
496
496
|
*/
|
|
497
497
|
transition: {
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
3
3
|
import { sanitizeError } from "../errors";
|
|
4
|
-
import type { ErrorInfo, ErrorPhase, MatchResult } from "../types";
|
|
5
4
|
import type {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
ErrorBoundaryHandler,
|
|
6
|
+
ErrorInfo,
|
|
7
|
+
ErrorPhase,
|
|
8
|
+
MatchResult,
|
|
9
|
+
NotFoundBoundaryHandler,
|
|
10
|
+
} from "../types";
|
|
11
|
+
import {
|
|
12
|
+
isPprEntry,
|
|
13
|
+
type EntryData,
|
|
14
|
+
type InterceptEntry,
|
|
15
|
+
type InterceptSelectorContext,
|
|
16
|
+
} from "../server/context.js";
|
|
10
17
|
import type { MatchApiDeps } from "./types.js";
|
|
11
18
|
import type { RouterContext } from "./router-context.js";
|
|
12
19
|
import { runWithRouterContext } from "./router-context.js";
|
|
@@ -30,7 +37,6 @@ import {
|
|
|
30
37
|
startRevalidationTrace,
|
|
31
38
|
flushRevalidationTrace,
|
|
32
39
|
} from "./logging.js";
|
|
33
|
-
import type { ErrorBoundaryHandler, NotFoundBoundaryHandler } from "../types";
|
|
34
40
|
import type { MiddlewareFn } from "./middleware.js";
|
|
35
41
|
import {
|
|
36
42
|
type TelemetrySink,
|
|
@@ -41,6 +47,7 @@ import {
|
|
|
41
47
|
buildCacheSignalSegments,
|
|
42
48
|
} from "./telemetry.js";
|
|
43
49
|
import { _getRequestContext } from "../server/request-context.js";
|
|
50
|
+
import { evaluatePprTransitionWhen } from "./transition-when.js";
|
|
44
51
|
|
|
45
52
|
/**
|
|
46
53
|
* Per-call telemetry lifecycle emitter for match()/matchPartial(). Each method
|
|
@@ -227,6 +234,35 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
227
234
|
if (reqCtx) reqCtx._cacheSignal = segments;
|
|
228
235
|
};
|
|
229
236
|
|
|
237
|
+
const evaluatePprTransitionWhenForMatch = (
|
|
238
|
+
ctx: MatchContext<TEnv>,
|
|
239
|
+
isPartial: boolean,
|
|
240
|
+
): void => {
|
|
241
|
+
const reqCtx = _getRequestContext();
|
|
242
|
+
if (!reqCtx) return;
|
|
243
|
+
reqCtx._pprTransitionDecisions = undefined;
|
|
244
|
+
if (!isPprEntry(ctx.manifestEntry)) return;
|
|
245
|
+
|
|
246
|
+
evaluatePprTransitionWhen(
|
|
247
|
+
ctx.entries,
|
|
248
|
+
reqCtx,
|
|
249
|
+
{
|
|
250
|
+
params: ctx.matched.params,
|
|
251
|
+
routeName: ctx.interceptSelectorContext.toRouteName,
|
|
252
|
+
},
|
|
253
|
+
(error, segmentId) =>
|
|
254
|
+
callOnError(error, "rendering", {
|
|
255
|
+
request: ctx.request,
|
|
256
|
+
url: ctx.url,
|
|
257
|
+
env: ctx.env,
|
|
258
|
+
params: ctx.matched.params,
|
|
259
|
+
segmentId,
|
|
260
|
+
isPartial,
|
|
261
|
+
handledByBoundary: false,
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
264
|
+
};
|
|
265
|
+
|
|
230
266
|
async function createMatchContextForFull(
|
|
231
267
|
request: Request,
|
|
232
268
|
env: TEnv,
|
|
@@ -313,6 +349,7 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
313
349
|
}
|
|
314
350
|
|
|
315
351
|
const ctx = result as MatchContext<TEnv>;
|
|
352
|
+
evaluatePprTransitionWhenForMatch(ctx, false);
|
|
316
353
|
|
|
317
354
|
try {
|
|
318
355
|
const state = createPipelineState();
|
|
@@ -424,6 +461,7 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
424
461
|
emitter.end(0, false);
|
|
425
462
|
return null;
|
|
426
463
|
}
|
|
464
|
+
evaluatePprTransitionWhenForMatch(ctx, true);
|
|
427
465
|
|
|
428
466
|
if (isRouterDebugEnabled()) {
|
|
429
467
|
startRevalidationTrace({
|
|
@@ -460,6 +460,8 @@ export function withCacheLookup<TEnv>(
|
|
|
460
460
|
state.shouldRevalidate = cacheResult.shouldRevalidate;
|
|
461
461
|
state.cachedSegments = cacheResult.segments;
|
|
462
462
|
state.cachedMatchedIds = cacheResult.segments.map((s) => s.id);
|
|
463
|
+
const pprTransitionDecisions =
|
|
464
|
+
_getRequestContext()?._pprTransitionDecisions;
|
|
463
465
|
|
|
464
466
|
const canCheckSegmentRevalidation =
|
|
465
467
|
!ctx.isFullMatch &&
|
|
@@ -533,8 +535,13 @@ export function withCacheLookup<TEnv>(
|
|
|
533
535
|
reason: "cached-no-rules",
|
|
534
536
|
});
|
|
535
537
|
}
|
|
536
|
-
|
|
537
|
-
segment.
|
|
538
|
+
// A PPR transition decision must reach the client even when this cached
|
|
539
|
+
// segment otherwise needs no update. Keep its replayed component so the
|
|
540
|
+
// partial result retains the segment for gateTransitions().
|
|
541
|
+
if (!pprTransitionDecisions?.has(segment.id)) {
|
|
542
|
+
segment.component = null;
|
|
543
|
+
segment.loading = undefined;
|
|
544
|
+
}
|
|
538
545
|
yield segment;
|
|
539
546
|
continue;
|
|
540
547
|
}
|
|
@@ -566,7 +573,7 @@ export function withCacheLookup<TEnv>(
|
|
|
566
573
|
shouldRevalidate,
|
|
567
574
|
});
|
|
568
575
|
|
|
569
|
-
if (!shouldRevalidate) {
|
|
576
|
+
if (!shouldRevalidate && !pprTransitionDecisions?.has(segment.id)) {
|
|
570
577
|
segment.component = null;
|
|
571
578
|
segment.loading = undefined;
|
|
572
579
|
}
|
|
@@ -23,10 +23,9 @@ import { getRequestContext } from "../../server/request-context.js";
|
|
|
23
23
|
* gate, which also avoids needless object allocation and payload growth.
|
|
24
24
|
*
|
|
25
25
|
* `when`: a server-only predicate. It is STRIPPED from the returned config (a
|
|
26
|
-
* function cannot cross Flight or the segment cache)
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* false. Used by both the fresh and revalidation resolution paths.
|
|
26
|
+
* function cannot cross Flight or the segment cache). PPR routes evaluate it
|
|
27
|
+
* from the manifest before the pipeline; other routes record it here for the
|
|
28
|
+
* existing post-handler gate. Used by both fresh and revalidation resolution.
|
|
30
29
|
*/
|
|
31
30
|
export function applyViewTransitionDefault(
|
|
32
31
|
transition: EntryData["transition"],
|
|
@@ -39,7 +38,12 @@ export function applyViewTransitionDefault(
|
|
|
39
38
|
if (segmentId !== undefined) {
|
|
40
39
|
try {
|
|
41
40
|
const ctx = getRequestContext();
|
|
42
|
-
(ctx.
|
|
41
|
+
if (!ctx._pprTransitionDecisions?.has(segmentId)) {
|
|
42
|
+
(ctx._transitionWhen ??= []).push({
|
|
43
|
+
id: segmentId,
|
|
44
|
+
when: result.when,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
43
47
|
} catch {
|
|
44
48
|
// No active request context (e.g. a unit test calling this util
|
|
45
49
|
// directly). Skip collection; the strip below still applies so the
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { getParallelSlotEntries, type EntryData } from "../server/context.js";
|
|
2
|
+
import type { RequestContext } from "../server/request-context.js";
|
|
3
|
+
import type { TransitionWhenContext } from "../types/segments.js";
|
|
4
|
+
|
|
5
|
+
export type TransitionWhenErrorReporter = (
|
|
6
|
+
error: unknown,
|
|
7
|
+
segmentId: string,
|
|
8
|
+
) => void;
|
|
9
|
+
|
|
10
|
+
export function createTransitionWhenContext<TEnv>(
|
|
11
|
+
ctx: RequestContext<TEnv>,
|
|
12
|
+
target?: {
|
|
13
|
+
params: Record<string, string>;
|
|
14
|
+
routeName?: string;
|
|
15
|
+
},
|
|
16
|
+
): TransitionWhenContext<Record<string, string>, TEnv> {
|
|
17
|
+
return {
|
|
18
|
+
currentUrl: ctx._gateCurrentUrl,
|
|
19
|
+
currentParams: ctx._gateCurrentParams,
|
|
20
|
+
fromRouteName: ctx._prevRouteKey as TransitionWhenContext["fromRouteName"],
|
|
21
|
+
nextUrl: ctx.url,
|
|
22
|
+
nextParams: target?.params ?? ctx.params,
|
|
23
|
+
toRouteName: (target?.routeName ??
|
|
24
|
+
ctx.routeName) as TransitionWhenContext["toRouteName"],
|
|
25
|
+
actionId: ctx._gateActionId,
|
|
26
|
+
actionUrl: ctx._gateActionUrl,
|
|
27
|
+
actionResult: ctx._gateActionResult,
|
|
28
|
+
formData: ctx._gateFormData,
|
|
29
|
+
method: ctx.request.method,
|
|
30
|
+
get: ctx.get,
|
|
31
|
+
env: ctx.env,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Evaluate server-only transition gates before handlers on a PPR route. */
|
|
36
|
+
export function evaluatePprTransitionWhen<TEnv>(
|
|
37
|
+
entries: EntryData[],
|
|
38
|
+
ctx: RequestContext<TEnv>,
|
|
39
|
+
target: {
|
|
40
|
+
params: Record<string, string>;
|
|
41
|
+
routeName?: string;
|
|
42
|
+
},
|
|
43
|
+
reportError: TransitionWhenErrorReporter,
|
|
44
|
+
): void {
|
|
45
|
+
const decisions = new Map<string, boolean>();
|
|
46
|
+
const whenContext = createTransitionWhenContext(ctx, target);
|
|
47
|
+
const visited = new Set<EntryData>();
|
|
48
|
+
|
|
49
|
+
const evaluate = (entry: EntryData, segmentId: string): void => {
|
|
50
|
+
const when = entry.transition?.when;
|
|
51
|
+
if (!when) return;
|
|
52
|
+
try {
|
|
53
|
+
decisions.set(segmentId, when(whenContext) !== false);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
decisions.set(segmentId, false);
|
|
56
|
+
reportError(error, segmentId);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const visitEntryAndOrphanLayouts = (entry: EntryData): void => {
|
|
61
|
+
if (visited.has(entry)) return;
|
|
62
|
+
visited.add(entry);
|
|
63
|
+
evaluate(entry, entry.shortCode);
|
|
64
|
+
|
|
65
|
+
for (const orphan of entry.layout) visitEntryAndOrphanLayouts(orphan);
|
|
66
|
+
// Rendered parallel slots are leaves identified by their owning segment.
|
|
67
|
+
for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
|
|
68
|
+
entry.parallel,
|
|
69
|
+
)) {
|
|
70
|
+
evaluate(parallelEntry, `${entry.shortCode}.${slot}`);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
for (const entry of entries) visitEntryAndOrphanLayouts(entry);
|
|
75
|
+
ctx._pprTransitionDecisions = decisions.size > 0 ? decisions : undefined;
|
|
76
|
+
}
|
|
@@ -277,10 +277,15 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
277
277
|
// JS/PE parity: this is an action's revalidation render, so mark it BEFORE
|
|
278
278
|
// matching — a stale `foregroundOnAction` cache entry must re-execute in the
|
|
279
279
|
// foreground during the re-render, exactly as the JS path's
|
|
280
|
-
// revalidateAfterAction does.
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
getRequestContext()
|
|
280
|
+
// revalidateAfterAction does. PPR transition({ when }) runs during match,
|
|
281
|
+
// while foregroundOnAction reads _inActionRevalidation there too, so all
|
|
282
|
+
// action metadata must be available before matching.
|
|
283
|
+
const peReqCtx = getRequestContext();
|
|
284
|
+
peReqCtx._inActionRevalidation = true;
|
|
285
|
+
peReqCtx._gateActionId = directActionId ?? undefined;
|
|
286
|
+
peReqCtx._gateActionUrl = new URL(url);
|
|
287
|
+
peReqCtx._gateActionResult = actionResult;
|
|
288
|
+
peReqCtx._gateFormData = formData;
|
|
284
289
|
|
|
285
290
|
const match = await ctx.router.match(renderRequest, { env });
|
|
286
291
|
|
|
@@ -291,16 +296,6 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
291
296
|
});
|
|
292
297
|
}
|
|
293
298
|
|
|
294
|
-
// Expose the no-JS action to the transition({ when }) gate. currentUrl/Params
|
|
295
|
-
// are absent on this full-render path (no navigation snapshot); useActionState
|
|
296
|
-
// ids are block-scoped, so only a direct action id is available here.
|
|
297
|
-
// actionUrl is the page the action was submitted from (this request's url).
|
|
298
|
-
const peReqCtx = getRequestContext();
|
|
299
|
-
peReqCtx._gateActionId = directActionId ?? undefined;
|
|
300
|
-
peReqCtx._gateActionUrl = new URL(url);
|
|
301
|
-
peReqCtx._gateActionResult = actionResult;
|
|
302
|
-
peReqCtx._gateFormData = formData;
|
|
303
|
-
|
|
304
299
|
const payload: RscPayload = {
|
|
305
300
|
metadata: {
|
|
306
301
|
pathname: url.pathname,
|