@rangojs/router 0.26.5 → 0.27.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/dist/bin/rango.js +2 -2
- package/dist/types/browser/action-coordinator.d.ts +0 -4
- package/dist/types/browser/action-fence.d.ts +11 -3
- package/dist/types/browser/event-controller.d.ts +18 -0
- package/dist/types/browser/history-state.d.ts +4 -0
- package/dist/types/browser/intercept-utils.d.ts +23 -0
- package/dist/types/browser/rsc-router.d.ts +1 -1
- package/dist/types/prefetch-markers.d.ts +12 -5
- package/dist/types/router/match-context.d.ts +8 -0
- package/dist/types/router/match-middleware/cache-lookup.d.ts +1 -1
- package/dist/types/router/match-middleware/cache-store.d.ts +2 -2
- package/dist/types/router/revalidation.d.ts +6 -1
- package/dist/types/router/router-context.d.ts +2 -17
- package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
- package/dist/types/testing/serve-shell-request.d.ts +7 -0
- package/dist/types/types/handler-context.d.ts +9 -6
- package/dist/types/vite/plugins/server-ref-hashing.d.ts +8 -1
- package/dist/vite/index.js +10 -6
- package/package.json +2 -2
- package/skills/client-urls/SKILL.md +2 -1
- package/skills/document-cache/SKILL.md +5 -0
- package/skills/intercept/SKILL.md +36 -6
- package/skills/layout/SKILL.md +11 -7
- package/skills/loader/SKILL.md +30 -13
- package/skills/middleware/SKILL.md +1 -1
- package/skills/migrate-nextjs/SKILL.md +5 -4
- package/skills/parallel/SKILL.md +5 -4
- package/skills/ppr/SKILL.md +1 -0
- package/skills/rango/SKILL.md +18 -10
- package/skills/server-actions/SKILL.md +43 -5
- package/skills/testing/predicates.md +1 -1
- package/skills/testing/serve-shell-request.md +7 -7
- package/skills/ux-architecture/SKILL.md +3 -2
- package/src/browser/action-coordinator.ts +2 -13
- package/src/browser/action-fence.ts +11 -3
- package/src/browser/event-controller.ts +37 -0
- package/src/browser/history-state.ts +9 -1
- package/src/browser/intercept-utils.ts +51 -0
- package/src/browser/navigation-bridge.ts +20 -17
- package/src/browser/navigation-client.ts +5 -10
- package/src/browser/navigation-transaction.ts +3 -10
- package/src/browser/partial-update.ts +8 -0
- package/src/browser/rsc-router.tsx +16 -2
- package/src/browser/server-action-bridge.ts +223 -78
- package/src/cache/document-cache.ts +12 -6
- package/src/prefetch-markers.ts +13 -5
- package/src/router/match-context.ts +13 -0
- package/src/router/match-middleware/cache-lookup.ts +21 -10
- package/src/router/match-middleware/cache-store.ts +10 -4
- package/src/router/match-result.ts +1 -20
- package/src/router/navigation-snapshot.ts +2 -1
- package/src/router/revalidation.ts +33 -7
- package/src/router/router-context.ts +4 -22
- package/src/rsc/rsc-rendering.ts +12 -4
- package/src/rsc/shell-capture-constants.ts +2 -0
- package/src/ssr/ssr-root.tsx +1 -0
- package/src/testing/serve-shell-request.ts +20 -2
- package/src/types/handler-context.ts +9 -6
- package/src/vite/plugins/server-ref-hashing.ts +16 -5
package/dist/bin/rango.js
CHANGED
|
@@ -1644,7 +1644,7 @@ var init_package = __esm({
|
|
|
1644
1644
|
"package.json"() {
|
|
1645
1645
|
package_default = {
|
|
1646
1646
|
name: "@rangojs/router",
|
|
1647
|
-
version: "0.
|
|
1647
|
+
version: "0.27.0",
|
|
1648
1648
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
1649
1649
|
keywords: [
|
|
1650
1650
|
"react",
|
|
@@ -1852,7 +1852,7 @@ var init_package = __esm({
|
|
|
1852
1852
|
},
|
|
1853
1853
|
dependencies: {
|
|
1854
1854
|
"@types/debug": "^4.1.12",
|
|
1855
|
-
"@vitejs/plugin-rsc": "^0.5.
|
|
1855
|
+
"@vitejs/plugin-rsc": "^0.5.38",
|
|
1856
1856
|
debug: "^4.4.1",
|
|
1857
1857
|
"magic-string": "^0.30.17",
|
|
1858
1858
|
picomatch: "^4.0.4",
|
|
@@ -6,8 +6,6 @@ import type { ActionEntry } from "./event-controller.js";
|
|
|
6
6
|
*/
|
|
7
7
|
type ActionScenario = {
|
|
8
8
|
type: "navigated-away";
|
|
9
|
-
historyKeyChanged: boolean;
|
|
10
|
-
onInterceptRoute: boolean;
|
|
11
9
|
} | {
|
|
12
10
|
type: "hmr-missing";
|
|
13
11
|
} | {
|
|
@@ -44,8 +42,6 @@ export interface ActionOutcomeInput {
|
|
|
44
42
|
reconciledSegmentCount: number;
|
|
45
43
|
/** Number of matched segment IDs from server */
|
|
46
44
|
matchedCount: number;
|
|
47
|
-
/** Current intercept source URL (null when not on intercept route) */
|
|
48
|
-
currentInterceptSource: string | null;
|
|
49
45
|
}
|
|
50
46
|
/**
|
|
51
47
|
* Classify a post-reconciliation action outcome. Ordered priority chain: each
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
* start. While the fence is up, the decision of whether the action invalidated
|
|
7
7
|
* anything is deferred to the response:
|
|
8
8
|
*
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* - a navigation during the flight is an ordinary one: a prefetch is served,
|
|
10
|
+
* and the page it commits may hold pre-write data until the follow-up below,
|
|
11
|
+
* - a navigation without a prefetch, and a prefetch, fetch with
|
|
12
|
+
* `cache: "no-store"` so they cannot be served stale bytes from the
|
|
13
|
+
* browser's Vary-keyed HTTP cache,
|
|
12
14
|
* - popstate reads are treated as stale-while-revalidate.
|
|
13
15
|
*
|
|
14
16
|
* Nothing is wiped, rotated, or broadcast while the fence is up — that is what
|
|
@@ -16,6 +18,12 @@
|
|
|
16
18
|
* actions compose: each action raises and lowers its own reference, and the
|
|
17
19
|
* fence is down only when the count reaches zero.
|
|
18
20
|
*
|
|
21
|
+
* The drop runs the batch's follow-up: when an action of the batch did not
|
|
22
|
+
* land on the page on screen, the action bridge refetches that page once,
|
|
23
|
+
* whole, after the navigation the user started, unless an intercept is on
|
|
24
|
+
* screen (server-action-bridge.ts followUp; the rules are R1-R7 in
|
|
25
|
+
* docs/internal/execution-model.md).
|
|
26
|
+
*
|
|
19
27
|
* The refcount is a single module-level counter, not keyed by routerId.
|
|
20
28
|
* Consumers (navigation-client.ts, prefetch/fetch.ts, navigation-bridge.ts)
|
|
21
29
|
* read it unscoped, so an action in one router would suppress another router's
|
|
@@ -20,6 +20,18 @@ export interface NavigationEntry {
|
|
|
20
20
|
options?: NavigateOptionsInternal & {
|
|
21
21
|
skipLoadingState?: boolean;
|
|
22
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Resolves once the navigation has ended: its transaction ended and every
|
|
25
|
+
* stream it opened closed. True when it committed, false when it ended
|
|
26
|
+
* without a commit (a failure) or was left (aborted, which settles it at
|
|
27
|
+
* once). A `prefetch: false` fill holds the transaction open
|
|
28
|
+
* (partial-update.ts awaits fillHoles before it resolves) and its stream
|
|
29
|
+
* until the fill's answer streamed, so a waiter never cuts a fill or a
|
|
30
|
+
* stream short. Only the action bridge's follow-up awaits it
|
|
31
|
+
* (server-action-bridge.ts followUp); no hook reads it, and settling it
|
|
32
|
+
* notifies nothing.
|
|
33
|
+
*/
|
|
34
|
+
settled: Promise<boolean>;
|
|
23
35
|
}
|
|
24
36
|
/**
|
|
25
37
|
* Entry tracking an in-flight action
|
|
@@ -279,6 +291,12 @@ export interface EventController {
|
|
|
279
291
|
/** True once after a `treeless` commit. */
|
|
280
292
|
takeTreelessLocationState(): boolean;
|
|
281
293
|
getCurrentNavigation(): NavigationEntry | null;
|
|
294
|
+
/**
|
|
295
|
+
* The newest navigation until it settles (NavigationEntry.settled): before
|
|
296
|
+
* its commit, and after it while its transaction or a stream is still
|
|
297
|
+
* open. Null once it settled or was left.
|
|
298
|
+
*/
|
|
299
|
+
getUnsettledNavigation(): NavigationEntry | null;
|
|
282
300
|
getInflightActions(): Map<string, ActionEntry>;
|
|
283
301
|
/** Whether any concurrent actions have occurred (shared across all handles) */
|
|
284
302
|
hadAnyConcurrentActions(): boolean;
|
|
@@ -46,6 +46,10 @@ export declare function stripShellMissMarker(): void;
|
|
|
46
46
|
*
|
|
47
47
|
* A slot is recognised by its key alone (the `~r` suffix): at start-up the
|
|
48
48
|
* module that defines it may not be loaded.
|
|
49
|
+
*
|
|
50
|
+
* The same pass drops the entry's intercept marks (`intercept`,
|
|
51
|
+
* `sourceUrl`): a document request never renders an intercept, so the entry
|
|
52
|
+
* is a full page for later Back/Forward.
|
|
49
53
|
*/
|
|
50
54
|
export declare function clearLocationStateOnDocumentLoad(): void;
|
|
51
55
|
/**
|
|
@@ -31,3 +31,26 @@ export declare function hasActiveIntercept(slots?: Record<string, SlotState>): b
|
|
|
31
31
|
* whether interception happens depends on the current page context.
|
|
32
32
|
*/
|
|
33
33
|
export declare function isInterceptOnlyCache(segments: ResolvedSegment[]): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* An intercept's `matched`: the client's ids without any of the slot's (its
|
|
36
|
+
* id and every loader segment on it, another intercept's into the same slot
|
|
37
|
+
* included), then the slot's (interceptSegmentIds). One order for the
|
|
38
|
+
* prefetch, its fill, an action and a revisit: a fill whose `matched` differs
|
|
39
|
+
* from its tree's is rendered as a new tree (browser/partial-update.ts
|
|
40
|
+
* fillHoles). Shared by router/match-result.ts and wholeInterceptMatched.
|
|
41
|
+
*/
|
|
42
|
+
export declare function interceptMatched(clientIds: readonly string[], slotIds: readonly string[]): string[];
|
|
43
|
+
/**
|
|
44
|
+
* The `matched` the browser keeps for an intercept answer to a request that
|
|
45
|
+
* held no segments (a followed redirect, a Back/Forward revalidation with
|
|
46
|
+
* handles pending, the dev HMR refetch), or undefined to keep the answer's.
|
|
47
|
+
* The server never renders the page under an intercept and lists it only
|
|
48
|
+
* from the ids a request held, so such an answer has an empty outlet under
|
|
49
|
+
* the slot. The page it lands over (`held`) stays, as the server lists it for
|
|
50
|
+
* a request that holds it.
|
|
51
|
+
*/
|
|
52
|
+
export declare function wholeInterceptMatched(held: readonly ResolvedSegment[], answer: {
|
|
53
|
+
matched?: string[];
|
|
54
|
+
segments?: ResolvedSegment[];
|
|
55
|
+
slots?: Record<string, SlotState>;
|
|
56
|
+
}): string[] | undefined;
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The wire markers of prefetches
|
|
3
|
-
* (docs/design/prefetch-false.md): shared by the browser
|
|
4
|
-
* (router/segment-resolution/prefetch-deferral.ts requestKind,
|
|
5
|
-
* rsc/prefetch-render-error.ts
|
|
6
|
-
*
|
|
2
|
+
* The wire markers of prefetches, of `prefetch: false`
|
|
3
|
+
* (docs/design/prefetch-false.md) and of stale requests: shared by the browser
|
|
4
|
+
* and the server (router/segment-resolution/prefetch-deferral.ts requestKind,
|
|
5
|
+
* rsc/prefetch-render-error.ts, rsc/rsc-rendering.ts,
|
|
6
|
+
* cache/document-cache.ts, router/navigation-snapshot.ts). Dependency free,
|
|
7
|
+
* like segment-fragments.ts: the browser must not import the server modules
|
|
8
|
+
* that use them.
|
|
7
9
|
*/
|
|
8
10
|
/** Request header of a `<Link>` prefetch (browser/prefetch/fetch.ts). */
|
|
9
11
|
export declare const PREFETCH_HEADER = "X-Rango-Prefetch";
|
|
10
12
|
/** Query param of a fill request (browser/navigation-client.ts). */
|
|
11
13
|
export declare const FILL_PARAM = "_rsc_fill";
|
|
14
|
+
/**
|
|
15
|
+
* Query param of a stale request (browser/navigation-client.ts): `revalidate()`
|
|
16
|
+
* sees `stale: true` (router/navigation-snapshot.ts).
|
|
17
|
+
*/
|
|
18
|
+
export declare const STALE_PARAM = "_rsc_stale";
|
|
12
19
|
/**
|
|
13
20
|
* The last row of a prefetch whose render reported an error
|
|
14
21
|
* (rsc/prefetch-render-error.ts): a Flight hint with a code React does not
|
|
@@ -166,6 +166,14 @@ export interface MatchContext<TEnv = any> {
|
|
|
166
166
|
}>;
|
|
167
167
|
isFullMatch: boolean;
|
|
168
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* An action, or a stale partial (`_rsc_stale`: Back/Forward onto an entry
|
|
171
|
+
* marked stale, `refresh()`, the cross-tab refresh, the action follow-up, a
|
|
172
|
+
* zero-segment refetch), renders live: no route-cache or prerender-store read
|
|
173
|
+
* a live handler can replace, and no store. An action's render is never
|
|
174
|
+
* stored, so a record can be older than the copy the client holds after it.
|
|
175
|
+
*/
|
|
176
|
+
export declare function rendersLive(ctx: Pick<MatchContext, "isAction" | "stale">): boolean;
|
|
169
177
|
/**
|
|
170
178
|
* Mutable state that flows through the pipeline
|
|
171
179
|
*/
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
*/
|
|
94
94
|
import type { ResolvedSegment } from "../../types.js";
|
|
95
95
|
import { type EntryData } from "../../server/context.js";
|
|
96
|
-
import type
|
|
96
|
+
import { type MatchContext, type MatchPipelineState } from "../match-context.js";
|
|
97
97
|
import type { PrerenderStore } from "../../prerender/store.js";
|
|
98
98
|
import type { PrerenderTargetObject, ResolvedPrerender } from "../../prerender/on-demand.js";
|
|
99
99
|
import { type RequestContext } from "../../server/request-context.js";
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* +---------------------+
|
|
20
20
|
* | Should skip cache? |
|
|
21
21
|
* | - !cacheScope |──yes──> return
|
|
22
|
-
* | -
|
|
22
|
+
* | - rendersLive |
|
|
23
23
|
* | - cacheHit |
|
|
24
24
|
* | - method !== GET |
|
|
25
25
|
* +---------------------+
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
* - Non-GET request (only GET requests are cacheable)
|
|
102
102
|
*/
|
|
103
103
|
import type { ResolvedSegment } from "../../types.js";
|
|
104
|
-
import type
|
|
104
|
+
import { type MatchContext, type MatchPipelineState } from "../match-context.js";
|
|
105
105
|
import type { GeneratorMiddleware } from "./cache-lookup.js";
|
|
106
106
|
/**
|
|
107
107
|
* Creates cache store middleware
|
|
@@ -40,8 +40,13 @@ export interface EvaluateRevalidationOptions<TEnv> {
|
|
|
40
40
|
context: HandlerContext<any, TEnv>;
|
|
41
41
|
/** Action context if triggered by action */
|
|
42
42
|
actionContext?: ActionContext;
|
|
43
|
-
/**
|
|
43
|
+
/** The browser's `_rsc_stale` signal: seeds the action defaults. */
|
|
44
44
|
stale?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The route-cache entry is past its TTL (SWR). Predicates see it as `stale`;
|
|
47
|
+
* it never seeds the action defaults.
|
|
48
|
+
*/
|
|
49
|
+
cacheStale?: boolean;
|
|
45
50
|
/** Trace source hint for the revalidation trace */
|
|
46
51
|
traceSource?: RevalidationTraceEntry["source"];
|
|
47
52
|
/**
|
|
@@ -4,6 +4,7 @@ import type { HandlerContext, ResolvedSegment, ShouldRevalidateFn } from "../typ
|
|
|
4
4
|
import type { RouteMatchResult } from "./pattern-matching.js";
|
|
5
5
|
import type { TelemetrySink } from "./telemetry.js";
|
|
6
6
|
import type { ResolveSegmentOptions } from "./segment-resolution.js";
|
|
7
|
+
import type { EvaluateRevalidationOptions } from "./revalidation.js";
|
|
7
8
|
/**
|
|
8
9
|
* Revalidation context passed to segment resolution
|
|
9
10
|
*/
|
|
@@ -70,23 +71,7 @@ export interface RouterContext<TEnv = any> {
|
|
|
70
71
|
items: T[];
|
|
71
72
|
matchedIds: string[];
|
|
72
73
|
}>;
|
|
73
|
-
evaluateRevalidation: (params:
|
|
74
|
-
segment: ResolvedSegment;
|
|
75
|
-
prevParams: Record<string, string>;
|
|
76
|
-
getPrevSegment: (() => Promise<ResolvedSegment | undefined>) | null;
|
|
77
|
-
request: Request;
|
|
78
|
-
prevUrl: URL;
|
|
79
|
-
nextUrl: URL;
|
|
80
|
-
revalidations: Array<{
|
|
81
|
-
name: string;
|
|
82
|
-
fn: any;
|
|
83
|
-
}>;
|
|
84
|
-
routeKey: string;
|
|
85
|
-
context: HandlerContext<any, TEnv>;
|
|
86
|
-
actionContext?: any;
|
|
87
|
-
stale?: boolean;
|
|
88
|
-
traceSource?: "segment-resolution" | "cache-hit" | "loader" | "parallel" | "orphan-layout" | "route-handler" | "layout-handler" | "intercept-loader";
|
|
89
|
-
}) => Promise<boolean>;
|
|
74
|
+
evaluateRevalidation: (params: EvaluateRevalidationOptions<TEnv>) => Promise<boolean>;
|
|
90
75
|
getRequestContext: () => {
|
|
91
76
|
waitUntil: (fn: () => Promise<void>) => void;
|
|
92
77
|
_handleStore?: any;
|
|
@@ -41,7 +41,7 @@ export declare const PPR_REPLAY_STATUS_HEADER: string;
|
|
|
41
41
|
* producer (rsc-rendering.ts) and the parser (testing/shell-status.ts): a
|
|
42
42
|
* reason the parser does not list parses as null.
|
|
43
43
|
*/
|
|
44
|
-
export declare const PPR_REPLAY_BYPASS_REASONS: readonly ["method", "dynamic", "nonce", "store-unavailable", "passive-read-unsupported", "no-navigation-context", "prerender-store", "intercept", "cache-disabled", "read-error", "no-entry", "invalid-version", "corrupt-entry", "no-segment-snapshot", "snapshot-miss", "explicit-cache-hit", "rewritten"];
|
|
44
|
+
export declare const PPR_REPLAY_BYPASS_REASONS: readonly ["method", "stale", "dynamic", "nonce", "store-unavailable", "passive-read-unsupported", "no-navigation-context", "prerender-store", "intercept", "cache-disabled", "read-error", "no-entry", "invalid-version", "corrupt-entry", "no-segment-snapshot", "snapshot-miss", "explicit-cache-hit", "rewritten"];
|
|
45
45
|
/** One of {@link PPR_REPLAY_BYPASS_REASONS}. */
|
|
46
46
|
export type PprReplayBypassReason = (typeof PPR_REPLAY_BYPASS_REASONS)[number];
|
|
47
47
|
/**
|
|
@@ -76,6 +76,12 @@ export interface ServeShellRequestOptions<TEnv = any> {
|
|
|
76
76
|
* that source. `rewriteTarget` sends the `metadata.rewriteTarget` the page at
|
|
77
77
|
* `from` carried when middleware rewrote it (`_rsc_rt`), as the browser
|
|
78
78
|
* does: the server then diffs against the route that rendered that page.
|
|
79
|
+
* `stale: true` sends a stale request (`_rsc_stale`), as the browser sends
|
|
80
|
+
* it: every `revalidate()` sees `stale: true` (see `revalidate()`'s `stale`)
|
|
81
|
+
* and segments take an action's revalidation defaults; it renders live, as
|
|
82
|
+
* an action does, so neither the route cache nor the document cache answers
|
|
83
|
+
* or stores it, and a `ppr` route's `replayStatus` is
|
|
84
|
+
* `{ outcome: "BYPASS", reason: "stale" }`.
|
|
79
85
|
*/
|
|
80
86
|
partial?: true | {
|
|
81
87
|
from?: string;
|
|
@@ -84,6 +90,7 @@ export interface ServeShellRequestOptions<TEnv = any> {
|
|
|
84
90
|
prefetch?: boolean;
|
|
85
91
|
fill?: boolean;
|
|
86
92
|
interceptSource?: string;
|
|
93
|
+
stale?: boolean;
|
|
87
94
|
};
|
|
88
95
|
/**
|
|
89
96
|
* POST this form as a submit before hydration: the no-JS request the
|
|
@@ -510,10 +510,10 @@ export type IsActionFn = (...actions: ActionRef[]) => boolean;
|
|
|
510
510
|
* ```ts
|
|
511
511
|
* import * as CartActions from "./actions/cart";
|
|
512
512
|
*
|
|
513
|
-
* // Idiom A — "mine, else defer": re-render on my actions
|
|
514
|
-
* // undefined so the segment's default decision
|
|
515
|
-
* // revalidators get a say).
|
|
516
|
-
* revalidate((ctx) => ctx.isAction(CartActions) || undefined)
|
|
513
|
+
* // Idiom A — "mine, else defer": re-render on my actions and on Back after
|
|
514
|
+
* // an action, otherwise return undefined so the segment's default decision
|
|
515
|
+
* // still applies (and downstream revalidators get a say).
|
|
516
|
+
* revalidate((ctx) => ctx.isAction(CartActions) || ctx.stale || undefined)
|
|
517
517
|
*
|
|
518
518
|
* // Idiom B — "mine only": re-render on my actions and suppress everything
|
|
519
519
|
* // else (isAction returns a raw boolean, so an unrelated action yields false).
|
|
@@ -629,8 +629,11 @@ export type ShouldRevalidateFn<TParams = GenericParams, TEnv = any> = (args: {
|
|
|
629
629
|
*/
|
|
630
630
|
toRouteName?: DefaultRouteName;
|
|
631
631
|
/**
|
|
632
|
-
* `true`
|
|
633
|
-
*
|
|
632
|
+
* `true` on a revalidation of a page the browser marked stale (the
|
|
633
|
+
* `_rsc_stale` query param): Back/Forward onto a history entry after a
|
|
634
|
+
* server action in this or another tab, or while one is in flight. A request
|
|
635
|
+
* that holds no segments (the follow-up after an action, `refresh()`) is
|
|
636
|
+
* marked stale too, but there every segment renders and no predicate runs.
|
|
634
637
|
*
|
|
635
638
|
* This is NOT segment cache staleness (loaders are never segment-cached).
|
|
636
639
|
* Use this to decide whether loader data should be re-fetched after an
|
|
@@ -3,7 +3,14 @@ import type { Plugin } from "vite";
|
|
|
3
3
|
* Replace dev-style server-reference ids with their production hashes. The temp
|
|
4
4
|
* server renders Static/Prerender in a dev/serve Vite server, so plugin-rsc mints
|
|
5
5
|
* a dev-style id (e.g. "/src/urls/prerender.tsx"); rewriting only the id (group 2)
|
|
6
|
-
* leaves the hoisted function
|
|
6
|
+
* leaves the hoisted function untouched.
|
|
7
|
+
*
|
|
8
|
+
* The same id also prefixes the action id ("<id>#<name>") that plugin-rsc
|
|
9
|
+
* >= 0.5.37 passes to encryptActionBoundArgs/decryptActionBoundArgs as AES-GCM
|
|
10
|
+
* additional data. The production bundle decrypts under "<hash>#<name>", so bound
|
|
11
|
+
* args encrypted at build time under the dev-style id fail with OperationError on
|
|
12
|
+
* invocation. Rewriting every `"<id>#` literal to `"<hash>#` binds them to the
|
|
13
|
+
* runtime's id.
|
|
7
14
|
* Exported for testing; used by hashServerRefs. Returns null if nothing changed.
|
|
8
15
|
*/
|
|
9
16
|
export declare function transformServerRefs(code: string, projectRoot: string): string | null;
|
package/dist/vite/index.js
CHANGED
|
@@ -7078,7 +7078,7 @@ function resolveVersionsFrom(table, routerId2) {
|
|
|
7078
7078
|
// package.json
|
|
7079
7079
|
var package_default = {
|
|
7080
7080
|
name: "@rangojs/router",
|
|
7081
|
-
version: "0.
|
|
7081
|
+
version: "0.27.0",
|
|
7082
7082
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
7083
7083
|
keywords: [
|
|
7084
7084
|
"react",
|
|
@@ -7286,7 +7286,7 @@ var package_default = {
|
|
|
7286
7286
|
},
|
|
7287
7287
|
dependencies: {
|
|
7288
7288
|
"@types/debug": "^4.1.12",
|
|
7289
|
-
"@vitejs/plugin-rsc": "^0.5.
|
|
7289
|
+
"@vitejs/plugin-rsc": "^0.5.38",
|
|
7290
7290
|
debug: "^4.4.1",
|
|
7291
7291
|
"magic-string": "^0.30.17",
|
|
7292
7292
|
picomatch: "^4.0.4",
|
|
@@ -10468,18 +10468,22 @@ init_client_ref_hashing();
|
|
|
10468
10468
|
var debug9 = createRangoDebugger(NS.transform);
|
|
10469
10469
|
function transformServerRefs(code, projectRoot) {
|
|
10470
10470
|
if (!code.includes("registerServerReference")) return null;
|
|
10471
|
-
|
|
10472
|
-
|
|
10471
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
10472
|
+
let result = code.replace(
|
|
10473
10473
|
registerServerReferenceRegex(),
|
|
10474
10474
|
(match, _value, refKey) => {
|
|
10475
10475
|
const cleanKey = refKey.split("?")[0];
|
|
10476
10476
|
const hash = computeProductionHash(projectRoot, cleanKey);
|
|
10477
10477
|
if (hash === cleanKey) return match;
|
|
10478
|
-
|
|
10478
|
+
hashes.set(refKey, hash);
|
|
10479
10479
|
return match.replace(`"${refKey}"`, `"${hash}"`);
|
|
10480
10480
|
}
|
|
10481
10481
|
);
|
|
10482
|
-
|
|
10482
|
+
if (hashes.size === 0) return null;
|
|
10483
|
+
for (const [refKey, hash] of hashes) {
|
|
10484
|
+
result = result.replaceAll(`"${refKey}#`, `"${hash}#`);
|
|
10485
|
+
}
|
|
10486
|
+
return result;
|
|
10483
10487
|
}
|
|
10484
10488
|
function hashServerRefs(projectRoot) {
|
|
10485
10489
|
const counter = createCounter(debug9, "hash-server-refs");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -194,7 +194,7 @@
|
|
|
194
194
|
},
|
|
195
195
|
"dependencies": {
|
|
196
196
|
"@types/debug": "^4.1.12",
|
|
197
|
-
"@vitejs/plugin-rsc": "^0.5.
|
|
197
|
+
"@vitejs/plugin-rsc": "^0.5.38",
|
|
198
198
|
"debug": "^4.4.1",
|
|
199
199
|
"magic-string": "^0.30.17",
|
|
200
200
|
"picomatch": "^4.0.4",
|
|
@@ -326,7 +326,8 @@ Per request: a JS action POST runs the predicate with `method: "POST"`,
|
|
|
326
326
|
`FormData` argument) and `actionUrl`; the default is true. An in-group
|
|
327
327
|
navigation GET defaults to true when params or search changed. A prefetch
|
|
328
328
|
partial runs the predicate, so a prefetched in-group navigation honours it. A
|
|
329
|
-
stale back/forward revalidation sees `stale: true
|
|
329
|
+
stale back/forward revalidation sees `stale: true` and takes an action's
|
|
330
|
+
defaults (the default is true). Document requests and
|
|
330
331
|
no-JS form posts hold no segments, so every loader renders whatever a predicate
|
|
331
332
|
says. On the action-triggered refetch GET (consolidation) `isAction()` is false,
|
|
332
333
|
as for `urls()`. `currentParams`/`nextParams` are the server's route params,
|
|
@@ -202,6 +202,11 @@ suffixes are still appended.
|
|
|
202
202
|
|
|
203
203
|
- Server actions (`_rsc_action`)
|
|
204
204
|
- Loader requests (`_rsc_loader`)
|
|
205
|
+
- Stale requests (`_rsc_stale`): the client marks stale the follow-up
|
|
206
|
+
revalidation after an action, a Back/Forward entry's revalidation, and any
|
|
207
|
+
request that holds no segments (`router.refresh()`, a cross-tab refresh).
|
|
208
|
+
Each renders at the origin and is not stored, because its data may predate
|
|
209
|
+
a write and the cache cannot tell.
|
|
205
210
|
- Non-GET requests
|
|
206
211
|
- Responses without an `s-maxage` `Cache-Control` directive
|
|
207
212
|
- Responses whose `Cache-Control` also contains `private`, `no-store`, or an
|
|
@@ -64,11 +64,29 @@ export const urlpatterns = urls(({ path, layout, intercept, loader, loading }) =
|
|
|
64
64
|
|
|
65
65
|
## Navigation Behavior
|
|
66
66
|
|
|
67
|
-
| Navigation Type
|
|
68
|
-
|
|
|
69
|
-
| Click link `/shop/product/abc`
|
|
70
|
-
| Direct URL `/shop/product/abc
|
|
71
|
-
|
|
|
67
|
+
| Navigation Type | What Renders |
|
|
68
|
+
| ----------------------------------------------------- | ---------------------------------------------------- |
|
|
69
|
+
| Click link `/shop/product/abc` | `<ProductModal />` in `@modal`, background preserved |
|
|
70
|
+
| Direct URL `/shop/product/abc`, or a reload | Full `<ProductPage />` page |
|
|
71
|
+
| Back from the open intercept | The intercept closes; the source page stays |
|
|
72
|
+
| Back/Forward onto the intercept's entry | The intercept over its source page |
|
|
73
|
+
| The same, uncached, from a page other than the source | Full `<ProductPage />` page, as a document load |
|
|
74
|
+
|
|
75
|
+
Only a client navigation from the source page shows an intercept. Back/Forward
|
|
76
|
+
restores an intercept's entry from the history cache (`initBrowserApp`'s
|
|
77
|
+
`storeOptions.cacheSize`, 20 by default), or fetches it when its source page is
|
|
78
|
+
on screen. From any other page (after a reload, or once the entry was evicted)
|
|
79
|
+
it costs a full page load, from the top, which drops `clearOnReload` location
|
|
80
|
+
state like any document load, and the entry is a full page from then on. In
|
|
81
|
+
dev, an edit that changes the app version clears the history cache, so the
|
|
82
|
+
next Back/Forward onto an intercept entry takes that path.
|
|
83
|
+
|
|
84
|
+
A server action on `/shop` that calls `redirect("/shop/product/abc")` opens the
|
|
85
|
+
intercept too, and so does a link from `/shop` to a route that redirects
|
|
86
|
+
there. The page under it is the browser's copy, as on a link click: it
|
|
87
|
+
keeps its last render, so it shows its pre-action data until the intercept
|
|
88
|
+
closes. An action submitted from inside an open intercept leaves that page the
|
|
89
|
+
same way.
|
|
72
90
|
|
|
73
91
|
## Intercept with Layout
|
|
74
92
|
|
|
@@ -146,6 +164,18 @@ the modal keeps the data of the others, as it does on a link to another
|
|
|
146
164
|
intercepted route that reads the same loader. In `clientUrls()` the predicate
|
|
147
165
|
runs on the server too (`/client-urls`).
|
|
148
166
|
|
|
167
|
+
The page under an intercept does not re-render while the intercept is on
|
|
168
|
+
screen. An action that settles after the intercept opened (one sent from the
|
|
169
|
+
page underneath just before the user opened it) runs no background
|
|
170
|
+
revalidation. The page underneath keeps its data until the user leaves the
|
|
171
|
+
intercept. Leaving it with Back revalidates that page as stale (the
|
|
172
|
+
action marked the history cache stale): loaders that opt in with
|
|
173
|
+
`revalidate(({ stale }) => stale)` re-run. Going Back or Forward out of the
|
|
174
|
+
intercept while an action from it is in flight does not abort the action:
|
|
175
|
+
`useAction()` gets its result, and the page underneath gets the background
|
|
176
|
+
revalidation (`/server-actions`, "When the user navigates while the action
|
|
177
|
+
runs").
|
|
178
|
+
|
|
149
179
|
A `revalidate()` placed directly in the intercept's `use()` (or returned from
|
|
150
180
|
its handler's `.use`) throws at definition time. Attach it to the loader:
|
|
151
181
|
|
|
@@ -177,7 +207,7 @@ import type { Revalidate } from "@rangojs/router";
|
|
|
177
207
|
import * as ProductActions from "./actions/product";
|
|
178
208
|
|
|
179
209
|
export const revalidateProductShell: Revalidate = (ctx) =>
|
|
180
|
-
ctx.isAction(ProductActions) || undefined;
|
|
210
|
+
ctx.stale || ctx.isAction(ProductActions) || undefined;
|
|
181
211
|
|
|
182
212
|
layout(ProductLayout, () => [
|
|
183
213
|
revalidate(revalidateProductShell), // producer reruns
|
package/skills/layout/SKILL.md
CHANGED
|
@@ -238,7 +238,8 @@ A standalone `layout()` renders when it is first mounted and is then kept as-is:
|
|
|
238
238
|
params auto-filled) keep the old params too; render param-dependent nav in a
|
|
239
239
|
client component with `useReverse(routes)` (`/links`).
|
|
240
240
|
- **Actions** skip it too (the revalidation trace calls this
|
|
241
|
-
`action:parent-chain-skip`)
|
|
241
|
+
`action:parent-chain-skip`), and so does Back/Forward onto a page an action
|
|
242
|
+
made stale (`stale:parent-chain-skip`).
|
|
242
243
|
|
|
243
244
|
Orphan layouts inside a `path()` are the opposite: they belong to the route
|
|
244
245
|
entry and re-render with it (on param/search changes and on every action).
|
|
@@ -249,9 +250,9 @@ or `undefined` to defer to the default:
|
|
|
249
250
|
```typescript
|
|
250
251
|
import * as CartActions from "./actions/cart";
|
|
251
252
|
|
|
252
|
-
// Re-render after any cart action
|
|
253
|
+
// Re-render after any cart action, and on Back after an action
|
|
253
254
|
layout(<CartLayout />, () => [
|
|
254
|
-
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
255
|
+
revalidate((ctx) => ctx.isAction(CartActions) || ctx.stale || undefined),
|
|
255
256
|
|
|
256
257
|
path("/cart", CartPage, { name: "cart" }),
|
|
257
258
|
])
|
|
@@ -292,7 +293,7 @@ import type { Revalidate } from "@rangojs/router";
|
|
|
292
293
|
import { addToCart } from "./actions/cart";
|
|
293
294
|
|
|
294
295
|
export const revalidateCartData: Revalidate = (ctx) =>
|
|
295
|
-
ctx.isAction(addToCart) || undefined;
|
|
296
|
+
ctx.stale || ctx.isAction(addToCart) || undefined;
|
|
296
297
|
```
|
|
297
298
|
|
|
298
299
|
```typescript
|
|
@@ -305,8 +306,11 @@ layout(<CartLayout />, () => [
|
|
|
305
306
|
```
|
|
306
307
|
|
|
307
308
|
On the layout the contract adds the re-render the default skips after an
|
|
308
|
-
action
|
|
309
|
-
|
|
309
|
+
action, and on Back/Forward onto a page an action made stale (`ctx.stale`).
|
|
310
|
+
The route segment already re-runs on both, so there it changes nothing by
|
|
311
|
+
default; the shared name documents the dependency. Leave out `ctx.stale` and
|
|
312
|
+
Back after any action re-runs the page without its layout: the page reads
|
|
313
|
+
`undefined`.
|
|
310
314
|
|
|
311
315
|
If a segment depends on multiple upstream domains, compose multiple
|
|
312
316
|
contracts (`revalidateAuthData`, `revalidateCartData`, and so on).
|
|
@@ -319,7 +323,7 @@ import { revalidate, type Revalidate } from "@rangojs/router";
|
|
|
319
323
|
import * as AuthActions from "./actions/auth";
|
|
320
324
|
|
|
321
325
|
export const revalidateAuthData: Revalidate = (ctx) =>
|
|
322
|
-
ctx.isAction(AuthActions) || undefined;
|
|
326
|
+
ctx.stale || ctx.isAction(AuthActions) || undefined;
|
|
323
327
|
export const revalidateAuth = () => [revalidate(revalidateAuthData)];
|
|
324
328
|
```
|
|
325
329
|
|
package/skills/loader/SKILL.md
CHANGED
|
@@ -396,12 +396,26 @@ Segment defaults when no revalidator decides (from
|
|
|
396
396
|
`src/router/revalidation.ts`). A segment the client does not hold yet always
|
|
397
397
|
renders.
|
|
398
398
|
|
|
399
|
-
| Segment | After an action (POST) | Navigation |
|
|
400
|
-
| -------------------------------------------------------------------- |
|
|
401
|
-
| Route (`path()`) | `true`
|
|
402
|
-
| Owned by the route (loaders, layouts, parallels inside the `path()`) | `true`
|
|
403
|
-
| Loader above the route (e.g. on a parent `layout()`) | `true`
|
|
404
|
-
| Layout or parallel above the route | `false`
|
|
399
|
+
| Segment | After an action (POST), or a stale Back/Forward | Navigation |
|
|
400
|
+
| -------------------------------------------------------------------- | ----------------------------------------------- | ----------------------------------- |
|
|
401
|
+
| Route (`path()`) | `true` | `true` when params or search change |
|
|
402
|
+
| Owned by the route (loaders, layouts, parallels inside the `path()`) | `true` | `true` when params or search change |
|
|
403
|
+
| Loader above the route (e.g. on a parent `layout()`) | `true` | `false` |
|
|
404
|
+
| Layout or parallel above the route | `false` | `false` |
|
|
405
|
+
|
|
406
|
+
A stale Back/Forward is the background revalidation of a page a server action
|
|
407
|
+
(in this tab or another) marked stale: Back renders the cached page, then
|
|
408
|
+
revalidates it with `stale: true`. It takes an action's defaults, so a layout's
|
|
409
|
+
cart count shows the write with no opt-in. It renders live, as an action does:
|
|
410
|
+
no route-cache record or PPR replay answers it. It is not an action:
|
|
411
|
+
`isAction()` is false and `actionId` is undefined. To keep a loader as it was
|
|
412
|
+
on that request:
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
loader(RecommendationsLoader, () => [
|
|
416
|
+
revalidate(({ stale }) => (stale ? false : undefined)),
|
|
417
|
+
]);
|
|
418
|
+
```
|
|
405
419
|
|
|
406
420
|
#### `|| undefined` (defer) vs `?? false` (hard) — pick deliberately
|
|
407
421
|
|
|
@@ -421,9 +435,10 @@ Pick the idiom by what the segment's default already does (table above). After
|
|
|
421
435
|
an action, loaders, the route, and segments inside the `path()` already re-run;
|
|
422
436
|
layouts and parallels above the route are skipped:
|
|
423
437
|
|
|
424
|
-
- **Add a signal where the default skips.**
|
|
425
|
-
|
|
426
|
-
|
|
438
|
+
- **Add a signal where the default skips.**
|
|
439
|
+
`ctx.isAction(X) || ctx.stale || undefined` on a parent layout or a
|
|
440
|
+
layout-level parallel re-renders it after `X` and on Back after an action,
|
|
441
|
+
and defers otherwise. On a loader, the route, or a segment inside the `path()` it
|
|
427
442
|
changes nothing: they already re-run after every action, and on navigation it
|
|
428
443
|
defers.
|
|
429
444
|
- **Narrow after actions where the default re-runs.**
|
|
@@ -487,10 +502,12 @@ the same named revalidation contract across producer and consumer segments.
|
|
|
487
502
|
import type { Revalidate } from "@rangojs/router";
|
|
488
503
|
import * as AccountActions from "./actions/account";
|
|
489
504
|
|
|
490
|
-
// Match by reference with ctx.isAction() (rename-safe),
|
|
491
|
-
//
|
|
505
|
+
// Match by reference with ctx.isAction() (rename-safe), true on a stale
|
|
506
|
+
// Back/Forward (the page re-runs, so its producer must), and defer
|
|
507
|
+
// (|| undefined) so these contracts compose — a hard `false` would
|
|
508
|
+
// short-circuit the rest.
|
|
492
509
|
export const revalidateAccountScope: Revalidate = (ctx) =>
|
|
493
|
-
ctx.isAction(AccountActions) || undefined;
|
|
510
|
+
ctx.stale || ctx.isAction(AccountActions) || undefined;
|
|
494
511
|
|
|
495
512
|
// urls.tsx
|
|
496
513
|
layout(AccountLayout, () => [
|
|
@@ -955,7 +972,7 @@ function ProductPage() {
|
|
|
955
972
|
|
|
956
973
|
A skeleton that has appeared stays on screen for at least about 300 ms before React replaces it with what it was waiting for: React does not replace a fallback sooner than 300 ms after the last fallback committed ([#1078](https://github.com/rangojs/rango/issues/1078)). On a cold click that mounts the `loading()` boundary, a loader that finishes sooner than that still shows its skeleton for about 300 ms, while a slower loader is not delayed. The router leaves this to React: an early skeleton is preferred to holding the click, and a fully prefetched click commits complete and shows none. A nested `loading()` whose data lands after the outer one has been revealed shows its own fallback, and the 300 ms starts again. The loaders feature page on the docs site has the measured figures. Under `transition()` a related cost applies; see [view-transitions](../view-transitions/SKILL.md).
|
|
957
974
|
|
|
958
|
-
A loader that throws (anything but `notFound()` or `redirect()`) throws at its `useLoader` read on every client lane: a plain or fully prefetched click, Back/Forward restored from the history cache, and the rerun after a server action. A prefetch in which a loader threw is never used, in memory or from the browser's HTTP cache: the click asks the server again, so a loader that has recovered shows its data. The nearest boundary to the read catches it (your React error boundary around the read, else the route's `errorBoundary()`, else the router's root one), and the layouts above keep their instance. The background refresh after Back/Forward onto a stale page is the exception: a loader that fails there leaves the page on screen and logs `[Browser] Background revalidation failed`. Not covered: an `intercept()`'s loaders, and a parallel slot's loaders under its own `loading()`, which are decoded above the slot's content.
|
|
975
|
+
A loader that throws (anything but `notFound()` or `redirect()`) throws at its `useLoader` read on every client lane: a plain or fully prefetched click, Back/Forward restored from the history cache, and the rerun after a server action. A prefetch in which a loader threw is never used, in memory or from the browser's HTTP cache: the click asks the server again, so a loader that has recovered shows its data. The nearest boundary to the read catches it (your React error boundary around the read, else the route's `errorBoundary()`, else the router's root one), and the layouts above keep their instance. The background refresh after Back/Forward onto a stale page is the exception: it re-runs every loader on the defaults (see the defaults table), and a loader that fails there leaves the page on screen as it was and logs `[Browser] Background revalidation failed`. Not covered: an `intercept()`'s loaders, and a parallel slot's loaders under its own `loading()`, which are decoded above the slot's content.
|
|
959
976
|
|
|
960
977
|
## Loader Authority: notFound() and redirect()
|
|
961
978
|
|