@ivogt/rsc-router 0.0.0-experimental.1
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/README.md +19 -0
- package/package.json +131 -0
- package/src/__mocks__/version.ts +6 -0
- package/src/__tests__/route-definition.test.ts +63 -0
- package/src/browser/event-controller.ts +876 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/link-interceptor.ts +121 -0
- package/src/browser/lru-cache.ts +69 -0
- package/src/browser/merge-segment-loaders.ts +126 -0
- package/src/browser/navigation-bridge.ts +891 -0
- package/src/browser/navigation-client.ts +155 -0
- package/src/browser/navigation-store.ts +823 -0
- package/src/browser/partial-update.ts +545 -0
- package/src/browser/react/Link.tsx +248 -0
- package/src/browser/react/NavigationProvider.tsx +228 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +53 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +120 -0
- package/src/browser/react/location-state.ts +62 -0
- package/src/browser/react/use-action.ts +240 -0
- package/src/browser/react/use-client-cache.ts +56 -0
- package/src/browser/react/use-handle.ts +178 -0
- package/src/browser/react/use-link-status.ts +134 -0
- package/src/browser/react/use-navigation.ts +150 -0
- package/src/browser/react/use-segments.ts +188 -0
- package/src/browser/request-controller.ts +149 -0
- package/src/browser/rsc-router.tsx +310 -0
- package/src/browser/scroll-restoration.ts +324 -0
- package/src/browser/server-action-bridge.ts +747 -0
- package/src/browser/shallow.ts +35 -0
- package/src/browser/types.ts +443 -0
- package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
- package/src/cache/__tests__/memory-store.test.ts +484 -0
- package/src/cache/cache-scope.ts +565 -0
- package/src/cache/cf/__tests__/cf-cache-store.test.ts +361 -0
- package/src/cache/cf/cf-cache-store.ts +274 -0
- package/src/cache/cf/index.ts +19 -0
- package/src/cache/index.ts +52 -0
- package/src/cache/memory-segment-store.ts +150 -0
- package/src/cache/memory-store.ts +253 -0
- package/src/cache/types.ts +366 -0
- package/src/client.rsc.tsx +88 -0
- package/src/client.tsx +609 -0
- package/src/components/DefaultDocument.tsx +20 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +259 -0
- package/src/handle.ts +120 -0
- package/src/handles/MetaTags.tsx +178 -0
- package/src/handles/index.ts +6 -0
- package/src/handles/meta.ts +247 -0
- package/src/href-client.ts +128 -0
- package/src/href.ts +139 -0
- package/src/index.rsc.ts +69 -0
- package/src/index.ts +84 -0
- package/src/loader.rsc.ts +204 -0
- package/src/loader.ts +47 -0
- package/src/network-error-thrower.tsx +21 -0
- package/src/outlet-context.ts +15 -0
- package/src/root-error-boundary.tsx +277 -0
- package/src/route-content-wrapper.tsx +198 -0
- package/src/route-definition.ts +1333 -0
- package/src/route-map-builder.ts +140 -0
- package/src/route-types.ts +148 -0
- package/src/route-utils.ts +89 -0
- package/src/router/__tests__/match-context.test.ts +104 -0
- package/src/router/__tests__/match-pipelines.test.ts +537 -0
- package/src/router/__tests__/match-result.test.ts +566 -0
- package/src/router/__tests__/on-error.test.ts +935 -0
- package/src/router/__tests__/pattern-matching.test.ts +577 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/handler-context.ts +60 -0
- package/src/router/loader-resolution.ts +326 -0
- package/src/router/manifest.ts +116 -0
- package/src/router/match-context.ts +261 -0
- package/src/router/match-middleware/background-revalidation.ts +236 -0
- package/src/router/match-middleware/cache-lookup.ts +261 -0
- package/src/router/match-middleware/cache-store.ts +250 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +268 -0
- package/src/router/match-middleware/segment-resolution.ts +174 -0
- package/src/router/match-pipelines.ts +214 -0
- package/src/router/match-result.ts +212 -0
- package/src/router/metrics.ts +62 -0
- package/src/router/middleware.test.ts +1355 -0
- package/src/router/middleware.ts +748 -0
- package/src/router/pattern-matching.ts +271 -0
- package/src/router/revalidation.ts +190 -0
- package/src/router/router-context.ts +299 -0
- package/src/router/types.ts +96 -0
- package/src/router.ts +3484 -0
- package/src/rsc/__tests__/helpers.test.ts +175 -0
- package/src/rsc/handler.ts +942 -0
- package/src/rsc/helpers.ts +64 -0
- package/src/rsc/index.ts +56 -0
- package/src/rsc/nonce.ts +18 -0
- package/src/rsc/types.ts +225 -0
- package/src/segment-system.tsx +405 -0
- package/src/server/__tests__/request-context.test.ts +171 -0
- package/src/server/context.ts +340 -0
- package/src/server/handle-store.ts +230 -0
- package/src/server/loader-registry.ts +174 -0
- package/src/server/request-context.ts +470 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +126 -0
- package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
- package/src/ssr/index.tsx +215 -0
- package/src/types.ts +1473 -0
- package/src/use-loader.tsx +346 -0
- package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
- package/src/vite/expose-action-id.ts +344 -0
- package/src/vite/expose-handle-id.ts +209 -0
- package/src/vite/expose-loader-id.ts +357 -0
- package/src/vite/expose-location-state-id.ts +177 -0
- package/src/vite/index.ts +608 -0
- package/src/vite/version.d.ts +12 -0
- package/src/vite/virtual-entries.ts +109 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { Suspense, use, useId } from "react";
|
|
4
|
+
import { invariant } from "./errors";
|
|
5
|
+
import { OutletProvider } from "./client.js";
|
|
6
|
+
import type { ResolvedSegment } from "./types.js";
|
|
7
|
+
import { isLoaderDataResult } from "./types.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Stable async wrapper component for route content
|
|
11
|
+
* Using a module-level component ensures React sees the same component reference
|
|
12
|
+
* across renders, preventing unnecessary remounts during actions.
|
|
13
|
+
*
|
|
14
|
+
* When content is a pending promise, React suspends and shows the nearest
|
|
15
|
+
* Suspense fallback. When content is already resolved, it renders immediately
|
|
16
|
+
* without suspension.
|
|
17
|
+
*
|
|
18
|
+
* @param segmentId - Stable ID from segment, used for consistent keys across renders
|
|
19
|
+
*/
|
|
20
|
+
export function RouteContentWrapper({
|
|
21
|
+
content,
|
|
22
|
+
fallback,
|
|
23
|
+
segmentId,
|
|
24
|
+
}: {
|
|
25
|
+
content: Promise<ReactNode>;
|
|
26
|
+
fallback?: ReactNode;
|
|
27
|
+
segmentId?: string;
|
|
28
|
+
}): ReactNode {
|
|
29
|
+
// Use server-provided segmentId for stable keys, fall back to useId for backwards compat
|
|
30
|
+
const generatedId = useId();
|
|
31
|
+
const id = segmentId || generatedId;
|
|
32
|
+
if (!content) {
|
|
33
|
+
// Already resolved
|
|
34
|
+
return content as ReactNode;
|
|
35
|
+
}
|
|
36
|
+
return (
|
|
37
|
+
<Suspense fallback={fallback ?? null} key={"route-content-suspense-" + id}>
|
|
38
|
+
<Suspender content={content} key={id} />
|
|
39
|
+
</Suspense>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function RouteContentWrapperCallback<T>({
|
|
44
|
+
resolve,
|
|
45
|
+
fallback,
|
|
46
|
+
children,
|
|
47
|
+
}: {
|
|
48
|
+
resolve: Promise<T> | T;
|
|
49
|
+
fallback?: ReactNode;
|
|
50
|
+
children: (data: T) => ReactNode;
|
|
51
|
+
}): ReactNode {
|
|
52
|
+
const id = useId();
|
|
53
|
+
invariant(children, "RouteContentWrapperCallback requires children");
|
|
54
|
+
invariant(
|
|
55
|
+
typeof children === "function",
|
|
56
|
+
"RouteContentWrapperCallback requires children to be a function"
|
|
57
|
+
);
|
|
58
|
+
invariant(
|
|
59
|
+
resolve !== undefined,
|
|
60
|
+
"RouteContentWrapperCallback requires resolve"
|
|
61
|
+
);
|
|
62
|
+
return (
|
|
63
|
+
<Suspense
|
|
64
|
+
fallback={fallback ?? null}
|
|
65
|
+
key={"route-content-suspense-callback-" + id}
|
|
66
|
+
>
|
|
67
|
+
<SuspenderCallback resolve={resolve} key={id}>
|
|
68
|
+
{children}
|
|
69
|
+
</SuspenderCallback>
|
|
70
|
+
</Suspense>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const Suspender = ({
|
|
75
|
+
content,
|
|
76
|
+
}: {
|
|
77
|
+
content: Promise<ReactNode> | ReactNode;
|
|
78
|
+
}): ReactNode => {
|
|
79
|
+
invariant(content instanceof Promise, "Suspender expects a Promise content");
|
|
80
|
+
|
|
81
|
+
return use(content);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const SuspenderCallback = <T,>({
|
|
85
|
+
resolve,
|
|
86
|
+
children,
|
|
87
|
+
}: {
|
|
88
|
+
resolve: Promise<T> | T;
|
|
89
|
+
children: (data: T) => ReactNode;
|
|
90
|
+
}): ReactNode => {
|
|
91
|
+
return resolve instanceof Promise
|
|
92
|
+
? children(use(resolve))
|
|
93
|
+
: children(resolve);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* LoaderBoundary - Client component that resolves loader promises and renders OutletProvider
|
|
98
|
+
*
|
|
99
|
+
* This component enables streaming with loaders by:
|
|
100
|
+
* 1. Receiving loader promises (serializable across RSC boundary)
|
|
101
|
+
* 2. Using React's use() to resolve them (triggers Suspense)
|
|
102
|
+
* 3. Rendering OutletProvider with resolved data
|
|
103
|
+
*
|
|
104
|
+
* The callback logic lives inside this client component, avoiding the
|
|
105
|
+
* "Functions are not valid as a child of Client Components" error.
|
|
106
|
+
*/
|
|
107
|
+
export interface LoaderBoundaryProps {
|
|
108
|
+
loaderDataPromise: Promise<any[]> | any[];
|
|
109
|
+
loaderIds: string[];
|
|
110
|
+
fallback?: ReactNode;
|
|
111
|
+
outletKey: string;
|
|
112
|
+
outletContent: ReactNode;
|
|
113
|
+
segment: ResolvedSegment;
|
|
114
|
+
parallel?: ResolvedSegment[];
|
|
115
|
+
children: ReactNode;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function LoaderBoundary({
|
|
119
|
+
loaderDataPromise,
|
|
120
|
+
loaderIds,
|
|
121
|
+
fallback,
|
|
122
|
+
outletKey,
|
|
123
|
+
outletContent,
|
|
124
|
+
segment,
|
|
125
|
+
parallel,
|
|
126
|
+
children,
|
|
127
|
+
}: LoaderBoundaryProps): ReactNode {
|
|
128
|
+
const id = useId();
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<Suspense fallback={fallback ?? null} key={`loader-boundary-${id}`}>
|
|
132
|
+
<LoaderResolver
|
|
133
|
+
loaderDataPromise={loaderDataPromise}
|
|
134
|
+
loaderIds={loaderIds}
|
|
135
|
+
outletKey={outletKey}
|
|
136
|
+
outletContent={outletContent}
|
|
137
|
+
segment={segment}
|
|
138
|
+
parallel={parallel}
|
|
139
|
+
>
|
|
140
|
+
{children}
|
|
141
|
+
</LoaderResolver>
|
|
142
|
+
</Suspense>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Internal component that resolves loader promises and renders OutletProvider
|
|
148
|
+
*/
|
|
149
|
+
function LoaderResolver({
|
|
150
|
+
loaderDataPromise,
|
|
151
|
+
loaderIds,
|
|
152
|
+
outletKey,
|
|
153
|
+
outletContent,
|
|
154
|
+
segment,
|
|
155
|
+
parallel,
|
|
156
|
+
children,
|
|
157
|
+
}: Omit<LoaderBoundaryProps, "fallback">): ReactNode {
|
|
158
|
+
// Resolve loader promises using React's use()
|
|
159
|
+
const resolvedData =
|
|
160
|
+
loaderDataPromise instanceof Promise
|
|
161
|
+
? use(loaderDataPromise)
|
|
162
|
+
: loaderDataPromise;
|
|
163
|
+
|
|
164
|
+
// Build loaderData record from resolved values
|
|
165
|
+
const loaderData: Record<string, any> = {};
|
|
166
|
+
let loaderErrorFallback: ReactNode = null;
|
|
167
|
+
|
|
168
|
+
loaderIds.forEach((id, i) => {
|
|
169
|
+
const result = resolvedData[i];
|
|
170
|
+
|
|
171
|
+
if (isLoaderDataResult(result)) {
|
|
172
|
+
if (result.ok) {
|
|
173
|
+
loaderData[id] = result.data;
|
|
174
|
+
} else {
|
|
175
|
+
if (result.fallback) {
|
|
176
|
+
loaderErrorFallback = result.fallback;
|
|
177
|
+
} else {
|
|
178
|
+
throw new Error(result.error.message);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
// Legacy format - direct data
|
|
183
|
+
loaderData[id] = result;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<OutletProvider
|
|
189
|
+
key={outletKey}
|
|
190
|
+
content={outletContent}
|
|
191
|
+
segment={segment}
|
|
192
|
+
parallel={parallel}
|
|
193
|
+
loaderData={Object.keys(loaderData).length > 0 ? loaderData : undefined}
|
|
194
|
+
>
|
|
195
|
+
{loaderErrorFallback ?? children}
|
|
196
|
+
</OutletProvider>
|
|
197
|
+
);
|
|
198
|
+
}
|