@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
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ Node setup.
|
|
|
38
38
|
|
|
39
39
|
## Using the skills with your coding agent
|
|
40
40
|
|
|
41
|
-
This package ships
|
|
41
|
+
This package ships agent skills in `node_modules/@rangojs/router/skills/` —
|
|
42
42
|
task-focused guides written for LLM coding agents. Start at
|
|
43
43
|
`skills/rango/SKILL.md` (the mental model + catalog); a machine-readable index
|
|
44
44
|
is at `skills/catalog.json`.
|
|
@@ -375,6 +375,7 @@ when the requirement appears:
|
|
|
375
375
|
| test loaders, middleware, handlers, Flight | [`/testing`](./skills/testing/SKILL.md) |
|
|
376
376
|
| see where request time goes | [`/observability`](./skills/observability/SKILL.md) |
|
|
377
377
|
| deploy to Vercel (cache store, tracing, output) | [`/vercel`](./skills/vercel/SKILL.md) |
|
|
378
|
+
| choose in-function vs CDN caching | [`/deployment-caching`](./skills/deployment-caching/SKILL.md) |
|
|
378
379
|
| compare Rango with Next.js / TanStack / Waku | [`/comparison`](./skills/comparison/SKILL.md) |
|
|
379
380
|
|
|
380
381
|
The [`/rango` skill](./skills/rango/SKILL.md) is the full catalog and the
|
|
@@ -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:
|
|
@@ -279,9 +279,10 @@ export interface ShellCacheEntry {
|
|
|
279
279
|
*/
|
|
280
280
|
handlerLiveHoles?: boolean;
|
|
281
281
|
/**
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
282
|
+
* Legacy/fallback marker for a transition({ when }) predicate that was not
|
|
283
|
+
* evaluated by the PPR pre-handler gate. Such an entry stays conservatively
|
|
284
|
+
* ineligible for handler-free replay. New PPR matches evaluate known segment
|
|
285
|
+
* predicates before the pipeline and do not set this marker.
|
|
285
286
|
*/
|
|
286
287
|
transitionWhen?: true;
|
|
287
288
|
/** Capture-generation start time; tag invalidations at or after it win. */
|
|
@@ -349,11 +349,11 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
349
349
|
* startTransition only when the router sets viewTransition: false.
|
|
350
350
|
*
|
|
351
351
|
* Conditional hold: pass `when: (ctx) => boolean` to gate the transition per
|
|
352
|
-
* request. It runs server-side
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
352
|
+
* request. It normally runs server-side after the route handler. On a `ppr`
|
|
353
|
+
* route it is automatically hoisted before route handlers and reevaluated on
|
|
354
|
+
* every replay, so it may read URL/params/action/env and middleware-set context
|
|
355
|
+
* but not handler-set context. Returning false drops this transition for the
|
|
356
|
+
* request. This is distinct from intercept()'s match-time `when` selector.
|
|
357
357
|
*
|
|
358
358
|
* ```typescript
|
|
359
359
|
* // Attach to a single route
|
|
@@ -380,7 +380,7 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
380
380
|
* @param config - ViewTransition configuration (enter, exit, update, share,
|
|
381
381
|
* default, name), `viewTransition: "auto" | false` to toggle the router
|
|
382
382
|
* boundary (createRouter({ viewTransition }) sets the app-wide default), and
|
|
383
|
-
* `when: (ctx) => boolean` to gate the transition per request
|
|
383
|
+
* `when: (ctx) => boolean` to gate the transition per request
|
|
384
384
|
* @param children - Optional callback returning child routes to wrap
|
|
385
385
|
*/
|
|
386
386
|
transition: {
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import type { ErrorInfo, ErrorPhase, MatchResult } from "../types";
|
|
3
|
-
import type
|
|
2
|
+
import type { ErrorBoundaryHandler, ErrorInfo, ErrorPhase, MatchResult } from "../types";
|
|
3
|
+
import { type EntryData, type InterceptEntry, type InterceptSelectorContext } from "../server/context.js";
|
|
4
4
|
import type { MatchApiDeps } from "./types.js";
|
|
5
5
|
import type { RouterContext } from "./router-context.js";
|
|
6
6
|
import { type ActionContext } from "./match-context.js";
|
|
7
|
-
import type { ErrorBoundaryHandler } from "../types";
|
|
8
7
|
import type { MiddlewareFn } from "./middleware.js";
|
|
9
8
|
import { type TelemetrySink } from "./telemetry.js";
|
|
10
9
|
export interface MatchHandlerDeps<TEnv = any> {
|
|
@@ -20,9 +20,8 @@ import type { EntryData } from "../../server/context";
|
|
|
20
20
|
* gate, which also avoids needless object allocation and payload growth.
|
|
21
21
|
*
|
|
22
22
|
* `when`: a server-only predicate. It is STRIPPED from the returned config (a
|
|
23
|
-
* function cannot cross Flight or the segment cache)
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* false. Used by both the fresh and revalidation resolution paths.
|
|
23
|
+
* function cannot cross Flight or the segment cache). PPR routes evaluate it
|
|
24
|
+
* from the manifest before the pipeline; other routes record it here for the
|
|
25
|
+
* existing post-handler gate. Used by both fresh and revalidation resolution.
|
|
27
26
|
*/
|
|
28
27
|
export declare function applyViewTransitionDefault(transition: EntryData["transition"], viewTransitionDefault: "auto" | false | undefined, segmentId?: string): EntryData["transition"];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type EntryData } from "../server/context.js";
|
|
2
|
+
import type { RequestContext } from "../server/request-context.js";
|
|
3
|
+
import type { TransitionWhenContext } from "../types/segments.js";
|
|
4
|
+
export type TransitionWhenErrorReporter = (error: unknown, segmentId: string) => void;
|
|
5
|
+
export declare function createTransitionWhenContext<TEnv>(ctx: RequestContext<TEnv>, target?: {
|
|
6
|
+
params: Record<string, string>;
|
|
7
|
+
routeName?: string;
|
|
8
|
+
}): TransitionWhenContext<Record<string, string>, TEnv>;
|
|
9
|
+
/** Evaluate server-only transition gates before handlers on a PPR route. */
|
|
10
|
+
export declare function evaluatePprTransitionWhen<TEnv>(entries: EntryData[], ctx: RequestContext<TEnv>, target: {
|
|
11
|
+
params: Record<string, string>;
|
|
12
|
+
routeName?: string;
|
|
13
|
+
}, reportError: TransitionWhenErrorReporter): void;
|
|
@@ -19,6 +19,8 @@ import { type EntryData } from "../server/context.js";
|
|
|
19
19
|
import type { ShellCacheEntry, SegmentCacheStore } from "../cache/types.js";
|
|
20
20
|
/** Debug/status header the browser (and e2e assertions) can read: HIT | MISS. */
|
|
21
21
|
export declare const SHELL_STATUS_HEADER = "x-rango-shell";
|
|
22
|
+
/** Partial-navigation status header set only after captured PPR segments are consumed. */
|
|
23
|
+
export declare const PPR_REPLAY_STATUS_HEADER = "x-rango-ppr-replay";
|
|
22
24
|
/**
|
|
23
25
|
* Default shell ttl (seconds) for `ppr: true` and for a PartialPrerenderProps
|
|
24
26
|
* that omits `ttl`.
|
|
@@ -4,24 +4,20 @@ import type { OnErrorCallback } from "../types/error-types.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* Apply transition({ when }) gates to a payload's segments.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* to the router's onError (phase "rendering") and then treated as "do not hold"
|
|
13
|
-
* (conservative), so a buggy predicate degrades to no transition rather than
|
|
14
|
-
* failing the response.
|
|
7
|
+
* PPR predicates were evaluated from the manifest before cache lookup and route
|
|
8
|
+
* handlers; their request-specific decisions are projected onto a copy so they
|
|
9
|
+
* never mutate reusable cache/prerender/shell records. Ordinary-route predicates
|
|
10
|
+
* were collected during fresh resolution and are evaluated here after handlers.
|
|
11
|
+
* Both paths drop the segment transition when the predicate does not hold.
|
|
15
12
|
*
|
|
16
13
|
* Mutating the segments here is safe: the segment cache stores a serialized copy
|
|
17
14
|
* (segment-codec), written during match() BEFORE this gate runs, so dropping a
|
|
18
|
-
* transition never corrupts a cache entry.
|
|
15
|
+
* transition never corrupts a cache entry. On an ordinary route, a cache hit
|
|
19
16
|
* skips resolution, collects no predicate, and replays the cached transition
|
|
20
|
-
* as-is (it was serialized before the gate)
|
|
21
|
-
*
|
|
22
|
-
* avoid caching a route whose transition decision is request-dependent.
|
|
17
|
+
* as-is (it was serialized before the gate). PPR routes are the exception: their
|
|
18
|
+
* manifest predicates were evaluated before lookup and apply to replayed data.
|
|
23
19
|
*
|
|
24
|
-
* Returns the same array
|
|
25
|
-
*
|
|
20
|
+
* Returns the same array for the ordinary post-handler path. A PPR drop returns a
|
|
21
|
+
* copy containing only the request-specific transition changes.
|
|
26
22
|
*/
|
|
27
23
|
export declare function gateTransitions(segments: MatchResult["segments"], ctx: ReturnType<typeof getRequestContext>, onError?: OnErrorCallback): MatchResult["segments"];
|
|
@@ -141,6 +141,8 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
|
|
|
141
141
|
id: string;
|
|
142
142
|
when: TransitionWhenFn;
|
|
143
143
|
}>;
|
|
144
|
+
/** @internal PPR transition decisions evaluated before cache lookup/handlers. */
|
|
145
|
+
_pprTransitionDecisions?: Map<string, boolean>;
|
|
144
146
|
/** @internal Cache store for segment caching (optional, used by CacheScope) */
|
|
145
147
|
_cacheStore?: SegmentCacheStore;
|
|
146
148
|
/**
|
|
@@ -202,6 +204,8 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
|
|
|
202
204
|
* segment record even when the triggering request is partial.
|
|
203
205
|
*/
|
|
204
206
|
keyPrefix?: "doc";
|
|
207
|
+
/** @internal Called only after the implicit cache hit decodes successfully. */
|
|
208
|
+
onHit?: () => void;
|
|
205
209
|
};
|
|
206
210
|
/**
|
|
207
211
|
* @internal Shell-HIT tail marker: cache/prerender-store hits during THIS
|
|
@@ -514,7 +518,7 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
|
|
|
514
518
|
* This is the type exported to library consumers. Internal code should
|
|
515
519
|
* use the full RequestContext interface directly.
|
|
516
520
|
*/
|
|
517
|
-
export type PublicRequestContext<TEnv = DefaultEnv, TParams = Record<string, string>> = Omit<RequestContext<TEnv, TParams>, "cookie" | "cookies" | "setCookie" | "deleteCookie" | "_handleStore" | "_transitionWhen" | "_cacheStore" | "_shellCaptureRun" | "_shellImplicitCache" | "_shellLoaderSeed" | "_shellCaptureLoaderRecords" | "_shellCaptureHandleLiveness" | "_shellCaptureLoaderHandleValues" | "_shellFragmentPayload" | "_shellCaptureGuardTripped" | "_shellCaptureGuardTrippedLoaderId" | "_explicitTaggedStores" | "_requestTags" | "_cacheProfiles" | "_onResponseCallbacks" | "_pendingBackgroundTasks" | "_themeConfig" | "_locationState" | "_routeName" | "_prevRouteKey" | "_gateCurrentUrl" | "_gateCurrentParams" | "_gateActionId" | "_gateActionUrl" | "_gateActionResult" | "_gateFormData" | "_inActionRevalidation" | "_reportedErrors" | "_renderBarrier" | "_resolveRenderBarrier" | "_renderBarrierSegmentOrder" | "_treeHasStreaming" | "_renderBarrierWaiters" | "_handlerLoaderDeps" | "_renderBarrierHandleSnapshot" | "_renderBarrierGuardClosed" | "_reportBackgroundError" | "_debugPerformance" | "_metricsStore" | "_renderForeground" | "_renderDiagnosticsEnabled" | "_handlerStart" | "_tracing" | "_basename" | "_routerId" | "_setStatus" | "_rotateStateCookie" | "_setKeepCacheDirective" | "_variables" | "_classifiedRoute" | "_cacheSignal" | "_dynamic" | "res">;
|
|
521
|
+
export type PublicRequestContext<TEnv = DefaultEnv, TParams = Record<string, string>> = Omit<RequestContext<TEnv, TParams>, "cookie" | "cookies" | "setCookie" | "deleteCookie" | "_handleStore" | "_transitionWhen" | "_pprTransitionDecisions" | "_cacheStore" | "_shellCaptureRun" | "_shellImplicitCache" | "_shellLoaderSeed" | "_shellCaptureLoaderRecords" | "_shellCaptureHandleLiveness" | "_shellCaptureLoaderHandleValues" | "_shellFragmentPayload" | "_shellCaptureGuardTripped" | "_shellCaptureGuardTrippedLoaderId" | "_explicitTaggedStores" | "_requestTags" | "_cacheProfiles" | "_onResponseCallbacks" | "_pendingBackgroundTasks" | "_themeConfig" | "_locationState" | "_routeName" | "_prevRouteKey" | "_gateCurrentUrl" | "_gateCurrentParams" | "_gateActionId" | "_gateActionUrl" | "_gateActionResult" | "_gateFormData" | "_inActionRevalidation" | "_reportedErrors" | "_renderBarrier" | "_resolveRenderBarrier" | "_renderBarrierSegmentOrder" | "_treeHasStreaming" | "_renderBarrierWaiters" | "_handlerLoaderDeps" | "_renderBarrierHandleSnapshot" | "_renderBarrierGuardClosed" | "_reportBackgroundError" | "_debugPerformance" | "_metricsStore" | "_renderForeground" | "_renderDiagnosticsEnabled" | "_handlerStart" | "_tracing" | "_basename" | "_routerId" | "_setStatus" | "_rotateStateCookie" | "_setKeepCacheDirective" | "_variables" | "_classifiedRoute" | "_cacheSignal" | "_dynamic" | "res">;
|
|
518
522
|
/**
|
|
519
523
|
* Marker for a waitUntil-scheduled fn whose task promise must NOT enter
|
|
520
524
|
* _pendingBackgroundTasks. Used by the PPR shell capture for its own task:
|
|
@@ -4,7 +4,7 @@ import { createPageHelpers, createStopwatch, getHistoryState, getNumericContent,
|
|
|
4
4
|
import { createParity, type ExpectParityOptions, type Parity, type ParityDescribeOptions, type ParityIntent } from "./parity.js";
|
|
5
5
|
import { createRangoMatchers, type RangoMatchers } from "./matchers.js";
|
|
6
6
|
export { assertCacheStatus, assertCacheDecision, parseCacheHeader, createCacheSink, filterCacheDecisions, type CacheSink, type ExpectedCacheStatus, type CacheStatusTarget, } from "../cache-status.js";
|
|
7
|
-
export { assertShellStatus, parseShellStatus, shellCacheKey, SHELL_STATUS_HEADER, type ShellStatus, type ShellStatusTarget, } from "../shell-status.js";
|
|
7
|
+
export { assertPprReplayStatus, assertShellStatus, parsePprReplayStatus, parseShellStatus, PPR_REPLAY_STATUS_HEADER, shellCacheKey, SHELL_STATUS_HEADER, type PprReplayBypassReason, type PprReplayStatus, type ShellStatus, type ShellStatusTarget, } from "../shell-status.js";
|
|
8
8
|
export { testId, waitForHydration, waitForNavigation, goBack, goForward, getHistoryState, waitForElement, isVisibleInViewport, parseNumber, getNumericContent, createStopwatch, measureTime, createPageHelpers, createUseFixture, createParity, createRangoMatchers, };
|
|
9
9
|
export type { Fixture, FixtureOptions, PageHelpers, Stopwatch, Parity, ParityIntent, ParityDescribeOptions, ExpectParityOptions, RangoMatchers, };
|
|
10
10
|
export interface RangoE2E extends PageHelpers, Parity {
|
|
@@ -44,8 +44,8 @@ export type { DispatchOptions } from "./dispatch.js";
|
|
|
44
44
|
export { assertCacheStatus, assertCacheDecision, parseCacheHeader, createCacheSink, filterCacheDecisions, } from "./cache-status.js";
|
|
45
45
|
export type { ExpectedCacheStatus, CacheStatusTarget, CacheSink, } from "./cache-status.js";
|
|
46
46
|
export type { TelemetryEvent, TelemetrySink, CacheDecisionEvent, CacheSegmentSignal, CacheSegmentStatus, } from "../router/telemetry.js";
|
|
47
|
-
export { assertShellStatus, parseShellStatus, shellCacheKey, SHELL_STATUS_HEADER, } from "./shell-status.js";
|
|
48
|
-
export type { ShellStatus, ShellStatusTarget } from "./shell-status.js";
|
|
47
|
+
export { assertPprReplayStatus, assertShellStatus, parsePprReplayStatus, parseShellStatus, PPR_REPLAY_STATUS_HEADER, shellCacheKey, SHELL_STATUS_HEADER, } from "./shell-status.js";
|
|
48
|
+
export type { PprReplayBypassReason, PprReplayStatus, ShellStatus, ShellStatusTarget, } from "./shell-status.js";
|
|
49
49
|
export { collectHandle } from "./collect-handle.js";
|
|
50
50
|
export { diffGeneratedRoutes, assertGeneratedRoutesMatch, } from "./generated-routes.js";
|
|
51
51
|
export type { GeneratedRoutesDiff, GeneratedRouteMismatch, } from "./generated-routes.js";
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* runTransitionWhen — unit-test a transition({ when }) predicate in isolation.
|
|
3
3
|
*
|
|
4
|
-
* Runs the SAME
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* TransitionWhenContext and evaluates the predicate post-handler). So the
|
|
4
|
+
* Runs the SAME server functions the router uses — the PPR pre-handler evaluator
|
|
5
|
+
* when `ppr: true`, applyViewTransitionDefault (which strips the server-only
|
|
6
|
+
* function), and gateTransitions. So the
|
|
8
7
|
* predicate sees exactly the navigation/action metadata it would at runtime
|
|
9
8
|
* (currentUrl/currentParams/fromRouteName, nextUrl/nextParams/toRouteName,
|
|
10
9
|
* actionId/actionUrl/actionResult/formData/method, get/env), and `kept` reflects
|
|
@@ -39,7 +38,7 @@ export interface RunTransitionWhenOptions<TEnv = any> {
|
|
|
39
38
|
toRouteName?: string;
|
|
40
39
|
/** Environment bindings surfaced as `env` (and `ctx.env`). */
|
|
41
40
|
env?: TEnv;
|
|
42
|
-
/** Variables
|
|
41
|
+
/** Variables readable via the predicate's `get()`. With `ppr`, these model pre-handler middleware/input state. */
|
|
43
42
|
vars?: VarsInit;
|
|
44
43
|
/** Navigation SOURCE url (`currentUrl`): a URL or path string. */
|
|
45
44
|
currentUrl?: string | URL;
|
|
@@ -57,6 +56,8 @@ export interface RunTransitionWhenOptions<TEnv = any> {
|
|
|
57
56
|
formData?: FormData;
|
|
58
57
|
/** Receives an error thrown by the predicate (the gate reports to `router.onError`, phase `"rendering"`). */
|
|
59
58
|
onError?: OnErrorCallback;
|
|
59
|
+
/** Model a route with `ppr`, where the predicate runs before route handlers and cache lookup. */
|
|
60
|
+
ppr?: boolean;
|
|
60
61
|
}
|
|
61
62
|
/**
|
|
62
63
|
* Result of runTransitionWhen.
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* PPR shell-status testing primitives for @rangojs/router consumers.
|
|
3
3
|
*
|
|
4
4
|
* Companion to `cache-status.ts` (segment/document cache): this module covers
|
|
5
|
-
* the **shell axis** (`ppr` path option → `x-rango-shell: HIT | MISS`)
|
|
5
|
+
* the **shell axis** (`ppr` path option → `x-rango-shell: HIT | MISS`) and
|
|
6
|
+
* partial-navigation segment replay (`x-rango-ppr-replay: HIT`).
|
|
6
7
|
*
|
|
7
8
|
* ## Spike conclusions (plan 009)
|
|
8
9
|
*
|
|
@@ -11,8 +12,9 @@
|
|
|
11
12
|
* + `ppr: true` on a response route is a no-op for shell serve/capture. The
|
|
12
13
|
* production path lives in `rsc/rsc-rendering.ts` + `rsc/shell-capture.ts`.
|
|
13
14
|
*
|
|
14
|
-
* 2. **Smallest
|
|
15
|
-
*
|
|
15
|
+
* 2. **Smallest status signals:** `x-rango-shell` (`HIT` | `MISS`) on document
|
|
16
|
+
* GETs, and `x-rango-ppr-replay` on partial requests to ppr routes. A replay
|
|
17
|
+
* HIT is reported only when matching consumes the captured segment record.
|
|
16
18
|
* Secondary unit signal: `store.getShell(shellCacheKey(url))` after a real
|
|
17
19
|
* capture flush (background `putShell`). There is no Flight flag for shell HIT.
|
|
18
20
|
*
|
|
@@ -27,8 +29,21 @@
|
|
|
27
29
|
*/
|
|
28
30
|
/** Production header name (`rsc/shell-serve.ts` `SHELL_STATUS_HEADER`). */
|
|
29
31
|
export declare const SHELL_STATUS_HEADER: string;
|
|
32
|
+
/** Partial-navigation replay decision header from `rsc/rsc-rendering.ts`. */
|
|
33
|
+
export declare const PPR_REPLAY_STATUS_HEADER: string;
|
|
30
34
|
/** Values the serve path writes on `x-rango-shell`. */
|
|
31
35
|
export type ShellStatus = "HIT" | "MISS";
|
|
36
|
+
declare const PPR_REPLAY_BYPASS_REASONS: readonly ["method", "dynamic", "nonce", "store-unavailable", "passive-read-unsupported", "read-error", "no-entry", "invalid-version", "corrupt-entry", "handler-live-holes", "transition-when", "no-segment-snapshot", "snapshot-miss", "stale-build-entry"];
|
|
37
|
+
/** Bounded reasons a PPR partial request can fall open to ordinary matching. */
|
|
38
|
+
export type PprReplayBypassReason = (typeof PPR_REPLAY_BYPASS_REASONS)[number];
|
|
39
|
+
/** Parsed `x-rango-ppr-replay` value. */
|
|
40
|
+
export type PprReplayStatus = {
|
|
41
|
+
outcome: "HIT";
|
|
42
|
+
freshness: "fresh" | "stale";
|
|
43
|
+
} | {
|
|
44
|
+
outcome: "BYPASS";
|
|
45
|
+
reason: PprReplayBypassReason;
|
|
46
|
+
};
|
|
32
47
|
/** A target carrying response headers (a Response or a `{ headers }` object). */
|
|
33
48
|
export type ShellStatusTarget = Response | {
|
|
34
49
|
headers: Headers;
|
|
@@ -56,3 +71,8 @@ export declare function parseShellStatus(target: ShellStatusTarget): ShellStatus
|
|
|
56
71
|
* invent a HIT Response in unit tests — that fakes the serve path.
|
|
57
72
|
*/
|
|
58
73
|
export declare function assertShellStatus(target: ShellStatusTarget, expected: ShellStatus): void;
|
|
74
|
+
/** Parse the dedicated PPR partial-navigation replay decision header. */
|
|
75
|
+
export declare function parsePprReplayStatus(target: ShellStatusTarget): PprReplayStatus | null;
|
|
76
|
+
/** Assert a partial response's PPR replay outcome and freshness/reason. */
|
|
77
|
+
export declare function assertPprReplayStatus(target: ShellStatusTarget, expected: PprReplayStatus): void;
|
|
78
|
+
export {};
|
|
@@ -12,11 +12,12 @@ export type ViewTransitionClass = Record<string, string> | string;
|
|
|
12
12
|
*
|
|
13
13
|
* It mirrors the {@link ShouldRevalidateFn} args a `revalidate()` predicate
|
|
14
14
|
* gets — the same navigation/action metadata — so the two read the same shape,
|
|
15
|
-
* plus `get`/`env` for
|
|
15
|
+
* plus `get`/`env` for request-context reads. There is no full `HandlerContext`
|
|
16
16
|
* here: the gate runs at the RSC-payload layer with the request context, not a
|
|
17
17
|
* handler context, so handler-only sugar (`search`/`build`/`dev`/`headers`) is
|
|
18
|
-
* absent by design. `get`
|
|
19
|
-
* via `ctx.set(...)`
|
|
18
|
+
* absent by design. On ordinary routes, `get` can read what handlers or
|
|
19
|
+
* middleware set via `ctx.set(...)`. On `ppr` routes the gate runs before route
|
|
20
|
+
* handlers, so only middleware-established values are available.
|
|
20
21
|
*
|
|
21
22
|
* Field availability (all source fields are optional — never fabricated):
|
|
22
23
|
* - `currentUrl` / `currentParams` / `fromRouteName` (the navigation SOURCE) are
|
|
@@ -41,27 +42,30 @@ export type ViewTransitionClass = Record<string, string> | string;
|
|
|
41
42
|
* time, so `currentUrl`/`currentParams`/`fromRouteName` reflect the page the
|
|
42
43
|
* prefetch fired from, NOT necessarily the page the user actually navigates from
|
|
43
44
|
* — the decision is baked into the stored Flight payload and replayed verbatim.
|
|
44
|
-
* A `cache()`/prerender hit replays the stored transition with the
|
|
45
|
-
* re-run
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
45
|
+
* A non-PPR `cache()`/prerender hit replays the stored transition with the
|
|
46
|
+
* predicate NOT re-run. A `ppr` route instead evaluates the predicate before
|
|
47
|
+
* handlers on every match, including cache, prerender, and PPR replay. Prefetch
|
|
48
|
+
* still freezes the server decision into its Flight payload. If the gate must
|
|
49
|
+
* reflect the exact click-time source, source-scope the prefetch
|
|
50
|
+
* (`<Link prefetchKey=":source">`).
|
|
49
51
|
*/
|
|
50
52
|
export type TransitionWhenContext<TParams = Record<string, string>, TEnv = unknown> = Partial<Pick<RevalidateParams<TParams, TEnv>, "currentUrl" | "currentParams" | "fromRouteName">> & Pick<RevalidateParams<TParams, TEnv>, "nextUrl" | "nextParams" | "toRouteName" | "actionId" | "actionUrl" | "actionResult" | "formData" | "method"> & Pick<HandlerContext<any, TEnv>, "get" | "env">;
|
|
51
53
|
/**
|
|
52
54
|
* Predicate that gates whether a transition() applies for the current request.
|
|
53
55
|
*
|
|
54
|
-
* Evaluated server-side
|
|
55
|
-
* handler
|
|
56
|
-
*
|
|
56
|
+
* Evaluated server-side outside any cache scope. On ordinary routes it runs
|
|
57
|
+
* AFTER the route's handler, so `get(...)` can read handler/middleware state. A
|
|
58
|
+
* route with `ppr` automatically hoists it before route handlers and reevaluates
|
|
59
|
+
* it on every cache/prerender/PPR replay; there `get(...)` can read middleware
|
|
60
|
+
* state, but not values set by route handlers. Return false to drop this
|
|
61
|
+
* segment's transition for the request; return true to apply it. The
|
|
57
62
|
* context ({@link TransitionWhenContext}) carries the same navigation/action
|
|
58
63
|
* metadata a `revalidate()` predicate sees plus `get`/`env`. If it throws, the
|
|
59
64
|
* error is reported to the router's onError (phase "rendering") and the
|
|
60
65
|
* transition is dropped (the navigation does not hold).
|
|
61
66
|
*
|
|
62
67
|
* Distinct from intercept()'s `when` config selector, which runs at MATCH time
|
|
63
|
-
* over `{ from, to, params, segments, … }
|
|
64
|
-
* post-handler over the resolved payload.
|
|
68
|
+
* over `{ from, to, params, segments, … }`.
|
|
65
69
|
*
|
|
66
70
|
* Scope: dropping a transition removes only THIS segment's contribution to the
|
|
67
71
|
* navigation's hold. The startTransition hold is navigation-wide — it engages if
|
|
@@ -69,10 +73,10 @@ export type TransitionWhenContext<TParams = Record<string, string>, TEnv = unkno
|
|
|
69
73
|
* navigation stream its loading fallback only when no other matched segment
|
|
70
74
|
* keeps a transition (the common case: a single transition on the route).
|
|
71
75
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* {@link TransitionWhenContext}.
|
|
76
|
+
* On non-PPR routes it runs only during fresh resolution. On PPR routes it runs
|
|
77
|
+
* before handlers for every match, including runtime cache, build-time
|
|
78
|
+
* prerender, and PPR segment replay. A prefetched navigation still freezes the
|
|
79
|
+
* result to prefetch time; see {@link TransitionWhenContext}.
|
|
76
80
|
*/
|
|
77
81
|
export type TransitionWhenFn = (ctx: TransitionWhenContext) => boolean;
|
|
78
82
|
/**
|
|
@@ -105,11 +109,12 @@ export interface TransitionConfig {
|
|
|
105
109
|
viewTransition?: "auto" | false;
|
|
106
110
|
/**
|
|
107
111
|
* Optional server-side predicate that gates this transition per request. When
|
|
108
|
-
* present and it returns false
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
112
|
+
* present and it returns false, the router drops this segment's transition for
|
|
113
|
+
* the request, so the navigation streams its loading fallback instead of
|
|
114
|
+
* holding. PPR routes evaluate it before route handlers and on every replay;
|
|
115
|
+
* other routes evaluate it after handlers on fresh resolution. The predicate
|
|
116
|
+
* is server-only and never serialized to the client; only its resolved effect
|
|
117
|
+
* crosses. See {@link TransitionWhenFn}.
|
|
113
118
|
*/
|
|
114
119
|
when?: TransitionWhenFn;
|
|
115
120
|
}
|
|
@@ -183,9 +183,10 @@ export type PathHelpers<TEnv> = {
|
|
|
183
183
|
* transition cannot fire without a startTransition. See
|
|
184
184
|
* skills/view-transitions for the startTransition x ViewTransition matrix.
|
|
185
185
|
*
|
|
186
|
-
* Pass `when: (ctx) => boolean` to gate the transition per request
|
|
187
|
-
*
|
|
188
|
-
*
|
|
186
|
+
* Pass `when: (ctx) => boolean` to gate the transition per request. It normally
|
|
187
|
+
* runs after the route handler; `ppr` routes automatically run it before route
|
|
188
|
+
* handlers and on every replay, where `ctx.get(...)` sees middleware state but
|
|
189
|
+
* not handler-set values.
|
|
189
190
|
*/
|
|
190
191
|
transition: {
|
|
191
192
|
(): TransitionItem;
|
package/dist/vite/index.js
CHANGED
|
@@ -2530,7 +2530,7 @@ import { resolve } from "node:path";
|
|
|
2530
2530
|
// package.json
|
|
2531
2531
|
var package_default = {
|
|
2532
2532
|
name: "@rangojs/router",
|
|
2533
|
-
version: "0.
|
|
2533
|
+
version: "0.2.0",
|
|
2534
2534
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
2535
2535
|
keywords: [
|
|
2536
2536
|
"react",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -176,19 +176,6 @@
|
|
|
176
176
|
"access": "public",
|
|
177
177
|
"tag": "latest"
|
|
178
178
|
},
|
|
179
|
-
"scripts": {
|
|
180
|
-
"build": "pnpm run build:types && pnpm exec esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm exec esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && pnpm exec esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
|
|
181
|
-
"build:types": "rm -rf dist/types && pnpm exec tsc -p tsconfig.types.json",
|
|
182
|
-
"prepublishOnly": "pnpm build",
|
|
183
|
-
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
184
|
-
"test": "playwright test",
|
|
185
|
-
"test:ui": "playwright test --ui",
|
|
186
|
-
"test:hmr-local": "playwright test --project=dev-warmup --project=hmr-basename --project=hmr-prerender --no-deps --workers=1 && RANGO_E2E_ROUTE_HMR_ONLY=1 playwright test --project=hmr-routes --no-deps --workers=1",
|
|
187
|
-
"test:hmr-routes-local": "RANGO_E2E_ROUTE_HMR_ONLY=1 playwright test --project=hmr-routes --no-deps --workers=1",
|
|
188
|
-
"test:unit": "pnpm run build:types && vitest run",
|
|
189
|
-
"test:unit:watch": "vitest",
|
|
190
|
-
"test:unit:rsc": "vitest run --config vitest.rsc.config.ts"
|
|
191
|
-
},
|
|
192
179
|
"dependencies": {
|
|
193
180
|
"@types/debug": "^4.1.12",
|
|
194
181
|
"@vitejs/plugin-rsc": "^0.5.27",
|
|
@@ -204,19 +191,19 @@
|
|
|
204
191
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
205
192
|
"@opentelemetry/sdk-trace-base": "^2.9.0",
|
|
206
193
|
"@playwright/test": "^1.49.1",
|
|
207
|
-
"@shared/e2e": "workspace:*",
|
|
208
194
|
"@testing-library/dom": "^10.4.1",
|
|
209
195
|
"@testing-library/react": "^16.3.2",
|
|
210
196
|
"@types/node": "^24.10.1",
|
|
211
|
-
"@types/react": "
|
|
212
|
-
"@types/react-dom": "
|
|
197
|
+
"@types/react": "^19.2.7",
|
|
198
|
+
"@types/react-dom": "^19.2.3",
|
|
213
199
|
"esbuild": "^0.28.1",
|
|
214
200
|
"happy-dom": "^20.10.1",
|
|
215
201
|
"jiti": "^2.7.0",
|
|
216
|
-
"react": "
|
|
217
|
-
"react-dom": "
|
|
202
|
+
"react": "^19.2.6",
|
|
203
|
+
"react-dom": "^19.2.6",
|
|
218
204
|
"typescript": "^5.3.0",
|
|
219
|
-
"vitest": "^4.1.9"
|
|
205
|
+
"vitest": "^4.1.9",
|
|
206
|
+
"@shared/e2e": "0.0.1"
|
|
220
207
|
},
|
|
221
208
|
"peerDependencies": {
|
|
222
209
|
"@cloudflare/vite-plugin": "^1.42.1",
|
|
@@ -255,5 +242,17 @@
|
|
|
255
242
|
},
|
|
256
243
|
"engines": {
|
|
257
244
|
"node": ">=24.0.0"
|
|
245
|
+
},
|
|
246
|
+
"scripts": {
|
|
247
|
+
"build": "pnpm run build:types && pnpm exec esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm exec esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && pnpm exec esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
|
|
248
|
+
"build:types": "rm -rf dist/types && pnpm exec tsc -p tsconfig.types.json",
|
|
249
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
250
|
+
"test": "playwright test",
|
|
251
|
+
"test:ui": "playwright test --ui",
|
|
252
|
+
"test:hmr-local": "playwright test --project=dev-warmup --project=hmr-basename --project=hmr-prerender --no-deps --workers=1 && RANGO_E2E_ROUTE_HMR_ONLY=1 playwright test --project=hmr-routes --no-deps --workers=1",
|
|
253
|
+
"test:hmr-routes-local": "RANGO_E2E_ROUTE_HMR_ONLY=1 playwright test --project=hmr-routes --no-deps --workers=1",
|
|
254
|
+
"test:unit": "pnpm run build:types && vitest run",
|
|
255
|
+
"test:unit:watch": "vitest",
|
|
256
|
+
"test:unit:rsc": "vitest run --config vitest.rsc.config.ts"
|
|
258
257
|
}
|
|
259
|
-
}
|
|
258
|
+
}
|
|
@@ -131,8 +131,10 @@ recompute for a merely-aging entry.
|
|
|
131
131
|
layer.
|
|
132
132
|
- **Client forward/back** is SWR after a mutation — see "Correctness &
|
|
133
133
|
invalidation" → Client cache.
|
|
134
|
-
- **
|
|
135
|
-
|
|
134
|
+
- **Store-backed document layer** uses the HTTP `stale-while-revalidate`
|
|
135
|
+
directive as policy for `createDocumentCacheMiddleware`; see
|
|
136
|
+
`/document-cache`. A platform CDN may independently consume the same header
|
|
137
|
+
and bypass the function on hits; see `/deployment-caching`.
|
|
136
138
|
|
|
137
139
|
SWR softens normal TTL expiry, **not** a cross-deploy cold cache — a new build
|
|
138
140
|
has no stale entry to serve (see version-segmented store keys above).
|
|
@@ -485,5 +487,6 @@ overrides — see `/loader` for the full reference.
|
|
|
485
487
|
|
|
486
488
|
- `/caching` — cache() DSL setup, stores, nested boundaries
|
|
487
489
|
- `/use-cache` — "use cache" directive details, profiles, transforms, guards
|
|
488
|
-
- `/document-cache` —
|
|
490
|
+
- `/document-cache` — store-backed complete-response middleware
|
|
491
|
+
- `/deployment-caching` — in-function versus external CDN cache boundaries
|
|
489
492
|
- `/ppr` — PPR shell caching: cached HTML shell + live loader holes (different layer)
|
package/skills/catalog.json
CHANGED
|
@@ -69,9 +69,15 @@
|
|
|
69
69
|
"argumentHint": "",
|
|
70
70
|
"path": "skills/defer-hydration/SKILL.md"
|
|
71
71
|
},
|
|
72
|
+
{
|
|
73
|
+
"name": "deployment-caching",
|
|
74
|
+
"description": "Choose the deployment cache boundary for a Rango app: in-function segment/prerender/PPR caches, store-backed whole-response caching, or an external CDN cache. Use when comparing Cloudflare, Vercel, and Node deployments; deciding whether Cache-Control can reduce origin work; or reasoning about middleware, live loaders, PPR, and CDN behavior.",
|
|
75
|
+
"argumentHint": "[cloudflare|vercel|node]",
|
|
76
|
+
"path": "skills/deployment-caching/SKILL.md"
|
|
77
|
+
},
|
|
72
78
|
{
|
|
73
79
|
"name": "document-cache",
|
|
74
|
-
"description": "Cache
|
|
80
|
+
"description": "Cache complete HTTP responses in Rango's configured app store with createDocumentCacheMiddleware, using Cache-Control s-maxage as policy. Use when reusing a whole HTML/RSC response, comparing the store-backed middleware with a platform CDN cache, or deciding whether full-response caching is safe.",
|
|
75
81
|
"argumentHint": "[setup]",
|
|
76
82
|
"path": "skills/document-cache/SKILL.md"
|
|
77
83
|
},
|