@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.
Files changed (48) hide show
  1. package/README.md +2 -1
  2. package/dist/types/cache/document-cache.d.ts +3 -1
  3. package/dist/types/cache/types.d.ts +4 -3
  4. package/dist/types/route-definition/helpers-types.d.ts +6 -6
  5. package/dist/types/router/match-handlers.d.ts +2 -3
  6. package/dist/types/router/segment-resolution/view-transition-default.d.ts +3 -4
  7. package/dist/types/router/transition-when.d.ts +13 -0
  8. package/dist/types/rsc/shell-serve.d.ts +2 -0
  9. package/dist/types/rsc/transition-gate.d.ts +10 -14
  10. package/dist/types/server/request-context.d.ts +5 -1
  11. package/dist/types/testing/e2e/index.d.ts +1 -1
  12. package/dist/types/testing/index.d.ts +2 -2
  13. package/dist/types/testing/run-transition-when.d.ts +6 -5
  14. package/dist/types/testing/shell-status.d.ts +23 -3
  15. package/dist/types/types/segments.d.ts +27 -22
  16. package/dist/types/urls/path-helper-types.d.ts +4 -3
  17. package/dist/vite/index.js +1 -1
  18. package/package.json +20 -21
  19. package/skills/cache-guide/SKILL.md +6 -3
  20. package/skills/catalog.json +7 -1
  21. package/skills/deployment-caching/SKILL.md +176 -0
  22. package/skills/document-cache/SKILL.md +30 -3
  23. package/skills/ppr/SKILL.md +57 -25
  24. package/skills/prerender/SKILL.md +15 -8
  25. package/skills/rango/SKILL.md +20 -17
  26. package/skills/testing/SKILL.md +1 -1
  27. package/skills/testing/cache-prerender.md +5 -1
  28. package/skills/vercel/SKILL.md +22 -1
  29. package/skills/view-transitions/SKILL.md +12 -8
  30. package/src/cache/cache-scope.ts +6 -1
  31. package/src/cache/document-cache.ts +4 -2
  32. package/src/cache/types.ts +4 -3
  33. package/src/route-definition/helpers-types.ts +6 -6
  34. package/src/router/match-handlers.ts +44 -6
  35. package/src/router/match-middleware/cache-lookup.ts +10 -3
  36. package/src/router/segment-resolution/view-transition-default.ts +9 -5
  37. package/src/router/transition-when.ts +76 -0
  38. package/src/rsc/progressive-enhancement.ts +9 -14
  39. package/src/rsc/rsc-rendering.ts +142 -31
  40. package/src/rsc/shell-serve.ts +3 -0
  41. package/src/rsc/transition-gate.ts +37 -40
  42. package/src/server/request-context.ts +6 -0
  43. package/src/testing/e2e/index.ts +5 -0
  44. package/src/testing/index.ts +9 -1
  45. package/src/testing/run-transition-when.ts +42 -9
  46. package/src/testing/shell-status.ts +92 -3
  47. package/src/types/segments.ts +27 -22
  48. 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 43 agent skills in `node_modules/@rangojs/router/skills/` —
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 at the edge based on Cache-Control headers.
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
- * The capture encountered transition({ when }). Its effective hold policy is
283
- * request-dependent, so handler-free document and navigation replay must
284
- * re-run resolution to collect and evaluate the predicate.
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 AFTER the route handler (so it can read state
353
- * the handler set via `ctx.get(...)`); returning false drops this transition
354
- * for the request, so the navigation streams its loading() skeleton instead of
355
- * holding. This is a post-handler predicate distinct from intercept()'s
356
- * match-time `when` config selector (`intercept(slot, route, Comp, { when })`).
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 post-handler
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 { EntryData, InterceptEntry, InterceptSelectorContext } from "../server/context";
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) and recorded on the request
24
- * context keyed by `segmentId`, so rsc-rendering can evaluate it post-handler
25
- * outside any cache scope and drop this segment's transition when it returns
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
- * The predicates were collected during resolution (keyed by segment id) and
8
- * stripped from the serialized config; here after handlers ran and outside any
9
- * cache scope we evaluate each and drop the segment's transition when the
10
- * predicate does not hold, so the navigation streams its loading fallback
11
- * instead of holding the previous content. A predicate that throws is reported
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. The flip side is that a cache hit
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) combining transition({ when })
21
- * with cache() on the same segment freezes the gate to its cached state, so
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 (mutated) for inline use at the payload's `segments`
25
- * field.
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 two server functions the router uses — applyViewTransitionDefault
5
- * (strips the `when` function from the serialized config and records the
6
- * predicate on the request context) and gateTransitions (assembles the
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 a handler/middleware would have set this request, readable via the predicate's `get()`. */
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 HIT signal:** response header `x-rango-shell` (`HIT` | `MISS`),
15
- * always on document GETs to a ppr route that the serve path considered.
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 post-handler reads. There is no full `HandlerContext`
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` is the way to read what the handler/middleware set
19
- * via `ctx.set(...)` this request.
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 predicate NOT
45
- * re-run at all. So a source-sensitive predicate can be frozen to prefetch-time
46
- * or store-time state. This is accepted (~99% of navigations match), but if your
47
- * gate must reflect the exact click-time source, source-scope the prefetch
48
- * (`<Link prefetchKey=":source">`) and do not `cache()` that segment.
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 AFTER the route's handler runs (so `get(...)` can read
55
- * handler/middleware-set state) and outside any cache scope. Return false to
56
- * drop this segment's transition for the request; return true to apply it. The
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, … }`; a transition `when` runs
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
- * Evaluated on every fresh (cache-miss) resolution; it is NOT re-run when a
73
- * segment is replayed from the runtime cache or a build-time prerender, and a
74
- * prefetched navigation freezes it to prefetch-time state see the caveat on
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 (evaluated post-handler), the router drops this
109
- * segment's transition for the request, so the navigation streams its loading
110
- * fallback instead of holding. The predicate is server-only and never
111
- * serialized to the client; only its resolved effect (transition kept or
112
- * dropped) crosses. See {@link TransitionWhenFn}.
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: it runs
187
- * server-side after the route handler (can read `ctx.get(...)`), and returning
188
- * false drops the transition so the navigation streams its loading() skeleton.
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;
@@ -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.1.1",
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.1.1",
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": "catalog:",
212
- "@types/react-dom": "catalog:",
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": "catalog:",
217
- "react-dom": "catalog:",
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
- - **Edge / document layer** uses the HTTP `stale-while-revalidate` directive; see
135
- `/document-cache`.
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` — Edge caching with Cache-Control headers (different layer)
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)
@@ -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 the whole HTTP response at the edge with Cache-Control headers. Use when caching an entire page or response at a CDN edge, setting Cache-Control headers, or cutting origin hits for public pages not for caching a single segment or function.",
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
  },