@rangojs/router 0.22.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/rango.js +10 -4
- package/dist/testing/vitest.js +31 -1
- package/dist/types/browser/defer.d.ts +48 -0
- package/dist/types/browser/document-revealed.d.ts +17 -0
- package/dist/types/browser/event-controller.d.ts +7 -1
- package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
- package/dist/types/browser/navigation-transaction.d.ts +21 -7
- package/dist/types/browser/partial-update.d.ts +39 -8
- package/dist/types/browser/react/NavigationProvider.d.ts +19 -5
- package/dist/types/browser/react/context.d.ts +3 -2
- package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
- package/dist/types/browser/response-adapter.d.ts +7 -0
- package/dist/types/browser/types.d.ts +24 -10
- package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/router-processing.d.ts +1 -1
- package/dist/types/build/route-types/scan-filter.d.ts +22 -2
- package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
- package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
- package/dist/types/cache/document-cache-policy.d.ts +24 -0
- package/dist/types/cache/document-cache.d.ts +2 -10
- package/dist/types/client-urls/client-urls.d.ts +1 -1
- package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
- package/dist/types/client-urls/navigation.d.ts +0 -37
- package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
- package/dist/types/client-urls/server-projection.d.ts +29 -1
- package/dist/types/client-urls/types.d.ts +34 -69
- package/dist/types/client.d.ts +0 -1
- package/dist/types/context-var.d.ts +12 -0
- package/dist/types/errors.d.ts +15 -0
- package/dist/types/handles/deferred-resolution.d.ts +2 -1
- package/dist/types/handles/pushed-by.d.ts +31 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.rsc.d.ts +2 -0
- package/dist/types/loader-store.d.ts +8 -0
- package/dist/types/prefetch-markers.d.ts +11 -0
- package/dist/types/response-utils.d.ts +7 -0
- package/dist/types/reverse.d.ts +8 -8
- package/dist/types/route-definition/helpers-types.d.ts +27 -0
- package/dist/types/route-definition/rewrite.d.ts +56 -0
- package/dist/types/route-map-builder.d.ts +7 -2
- package/dist/types/router/basename.d.ts +7 -0
- package/dist/types/router/handler-context.d.ts +1 -5
- package/dist/types/router/internal-params.d.ts +8 -0
- package/dist/types/router/is-action.d.ts +2 -1
- package/dist/types/router/middleware-types.d.ts +15 -0
- package/dist/types/router/revalidation.d.ts +1 -2
- package/dist/types/router/route-trie-builder.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
- package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +264 -0
- package/dist/types/rsc/full-payload.d.ts +11 -0
- package/dist/types/rsc/handler.d.ts +33 -0
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/loader-fetch.d.ts +16 -0
- package/dist/types/rsc/rewrite-passes.d.ts +74 -0
- package/dist/types/rsc/ssr-setup.d.ts +3 -1
- package/dist/types/server/context.d.ts +21 -1
- package/dist/types/server/handle-store.d.ts +9 -1
- package/dist/types/server/request-context.d.ts +52 -2
- package/dist/types/server/rewrite-trail.d.ts +49 -0
- package/dist/types/server/slot-accessors.d.ts +29 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +4 -4
- package/dist/types/testing/internal/action-id.d.ts +10 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
- package/dist/types/testing/internal/test-action.d.ts +8 -0
- package/dist/types/testing/render-route.d.ts +8 -0
- package/dist/types/testing/run-loader.d.ts +2 -1
- package/dist/types/testing/run-middleware.d.ts +20 -0
- package/dist/types/testing/run-revalidate.d.ts +74 -0
- package/dist/types/testing/run-transition-when.d.ts +2 -3
- package/dist/types/testing/serve-shell-request.d.ts +40 -1
- package/dist/types/testing/vitest.d.ts +11 -0
- package/dist/types/thenable-status.d.ts +6 -0
- package/dist/types/types/boundaries.d.ts +9 -0
- package/dist/types/types/handler-context.d.ts +14 -0
- package/dist/types/types/loader-types.d.ts +30 -0
- package/dist/types/types/request-scope.d.ts +7 -0
- package/dist/types/types/segments.d.ts +21 -4
- package/dist/types/urls/path-helper-types.d.ts +25 -4
- package/dist/types/urls/type-extraction.d.ts +6 -2
- package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
- package/dist/types/vite/discovery/state.d.ts +14 -4
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
- package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
- package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
- package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
- package/dist/types/vite/router-discovery.d.ts +10 -0
- package/dist/vite/index.js +974 -379
- package/package.json +1 -1
- package/skills/caching/SKILL.md +54 -5
- package/skills/catalog.json +3 -3
- package/skills/client-urls/SKILL.md +197 -24
- package/skills/cloudflare/SKILL.md +14 -9
- package/skills/hooks/SKILL.md +16 -3
- package/skills/hooks/data.md +3 -1
- package/skills/links/SKILL.md +24 -0
- package/skills/loader/SKILL.md +199 -2
- package/skills/middleware/SKILL.md +183 -2
- package/skills/migrate-nextjs/SKILL.md +2 -2
- package/skills/migrate-react-router/SKILL.md +10 -7
- package/skills/migrate-react-router/data-and-actions.md +1 -1
- package/skills/ppr/SKILL.md +22 -0
- package/skills/prerender/SKILL.md +22 -8
- package/skills/rango/SKILL.md +13 -7
- package/skills/route/SKILL.md +139 -0
- package/skills/server-actions/SKILL.md +27 -0
- package/skills/testing/SKILL.md +3 -2
- package/skills/testing/client-components.md +2 -1
- package/skills/testing/middleware.md +31 -10
- package/skills/testing/predicates.md +67 -32
- package/skills/testing/response-routes.md +1 -0
- package/skills/testing/serve-shell-request.md +107 -19
- package/skills/testing/setup.md +2 -2
- package/skills/typesafety/route-types.md +2 -2
- package/skills/use-cache/SKILL.md +4 -2
- package/skills/view-transitions/SKILL.md +26 -0
- package/src/__augment-tests__/augmented.check.ts +20 -0
- package/src/browser/defer.ts +82 -0
- package/src/browser/document-revealed.ts +52 -0
- package/src/browser/event-controller.ts +46 -32
- package/src/browser/merge-segment-loaders.ts +31 -18
- package/src/browser/navigation-bridge.ts +82 -16
- package/src/browser/navigation-client.ts +48 -36
- package/src/browser/navigation-store.ts +13 -0
- package/src/browser/navigation-transaction.ts +25 -8
- package/src/browser/partial-update.ts +554 -146
- package/src/browser/prefetch/fetch.ts +2 -1
- package/src/browser/react/Link.tsx +7 -9
- package/src/browser/react/NavigationProvider.tsx +88 -49
- package/src/browser/react/context.ts +3 -2
- package/src/browser/react/filter-segment-order.ts +13 -2
- package/src/browser/react/use-router.ts +2 -4
- package/src/browser/response-adapter.ts +20 -0
- package/src/browser/rsc-router.tsx +17 -45
- package/src/browser/server-action-bridge.ts +25 -63
- package/src/browser/types.ts +24 -10
- package/src/browser/validate-redirect-origin.ts +16 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/router-processing.ts +3 -6
- package/src/build/route-types/scan-filter.ts +45 -11
- package/src/cache/cf/cf-cache-store.ts +59 -21
- package/src/cache/cf/cf-cache-types.ts +24 -2
- package/src/cache/document-cache-policy.ts +131 -0
- package/src/cache/document-cache.ts +21 -107
- package/src/client-urls/client-urls.ts +22 -6
- package/src/client-urls/hoisted-revalidate.ts +25 -0
- package/src/client-urls/navigation.ts +0 -96
- package/src/client-urls/revalidate-registry.ts +30 -0
- package/src/client-urls/server-projection.ts +151 -65
- package/src/client-urls/types.ts +39 -71
- package/src/client.tsx +9 -5
- package/src/context-var.ts +32 -2
- package/src/errors.ts +34 -0
- package/src/handles/deferred-resolution.ts +28 -5
- package/src/handles/pushed-by.ts +148 -0
- package/src/index.rsc.ts +8 -0
- package/src/index.ts +7 -0
- package/src/loader-store.ts +18 -7
- package/src/prefetch-markers.ts +13 -0
- package/src/response-utils.ts +19 -0
- package/src/reverse.ts +17 -14
- package/src/route-content-wrapper.tsx +18 -4
- package/src/route-definition/dsl-helpers.ts +27 -4
- package/src/route-definition/helpers-types.ts +27 -1
- package/src/route-definition/redirect.ts +2 -15
- package/src/route-definition/rewrite.ts +146 -0
- package/src/route-map-builder.ts +14 -11
- package/src/router/basename.ts +20 -0
- package/src/router/handler-context.ts +3 -13
- package/src/router/intercept-resolution.ts +35 -16
- package/src/router/internal-params.ts +16 -0
- package/src/router/is-action.ts +2 -1
- package/src/router/loader-resolution.ts +85 -4
- package/src/router/match-api.ts +36 -1
- package/src/router/match-middleware/background-revalidation.ts +16 -4
- package/src/router/match-middleware/cache-lookup.ts +35 -7
- package/src/router/match-middleware/cache-store.ts +15 -1
- package/src/router/match-middleware/intercept-resolution.ts +4 -2
- package/src/router/match-result.ts +14 -2
- package/src/router/middleware-types.ts +16 -0
- package/src/router/middleware.ts +127 -49
- package/src/router/navigation-snapshot.ts +2 -1
- package/src/router/revalidation.ts +1 -1
- package/src/router/route-trie-builder.ts +28 -1
- package/src/router/segment-resolution/fresh.ts +22 -18
- package/src/router/segment-resolution/loader-cache.ts +2 -1
- package/src/router/segment-resolution/loader-mask.ts +5 -3
- package/src/router/segment-resolution/loader-snapshot.ts +18 -4
- package/src/router/segment-resolution/mask-nested.ts +8 -4
- package/src/router/segment-resolution/prefetch-deferral.ts +583 -0
- package/src/router/segment-resolution/revalidation.ts +215 -82
- package/src/rsc/full-payload.ts +37 -5
- package/src/rsc/handler.ts +82 -55
- package/src/rsc/helpers.ts +24 -0
- package/src/rsc/loader-fetch.ts +79 -24
- package/src/rsc/response-cache-serve.ts +18 -2
- package/src/rsc/response-route-handler.ts +4 -7
- package/src/rsc/rewrite-passes.ts +203 -0
- package/src/rsc/rsc-rendering.ts +54 -9
- package/src/rsc/shell-capture.ts +2 -1
- package/src/rsc/ssr-setup.ts +11 -5
- package/src/segment-loader-promise.ts +23 -20
- package/src/segment-system.tsx +60 -26
- package/src/server/context.ts +39 -5
- package/src/server/cookie-parse.ts +36 -12
- package/src/server/cookie-store.ts +7 -3
- package/src/server/handle-store.ts +152 -25
- package/src/server/request-context.ts +175 -64
- package/src/server/request-headers.ts +4 -14
- package/src/server/rewrite-trail.ts +190 -0
- package/src/server/slot-accessors.ts +57 -0
- package/src/server.ts +4 -0
- package/src/ssr/index.tsx +118 -16
- package/src/testing/action-form.ts +2 -11
- package/src/testing/dispatch.ts +138 -67
- package/src/testing/e2e/page-helpers.ts +3 -1
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +7 -3
- package/src/testing/internal/action-id.ts +16 -1
- package/src/testing/internal/client-urls-projection.ts +70 -0
- package/src/testing/internal/test-action.ts +25 -0
- package/src/testing/render-route.tsx +16 -3
- package/src/testing/run-loader.ts +4 -1
- package/src/testing/run-middleware.ts +56 -11
- package/src/testing/run-revalidate.ts +142 -0
- package/src/testing/run-transition-when.ts +7 -8
- package/src/testing/serve-shell-request.ts +131 -8
- package/src/testing/vitest.ts +67 -1
- package/src/thenable-status.ts +11 -0
- package/src/types/boundaries.ts +12 -0
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +30 -0
- package/src/types/request-scope.ts +8 -0
- package/src/types/segments.ts +26 -4
- package/src/urls/path-helper-types.ts +27 -5
- package/src/urls/type-extraction.ts +30 -10
- package/src/use-loader.tsx +18 -13
- package/src/vite/discovery/client-urls-projection.ts +20 -21
- package/src/vite/discovery/discover-routers.ts +94 -26
- package/src/vite/discovery/state.ts +28 -4
- package/src/vite/discovery/virtual-module-codegen.ts +22 -17
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
- package/src/vite/plugins/expose-internal-ids.ts +2 -3
- package/src/vite/plugins/hoist/literal.ts +417 -0
- package/src/vite/plugins/hoist/scope.ts +370 -0
- package/src/vite/plugins/hoist/source.ts +84 -0
- package/src/vite/plugins/transition-when-hoist.ts +113 -779
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +116 -53
- package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
- package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
- package/dist/types/testing/run-client-revalidate.d.ts +0 -43
- package/src/client-urls/revalidate-chain.ts +0 -83
- package/src/client-urls/revalidation-protocol.ts +0 -56
- package/src/testing/run-client-revalidate.ts +0 -92
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pass loop that honors `throw rewrite()` (route-definition/rewrite.ts):
|
|
3
|
+
* one catch around the global middleware chain moves the same request
|
|
4
|
+
* context to the target (server/request-context.ts rebaseRequestContext) and
|
|
5
|
+
* runs the request again from the top, as a direct visit to the target
|
|
6
|
+
* would run it. Shared by the request handler (rsc/handler.ts) and the
|
|
7
|
+
* dispatch testing primitive, so the two cannot drift.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { RewriteLoopError } from "../errors.js";
|
|
11
|
+
import {
|
|
12
|
+
describeRewriteTarget,
|
|
13
|
+
isRewrite,
|
|
14
|
+
rewriteNotSupported,
|
|
15
|
+
type Rewrite,
|
|
16
|
+
} from "../route-definition/rewrite.js";
|
|
17
|
+
import { prefixBasename } from "../router/basename.js";
|
|
18
|
+
import { createReverseFunction } from "../router/handler-context.js";
|
|
19
|
+
import { stripInternalParams } from "../router/internal-params.js";
|
|
20
|
+
import {
|
|
21
|
+
rebaseRequestContext,
|
|
22
|
+
type RequestContext,
|
|
23
|
+
} from "../server/request-context.js";
|
|
24
|
+
import { recordRewriteHop } from "../server/rewrite-trail.js";
|
|
25
|
+
|
|
26
|
+
/** Response header naming the route path a rewritten request rendered. */
|
|
27
|
+
export const REWRITE_HEADER = "x-rango-rewrite";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a rewrite's target against the router's route map (global names,
|
|
31
|
+
* no param auto-fill, as middleware `ctx.reverse`) and the request origin.
|
|
32
|
+
* A path is basename-prefixed like `redirect()` and must stay under the
|
|
33
|
+
* basename once normalized (`/../admin` cannot step out of it). The
|
|
34
|
+
* target's own `_rsc*` params are dropped: only the visitor's transport
|
|
35
|
+
* params carry over. Throws for an unknown name, a missing param, another
|
|
36
|
+
* origin or a path outside the basename.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveRewriteTarget(
|
|
39
|
+
rewrite: Rewrite,
|
|
40
|
+
routeMap: Record<string, string>,
|
|
41
|
+
base: URL,
|
|
42
|
+
basename?: string,
|
|
43
|
+
): URL {
|
|
44
|
+
const { target } = rewrite;
|
|
45
|
+
const path =
|
|
46
|
+
"path" in target
|
|
47
|
+
? prefixBasename(target.path, basename)
|
|
48
|
+
: // `{}`, not undefined: a route with params then throws for the
|
|
49
|
+
// missing ones instead of resolving to its raw pattern.
|
|
50
|
+
createReverseFunction(routeMap)(
|
|
51
|
+
target.name,
|
|
52
|
+
target.params ?? {},
|
|
53
|
+
target.search,
|
|
54
|
+
);
|
|
55
|
+
const resolved = stripInternalParams(new URL(path, base.origin));
|
|
56
|
+
if (resolved.origin !== base.origin) {
|
|
57
|
+
throw new TypeError(
|
|
58
|
+
`${describeRewriteTarget(target)} must name a path in this app, not another origin.`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (
|
|
62
|
+
basename &&
|
|
63
|
+
resolved.pathname !== basename &&
|
|
64
|
+
!resolved.pathname.startsWith(basename + "/")
|
|
65
|
+
) {
|
|
66
|
+
throw new TypeError(
|
|
67
|
+
`${describeRewriteTarget(target)} resolves to ${resolved.pathname}, outside the router's basename ${basename}.`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
resolved.hash = "";
|
|
71
|
+
return resolved;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The raw URL of a rewritten pass: the target's path and search plus the
|
|
76
|
+
* visitor's internal `_rsc*` transport params, so a navigation stays a
|
|
77
|
+
* navigation (same segments, version, partial flag).
|
|
78
|
+
*/
|
|
79
|
+
function rewriteTransportUrl(target: URL, visitorRaw: URL): URL {
|
|
80
|
+
const raw = new URL(target);
|
|
81
|
+
visitorRaw.searchParams.forEach((value, key) => {
|
|
82
|
+
if (key.startsWith("_rsc")) raw.searchParams.append(key, value);
|
|
83
|
+
});
|
|
84
|
+
return raw;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The next pass of a rewritten request. */
|
|
88
|
+
export interface RewritePass {
|
|
89
|
+
request: Request;
|
|
90
|
+
/** The pass's raw URL, `_rsc*` params included. */
|
|
91
|
+
url: URL;
|
|
92
|
+
/** The target's clean URL. */
|
|
93
|
+
target: URL;
|
|
94
|
+
hops: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Move the request context to `rewrite`'s target for the next pass: check
|
|
99
|
+
* that this request can be rewritten, resolve the target, count the hop,
|
|
100
|
+
* then rebase the context. Throws the site error for a mutation or a loader
|
|
101
|
+
* fetch, a resolution error, and RewriteLoopError.
|
|
102
|
+
*/
|
|
103
|
+
export function applyRewrite(
|
|
104
|
+
rewrite: Rewrite,
|
|
105
|
+
passRequest: Request,
|
|
106
|
+
reqCtx: RequestContext<any>,
|
|
107
|
+
routeMap: Record<string, string>,
|
|
108
|
+
basename: string | undefined,
|
|
109
|
+
): RewritePass {
|
|
110
|
+
// A loader fetch first: `load()` with a body is a POST, and its refusal
|
|
111
|
+
// is the loader fetch's.
|
|
112
|
+
if (reqCtx.originalUrl.searchParams.has("_rsc_loader")) {
|
|
113
|
+
throw rewriteNotSupported(rewrite, "loader-fetch");
|
|
114
|
+
}
|
|
115
|
+
if (passRequest.method !== "GET" && passRequest.method !== "HEAD") {
|
|
116
|
+
throw rewriteNotSupported(rewrite, "mutation");
|
|
117
|
+
}
|
|
118
|
+
const target = resolveRewriteTarget(rewrite, routeMap, reqCtx.url, basename);
|
|
119
|
+
const hops = recordRewriteHop(reqCtx, reqCtx.url, target);
|
|
120
|
+
const url = rewriteTransportUrl(target, reqCtx.originalUrl);
|
|
121
|
+
const request = new Request(url, passRequest);
|
|
122
|
+
rebaseRequestContext(reqCtx, request, url);
|
|
123
|
+
return { request, url, target, hops };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The response to a rewrite loop. */
|
|
127
|
+
export function loopDetectedResponse(): Response {
|
|
128
|
+
return new Response("Loop Detected", {
|
|
129
|
+
status: 508,
|
|
130
|
+
headers: { "content-type": "text/plain;charset=utf-8" },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface RewritePassesOptions {
|
|
135
|
+
reqCtx: RequestContext<any>;
|
|
136
|
+
/** The visitor's request and raw URL: the first pass. */
|
|
137
|
+
request: Request;
|
|
138
|
+
url: URL;
|
|
139
|
+
routeMap: () => Record<string, string>;
|
|
140
|
+
basename: string | undefined;
|
|
141
|
+
/** One pass: the global chain matched on `url`'s path around the core handler. */
|
|
142
|
+
runPass: (request: Request, url: URL) => Promise<Response>;
|
|
143
|
+
/** After a hop moved the context to its target. */
|
|
144
|
+
onHop?: (pass: RewritePass) => void;
|
|
145
|
+
/** Report a rewrite the request cannot take (onError, phase "routing"). */
|
|
146
|
+
report: (error: unknown, at: { request: Request; url: URL }) => void;
|
|
147
|
+
/**
|
|
148
|
+
* The response for a refused rewrite other than a loop (a loader fetch's
|
|
149
|
+
* error response); undefined rethrows the error.
|
|
150
|
+
*/
|
|
151
|
+
refuse?: (error: unknown, rewrite: Rewrite) => Response | undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface RewritePassesResult {
|
|
155
|
+
response: Response;
|
|
156
|
+
/** The pathname the final pass rendered, when a rewrite took the request there. */
|
|
157
|
+
rewrittenTo?: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Run the request pass by pass until one produces a response. A `Rewrite`
|
|
162
|
+
* that unwinds a pass moves the context to its target and runs the next;
|
|
163
|
+
* a loop answers 508; a rewrite the request cannot take is reported and
|
|
164
|
+
* becomes `refuse`'s response, or is thrown.
|
|
165
|
+
*/
|
|
166
|
+
export async function runRewritePasses(
|
|
167
|
+
opts: RewritePassesOptions,
|
|
168
|
+
): Promise<RewritePassesResult> {
|
|
169
|
+
let request = opts.request;
|
|
170
|
+
let url = opts.url;
|
|
171
|
+
let rewrittenTo: string | undefined;
|
|
172
|
+
for (;;) {
|
|
173
|
+
let rewrite: Rewrite;
|
|
174
|
+
try {
|
|
175
|
+
return { response: await opts.runPass(request, url), rewrittenTo };
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if (!isRewrite(error)) throw error;
|
|
178
|
+
rewrite = error;
|
|
179
|
+
}
|
|
180
|
+
let pass: RewritePass;
|
|
181
|
+
try {
|
|
182
|
+
pass = applyRewrite(
|
|
183
|
+
rewrite,
|
|
184
|
+
request,
|
|
185
|
+
opts.reqCtx,
|
|
186
|
+
opts.routeMap(),
|
|
187
|
+
opts.basename,
|
|
188
|
+
);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
opts.report(error, { request, url });
|
|
191
|
+
if (error instanceof RewriteLoopError) {
|
|
192
|
+
return { response: loopDetectedResponse() };
|
|
193
|
+
}
|
|
194
|
+
const refused = opts.refuse?.(error, rewrite);
|
|
195
|
+
if (refused) return { response: refused };
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
request = pass.request;
|
|
199
|
+
url = pass.url;
|
|
200
|
+
rewrittenTo = pass.target.pathname;
|
|
201
|
+
opts.onHop?.(pass);
|
|
202
|
+
}
|
|
203
|
+
}
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
import type { HandlerContext } from "./handler-context.js";
|
|
52
52
|
import { attachTransitionWhen } from "./attach-transition-when.js";
|
|
53
53
|
import { publicRouteName } from "../route-name.js";
|
|
54
|
-
import { buildFullPayload } from "./full-payload.js";
|
|
54
|
+
import { buildFullPayload, renderedLocation } from "./full-payload.js";
|
|
55
55
|
import {
|
|
56
56
|
capShellWindow,
|
|
57
57
|
lastStoredCaptureSeq,
|
|
@@ -94,6 +94,7 @@ import {
|
|
|
94
94
|
type DevShellLookup,
|
|
95
95
|
} from "./shell-build-manifest.js";
|
|
96
96
|
import { contextGet } from "../context-var.js";
|
|
97
|
+
import { PREFETCH_HEADER } from "../prefetch-markers.js";
|
|
97
98
|
import {
|
|
98
99
|
isWarmReplace,
|
|
99
100
|
noteWarmShellEvent,
|
|
@@ -302,7 +303,10 @@ type PreparedRender =
|
|
|
302
303
|
| {
|
|
303
304
|
kind: "payload";
|
|
304
305
|
payload: RscPayload;
|
|
305
|
-
/**
|
|
306
|
+
/**
|
|
307
|
+
* The response depends on the source page: an intercept targets the
|
|
308
|
+
* route, or it carries deferred units (`prefetch: false`).
|
|
309
|
+
*/
|
|
306
310
|
sourceScoped: boolean;
|
|
307
311
|
pprReplayStatus?: PprReplayStatus;
|
|
308
312
|
/** The key a navigation-only heal capture stores under, when one is needed. */
|
|
@@ -831,11 +835,17 @@ function* preparePayloadPlan<TEnv>(
|
|
|
831
835
|
};
|
|
832
836
|
}
|
|
833
837
|
|
|
838
|
+
// What a prefetch defers depends on what the source page holds: a page
|
|
839
|
+
// that holds the segment gets it rendered. Adopted from another page, a
|
|
840
|
+
// deferring payload held the click on a fill for content a plain click
|
|
841
|
+
// showed at once, and ran held work again.
|
|
842
|
+
const deferring = result.segments.some((s) => s.deferred);
|
|
843
|
+
if (deferring) reqCtx._payloadDeferred = true;
|
|
834
844
|
return {
|
|
835
845
|
kind: "payload",
|
|
836
846
|
payload: {
|
|
837
847
|
metadata: {
|
|
838
|
-
pathname: url.pathname,
|
|
848
|
+
pathname: renderedLocation(reqCtx, url).pathname,
|
|
839
849
|
// routerId is serialized on every payload (including within-session
|
|
840
850
|
// ones) so the frontend can read the current app/router identity. It
|
|
841
851
|
// always equals the current app's id: a cross-app navigation is
|
|
@@ -860,7 +870,7 @@ function* preparePayloadPlan<TEnv>(
|
|
|
860
870
|
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
861
871
|
},
|
|
862
872
|
},
|
|
863
|
-
sourceScoped: result.interceptTargeted === true,
|
|
873
|
+
sourceScoped: result.interceptTargeted === true || deferring,
|
|
864
874
|
pprReplayStatus,
|
|
865
875
|
healKey,
|
|
866
876
|
};
|
|
@@ -1200,16 +1210,34 @@ function renderPreparedRscResponse<TEnv>(
|
|
|
1200
1210
|
rscHeaders["x-rsc-prefetch-scope"] = "source";
|
|
1201
1211
|
}
|
|
1202
1212
|
// Enable browser HTTP caching for prefetch responses only.
|
|
1203
|
-
// Requires X-Rango-Prefetch
|
|
1204
|
-
// a source-agnostic response (see sourceScoped above), and a
|
|
1205
|
-
// cache-control value (false disables caching).
|
|
1206
|
-
|
|
1207
|
-
if (isPrefetch && isPartial && !sourceScoped) {
|
|
1213
|
+
// Requires a prefetch request (X-Rango-Prefetch, sent by Link prefetch
|
|
1214
|
+
// fetch), a source-agnostic response (see sourceScoped above), and a
|
|
1215
|
+
// configured cache-control value (false disables caching).
|
|
1216
|
+
if (reqCtx._requestKind === "prefetch" && isPartial && !sourceScoped) {
|
|
1208
1217
|
const cc = ctx.router.prefetchCacheControl;
|
|
1209
1218
|
if (cc) {
|
|
1210
1219
|
rscHeaders["cache-control"] = cc;
|
|
1211
1220
|
}
|
|
1212
1221
|
}
|
|
1222
|
+
// prefetch: false (docs/design/prefetch-false.md, R7). A response that
|
|
1223
|
+
// carries deferred units answers one page's prefetch, and a fill is the
|
|
1224
|
+
// second half of one adoption: neither is reused by a shared cache or the
|
|
1225
|
+
// document cache (which also refuses a payload marked `_payloadDeferred`),
|
|
1226
|
+
// whatever Cache-Control the route set. A route with no flag keeps its
|
|
1227
|
+
// headers.
|
|
1228
|
+
if (
|
|
1229
|
+
reqCtx._payloadDeferred ||
|
|
1230
|
+
(isPartial && reqCtx._requestKind === "fill")
|
|
1231
|
+
) {
|
|
1232
|
+
rscHeaders["cache-control"] = NOT_REUSED;
|
|
1233
|
+
}
|
|
1234
|
+
// The browser may still keep the body, and answers from it without asking
|
|
1235
|
+
// while it reloads a document for back/forward. A navigation is the same
|
|
1236
|
+
// URL as its prefetch: the header that tells them apart keeps the deferring
|
|
1237
|
+
// body from answering one.
|
|
1238
|
+
if (reqCtx._payloadDeferred) {
|
|
1239
|
+
rscHeaders.vary += `, ${PREFETCH_HEADER}`;
|
|
1240
|
+
}
|
|
1213
1241
|
|
|
1214
1242
|
const isFlightResponse = isRscRequest(request, url, isPartial);
|
|
1215
1243
|
const stageTracking = {
|
|
@@ -1694,6 +1722,21 @@ function publishTailTiming(
|
|
|
1694
1722
|
|
|
1695
1723
|
const PENDING = Symbol("pending");
|
|
1696
1724
|
|
|
1725
|
+
/**
|
|
1726
|
+
* Cache-Control of a response that is not to be reused: one that carries
|
|
1727
|
+
* deferred units (`prefetch: false`), and a fill. `private` keeps it out of
|
|
1728
|
+
* shared caches; `no-cache` makes the browser ask again, and with no
|
|
1729
|
+
* validator on the response that is a full request. The browser may keep the
|
|
1730
|
+
* body all the same (Chromium does) and answers from it, without asking,
|
|
1731
|
+
* while it reloads a document for back/forward. That is harmless: the client
|
|
1732
|
+
* completes any payload that carries deferred work, wherever it came from.
|
|
1733
|
+
* Not `no-store`:
|
|
1734
|
+
* Chromium reports a `no-store` fetch whose body is read through a stream
|
|
1735
|
+
* as canceled (net::ERR_ABORTED in DevTools, and Playwright's
|
|
1736
|
+
* response.finished() never settles), though every byte arrived.
|
|
1737
|
+
*/
|
|
1738
|
+
const NOT_REUSED = "private, no-cache";
|
|
1739
|
+
|
|
1697
1740
|
/**
|
|
1698
1741
|
* Whether `value` is not a promise, or a promise that has already settled:
|
|
1699
1742
|
* racing it against an already-resolved promise, a settled one's reaction is
|
|
@@ -1824,6 +1867,8 @@ function serveShellHit(
|
|
|
1824
1867
|
payload.metadata.initialTheme = entry.initialTheme as
|
|
1825
1868
|
| import("../theme/types.js").Theme
|
|
1826
1869
|
| undefined;
|
|
1870
|
+
// Same for the location: the capture rendered the target's.
|
|
1871
|
+
payload.metadata.pathname = url.pathname;
|
|
1827
1872
|
}
|
|
1828
1873
|
// Full Flight render per request: hydration needs the whole payload (there
|
|
1829
1874
|
// is no Flight-side resume — a React limitation, not ours).
|
package/src/rsc/shell-capture.ts
CHANGED
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
} from "../server/context.js";
|
|
74
74
|
import { isThenable } from "../handles/is-thenable.js";
|
|
75
75
|
import { requestHeaders } from "../server/request-headers.js";
|
|
76
|
+
import { PREFETCH_HEADER } from "../prefetch-markers.js";
|
|
76
77
|
import type {
|
|
77
78
|
CachedEntryData,
|
|
78
79
|
ShellCacheEntry,
|
|
@@ -1448,7 +1449,7 @@ function createNavigationCaptureRequest(request: Request, url: URL): Request {
|
|
|
1448
1449
|
headers.set("accept", "text/html");
|
|
1449
1450
|
for (const name of [
|
|
1450
1451
|
"rsc-action",
|
|
1451
|
-
|
|
1452
|
+
PREFETCH_HEADER,
|
|
1452
1453
|
"x-rango-state",
|
|
1453
1454
|
"x-rsc-hmr",
|
|
1454
1455
|
"x-rsc-router-client-path",
|
package/src/rsc/ssr-setup.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
RSC_WIRE_MIME,
|
|
23
23
|
} from "../router/content-negotiation.js";
|
|
24
24
|
import { _getRequestContext } from "../server/request-context.js";
|
|
25
|
+
import { renderedLocation } from "./full-payload.js";
|
|
25
26
|
|
|
26
27
|
export type SSRSetup = readonly [SSRModule, SSRStreamMode];
|
|
27
28
|
|
|
@@ -38,6 +39,8 @@ export const SSR_SETUP_VAR = "__ssrSetup";
|
|
|
38
39
|
* lazily so stores created after kickoff are still captured). Without a
|
|
39
40
|
* getter the promises run bare — no `.then()` microtasks, no
|
|
40
41
|
* `performance.now()` calls — keeping the non-debug hot path lean.
|
|
42
|
+
* `ssrModule` reuses a load already started (a rewritten request's earlier
|
|
43
|
+
* pass).
|
|
41
44
|
*/
|
|
42
45
|
export function startSSRSetup<TEnv>(
|
|
43
46
|
ctx: HandlerContext<TEnv>,
|
|
@@ -45,16 +48,17 @@ export function startSSRSetup<TEnv>(
|
|
|
45
48
|
env: TEnv,
|
|
46
49
|
url: URL,
|
|
47
50
|
getMetricsStore?: () => MetricsStore | undefined,
|
|
51
|
+
ssrModule?: Promise<SSRModule>,
|
|
48
52
|
): Promise<SSRSetup> {
|
|
49
53
|
if (!getMetricsStore) {
|
|
50
54
|
return Promise.all([
|
|
51
|
-
ctx.loadSSRModule(),
|
|
55
|
+
ssrModule ?? ctx.loadSSRModule(),
|
|
52
56
|
ctx.resolveStreamMode(request, env, url),
|
|
53
57
|
]);
|
|
54
58
|
}
|
|
55
59
|
const start = performance.now();
|
|
56
60
|
return Promise.all([
|
|
57
|
-
ctx.loadSSRModule().then((mod) => {
|
|
61
|
+
(ssrModule ?? ctx.loadSSRModule()).then((mod) => {
|
|
58
62
|
appendMetric(
|
|
59
63
|
getMetricsStore(),
|
|
60
64
|
"ssr:module-load",
|
|
@@ -110,7 +114,9 @@ export function createSsrHtmlStage<TEnv>(
|
|
|
110
114
|
options.url,
|
|
111
115
|
options.metricsStore,
|
|
112
116
|
);
|
|
113
|
-
const
|
|
117
|
+
const reqCtx = _getRequestContext();
|
|
118
|
+
const renderErrors = reqCtx?._renderErrors;
|
|
119
|
+
const location = renderedLocation(reqCtx, options.url);
|
|
114
120
|
return {
|
|
115
121
|
render: () =>
|
|
116
122
|
// search: the LIVE request's query string, threaded out-of-band into
|
|
@@ -121,11 +127,11 @@ export function createSsrHtmlStage<TEnv>(
|
|
|
121
127
|
ssrModule.renderHTML(flight.stream, {
|
|
122
128
|
...options.render,
|
|
123
129
|
streamMode,
|
|
124
|
-
search:
|
|
130
|
+
search: location.search,
|
|
125
131
|
// origin: same out-of-band channel — seeds the SSR store location
|
|
126
132
|
// so origin-dependent markup (Link's data-external) agrees with the
|
|
127
133
|
// browser's window.location across hydration.
|
|
128
|
-
origin:
|
|
134
|
+
origin: location.origin,
|
|
129
135
|
// A component that throws inside a Suspense boundary leaves an
|
|
130
136
|
// errored boundary in a completed document; the document cache
|
|
131
137
|
// reads the list before storing it.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ResolvedSegment } from "./types.js";
|
|
2
2
|
import { INTERNAL_RANGO_DEBUG } from "./internal-debug.js";
|
|
3
|
+
import { asStream } from "./browser/defer.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Browser: a boundary is handed the settled value. React treats a promise as
|
|
@@ -7,7 +8,13 @@ import { INTERNAL_RANGO_DEBUG } from "./internal-debug.js";
|
|
|
7
8
|
* promise it has not read, in a render that cannot wait, shows its fallback.
|
|
8
9
|
* So a cache entry records its array once the aggregate fulfils and later
|
|
9
10
|
* calls return that same array; until then (and when it rejects) they return
|
|
10
|
-
* the promise. A boundary with no loaders gets one shared empty array.
|
|
11
|
+
* the promise. A boundary with no loaders gets one shared empty array. An
|
|
12
|
+
* aggregate whose sources are all streams that have fulfilled (React's
|
|
13
|
+
* `status` on the thenable, which Flight sets itself; a plain value is not a
|
|
14
|
+
* stream, whatever fields it carries) is known on its first
|
|
15
|
+
* call and is the array from the start: the sources of a page whose entry was
|
|
16
|
+
* rewritten without a render (browser/partial-update.ts fillHoles) are
|
|
17
|
+
* first aggregated by the next navigation that keeps them.
|
|
11
18
|
*
|
|
12
19
|
* Server: a fresh promise per call, so Suspense emits the fallback in the
|
|
13
20
|
* streamed HTML. A shared resolved promise would carry React's `.status`
|
|
@@ -22,10 +29,10 @@ const IS_BROWSER = typeof window !== "undefined";
|
|
|
22
29
|
|
|
23
30
|
interface LoaderCacheEntry {
|
|
24
31
|
sources: any[];
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
// The aggregate while it is pending (a rejection passes through untouched),
|
|
33
|
+
// then the array it fulfilled with. The array from the start for sources
|
|
34
|
+
// that have all fulfilled.
|
|
35
|
+
result: Promise<any[]> | any[];
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
// Cap the per-key entries array. A stable first-ref (e.g. a layout loader whose
|
|
@@ -91,13 +98,7 @@ export function buildLoaderPromise(loaders: ResolvedSegment[]): Promise<any[]> {
|
|
|
91
98
|
}
|
|
92
99
|
}
|
|
93
100
|
}
|
|
94
|
-
return Promise.all(
|
|
95
|
-
loaders.map((loader) =>
|
|
96
|
-
loader.loaderData instanceof Promise
|
|
97
|
-
? loader.loaderData
|
|
98
|
-
: Promise.resolve(loader.loaderData),
|
|
99
|
-
),
|
|
100
|
-
);
|
|
101
|
+
return Promise.all(loaders.map((loader) => asStream(loader.loaderData)));
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
function isObjectLike(value: unknown): value is object {
|
|
@@ -131,17 +132,19 @@ export function getMemoizedLoaderPromise(
|
|
|
131
132
|
if (entries) {
|
|
132
133
|
for (const entry of entries) {
|
|
133
134
|
if (hasSameReferences(entry.sources, sources)) {
|
|
134
|
-
return entry.
|
|
135
|
+
return entry.result;
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
const newEntry = { sources
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
const newEntry: LoaderCacheEntry = { sources, result: [] };
|
|
141
|
+
newEntry.result = sources.every(
|
|
142
|
+
(source) =>
|
|
143
|
+
source instanceof Promise &&
|
|
144
|
+
(source as { status?: string }).status === "fulfilled",
|
|
145
|
+
)
|
|
146
|
+
? sources.map((source) => source.value)
|
|
147
|
+
: buildLoaderPromise(loaders).then((values) => (newEntry.result = values));
|
|
145
148
|
if (entries) {
|
|
146
149
|
// Bound the array: drop the oldest entry before appending when at the cap.
|
|
147
150
|
if (entries.length >= MAX_ENTRIES_PER_KEY) {
|
|
@@ -153,5 +156,5 @@ export function getMemoizedLoaderPromise(
|
|
|
153
156
|
} else {
|
|
154
157
|
primitiveLoaderCache.set(first, [newEntry]);
|
|
155
158
|
}
|
|
156
|
-
return
|
|
159
|
+
return newEntry.result;
|
|
157
160
|
}
|
package/src/segment-system.tsx
CHANGED
|
@@ -9,6 +9,8 @@ import { AuditedOutletProvider, OutletProvider } from "./outlet-provider.js";
|
|
|
9
9
|
import { withOptimisticCommitNone } from "./browser/optimistic-commit.js";
|
|
10
10
|
import { MountContextProvider } from "./browser/react/mount-context.js";
|
|
11
11
|
import type { ResolvedSegment, RootLayoutProps } from "./types.js";
|
|
12
|
+
import { isRenderableLoading } from "./types/boundaries.js";
|
|
13
|
+
import { isPlaceholder } from "./types/segments.js";
|
|
12
14
|
import { decodeLoaderResults } from "./decode-loader-results.js";
|
|
13
15
|
import { LoaderRedirect } from "./loader-redirect.js";
|
|
14
16
|
import { invariant } from "./errors.js";
|
|
@@ -23,6 +25,7 @@ import { RootErrorBoundary } from "./root-error-boundary.js";
|
|
|
23
25
|
import { INTERNAL_RANGO_DEBUG } from "./internal-debug.js";
|
|
24
26
|
import { INTERNAL_RANGO_SUSPENSE_AUDIT } from "./internal-suspense-audit.js";
|
|
25
27
|
import { getBoundaryContent } from "./segment-boundary-content.js";
|
|
28
|
+
import { asStream } from "./browser/defer.js";
|
|
26
29
|
import {
|
|
27
30
|
buildLoaderPromise,
|
|
28
31
|
getMemoizedLoaderPromise,
|
|
@@ -61,15 +64,6 @@ function segDebugLog(msg: string, details?: Record<string, unknown>): void {
|
|
|
61
64
|
const ReactViewTransition: any =
|
|
62
65
|
"ViewTransition" in React ? (React as any).ViewTransition : null;
|
|
63
66
|
|
|
64
|
-
// A loading skeleton is renderable only when it is a real ReactNode value.
|
|
65
|
-
// `false` is treated as "not renderable" here. This is the three-term gate;
|
|
66
|
-
// the distinct two-term gate at the LoaderBoundary site deliberately treats
|
|
67
|
-
// `false` as "create a boundary without a RouteContentWrapper"
|
|
68
|
-
// (tree-structure.md), so it must NOT use this helper.
|
|
69
|
-
function isRenderableLoading(loading: ReactNode): boolean {
|
|
70
|
-
return loading !== undefined && loading !== null && loading !== false;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
67
|
// Exported for unit testing the no-parallel fast path (D6); internal otherwise.
|
|
74
68
|
export function restoreParallelLoaderMarkers(
|
|
75
69
|
segments: ResolvedSegment[],
|
|
@@ -264,32 +258,61 @@ function wrapDefaultOutletContent(
|
|
|
264
258
|
* reader sits above every boundary, so the throw is a Fizz shell error (500).
|
|
265
259
|
* `settledFallback` is that node; the caller plants it and, for a redirect,
|
|
266
260
|
* replaces the whole page (an ancestor reading the loader would still throw).
|
|
261
|
+
*
|
|
262
|
+
* `settle` is the forceAwait/action lane of a loader set that holds a
|
|
263
|
+
* deferred loader (`prefetch: false`, see hasDeferred): every other loader is
|
|
264
|
+
* settled like a flagged one, so the commit stays whole for them, and the
|
|
265
|
+
* deferred one keeps its hole for the read site to suspend on. A settled
|
|
266
|
+
* error with no boundary is left to the read site there, where the aggregate
|
|
267
|
+
* lane it replaces would have thrown it.
|
|
267
268
|
*/
|
|
268
|
-
async function buildLoaderStreams(
|
|
269
|
+
async function buildLoaderStreams(
|
|
270
|
+
loaders: ResolvedSegment[],
|
|
271
|
+
settle?: boolean,
|
|
272
|
+
): Promise<{
|
|
269
273
|
streams: Record<string, unknown>;
|
|
270
274
|
awaitedIds: string[] | undefined;
|
|
271
275
|
settledFallback: ReactNode | undefined;
|
|
272
276
|
}> {
|
|
273
277
|
const streams: Record<string, unknown> = {};
|
|
274
278
|
let awaitedIds: string[] | undefined;
|
|
275
|
-
let
|
|
279
|
+
let settledIds: string[] | undefined;
|
|
280
|
+
let settledValues: unknown[] | undefined;
|
|
276
281
|
for (const l of loaders) {
|
|
277
|
-
if (l.awaitBeforeFlush) {
|
|
282
|
+
if (l.awaitBeforeFlush || (settle && !isPlaceholder(l))) {
|
|
278
283
|
const value = await l.loaderData;
|
|
279
284
|
streams[l.loaderId!] = value;
|
|
280
|
-
(awaitedIds ??= []).push(l.loaderId!);
|
|
281
|
-
(
|
|
285
|
+
if (l.awaitBeforeFlush) (awaitedIds ??= []).push(l.loaderId!);
|
|
286
|
+
(settledIds ??= []).push(l.loaderId!);
|
|
287
|
+
(settledValues ??= []).push(value);
|
|
282
288
|
} else {
|
|
283
289
|
streams[l.loaderId!] = l.loaderData;
|
|
284
290
|
}
|
|
285
291
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
292
|
+
let settledFallback: ReactNode | undefined;
|
|
293
|
+
if (settledIds) {
|
|
294
|
+
try {
|
|
295
|
+
settledFallback =
|
|
296
|
+
decodeLoaderResults(settledValues!, settledIds).errorFallback ??
|
|
297
|
+
undefined;
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (!settle) throw error;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
290
302
|
return { streams, awaitedIds, settledFallback };
|
|
291
303
|
}
|
|
292
304
|
|
|
305
|
+
/**
|
|
306
|
+
* A loader set that holds a deferred loader (`prefetch: false`): its
|
|
307
|
+
* `loaderData` is a hole (browser/defer.ts) the browser resolves when the
|
|
308
|
+
* fill request lands (browser/partial-update.ts fillHoles), so no lane may
|
|
309
|
+
* await the set as a whole. The
|
|
310
|
+
* tree build would wait for a request that is only sent once it returns.
|
|
311
|
+
*/
|
|
312
|
+
function hasDeferred(loaders: ResolvedSegment[]): boolean {
|
|
313
|
+
return loaders.some(isPlaceholder);
|
|
314
|
+
}
|
|
315
|
+
|
|
293
316
|
function isLoaderRedirectNode(node: ReactNode): boolean {
|
|
294
317
|
return isValidElement(node) && node.type === LoaderRedirect;
|
|
295
318
|
}
|
|
@@ -354,6 +377,11 @@ export async function renderSegments(
|
|
|
354
377
|
rootLayout: RootLayout,
|
|
355
378
|
transitionGatedOff,
|
|
356
379
|
} = options || {};
|
|
380
|
+
// forceAwait and action lanes settle loader data before the commit, except
|
|
381
|
+
// a set that holds a deferred loader (hasDeferred).
|
|
382
|
+
const settleLoaders = !!(forceAwait || isAction);
|
|
383
|
+
const settleAll = (loaders: ResolvedSegment[]): boolean =>
|
|
384
|
+
settleLoaders && !hasDeferred(loaders);
|
|
357
385
|
|
|
358
386
|
const segDebug = INTERNAL_RANGO_DEBUG;
|
|
359
387
|
const segDebugStart = segDebug ? performance.now() : 0;
|
|
@@ -484,6 +512,8 @@ export async function renderSegments(
|
|
|
484
512
|
(loader) => loader.loaderId && loader.loaderData !== undefined,
|
|
485
513
|
);
|
|
486
514
|
|
|
515
|
+
// A deferred unit's component is a hole (browser/defer.ts), never a
|
|
516
|
+
// Promise: the awaits below pass it through.
|
|
487
517
|
let resolvedComponent = component;
|
|
488
518
|
if (isAction && component instanceof Promise) {
|
|
489
519
|
const componentAwaitStart = segDebug ? performance.now() : 0;
|
|
@@ -540,7 +570,11 @@ export async function renderSegments(
|
|
|
540
570
|
componentTypeof: typeof c,
|
|
541
571
|
});
|
|
542
572
|
}
|
|
543
|
-
|
|
573
|
+
// A hole here is read by React itself (browser/defer.ts asStream).
|
|
574
|
+
const streamed = asStream(resolvedComponent) as ReactNode;
|
|
575
|
+
nodeContent = isPlaceholder(node.segment)
|
|
576
|
+
? streamed
|
|
577
|
+
: registerLazyRef(streamed);
|
|
544
578
|
}
|
|
545
579
|
|
|
546
580
|
// Wrap with <ViewTransition> if transition config exists (React 19.3+ / experimental).
|
|
@@ -614,7 +648,7 @@ export async function renderSegments(
|
|
|
614
648
|
// for content-suspending routes (segment-loader-promise.ts).
|
|
615
649
|
let boundaryLoaderStreams: Record<string, unknown> | undefined;
|
|
616
650
|
let boundaryAwaitedLoaderIds: string[] | undefined;
|
|
617
|
-
if (
|
|
651
|
+
if (settleAll(loaderEntries)) {
|
|
618
652
|
const awaitStart = segDebug ? performance.now() : 0;
|
|
619
653
|
boundaryLoaderData =
|
|
620
654
|
loaderDataPromise instanceof Promise
|
|
@@ -632,7 +666,7 @@ export async function renderSegments(
|
|
|
632
666
|
streams: boundaryLoaderStreams,
|
|
633
667
|
awaitedIds: boundaryAwaitedLoaderIds,
|
|
634
668
|
settledFallback,
|
|
635
|
-
} = await buildLoaderStreams(loaderEntries));
|
|
669
|
+
} = await buildLoaderStreams(loaderEntries, settleLoaders));
|
|
636
670
|
if (settledFallback !== undefined) {
|
|
637
671
|
if (isLoaderRedirectNode(settledFallback)) {
|
|
638
672
|
content = settledFallback;
|
|
@@ -698,7 +732,7 @@ export async function renderSegments(
|
|
|
698
732
|
let errorFallback: ReactNode = null;
|
|
699
733
|
let loaderStreams: Record<string, unknown> | undefined;
|
|
700
734
|
let awaitedLoaderIds: string[] | undefined;
|
|
701
|
-
if (
|
|
735
|
+
if (settleAll(layoutLoaders)) {
|
|
702
736
|
const layoutAwaitStart = segDebug ? performance.now() : 0;
|
|
703
737
|
const resolvedData = await buildLoaderPromise(layoutLoaders);
|
|
704
738
|
if (segDebug) {
|
|
@@ -726,7 +760,7 @@ export async function renderSegments(
|
|
|
726
760
|
streams: loaderStreams,
|
|
727
761
|
awaitedIds: awaitedLoaderIds,
|
|
728
762
|
settledFallback,
|
|
729
|
-
} = await buildLoaderStreams(layoutLoaders));
|
|
763
|
+
} = await buildLoaderStreams(layoutLoaders, settleLoaders));
|
|
730
764
|
if (settledFallback !== undefined) {
|
|
731
765
|
if (isLoaderRedirectNode(settledFallback)) {
|
|
732
766
|
content = settledFallback;
|
|
@@ -780,9 +814,9 @@ export async function renderSegments(
|
|
|
780
814
|
// loader paid the pre-flush await, settle the aggregate so
|
|
781
815
|
// LoaderResolver decodes the array without a Flight-chunk use().
|
|
782
816
|
const settleParallelAggregate =
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
ownedLoaders
|
|
817
|
+
(settleLoaders ||
|
|
818
|
+
ownedLoaders.every((l) => l.awaitBeforeFlush === true)) &&
|
|
819
|
+
!hasDeferred(ownedLoaders);
|
|
786
820
|
if (settleParallelAggregate) {
|
|
787
821
|
const parallelAwaitStart = segDebug ? performance.now() : 0;
|
|
788
822
|
p.loaderDataPromise =
|