@rangojs/router 0.5.2 → 0.7.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 +343 -125
- package/dist/types/browser/react/use-router.d.ts +10 -3
- package/dist/types/browser/react/use-search-params.d.ts +57 -10
- package/dist/types/browser/types.d.ts +22 -0
- package/dist/types/build/merge-full-manifests.d.ts +3 -0
- package/dist/types/build/route-trie.d.ts +4 -73
- package/dist/types/build/route-types/per-module-writer.d.ts +6 -4
- package/dist/types/build/route-types/router-processing.d.ts +2 -3
- package/dist/types/cache/cache-exec-scope.d.ts +31 -0
- package/dist/types/cache/taint.d.ts +12 -6
- package/dist/types/client-urls/client-root.d.ts +38 -0
- package/dist/types/client-urls/client-urls.d.ts +5 -0
- package/dist/types/client-urls/navigation.d.ts +38 -0
- package/dist/types/client-urls/revalidation-protocol.d.ts +25 -0
- package/dist/types/client-urls/server-projection.d.ts +71 -0
- package/dist/types/client-urls/types.d.ts +147 -0
- package/dist/types/client.d.ts +12 -4
- package/dist/types/client.rsc.d.ts +4 -1
- package/dist/types/decode-loader-results.d.ts +37 -0
- package/dist/types/errors.d.ts +1 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/loader-redirect.d.ts +27 -0
- package/dist/types/outlet-context.d.ts +12 -0
- package/dist/types/outlet-provider.d.ts +3 -1
- package/dist/types/redirect-origin.d.ts +4 -0
- package/dist/types/route-content-wrapper.d.ts +42 -1
- package/dist/types/route-definition/helpers-types.d.ts +13 -2
- package/dist/types/router/error-handling.d.ts +35 -1
- package/dist/types/router/intercept-resolution.d.ts +12 -0
- package/dist/types/router/loader-resolution.d.ts +24 -2
- package/dist/types/router/revalidation.d.ts +7 -0
- package/dist/types/router/route-trie-builder.d.ts +77 -0
- package/dist/types/router/router-interfaces.d.ts +20 -0
- package/dist/types/router/segment-resolution/helpers.d.ts +1 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -9
- package/dist/types/router/trie-matching.d.ts +1 -1
- package/dist/types/rsc/manifest-init.d.ts +5 -5
- package/dist/types/rsc/shell-capture.d.ts +9 -0
- package/dist/types/rsc/shell-serve.d.ts +11 -0
- package/dist/types/rsc/types.d.ts +30 -0
- package/dist/types/segment-system.d.ts +2 -0
- package/dist/types/server/context.d.ts +10 -0
- package/dist/types/server/handle-store.d.ts +34 -3
- package/dist/types/server/request-context.d.ts +11 -1
- package/dist/types/server.d.ts +1 -0
- package/dist/types/ssr/index.d.ts +22 -0
- package/dist/types/ssr/ssr-root.d.ts +10 -0
- package/dist/types/testing/dom.entry.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +16 -6
- package/dist/types/testing/run-loader.d.ts +9 -0
- package/dist/types/types/boundaries.d.ts +22 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/loader-types.d.ts +57 -5
- package/dist/types/types/segments.d.ts +7 -0
- package/dist/types/urls/path-helper-types.d.ts +13 -4
- package/dist/types/vite/discovery/client-urls-projection.d.ts +53 -0
- package/dist/types/vite/discovery/discover-routers.d.ts +1 -1
- package/dist/types/vite/discovery/state.d.ts +8 -1
- package/dist/types/vite/plugins/client-ref-dedup.d.ts +0 -11
- package/dist/vite/index.js +6159 -2959
- package/package.json +6 -5
- package/skills/breadcrumbs/SKILL.md +39 -9
- package/skills/catalog.json +7 -1
- package/skills/client-urls/SKILL.md +338 -0
- package/skills/comparison/references/framework-comparison.md +23 -9
- package/skills/hooks/SKILL.md +2 -2
- package/skills/hooks/data.md +11 -2
- package/skills/hooks/handle-and-actions.md +7 -0
- package/skills/hooks/outlets.md +26 -5
- package/skills/hooks/urls.md +40 -3
- package/skills/loader/SKILL.md +132 -20
- package/skills/migrate-nextjs/SKILL.md +70 -10
- package/skills/migrate-react-router/SKILL.md +49 -13
- package/skills/migrate-react-router/component-migration.md +18 -13
- package/skills/migrate-react-router/data-and-actions.md +14 -3
- package/skills/migrate-react-router/route-mapping.md +15 -2
- package/skills/parallel/SKILL.md +32 -1
- package/skills/ppr/SKILL.md +16 -6
- package/skills/prerender/SKILL.md +8 -4
- package/skills/rango/SKILL.md +21 -17
- package/skills/react-compiler/SKILL.md +3 -3
- package/skills/route/SKILL.md +5 -2
- package/skills/router-setup/SKILL.md +16 -2
- package/skills/scripts/SKILL.md +16 -6
- package/skills/shell-manifest/SKILL.md +16 -7
- package/skills/testing/SKILL.md +2 -2
- package/skills/testing/client-components.md +6 -0
- package/skills/testing/handles.md +30 -8
- package/skills/testing/loader.md +51 -49
- package/skills/testing/middleware.md +1 -1
- package/skills/theme/SKILL.md +8 -5
- package/src/bin/rango.ts +7 -3
- package/src/browser/navigation-bridge.ts +6 -0
- package/src/browser/navigation-client.ts +5 -0
- package/src/browser/partial-update.ts +65 -13
- package/src/browser/react/use-router.ts +40 -11
- package/src/browser/react/use-search-params.ts +140 -17
- package/src/browser/rsc-router.tsx +59 -0
- package/src/browser/server-action-bridge.ts +26 -0
- package/src/browser/types.ts +22 -0
- package/src/build/merge-full-manifests.ts +161 -0
- package/src/build/route-trie.ts +9 -332
- package/src/build/route-types/include-resolution.ts +66 -11
- package/src/build/route-types/per-module-writer.ts +11 -6
- package/src/build/route-types/router-processing.ts +184 -153
- package/src/build/runtime-discovery.ts +23 -12
- package/src/cache/cache-exec-scope.ts +47 -0
- package/src/cache/cache-runtime.ts +24 -25
- package/src/cache/taint.ts +28 -9
- package/src/client-urls/client-root.tsx +168 -0
- package/src/client-urls/client-urls.ts +776 -0
- package/src/client-urls/navigation.ts +237 -0
- package/src/client-urls/revalidation-protocol.ts +56 -0
- package/src/client-urls/server-projection.ts +670 -0
- package/src/client-urls/types.ts +201 -0
- package/src/client.rsc.tsx +12 -0
- package/src/client.tsx +49 -6
- package/src/decode-loader-results.ts +113 -0
- package/src/errors.ts +14 -0
- package/src/handles/deferred-resolution.ts +14 -7
- package/src/index.ts +1 -0
- package/src/loader-redirect.tsx +64 -0
- package/src/outlet-context.ts +12 -0
- package/src/outlet-provider.tsx +15 -1
- package/src/redirect-origin.ts +29 -0
- package/src/route-content-wrapper.tsx +96 -3
- package/src/route-definition/dsl-helpers.ts +28 -3
- package/src/route-definition/helpers-types.ts +13 -0
- package/src/route-definition/redirect.ts +17 -18
- package/src/router/error-handling.ts +65 -11
- package/src/router/intercept-resolution.ts +29 -0
- package/src/router/loader-resolution.ts +261 -28
- package/src/router/match-result.ts +7 -0
- package/src/router/revalidation.ts +24 -11
- package/src/router/route-trie-builder.ts +334 -0
- package/src/router/router-interfaces.ts +38 -0
- package/src/router/segment-resolution/fresh.ts +55 -2
- package/src/router/segment-resolution/helpers.ts +9 -11
- package/src/router/segment-resolution/loader-cache.ts +34 -23
- package/src/router/segment-resolution/loader-mask.ts +9 -9
- package/src/router/segment-resolution/revalidation.ts +23 -2
- package/src/router/trie-matching.ts +3 -3
- package/src/router.ts +46 -1
- package/src/rsc/full-payload.ts +6 -0
- package/src/rsc/handler.ts +10 -7
- package/src/rsc/loader-fetch.ts +2 -2
- package/src/rsc/manifest-init.ts +28 -9
- package/src/rsc/rsc-rendering.ts +15 -1
- package/src/rsc/shell-capture.ts +12 -0
- package/src/rsc/shell-serve.ts +15 -2
- package/src/rsc/ssr-setup.ts +10 -1
- package/src/rsc/types.ts +31 -2
- package/src/segment-system.tsx +83 -26
- package/src/server/context.ts +10 -0
- package/src/server/cookie-store.ts +19 -19
- package/src/server/handle-store.ts +185 -48
- package/src/server/request-context.ts +30 -6
- package/src/server.ts +7 -0
- package/src/ssr/index.tsx +37 -2
- package/src/ssr/ssr-root.tsx +29 -2
- package/src/testing/dom.entry.ts +1 -1
- package/src/testing/render-route.tsx +22 -8
- package/src/testing/run-loader.ts +51 -13
- package/src/types/boundaries.ts +19 -0
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +60 -5
- package/src/types/segments.ts +7 -0
- package/src/urls/include-helper.ts +22 -4
- package/src/urls/path-helper-types.ts +17 -1
- package/src/use-loader.tsx +67 -6
- package/src/vite/discovery/client-urls-projection.ts +322 -0
- package/src/vite/discovery/discover-routers.ts +43 -17
- package/src/vite/discovery/state.ts +11 -1
- package/src/vite/discovery/virtual-module-codegen.ts +20 -0
- package/src/vite/plugins/client-ref-dedup.ts +281 -19
- package/src/vite/plugins/expose-action-id.ts +45 -23
- package/src/vite/plugins/virtual-entries.ts +12 -3
- package/src/vite/router-discovery.ts +163 -12
package/src/redirect-origin.ts
CHANGED
|
@@ -134,6 +134,35 @@ export function isExternalRedirect(response: Response): boolean {
|
|
|
134
134
|
return externalRedirects.has(response);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Out-of-band carrier for `redirect(url, { state })` location state, keyed on
|
|
139
|
+
* the Response object like the external brand above. Needed by the LOADER
|
|
140
|
+
* lane: streaming loaders settle AFTER attachLocationStateIfPresent stamped
|
|
141
|
+
* payload metadata (rsc-rendering.ts runs it between payload prep and Flight
|
|
142
|
+
* serialization), so request-context state set at throw time misses the
|
|
143
|
+
* payload. The thrown Response IS the object loader-resolution catches — no
|
|
144
|
+
* rebuild path intervenes — so a WeakMap entry survives to the catch site,
|
|
145
|
+
* where it is serialized onto the loader-result redirect marker and delivered
|
|
146
|
+
* WITH the redirect navigation (LoaderRedirect), merging at the target like
|
|
147
|
+
* an action write. Handler/middleware/action redirects keep the metadata lane.
|
|
148
|
+
*/
|
|
149
|
+
const redirectStates = new WeakMap<Response, Record<string, unknown>>();
|
|
150
|
+
|
|
151
|
+
/** Brand a redirect Response with its RESOLVED location-state record. */
|
|
152
|
+
export function attachRedirectState(
|
|
153
|
+
response: Response,
|
|
154
|
+
state: Record<string, unknown>,
|
|
155
|
+
): void {
|
|
156
|
+
redirectStates.set(response, state);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Read the resolved location-state record off a redirect Response. */
|
|
160
|
+
export function getRedirectState(
|
|
161
|
+
response: Response,
|
|
162
|
+
): Record<string, unknown> | undefined {
|
|
163
|
+
return redirectStates.get(response);
|
|
164
|
+
}
|
|
165
|
+
|
|
137
166
|
/**
|
|
138
167
|
* Reserved internal header name. No longer a trust signal -- the external
|
|
139
168
|
* opt-in is the out-of-band brand above. It is kept only so the redirect-rebuild
|
|
@@ -1,9 +1,64 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
|
-
import { Suspense, use } from "react";
|
|
3
|
+
import { Component, Suspense, use } from "react";
|
|
4
4
|
import { OutletProvider } from "./outlet-provider.js";
|
|
5
5
|
import type { ResolvedSegment } from "./types.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
decodeLoaderResults,
|
|
8
|
+
LOADER_ERROR_FALLBACK,
|
|
9
|
+
LOADER_NOT_FOUND_FALLBACK,
|
|
10
|
+
LOADER_REDIRECT,
|
|
11
|
+
} from "./decode-loader-results.js";
|
|
12
|
+
import { LoaderRedirect } from "./loader-redirect.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Router-owned error boundary for read-site loader errors. segment-system
|
|
16
|
+
* wraps every loader-bearing segment's children in one (unconditionally —
|
|
17
|
+
* streams and forceAwait lanes alike, so the tree shape never differs between
|
|
18
|
+
* navigation lanes; see docs/tree-structure.md). A loader error thrown by a
|
|
19
|
+
* suspending read carries its errorBoundary() fallback via
|
|
20
|
+
* LOADER_ERROR_FALLBACK (decodeLoaderEntry); this boundary renders that node,
|
|
21
|
+
* restoring the pre-streaming errorFallback-swap contract.
|
|
22
|
+
*
|
|
23
|
+
* Loader-thrown AUTHORITY SIGNALS ride sibling markers:
|
|
24
|
+
* - LOADER_NOT_FOUND_FALLBACK (notFound()): renders the SERVER-RENDERED
|
|
25
|
+
* not-found UI carried on the marker — nearest notFoundBoundary → router
|
|
26
|
+
* notFound option — zero extra fetches. Document lane: Fizz emitted the
|
|
27
|
+
* Suspense fallback and replays the throw at hydration, so the swap happens
|
|
28
|
+
* client-side (the HTTP status was already set opportunistically by the
|
|
29
|
+
* producer when the rejection won the flush race).
|
|
30
|
+
* - LOADER_REDIRECT (redirect()): mounts LoaderRedirect, which navigates.
|
|
31
|
+
*
|
|
32
|
+
* Errors without any marker rethrow to the app's own boundaries.
|
|
33
|
+
*/
|
|
34
|
+
export class StreamedLoaderErrorBoundary extends Component<
|
|
35
|
+
{ children: ReactNode },
|
|
36
|
+
{ error: unknown }
|
|
37
|
+
> {
|
|
38
|
+
state: { error: unknown } = { error: null };
|
|
39
|
+
|
|
40
|
+
static getDerivedStateFromError(error: unknown): { error: unknown } {
|
|
41
|
+
return { error };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
render(): ReactNode {
|
|
45
|
+
const { error } = this.state;
|
|
46
|
+
if (error !== null && error !== undefined) {
|
|
47
|
+
const marked = error as Record<PropertyKey, unknown>;
|
|
48
|
+
const notFoundFallback = marked[LOADER_NOT_FOUND_FALLBACK];
|
|
49
|
+
if (notFoundFallback !== undefined) return notFoundFallback as ReactNode;
|
|
50
|
+
const redirect = marked[LOADER_REDIRECT];
|
|
51
|
+
if (redirect !== undefined) {
|
|
52
|
+
const r = redirect as { to: string; state?: Record<string, unknown> };
|
|
53
|
+
return <LoaderRedirect to={r.to} state={r.state} />;
|
|
54
|
+
}
|
|
55
|
+
const fallback = marked[LOADER_ERROR_FALLBACK];
|
|
56
|
+
if (fallback !== undefined) return fallback as ReactNode;
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
return this.props.children;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
7
62
|
|
|
8
63
|
/**
|
|
9
64
|
* Stable async wrapper component for route content
|
|
@@ -67,6 +122,13 @@ const Suspender = ({
|
|
|
67
122
|
export interface LoaderBoundaryProps {
|
|
68
123
|
loaderDataPromise: Promise<any[]> | any[];
|
|
69
124
|
loaderIds: string[];
|
|
125
|
+
/**
|
|
126
|
+
* SPIKE (streaming useLoader): per-loader UNDECODED results from the
|
|
127
|
+
* producer (values or individually-pending promises). When present, the
|
|
128
|
+
* resolver passes them through instead of resolving the aggregate above
|
|
129
|
+
* the children; useLoader suspends per loader at the read site.
|
|
130
|
+
*/
|
|
131
|
+
loaderStreams?: Record<string, unknown>;
|
|
70
132
|
fallback?: ReactNode;
|
|
71
133
|
outletKey: string;
|
|
72
134
|
outletContent: ReactNode;
|
|
@@ -78,6 +140,7 @@ export interface LoaderBoundaryProps {
|
|
|
78
140
|
export function LoaderBoundary({
|
|
79
141
|
loaderDataPromise,
|
|
80
142
|
loaderIds,
|
|
143
|
+
loaderStreams,
|
|
81
144
|
fallback,
|
|
82
145
|
outletKey,
|
|
83
146
|
outletContent,
|
|
@@ -90,6 +153,7 @@ export function LoaderBoundary({
|
|
|
90
153
|
<LoaderResolver
|
|
91
154
|
loaderDataPromise={loaderDataPromise}
|
|
92
155
|
loaderIds={loaderIds}
|
|
156
|
+
loaderStreams={loaderStreams}
|
|
93
157
|
outletKey={outletKey}
|
|
94
158
|
outletContent={outletContent}
|
|
95
159
|
segment={segment}
|
|
@@ -103,17 +167,46 @@ export function LoaderBoundary({
|
|
|
103
167
|
|
|
104
168
|
/**
|
|
105
169
|
* Internal component that resolves loader promises and renders OutletProvider
|
|
170
|
+
*
|
|
171
|
+
* SPIKE (streaming useLoader): when the producer provides per-loader streams,
|
|
172
|
+
* nothing resolves here above the children — the streams pass through
|
|
173
|
+
* OutletProvider and useLoader suspends at the read site, with the
|
|
174
|
+
* LoaderBoundary's loading() fallback as the catching boundary. The streams
|
|
175
|
+
* MUST be per-loader promises from the producer: deriving them by splitting
|
|
176
|
+
* the aggregate (aggregate.then(r => r[i])) is wrong — Promise.all resolves
|
|
177
|
+
* at the SLOWEST loader, so every derived promise inherits the slowest
|
|
178
|
+
* timing and per-loader granularity is erased (measured: a 400ms loader's
|
|
179
|
+
* content held until a 2000ms sibling resolved).
|
|
180
|
+
*
|
|
181
|
+
* Without streams, the pre-spike behavior is preserved: a pending aggregate
|
|
182
|
+
* resolves ABOVE via use() (parallel/intercept slots still take this path —
|
|
183
|
+
* ResolvedSegment carries only the aggregate for slots), and a resolved
|
|
184
|
+
* array decodes synchronously (forceAwait/action lanes commit whole).
|
|
106
185
|
*/
|
|
107
186
|
function LoaderResolver({
|
|
108
187
|
loaderDataPromise,
|
|
109
188
|
loaderIds,
|
|
189
|
+
loaderStreams,
|
|
110
190
|
outletKey,
|
|
111
191
|
outletContent,
|
|
112
192
|
segment,
|
|
113
193
|
parallel,
|
|
114
194
|
children,
|
|
115
195
|
}: Omit<LoaderBoundaryProps, "fallback">): ReactNode {
|
|
116
|
-
|
|
196
|
+
if (loaderStreams) {
|
|
197
|
+
return (
|
|
198
|
+
<OutletProvider
|
|
199
|
+
key={outletKey}
|
|
200
|
+
content={outletContent}
|
|
201
|
+
segment={segment}
|
|
202
|
+
parallel={parallel}
|
|
203
|
+
loaderStreams={loaderStreams}
|
|
204
|
+
>
|
|
205
|
+
{children}
|
|
206
|
+
</OutletProvider>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
117
210
|
const resolvedData =
|
|
118
211
|
loaderDataPromise instanceof Promise
|
|
119
212
|
? use(loaderDataPromise)
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type HelperContext,
|
|
20
20
|
type InterceptEntry,
|
|
21
21
|
type InterceptConfig,
|
|
22
|
+
type LoaderEntry,
|
|
22
23
|
} from "../server/context";
|
|
23
24
|
import { invariant } from "../errors";
|
|
24
25
|
import { validateUserRouteName } from "../route-name.js";
|
|
@@ -782,7 +783,11 @@ const intercept = (
|
|
|
782
783
|
/**
|
|
783
784
|
* Loader helper - attaches a loader to the current entry
|
|
784
785
|
*/
|
|
785
|
-
const loader: RouteHelpers<any, any>["loader"] = (
|
|
786
|
+
const loader: RouteHelpers<any, any>["loader"] = (
|
|
787
|
+
loaderDef,
|
|
788
|
+
optionsOrUse,
|
|
789
|
+
maybeUse,
|
|
790
|
+
) => {
|
|
786
791
|
const { store, ctx } = requireDslContext(
|
|
787
792
|
"loader() must be called inside urls()",
|
|
788
793
|
);
|
|
@@ -792,12 +797,32 @@ const loader: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
|
|
|
792
797
|
invariant(false, "No parent entry available for loader()");
|
|
793
798
|
}
|
|
794
799
|
|
|
800
|
+
// loader(Def, use) and loader(Def, options, use) — same options-or-use
|
|
801
|
+
// disambiguation path() uses, so the long-standing 2-arg form is untouched.
|
|
802
|
+
const optionsGiven =
|
|
803
|
+
typeof optionsOrUse === "function" ? undefined : optionsOrUse;
|
|
804
|
+
const use = typeof optionsOrUse === "function" ? optionsOrUse : maybeUse;
|
|
805
|
+
invariant(
|
|
806
|
+
!(typeof optionsOrUse === "function" && maybeUse !== undefined),
|
|
807
|
+
"loader() received two use() callbacks. Pass loader(Def, options, use) or loader(Def, use).",
|
|
808
|
+
);
|
|
809
|
+
invariant(
|
|
810
|
+
optionsGiven?.stream === undefined || optionsGiven.stream === "navigation",
|
|
811
|
+
`loader() stream must be "navigation" (got ${JSON.stringify(optionsGiven?.stream)}). Omit it to stream on every render.`,
|
|
812
|
+
);
|
|
813
|
+
|
|
795
814
|
const name = `${ctx.namespace}.$${store.getNextIndex("loader")}`;
|
|
796
815
|
|
|
797
|
-
// Create loader entry with empty revalidate array
|
|
798
|
-
|
|
816
|
+
// Create loader entry with empty revalidate array. awaitBeforeFlush is
|
|
817
|
+
// resolved here, at DSL-evaluation time, for the same reason
|
|
818
|
+
// loading({ ssr: false }) is (below) — per-isSSR entry caching makes the
|
|
819
|
+
// flag request-mode-correct with no isSSR threading; see LoaderEntry.
|
|
820
|
+
const loaderEntry: LoaderEntry = {
|
|
799
821
|
loader: loaderDef,
|
|
800
822
|
revalidate: [] as ShouldRevalidateFn<any, any>[],
|
|
823
|
+
...(optionsGiven?.stream === "navigation" && ctx.isSSR
|
|
824
|
+
? { awaitBeforeFlush: true as const }
|
|
825
|
+
: {}),
|
|
801
826
|
};
|
|
802
827
|
|
|
803
828
|
// Merge handler.use defaults (attached to the loader definition) with explicit use
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
PartialCacheOptions,
|
|
6
6
|
ErrorBoundaryHandler,
|
|
7
7
|
LoaderDefinition,
|
|
8
|
+
LoaderOptions,
|
|
8
9
|
MiddlewareFn,
|
|
9
10
|
NotFoundBoundaryHandler,
|
|
10
11
|
ResolvedRouteMap,
|
|
@@ -309,11 +310,23 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
309
310
|
* return <div>{data.name}</div>;
|
|
310
311
|
* }
|
|
311
312
|
* ```
|
|
313
|
+
* Pass `{ stream: "navigation" }` to await this loader before first flush on
|
|
314
|
+
* DOCUMENT requests (see {@link LoaderOptions}) — the opt-in for loaders whose
|
|
315
|
+
* data, handle pushes, or thrown notFound()/redirect() must be in the SSR'd
|
|
316
|
+
* HTML. Per-loader: a dynamic sibling in the same segment keeps streaming.
|
|
317
|
+
*
|
|
318
|
+
* ```typescript
|
|
319
|
+
* loader(ProductLoader, { stream: "navigation" }, () => [cache()]),
|
|
320
|
+
* loader(RecommendationsLoader), // still streams behind loading()
|
|
321
|
+
* ```
|
|
322
|
+
*
|
|
312
323
|
* @param loaderDef - Loader created with createLoader()
|
|
324
|
+
* @param optionsOrUse - Delivery options, or the use() callback when passing none
|
|
313
325
|
* @param use - Optional callback for loader-specific revalidation rules
|
|
314
326
|
*/
|
|
315
327
|
loader: <TData>(
|
|
316
328
|
loaderDef: LoaderDefinition<TData>,
|
|
329
|
+
optionsOrUse?: LoaderOptions | (() => UseItems<LoaderUseItem>),
|
|
317
330
|
use?: () => UseItems<LoaderUseItem>,
|
|
318
331
|
) => LoaderItem;
|
|
319
332
|
/**
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
|
|
2
|
+
import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
|
|
2
3
|
import {
|
|
3
4
|
getRequestContext,
|
|
4
5
|
_getRequestContext,
|
|
5
6
|
} from "../server/request-context.js";
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
attachRedirectState,
|
|
9
|
+
markExternalRedirect,
|
|
10
|
+
} from "../redirect-origin.js";
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* Create a soft redirect Response for middleware short-circuit
|
|
@@ -77,23 +81,6 @@ export function redirect(
|
|
|
77
81
|
if (state) {
|
|
78
82
|
const ctx = getRequestContext();
|
|
79
83
|
ctx.setLocationState(state);
|
|
80
|
-
|
|
81
|
-
if (process.env.NODE_ENV !== "production") {
|
|
82
|
-
const reqCtx = getRequestContext();
|
|
83
|
-
// Warn only on true full-page SSR loads. SPA partial requests and server
|
|
84
|
-
// actions both deliver state through Flight payloads, so suppress for those.
|
|
85
|
-
if (
|
|
86
|
-
reqCtx &&
|
|
87
|
-
!reqCtx.originalUrl.searchParams.has("_rsc_partial") &&
|
|
88
|
-
!reqCtx.request.headers.has("rsc-action") &&
|
|
89
|
-
!reqCtx.originalUrl.searchParams.has("_rsc_action")
|
|
90
|
-
) {
|
|
91
|
-
console.warn(
|
|
92
|
-
`[Router] redirect() with state during a full-page (SSR) request to "${url}". ` +
|
|
93
|
-
"Location state is only delivered during SPA navigations and will be lost on this request.",
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
84
|
}
|
|
98
85
|
|
|
99
86
|
// Auto-prefix root-relative URLs with basename for app-local redirects.
|
|
@@ -120,6 +107,18 @@ export function redirect(
|
|
|
120
107
|
|
|
121
108
|
const response = new Response(null, { status, headers });
|
|
122
109
|
|
|
110
|
+
// Also brand the Response itself with the resolved state (see
|
|
111
|
+
// redirect-origin.ts): the request-context write above misses the payload
|
|
112
|
+
// when a STREAMING loader throws this Response after metadata flushed, so
|
|
113
|
+
// the loader lane reads the state off the thrown object and ships it on
|
|
114
|
+
// the redirect marker instead.
|
|
115
|
+
if (state) {
|
|
116
|
+
attachRedirectState(
|
|
117
|
+
response,
|
|
118
|
+
resolveLocationStateEntries(Array.isArray(state) ? state : [state]),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
123
122
|
// Mark an explicit off-host redirect with an out-of-band brand so the
|
|
124
123
|
// same-origin guard (rsc/redirect-guard.ts) lets it through. The brand is a
|
|
125
124
|
// WeakSet membership on this Response object -- NOT a wire header -- so the
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Also includes the shared invokeOnError utility for error callback invocation.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type
|
|
8
|
+
import { createElement, type ReactNode } from "react";
|
|
9
9
|
import type { EntryData } from "../server/context";
|
|
10
10
|
import type {
|
|
11
11
|
ResolvedSegment,
|
|
@@ -281,6 +281,69 @@ export function createNotFoundInfo(
|
|
|
281
281
|
};
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
/** Router-level `createRouter({ notFound })` option shape. */
|
|
285
|
+
export type NotFoundComponentOption =
|
|
286
|
+
| ReactNode
|
|
287
|
+
| ((props: { pathname: string }) => ReactNode);
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Pick the effective notFound fallback: nearest `notFoundBoundary`, else the
|
|
291
|
+
* router-level `notFound` option, else a plain default.
|
|
292
|
+
*
|
|
293
|
+
* The router option is resolved ONLY when no nearer boundary won — it may be a
|
|
294
|
+
* user render function doing arbitrary work, so computing it eagerly (as three
|
|
295
|
+
* earlier copies of this policy did) burned a full render on every 404 that a
|
|
296
|
+
* boundary was going to handle anyway.
|
|
297
|
+
*
|
|
298
|
+
* Every 404 origin routes through here — segment resolution
|
|
299
|
+
* (`segment-resolution/helpers.ts`), loader-thrown `notFound()`
|
|
300
|
+
* (`loader-resolution.ts`), and the unmatched-route path (`rsc/handler.ts`) —
|
|
301
|
+
* so the default text and the boundary-wins precedence cannot drift apart.
|
|
302
|
+
*/
|
|
303
|
+
export function resolveNotFoundFallback(
|
|
304
|
+
boundary: ReactNode | NotFoundBoundaryHandler | null | undefined,
|
|
305
|
+
notFoundComponent: NotFoundComponentOption | undefined,
|
|
306
|
+
pathname: string | undefined,
|
|
307
|
+
): ReactNode | NotFoundBoundaryHandler {
|
|
308
|
+
if (boundary !== null && boundary !== undefined) return boundary;
|
|
309
|
+
return resolveDefaultNotFound(notFoundComponent, pathname);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The router-level default alone: `createRouter({ notFound })` rendered, or the
|
|
314
|
+
* plain fallback node. Separate from {@link resolveNotFoundFallback} for the
|
|
315
|
+
* unmatched-route path, which has no entry chain and therefore no boundary to
|
|
316
|
+
* consult — it gets a `ReactNode` back rather than a possible handler.
|
|
317
|
+
*/
|
|
318
|
+
export function resolveDefaultNotFound(
|
|
319
|
+
notFoundComponent: NotFoundComponentOption | undefined,
|
|
320
|
+
pathname: string | undefined,
|
|
321
|
+
): ReactNode {
|
|
322
|
+
if (typeof notFoundComponent === "function") {
|
|
323
|
+
return notFoundComponent({ pathname: pathname ?? "" });
|
|
324
|
+
}
|
|
325
|
+
return notFoundComponent ?? createElement("h1", null, "Not Found");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Invoke a notFound fallback that may be a boundary handler or a plain node.
|
|
330
|
+
*
|
|
331
|
+
* Callers that cannot propagate a throw (the loader envelope) wrap this in
|
|
332
|
+
* their own try/catch; it deliberately does not swallow, so a throwing boundary
|
|
333
|
+
* still surfaces where that is the correct behavior.
|
|
334
|
+
*/
|
|
335
|
+
export function renderNotFoundFallback(
|
|
336
|
+
fallback: ReactNode | NotFoundBoundaryHandler,
|
|
337
|
+
notFoundInfo: NotFoundInfo,
|
|
338
|
+
): ReactNode {
|
|
339
|
+
if (typeof fallback === "function") {
|
|
340
|
+
return fallback({
|
|
341
|
+
notFound: notFoundInfo,
|
|
342
|
+
} satisfies NotFoundBoundaryFallbackProps);
|
|
343
|
+
}
|
|
344
|
+
return fallback;
|
|
345
|
+
}
|
|
346
|
+
|
|
284
347
|
/**
|
|
285
348
|
* Create a notFound segment with the fallback component
|
|
286
349
|
* Renders the fallback with not found info
|
|
@@ -291,16 +354,7 @@ export function createNotFoundSegment(
|
|
|
291
354
|
entry: EntryData,
|
|
292
355
|
params: Record<string, string>,
|
|
293
356
|
): ResolvedSegment {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if (typeof fallback === "function") {
|
|
297
|
-
const props: NotFoundBoundaryFallbackProps = {
|
|
298
|
-
notFound: notFoundInfo,
|
|
299
|
-
};
|
|
300
|
-
component = fallback(props);
|
|
301
|
-
} else {
|
|
302
|
-
component = fallback;
|
|
303
|
-
}
|
|
357
|
+
const component: ReactNode = renderNotFoundFallback(fallback, notFoundInfo);
|
|
304
358
|
|
|
305
359
|
return {
|
|
306
360
|
id: `${entry.shortCode}.notFound`,
|
|
@@ -86,6 +86,35 @@ export function evaluateInterceptWhen(
|
|
|
86
86
|
* Find an intercept for the target route by walking up the entry chain.
|
|
87
87
|
* Returns the first (innermost) matching intercept along with the entry that defines it.
|
|
88
88
|
*/
|
|
89
|
+
/**
|
|
90
|
+
* Collect every intercept TARGET route name reachable from an origin entry —
|
|
91
|
+
* the same chain walk findInterceptForRoute() performs when this location is
|
|
92
|
+
* the navigation origin. Shipped in payload metadata so the browser-local
|
|
93
|
+
* clientUrls matcher can DECLINE its optimistic presentation for targets an
|
|
94
|
+
* intercept would claim (the committed result keeps the origin page + modal,
|
|
95
|
+
* so destination loading would flash and revert). Deliberately includes
|
|
96
|
+
* `when`-conditional intercepts: selectors need the live navigation context,
|
|
97
|
+
* so the browser stays conservative — worst case a non-intercepted navigation
|
|
98
|
+
* loses its optimistic loading, never the reverse.
|
|
99
|
+
*/
|
|
100
|
+
export function collectInterceptTargetNames(
|
|
101
|
+
fromEntry: EntryData | null,
|
|
102
|
+
): string[] {
|
|
103
|
+
const names = new Set<string>();
|
|
104
|
+
let current: EntryData | null = fromEntry;
|
|
105
|
+
while (current) {
|
|
106
|
+
// Tolerate partial entries (unit-test mocks omit the arrays); real
|
|
107
|
+
// registration always populates layout/intercept.
|
|
108
|
+
for (const source of [current, ...(current.layout ?? [])]) {
|
|
109
|
+
for (const intercept of source.intercept ?? []) {
|
|
110
|
+
names.add(intercept.routeName);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
current = current.parent;
|
|
114
|
+
}
|
|
115
|
+
return [...names];
|
|
116
|
+
}
|
|
117
|
+
|
|
89
118
|
export function findInterceptForRoute(
|
|
90
119
|
targetRouteKey: string,
|
|
91
120
|
fromEntry: EntryData | null,
|