@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,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the client-side prefetch limits — the in-memory prefetch cache size
|
|
3
|
+
* and the speculative-prefetch queue concurrency — once, at router init. Both
|
|
4
|
+
* are positive-integer counts shipped to the browser in payload metadata; the
|
|
5
|
+
* browser entry feeds them to the prefetch cache (`initPrefetchCache`) and the
|
|
6
|
+
* prefetch queue (`setPrefetchConcurrency`) at startup.
|
|
7
|
+
*
|
|
8
|
+
* Policy note: this is a SEPARATE policy from resolvePrefetchCacheTTL
|
|
9
|
+
* (prefetch-cache-ttl.ts). These are counts, not durations, so the disable
|
|
10
|
+
* paths differ: a TTL of `false`/0 intentionally turns prefetching off, but a
|
|
11
|
+
* cache size or concurrency of 0 would silently break prefetching while leaving
|
|
12
|
+
* it nominally enabled — never the intent of passing a number. So any value
|
|
13
|
+
* that is not a finite integer >= 1 (0, negative, fractional below 1, NaN,
|
|
14
|
+
* Infinity, undefined) falls back to the default. Finite values >= 1 are
|
|
15
|
+
* floored. Do not unify this guard with the TTL guard.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_PREFETCH_CACHE_SIZE = 100;
|
|
19
|
+
export const DEFAULT_PREFETCH_CONCURRENCY = 2;
|
|
20
|
+
|
|
21
|
+
export function resolvePrefetchCacheSize(raw: number | undefined): number {
|
|
22
|
+
return resolvePositiveCount(raw, DEFAULT_PREFETCH_CACHE_SIZE);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolvePrefetchConcurrency(raw: number | undefined): number {
|
|
26
|
+
return resolvePositiveCount(raw, DEFAULT_PREFETCH_CONCURRENCY);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolvePositiveCount(
|
|
30
|
+
raw: number | undefined,
|
|
31
|
+
fallback: number,
|
|
32
|
+
): number {
|
|
33
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw >= 1) {
|
|
34
|
+
return Math.floor(raw);
|
|
35
|
+
}
|
|
36
|
+
return fallback;
|
|
37
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import { createHandleStore } from "../server/handle-store.js";
|
|
3
|
+
import { resolveSegmentHandleValues } from "../handles/deferred-resolution.js";
|
|
3
4
|
import { getRequestContext } from "../server/request-context.js";
|
|
4
5
|
import {
|
|
5
6
|
runWithRequestContext,
|
|
@@ -32,7 +33,9 @@ import type {
|
|
|
32
33
|
import type { RouteMatchResult } from "./pattern-matching.js";
|
|
33
34
|
|
|
34
35
|
export interface PrerenderMatchDeps<TEnv = any> {
|
|
35
|
-
findMatch: (
|
|
36
|
+
findMatch: (
|
|
37
|
+
pathname: string,
|
|
38
|
+
) => RouteMatchResult<TEnv> | null | Promise<RouteMatchResult<TEnv> | null>;
|
|
36
39
|
buildRouterContext: () => RouterContext<TEnv>;
|
|
37
40
|
mergedRouteMap: Record<string, string>;
|
|
38
41
|
resolveAllSegments: (
|
|
@@ -73,7 +76,7 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
73
76
|
passthrough?: true;
|
|
74
77
|
} | null> {
|
|
75
78
|
// 1. Find the matching route entry
|
|
76
|
-
const matched = deps.findMatch(pathname);
|
|
79
|
+
const matched = await deps.findMatch(pathname);
|
|
77
80
|
if (!matched) return null;
|
|
78
81
|
|
|
79
82
|
// Use params from trie match if available, fall back to provided params
|
|
@@ -293,7 +296,9 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
293
296
|
for (const seg of nonLoaderSegments) {
|
|
294
297
|
const segHandles = handleStore.getDataForSegment(seg.id);
|
|
295
298
|
if (Object.keys(segHandles).length > 0) {
|
|
296
|
-
|
|
299
|
+
// Resolve deferred values before encoding so the baked artifact holds
|
|
300
|
+
// resolved data (prerender = build-time cache).
|
|
301
|
+
handlesRecord[seg.id] = await resolveSegmentHandleValues(segHandles);
|
|
297
302
|
}
|
|
298
303
|
}
|
|
299
304
|
const handles = await encodeHandles(handlesRecord);
|
|
@@ -394,7 +399,8 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
394
399
|
for (const seg of interceptResolvedSegments) {
|
|
395
400
|
const segHandles = handleStore.getDataForSegment(seg.id);
|
|
396
401
|
if (Object.keys(segHandles).length > 0) {
|
|
397
|
-
interceptHandlesRecord[seg.id] =
|
|
402
|
+
interceptHandlesRecord[seg.id] =
|
|
403
|
+
await resolveSegmentHandleValues(segHandles);
|
|
398
404
|
}
|
|
399
405
|
}
|
|
400
406
|
// The intercept artifact serves main + intercept segments together, so
|
|
@@ -533,7 +539,7 @@ export async function renderStaticSegment<TEnv = any>(
|
|
|
533
539
|
const segHandles = handleStore.getDataForSegment(handlerId);
|
|
534
540
|
const handles =
|
|
535
541
|
Object.keys(segHandles).length > 0
|
|
536
|
-
? await encodeHandleValue(segHandles)
|
|
542
|
+
? await encodeHandleValue(await resolveSegmentHandleValues(segHandles))
|
|
537
543
|
: "";
|
|
538
544
|
|
|
539
545
|
return { encoded: serialized.encoded, handles };
|
|
@@ -6,7 +6,9 @@ import type { MiddlewareFn } from "./middleware.js";
|
|
|
6
6
|
import { resolveRoute } from "./route-snapshot.js";
|
|
7
7
|
|
|
8
8
|
export interface PreviewMatchDeps<TEnv = any> {
|
|
9
|
-
findMatch: (
|
|
9
|
+
findMatch: (
|
|
10
|
+
pathname: string,
|
|
11
|
+
) => RouteMatchResult<TEnv> | null | Promise<RouteMatchResult<TEnv> | null>;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
/**
|
|
@@ -121,7 +121,9 @@ export type {
|
|
|
121
121
|
};
|
|
122
122
|
|
|
123
123
|
export interface ClassifyRequestDeps<TEnv = any> {
|
|
124
|
-
findMatch: (
|
|
124
|
+
findMatch: (
|
|
125
|
+
pathname: string,
|
|
126
|
+
) => RouteMatchResult<TEnv> | null | Promise<RouteMatchResult<TEnv> | null>;
|
|
125
127
|
routerVersion: string;
|
|
126
128
|
routerId: string;
|
|
127
129
|
}
|
|
@@ -148,6 +150,23 @@ export async function classifyRequest<TEnv = any>(
|
|
|
148
150
|
const pathname = url.pathname;
|
|
149
151
|
const isAction =
|
|
150
152
|
request.headers.has("rsc-action") || url.searchParams.has("_rsc_action");
|
|
153
|
+
const isLoaderFetch = url.searchParams.has("_rsc_loader");
|
|
154
|
+
const isPartialReq = url.searchParams.has("_rsc_partial");
|
|
155
|
+
const contentType = request.headers.get("content-type") || "";
|
|
156
|
+
const isFormSubmission =
|
|
157
|
+
contentType.includes("multipart/form-data") ||
|
|
158
|
+
contentType.includes("application/x-www-form-urlencoded");
|
|
159
|
+
const isPeRender = request.method === "POST" && !isAction && isFormSubmission;
|
|
160
|
+
|
|
161
|
+
// A plain document request (full-render mode) is the only mode that renders
|
|
162
|
+
// through match() with isSSR:true; every other mode (action, loader, PE,
|
|
163
|
+
// partial) renders — if at all — through the non-SSR matchPartial() path.
|
|
164
|
+
// Resolve the manifest under the flag the eventual render will use so
|
|
165
|
+
// createMatchContextForFull can reuse this snapshot instead of re-resolving.
|
|
166
|
+
// Response routes ignore isSSR entirely (they short-circuit before match),
|
|
167
|
+
// so the flag is moot for them. Mode detection below reuses these booleans.
|
|
168
|
+
const willFullRender =
|
|
169
|
+
!isAction && !isLoaderFetch && !isPartialReq && !isPeRender;
|
|
151
170
|
|
|
152
171
|
const clientVersion = url.searchParams.get("_rsc_v");
|
|
153
172
|
if (
|
|
@@ -189,6 +208,7 @@ export async function classifyRequest<TEnv = any>(
|
|
|
189
208
|
const result = await resolveRoute<TEnv>(pathname, {
|
|
190
209
|
findMatch: deps.findMatch,
|
|
191
210
|
lite: true,
|
|
211
|
+
isSSR: willFullRender,
|
|
192
212
|
});
|
|
193
213
|
|
|
194
214
|
if (!result) {
|
|
@@ -229,7 +249,6 @@ export async function classifyRequest<TEnv = any>(
|
|
|
229
249
|
|
|
230
250
|
const actionId =
|
|
231
251
|
request.headers.get("rsc-action") || url.searchParams.get("_rsc_action");
|
|
232
|
-
const isLoaderFetch = url.searchParams.has("_rsc_loader");
|
|
233
252
|
|
|
234
253
|
const hasVariants =
|
|
235
254
|
snapshot.matched.negotiateVariants &&
|
|
@@ -244,15 +263,11 @@ export async function classifyRequest<TEnv = any>(
|
|
|
244
263
|
return { mode: "loader", route: snapshot };
|
|
245
264
|
}
|
|
246
265
|
|
|
247
|
-
|
|
248
|
-
const isFormSubmission =
|
|
249
|
-
contentType.includes("multipart/form-data") ||
|
|
250
|
-
contentType.includes("application/x-www-form-urlencoded");
|
|
251
|
-
if (request.method === "POST" && !isAction && isFormSubmission) {
|
|
266
|
+
if (isPeRender) {
|
|
252
267
|
return { mode: "pe-render", route: snapshot };
|
|
253
268
|
}
|
|
254
269
|
|
|
255
|
-
if (
|
|
270
|
+
if (isPartialReq) {
|
|
256
271
|
return { mode: "partial-render", route: snapshot, negotiated };
|
|
257
272
|
}
|
|
258
273
|
|
|
@@ -48,6 +48,15 @@ export interface RouteSnapshot<TEnv = any> {
|
|
|
48
48
|
isPassthrough: boolean;
|
|
49
49
|
/** Response type for non-RSC routes (e.g. "application/json") */
|
|
50
50
|
responseType?: string;
|
|
51
|
+
/**
|
|
52
|
+
* The isSSR flag the manifest was resolved with. Recorded so consumers can
|
|
53
|
+
* decide whether the snapshot is reusable: a snapshot resolved with isSSR
|
|
54
|
+
* differs (loading({ ssr: false }) entries) and is cached under a different
|
|
55
|
+
* manifest partition, so the full (document) path only reuses an isSSR:true
|
|
56
|
+
* snapshot and the partial path only reuses a non-isSSR one. Undefined on
|
|
57
|
+
* inline snapshots (redirect / not-found) that are never reused.
|
|
58
|
+
*/
|
|
59
|
+
isSSR?: boolean;
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
export type ResolveRouteResult<TEnv = any> =
|
|
@@ -56,7 +65,9 @@ export type ResolveRouteResult<TEnv = any> =
|
|
|
56
65
|
| null;
|
|
57
66
|
|
|
58
67
|
export interface ResolveRouteDeps<TEnv = any> {
|
|
59
|
-
findMatch: (
|
|
68
|
+
findMatch: (
|
|
69
|
+
pathname: string,
|
|
70
|
+
) => RouteMatchResult<TEnv> | null | Promise<RouteMatchResult<TEnv> | null>;
|
|
60
71
|
metricsStore?: MetricsStore;
|
|
61
72
|
isSSR?: boolean;
|
|
62
73
|
/**
|
|
@@ -105,7 +116,7 @@ export async function resolveRoute<TEnv = any>(
|
|
|
105
116
|
|
|
106
117
|
const routeMatchStart =
|
|
107
118
|
metricsStore && !skipRouteMatchMetric ? performance.now() : 0;
|
|
108
|
-
const matched = deps.findMatch(pathname);
|
|
119
|
+
const matched = await deps.findMatch(pathname);
|
|
109
120
|
if (metricsStore && !skipRouteMatchMetric) {
|
|
110
121
|
metricsStore.metrics.push({
|
|
111
122
|
label: "route-matching",
|
|
@@ -175,6 +186,7 @@ export async function resolveRoute<TEnv = any>(
|
|
|
175
186
|
cacheScope,
|
|
176
187
|
isPassthrough,
|
|
177
188
|
responseType,
|
|
189
|
+
isSSR,
|
|
178
190
|
},
|
|
179
191
|
};
|
|
180
192
|
}
|
|
@@ -55,7 +55,9 @@ export interface InterceptResult {
|
|
|
55
55
|
* Instead of passing 20+ parameters, middleware calls getRouterContext() to access them.
|
|
56
56
|
*/
|
|
57
57
|
export interface RouterContext<TEnv = any> {
|
|
58
|
-
findMatch: (
|
|
58
|
+
findMatch: (
|
|
59
|
+
pathname: string,
|
|
60
|
+
) => RouteMatchResult | null | Promise<RouteMatchResult | null>;
|
|
59
61
|
|
|
60
62
|
loadManifest: (
|
|
61
63
|
entry: any,
|
|
@@ -9,12 +9,14 @@ import type { NonceProvider } from "../rsc/types.js";
|
|
|
9
9
|
import type { ExecutionContext } from "../server/request-context.js";
|
|
10
10
|
import type { SerializedSegmentData } from "../cache/types.js";
|
|
11
11
|
import type { MiddlewareEntry, MiddlewareFn } from "./middleware.js";
|
|
12
|
+
import type { RouteMatchResult } from "./pattern-matching.js";
|
|
12
13
|
import type { ExtractParams } from "../types/route-config.js";
|
|
13
14
|
import { RSC_ROUTER_BRAND } from "./router-registry.js";
|
|
14
15
|
import type { RangoOptions, RootLayoutProps } from "./router-options.js";
|
|
15
16
|
import type { DefaultVars } from "../types/global-namespace.js";
|
|
16
17
|
import type { ResolvedTimeouts, OnTimeoutCallback } from "./timeout.js";
|
|
17
18
|
import type { ResolvedTracing } from "./tracing.js";
|
|
19
|
+
import type { TelemetrySink } from "./telemetry.js";
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Options passed to router.fetch(), router.match(), and other request entrypoints.
|
|
@@ -301,6 +303,20 @@ export interface RangoInternal<
|
|
|
301
303
|
*/
|
|
302
304
|
readonly prefetchCacheTTL: number;
|
|
303
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Maximum number of decoded prefetch payloads the client keeps in its
|
|
308
|
+
* in-memory prefetch cache (FIFO eviction at capacity). Shipped to the
|
|
309
|
+
* client in payload metadata. Derived from prefetchCacheSize.
|
|
310
|
+
*/
|
|
311
|
+
readonly prefetchCacheSize: number;
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Maximum number of speculative prefetch requests the client runs
|
|
315
|
+
* concurrently. Shipped to the client in payload metadata. Derived from
|
|
316
|
+
* prefetchConcurrency.
|
|
317
|
+
*/
|
|
318
|
+
readonly prefetchConcurrency: number;
|
|
319
|
+
|
|
304
320
|
/**
|
|
305
321
|
* Resolved rango state cookie name (`{prefix}_{routerId}`), composed once at
|
|
306
322
|
* router init and shipped to the client in payload metadata. The server-side
|
|
@@ -335,6 +351,14 @@ export interface RangoInternal<
|
|
|
335
351
|
*/
|
|
336
352
|
readonly tracing?: ResolvedTracing;
|
|
337
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Raw telemetry sink from RangoOptions, exposed so handler-level emitters
|
|
356
|
+
* (rsc/handler.ts timeout/origin/late-handle) can emit WITHOUT the
|
|
357
|
+
* RouterContext ALS, which only match()/matchPartial() enter. See
|
|
358
|
+
* observeEvent's emitter list in router/instrument.ts.
|
|
359
|
+
*/
|
|
360
|
+
readonly telemetry?: TelemetrySink;
|
|
361
|
+
|
|
338
362
|
/**
|
|
339
363
|
* Whether ?__debug_manifest is allowed in production.
|
|
340
364
|
* Always enabled in development.
|
|
@@ -502,7 +526,14 @@ export interface RangoInternal<
|
|
|
502
526
|
* Used by classifyRequest() for request classification without
|
|
503
527
|
* entering the full match pipeline.
|
|
504
528
|
*/
|
|
505
|
-
|
|
529
|
+
// Async since a lazy async include (`() => import()`) must resolve before its
|
|
530
|
+
// routes can match. Typed (not `any`) so a consumer doing
|
|
531
|
+
// `const m = router.findMatch(p); if (!m) ...; m.entry` gets a compile error
|
|
532
|
+
// (m is a Promise) instead of the silent always-truthy bug.
|
|
533
|
+
findMatch(
|
|
534
|
+
pathname: string,
|
|
535
|
+
metricsStore?: any,
|
|
536
|
+
): Promise<RouteMatchResult<TEnv> | null>;
|
|
506
537
|
|
|
507
538
|
/**
|
|
508
539
|
* Debug utility to serialize the manifest for inspection
|
|
@@ -497,6 +497,36 @@ export interface RangoOptions<TEnv = any> {
|
|
|
497
497
|
*/
|
|
498
498
|
prefetchCacheTTL?: number | false;
|
|
499
499
|
|
|
500
|
+
/**
|
|
501
|
+
* Maximum number of decoded prefetch payloads the client keeps in its
|
|
502
|
+
* in-memory prefetch cache. When the cache is full the oldest entry is
|
|
503
|
+
* evicted (FIFO) to make room for a new prefetch.
|
|
504
|
+
*
|
|
505
|
+
* Each entry retains a fully decoded RSC payload (and the route's client
|
|
506
|
+
* chunks pulled in while decoding), so this is the lever on client-side
|
|
507
|
+
* prefetch memory: a higher value warms more routes at the cost of more
|
|
508
|
+
* retained payloads. Staleness is bounded separately by `prefetchCacheTTL`;
|
|
509
|
+
* this bounds the entry COUNT.
|
|
510
|
+
*
|
|
511
|
+
* Values below 1 (or non-finite) fall back to the default. To turn
|
|
512
|
+
* prefetching off entirely, set `prefetchCacheTTL: false` instead.
|
|
513
|
+
*
|
|
514
|
+
* @default 100
|
|
515
|
+
*/
|
|
516
|
+
prefetchCacheSize?: number;
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Maximum number of speculative prefetch requests (viewport/render strategy)
|
|
520
|
+
* the client runs concurrently. Hover prefetches bypass this queue and fire
|
|
521
|
+
* immediately; this caps only the background, idle-gated queue so prefetches
|
|
522
|
+
* never saturate the browser's connection pool.
|
|
523
|
+
*
|
|
524
|
+
* Values below 1 (or non-finite) fall back to the default.
|
|
525
|
+
*
|
|
526
|
+
* @default 2
|
|
527
|
+
*/
|
|
528
|
+
prefetchConcurrency?: number;
|
|
529
|
+
|
|
500
530
|
/**
|
|
501
531
|
* Prefix for the rango state cookie name. The resolved name is
|
|
502
532
|
* `{prefix}_{routerId}`; the prefix is sanitized to cookie-name-safe
|
|
@@ -19,6 +19,10 @@ import type {
|
|
|
19
19
|
} from "../../types";
|
|
20
20
|
import type { SegmentResolutionDeps } from "../types.js";
|
|
21
21
|
import { resolveLoaderData } from "./loader-cache.js";
|
|
22
|
+
import {
|
|
23
|
+
isShellCaptureActive,
|
|
24
|
+
entryLoadingMasksLoaders,
|
|
25
|
+
} from "./loader-mask.js";
|
|
22
26
|
import {
|
|
23
27
|
handleHandlerResult,
|
|
24
28
|
tryStaticHandler,
|
|
@@ -60,6 +64,24 @@ export async function resolveLoaders<TEnv>(
|
|
|
60
64
|
const hasLoading = "loading" in entry && entry.loading !== undefined;
|
|
61
65
|
const loadingDisabled = hasLoading && entry.loading === false;
|
|
62
66
|
|
|
67
|
+
// Emit the streaming (non-awaiting) loader shape when loading is enabled OR
|
|
68
|
+
// during a PPR shell capture. In capture, LIVE-lane loaders are masked with
|
|
69
|
+
// never-resolving promises (loader-mask.ts); the loading-disabled branch below
|
|
70
|
+
// AWAITS the loader promises, which would hang the capture render's match()
|
|
71
|
+
// forever on those masked promises. Forcing the streaming shape lets match()
|
|
72
|
+
// complete so the prerender can postpone the loader subtrees as holes. The
|
|
73
|
+
// `!loadingDisabled` short-circuit keeps the ALS check off the hot path (only
|
|
74
|
+
// loading-disabled entries consult it), so normal requests are unchanged.
|
|
75
|
+
const emitStreaming = !loadingDisabled || isShellCaptureActive();
|
|
76
|
+
|
|
77
|
+
// PPR lane decision for this entry's loaders (loader-container-bake): an
|
|
78
|
+
// entry WITHOUT renderable loading() puts its loaders on the BAKE lane —
|
|
79
|
+
// executed at capture (container bakes, nested pending promises hole at the
|
|
80
|
+
// consumer's Suspense) and overlay-pinned from the shell snapshot on a HIT.
|
|
81
|
+
// Renderable loading() keeps the LIVE lane (masked at capture, always
|
|
82
|
+
// fresh). Computed per entry; resolveLoaderData applies the policy.
|
|
83
|
+
const bakeLane = !entryLoadingMasksLoaders(entry.loading);
|
|
84
|
+
|
|
63
85
|
// Error context for wrapLoaderPromise: without it, a throwing DSL loader never
|
|
64
86
|
// fires createRouter({ onError }) (phase "loader") nor emits the loader.error
|
|
65
87
|
// telemetry event — wrapLoaderPromise only builds the onError/telemetry path
|
|
@@ -67,7 +89,7 @@ export async function resolveLoaders<TEnv>(
|
|
|
67
89
|
// loader failures the same way handlers/actions/routing/fetchable-loaders do.
|
|
68
90
|
const errorContext = buildLoaderErrorContext(ctx);
|
|
69
91
|
|
|
70
|
-
if (
|
|
92
|
+
if (emitStreaming) {
|
|
71
93
|
// Streaming loaders: promises kick off now, settle during RSC serialization.
|
|
72
94
|
const segments = loaderEntries.map((loaderEntry, i) => {
|
|
73
95
|
const { loader } = loaderEntry;
|
|
@@ -82,7 +104,12 @@ export async function resolveLoaders<TEnv>(
|
|
|
82
104
|
loaderId: loader.$$id,
|
|
83
105
|
loaderData: deps.wrapLoaderPromise(
|
|
84
106
|
runInsideLoaderScope(() =>
|
|
85
|
-
resolveLoaderData(
|
|
107
|
+
resolveLoaderData(
|
|
108
|
+
loaderEntry,
|
|
109
|
+
ctx,
|
|
110
|
+
ctx.pathname,
|
|
111
|
+
bakeLane ? segmentId : null,
|
|
112
|
+
),
|
|
86
113
|
),
|
|
87
114
|
entry,
|
|
88
115
|
segmentId,
|
|
@@ -111,7 +138,12 @@ export async function resolveLoaders<TEnv>(
|
|
|
111
138
|
const segmentId = `${shortCode}D${i}.${loader.$$id}`;
|
|
112
139
|
const wrapped = deps.wrapLoaderPromise(
|
|
113
140
|
runInsideLoaderScope(() =>
|
|
114
|
-
resolveLoaderData(
|
|
141
|
+
resolveLoaderData(
|
|
142
|
+
loaderEntry,
|
|
143
|
+
ctx,
|
|
144
|
+
ctx.pathname,
|
|
145
|
+
bakeLane ? segmentId : null,
|
|
146
|
+
),
|
|
115
147
|
),
|
|
116
148
|
entry,
|
|
117
149
|
segmentId,
|
|
@@ -202,6 +234,7 @@ export async function resolveSegment<TEnv>(
|
|
|
202
234
|
transition: applyViewTransitionDefault(
|
|
203
235
|
entry.transition,
|
|
204
236
|
deps.viewTransitionDefault,
|
|
237
|
+
entry.shortCode,
|
|
205
238
|
),
|
|
206
239
|
params,
|
|
207
240
|
belongsToRoute: false,
|
|
@@ -345,6 +378,7 @@ export async function resolveSegment<TEnv>(
|
|
|
345
378
|
transition: applyViewTransitionDefault(
|
|
346
379
|
entry.transition,
|
|
347
380
|
deps.viewTransitionDefault,
|
|
381
|
+
entry.shortCode,
|
|
348
382
|
),
|
|
349
383
|
params,
|
|
350
384
|
belongsToRoute: true,
|
|
@@ -432,6 +466,7 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
432
466
|
transition: applyViewTransitionDefault(
|
|
433
467
|
orphan.transition,
|
|
434
468
|
deps.viewTransitionDefault,
|
|
469
|
+
orphan.shortCode,
|
|
435
470
|
),
|
|
436
471
|
...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
|
|
437
472
|
});
|
|
@@ -565,6 +600,7 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
565
600
|
transition: applyViewTransitionDefault(
|
|
566
601
|
parallelEntry.transition,
|
|
567
602
|
deps.viewTransitionDefault,
|
|
603
|
+
`${parentShortCode}.${slot}`,
|
|
568
604
|
),
|
|
569
605
|
params,
|
|
570
606
|
slot,
|
|
@@ -21,7 +21,11 @@
|
|
|
21
21
|
import type { LoaderEntry } from "../../server/context.js";
|
|
22
22
|
import type { HandlerContext, InternalHandlerContext } from "../../types.js";
|
|
23
23
|
import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
getRequestContext,
|
|
26
|
+
_getRequestContext,
|
|
27
|
+
runWithRequestContext,
|
|
28
|
+
} from "../../server/request-context.js";
|
|
25
29
|
import { sortedRouteParams } from "../../cache/cache-key-utils.js";
|
|
26
30
|
import {
|
|
27
31
|
resolveTtl,
|
|
@@ -32,7 +36,15 @@ import {
|
|
|
32
36
|
DEFAULT_ROUTE_TTL,
|
|
33
37
|
} from "../../cache/cache-policy.js";
|
|
34
38
|
import { readThroughItem } from "../../cache/read-through-swr.js";
|
|
39
|
+
import {
|
|
40
|
+
maskNestedContainerThenables,
|
|
41
|
+
overlayLoaderContainer,
|
|
42
|
+
} from "./loader-snapshot.js";
|
|
35
43
|
import { recordRequestTags } from "../../cache/cache-tag.js";
|
|
44
|
+
import {
|
|
45
|
+
isShellCaptureActive,
|
|
46
|
+
createMaskedLoaderPromise,
|
|
47
|
+
} from "./loader-mask.js";
|
|
36
48
|
// Lazy-loaded to avoid pulling @vitejs/plugin-rsc/rsc into modules that
|
|
37
49
|
// import segment-resolution but never use loader caching.
|
|
38
50
|
let _serializeResult: typeof import("../../cache/segment-codec.js").serializeResult;
|
|
@@ -123,6 +135,76 @@ export function resolveLoaderData<TEnv>(
|
|
|
123
135
|
loaderEntry: LoaderEntry,
|
|
124
136
|
ctx: HandlerContext<any, TEnv>,
|
|
125
137
|
pathname: string,
|
|
138
|
+
bakeSegmentKey?: string | null,
|
|
139
|
+
): Promise<any> {
|
|
140
|
+
// One ALS read serves the capture check, the record registration, and the
|
|
141
|
+
// seed lookup — this runs for every loader on every request.
|
|
142
|
+
const reqCtx = _getRequestContext();
|
|
143
|
+
// PPR shell capture policy — gated here, the single funnel every loader
|
|
144
|
+
// segment path routes through (fresh resolveLoaders, cache-hit
|
|
145
|
+
// resolveLoadersOnly, revalidation resolveLoadersOnlyWithRevalidation).
|
|
146
|
+
//
|
|
147
|
+
// Two lanes (docs/design/loader-container-bake.md):
|
|
148
|
+
// - LIVE lane (entry has renderable loading(); no `bakeSegmentKey`): never
|
|
149
|
+
// execute during capture. The slot gets a never-resolving promise so the
|
|
150
|
+
// LoaderBoundary postpones (a hole). See loader-mask.ts.
|
|
151
|
+
// - BAKE lane (no renderable loading(); callers pass `bakeSegmentKey`):
|
|
152
|
+
// execute during capture exactly like axis 1 — the settled container bakes
|
|
153
|
+
// into the prelude, nested pending promises postpone at the consumer's own
|
|
154
|
+
// Suspense. The container promise is registered on the derived context so
|
|
155
|
+
// captureAndStoreShell pins it into the snapshot's loader family; on a
|
|
156
|
+
// shell HIT the recorded container is overlaid onto the fresh run so the
|
|
157
|
+
// payload matches the frozen prelude byte-for-byte.
|
|
158
|
+
if (isShellCaptureActive(reqCtx)) {
|
|
159
|
+
if (!bakeSegmentKey) {
|
|
160
|
+
return createMaskedLoaderPromise();
|
|
161
|
+
}
|
|
162
|
+
const containerPromise = executeLoaderData(loaderEntry, ctx, pathname);
|
|
163
|
+
// Pre-attach a no-op catch: a bake-lane rejection during capture must
|
|
164
|
+
// surface through the drain's refusal (and the wrapper's error boundary),
|
|
165
|
+
// never as an unhandled rejection that can kill the worker before the
|
|
166
|
+
// drain probes this record.
|
|
167
|
+
containerPromise.catch(() => {});
|
|
168
|
+
// Nested-promise SHAPE is the liveness declaration: mask nested thenables
|
|
169
|
+
// in the capture's copy of the container so the consuming subtree
|
|
170
|
+
// postpones as a hole no matter when the promise settles, elide records a
|
|
171
|
+
// HOLE marker, and every HIT streams the fresh value. Without this, a
|
|
172
|
+
// nested promise that settled before the quiet window baked its value into
|
|
173
|
+
// the SHARED shell and the snapshot pinned it for every visitor
|
|
174
|
+
// (per-request basket data served cross-session, found live). The raw
|
|
175
|
+
// container is untouched: handler-side ctx.use consumption (the
|
|
176
|
+
// consumption-lane rule, semantic-matrix PPR3) keeps real values.
|
|
177
|
+
const maskedPromise = containerPromise.then((container: unknown) =>
|
|
178
|
+
maskNestedContainerThenables(container),
|
|
179
|
+
);
|
|
180
|
+
maskedPromise.catch(() => {});
|
|
181
|
+
reqCtx?._shellCaptureLoaderRecords?.set(bakeSegmentKey, maskedPromise);
|
|
182
|
+
return maskedPromise;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (bakeSegmentKey) {
|
|
186
|
+
const seed = reqCtx?._shellLoaderSeed;
|
|
187
|
+
if (seed && seed.has(bakeSegmentKey)) {
|
|
188
|
+
const recorded = seed.get(bakeSegmentKey);
|
|
189
|
+
// HIT tail: run fresh (only the loader body can mint the live nested
|
|
190
|
+
// promises), then pin the recorded paths over it. A fresh REJECTION here
|
|
191
|
+
// skips the overlay and flows to the per-loader error boundary — the
|
|
192
|
+
// payload then diverges from the prelude (same residual class as uncached
|
|
193
|
+
// nondeterminism in shell material).
|
|
194
|
+
return executeLoaderData(loaderEntry, ctx, pathname).then(
|
|
195
|
+
(fresh: unknown) => overlayLoaderContainer(fresh, recorded),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return executeLoaderData(loaderEntry, ctx, pathname);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The pre-policy loader execution: cache read-through or plain ctx.use. */
|
|
204
|
+
function executeLoaderData<TEnv>(
|
|
205
|
+
loaderEntry: LoaderEntry,
|
|
206
|
+
ctx: HandlerContext<any, TEnv>,
|
|
207
|
+
pathname: string,
|
|
126
208
|
): Promise<any> {
|
|
127
209
|
const cacheConfig = loaderEntry.cache;
|
|
128
210
|
|
|
@@ -202,11 +284,20 @@ export function resolveLoaderData<TEnv>(
|
|
|
202
284
|
ctx.params,
|
|
203
285
|
);
|
|
204
286
|
|
|
287
|
+
// Capture the request context up front (foreground, ALS present) so the
|
|
288
|
+
// background stale revalidation can re-establish it. On workerd a waitUntil
|
|
289
|
+
// task runs detached from the request's I/O context, so a loader body that
|
|
290
|
+
// reads the ambient getRequestContext() would otherwise throw "called
|
|
291
|
+
// outside of a request context" and the revalidation would fail silently.
|
|
292
|
+
// The wrap is applied via wrapBackground (background path only); the
|
|
293
|
+
// foreground miss runs execute() directly since its context is present.
|
|
294
|
+
const requestCtxForExecute = getRequestContext();
|
|
205
295
|
return readThroughItem({
|
|
206
296
|
getItem: (k) => store.getItem!(k),
|
|
207
297
|
setItem: (k, v, o) => store.setItem!(k, v, o),
|
|
208
298
|
key,
|
|
209
299
|
execute: () => runMiss(loaderEntry.loader),
|
|
300
|
+
wrapBackground: (run) => runWithRequestContext(requestCtxForExecute, run),
|
|
210
301
|
serialize: (d) => codec.serializeResult(d),
|
|
211
302
|
deserialize: (v) => codec.deserializeResult(v),
|
|
212
303
|
storeOptions: { ttl, swr, tags },
|
|
@@ -214,7 +305,7 @@ export function resolveLoaderData<TEnv>(
|
|
|
214
305
|
onStale: () => debugLoaderCacheLog(`[LoaderCache] STALE: ${key}`),
|
|
215
306
|
onMiss: () => debugLoaderCacheLog(`[LoaderCache] MISS: ${key}`),
|
|
216
307
|
onCached: () => debugLoaderCacheLog(`[LoaderCache] Cached: ${key}`),
|
|
217
|
-
host:
|
|
308
|
+
host: requestCtxForExecute,
|
|
218
309
|
});
|
|
219
310
|
})();
|
|
220
311
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PPR shell-capture loader masking.
|
|
3
|
+
*
|
|
4
|
+
* During a shell CAPTURE re-render (Axis 2, see docs/design/ppr-shell-resume.md)
|
|
5
|
+
* route loaders are the "live lane": they must NOT execute — no side effects, no
|
|
6
|
+
* cost, no cache round-trips. Instead every loader segment's value slot receives
|
|
7
|
+
* a never-resolving promise, so the loader-consuming Suspense subtree stays
|
|
8
|
+
* pending and React's static `prerender` marks it as a postponed hole. The frozen
|
|
9
|
+
* shell (prelude) captures only the fallback; the resumed serve pass runs the
|
|
10
|
+
* loaders fresh through the unchanged execution path and streams their output
|
|
11
|
+
* into the holes.
|
|
12
|
+
*
|
|
13
|
+
* Capture mode is signalled by `requestCtx._shellCaptureRun`, set to true ONLY on
|
|
14
|
+
* the derived request context of the background capture task (shell-capture.ts) —
|
|
15
|
+
* NOT by the foreground render, whose `_shellCapture` descriptor merely means "a
|
|
16
|
+
* capture is wanted" and must not change behavior. This module is the single home
|
|
17
|
+
* for the mask so every loader execution site gates the same way (loader-cache.ts
|
|
18
|
+
* `resolveLoaderData`, fresh.ts `resolveLoaders`).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
_getRequestContext,
|
|
23
|
+
type RequestContext,
|
|
24
|
+
} from "../../server/request-context.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* True when the current render is the active PPR shell capture and route loaders
|
|
28
|
+
* must be masked rather than executed. Reads `_shellCaptureRun` off the ALS
|
|
29
|
+
* request context (the capture task re-establishes its derived context via
|
|
30
|
+
* runWithRequestContext), so it is accurate at the loader resolution sites, which
|
|
31
|
+
* run synchronously inside the pipeline's context frame.
|
|
32
|
+
*/
|
|
33
|
+
export function isShellCaptureActive(
|
|
34
|
+
reqCtx: RequestContext<any> | undefined = _getRequestContext(),
|
|
35
|
+
): boolean {
|
|
36
|
+
return reqCtx?._shellCaptureRun === true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// createMaskedLoaderPromise moved to the leaf module mask-nested.ts (shared
|
|
40
|
+
// with the handle-push funnel in request-context, which cannot import THIS
|
|
41
|
+
// module without a cycle). Re-exported to keep the mask API in one place.
|
|
42
|
+
export { createMaskedLoaderPromise } from "./mask-nested.js";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Lane decision for an entry's loaders under PPR (the loading() value decides;
|
|
46
|
+
* docs/design/loader-container-bake.md):
|
|
47
|
+
*
|
|
48
|
+
* - RENDERABLE loading() (the LoaderBoundary Suspense fallback) — the LIVE
|
|
49
|
+
* lane: masked at capture, guaranteed fresh on every serve. Returns true.
|
|
50
|
+
* - No loading() (absent, or explicitly `false`, incl. `loading(x, { ssr:
|
|
51
|
+
* false })` under the SSR manifest) — the BAKE lane: the loader executes
|
|
52
|
+
* during capture, its settled container bakes, nested pending promises hole
|
|
53
|
+
* at the consumer's own Suspense. Returns false.
|
|
54
|
+
*
|
|
55
|
+
* Mirrors segment-system's isRenderableLoading so the mask decision and the
|
|
56
|
+
* tree's boundary placement can never disagree.
|
|
57
|
+
*/
|
|
58
|
+
export function entryLoadingMasksLoaders(loading: unknown): boolean {
|
|
59
|
+
return loading !== undefined && loading !== null && loading !== false;
|
|
60
|
+
}
|