@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,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `throw rewrite(target)`: middleware renders another route at the same URL.
|
|
3
|
+
*
|
|
4
|
+
* The value only: what middleware throws and how the router recognizes it.
|
|
5
|
+
* The pass loop that honors it lives in rsc/rewrite-passes.ts, the hop trail
|
|
6
|
+
* in server/rewrite-trail.ts.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { GlobalReverseFunction } from "../reverse.js";
|
|
10
|
+
import type { DefaultReverseRouteMap } from "../types/global-namespace.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Cross-realm brand: a Vite dev server can load this module twice (the RSC
|
|
14
|
+
* and the SSR graph), so `instanceof` alone misses a Rewrite from the other
|
|
15
|
+
* copy.
|
|
16
|
+
*/
|
|
17
|
+
const REWRITE_BRAND: unique symbol = Symbol.for("rango.rewrite");
|
|
18
|
+
|
|
19
|
+
/** The target as written: a path, or a global route name with its params. */
|
|
20
|
+
export type RewriteTarget =
|
|
21
|
+
| { readonly path: string }
|
|
22
|
+
| {
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly params?: Record<string, string>;
|
|
25
|
+
readonly search?: Record<string, unknown>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const REWRITE_RULE =
|
|
29
|
+
"rewrite() is for middleware: throw it from router.use() or route " +
|
|
30
|
+
"middleware (middleware() in urls()), before await next(), while it " +
|
|
31
|
+
"handles a GET or HEAD page request. Loaders and route handlers can't " +
|
|
32
|
+
"rewrite: throw rewrite() from the route's middleware instead. Intercept " +
|
|
33
|
+
"middleware and loader fetches can't either. In a server action use " +
|
|
34
|
+
"redirect(); for a mutation return a Response from middleware.";
|
|
35
|
+
|
|
36
|
+
export function describeRewriteTarget(target: RewriteTarget): string {
|
|
37
|
+
return "path" in target
|
|
38
|
+
? `rewrite(${JSON.stringify(target.path)})`
|
|
39
|
+
: `rewrite(${JSON.stringify(target.name)})`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The value `rewrite()` returns, thrown by middleware. An Error, so a throw
|
|
44
|
+
* where a rewrite is not supported fails loudly with the rule in its message.
|
|
45
|
+
*/
|
|
46
|
+
export class Rewrite extends Error {
|
|
47
|
+
name = "Rewrite" as const;
|
|
48
|
+
readonly target: RewriteTarget;
|
|
49
|
+
|
|
50
|
+
constructor(target: RewriteTarget) {
|
|
51
|
+
super(`${describeRewriteTarget(target)}: ${REWRITE_RULE}`);
|
|
52
|
+
Object.setPrototypeOf(this, Rewrite.prototype);
|
|
53
|
+
this.target = target;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
Object.defineProperty(Rewrite.prototype, REWRITE_BRAND, { value: true });
|
|
57
|
+
|
|
58
|
+
/** Whether `value` is a Rewrite thrown by `rewrite()` (ctx.isRewrite). */
|
|
59
|
+
export function isRewrite(value: unknown): value is Rewrite {
|
|
60
|
+
return (
|
|
61
|
+
typeof value === "object" &&
|
|
62
|
+
value !== null &&
|
|
63
|
+
(value as { [REWRITE_BRAND]?: unknown })[REWRITE_BRAND] === true
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* `rewrite(path)` or `rewrite(name, params?, search?)`: a path in this app
|
|
69
|
+
* (basename-prefixed like `redirect()`, may carry `?search`), or a global
|
|
70
|
+
* route name with the params and search `ctx.reverse` takes.
|
|
71
|
+
*/
|
|
72
|
+
export type RewriteFunction<TRoutes = DefaultReverseRouteMap> = ((
|
|
73
|
+
path: `/${string}`,
|
|
74
|
+
) => Rewrite) &
|
|
75
|
+
GlobalReverseFunction<TRoutes, Rewrite>;
|
|
76
|
+
|
|
77
|
+
function createRewrite(
|
|
78
|
+
target: string,
|
|
79
|
+
params?: Record<string, string>,
|
|
80
|
+
search?: Record<string, unknown>,
|
|
81
|
+
): Rewrite {
|
|
82
|
+
if (typeof target !== "string" || target === "") {
|
|
83
|
+
throw new TypeError(
|
|
84
|
+
'rewrite() takes a path starting with "/" or a global route name.',
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (target.startsWith("/")) {
|
|
88
|
+
if (params !== undefined || search !== undefined) {
|
|
89
|
+
throw new TypeError(
|
|
90
|
+
`rewrite(${JSON.stringify(target)}) takes params only with a route name, not with a path.`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return new Rewrite({ path: target });
|
|
94
|
+
}
|
|
95
|
+
if (target.startsWith(".")) {
|
|
96
|
+
throw new TypeError(
|
|
97
|
+
`rewrite(${JSON.stringify(target)}): a rewrite resolves global route names only (no include() scope).`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return new Rewrite({ name: target, params, search });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Render another route at the same URL. Throw it from middleware, before
|
|
105
|
+
* `await next()`:
|
|
106
|
+
*
|
|
107
|
+
* ```ts
|
|
108
|
+
* router.use(async (ctx, next) => {
|
|
109
|
+
* if (!locked || ctx.pathname === "/maintenance") return next();
|
|
110
|
+
* throw rewrite("maintenance");
|
|
111
|
+
* });
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
114
|
+
* The request runs again as the target (global middleware, route
|
|
115
|
+
* middleware, render) with the same request context: values set before the
|
|
116
|
+
* throw stay readable. `ctx.url` is the target; `ctx.rewrittenFrom` is the
|
|
117
|
+
* visitor's URL. See the `/middleware` skill.
|
|
118
|
+
*/
|
|
119
|
+
export const rewrite: RewriteFunction = createRewrite as RewriteFunction;
|
|
120
|
+
|
|
121
|
+
/** A dispatch site that cannot honor a rewrite. */
|
|
122
|
+
export type RewriteSite = "mutation" | "loader-fetch" | "intercept";
|
|
123
|
+
|
|
124
|
+
const SITE_MESSAGE: Record<RewriteSite, string> = {
|
|
125
|
+
mutation:
|
|
126
|
+
"middleware threw it while handling a non-GET request (a server action " +
|
|
127
|
+
"or a form post). Return a Response for mutations instead, e.g. " +
|
|
128
|
+
'new Response("Maintenance", { status: 503 }).',
|
|
129
|
+
"loader-fetch":
|
|
130
|
+
"middleware threw it during a loader fetch (useFetchLoader, load(), " +
|
|
131
|
+
"useRefreshLoaders). Return a Response there.",
|
|
132
|
+
intercept:
|
|
133
|
+
"intercept middleware threw it. Rewrite from router.use() or the " +
|
|
134
|
+
"route's middleware(), or return a Response.",
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/** The error a rewrite becomes at a site that cannot honor it. */
|
|
138
|
+
export function rewriteNotSupported(
|
|
139
|
+
rewrite: Rewrite,
|
|
140
|
+
site: RewriteSite,
|
|
141
|
+
): Error {
|
|
142
|
+
return new Error(
|
|
143
|
+
`${describeRewriteTarget(rewrite.target)} is not supported here: ${SITE_MESSAGE[site]}`,
|
|
144
|
+
{ cause: rewrite },
|
|
145
|
+
);
|
|
146
|
+
}
|
package/src/route-map-builder.ts
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
* See docs/manifests.md for the full data flow.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import {
|
|
12
|
+
nullPrototypeStaticChildren,
|
|
13
|
+
type TrieNode,
|
|
14
|
+
} from "./router/route-trie-builder.js";
|
|
15
|
+
|
|
11
16
|
let globalRouteMap: Record<string, string> = {};
|
|
12
17
|
|
|
13
18
|
let cachedManifest: Record<string, string> | null = null;
|
|
@@ -86,8 +91,7 @@ export function clearCachedManifest(): void {
|
|
|
86
91
|
// precomputed entries so multi-router setups (e.g. site + admin via
|
|
87
92
|
// createHostRouter()) don't see each other's routes.
|
|
88
93
|
const perRouterManifestMap: Map<string, Record<string, string>> = new Map();
|
|
89
|
-
const perRouterTrieMap: Map<string,
|
|
90
|
-
new Map();
|
|
94
|
+
const perRouterTrieMap: Map<string, TrieNode> = new Map();
|
|
91
95
|
const perRouterPrecomputedEntriesMap: Map<
|
|
92
96
|
string,
|
|
93
97
|
Array<{ staticPrefix: string; routes: Record<string, string> }>
|
|
@@ -132,11 +136,12 @@ export function getRouterManifest(
|
|
|
132
136
|
return perRouterManifestMap.get(routerId);
|
|
133
137
|
}
|
|
134
138
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Install a router's trie. A trie that arrives as JSON (the build manifest)
|
|
141
|
+
* or across the dev runner boundary gets null-prototype static maps first.
|
|
142
|
+
*/
|
|
143
|
+
export function setRouterTrie(routerId: string, trie: TrieNode): void {
|
|
144
|
+
perRouterTrieMap.set(routerId, nullPrototypeStaticChildren(trie));
|
|
140
145
|
}
|
|
141
146
|
|
|
142
147
|
// Routers whose trie came from the COMPLETE build manifest (deserialized via
|
|
@@ -156,9 +161,7 @@ export function isRouterTrieAuthoritative(routerId: string): boolean {
|
|
|
156
161
|
return authoritativeTrieRouters.has(routerId);
|
|
157
162
|
}
|
|
158
163
|
|
|
159
|
-
export function getRouterTrie(
|
|
160
|
-
routerId: string,
|
|
161
|
-
): import("./build/route-trie.js").TrieNode | undefined {
|
|
164
|
+
export function getRouterTrie(routerId: string): TrieNode | undefined {
|
|
162
165
|
return perRouterTrieMap.get(routerId);
|
|
163
166
|
}
|
|
164
167
|
|
|
@@ -199,7 +202,7 @@ export async function ensureRouterManifest(routerId: string): Promise<void> {
|
|
|
199
202
|
if (loader) {
|
|
200
203
|
const mod = await loader();
|
|
201
204
|
if (mod.trie) {
|
|
202
|
-
|
|
205
|
+
setRouterTrie(routerId, mod.trie);
|
|
203
206
|
// A trie serialized into the build manifest comes from complete
|
|
204
207
|
// discovery — misses are authoritative 404s (see find-match.ts).
|
|
205
208
|
markRouterTrieAuthoritative(routerId);
|
package/src/router/basename.ts
CHANGED
|
@@ -12,3 +12,23 @@ export function normalizeBasename(basename?: string): string | undefined {
|
|
|
12
12
|
const trimmed = basename.replace(/^\/+|\/+$/g, "");
|
|
13
13
|
return trimmed ? "/" + trimmed : undefined;
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Prefix a root-relative path with the basename unless it already carries it
|
|
18
|
+
* (the basename followed by "/", "?", "#" or the end), so "/admin?tab=x" and
|
|
19
|
+
* "/admin#frag" under basename "/admin" are not prefixed twice. Shared by
|
|
20
|
+
* redirect(), rewrite() and useRouter().
|
|
21
|
+
*/
|
|
22
|
+
export function prefixBasename(path: string, basename?: string): string {
|
|
23
|
+
if (
|
|
24
|
+
!basename ||
|
|
25
|
+
!path.startsWith("/") ||
|
|
26
|
+
path === basename ||
|
|
27
|
+
path.startsWith(basename + "/") ||
|
|
28
|
+
path.startsWith(basename + "?") ||
|
|
29
|
+
path.startsWith(basename + "#")
|
|
30
|
+
) {
|
|
31
|
+
return path;
|
|
32
|
+
}
|
|
33
|
+
return path === "/" ? basename : basename + path;
|
|
34
|
+
}
|
|
@@ -30,6 +30,7 @@ import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
|
30
30
|
import { PRERENDER_PASSTHROUGH } from "../prerender.js";
|
|
31
31
|
import { substitutePatternParams } from "./substitute-pattern-params.js";
|
|
32
32
|
import { fireAndForgetWaitUntil } from "../types/request-scope.js";
|
|
33
|
+
import { defineRewrittenFrom } from "../server/rewrite-trail.js";
|
|
33
34
|
|
|
34
35
|
// Mutating Headers methods guarded so they throw inside "use cache" / cache()
|
|
35
36
|
// scope. Module-level constant (read-only, .has() lookups) so it is allocated
|
|
@@ -87,19 +88,7 @@ export function guardHeaderWrites(
|
|
|
87
88
|
});
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
|
|
91
|
-
* Strip internal _rsc* query params from a URL.
|
|
92
|
-
* Returns a new URL with only user-facing params.
|
|
93
|
-
*/
|
|
94
|
-
export function stripInternalParams(url: URL): URL {
|
|
95
|
-
const clean = new URL(url);
|
|
96
|
-
for (const key of [...clean.searchParams.keys()]) {
|
|
97
|
-
if (key.startsWith("_rsc")) {
|
|
98
|
-
clean.searchParams.delete(key);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return clean;
|
|
102
|
-
}
|
|
91
|
+
export { stripInternalParams } from "./internal-params.js";
|
|
103
92
|
|
|
104
93
|
/**
|
|
105
94
|
* Resolve route name with namespace prefix support.
|
|
@@ -362,6 +351,7 @@ export function createHandlerContext<TEnv>(
|
|
|
362
351
|
(ctx as { [THEME_REQUEST_CONTEXT]?: unknown })[THEME_REQUEST_CONTEXT] =
|
|
363
352
|
requestContext;
|
|
364
353
|
Object.defineProperty(ctx, "theme", THEME_DESCRIPTOR);
|
|
354
|
+
defineRewrittenFrom(ctx, requestContext);
|
|
365
355
|
// Brand with taint symbol so "use cache" excludes ctx from cache keys
|
|
366
356
|
(ctx as any)[NOCACHE_SYMBOL] = true;
|
|
367
357
|
return ctx;
|
|
@@ -262,13 +262,23 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
262
262
|
|
|
263
263
|
latchPprHeaderScopeForInterceptTarget(interceptEntry);
|
|
264
264
|
|
|
265
|
+
// The intercept segment's id: the tag scope its handler and layout record in
|
|
266
|
+
// (#957, cache-tag.ts runInSegmentTagScope) and the owner of its handler's
|
|
267
|
+
// and loaders' handle pushes, pinned before the loader kickoffs as
|
|
268
|
+
// fresh.ts/revalidation.ts pin theirs (createLoaderExecutor captures it at
|
|
269
|
+
// kickoff; a handler's ctx.use(Handle) reads it when called).
|
|
270
|
+
const slotSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
|
|
271
|
+
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
272
|
+
slotSegmentId;
|
|
273
|
+
const hasLoaders = interceptEntry.loader.length > 0;
|
|
274
|
+
|
|
265
275
|
const loaderPromises: Promise<any>[] = [];
|
|
266
276
|
const loaderIds: string[] = [];
|
|
267
277
|
|
|
268
278
|
for (let i = 0; i < interceptEntry.loader.length; i++) {
|
|
269
279
|
const loaderEntry = interceptEntry.loader[i];
|
|
270
280
|
const { loader, revalidate: loaderRevalidateFns } = loaderEntry;
|
|
271
|
-
const segmentId = `${
|
|
281
|
+
const segmentId = `${slotSegmentId}D${i}.${loader.$$id}`;
|
|
272
282
|
|
|
273
283
|
if (revalidationContext) {
|
|
274
284
|
const {
|
|
@@ -282,8 +292,7 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
282
292
|
stale,
|
|
283
293
|
} = revalidationContext;
|
|
284
294
|
|
|
285
|
-
|
|
286
|
-
if (clientSegmentIds.has(interceptSegmentId)) {
|
|
295
|
+
if (clientSegmentIds.has(slotSegmentId)) {
|
|
287
296
|
const dummySegment: ResolvedSegment = {
|
|
288
297
|
id: segmentId,
|
|
289
298
|
namespace: `intercept:${interceptEntry.routeName}`,
|
|
@@ -331,8 +340,9 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
331
340
|
deps.wrapLoaderPromise(
|
|
332
341
|
// The DSL binding's funnel, as for route loaders: a loader's own
|
|
333
342
|
// cache() applies here too.
|
|
334
|
-
runInsideLoaderScope(
|
|
335
|
-
resolveLoaderData(loaderEntry, context, context.pathname),
|
|
343
|
+
runInsideLoaderScope(
|
|
344
|
+
() => resolveLoaderData(loaderEntry, context, context.pathname),
|
|
345
|
+
loader.$$id,
|
|
336
346
|
),
|
|
337
347
|
parentEntry,
|
|
338
348
|
segmentId,
|
|
@@ -344,9 +354,6 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
344
354
|
);
|
|
345
355
|
}
|
|
346
356
|
|
|
347
|
-
// The intercept segment's id, and the tag scope its handler and layout
|
|
348
|
-
// record in (#957, cache-tag.ts runInSegmentTagScope).
|
|
349
|
-
const slotSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
|
|
350
357
|
let handlerResult: ReactNode;
|
|
351
358
|
try {
|
|
352
359
|
const handler = interceptEntry.handler;
|
|
@@ -398,7 +405,7 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
398
405
|
const streamedComponent = (promise: Promise<unknown>): ReactNode =>
|
|
399
406
|
trackStreamedHandler(deps, promise, {
|
|
400
407
|
boundaryEntry: parentEntry,
|
|
401
|
-
segmentId:
|
|
408
|
+
segmentId: slotSegmentId,
|
|
402
409
|
segmentType: "parallel",
|
|
403
410
|
context,
|
|
404
411
|
routeKey: interceptEntry.routeName,
|
|
@@ -441,6 +448,13 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
441
448
|
}
|
|
442
449
|
}
|
|
443
450
|
|
|
451
|
+
// The aggregate lists the loaders that ran; empty, not absent, when the
|
|
452
|
+
// client holds the slot and all skipped. Absent replaces the client's
|
|
453
|
+
// aggregate, empty merges into it (merge-segment-loaders.ts).
|
|
454
|
+
if (hasLoaders && loaderPromises.length === 0) {
|
|
455
|
+
loaderDataPromise = [];
|
|
456
|
+
}
|
|
457
|
+
|
|
444
458
|
const interceptSegment = {
|
|
445
459
|
id: slotSegmentId,
|
|
446
460
|
namespace: `intercept:${interceptEntry.routeName}`,
|
|
@@ -454,7 +468,7 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
454
468
|
belongsToRoute,
|
|
455
469
|
parallelName: `intercept:${interceptEntry.routeName}.${interceptEntry.slotName}`,
|
|
456
470
|
loaderDataPromise,
|
|
457
|
-
loaderIds:
|
|
471
|
+
loaderIds: hasLoaders ? loaderIds : undefined,
|
|
458
472
|
};
|
|
459
473
|
segments.push(interceptSegment);
|
|
460
474
|
|
|
@@ -497,6 +511,10 @@ export async function resolveInterceptLoadersOnly<TEnv>(
|
|
|
497
511
|
|
|
498
512
|
latchPprHeaderScopeForInterceptTarget(interceptEntry);
|
|
499
513
|
|
|
514
|
+
// The loaders' handle push owner, as in resolveInterceptEntry.
|
|
515
|
+
const slotSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
|
|
516
|
+
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
517
|
+
slotSegmentId;
|
|
500
518
|
const loaderPromises: Promise<any>[] = [];
|
|
501
519
|
const loaderIds: string[] = [];
|
|
502
520
|
|
|
@@ -514,10 +532,9 @@ export async function resolveInterceptLoadersOnly<TEnv>(
|
|
|
514
532
|
for (let i = 0; i < interceptEntry.loader.length; i++) {
|
|
515
533
|
const loaderEntry = interceptEntry.loader[i];
|
|
516
534
|
const { loader, revalidate: loaderRevalidateFns } = loaderEntry;
|
|
517
|
-
const segmentId = `${
|
|
535
|
+
const segmentId = `${slotSegmentId}D${i}.${loader.$$id}`;
|
|
518
536
|
|
|
519
|
-
|
|
520
|
-
if (clientSegmentIds.has(interceptSegmentId)) {
|
|
537
|
+
if (clientSegmentIds.has(slotSegmentId)) {
|
|
521
538
|
const dummySegment: ResolvedSegment = {
|
|
522
539
|
id: segmentId,
|
|
523
540
|
namespace: `intercept:${interceptEntry.routeName}`,
|
|
@@ -564,8 +581,9 @@ export async function resolveInterceptLoadersOnly<TEnv>(
|
|
|
564
581
|
deps.wrapLoaderPromise(
|
|
565
582
|
// The DSL binding's funnel, as for route loaders: a loader's own
|
|
566
583
|
// cache() applies here too.
|
|
567
|
-
runInsideLoaderScope(
|
|
568
|
-
resolveLoaderData(loaderEntry, context, context.pathname),
|
|
584
|
+
runInsideLoaderScope(
|
|
585
|
+
() => resolveLoaderData(loaderEntry, context, context.pathname),
|
|
586
|
+
loader.$$id,
|
|
569
587
|
),
|
|
570
588
|
parentEntry,
|
|
571
589
|
segmentId,
|
|
@@ -579,8 +597,9 @@ export async function resolveInterceptLoadersOnly<TEnv>(
|
|
|
579
597
|
);
|
|
580
598
|
}
|
|
581
599
|
|
|
600
|
+
// Every loader held: the empty aggregate (see resolveInterceptEntry).
|
|
582
601
|
if (loaderPromises.length === 0) {
|
|
583
|
-
return
|
|
602
|
+
return { loaderDataPromise: [], loaderIds };
|
|
584
603
|
}
|
|
585
604
|
|
|
586
605
|
// Match fresh-path semantics: only defer (no await) when loading is truthy.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip internal _rsc* query params from a URL.
|
|
3
|
+
* Returns a new URL with only user-facing params.
|
|
4
|
+
*
|
|
5
|
+
* A leaf, so request-scoped modules (server/rewrite-trail.ts) can strip
|
|
6
|
+
* without importing the handler context's graph.
|
|
7
|
+
*/
|
|
8
|
+
export function stripInternalParams(url: URL): URL {
|
|
9
|
+
const clean = new URL(url);
|
|
10
|
+
for (const key of [...clean.searchParams.keys()]) {
|
|
11
|
+
if (key.startsWith("_rsc")) {
|
|
12
|
+
clean.searchParams.delete(key);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return clean;
|
|
16
|
+
}
|
package/src/router/is-action.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared `isAction()` matcher for revalidate predicates (server
|
|
2
|
+
* Shared `isAction()` matcher for revalidate() predicates (server) and
|
|
3
|
+
* transition({ when }) (browser).
|
|
3
4
|
*
|
|
4
5
|
* Action identity is `actionId`. The helper resolves an imported reference the
|
|
5
6
|
* same way the action boundary derives that id (`$id ?? $$id`), so a
|
|
@@ -55,6 +55,13 @@ import {
|
|
|
55
55
|
} from "../server/handle-store.js";
|
|
56
56
|
import { getFetchableLoader } from "../server/fetchable-loader-store.js";
|
|
57
57
|
import { _getRequestContext } from "../server/request-context.js";
|
|
58
|
+
import { defineRewrittenFrom } from "../server/rewrite-trail.js";
|
|
59
|
+
import {
|
|
60
|
+
firstUnitCandidate,
|
|
61
|
+
loaderOwner,
|
|
62
|
+
startDeferredLoader,
|
|
63
|
+
type PrefetchDeferral,
|
|
64
|
+
} from "./segment-resolution/prefetch-deferral.js";
|
|
58
65
|
import {
|
|
59
66
|
assertNonCacheableReadAllowed,
|
|
60
67
|
isInsideLoaderScope,
|
|
@@ -372,6 +379,29 @@ function detectLoaderCycle(
|
|
|
372
379
|
return dfs(to);
|
|
373
380
|
}
|
|
374
381
|
|
|
382
|
+
const warnedRenderedBesideUnit = new Set<string>();
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Dev only, once per loader: a loader that is not deferrable awaits
|
|
386
|
+
* ctx.rendered() in a tree where a prefetch can skip a handler. The prefetch
|
|
387
|
+
* rejects that call (see rendered() below); saying so on a document or a
|
|
388
|
+
* navigation shows it before production, where prefetch is automatic.
|
|
389
|
+
*/
|
|
390
|
+
function warnRenderedBesideUnit(
|
|
391
|
+
deferral: PrefetchDeferral,
|
|
392
|
+
loaderId: string,
|
|
393
|
+
): void {
|
|
394
|
+
if (warnedRenderedBesideUnit.has(loaderId)) return;
|
|
395
|
+
const unit = firstUnitCandidate(deferral);
|
|
396
|
+
if (!unit) return;
|
|
397
|
+
warnedRenderedBesideUnit.add(loaderId);
|
|
398
|
+
console.warn(
|
|
399
|
+
`[rango] loader "${loaderId}" awaits ctx.rendered() on a route where "${unit.shortCode}" (${unit.id}) has loading(fallback, { prefetch: false }). ` +
|
|
400
|
+
`A prefetch of this route skips that handler, so ctx.rendered() throws in the prefetch and this loader is an error on the click that adopts it. ` +
|
|
401
|
+
`Remove { prefetch: false } from that loading(), or stop calling ctx.rendered() in this loader.`,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
375
405
|
/**
|
|
376
406
|
* Creates a memoizing loader executor with cycle detection.
|
|
377
407
|
* Shared by setupLoaderAccess and setupLoaderAccessSilent; only the handle
|
|
@@ -397,7 +427,9 @@ function createLoaderExecutor<TEnv>(
|
|
|
397
427
|
/**
|
|
398
428
|
* A stale refresh's executor (setupLoaderAccess _runLoaderIsolated): its
|
|
399
429
|
* runs' pushes are diverted from the page's store, so a run's end says
|
|
400
|
-
* nothing about the page's replayed values (HandleStore.settleLoaderRun)
|
|
430
|
+
* nothing about the page's replayed values (HandleStore.settleLoaderRun),
|
|
431
|
+
* and it never starts a loader the page's prefetch deferred
|
|
432
|
+
* (startDeferredLoader): a refresh reads each loader fresh.
|
|
401
433
|
*/
|
|
402
434
|
detached?: boolean;
|
|
403
435
|
/**
|
|
@@ -472,7 +504,14 @@ function createLoaderExecutor<TEnv>(
|
|
|
472
504
|
const bound =
|
|
473
505
|
callerLoaderId !== null &&
|
|
474
506
|
internal._loaderCacheOverrides?.get(loader.$$id);
|
|
475
|
-
|
|
507
|
+
// A loader the prefetch deferred starts through its registration, cache()
|
|
508
|
+
// binding included, as a navigation's run of it does.
|
|
509
|
+
const memo =
|
|
510
|
+
bound ||
|
|
511
|
+
loaderPromises.get(loader.$$id) ||
|
|
512
|
+
(executorOptions?.detached
|
|
513
|
+
? undefined
|
|
514
|
+
: startDeferredLoader(ctx, loader.$$id));
|
|
476
515
|
if (memo) {
|
|
477
516
|
if (recordTags) readValueTags(memo);
|
|
478
517
|
// A "use cache" body reading it refuses an identity read the run made
|
|
@@ -511,8 +550,9 @@ function createLoaderExecutor<TEnv>(
|
|
|
511
550
|
// resolution continues — by the time the async body pushes, it would be
|
|
512
551
|
// stale. Attributing to the owning route/layout segment gives handle
|
|
513
552
|
// pushes the same granularity handler pushes have (segmentOrder-correct
|
|
514
|
-
// for breadcrumb accumulation).
|
|
515
|
-
|
|
553
|
+
// for breadcrumb accumulation). A loader a prefetch deferred and a reader
|
|
554
|
+
// starts anyway keeps the segment that registers it (loaderOwner).
|
|
555
|
+
const owningSegmentId = loaderOwner(ctx, loader.$$id);
|
|
516
556
|
|
|
517
557
|
let renderedResolved = false;
|
|
518
558
|
let renderedPromise: Promise<void> | null = null;
|
|
@@ -633,6 +673,32 @@ function createLoaderExecutor<TEnv>(
|
|
|
633
673
|
);
|
|
634
674
|
}
|
|
635
675
|
|
|
676
|
+
// prefetch: false (segment-resolution/prefetch-deferral.ts). A
|
|
677
|
+
// deferrable loader runs in a fill request, where its entry is held
|
|
678
|
+
// and its handler does not run: there is no render to wait for.
|
|
679
|
+
// Refused on every request kind, so it is seen on a document load.
|
|
680
|
+
const deferral = internal._prefetchDeferral;
|
|
681
|
+
if (deferral?.scope.loaderIds.has(currentLoaderId)) {
|
|
682
|
+
throw new Error(
|
|
683
|
+
`ctx.rendered() is not available to loader "${currentLoaderId}": it is deferred in prefetches ` +
|
|
684
|
+
`(registered with loader(Def, { prefetch: false }), or behind a loading(fallback, { prefetch: false })), ` +
|
|
685
|
+
`and the fill request that runs it afterwards does not re-run its entry's handler, so there is no render to wait for and no handle data to read. ` +
|
|
686
|
+
`Remove { prefetch: false } from the loader (or from the loading() above it), or stop calling ctx.rendered() in this loader.`,
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
if (deferral?.mode === "fill") {
|
|
690
|
+
throw new Error(
|
|
691
|
+
`ctx.rendered() is not available to loader "${currentLoaderId}" in a fill request: the handlers the client already holds do not run, so their handle data is missing.`,
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
if (
|
|
695
|
+
process.env.NODE_ENV !== "production" &&
|
|
696
|
+
deferral &&
|
|
697
|
+
!deferral.mode
|
|
698
|
+
) {
|
|
699
|
+
warnRenderedBesideUnit(deferral, currentLoaderId);
|
|
700
|
+
}
|
|
701
|
+
|
|
636
702
|
// awaitBeforeFlush cycle: segment resolution awaits this loader
|
|
637
703
|
// (loader(Def, { ssr: false })), the barrier awaits segment
|
|
638
704
|
// resolution, and rendered() awaits the barrier — waiting here can
|
|
@@ -684,6 +750,17 @@ function createLoaderExecutor<TEnv>(
|
|
|
684
750
|
// keeps a handler from depending on this loader.
|
|
685
751
|
const streaming = reqCtx._treeHasStreaming === true;
|
|
686
752
|
renderedPromise = reqCtx._renderBarrier.then(async () => {
|
|
753
|
+
// A prefetch that skipped a handler unit has none of that handler's
|
|
754
|
+
// pushes, and the fill never re-runs this loader: an error, never
|
|
755
|
+
// a handle list with a hole in it. Known only once resolution has
|
|
756
|
+
// decided the unit, which is what the barrier waits for.
|
|
757
|
+
if (deferral?.mode === "prefetch" && deferral.deferredUnit) {
|
|
758
|
+
throw new Error(
|
|
759
|
+
`ctx.rendered() in loader "${currentLoaderId}" cannot settle in a prefetch of this route: segment "${deferral.deferredUnit}" has loading(fallback, { prefetch: false }), ` +
|
|
760
|
+
`so the prefetch skipped its handler and its handle data does not exist. ` +
|
|
761
|
+
`Remove { prefetch: false } from that loading(), or stop calling ctx.rendered() in this loader.`,
|
|
762
|
+
);
|
|
763
|
+
}
|
|
687
764
|
if (streaming) {
|
|
688
765
|
reqCtx._handleStore.seal();
|
|
689
766
|
await reqCtx._handleStore.settled;
|
|
@@ -701,6 +778,7 @@ function createLoaderExecutor<TEnv>(
|
|
|
701
778
|
return renderedPromise;
|
|
702
779
|
},
|
|
703
780
|
};
|
|
781
|
+
defineRewrittenFrom(loaderCtx, reqCtxRef ?? _getRequestContext());
|
|
704
782
|
(loaderCtx as any)[NOCACHE_SYMBOL] = true;
|
|
705
783
|
|
|
706
784
|
// Meter this loader once via observePhase (loader:<id> perf metric +
|
|
@@ -807,6 +885,9 @@ export function setupLoaderAccess<TEnv>(
|
|
|
807
885
|
// pushes, and a later run here replaces them (HandleStore.pushReplayed).
|
|
808
886
|
const claimed = new Set<string>();
|
|
809
887
|
const internal = ctx as InternalHandlerContext<any, TEnv>;
|
|
888
|
+
internal._loaderStarted = (loaderId) =>
|
|
889
|
+
loaderPromises.has(loaderId) ||
|
|
890
|
+
internal._loaderCacheOverrides?.has(loaderId) === true;
|
|
810
891
|
internal._claimLoaderPushes = (loaderId) => {
|
|
811
892
|
if (loaderPromises.has(loaderId) || claimed.has(loaderId)) return false;
|
|
812
893
|
claimed.add(loaderId);
|
package/src/router/match-api.ts
CHANGED
|
@@ -32,7 +32,12 @@ import {
|
|
|
32
32
|
getContext,
|
|
33
33
|
type InterceptSelectorContext,
|
|
34
34
|
} from "../server/context";
|
|
35
|
-
import type {
|
|
35
|
+
import type {
|
|
36
|
+
ErrorBoundaryHandler,
|
|
37
|
+
ErrorInfo,
|
|
38
|
+
InternalHandlerContext,
|
|
39
|
+
MatchResult,
|
|
40
|
+
} from "../types";
|
|
36
41
|
import type { ReactNode } from "react";
|
|
37
42
|
import type { MatchContext } from "./match-context.js";
|
|
38
43
|
import type { MatchApiDeps, ActionContext } from "./types.js";
|
|
@@ -49,6 +54,11 @@ import {
|
|
|
49
54
|
type RouteSnapshot,
|
|
50
55
|
} from "./route-snapshot.js";
|
|
51
56
|
import { resolveNavigation } from "./navigation-snapshot.js";
|
|
57
|
+
import {
|
|
58
|
+
partialDeferralMode,
|
|
59
|
+
planPrefetchDeferral,
|
|
60
|
+
requestKind,
|
|
61
|
+
} from "./segment-resolution/prefetch-deferral.js";
|
|
52
62
|
|
|
53
63
|
/**
|
|
54
64
|
* Create match context for full requests (document/SSR).
|
|
@@ -174,6 +184,9 @@ export async function createMatchContextForFull<TEnv>(
|
|
|
174
184
|
if (reqCtx?._shellCaptureRun) {
|
|
175
185
|
reqCtx._shellCaptureLoaderLanes = routeLoaderLanes(snapshot.entries);
|
|
176
186
|
}
|
|
187
|
+
// A document never defers; the plan feeds the ctx.rendered() guards.
|
|
188
|
+
(handlerContext as InternalHandlerContext<any, TEnv>)._prefetchDeferral =
|
|
189
|
+
planPrefetchDeferral(snapshot.entries, undefined, matched, cacheScope);
|
|
177
190
|
|
|
178
191
|
return {
|
|
179
192
|
request,
|
|
@@ -453,6 +466,28 @@ export async function createMatchContextForPartial<TEnv>(
|
|
|
453
466
|
if (cacheScope) armRecordTagOwners();
|
|
454
467
|
if (bindsLoaderCache(snapshot.entries)) armLoaderTagSets();
|
|
455
468
|
|
|
469
|
+
// `prefetch: false` (segment-resolution/prefetch-deferral.ts). With no
|
|
470
|
+
// request context (a match run on its own) the request is asked directly.
|
|
471
|
+
const kind = reqCtx ? reqCtx._requestKind : requestKind(request, rawUrl);
|
|
472
|
+
const deferral = planPrefetchDeferral(
|
|
473
|
+
snapshot.entries,
|
|
474
|
+
partialDeferralMode({
|
|
475
|
+
fill: kind === "fill",
|
|
476
|
+
prefetch: kind === "prefetch",
|
|
477
|
+
method: request.method,
|
|
478
|
+
isAction,
|
|
479
|
+
isIntercept,
|
|
480
|
+
isShellCapture: reqCtx?._shellCaptureRun === true,
|
|
481
|
+
}),
|
|
482
|
+
matched,
|
|
483
|
+
cacheScope,
|
|
484
|
+
// As the request listed them, not the copy forced above: see
|
|
485
|
+
// prefetch-deferral.ts.
|
|
486
|
+
nav.clientSegmentSet,
|
|
487
|
+
);
|
|
488
|
+
(handlerContext as InternalHandlerContext<any, TEnv>)._prefetchDeferral =
|
|
489
|
+
deferral;
|
|
490
|
+
|
|
456
491
|
return {
|
|
457
492
|
request,
|
|
458
493
|
url: cleanUrl,
|