@rangojs/router 0.1.0 → 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/bin/rango.js +11 -1
- package/dist/types/browser/dev-discovery.d.ts +11 -0
- package/dist/types/browser/navigation-store.d.ts +3 -13
- package/dist/types/browser/prefetch/loader.d.ts +10 -0
- package/dist/types/browser/prefetch/runtime.d.ts +3 -0
- package/dist/types/browser/types.d.ts +4 -16
- package/dist/types/build/generate-manifest.d.ts +6 -0
- package/dist/types/cache/cache-scope.d.ts +7 -5
- package/dist/types/cache/cf/cf-cache-store.d.ts +10 -9
- package/dist/types/cache/document-cache.d.ts +3 -1
- package/dist/types/cache/memory-segment-store.d.ts +6 -6
- package/dist/types/cache/shell-snapshot.d.ts +21 -8
- package/dist/types/cache/types.d.ts +25 -7
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +9 -8
- package/dist/types/dev-discovery-protocol.d.ts +5 -0
- package/dist/types/prerender/store.d.ts +1 -0
- package/dist/types/route-definition/helpers-types.d.ts +6 -6
- package/dist/types/route-map-builder.d.ts +4 -29
- package/dist/types/router/match-handlers.d.ts +2 -3
- package/dist/types/router/prerender-match.d.ts +4 -1
- package/dist/types/router/router-interfaces.d.ts +3 -1
- 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/handler-context.d.ts +1 -0
- package/dist/types/rsc/render-pipeline.d.ts +3 -2
- package/dist/types/rsc/rsc-rendering.d.ts +5 -10
- package/dist/types/rsc/shell-capture.d.ts +4 -8
- package/dist/types/rsc/shell-serve.d.ts +2 -0
- package/dist/types/rsc/transition-gate.d.ts +10 -14
- package/dist/types/rsc/types.d.ts +2 -0
- package/dist/types/server/context.d.ts +16 -0
- package/dist/types/server/request-context.d.ts +22 -8
- package/dist/types/server.d.ts +1 -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/route-entry.d.ts +3 -0
- package/dist/types/types/segments.d.ts +27 -22
- package/dist/types/urls/path-helper-types.d.ts +4 -3
- package/dist/types/vite/discovery/state.d.ts +3 -2
- package/dist/types/vite/utils/manifest-utils.d.ts +1 -1
- package/dist/vite/index.js +194 -107
- package/package.json +24 -20
- package/skills/cache-guide/SKILL.md +6 -3
- package/skills/caching/SKILL.md +1 -1
- 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 +90 -14
- 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/browser/dev-discovery.ts +66 -0
- package/src/browser/navigation-client.ts +6 -0
- package/src/browser/navigation-store.ts +4 -271
- package/src/browser/partial-update.ts +7 -14
- package/src/browser/prefetch/cache.ts +1 -1
- package/src/browser/prefetch/loader.ts +110 -0
- package/src/browser/prefetch/runtime.ts +7 -0
- package/src/browser/react/Link.tsx +7 -10
- package/src/browser/react/NavigationProvider.tsx +1 -1
- package/src/browser/react/use-router.ts +1 -1
- package/src/browser/rsc-router.tsx +4 -2
- package/src/browser/types.ts +4 -27
- package/src/build/generate-manifest.ts +11 -0
- package/src/cache/cache-scope.ts +23 -7
- package/src/cache/cf/cf-cache-store.ts +63 -39
- package/src/cache/document-cache.ts +4 -2
- package/src/cache/memory-segment-store.ts +17 -6
- package/src/cache/shell-snapshot.ts +45 -15
- package/src/cache/types.ts +24 -6
- package/src/cache/vercel/vercel-cache-store.ts +52 -22
- package/src/dev-discovery-protocol.ts +11 -0
- package/src/prerender/build-shell-capture.ts +7 -1
- package/src/prerender/store.ts +5 -0
- package/src/route-definition/dsl-helpers.ts +7 -2
- package/src/route-definition/helpers-types.ts +6 -6
- package/src/route-map-builder.ts +56 -49
- package/src/router/handler-context.ts +13 -5
- package/src/router/lazy-includes.ts +1 -0
- package/src/router/match-api.ts +8 -4
- package/src/router/match-handlers.ts +44 -6
- package/src/router/match-middleware/cache-lookup.ts +10 -3
- package/src/router/prerender-match.ts +19 -2
- package/src/router/router-interfaces.ts +4 -0
- package/src/router/segment-resolution/static-store.ts +36 -10
- package/src/router/segment-resolution/view-transition-default.ts +9 -5
- package/src/router/transition-when.ts +76 -0
- package/src/router.ts +34 -3
- package/src/rsc/handler-context.ts +1 -0
- package/src/rsc/handler.ts +3 -1
- package/src/rsc/loader-fetch.ts +2 -2
- package/src/rsc/manifest-init.ts +4 -11
- package/src/rsc/progressive-enhancement.ts +9 -14
- package/src/rsc/render-pipeline.ts +10 -2
- package/src/rsc/rsc-rendering.ts +319 -147
- package/src/rsc/shell-capture.ts +34 -11
- package/src/rsc/shell-serve.ts +3 -0
- package/src/rsc/transition-gate.ts +37 -40
- package/src/rsc/types.ts +2 -0
- package/src/server/context.ts +28 -0
- package/src/server/request-context.ts +38 -10
- package/src/server.ts +0 -2
- package/src/testing/dispatch.ts +1 -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/route-entry.ts +3 -0
- package/src/types/segments.ts +27 -22
- package/src/urls/include-helper.ts +1 -0
- package/src/urls/path-helper-types.ts +4 -3
- package/src/urls/path-helper.ts +8 -4
- package/src/vite/discovery/bundle-postprocess.ts +10 -7
- package/src/vite/discovery/discover-routers.ts +7 -66
- package/src/vite/discovery/prerender-collection.ts +13 -2
- package/src/vite/discovery/state.ts +4 -4
- package/src/vite/discovery/virtual-module-codegen.ts +75 -42
- package/src/vite/plugins/version-injector.ts +0 -1
- package/src/vite/plugins/virtual-entries.ts +11 -1
- package/src/vite/router-discovery.ts +132 -22
- package/src/vite/utils/manifest-utils.ts +1 -4
- package/src/vite/utils/shared-utils.ts +7 -0
package/src/rsc/shell-capture.ts
CHANGED
|
@@ -476,6 +476,8 @@ export interface ShellCaptureDebugEvent {
|
|
|
476
476
|
snapshotBytes?: number;
|
|
477
477
|
/** True when the snapshot exceeded maxSnapshotBytes and was dropped. */
|
|
478
478
|
snapshotSkipped?: boolean;
|
|
479
|
+
/** Outcome reported by a store that supports shell-write acknowledgements. */
|
|
480
|
+
storeWrite?: "stored" | "invalidated";
|
|
479
481
|
/** Consecutive failure count in the key's backoff entry, when one exists. */
|
|
480
482
|
backoffFailures?: number;
|
|
481
483
|
/** Ms remaining in the key's backoff window, when one exists. */
|
|
@@ -516,6 +518,9 @@ export function describeShellCaptureEvent(
|
|
|
516
518
|
`snapshot=${event.snapshotBytes}b${event.snapshotSkipped ? " (over cap, skipped)" : ""}`,
|
|
517
519
|
);
|
|
518
520
|
}
|
|
521
|
+
if (event.storeWrite !== undefined) {
|
|
522
|
+
parts.push(`store-write=${event.storeWrite}`);
|
|
523
|
+
}
|
|
519
524
|
if (event.backoffFailures !== undefined) {
|
|
520
525
|
parts.push(`backoff-failures=${event.backoffFailures}`);
|
|
521
526
|
}
|
|
@@ -941,13 +946,15 @@ type CaptureAttemptOutcome = "stored" | "redirect" | "no-shell" | "refused";
|
|
|
941
946
|
* bag, not a return value: captureAndStoreShell's outcome type stays a string
|
|
942
947
|
* union its existing callers (producer B, tests) consume unchanged.
|
|
943
948
|
*/
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
949
|
+
type CaptureAttemptStats = Pick<
|
|
950
|
+
ShellCaptureDebugEvent,
|
|
951
|
+
| "barrierWaitMs"
|
|
952
|
+
| "writeSettleMs"
|
|
953
|
+
| "preludeBytes"
|
|
954
|
+
| "snapshotBytes"
|
|
955
|
+
| "snapshotSkipped"
|
|
956
|
+
| "storeWrite"
|
|
957
|
+
>;
|
|
951
958
|
|
|
952
959
|
/**
|
|
953
960
|
* Run the shell capture with a single in-place retry, then store the result.
|
|
@@ -1116,6 +1123,7 @@ async function attemptCapture(
|
|
|
1116
1123
|
reqCtx,
|
|
1117
1124
|
descriptor,
|
|
1118
1125
|
);
|
|
1126
|
+
const captureStartedAt = Date.now();
|
|
1119
1127
|
|
|
1120
1128
|
return runWithRequestContext(derivedCtx, async () => {
|
|
1121
1129
|
const match = await ctx.router.match(request, { env });
|
|
@@ -1157,6 +1165,7 @@ async function attemptCapture(
|
|
|
1157
1165
|
derivedCtx,
|
|
1158
1166
|
descriptor,
|
|
1159
1167
|
stats,
|
|
1168
|
+
captureStartedAt,
|
|
1160
1169
|
);
|
|
1161
1170
|
});
|
|
1162
1171
|
}
|
|
@@ -1329,6 +1338,7 @@ export function deriveShellCaptureContext(
|
|
|
1329
1338
|
ttl: descriptor.ttl,
|
|
1330
1339
|
swr: descriptor.swr,
|
|
1331
1340
|
store: new SnapshotOnlySegmentStore(recordingStore),
|
|
1341
|
+
keyPrefix: "doc",
|
|
1332
1342
|
};
|
|
1333
1343
|
}
|
|
1334
1344
|
|
|
@@ -1355,6 +1365,7 @@ async function captureAndStoreShell(
|
|
|
1355
1365
|
reqCtx: RequestContext<any>,
|
|
1356
1366
|
capture: ShellCaptureDescriptor,
|
|
1357
1367
|
stats?: CaptureAttemptStats,
|
|
1368
|
+
captureStartedAt: number = Date.now(),
|
|
1358
1369
|
): Promise<Exclude<CaptureAttemptOutcome, "redirect">> {
|
|
1359
1370
|
const captureShellHTML = ssrModule.captureShellHTML!;
|
|
1360
1371
|
|
|
@@ -1661,15 +1672,26 @@ async function captureAndStoreShell(
|
|
|
1661
1672
|
handlerLiveHoles: handlerLayerIsLive(
|
|
1662
1673
|
reqCtx._shellCaptureHandleLiveness,
|
|
1663
1674
|
),
|
|
1664
|
-
|
|
1675
|
+
transitionWhen: reqCtx._transitionWhen?.length ? true : undefined,
|
|
1676
|
+
createdAt: captureStartedAt,
|
|
1665
1677
|
};
|
|
1666
|
-
await store.putShell(
|
|
1678
|
+
const storeWrite = await store.putShell(
|
|
1667
1679
|
capture.key,
|
|
1668
1680
|
entry,
|
|
1669
1681
|
capture.ttl,
|
|
1670
1682
|
capture.swr,
|
|
1671
1683
|
shellTags,
|
|
1672
1684
|
);
|
|
1685
|
+
if (stats && storeWrite) stats.storeWrite = storeWrite;
|
|
1686
|
+
if (storeWrite === "invalidated") {
|
|
1687
|
+
warnCaptureRefusedOnce(
|
|
1688
|
+
capture.key,
|
|
1689
|
+
"one of the shell's tags was invalidated after this capture generation started, so the store rejected the write. " +
|
|
1690
|
+
"If capture code deterministically calls updateTag() on its own shell tag, move that mutation out of the render; " +
|
|
1691
|
+
"otherwise every generation is invalidated before it can be served.",
|
|
1692
|
+
);
|
|
1693
|
+
return "refused";
|
|
1694
|
+
}
|
|
1673
1695
|
} catch (error) {
|
|
1674
1696
|
// Best-effort: a failed put must never throw out of the background task.
|
|
1675
1697
|
reportCacheError(
|
|
@@ -1680,8 +1702,9 @@ async function captureAndStoreShell(
|
|
|
1680
1702
|
);
|
|
1681
1703
|
}
|
|
1682
1704
|
}
|
|
1683
|
-
// A shell was captured (
|
|
1684
|
-
//
|
|
1705
|
+
// A shell was captured (ordinary store I/O failure is reported, not retried).
|
|
1706
|
+
// An acknowledged invalidation rejection returned `refused` above so the
|
|
1707
|
+
// scheduler backs the key off instead of recapturing on every request.
|
|
1685
1708
|
return "stored";
|
|
1686
1709
|
} finally {
|
|
1687
1710
|
// Stop the hop loop for the pathological never-quiets path (quiesce never
|
package/src/rsc/shell-serve.ts
CHANGED
|
@@ -24,6 +24,9 @@ import type { ShellCacheEntry, SegmentCacheStore } from "../cache/types.js";
|
|
|
24
24
|
/** Debug/status header the browser (and e2e assertions) can read: HIT | MISS. */
|
|
25
25
|
export const SHELL_STATUS_HEADER = "x-rango-shell";
|
|
26
26
|
|
|
27
|
+
/** Partial-navigation status header set only after captured PPR segments are consumed. */
|
|
28
|
+
export const PPR_REPLAY_STATUS_HEADER = "x-rango-ppr-replay";
|
|
29
|
+
|
|
27
30
|
/**
|
|
28
31
|
* Default shell ttl (seconds) for `ppr: true` and for a PartialPrerenderProps
|
|
29
32
|
* that omits `ttl`.
|
|
@@ -1,41 +1,58 @@
|
|
|
1
1
|
import type { MatchResult } from "../types.js";
|
|
2
|
-
import type { TransitionWhenContext } from "../types/segments.js";
|
|
3
2
|
import type { getRequestContext } from "../server/request-context.js";
|
|
4
3
|
import { invokeOnError } from "../router/error-handling.js";
|
|
5
4
|
import type { OnErrorCallback } from "../types/error-types.js";
|
|
5
|
+
import { createTransitionWhenContext } from "../router/transition-when.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Apply transition({ when }) gates to a payload's segments.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* to the router's onError (phase "rendering") and then treated as "do not hold"
|
|
16
|
-
* (conservative), so a buggy predicate degrades to no transition rather than
|
|
17
|
-
* failing the response.
|
|
10
|
+
* PPR predicates were evaluated from the manifest before cache lookup and route
|
|
11
|
+
* handlers; their request-specific decisions are projected onto a copy so they
|
|
12
|
+
* never mutate reusable cache/prerender/shell records. Ordinary-route predicates
|
|
13
|
+
* were collected during fresh resolution and are evaluated here after handlers.
|
|
14
|
+
* Both paths drop the segment transition when the predicate does not hold.
|
|
18
15
|
*
|
|
19
16
|
* Mutating the segments here is safe: the segment cache stores a serialized copy
|
|
20
17
|
* (segment-codec), written during match() BEFORE this gate runs, so dropping a
|
|
21
|
-
* transition never corrupts a cache entry.
|
|
18
|
+
* transition never corrupts a cache entry. On an ordinary route, a cache hit
|
|
22
19
|
* skips resolution, collects no predicate, and replays the cached transition
|
|
23
|
-
* as-is (it was serialized before the gate)
|
|
24
|
-
*
|
|
25
|
-
* avoid caching a route whose transition decision is request-dependent.
|
|
20
|
+
* as-is (it was serialized before the gate). PPR routes are the exception: their
|
|
21
|
+
* manifest predicates were evaluated before lookup and apply to replayed data.
|
|
26
22
|
*
|
|
27
|
-
* Returns the same array
|
|
28
|
-
*
|
|
23
|
+
* Returns the same array for the ordinary post-handler path. A PPR drop returns a
|
|
24
|
+
* copy containing only the request-specific transition changes.
|
|
29
25
|
*/
|
|
30
26
|
export function gateTransitions(
|
|
31
27
|
segments: MatchResult["segments"],
|
|
32
28
|
ctx: ReturnType<typeof getRequestContext>,
|
|
33
29
|
onError?: OnErrorCallback,
|
|
34
30
|
): MatchResult["segments"] {
|
|
31
|
+
const pprDecisions = ctx._pprTransitionDecisions;
|
|
32
|
+
let gatedSegments = segments;
|
|
33
|
+
|
|
34
|
+
const dropTransition = (id: string): void => {
|
|
35
|
+
const index = gatedSegments.findIndex((segment) => segment.id === id);
|
|
36
|
+
if (index === -1) return;
|
|
37
|
+
if (!pprDecisions) {
|
|
38
|
+
gatedSegments[index].transition = undefined;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (gatedSegments === segments) gatedSegments = [...segments];
|
|
42
|
+
gatedSegments[index] = {
|
|
43
|
+
...gatedSegments[index],
|
|
44
|
+
transition: undefined,
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (pprDecisions) {
|
|
49
|
+
for (const [id, keep] of pprDecisions) {
|
|
50
|
+
if (!keep) dropTransition(id);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
35
53
|
const predicates = ctx._transitionWhen;
|
|
36
|
-
if (predicates
|
|
54
|
+
if (predicates) {
|
|
37
55
|
for (const { id, when } of predicates) {
|
|
38
|
-
let drop: boolean;
|
|
39
56
|
try {
|
|
40
57
|
// Assemble the ShouldRevalidateFn-shaped predicate context from the
|
|
41
58
|
// request context. Source fields (currentUrl/currentParams/fromRouteName)
|
|
@@ -44,28 +61,11 @@ export function gateTransitions(
|
|
|
44
61
|
// method/get/env come straight off ctx (setRequestContextParams ran
|
|
45
62
|
// before the gate). Source/action fields are undefined when absent —
|
|
46
63
|
// never fabricated (see TransitionWhenContext).
|
|
47
|
-
|
|
48
|
-
currentUrl: ctx._gateCurrentUrl,
|
|
49
|
-
currentParams: ctx._gateCurrentParams,
|
|
50
|
-
fromRouteName:
|
|
51
|
-
ctx._prevRouteKey as TransitionWhenContext["fromRouteName"],
|
|
52
|
-
nextUrl: ctx.url,
|
|
53
|
-
nextParams: ctx.params,
|
|
54
|
-
toRouteName: ctx.routeName,
|
|
55
|
-
actionId: ctx._gateActionId,
|
|
56
|
-
actionUrl: ctx._gateActionUrl,
|
|
57
|
-
actionResult: ctx._gateActionResult,
|
|
58
|
-
formData: ctx._gateFormData,
|
|
59
|
-
method: ctx.request.method,
|
|
60
|
-
get: ctx.get,
|
|
61
|
-
env: ctx.env,
|
|
62
|
-
};
|
|
63
|
-
drop = when(whenCtx) === false;
|
|
64
|
+
if (when(createTransitionWhenContext(ctx)) !== false) continue;
|
|
64
65
|
} catch (error) {
|
|
65
66
|
// A throwing predicate must not fail the response: report it and treat
|
|
66
67
|
// the transition as gated off (do not hold). invokeOnError no-ops when
|
|
67
68
|
// onError is undefined.
|
|
68
|
-
drop = true;
|
|
69
69
|
invokeOnError(
|
|
70
70
|
onError,
|
|
71
71
|
error,
|
|
@@ -79,11 +79,8 @@ export function gateTransitions(
|
|
|
79
79
|
"RSC",
|
|
80
80
|
);
|
|
81
81
|
}
|
|
82
|
-
|
|
83
|
-
const seg = segments.find((s) => s.id === id);
|
|
84
|
-
if (seg) seg.transition = undefined;
|
|
85
|
-
}
|
|
82
|
+
dropTransition(id);
|
|
86
83
|
}
|
|
87
84
|
}
|
|
88
|
-
return
|
|
85
|
+
return gatedSegments;
|
|
89
86
|
}
|
package/src/rsc/types.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface RscPayload {
|
|
|
41
41
|
handles?: AsyncGenerator<HandleData, void, unknown>;
|
|
42
42
|
/** RSC version string for cache invalidation */
|
|
43
43
|
version?: string;
|
|
44
|
+
/** Cloudflare dev worker generation used for stale-document convergence. */
|
|
45
|
+
devDiscoveryEpoch?: number;
|
|
44
46
|
/** TTL in milliseconds for the client-side in-memory prefetch cache */
|
|
45
47
|
prefetchCacheTTL?: number;
|
|
46
48
|
/** Max entries in the client-side in-memory prefetch cache (FIFO eviction) */
|
package/src/server/context.ts
CHANGED
|
@@ -298,6 +298,10 @@ interface HelperContext {
|
|
|
298
298
|
counters: Record<string, number>;
|
|
299
299
|
forRoute?: string;
|
|
300
300
|
mountIndex?: number;
|
|
301
|
+
/** Owning router id, when known (threaded by generateManifestFull). Scopes
|
|
302
|
+
* the search-schema/root-scope registries so same-named routes in
|
|
303
|
+
* different routers don't clobber each other (route-map-builder.ts). */
|
|
304
|
+
routerId?: string;
|
|
301
305
|
metrics?: MetricsStore;
|
|
302
306
|
/** True when rendering for SSR (document requests) */
|
|
303
307
|
isSSR?: boolean;
|
|
@@ -491,6 +495,7 @@ export const getContext = (): {
|
|
|
491
495
|
counters: store.counters,
|
|
492
496
|
forRoute: store.forRoute,
|
|
493
497
|
mountIndex: store.mountIndex,
|
|
498
|
+
routerId: store.routerId,
|
|
494
499
|
metrics: store.metrics,
|
|
495
500
|
isSSR: store.isSSR,
|
|
496
501
|
patterns: store.patterns,
|
|
@@ -533,6 +538,7 @@ export const getContext = (): {
|
|
|
533
538
|
counters,
|
|
534
539
|
forRoute: store?.forRoute,
|
|
535
540
|
mountIndex: store?.mountIndex,
|
|
541
|
+
routerId: store?.routerId,
|
|
536
542
|
metrics: store?.metrics,
|
|
537
543
|
isSSR: store?.isSSR,
|
|
538
544
|
patterns,
|
|
@@ -666,6 +672,28 @@ export function getRootScoped(): boolean {
|
|
|
666
672
|
return store?.rootScoped ?? true;
|
|
667
673
|
}
|
|
668
674
|
|
|
675
|
+
/**
|
|
676
|
+
* Stamp build-time scope identity onto a Static() definition at mount time.
|
|
677
|
+
* The bake collector (prerender-collection.ts) renders defs OUTSIDE any
|
|
678
|
+
* evaluation scope and used to iterate the registry first-non-null, so these
|
|
679
|
+
* are the ONLY reliable carriers of the def's owning router, root-scope, and
|
|
680
|
+
* full route name — a name-keyed registry lookup at bake time reproduces the
|
|
681
|
+
* cross-router collision #757/#762 fixed (and the collector's historical
|
|
682
|
+
* `$$routePrefix` argument is a name PREFIX, which the root-scope registry
|
|
683
|
+
* never contains, silently degrading to the dot-heuristic).
|
|
684
|
+
* A definition mounted more than once is stamped each time; the last mount wins.
|
|
685
|
+
*/
|
|
686
|
+
export function stampStaticDefScope(
|
|
687
|
+
handler: unknown,
|
|
688
|
+
routeName?: string,
|
|
689
|
+
): void {
|
|
690
|
+
const store = RangoContext.getStore();
|
|
691
|
+
const def = handler as Record<string, unknown>;
|
|
692
|
+
if (routeName !== undefined) def.$$routeName = routeName;
|
|
693
|
+
def.$$rootScoped = getRootScoped();
|
|
694
|
+
if (store?.routerId !== undefined) def.$$routerId = store.routerId;
|
|
695
|
+
}
|
|
696
|
+
|
|
669
697
|
// Export HelperContext type for use in other modules
|
|
670
698
|
export type { HelperContext };
|
|
671
699
|
|
|
@@ -208,6 +208,9 @@ export interface RequestContext<
|
|
|
208
208
|
*/
|
|
209
209
|
_transitionWhen?: Array<{ id: string; when: TransitionWhenFn }>;
|
|
210
210
|
|
|
211
|
+
/** @internal PPR transition decisions evaluated before cache lookup/handlers. */
|
|
212
|
+
_pprTransitionDecisions?: Map<string, boolean>;
|
|
213
|
+
|
|
211
214
|
/** @internal Cache store for segment caching (optional, used by CacheScope) */
|
|
212
215
|
_cacheStore?: SegmentCacheStore;
|
|
213
216
|
|
|
@@ -254,21 +257,30 @@ export interface RequestContext<
|
|
|
254
257
|
>;
|
|
255
258
|
|
|
256
259
|
/**
|
|
257
|
-
* @internal Shell fast-path marker: makes the
|
|
258
|
-
* matched route as an implicit doc-level cache() boundary (see
|
|
260
|
+
* @internal Shell fast-path marker: makes the next eligible match treat the
|
|
261
|
+
* whole matched route as an implicit doc-level cache() boundary (see
|
|
259
262
|
* resolveShellImplicitCacheScope in cache/cache-scope.ts). Set ONLY on
|
|
260
263
|
* (a) the capture's derived context — with a record-only store so the
|
|
261
264
|
* capture's cacheRoute write lands in the snapshot, never the real store —
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
* own cache() config (including cache(false)) are never
|
|
266
|
-
* marker only applies when the route tree derived NO cache
|
|
265
|
+
* (b) a HIT tail's seeded context, and (c) an eligible normal-route partial
|
|
266
|
+
* replay, where `store` is a request-local segment overlay. Handler-live holes
|
|
267
|
+
* and conditional transitions decline replay.
|
|
268
|
+
* Routes with their own cache() config (including cache(false)) are never
|
|
269
|
+
* overridden: the marker only applies when the route tree derived NO cache
|
|
270
|
+
* scope.
|
|
267
271
|
*/
|
|
268
272
|
_shellImplicitCache?: {
|
|
269
273
|
ttl?: number;
|
|
270
274
|
swr?: number;
|
|
271
275
|
store?: SegmentCacheStore;
|
|
276
|
+
/**
|
|
277
|
+
* @internal Override the implicit scope's default cache namespace. Shell
|
|
278
|
+
* captures and navigation replay both consume the canonical document
|
|
279
|
+
* segment record even when the triggering request is partial.
|
|
280
|
+
*/
|
|
281
|
+
keyPrefix?: "doc";
|
|
282
|
+
/** @internal Called only after the implicit cache hit decodes successfully. */
|
|
283
|
+
onHit?: () => void;
|
|
272
284
|
};
|
|
273
285
|
|
|
274
286
|
/**
|
|
@@ -606,6 +618,10 @@ export interface RequestContext<
|
|
|
606
618
|
/** @internal Router basename for this request (used by redirect()) */
|
|
607
619
|
_basename?: string;
|
|
608
620
|
|
|
621
|
+
/** @internal Owning router id; scopes the search-schema/root-scope registry
|
|
622
|
+
* lookups per router (route-map-builder.ts) */
|
|
623
|
+
_routerId?: string;
|
|
624
|
+
|
|
609
625
|
/**
|
|
610
626
|
* @internal RouteSnapshot from classifyRequest, reused by match/matchPartial
|
|
611
627
|
* to avoid a second resolveRoute call. Cleared on HMR invalidation.
|
|
@@ -639,14 +655,22 @@ export type PublicRequestContext<
|
|
|
639
655
|
| "deleteCookie"
|
|
640
656
|
| "_handleStore"
|
|
641
657
|
| "_transitionWhen"
|
|
658
|
+
| "_pprTransitionDecisions"
|
|
642
659
|
| "_cacheStore"
|
|
643
660
|
| "_shellCaptureRun"
|
|
661
|
+
| "_shellImplicitCache"
|
|
662
|
+
| "_shellLoaderSeed"
|
|
663
|
+
| "_shellCaptureLoaderRecords"
|
|
664
|
+
| "_shellCaptureHandleLiveness"
|
|
665
|
+
| "_shellCaptureLoaderHandleValues"
|
|
644
666
|
| "_shellFragmentPayload"
|
|
667
|
+
| "_shellCaptureGuardTripped"
|
|
645
668
|
| "_shellCaptureGuardTrippedLoaderId"
|
|
646
669
|
| "_explicitTaggedStores"
|
|
647
670
|
| "_requestTags"
|
|
648
671
|
| "_cacheProfiles"
|
|
649
672
|
| "_onResponseCallbacks"
|
|
673
|
+
| "_pendingBackgroundTasks"
|
|
650
674
|
| "_themeConfig"
|
|
651
675
|
| "_locationState"
|
|
652
676
|
| "_routeName"
|
|
@@ -673,7 +697,9 @@ export type PublicRequestContext<
|
|
|
673
697
|
| "_renderForeground"
|
|
674
698
|
| "_renderDiagnosticsEnabled"
|
|
675
699
|
| "_handlerStart"
|
|
700
|
+
| "_tracing"
|
|
676
701
|
| "_basename"
|
|
702
|
+
| "_routerId"
|
|
677
703
|
| "_setStatus"
|
|
678
704
|
| "_rotateStateCookie"
|
|
679
705
|
| "_setKeepCacheDirective"
|
|
@@ -765,7 +791,7 @@ export function setRequestContextParams(
|
|
|
765
791
|
routeMap ?? getGlobalRouteMap(),
|
|
766
792
|
routeName,
|
|
767
793
|
params,
|
|
768
|
-
routeName ? isRouteRootScoped(routeName) : undefined,
|
|
794
|
+
routeName ? isRouteRootScoped(routeName, ctx._routerId) : undefined,
|
|
769
795
|
);
|
|
770
796
|
}
|
|
771
797
|
}
|
|
@@ -1496,7 +1522,7 @@ export function createUseFunction<TEnv>(
|
|
|
1496
1522
|
// RequestContext carries no `search` field, so reading `(ctx as any).search`
|
|
1497
1523
|
// here always yielded {} — dropping typed search for action/dispatch loaders.
|
|
1498
1524
|
const searchSchema = ctx._routeName
|
|
1499
|
-
? getSearchSchema(ctx._routeName)
|
|
1525
|
+
? getSearchSchema(ctx._routeName, ctx._routerId)
|
|
1500
1526
|
: undefined;
|
|
1501
1527
|
const loaderSearch = searchSchema
|
|
1502
1528
|
? parseSearchParams(ctx.searchParams, searchSchema)
|
|
@@ -1526,7 +1552,9 @@ export function createUseFunction<TEnv>(
|
|
|
1526
1552
|
getGlobalRouteMap(),
|
|
1527
1553
|
ctx._routeName,
|
|
1528
1554
|
ctx.params as Record<string, string>,
|
|
1529
|
-
ctx._routeName
|
|
1555
|
+
ctx._routeName
|
|
1556
|
+
? isRouteRootScoped(ctx._routeName, ctx._routerId)
|
|
1557
|
+
: undefined,
|
|
1530
1558
|
),
|
|
1531
1559
|
rendered: () => {
|
|
1532
1560
|
throw new Error(
|
package/src/server.ts
CHANGED
package/src/testing/dispatch.ts
CHANGED
|
@@ -413,6 +413,7 @@ export async function dispatch<TEnv = any>(
|
|
|
413
413
|
// with it. Mirror it so basename-redirect tests behave as they do in a real
|
|
414
414
|
// mounted app instead of always seeing no prefix.
|
|
415
415
|
requestContext._basename = router.basename;
|
|
416
|
+
requestContext._routerId = routerId;
|
|
416
417
|
|
|
417
418
|
// Match production's response-route reverse EXACTLY: the real handler builds
|
|
418
419
|
// it from the route map alone (response-route-handler.ts), with NO matched
|
package/src/testing/e2e/index.ts
CHANGED
|
@@ -41,10 +41,15 @@ export {
|
|
|
41
41
|
type CacheStatusTarget,
|
|
42
42
|
} from "../cache-status.js";
|
|
43
43
|
export {
|
|
44
|
+
assertPprReplayStatus,
|
|
44
45
|
assertShellStatus,
|
|
46
|
+
parsePprReplayStatus,
|
|
45
47
|
parseShellStatus,
|
|
48
|
+
PPR_REPLAY_STATUS_HEADER,
|
|
46
49
|
shellCacheKey,
|
|
47
50
|
SHELL_STATUS_HEADER,
|
|
51
|
+
type PprReplayBypassReason,
|
|
52
|
+
type PprReplayStatus,
|
|
48
53
|
type ShellStatus,
|
|
49
54
|
type ShellStatusTarget,
|
|
50
55
|
} from "../shell-status.js";
|
package/src/testing/index.ts
CHANGED
|
@@ -77,12 +77,20 @@ export type {
|
|
|
77
77
|
} from "../router/telemetry.js";
|
|
78
78
|
|
|
79
79
|
export {
|
|
80
|
+
assertPprReplayStatus,
|
|
80
81
|
assertShellStatus,
|
|
82
|
+
parsePprReplayStatus,
|
|
81
83
|
parseShellStatus,
|
|
84
|
+
PPR_REPLAY_STATUS_HEADER,
|
|
82
85
|
shellCacheKey,
|
|
83
86
|
SHELL_STATUS_HEADER,
|
|
84
87
|
} from "./shell-status.js";
|
|
85
|
-
export type {
|
|
88
|
+
export type {
|
|
89
|
+
PprReplayBypassReason,
|
|
90
|
+
PprReplayStatus,
|
|
91
|
+
ShellStatus,
|
|
92
|
+
ShellStatusTarget,
|
|
93
|
+
} from "./shell-status.js";
|
|
86
94
|
|
|
87
95
|
export { collectHandle } from "./collect-handle.js";
|
|
88
96
|
|
|
@@ -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
|
|
@@ -33,6 +32,9 @@ import type {
|
|
|
33
32
|
TransitionWhenContext,
|
|
34
33
|
} from "../types/segments.js";
|
|
35
34
|
import type { OnErrorCallback } from "../types/error-types.js";
|
|
35
|
+
import type { EntryData } from "../server/context.js";
|
|
36
|
+
import { evaluatePprTransitionWhen } from "../router/transition-when.js";
|
|
37
|
+
import { invokeOnError } from "../router/error-handling.js";
|
|
36
38
|
|
|
37
39
|
const toURL = (v: string | URL, base: URL): URL =>
|
|
38
40
|
typeof v === "string" ? new URL(v, base.origin) : v;
|
|
@@ -53,7 +55,7 @@ export interface RunTransitionWhenOptions<TEnv = any> {
|
|
|
53
55
|
toRouteName?: string;
|
|
54
56
|
/** Environment bindings surfaced as `env` (and `ctx.env`). */
|
|
55
57
|
env?: TEnv;
|
|
56
|
-
/** Variables
|
|
58
|
+
/** Variables readable via the predicate's `get()`. With `ppr`, these model pre-handler middleware/input state. */
|
|
57
59
|
vars?: VarsInit;
|
|
58
60
|
/** Navigation SOURCE url (`currentUrl`): a URL or path string. */
|
|
59
61
|
currentUrl?: string | URL;
|
|
@@ -71,6 +73,8 @@ export interface RunTransitionWhenOptions<TEnv = any> {
|
|
|
71
73
|
formData?: FormData;
|
|
72
74
|
/** Receives an error thrown by the predicate (the gate reports to `router.onError`, phase `"rendering"`). */
|
|
73
75
|
onError?: OnErrorCallback;
|
|
76
|
+
/** Model a route with `ppr`, where the predicate runs before route handlers and cache lookup. */
|
|
77
|
+
ppr?: boolean;
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
/**
|
|
@@ -138,8 +142,37 @@ export function runTransitionWhen<TEnv = any>(
|
|
|
138
142
|
: config;
|
|
139
143
|
|
|
140
144
|
return runWithRequestContext(reqCtx, () => {
|
|
141
|
-
|
|
142
|
-
|
|
145
|
+
if (opts.ppr) {
|
|
146
|
+
const entry = {
|
|
147
|
+
type: "route",
|
|
148
|
+
id: "tx-when-entry",
|
|
149
|
+
shortCode: "tx-when-seg",
|
|
150
|
+
parent: null,
|
|
151
|
+
transition: configForGate,
|
|
152
|
+
layout: [],
|
|
153
|
+
parallel: {},
|
|
154
|
+
} as unknown as EntryData;
|
|
155
|
+
evaluatePprTransitionWhen(
|
|
156
|
+
[entry],
|
|
157
|
+
reqCtx,
|
|
158
|
+
{ params: reqCtx.params, routeName: opts.toRouteName },
|
|
159
|
+
(error, segmentId) =>
|
|
160
|
+
invokeOnError(
|
|
161
|
+
opts.onError,
|
|
162
|
+
error,
|
|
163
|
+
"rendering",
|
|
164
|
+
{
|
|
165
|
+
request: reqCtx.request,
|
|
166
|
+
url: reqCtx.url,
|
|
167
|
+
params: reqCtx.params,
|
|
168
|
+
segmentId,
|
|
169
|
+
},
|
|
170
|
+
"RSC",
|
|
171
|
+
),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
// Ordinary predicates are collected here and evaluated by the gate. PPR
|
|
175
|
+
// predicates keep the decision already evaluated before this step.
|
|
143
176
|
const serialized = applyViewTransitionDefault(
|
|
144
177
|
configForGate,
|
|
145
178
|
undefined,
|
|
@@ -153,12 +186,12 @@ export function runTransitionWhen<TEnv = any>(
|
|
|
153
186
|
component: null,
|
|
154
187
|
transition: serialized,
|
|
155
188
|
} as ResolvedSegment;
|
|
156
|
-
gateTransitions(
|
|
189
|
+
const [gatedSegment] = gateTransitions(
|
|
157
190
|
[segment],
|
|
158
191
|
reqCtx as Parameters<typeof gateTransitions>[1],
|
|
159
192
|
opts.onError,
|
|
160
193
|
);
|
|
161
|
-
const kept =
|
|
194
|
+
const kept = gatedSegment.transition !== undefined;
|
|
162
195
|
return { kept, dropped: !kept, whenContext, ctx: reqCtx };
|
|
163
196
|
});
|
|
164
197
|
}
|