@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2
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/AGENTS.md +6 -10
- package/README.md +289 -938
- package/dist/bin/rango.js +271 -46
- package/dist/vite/index.js +673 -193
- package/package.json +10 -8
- package/skills/api-client/SKILL.md +1 -1
- package/skills/breadcrumbs/SKILL.md +31 -14
- package/skills/cache-guide/SKILL.md +5 -2
- package/skills/caching/SKILL.md +59 -4
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/debug-manifest/SKILL.md +1 -1
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +9 -1
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +8 -8
- package/skills/hooks/SKILL.md +54 -892
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +4 -4
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +46 -14
- package/skills/layout/SKILL.md +27 -10
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +23 -1
- package/skills/middleware/SKILL.md +7 -3
- package/skills/migrate-nextjs/SKILL.md +167 -6
- package/skills/migrate-react-router/SKILL.md +59 -677
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/observability/SKILL.md +9 -1
- package/skills/parallel/SKILL.md +23 -4
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +28 -18
- package/skills/rango/SKILL.md +84 -25
- package/skills/response-routes/SKILL.md +15 -1
- package/skills/route/SKILL.md +71 -4
- package/skills/router-setup/SKILL.md +14 -3
- package/skills/scripts/SKILL.md +1 -1
- package/skills/server-actions/SKILL.md +3 -2
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +1 -1
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/handles.md +4 -2
- package/skills/testing/render-handler.md +15 -14
- package/skills/testing/reverse-and-types.md +8 -7
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -919
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +30 -3
- package/skills/vercel/SKILL.md +1 -1
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/browser/event-controller.ts +62 -10
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +65 -16
- package/src/browser/navigation-client.ts +32 -2
- package/src/browser/navigation-store.ts +128 -14
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +76 -17
- package/src/browser/prefetch/cache.ts +51 -11
- package/src/browser/prefetch/fetch.ts +59 -21
- package/src/browser/prefetch/queue.ts +19 -4
- package/src/browser/react/Link.tsx +13 -3
- package/src/browser/react/NavigationProvider.tsx +108 -4
- package/src/browser/response-adapter.ts +38 -9
- package/src/browser/rsc-router.tsx +54 -4
- package/src/browser/scroll-restoration.ts +7 -5
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +22 -10
- package/src/browser/types.ts +54 -1
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/index.ts +3 -1
- package/src/build/route-trie.ts +35 -7
- package/src/build/route-types/include-resolution.ts +347 -47
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-key-utils.ts +29 -0
- package/src/cache/cache-runtime.ts +262 -71
- package/src/cache/cache-scope.ts +2 -17
- package/src/cache/cache-tag.ts +60 -14
- package/src/cache/cf/cf-cache-store.ts +243 -20
- package/src/cache/document-cache.ts +54 -21
- package/src/cache/index.ts +1 -0
- package/src/cache/memory-segment-store.ts +110 -3
- package/src/cache/profile-registry.ts +15 -0
- package/src/cache/read-through-swr.ts +15 -1
- package/src/cache/segment-codec.ts +4 -4
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/types.ts +158 -0
- package/src/cache/vercel/vercel-cache-store.ts +401 -124
- package/src/client.rsc.tsx +0 -3
- package/src/client.tsx +0 -3
- package/src/cloudflare/tracing.ts +7 -8
- package/src/defer.ts +11 -22
- package/src/handle.ts +37 -15
- package/src/handles/MetaTags.tsx +16 -82
- package/src/handles/breadcrumbs.ts +12 -14
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +7 -8
- package/src/handles/meta.ts +7 -44
- package/src/host/errors.ts +15 -0
- package/src/host/index.ts +1 -0
- package/src/index.rsc.ts +8 -2
- package/src/index.ts +19 -13
- package/src/internal-debug.ts +11 -8
- package/src/prerender.ts +17 -4
- package/src/redirect-origin.ts +14 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/route-content-wrapper.tsx +12 -5
- package/src/route-definition/dsl-helpers.ts +21 -32
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +43 -43
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/resolve-handler-use.ts +0 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -4
- package/src/route-types.ts +0 -5
- package/src/router/find-match.ts +86 -8
- package/src/router/instrument.ts +9 -4
- package/src/router/lazy-includes.ts +72 -12
- package/src/router/loader-resolution.ts +14 -2
- package/src/router/manifest.ts +56 -11
- package/src/router/match-api.ts +76 -32
- package/src/router/match-handlers.ts +181 -135
- package/src/router/match-middleware/background-revalidation.ts +40 -23
- package/src/router/match-middleware/cache-store.ts +39 -24
- package/src/router/match-result.ts +35 -15
- package/src/router/middleware.ts +64 -38
- package/src/router/navigation-snapshot.ts +7 -5
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +53 -64
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +11 -5
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +23 -8
- package/src/router/route-snapshot.ts +14 -2
- package/src/router/router-context.ts +3 -1
- package/src/router/router-interfaces.ts +32 -1
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +39 -3
- package/src/router/segment-resolution/loader-cache.ts +93 -2
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +3 -0
- package/src/router/segment-resolution/view-transition-default.ts +35 -15
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/telemetry-otel.ts +6 -8
- package/src/router/telemetry.ts +9 -1
- package/src/router/tracing.ts +14 -5
- package/src/router/trie-matching.ts +19 -11
- package/src/router/url-params.ts +13 -0
- package/src/router.ts +47 -16
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler.ts +60 -33
- package/src/rsc/manifest-init.ts +1 -1
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/progressive-enhancement.ts +61 -4
- package/src/rsc/redirect-guard.ts +2 -1
- package/src/rsc/rsc-rendering.ts +429 -37
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +30 -0
- package/src/segment-loader-promise.ts +18 -0
- package/src/segment-system.tsx +149 -14
- package/src/server/context.ts +67 -9
- package/src/server/cookie-store.ts +73 -1
- package/src/server/loader-registry.ts +13 -1
- package/src/server/request-context.ts +169 -10
- package/src/ssr/index.tsx +462 -178
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/testing/collect-handle.ts +14 -8
- package/src/testing/dispatch.ts +152 -40
- package/src/testing/generated-routes.ts +27 -11
- package/src/testing/index.ts +6 -0
- package/src/testing/render-handler.ts +14 -0
- package/src/testing/render-route.tsx +13 -10
- package/src/testing/run-transition-when.ts +164 -0
- package/src/theme/ThemeProvider.tsx +36 -26
- package/src/types/handler-context.ts +1 -1
- package/src/types/index.ts +2 -0
- package/src/types/route-config.ts +19 -7
- package/src/types/segments.ts +100 -0
- package/src/urls/include-helper.ts +10 -8
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +44 -12
- package/src/urls/path-helper.ts +5 -0
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +43 -18
- package/src/urls/urls-function.ts +0 -1
- package/src/vercel/tracing.ts +7 -7
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/index.ts +7 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugins/vercel-output.ts +114 -25
- package/src/vite/plugins/version-injector.ts +22 -7
- package/src/vite/plugins/virtual-entries.ts +80 -22
- package/src/vite/rango.ts +29 -19
- package/src/vite/router-discovery.ts +171 -43
- package/src/vite/utils/prerender-utils.ts +17 -4
- package/src/vite/utils/shared-utils.ts +47 -0
- package/src/network-error-thrower.tsx +0 -18
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integrated PPR shell serving (Axis 2, see docs/design/ppr-shell-resume.md).
|
|
3
|
+
*
|
|
4
|
+
* PPR is opt-in per PAGE ROUTE via the `ppr` path option
|
|
5
|
+
* (`path(pattern, Handler, { name, ppr: true | PartialPrerenderProps })`) and the
|
|
6
|
+
* serving logic is INTEGRAL to the render pipeline — there is no middleware to
|
|
7
|
+
* mount. This module owns the config/key/store plumbing the render layer
|
|
8
|
+
* (rsc-rendering.ts) uses at its COMMIT POINT, which sits after the WHOLE
|
|
9
|
+
* middleware chain (global `router.use()` chain AND route DSL `middleware()`,
|
|
10
|
+
* both of which wrap the render pass): any middleware rejection/redirect wins
|
|
11
|
+
* before a single shell byte is written.
|
|
12
|
+
*
|
|
13
|
+
* The shell store is the app-level `createRouter({ cache })` store
|
|
14
|
+
* (`requestCtx._cacheStore`). A store without the `getShell`/`putShell` family
|
|
15
|
+
* degrades a ppr route to axis 1 with a once-per-key warning (the declared
|
|
16
|
+
* intent cannot be honored — unlike an undeclared route, which is silent).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import React from "react";
|
|
20
|
+
import type { EntryData } from "../server/context.js";
|
|
21
|
+
import { sortedSearchString } from "../cache/cache-key-utils.js";
|
|
22
|
+
import type { ShellCacheEntry, SegmentCacheStore } from "../cache/types.js";
|
|
23
|
+
|
|
24
|
+
/** Debug/status header the browser (and e2e assertions) can read: HIT | MISS. */
|
|
25
|
+
export const SHELL_STATUS_HEADER = "x-rango-shell";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Default shell ttl (seconds) for `ppr: true` and for a PartialPrerenderProps
|
|
29
|
+
* that omits `ttl`.
|
|
30
|
+
*/
|
|
31
|
+
export const DEFAULT_PPR_TTL_SECONDS = 300;
|
|
32
|
+
|
|
33
|
+
/** The route's ppr option normalized to a concrete policy. */
|
|
34
|
+
export interface ResolvedPprConfig {
|
|
35
|
+
ttl: number;
|
|
36
|
+
swr?: number;
|
|
37
|
+
tags?: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Normalize the matched page route's `ppr` path option. Returns null when the
|
|
42
|
+
* route does not declare `ppr` (or declares `ppr: false`) — the caller then does
|
|
43
|
+
* NOTHING: no store read, no capture, no logs. Pure axis 1, zero cost.
|
|
44
|
+
*
|
|
45
|
+
* PPR is a DOCUMENT-level property of the page route; there is no subtree
|
|
46
|
+
* inheritance (declaring it on a layout is not supported — a follow-up).
|
|
47
|
+
*/
|
|
48
|
+
export function resolvePprConfig(
|
|
49
|
+
entry: EntryData | undefined | null,
|
|
50
|
+
): ResolvedPprConfig | null {
|
|
51
|
+
if (!entry || entry.type !== "route") return null;
|
|
52
|
+
const ppr = entry.ppr;
|
|
53
|
+
if (ppr === undefined || ppr === false) return null;
|
|
54
|
+
if (ppr === true) return { ttl: DEFAULT_PPR_TTL_SECONDS };
|
|
55
|
+
return {
|
|
56
|
+
ttl: ppr.ttl ?? DEFAULT_PPR_TTL_SECONDS,
|
|
57
|
+
swr: ppr.swr,
|
|
58
|
+
tags: ppr.tags,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Shell cache key: host + pathname + sorted search + a `:shell` namespace suffix
|
|
64
|
+
* (so it can never collide with a document-cache key; the store further isolates
|
|
65
|
+
* the shell family internally).
|
|
66
|
+
*
|
|
67
|
+
* The key includes the request HOST: in a multi-tenant host-router deployment
|
|
68
|
+
* (one worker, one shared KV/runtime-cache store) a host-less key would serve
|
|
69
|
+
* tenant A's captured shell to tenant B's users.
|
|
70
|
+
*/
|
|
71
|
+
export function buildShellKey(url: URL): string {
|
|
72
|
+
const sorted = sortedSearchString(url.searchParams);
|
|
73
|
+
const searchSuffix = sorted ? `?${sorted}` : "";
|
|
74
|
+
return `${url.host}${url.pathname}${searchSuffix}:shell`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Version gates for a stored shell: reactVersion AND buildVersion must both
|
|
79
|
+
* match the running server. The postponed blob encodes hole positions against
|
|
80
|
+
* one exact tree, so resuming it under a different React OR a different app
|
|
81
|
+
* build tree-mismatches inside resume() — after the 200 + prelude committed,
|
|
82
|
+
* with no recovery. Either mismatch is a miss: the recapture overwrites the
|
|
83
|
+
* same key (self-healing) and the entry otherwise ages out via TTL. An entry
|
|
84
|
+
* with no buildVersion (stored before the field existed) is a miss for the
|
|
85
|
+
* same reason — its build is unknown, so it cannot be proven resumable.
|
|
86
|
+
*/
|
|
87
|
+
export function isValidShellHit(
|
|
88
|
+
entry: ShellCacheEntry,
|
|
89
|
+
buildVersion: string,
|
|
90
|
+
): boolean {
|
|
91
|
+
return (
|
|
92
|
+
entry.reactVersion === React.version && entry.buildVersion === buildVersion
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Payload integrity gate, run BEFORE the HIT response commits: a stored entry
|
|
98
|
+
* whose prelude is not decodable base64 or whose postponed blob is not
|
|
99
|
+
* parseable JSON would otherwise throw AFTER the 200 + full static prelude
|
|
100
|
+
* flushed (`serveShellHit` decodes at stream construction, `resumeShellHTML`
|
|
101
|
+
* parses in the tail) — the client gets a visually complete page that never
|
|
102
|
+
* hydrates, re-served on every request until the entry ages out (no eviction
|
|
103
|
+
* path exists; failure schedules no recapture by itself). Checking here turns
|
|
104
|
+
* a corrupt entry (store-layer fault) into a plain MISS the recapture
|
|
105
|
+
* overwrites. Cost: one duplicate decode/parse per HIT, sub-ms against a
|
|
106
|
+
* prelude flush that dominates the path.
|
|
107
|
+
*/
|
|
108
|
+
export function hasIntactShellPayload(entry: ShellCacheEntry): boolean {
|
|
109
|
+
try {
|
|
110
|
+
base64ToBytes(entry.prelude);
|
|
111
|
+
if (entry.postponed !== null) JSON.parse(entry.postponed);
|
|
112
|
+
return true;
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Decode a base64 prelude back into bytes for stream composition. */
|
|
119
|
+
export function base64ToBytes(b64: string): Uint8Array {
|
|
120
|
+
const binary = atob(b64);
|
|
121
|
+
const bytes = new Uint8Array(binary.length);
|
|
122
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
123
|
+
return bytes;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** True when the store implements the shell entry family. */
|
|
127
|
+
export function hasShellFamily(
|
|
128
|
+
store: SegmentCacheStore | undefined,
|
|
129
|
+
): store is SegmentCacheStore & {
|
|
130
|
+
getShell: NonNullable<SegmentCacheStore["getShell"]>;
|
|
131
|
+
putShell: NonNullable<SegmentCacheStore["putShell"]>;
|
|
132
|
+
} {
|
|
133
|
+
return !!store?.getShell && !!store?.putShell;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Keys already warned about a missing shell store family (once per key). */
|
|
137
|
+
const warnedMissingStore = new Set<string>();
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Warn once per key that a route declared `ppr` but the app-level cache store
|
|
141
|
+
* does not implement the shell family (getShell/putShell), so the route stays on
|
|
142
|
+
* axis 1. Unlike an undeclared route (silent), a declared route that cannot be
|
|
143
|
+
* honored deserves a diagnostic.
|
|
144
|
+
*/
|
|
145
|
+
export function warnShellStoreMissingOnce(key: string): void {
|
|
146
|
+
if (warnedMissingStore.has(key)) return;
|
|
147
|
+
warnedMissingStore.add(key);
|
|
148
|
+
console.warn(
|
|
149
|
+
`[rango] Route for "${key}" declares the ppr path option, but the app-level ` +
|
|
150
|
+
"cache store does not implement the shell family (getShell/putShell), so " +
|
|
151
|
+
"the route is served on axis 1 without a shell. Use MemorySegmentCacheStore, " +
|
|
152
|
+
"CFCacheStore, or VercelCacheStore (or add the family to your custom store) " +
|
|
153
|
+
"via createRouter({ cache }).",
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Keys already warned about an active per-request nonce (once per key). */
|
|
158
|
+
const warnedNonceActive = new Set<string>();
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Warn once per key that a route declared `ppr` but a per-request CSP nonce is
|
|
162
|
+
* active for the request, so the route stays on axis 1 (a shared shell would
|
|
163
|
+
* freeze one request's nonce for every visitor — useNonce() renders it into every
|
|
164
|
+
* nonced script/style/meta and the browser's CSP would then reject the frozen
|
|
165
|
+
* nonce for all but the capture request). The nonce blocks capture whether it came
|
|
166
|
+
* from the `createRouter({ nonce })` provider or from a direct `ctx.set(nonce, …)`
|
|
167
|
+
* token write in middleware. Same declared-intent-cannot-be-honored doctrine as
|
|
168
|
+
* the missing-store warning above (an undeclared route stays silent).
|
|
169
|
+
*/
|
|
170
|
+
export function warnPprNonceActiveOnce(key: string): void {
|
|
171
|
+
if (warnedNonceActive.has(key)) return;
|
|
172
|
+
warnedNonceActive.add(key);
|
|
173
|
+
console.warn(
|
|
174
|
+
`[rango] Route for "${key}" declares the ppr path option, but a per-request ` +
|
|
175
|
+
"CSP nonce is active for this request (from createRouter({ nonce }) or a " +
|
|
176
|
+
"ctx.set(nonce, …) token write in middleware), so the route is served on " +
|
|
177
|
+
"axis 1 without a shell. A shell is shared per host+URL; baking one " +
|
|
178
|
+
"request's nonce into it would break CSP for every other visitor. Drop the " +
|
|
179
|
+
"ppr option on this route, or stop setting a per-request nonce for it.",
|
|
180
|
+
);
|
|
181
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { MatchResult } from "../types.js";
|
|
2
|
+
import type { TransitionWhenContext } from "../types/segments.js";
|
|
3
|
+
import type { getRequestContext } from "../server/request-context.js";
|
|
4
|
+
import { invokeOnError } from "../router/error-handling.js";
|
|
5
|
+
import type { OnErrorCallback } from "../types/error-types.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Apply transition({ when }) gates to a payload's segments.
|
|
9
|
+
*
|
|
10
|
+
* The predicates were collected during resolution (keyed by segment id) and
|
|
11
|
+
* stripped from the serialized config; here — after handlers ran and outside any
|
|
12
|
+
* cache scope — we evaluate each and drop the segment's transition when the
|
|
13
|
+
* predicate does not hold, so the navigation streams its loading fallback
|
|
14
|
+
* instead of holding the previous content. A predicate that throws is reported
|
|
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.
|
|
18
|
+
*
|
|
19
|
+
* Mutating the segments here is safe: the segment cache stores a serialized copy
|
|
20
|
+
* (segment-codec), written during match() BEFORE this gate runs, so dropping a
|
|
21
|
+
* transition never corrupts a cache entry. The flip side is that a cache hit
|
|
22
|
+
* skips resolution, collects no predicate, and replays the cached transition
|
|
23
|
+
* as-is (it was serialized before the gate) — combining transition({ when })
|
|
24
|
+
* with cache() on the same segment freezes the gate to its cached state, so
|
|
25
|
+
* avoid caching a route whose transition decision is request-dependent.
|
|
26
|
+
*
|
|
27
|
+
* Returns the same array (mutated) for inline use at the payload's `segments`
|
|
28
|
+
* field.
|
|
29
|
+
*/
|
|
30
|
+
export function gateTransitions(
|
|
31
|
+
segments: MatchResult["segments"],
|
|
32
|
+
ctx: ReturnType<typeof getRequestContext>,
|
|
33
|
+
onError?: OnErrorCallback,
|
|
34
|
+
): MatchResult["segments"] {
|
|
35
|
+
const predicates = ctx._transitionWhen;
|
|
36
|
+
if (predicates && predicates.length) {
|
|
37
|
+
for (const { id, when } of predicates) {
|
|
38
|
+
let drop: boolean;
|
|
39
|
+
try {
|
|
40
|
+
// Assemble the ShouldRevalidateFn-shaped predicate context from the
|
|
41
|
+
// request context. Source fields (currentUrl/currentParams/fromRouteName)
|
|
42
|
+
// were stashed at match time from the navigation snapshot; action fields
|
|
43
|
+
// at the action-bearing gate call sites. nextUrl/nextParams/toRouteName/
|
|
44
|
+
// method/get/env come straight off ctx (setRequestContextParams ran
|
|
45
|
+
// before the gate). Source/action fields are undefined when absent —
|
|
46
|
+
// never fabricated (see TransitionWhenContext).
|
|
47
|
+
const whenCtx: TransitionWhenContext = {
|
|
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
|
+
} catch (error) {
|
|
65
|
+
// A throwing predicate must not fail the response: report it and treat
|
|
66
|
+
// the transition as gated off (do not hold). invokeOnError no-ops when
|
|
67
|
+
// onError is undefined.
|
|
68
|
+
drop = true;
|
|
69
|
+
invokeOnError(
|
|
70
|
+
onError,
|
|
71
|
+
error,
|
|
72
|
+
"rendering",
|
|
73
|
+
{
|
|
74
|
+
request: ctx.request,
|
|
75
|
+
url: ctx.url,
|
|
76
|
+
params: ctx.params,
|
|
77
|
+
segmentId: id,
|
|
78
|
+
},
|
|
79
|
+
"RSC",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (drop) {
|
|
83
|
+
const seg = segments.find((s) => s.id === id);
|
|
84
|
+
if (seg) seg.transition = undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return segments;
|
|
89
|
+
}
|
package/src/rsc/types.ts
CHANGED
|
@@ -43,6 +43,10 @@ export interface RscPayload {
|
|
|
43
43
|
version?: string;
|
|
44
44
|
/** TTL in milliseconds for the client-side in-memory prefetch cache */
|
|
45
45
|
prefetchCacheTTL?: number;
|
|
46
|
+
/** Max entries in the client-side in-memory prefetch cache (FIFO eviction) */
|
|
47
|
+
prefetchCacheSize?: number;
|
|
48
|
+
/** Max concurrent speculative prefetch requests on the client */
|
|
49
|
+
prefetchConcurrency?: number;
|
|
46
50
|
/** Server-resolved rango state cookie name; the client reads it verbatim. */
|
|
47
51
|
stateCookieName?: string;
|
|
48
52
|
/** Theme configuration for FOUC prevention */
|
|
@@ -160,6 +164,32 @@ export interface SSRModule {
|
|
|
160
164
|
rscStream: ReadableStream<Uint8Array>,
|
|
161
165
|
options?: SSRRenderOptions,
|
|
162
166
|
) => Promise<ReadableStream<Uint8Array>>;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* PPR shell CAPTURE strategy (Axis 2). Prerenders the loader-masked shell over
|
|
170
|
+
* the Flight stream, aborts once quiescent, and returns the prelude bytes plus
|
|
171
|
+
* the postponed resume state — or null when the prelude degraded and must not
|
|
172
|
+
* be stored. Present only when the SSR virtual entry wires
|
|
173
|
+
* createShellCaptureHandler; the render layer feature-detects it. See
|
|
174
|
+
* docs/design/ppr-shell-resume.md.
|
|
175
|
+
*/
|
|
176
|
+
captureShellHTML?: (
|
|
177
|
+
rscStream: ReadableStream<Uint8Array>,
|
|
178
|
+
options: { quiesce: Promise<void>; maxWaitMs?: number },
|
|
179
|
+
) => Promise<{ prelude: Uint8Array; postponed: string | null } | null>;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* PPR shell RESUME strategy (Axis 2). Produces the per-request live portion of
|
|
183
|
+
* the document: resumes fizz over a fresh SsrRoot to emit only the postponed
|
|
184
|
+
* holes (or, for the DATA variant with postponed === null, just the fresh Flight
|
|
185
|
+
* payload scripts). The caller prepends the stored prelude bytes to form the
|
|
186
|
+
* composite response. Present only when the SSR virtual entry wires
|
|
187
|
+
* createShellResumeHandler; the render layer feature-detects it.
|
|
188
|
+
*/
|
|
189
|
+
resumeShellHTML?: (
|
|
190
|
+
rscStream: ReadableStream<Uint8Array>,
|
|
191
|
+
options: { postponed: string | null; nonce?: string },
|
|
192
|
+
) => Promise<ReadableStream<Uint8Array>>;
|
|
163
193
|
}
|
|
164
194
|
|
|
165
195
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ResolvedSegment } from "./types.js";
|
|
2
|
+
import { INTERNAL_RANGO_DEBUG } from "./internal-debug.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Cache of aggregate Promise.all results keyed on the first loader's
|
|
@@ -83,6 +84,23 @@ export function buildLoaderPromise(loaders: ResolvedSegment[]): Promise<any[]> {
|
|
|
83
84
|
if (loaders.length === 0) {
|
|
84
85
|
return Promise.resolve([]);
|
|
85
86
|
}
|
|
87
|
+
// Debug tap (browser only): log when each PENDING loader promise settles —
|
|
88
|
+
// i.e. when its data actually lands from the flight stream — independent of
|
|
89
|
+
// when the tree build awaits it. `.then(cb, cb)` observes on a branch, so
|
|
90
|
+
// rejections still propagate to the real consumers untouched.
|
|
91
|
+
if (INTERNAL_RANGO_DEBUG && IS_BROWSER) {
|
|
92
|
+
const tapStart = performance.now();
|
|
93
|
+
for (const loader of loaders) {
|
|
94
|
+
if (loader.loaderData instanceof Promise) {
|
|
95
|
+
const settle = (outcome: string) => () =>
|
|
96
|
+
console.log(
|
|
97
|
+
`[Browser][segments] loader ${loader.loaderId} ${outcome} @ ${Math.round(performance.now())}ms`,
|
|
98
|
+
{ msSinceRequested: Math.round(performance.now() - tapStart) },
|
|
99
|
+
);
|
|
100
|
+
loader.loaderData.then(settle("settled"), settle("rejected"));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
86
104
|
return Promise.all(
|
|
87
105
|
loaders.map((loader) =>
|
|
88
106
|
loader.loaderData instanceof Promise
|
package/src/segment-system.tsx
CHANGED
|
@@ -10,12 +10,29 @@ import {
|
|
|
10
10
|
LoaderBoundary,
|
|
11
11
|
} from "./route-content-wrapper.js";
|
|
12
12
|
import { RootErrorBoundary } from "./root-error-boundary.js";
|
|
13
|
+
import { INTERNAL_RANGO_DEBUG } from "./internal-debug.js";
|
|
13
14
|
import { getMemoizedContentPromise } from "./segment-content-promise.js";
|
|
14
15
|
import {
|
|
15
16
|
buildLoaderPromise,
|
|
16
17
|
getMemoizedLoaderPromise,
|
|
17
18
|
} from "./segment-loader-promise.js";
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Client-only debug log for the segment tree build. Gated on the baked flag
|
|
22
|
+
* AND `typeof window` (renderSegments also runs during SSR/RSC, which must
|
|
23
|
+
* stay silent). Timestamped so tree-build steps line up with the
|
|
24
|
+
* `[Browser][boot]` sequence around hydrateRoot.
|
|
25
|
+
*/
|
|
26
|
+
function segDebugLog(msg: string, details?: Record<string, unknown>): void {
|
|
27
|
+
if (!(INTERNAL_RANGO_DEBUG && typeof window === "object")) return;
|
|
28
|
+
const prefix = `[Browser][segments] ${msg} @ ${Math.round(performance.now())}ms`;
|
|
29
|
+
if (details) {
|
|
30
|
+
console.log(prefix, details);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
console.log(prefix);
|
|
34
|
+
}
|
|
35
|
+
|
|
19
36
|
// ViewTransition is only available in React experimental.
|
|
20
37
|
// Access via namespace import to avoid compile-time errors on stable React.
|
|
21
38
|
const ReactViewTransition: any =
|
|
@@ -211,6 +228,17 @@ export async function renderSegments(
|
|
|
211
228
|
rootLayout: RootLayout,
|
|
212
229
|
} = options || {};
|
|
213
230
|
|
|
231
|
+
const segDebug = INTERNAL_RANGO_DEBUG && typeof window === "object";
|
|
232
|
+
const segDebugStart = segDebug ? performance.now() : 0;
|
|
233
|
+
if (segDebug) {
|
|
234
|
+
segDebugLog("renderSegments start", {
|
|
235
|
+
segments: segments.map((s) => `${s.id}:${s.type}`),
|
|
236
|
+
isAction: !!isAction,
|
|
237
|
+
forceAwait: !!forceAwait,
|
|
238
|
+
intercepts: interceptSegments?.length ?? 0,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
214
242
|
const temporalLazyRefs: Promise<any>[] = [];
|
|
215
243
|
const normalizedSegments = restoreParallelLoaderMarkers(segments);
|
|
216
244
|
const normalizedInterceptSegments = interceptSegments
|
|
@@ -272,6 +300,15 @@ export async function renderSegments(
|
|
|
272
300
|
);
|
|
273
301
|
const { component, id, params, loading } = node.segment;
|
|
274
302
|
|
|
303
|
+
if (segDebug) {
|
|
304
|
+
segDebugLog(`segment ${id}`, {
|
|
305
|
+
type: node.segment.type,
|
|
306
|
+
loaders: node.loaders.map((l) => l.loaderId).filter(Boolean),
|
|
307
|
+
hasLoading: loading !== undefined && loading !== null,
|
|
308
|
+
parallel: node.parallel.map((p) => p.id),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
275
312
|
// Param-agnostic keys are opt-in via the transition() DSL (see
|
|
276
313
|
// inTransitionScope above). A route (and its route-owned layouts) inside a
|
|
277
314
|
// transition scope drops the param from its key, so navigating between two
|
|
@@ -316,14 +353,47 @@ export async function renderSegments(
|
|
|
316
353
|
resolvedComponent = await component;
|
|
317
354
|
}
|
|
318
355
|
|
|
319
|
-
let nodeContent: ReactNode =
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
356
|
+
let nodeContent: ReactNode = null;
|
|
357
|
+
if (isRenderableLoading(loading)) {
|
|
358
|
+
// forceAwait (popstate, stale-revalidation, fully-prefetched nav) renders a
|
|
359
|
+
// loading() route with the route content ALREADY resolved, so its
|
|
360
|
+
// RouteContentWrapper Suspender does not suspend for a microtask and flash
|
|
361
|
+
// the loading() fallback on a NORMAL (non-transition) commit. The router
|
|
362
|
+
// data is known-ready on these paths, so awaiting the content here is free.
|
|
363
|
+
// The wrapper tree is unchanged (RouteContentWrapper is still created with
|
|
364
|
+
// the same key/fallback) — only the `content` prop is a resolved node
|
|
365
|
+
// instead of a pending promise, which Suspender renders synchronously. This
|
|
366
|
+
// mirrors the forceAwait loaderData unwrap above; a CLIENT component that
|
|
367
|
+
// suspends on mount inside the content still reveals a fallback (it is not
|
|
368
|
+
// pre-resolved).
|
|
369
|
+
const contentPromise = getMemoizedContentPromise(resolvedComponent);
|
|
370
|
+
const loadingContent: Promise<ReactNode> | ReactNode = forceAwait
|
|
371
|
+
? await contentPromise
|
|
372
|
+
: contentPromise;
|
|
373
|
+
nodeContent = createElement(RouteContentWrapper, {
|
|
374
|
+
key: `suspense-loading-${id}`,
|
|
375
|
+
content: loadingContent,
|
|
376
|
+
fallback: loading,
|
|
377
|
+
segmentId: id,
|
|
378
|
+
});
|
|
379
|
+
} else {
|
|
380
|
+
// [VT-DIAG] Gated behind INTERNAL_RANGO_DEBUG. A segment in the no-loading()
|
|
381
|
+
// branch whose component decodes as a Promise/lazy gets registered into
|
|
382
|
+
// temporalLazyRefs and awaited before commit (see below) — which on builds
|
|
383
|
+
// where the segment component arrives deferred defeats client-nav streaming.
|
|
384
|
+
if (INTERNAL_RANGO_DEBUG && typeof window === "object") {
|
|
385
|
+
const c = resolvedComponent as unknown;
|
|
386
|
+
console.log("[VT-DIAG] renderSegments no-loading-branch segment", {
|
|
387
|
+
id,
|
|
388
|
+
type: node.segment.type,
|
|
389
|
+
componentIsPromise: c instanceof Promise,
|
|
390
|
+
componentIsLazy:
|
|
391
|
+
c != null && typeof c === "object" && "_payload" in c,
|
|
392
|
+
componentTypeof: typeof c,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
nodeContent = registerLazyRef(resolvedComponent);
|
|
396
|
+
}
|
|
327
397
|
|
|
328
398
|
// Wrap with <ViewTransition> if transition config exists (React experimental only).
|
|
329
399
|
// An empty config ({}) creates a bare <ViewTransition> boundary that participates
|
|
@@ -369,10 +439,25 @@ export async function renderSegments(
|
|
|
369
439
|
|
|
370
440
|
if (loading !== undefined && loading !== null) {
|
|
371
441
|
const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
|
|
442
|
+
let boundaryLoaderData: Promise<any[]> | any[] = loaderDataPromise;
|
|
443
|
+
if (forceAwait || isAction) {
|
|
444
|
+
const awaitStart = segDebug ? performance.now() : 0;
|
|
445
|
+
boundaryLoaderData = await loaderDataPromise;
|
|
446
|
+
if (segDebug) {
|
|
447
|
+
segDebugLog(`segment ${id}: loaders awaited (forceAwait/action)`, {
|
|
448
|
+
loaderIds,
|
|
449
|
+
ms: Math.round(performance.now() - awaitStart),
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
} else if (segDebug) {
|
|
453
|
+
segDebugLog(
|
|
454
|
+
`segment ${id}: streaming loaders via LoaderBoundary (suspense)`,
|
|
455
|
+
{ loaderIds },
|
|
456
|
+
);
|
|
457
|
+
}
|
|
372
458
|
content = createElement(LoaderBoundary, {
|
|
373
459
|
key: `loader-boundary-${key}`,
|
|
374
|
-
loaderDataPromise:
|
|
375
|
-
forceAwait || isAction ? await loaderDataPromise : loaderDataPromise,
|
|
460
|
+
loaderDataPromise: boundaryLoaderData,
|
|
376
461
|
loaderIds,
|
|
377
462
|
fallback: loading,
|
|
378
463
|
outletKey: key,
|
|
@@ -396,7 +481,17 @@ export async function renderSegments(
|
|
|
396
481
|
);
|
|
397
482
|
|
|
398
483
|
const layoutLoaderIds = layoutLoaders.map((l) => l.loaderId!);
|
|
484
|
+
// No loading() on this segment, so its loader data cannot stream behind
|
|
485
|
+
// a Suspense fallback — the tree build BLOCKS here until the data
|
|
486
|
+
// arrives. On the initial document this await runs before hydrateRoot.
|
|
487
|
+
const layoutAwaitStart = segDebug ? performance.now() : 0;
|
|
399
488
|
const resolvedData = await buildLoaderPromise(layoutLoaders);
|
|
489
|
+
if (segDebug) {
|
|
490
|
+
segDebugLog(`segment ${id}: layout loaders awaited (blocking)`, {
|
|
491
|
+
loaderIds: layoutLoaderIds,
|
|
492
|
+
ms: Math.round(performance.now() - layoutAwaitStart),
|
|
493
|
+
});
|
|
494
|
+
}
|
|
400
495
|
const { loaderData, errorFallback } = decodeLoaderResults(
|
|
401
496
|
resolvedData,
|
|
402
497
|
layoutLoaderIds,
|
|
@@ -429,10 +524,27 @@ export async function renderSegments(
|
|
|
429
524
|
|
|
430
525
|
p.loaderIds = ownedLoaders.map((l) => l.loaderId!);
|
|
431
526
|
const aggregated = getMemoizedLoaderPromise(ownedLoaders);
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
527
|
+
if ((forceAwait || isAction) && aggregated instanceof Promise) {
|
|
528
|
+
const parallelAwaitStart = segDebug ? performance.now() : 0;
|
|
529
|
+
p.loaderDataPromise = await aggregated;
|
|
530
|
+
if (segDebug) {
|
|
531
|
+
segDebugLog(
|
|
532
|
+
`segment ${id}: parallel ${p.id} loaders awaited (forceAwait/action)`,
|
|
533
|
+
{
|
|
534
|
+
loaderIds: p.loaderIds,
|
|
535
|
+
ms: Math.round(performance.now() - parallelAwaitStart),
|
|
536
|
+
},
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
} else {
|
|
540
|
+
p.loaderDataPromise = aggregated;
|
|
541
|
+
if (segDebug) {
|
|
542
|
+
segDebugLog(
|
|
543
|
+
`segment ${id}: parallel ${p.id} loaders streaming (suspense)`,
|
|
544
|
+
{ loaderIds: p.loaderIds },
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
436
548
|
}
|
|
437
549
|
}
|
|
438
550
|
|
|
@@ -462,7 +574,24 @@ export async function renderSegments(
|
|
|
462
574
|
children: content,
|
|
463
575
|
});
|
|
464
576
|
if (typeof window === "object") {
|
|
577
|
+
// [VT-DIAG] Gated behind INTERNAL_RANGO_DEBUG. If this await dominates the
|
|
578
|
+
// navigation time, a deferred/lazy segment component is being fully resolved
|
|
579
|
+
// before commit, which defeats client-nav streaming. The await itself is
|
|
580
|
+
// functional (it preloads lazy chunk refs); only the timing log is gated.
|
|
581
|
+
const vtDebug = INTERNAL_RANGO_DEBUG && temporalLazyRefs.length > 0;
|
|
582
|
+
const vtDebugStart = vtDebug ? performance.now() : 0;
|
|
583
|
+
if (vtDebug) {
|
|
584
|
+
console.log("[VT-DIAG] renderSegments awaiting temporalLazyRefs", {
|
|
585
|
+
count: temporalLazyRefs.length,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
465
588
|
await Promise.allSettled(temporalLazyRefs);
|
|
589
|
+
if (vtDebug) {
|
|
590
|
+
console.log("[VT-DIAG] renderSegments temporalLazyRefs settled", {
|
|
591
|
+
count: temporalLazyRefs.length,
|
|
592
|
+
ms: Math.round(performance.now() - vtDebugStart),
|
|
593
|
+
});
|
|
594
|
+
}
|
|
466
595
|
}
|
|
467
596
|
|
|
468
597
|
let result: ReactNode = errorBoundaryWrapped;
|
|
@@ -473,6 +602,12 @@ export async function renderSegments(
|
|
|
473
602
|
});
|
|
474
603
|
}
|
|
475
604
|
|
|
605
|
+
if (segDebug) {
|
|
606
|
+
segDebugLog("renderSegments complete", {
|
|
607
|
+
ms: Math.round(performance.now() - segDebugStart),
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
476
611
|
return result;
|
|
477
612
|
}
|
|
478
613
|
|
package/src/server/context.ts
CHANGED
|
@@ -150,6 +150,19 @@ export type InterceptWhenFn<TEnv = any> = (
|
|
|
150
150
|
ctx: InterceptSelectorContext<TEnv>,
|
|
151
151
|
) => boolean;
|
|
152
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Config object passed to intercept() (its 4th argument). `when` gates whether
|
|
155
|
+
* the intercept activates on a soft navigation — a single match-time selector or
|
|
156
|
+
* an array of them (ALL must return true; omit to always activate). This is the
|
|
157
|
+
* intercept counterpart to transition({ when }); both express conditional
|
|
158
|
+
* behavior as a config field rather than a separate DSL helper.
|
|
159
|
+
*
|
|
160
|
+
* @internal This type is an implementation detail and may change without notice.
|
|
161
|
+
*/
|
|
162
|
+
export interface InterceptConfig<TEnv = any> {
|
|
163
|
+
when?: InterceptWhenFn<TEnv> | InterceptWhenFn<TEnv>[];
|
|
164
|
+
}
|
|
165
|
+
|
|
153
166
|
/**
|
|
154
167
|
* Intercept entry stored in EntryData
|
|
155
168
|
* Contains the slot name, route to intercept, and handler
|
|
@@ -220,6 +233,13 @@ export type EntryData =
|
|
|
220
233
|
staticHandlerId?: string;
|
|
221
234
|
/** Response type for non-RSC routes (json, text, image, any) */
|
|
222
235
|
responseType?: string;
|
|
236
|
+
/**
|
|
237
|
+
* PPR (partial pre-rendering) opt-in from the path() `ppr` option. A
|
|
238
|
+
* document-level property of the page route: `true` uses the default
|
|
239
|
+
* shell policy, an object carries ttl/swr/tags. Read by the integrated
|
|
240
|
+
* PPR serve path (rsc/shell-serve.ts resolvePprConfig).
|
|
241
|
+
*/
|
|
242
|
+
ppr?: boolean | import("../urls/pattern-types.js").PartialPrerenderProps;
|
|
223
243
|
} & EntryPropCommon &
|
|
224
244
|
EntryPropDatas &
|
|
225
245
|
EntryPropSegments &
|
|
@@ -750,14 +770,23 @@ const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
|
|
|
750
770
|
|
|
751
771
|
// Purity-only scope: marks that a loader FUNCTION BODY is executing, regardless
|
|
752
772
|
// of how the loader was invoked (DSL via runInsideLoaderScope, or handler-
|
|
753
|
-
// invoked via ctx.use). Consulted
|
|
754
|
-
// request-scoped reads
|
|
755
|
-
//
|
|
756
|
-
//
|
|
773
|
+
// invoked via ctx.use). Consulted by isInsideCacheScope() to exempt
|
|
774
|
+
// request-scoped reads, by getCurrentLoaderBodyId() for guard-warning
|
|
775
|
+
// attribution, and by isInsideHandlerInvokedLoaderBody() for the
|
|
776
|
+
// consumption-lane rule (the shell-capture guard exemption). It deliberately
|
|
777
|
+
// does NOT affect isInsideLoaderScope(), so rendered()/barrier/deadlock
|
|
778
|
+
// gating (which must distinguish DSL from handler-invoked loaders) is
|
|
779
|
+
// unchanged.
|
|
757
780
|
const LOADER_BODY_SCOPE_KEY = Symbol.for("rangojs-router:loader-body-scope");
|
|
758
|
-
const loaderBodyScopeALS: AsyncLocalStorage<{
|
|
759
|
-
|
|
760
|
-
|
|
781
|
+
const loaderBodyScopeALS: AsyncLocalStorage<{
|
|
782
|
+
active: true;
|
|
783
|
+
loaderId?: string;
|
|
784
|
+
handlerInvoked?: boolean;
|
|
785
|
+
}> = ((globalThis as any)[LOADER_BODY_SCOPE_KEY] ??= new AsyncLocalStorage<{
|
|
786
|
+
active: true;
|
|
787
|
+
loaderId?: string;
|
|
788
|
+
handlerInvoked?: boolean;
|
|
789
|
+
}>());
|
|
761
790
|
|
|
762
791
|
/**
|
|
763
792
|
* Check if the current execution is inside a cache() DSL boundary.
|
|
@@ -802,8 +831,37 @@ export function runInsideLoaderScope<T>(fn: () => T): T {
|
|
|
802
831
|
* and handler-invoked via ctx.use) so request-scoped reads inside a loader
|
|
803
832
|
* never trip the cache-scope guards — loaders always run fresh.
|
|
804
833
|
*/
|
|
805
|
-
export function runInsideLoaderBodyScope<T>(
|
|
806
|
-
|
|
834
|
+
export function runInsideLoaderBodyScope<T>(
|
|
835
|
+
fn: () => T,
|
|
836
|
+
loaderId?: string,
|
|
837
|
+
handlerInvoked?: boolean,
|
|
838
|
+
): T {
|
|
839
|
+
return loaderBodyScopeALS.run({ active: true, loaderId, handlerInvoked }, fn);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* The $$id of the loader whose body is currently executing, or undefined
|
|
844
|
+
* outside any loader body. Used by the shell-capture identity guard
|
|
845
|
+
* (cookie-store.ts) so its refusal warning can name the loader that read
|
|
846
|
+
* cookies()/headers() instead of blaming a lane it cannot see — the old
|
|
847
|
+
* hardcoded "bake-lane loader" text misled a live-lane debugging session
|
|
848
|
+
* (issue #672, secondary).
|
|
849
|
+
*/
|
|
850
|
+
export function getCurrentLoaderBodyId(): string | undefined {
|
|
851
|
+
return loaderBodyScopeALS.getStore()?.loaderId;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* True while a HANDLER-invoked loader body (`await ctx.use(Loader)` from a
|
|
856
|
+
* handler, not the DSL segment funnel) is executing. The consumption-lane
|
|
857
|
+
* rule keys off this: handler consumption yields a BAKED copy in every shared
|
|
858
|
+
* artifact — cache(), "use cache", and the PPR shell — so the shell-capture
|
|
859
|
+
* identity guard (cookie-store.ts) permits cookies()/headers() here, exactly
|
|
860
|
+
* like the cache-purity guards do. DSL segment loaders (live lane masked at
|
|
861
|
+
* capture, bake lane guarded) never set the flag.
|
|
862
|
+
*/
|
|
863
|
+
export function isInsideHandlerInvokedLoaderBody(): boolean {
|
|
864
|
+
return loaderBodyScopeALS.getStore()?.handlerInvoked === true;
|
|
807
865
|
}
|
|
808
866
|
|
|
809
867
|
// Scope for handle PUSH CALLBACKS (push(() => ...), including async ones).
|