@rangojs/router 0.0.0-experimental.63 → 0.0.0-experimental.64
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/vite/index.js +1 -1
- package/package.json +1 -1
- package/src/client.tsx +2 -56
- package/src/route-definition/dsl-helpers.ts +5 -1
- package/src/route-definition/helpers-types.ts +4 -1
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/match-api.ts +124 -183
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-interfaces.ts +7 -0
- package/src/router/segment-resolution/fresh.ts +37 -0
- package/src/router/segment-resolution/revalidation.ts +43 -0
- package/src/router.ts +4 -0
- package/src/rsc/handler.ts +456 -373
- package/src/rsc/ssr-setup.ts +1 -1
- package/src/server/request-context.ts +7 -0
- package/src/urls/path-helper-types.ts +4 -1
- package/src/use-loader.tsx +73 -4
package/src/rsc/ssr-setup.ts
CHANGED
|
@@ -98,7 +98,7 @@ export function getSSRSetup<TEnv>(
|
|
|
98
98
|
* the isRscRequest decision in rsc-rendering.ts.
|
|
99
99
|
*
|
|
100
100
|
* Note: response/mime routes are excluded by the caller — this function
|
|
101
|
-
* runs after
|
|
101
|
+
* runs after classifyRequest() determines the request mode.
|
|
102
102
|
*/
|
|
103
103
|
export function mayNeedSSR(request: Request, url: URL): boolean {
|
|
104
104
|
if (
|
|
@@ -290,6 +290,12 @@ export interface RequestContext<
|
|
|
290
290
|
|
|
291
291
|
/** @internal Router basename for this request (used by redirect()) */
|
|
292
292
|
_basename?: string;
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* @internal RouteSnapshot from classifyRequest, reused by match/matchPartial
|
|
296
|
+
* to avoid a second resolveRoute call. Cleared on HMR invalidation.
|
|
297
|
+
*/
|
|
298
|
+
_classifiedRoute?: import("../router/route-snapshot.js").RouteSnapshot;
|
|
293
299
|
}
|
|
294
300
|
|
|
295
301
|
/**
|
|
@@ -322,6 +328,7 @@ export type PublicRequestContext<
|
|
|
322
328
|
| "_basename"
|
|
323
329
|
| "_setStatus"
|
|
324
330
|
| "_variables"
|
|
331
|
+
| "_classifiedRoute"
|
|
325
332
|
| "res"
|
|
326
333
|
>;
|
|
327
334
|
|
|
@@ -284,7 +284,10 @@ export type PathHelpers<TEnv> = {
|
|
|
284
284
|
/**
|
|
285
285
|
* Attach a loading component to the current route/layout
|
|
286
286
|
*/
|
|
287
|
-
loading: (
|
|
287
|
+
loading: (
|
|
288
|
+
component: ReactNode | (() => ReactNode),
|
|
289
|
+
options?: { ssr?: boolean },
|
|
290
|
+
) => LoadingItem;
|
|
288
291
|
|
|
289
292
|
/**
|
|
290
293
|
* Attach an error boundary to catch errors in this segment
|
package/src/use-loader.tsx
CHANGED
|
@@ -1,9 +1,70 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
isValidElement,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
type ReactNode,
|
|
12
|
+
} from "react";
|
|
4
13
|
import { OutletContext, type OutletContextValue } from "./outlet-context.js";
|
|
5
14
|
import type { LoaderDefinition, LoadOptions } from "./types.js";
|
|
6
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Extract a specific loader's data from a content ReactNode.
|
|
18
|
+
*
|
|
19
|
+
* When a route registers loaders via loader(), the resolved data lives in
|
|
20
|
+
* the route's OutletProvider (rendered as <Outlet /> content). Parallel
|
|
21
|
+
* slots are siblings of <Outlet />, so they can't find it by walking
|
|
22
|
+
* the parent context chain. This helper traverses wrapper elements
|
|
23
|
+
* (MountContextProvider, ViewTransition, etc.) to reach the OutletProvider
|
|
24
|
+
* and extract the loader data directly.
|
|
25
|
+
*/
|
|
26
|
+
const NOT_FOUND = Symbol("not-found");
|
|
27
|
+
|
|
28
|
+
function extractContentLoaderData(
|
|
29
|
+
node: ReactNode,
|
|
30
|
+
loaderId: string,
|
|
31
|
+
): unknown | typeof NOT_FOUND {
|
|
32
|
+
if (!isValidElement(node)) return NOT_FOUND;
|
|
33
|
+
const props = node.props as Record<string, any> | undefined;
|
|
34
|
+
if (!props) return NOT_FOUND;
|
|
35
|
+
|
|
36
|
+
// Direct OutletProvider with loaderData
|
|
37
|
+
if (props.loaderData && loaderId in props.loaderData) {
|
|
38
|
+
return props.loaderData[loaderId];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// LoaderBoundary: loaderIds + loaderDataPromise (already resolved array).
|
|
42
|
+
// When the segment has loading(), loaderData is resolved inside
|
|
43
|
+
// LoaderBoundary via use(). If the promise was pre-awaited (forceAwait
|
|
44
|
+
// or isAction), the prop is a raw array we can index into.
|
|
45
|
+
if (
|
|
46
|
+
props.loaderIds &&
|
|
47
|
+
Array.isArray(props.loaderIds) &&
|
|
48
|
+
props.loaderDataPromise &&
|
|
49
|
+
!(props.loaderDataPromise instanceof Promise)
|
|
50
|
+
) {
|
|
51
|
+
const idx = (props.loaderIds as string[]).indexOf(loaderId);
|
|
52
|
+
if (idx !== -1) {
|
|
53
|
+
const data = (props.loaderDataPromise as any[])[idx];
|
|
54
|
+
// loaderDataPromise entries may be { ok, data } result objects
|
|
55
|
+
if (data && typeof data === "object" && "ok" in data) {
|
|
56
|
+
return data.ok ? data.data : NOT_FOUND;
|
|
57
|
+
}
|
|
58
|
+
return data;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Traverse into wrapper elements (MountContextProvider, ViewTransition,
|
|
63
|
+
// Suspense wrappers, etc.)
|
|
64
|
+
if (props.children) return extractContentLoaderData(props.children, loaderId);
|
|
65
|
+
return NOT_FOUND;
|
|
66
|
+
}
|
|
67
|
+
|
|
7
68
|
/**
|
|
8
69
|
* Payload returned by loader RSC requests
|
|
9
70
|
*/
|
|
@@ -71,19 +132,27 @@ function useLoaderInternal<T>(
|
|
|
71
132
|
const context = useContext(OutletContext);
|
|
72
133
|
|
|
73
134
|
// Get data from context (SSR/navigation)
|
|
74
|
-
const
|
|
135
|
+
const contextData = useMemo((): T | undefined => {
|
|
75
136
|
let current: OutletContextValue | null | undefined = context;
|
|
76
137
|
while (current) {
|
|
77
138
|
if (current.loaderData && loader.$$id in current.loaderData) {
|
|
78
139
|
return current.loaderData[loader.$$id] as T;
|
|
79
140
|
}
|
|
141
|
+
// Check content element — the route's OutletProvider is rendered as
|
|
142
|
+
// <Outlet /> content (a child), so its loaderData isn't in the parent
|
|
143
|
+
// chain. Parallel slots need to reach into it to find route-level loaders.
|
|
144
|
+
const contentData = extractContentLoaderData(
|
|
145
|
+
current.content,
|
|
146
|
+
loader.$$id,
|
|
147
|
+
);
|
|
148
|
+
if (contentData !== NOT_FOUND) {
|
|
149
|
+
return contentData as T;
|
|
150
|
+
}
|
|
80
151
|
current = current.parent;
|
|
81
152
|
}
|
|
82
153
|
return undefined;
|
|
83
154
|
}, [context, loader.$$id]);
|
|
84
155
|
|
|
85
|
-
const contextData = getContextData();
|
|
86
|
-
|
|
87
156
|
// Local state for fetched data (from load() calls)
|
|
88
157
|
const [fetchedData, setFetchedData] = useState<T | undefined>(undefined);
|
|
89
158
|
const [isLoading, setIsLoading] = useState(false);
|