@bleedingdev/modern-js-plugin-tanstack 3.2.0-ultramodern.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/LICENSE +21 -0
- package/dist/cjs/cli/index.js +268 -0
- package/dist/cjs/cli/tanstackTypes.js +388 -0
- package/dist/cjs/cli.js +65 -0
- package/dist/cjs/runtime/DefaultNotFound.js +47 -0
- package/dist/cjs/runtime/basepathRewrite.js +62 -0
- package/dist/cjs/runtime/dataMutation.js +345 -0
- package/dist/cjs/runtime/hooks.js +57 -0
- package/dist/cjs/runtime/index.js +114 -0
- package/dist/cjs/runtime/lifecycle.js +125 -0
- package/dist/cjs/runtime/plugin.js +250 -0
- package/dist/cjs/runtime/plugin.node.js +304 -0
- package/dist/cjs/runtime/prefetchLink.js +55 -0
- package/dist/cjs/runtime/routeTree.js +492 -0
- package/dist/cjs/runtime/rsc/ClientSlot.js +53 -0
- package/dist/cjs/runtime/rsc/CompositeComponent.js +75 -0
- package/dist/cjs/runtime/rsc/ReplayableStream.js +141 -0
- package/dist/cjs/runtime/rsc/RscNodeRenderer.js +65 -0
- package/dist/cjs/runtime/rsc/SlotContext.js +54 -0
- package/dist/cjs/runtime/rsc/client.js +93 -0
- package/dist/cjs/runtime/rsc/createRscProxy.js +141 -0
- package/dist/cjs/runtime/rsc/index.js +42 -0
- package/dist/cjs/runtime/rsc/payloadRouter.js +211 -0
- package/dist/cjs/runtime/rsc/server.js +246 -0
- package/dist/cjs/runtime/rsc/slotUsageSanitizer.js +65 -0
- package/dist/cjs/runtime/rsc/symbols.js +72 -0
- package/dist/cjs/runtime/types.js +18 -0
- package/dist/cjs/runtime/utils.js +142 -0
- package/dist/cjs/runtime.js +58 -0
- package/dist/esm/cli/index.mjs +201 -0
- package/dist/esm/cli/tanstackTypes.mjs +341 -0
- package/dist/esm/cli.mjs +2 -0
- package/dist/esm/rslib-runtime.mjs +18 -0
- package/dist/esm/runtime/DefaultNotFound.mjs +13 -0
- package/dist/esm/runtime/basepathRewrite.mjs +28 -0
- package/dist/esm/runtime/dataMutation.mjs +305 -0
- package/dist/esm/runtime/hooks.mjs +8 -0
- package/dist/esm/runtime/index.mjs +6 -0
- package/dist/esm/runtime/lifecycle.mjs +82 -0
- package/dist/esm/runtime/plugin.mjs +214 -0
- package/dist/esm/runtime/plugin.node.mjs +268 -0
- package/dist/esm/runtime/prefetchLink.mjs +18 -0
- package/dist/esm/runtime/routeTree.mjs +452 -0
- package/dist/esm/runtime/rsc/ClientSlot.mjs +19 -0
- package/dist/esm/runtime/rsc/CompositeComponent.mjs +41 -0
- package/dist/esm/runtime/rsc/ReplayableStream.mjs +104 -0
- package/dist/esm/runtime/rsc/RscNodeRenderer.mjs +31 -0
- package/dist/esm/runtime/rsc/SlotContext.mjs +17 -0
- package/dist/esm/runtime/rsc/client.mjs +53 -0
- package/dist/esm/runtime/rsc/createRscProxy.mjs +107 -0
- package/dist/esm/runtime/rsc/index.mjs +1 -0
- package/dist/esm/runtime/rsc/payloadRouter.mjs +162 -0
- package/dist/esm/runtime/rsc/server.mjs +200 -0
- package/dist/esm/runtime/rsc/slotUsageSanitizer.mjs +31 -0
- package/dist/esm/runtime/rsc/symbols.mjs +17 -0
- package/dist/esm/runtime/types.mjs +0 -0
- package/dist/esm/runtime/utils.mjs +89 -0
- package/dist/esm/runtime.mjs +1 -0
- package/dist/esm-node/cli/index.mjs +205 -0
- package/dist/esm-node/cli/tanstackTypes.mjs +342 -0
- package/dist/esm-node/cli.mjs +3 -0
- package/dist/esm-node/rslib-runtime.mjs +19 -0
- package/dist/esm-node/runtime/DefaultNotFound.mjs +14 -0
- package/dist/esm-node/runtime/basepathRewrite.mjs +29 -0
- package/dist/esm-node/runtime/dataMutation.mjs +306 -0
- package/dist/esm-node/runtime/hooks.mjs +9 -0
- package/dist/esm-node/runtime/index.mjs +7 -0
- package/dist/esm-node/runtime/lifecycle.mjs +83 -0
- package/dist/esm-node/runtime/plugin.mjs +215 -0
- package/dist/esm-node/runtime/plugin.node.mjs +269 -0
- package/dist/esm-node/runtime/prefetchLink.mjs +19 -0
- package/dist/esm-node/runtime/routeTree.mjs +453 -0
- package/dist/esm-node/runtime/rsc/ClientSlot.mjs +20 -0
- package/dist/esm-node/runtime/rsc/CompositeComponent.mjs +42 -0
- package/dist/esm-node/runtime/rsc/ReplayableStream.mjs +105 -0
- package/dist/esm-node/runtime/rsc/RscNodeRenderer.mjs +32 -0
- package/dist/esm-node/runtime/rsc/SlotContext.mjs +18 -0
- package/dist/esm-node/runtime/rsc/client.mjs +54 -0
- package/dist/esm-node/runtime/rsc/createRscProxy.mjs +108 -0
- package/dist/esm-node/runtime/rsc/index.mjs +2 -0
- package/dist/esm-node/runtime/rsc/payloadRouter.mjs +163 -0
- package/dist/esm-node/runtime/rsc/server.mjs +201 -0
- package/dist/esm-node/runtime/rsc/slotUsageSanitizer.mjs +32 -0
- package/dist/esm-node/runtime/rsc/symbols.mjs +18 -0
- package/dist/esm-node/runtime/types.mjs +1 -0
- package/dist/esm-node/runtime/utils.mjs +90 -0
- package/dist/esm-node/runtime.mjs +2 -0
- package/dist/types/cli/index.d.ts +20 -0
- package/dist/types/cli/tanstackTypes.d.ts +11 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/runtime/DefaultNotFound.d.ts +2 -0
- package/dist/types/runtime/basepathRewrite.d.ts +8 -0
- package/dist/types/runtime/dataMutation.d.ts +29 -0
- package/dist/types/runtime/hooks.d.ts +18 -0
- package/dist/types/runtime/index.d.ts +9 -0
- package/dist/types/runtime/lifecycle.d.ts +22 -0
- package/dist/types/runtime/plugin.d.ts +17 -0
- package/dist/types/runtime/plugin.node.d.ts +17 -0
- package/dist/types/runtime/prefetchLink.d.ts +11 -0
- package/dist/types/runtime/routeTree.d.ts +11 -0
- package/dist/types/runtime/rsc/ClientSlot.d.ts +5 -0
- package/dist/types/runtime/rsc/CompositeComponent.d.ts +3 -0
- package/dist/types/runtime/rsc/ReplayableStream.d.ts +24 -0
- package/dist/types/runtime/rsc/RscNodeRenderer.d.ts +5 -0
- package/dist/types/runtime/rsc/SlotContext.d.ts +11 -0
- package/dist/types/runtime/rsc/client.d.ts +11 -0
- package/dist/types/runtime/rsc/createRscProxy.d.ts +7 -0
- package/dist/types/runtime/rsc/index.d.ts +2 -0
- package/dist/types/runtime/rsc/payloadRouter.d.ts +24 -0
- package/dist/types/runtime/rsc/server.d.ts +14 -0
- package/dist/types/runtime/rsc/slotUsageSanitizer.d.ts +2 -0
- package/dist/types/runtime/rsc/symbols.d.ts +46 -0
- package/dist/types/runtime/types.d.ts +68 -0
- package/dist/types/runtime/utils.d.ts +36 -0
- package/dist/types/runtime.d.ts +1 -0
- package/dist/types-direct/cli/index.d.ts +20 -0
- package/dist/types-direct/cli/tanstackTypes.d.ts +11 -0
- package/dist/types-direct/cli.d.ts +2 -0
- package/dist/types-direct/runtime/DefaultNotFound.d.ts +2 -0
- package/dist/types-direct/runtime/basepathRewrite.d.ts +8 -0
- package/dist/types-direct/runtime/dataMutation.d.ts +29 -0
- package/dist/types-direct/runtime/hooks.d.ts +18 -0
- package/dist/types-direct/runtime/index.d.ts +9 -0
- package/dist/types-direct/runtime/lifecycle.d.ts +22 -0
- package/dist/types-direct/runtime/plugin.d.ts +17 -0
- package/dist/types-direct/runtime/plugin.node.d.ts +17 -0
- package/dist/types-direct/runtime/prefetchLink.d.ts +11 -0
- package/dist/types-direct/runtime/routeTree.d.ts +11 -0
- package/dist/types-direct/runtime/rsc/ClientSlot.d.ts +5 -0
- package/dist/types-direct/runtime/rsc/CompositeComponent.d.ts +3 -0
- package/dist/types-direct/runtime/rsc/ReplayableStream.d.ts +24 -0
- package/dist/types-direct/runtime/rsc/RscNodeRenderer.d.ts +5 -0
- package/dist/types-direct/runtime/rsc/SlotContext.d.ts +11 -0
- package/dist/types-direct/runtime/rsc/client.d.ts +11 -0
- package/dist/types-direct/runtime/rsc/createRscProxy.d.ts +7 -0
- package/dist/types-direct/runtime/rsc/index.d.ts +2 -0
- package/dist/types-direct/runtime/rsc/payloadRouter.d.ts +24 -0
- package/dist/types-direct/runtime/rsc/server.d.ts +14 -0
- package/dist/types-direct/runtime/rsc/slotUsageSanitizer.d.ts +2 -0
- package/dist/types-direct/runtime/rsc/symbols.d.ts +46 -0
- package/dist/types-direct/runtime/types.d.ts +68 -0
- package/dist/types-direct/runtime/utils.d.ts +36 -0
- package/dist/types-direct/runtime.d.ts +1 -0
- package/package.json +126 -0
- package/rslib.config.mts +4 -0
- package/rstest.config.mts +43 -0
- package/src/cli/index.ts +388 -0
- package/src/cli/tanstackTypes.ts +503 -0
- package/src/cli.ts +2 -0
- package/src/runtime/DefaultNotFound.tsx +15 -0
- package/src/runtime/basepathRewrite.ts +59 -0
- package/src/runtime/dataMutation.tsx +517 -0
- package/src/runtime/hooks.ts +34 -0
- package/src/runtime/index.tsx +30 -0
- package/src/runtime/lifecycle.ts +150 -0
- package/src/runtime/plugin.node.tsx +534 -0
- package/src/runtime/plugin.tsx +395 -0
- package/src/runtime/prefetchLink.tsx +87 -0
- package/src/runtime/routeTree.ts +942 -0
- package/src/runtime/rsc/ClientSlot.tsx +25 -0
- package/src/runtime/rsc/CompositeComponent.tsx +65 -0
- package/src/runtime/rsc/ReplayableStream.ts +155 -0
- package/src/runtime/rsc/RscNodeRenderer.tsx +45 -0
- package/src/runtime/rsc/SlotContext.tsx +31 -0
- package/src/runtime/rsc/client.tsx +90 -0
- package/src/runtime/rsc/createRscProxy.tsx +189 -0
- package/src/runtime/rsc/index.ts +10 -0
- package/src/runtime/rsc/payloadRouter.ts +318 -0
- package/src/runtime/rsc/server.tsx +303 -0
- package/src/runtime/rsc/slotUsageSanitizer.ts +76 -0
- package/src/runtime/rsc/symbols.ts +106 -0
- package/src/runtime/ssr-shim.d.ts +12 -0
- package/src/runtime/types.ts +83 -0
- package/src/runtime/utils.tsx +161 -0
- package/src/runtime.ts +1 -0
- package/tests/router/cli.test.ts +386 -0
- package/tests/router/dataMutation.test.tsx +396 -0
- package/tests/router/prefetchLink.test.tsx +43 -0
- package/tests/router/routeTree.test.ts +502 -0
- package/tests/router/rsc.test.tsx +256 -0
- package/tests/router/tanstackTypes.test.ts +62 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tsgo.json +6 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import type { PayloadRoute, ServerPayload } from '@modern-js/runtime/context';
|
|
2
|
+
import { notFound, redirect } from '@tanstack/react-router';
|
|
3
|
+
|
|
4
|
+
type PayloadDecoder = (stream: ReadableStream<Uint8Array>) => Promise<unknown>;
|
|
5
|
+
|
|
6
|
+
type RouterStaticData = {
|
|
7
|
+
modernRouteAction?: unknown;
|
|
8
|
+
modernRouteHandle?: unknown;
|
|
9
|
+
modernRouteHasAction?: unknown;
|
|
10
|
+
modernRouteHasClientLoader?: unknown;
|
|
11
|
+
modernRouteHasLoader?: unknown;
|
|
12
|
+
modernRouteId?: unknown;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type RouterRouteLike = {
|
|
16
|
+
id?: unknown;
|
|
17
|
+
options?: {
|
|
18
|
+
index?: unknown;
|
|
19
|
+
path?: unknown;
|
|
20
|
+
staticData?: RouterStaticData;
|
|
21
|
+
};
|
|
22
|
+
parentRoute?: RouterRouteLike;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type RouterMatchLike = {
|
|
26
|
+
error?: unknown;
|
|
27
|
+
id?: unknown;
|
|
28
|
+
loaderData?: unknown;
|
|
29
|
+
params?: unknown;
|
|
30
|
+
pathname?: unknown;
|
|
31
|
+
pathnameBase?: unknown;
|
|
32
|
+
route?: RouterRouteLike;
|
|
33
|
+
routeId?: unknown;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type TanstackPayloadRouterLike = {
|
|
37
|
+
state?: {
|
|
38
|
+
location?: unknown;
|
|
39
|
+
matches?: unknown;
|
|
40
|
+
statusCode?: unknown;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type LoadRouteDataOptions = {
|
|
45
|
+
hasClientLoader?: boolean;
|
|
46
|
+
loadClientData: () => Promise<unknown>;
|
|
47
|
+
request: Request;
|
|
48
|
+
routeId?: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const payloadFetchCache = new Map<string, Promise<ServerPayload>>();
|
|
52
|
+
let payloadDecoder: PayloadDecoder | undefined;
|
|
53
|
+
|
|
54
|
+
function getRouteId(match: RouterMatchLike) {
|
|
55
|
+
const routeId = match.routeId ?? match.route?.id ?? match.id;
|
|
56
|
+
return typeof routeId === 'string' ? routeId : undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getRouteStaticData(match: RouterMatchLike) {
|
|
60
|
+
return match.route?.options?.staticData || {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getRouteParentId(match: RouterMatchLike) {
|
|
64
|
+
const parentId = match.route?.parentRoute?.id;
|
|
65
|
+
return typeof parentId === 'string' ? parentId : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function toRoutePath(match: RouterMatchLike) {
|
|
69
|
+
const path = match.route?.options?.path;
|
|
70
|
+
return typeof path === 'string' ? path : undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function toPayloadRoute(match: RouterMatchLike): PayloadRoute | undefined {
|
|
74
|
+
const routeId = getRouteId(match);
|
|
75
|
+
if (!routeId) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const staticData = getRouteStaticData(match);
|
|
80
|
+
const params =
|
|
81
|
+
match.params && typeof match.params === 'object'
|
|
82
|
+
? (match.params as Record<string, string>)
|
|
83
|
+
: {};
|
|
84
|
+
const pathname = typeof match.pathname === 'string' ? match.pathname : '';
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
handle: staticData.modernRouteHandle,
|
|
88
|
+
hasAction: Boolean(
|
|
89
|
+
staticData.modernRouteHasAction || staticData.modernRouteAction,
|
|
90
|
+
),
|
|
91
|
+
hasErrorBoundary: false,
|
|
92
|
+
hasLoader: Boolean(staticData.modernRouteHasLoader),
|
|
93
|
+
hasClientLoader: Boolean(staticData.modernRouteHasClientLoader),
|
|
94
|
+
id: routeId,
|
|
95
|
+
index: Boolean(match.route?.options?.index) || undefined,
|
|
96
|
+
params,
|
|
97
|
+
parentId: getRouteParentId(match),
|
|
98
|
+
path: toRoutePath(match),
|
|
99
|
+
pathname,
|
|
100
|
+
pathnameBase:
|
|
101
|
+
typeof match.pathnameBase === 'string' ? match.pathnameBase : pathname,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function createTanstackRscServerPayload(
|
|
106
|
+
router: TanstackPayloadRouterLike,
|
|
107
|
+
options: {
|
|
108
|
+
omitClientLoaderData?: boolean;
|
|
109
|
+
} = {},
|
|
110
|
+
): ServerPayload {
|
|
111
|
+
const matches = Array.isArray(router.state?.matches)
|
|
112
|
+
? (router.state.matches as RouterMatchLike[])
|
|
113
|
+
: [];
|
|
114
|
+
const routes: PayloadRoute[] = [];
|
|
115
|
+
const loaderData: Record<string, unknown> = {};
|
|
116
|
+
const errors: Record<string, unknown> = {};
|
|
117
|
+
|
|
118
|
+
for (const match of matches) {
|
|
119
|
+
const payloadRoute = toPayloadRoute(match);
|
|
120
|
+
if (!payloadRoute) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
routes.push(payloadRoute);
|
|
125
|
+
|
|
126
|
+
if (
|
|
127
|
+
'loaderData' in match &&
|
|
128
|
+
typeof match.loaderData !== 'undefined' &&
|
|
129
|
+
!(options.omitClientLoaderData && payloadRoute.hasClientLoader)
|
|
130
|
+
) {
|
|
131
|
+
loaderData[payloadRoute.id] = match.loaderData;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (typeof match.error !== 'undefined') {
|
|
135
|
+
errors[payloadRoute.id] = match.error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
type: 'render',
|
|
141
|
+
actionData: null,
|
|
142
|
+
errors: Object.keys(errors).length > 0 ? errors : null,
|
|
143
|
+
loaderData,
|
|
144
|
+
location: router.state?.location as ServerPayload['location'],
|
|
145
|
+
routes,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function handleTanstackRscRedirect(
|
|
150
|
+
headers: Headers,
|
|
151
|
+
basename: string,
|
|
152
|
+
status: number,
|
|
153
|
+
): Response {
|
|
154
|
+
const newHeaders = new Headers(headers);
|
|
155
|
+
let redirectUrl = headers.get('Location') || '/';
|
|
156
|
+
|
|
157
|
+
if (basename !== '/') {
|
|
158
|
+
redirectUrl = redirectUrl.replace(basename, '') || '/';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
newHeaders.set('X-Modernjs-Redirect', redirectUrl);
|
|
162
|
+
newHeaders.set('X-Modernjs-BaseUrl', basename);
|
|
163
|
+
newHeaders.delete('Location');
|
|
164
|
+
|
|
165
|
+
return new Response(null, {
|
|
166
|
+
headers: newHeaders,
|
|
167
|
+
status,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function isTanstackRscPayloadNavigationEnabled() {
|
|
172
|
+
return typeof window !== 'undefined';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function decodePayload(stream: ReadableStream<Uint8Array>) {
|
|
176
|
+
if (payloadDecoder) {
|
|
177
|
+
return payloadDecoder(stream);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const runtime = await import('@modern-js/runtime/rsc/client');
|
|
181
|
+
return runtime.createFromReadableStream(stream);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function isServerPayload(value: unknown): value is ServerPayload {
|
|
185
|
+
return (
|
|
186
|
+
Boolean(value) &&
|
|
187
|
+
typeof value === 'object' &&
|
|
188
|
+
(value as ServerPayload).type === 'render' &&
|
|
189
|
+
Array.isArray((value as ServerPayload).routes)
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function createPayloadFetchKey(request: Request) {
|
|
194
|
+
return request.url;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function isAbsoluteUrl(value: string) {
|
|
198
|
+
try {
|
|
199
|
+
void new URL(value);
|
|
200
|
+
return true;
|
|
201
|
+
} catch {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function fetchTanstackRscPayload(request: Request) {
|
|
207
|
+
const headers = new Headers(request.headers);
|
|
208
|
+
headers.set('x-rsc-tree', 'true');
|
|
209
|
+
|
|
210
|
+
const response = await fetch(request.url, {
|
|
211
|
+
credentials: 'same-origin',
|
|
212
|
+
headers,
|
|
213
|
+
method: 'GET',
|
|
214
|
+
signal: request.signal,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const redirectLocation = response.headers.get('X-Modernjs-Redirect');
|
|
218
|
+
if (redirectLocation) {
|
|
219
|
+
if (isAbsoluteUrl(redirectLocation)) {
|
|
220
|
+
throw redirect({ href: redirectLocation });
|
|
221
|
+
}
|
|
222
|
+
throw redirect({ to: redirectLocation || '/' });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (response.status === 404 && !response.body) {
|
|
226
|
+
throw notFound();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!response.body) {
|
|
230
|
+
throw new Error('TanStack RSC payload response body is null.');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const payload = await decodePayload(response.body);
|
|
234
|
+
if (!isServerPayload(payload)) {
|
|
235
|
+
throw new Error('Unexpected TanStack RSC payload type.');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return payload;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function loadTanstackRscPayload(request: Request) {
|
|
242
|
+
const key = createPayloadFetchKey(request);
|
|
243
|
+
let payloadPromise = payloadFetchCache.get(key);
|
|
244
|
+
if (!payloadPromise) {
|
|
245
|
+
payloadPromise = fetchTanstackRscPayload(request).finally(() => {
|
|
246
|
+
payloadFetchCache.delete(key);
|
|
247
|
+
});
|
|
248
|
+
payloadFetchCache.set(key, payloadPromise);
|
|
249
|
+
}
|
|
250
|
+
return payloadPromise;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function isSerializedNotFound(value: unknown) {
|
|
254
|
+
return (
|
|
255
|
+
Boolean(value) &&
|
|
256
|
+
typeof value === 'object' &&
|
|
257
|
+
(value as { isNotFound?: unknown }).isNotFound === true
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function toRouteErrors(payload: ServerPayload) {
|
|
262
|
+
return payload.errors && typeof payload.errors === 'object'
|
|
263
|
+
? (payload.errors as Record<string, unknown>)
|
|
264
|
+
: {};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function toRouteLoaderData(payload: ServerPayload) {
|
|
268
|
+
return payload.loaderData && typeof payload.loaderData === 'object'
|
|
269
|
+
? (payload.loaderData as Record<string, unknown>)
|
|
270
|
+
: {};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export async function loadTanstackRscRouteData({
|
|
274
|
+
hasClientLoader,
|
|
275
|
+
loadClientData,
|
|
276
|
+
request,
|
|
277
|
+
routeId,
|
|
278
|
+
}: LoadRouteDataOptions) {
|
|
279
|
+
if (hasClientLoader) {
|
|
280
|
+
return loadClientData();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!routeId) {
|
|
284
|
+
return loadClientData();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const payload = await loadTanstackRscPayload(request);
|
|
288
|
+
const errors = toRouteErrors(payload);
|
|
289
|
+
const routeError = errors[routeId];
|
|
290
|
+
if (typeof routeError !== 'undefined') {
|
|
291
|
+
if (isSerializedNotFound(routeError)) {
|
|
292
|
+
throw notFound({
|
|
293
|
+
...(routeError as Record<string, unknown>),
|
|
294
|
+
routeId,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
throw routeError;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const loaderData = toRouteLoaderData(payload);
|
|
301
|
+
if (routeId in loaderData) {
|
|
302
|
+
return loaderData[routeId];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const payloadRoute = payload.routes.find(route => route.id === routeId);
|
|
306
|
+
if (payloadRoute && payloadRoute.hasClientLoader) {
|
|
307
|
+
return loadClientData();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function __setTanstackRscPayloadDecoderForTests(
|
|
314
|
+
decoder?: PayloadDecoder,
|
|
315
|
+
) {
|
|
316
|
+
payloadDecoder = decoder;
|
|
317
|
+
payloadFetchCache.clear();
|
|
318
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createFromReadableStream,
|
|
3
|
+
renderRsc,
|
|
4
|
+
} from '@modern-js/runtime/rsc/server';
|
|
5
|
+
import { createSerializationAdapter } from '@tanstack/react-router';
|
|
6
|
+
import { RawStream } from '@tanstack/router-core';
|
|
7
|
+
import React, { createElement, use } from 'react';
|
|
8
|
+
import { ClientSlot } from './ClientSlot';
|
|
9
|
+
import { createRscProxy } from './createRscProxy';
|
|
10
|
+
import { ReplayableStream } from './ReplayableStream';
|
|
11
|
+
import { sanitizeSlotArgs } from './slotUsageSanitizer';
|
|
12
|
+
import {
|
|
13
|
+
type AnyCompositeComponent,
|
|
14
|
+
type AnyRenderableServerComponent,
|
|
15
|
+
isRenderableServerComponent,
|
|
16
|
+
isServerComponent,
|
|
17
|
+
RSC_SLOT_USAGES_STREAM,
|
|
18
|
+
type RscSlotUsageEvent,
|
|
19
|
+
SERVER_COMPONENT_STREAM,
|
|
20
|
+
type ServerComponentStream,
|
|
21
|
+
} from './symbols';
|
|
22
|
+
|
|
23
|
+
export { CompositeComponent } from './CompositeComponent';
|
|
24
|
+
export type {
|
|
25
|
+
AnyCompositeComponent,
|
|
26
|
+
AnyRenderableServerComponent,
|
|
27
|
+
CompositeComponentProps,
|
|
28
|
+
} from './symbols';
|
|
29
|
+
|
|
30
|
+
type SlotEmitter<T> = {
|
|
31
|
+
close: () => void;
|
|
32
|
+
emit: (value: T) => void;
|
|
33
|
+
stream: ReadableStream<T>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function createTreeGetter(stream: ServerComponentStream) {
|
|
37
|
+
let ready = false;
|
|
38
|
+
let tree: unknown;
|
|
39
|
+
const treePromise = Promise.resolve(
|
|
40
|
+
createFromReadableStream(stream.createReplayStream()),
|
|
41
|
+
).then(value => {
|
|
42
|
+
tree = value;
|
|
43
|
+
ready = true;
|
|
44
|
+
return value;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return () => {
|
|
48
|
+
if (ready) {
|
|
49
|
+
return tree;
|
|
50
|
+
}
|
|
51
|
+
return use(treePromise);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createSlotProxy(options?: {
|
|
56
|
+
onSlotCall?: (slot: string, args: unknown[]) => void;
|
|
57
|
+
}) {
|
|
58
|
+
const cache = new Map<string, (...args: unknown[]) => React.ReactNode>();
|
|
59
|
+
return new Proxy({} as Record<string, unknown>, {
|
|
60
|
+
get(_target, prop) {
|
|
61
|
+
if (prop === 'then' || typeof prop !== 'string') {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (prop === 'children') {
|
|
66
|
+
options?.onSlotCall?.('children', []);
|
|
67
|
+
return createElement(ClientSlot, { args: [], slot: 'children' });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!cache.has(prop)) {
|
|
71
|
+
cache.set(prop, (...args: unknown[]) => {
|
|
72
|
+
options?.onSlotCall?.(prop, args);
|
|
73
|
+
return createElement(ClientSlot, { args, slot: prop });
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return cache.get(prop);
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toReplayableFlightStream(
|
|
82
|
+
node: React.ReactElement,
|
|
83
|
+
handlers?: {
|
|
84
|
+
onCancel?: () => void;
|
|
85
|
+
onDone?: () => void;
|
|
86
|
+
onError?: () => void;
|
|
87
|
+
},
|
|
88
|
+
) {
|
|
89
|
+
const flightStream = renderRsc({
|
|
90
|
+
element: node,
|
|
91
|
+
}) as ReadableStream<Uint8Array>;
|
|
92
|
+
return new ReplayableStream(
|
|
93
|
+
handlers ? wrapReadableStream(flightStream, handlers) : flightStream,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function renderServerComponent<TNode extends React.ReactNode>(
|
|
98
|
+
node: TNode,
|
|
99
|
+
): Promise<TNode & AnyRenderableServerComponent<TNode>> {
|
|
100
|
+
const stream = toReplayableFlightStream(<>{node}</>);
|
|
101
|
+
const streamWrapper: ServerComponentStream = {
|
|
102
|
+
createReplayStream: () => stream.createReplayStream(),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return createRscProxy(createTreeGetter(streamWrapper), {
|
|
106
|
+
renderable: true,
|
|
107
|
+
stream: streamWrapper,
|
|
108
|
+
}) as TNode & AnyRenderableServerComponent<TNode>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function createCompositeComponent<
|
|
112
|
+
TProps extends object = Record<string, unknown>,
|
|
113
|
+
TReturn = React.ReactNode,
|
|
114
|
+
>(
|
|
115
|
+
component: (props: TProps) => React.ReactNode | Promise<React.ReactNode>,
|
|
116
|
+
): Promise<AnyCompositeComponent<TProps, TReturn>> {
|
|
117
|
+
const slotUsagesEmitter =
|
|
118
|
+
process.env.NODE_ENV === 'development'
|
|
119
|
+
? createReadableStreamEmitter<RscSlotUsageEvent>()
|
|
120
|
+
: undefined;
|
|
121
|
+
|
|
122
|
+
const slotProxy = createSlotProxy({
|
|
123
|
+
onSlotCall: slotUsagesEmitter
|
|
124
|
+
? (slot, args) => {
|
|
125
|
+
const sanitizedArgs = sanitizeSlotArgs(args);
|
|
126
|
+
slotUsagesEmitter.emit({
|
|
127
|
+
slot,
|
|
128
|
+
...(sanitizedArgs.length ? { args: sanitizedArgs } : {}),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
: undefined,
|
|
132
|
+
}) as TProps;
|
|
133
|
+
|
|
134
|
+
async function ServerComponentWrapper() {
|
|
135
|
+
return component(slotProxy);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const flightStream = toReplayableFlightStream(
|
|
139
|
+
createElement(ServerComponentWrapper as React.FC),
|
|
140
|
+
slotUsagesEmitter
|
|
141
|
+
? {
|
|
142
|
+
onCancel: slotUsagesEmitter.close,
|
|
143
|
+
onDone: slotUsagesEmitter.close,
|
|
144
|
+
onError: slotUsagesEmitter.close,
|
|
145
|
+
}
|
|
146
|
+
: undefined,
|
|
147
|
+
);
|
|
148
|
+
const streamWrapper: ServerComponentStream = {
|
|
149
|
+
createReplayStream: () => flightStream.createReplayStream(),
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return createRscProxy(createTreeGetter(streamWrapper), {
|
|
153
|
+
renderable: false,
|
|
154
|
+
slotUsagesStream: slotUsagesEmitter?.stream,
|
|
155
|
+
stream: streamWrapper,
|
|
156
|
+
}) as AnyCompositeComponent<TProps, TReturn>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
type SerializedRsc = {
|
|
160
|
+
kind: 'renderable' | 'composite';
|
|
161
|
+
stream: RawStream;
|
|
162
|
+
slotUsagesStream?: ReadableStream<RscSlotUsageEvent>;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const adapter = createSerializationAdapter({
|
|
166
|
+
key: '$MODERN_TANSTACK_RSC',
|
|
167
|
+
test: isServerComponent,
|
|
168
|
+
toSerializable: (component): SerializedRsc => {
|
|
169
|
+
const streamWrapper = component[SERVER_COMPONENT_STREAM];
|
|
170
|
+
if (!streamWrapper) {
|
|
171
|
+
throw new Error('Cannot serialize TanStack RSC without a Flight stream.');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const kind = isRenderableServerComponent(component)
|
|
175
|
+
? 'renderable'
|
|
176
|
+
: 'composite';
|
|
177
|
+
const slotUsagesStream =
|
|
178
|
+
process.env.NODE_ENV === 'development' && kind === 'composite'
|
|
179
|
+
? component[RSC_SLOT_USAGES_STREAM]
|
|
180
|
+
: undefined;
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
kind,
|
|
184
|
+
stream: new RawStream(streamWrapper.createReplayStream(), {
|
|
185
|
+
hint: 'text',
|
|
186
|
+
}),
|
|
187
|
+
...(slotUsagesStream ? { slotUsagesStream } : {}),
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
fromSerializable: (): never => {
|
|
191
|
+
throw new Error('TanStack RSC data should not be deserialized on server.');
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
export function getTanstackRscSerializationAdapters() {
|
|
196
|
+
return [adapter];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export const rscSerializationAdapter = getTanstackRscSerializationAdapters;
|
|
200
|
+
|
|
201
|
+
function createReadableStreamEmitter<T>(): SlotEmitter<T> {
|
|
202
|
+
let closed = false;
|
|
203
|
+
const queue: T[] = [];
|
|
204
|
+
let controller: ReadableStreamDefaultController<T> | null = null;
|
|
205
|
+
|
|
206
|
+
const stream = new ReadableStream<T>({
|
|
207
|
+
start(ctrl) {
|
|
208
|
+
controller = ctrl;
|
|
209
|
+
for (const value of queue) {
|
|
210
|
+
ctrl.enqueue(value);
|
|
211
|
+
}
|
|
212
|
+
queue.length = 0;
|
|
213
|
+
if (closed) {
|
|
214
|
+
ctrl.close();
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
cancel() {
|
|
218
|
+
closed = true;
|
|
219
|
+
controller = null;
|
|
220
|
+
queue.length = 0;
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const emit = (value: T) => {
|
|
225
|
+
if (closed) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (!controller) {
|
|
229
|
+
queue.push(value);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
controller.enqueue(value);
|
|
234
|
+
} catch {}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const close = () => {
|
|
238
|
+
if (closed) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
closed = true;
|
|
242
|
+
try {
|
|
243
|
+
controller?.close();
|
|
244
|
+
} catch {}
|
|
245
|
+
controller = null;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
return { close, emit, stream };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function wrapReadableStream<T>(
|
|
252
|
+
source: ReadableStream<T>,
|
|
253
|
+
handlers: {
|
|
254
|
+
onCancel?: () => void;
|
|
255
|
+
onDone?: () => void;
|
|
256
|
+
onError?: () => void;
|
|
257
|
+
},
|
|
258
|
+
) {
|
|
259
|
+
const reader = source.getReader();
|
|
260
|
+
let finished = false;
|
|
261
|
+
|
|
262
|
+
const finish = (kind: 'cancel' | 'done' | 'error') => {
|
|
263
|
+
if (finished) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
finished = true;
|
|
267
|
+
if (kind === 'cancel') {
|
|
268
|
+
handlers.onCancel?.();
|
|
269
|
+
} else if (kind === 'error') {
|
|
270
|
+
handlers.onError?.();
|
|
271
|
+
} else {
|
|
272
|
+
handlers.onDone?.();
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
reader.releaseLock();
|
|
276
|
+
} catch {}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
return new ReadableStream<T>({
|
|
280
|
+
async pull(controller) {
|
|
281
|
+
try {
|
|
282
|
+
const { done, value } = await reader.read();
|
|
283
|
+
if (done) {
|
|
284
|
+
controller.close();
|
|
285
|
+
finish('done');
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
controller.enqueue(value);
|
|
289
|
+
} catch (err) {
|
|
290
|
+
try {
|
|
291
|
+
controller.error(err);
|
|
292
|
+
} catch {}
|
|
293
|
+
finish('error');
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
async cancel(reason) {
|
|
297
|
+
try {
|
|
298
|
+
await reader.cancel(reason);
|
|
299
|
+
} catch {}
|
|
300
|
+
finish('cancel');
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { isValidElement } from 'react';
|
|
2
|
+
import type { SerializableSlotArg } from './symbols';
|
|
3
|
+
|
|
4
|
+
const REACT_ELEMENT_TYPE = Symbol.for('react.element');
|
|
5
|
+
const REACT_PORTAL_TYPE = Symbol.for('react.portal');
|
|
6
|
+
const REACT_TRANSITIONAL_ELEMENT_TYPE = Symbol.for(
|
|
7
|
+
'react.transitional.element',
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
function isReactElementLike(value: unknown) {
|
|
11
|
+
if (!value || (typeof value !== 'object' && typeof value !== 'function')) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (isValidElement(value)) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const type = (value as { $$typeof?: unknown }).$$typeof;
|
|
20
|
+
return (
|
|
21
|
+
type === REACT_ELEMENT_TYPE ||
|
|
22
|
+
type === REACT_TRANSITIONAL_ELEMENT_TYPE ||
|
|
23
|
+
type === REACT_PORTAL_TYPE
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sanitizeSlotArg(
|
|
28
|
+
value: unknown,
|
|
29
|
+
seen: WeakSet<object>,
|
|
30
|
+
): SerializableSlotArg {
|
|
31
|
+
if (isReactElementLike(value)) {
|
|
32
|
+
return 'React element';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (value === null || value === undefined) {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (
|
|
40
|
+
typeof value === 'string' ||
|
|
41
|
+
typeof value === 'number' ||
|
|
42
|
+
typeof value === 'boolean' ||
|
|
43
|
+
typeof value === 'bigint'
|
|
44
|
+
) {
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof value !== 'object' && typeof value !== 'function') {
|
|
49
|
+
return String(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (seen.has(value)) {
|
|
53
|
+
return '[Circular]';
|
|
54
|
+
}
|
|
55
|
+
seen.add(value);
|
|
56
|
+
|
|
57
|
+
if (Array.isArray(value)) {
|
|
58
|
+
return value.map(item => sanitizeSlotArg(item, seen));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const proto = Object.getPrototypeOf(value);
|
|
62
|
+
if (proto === Object.prototype || proto === null) {
|
|
63
|
+
const out: Record<string, SerializableSlotArg> = {};
|
|
64
|
+
for (const [key, item] of Object.entries(value)) {
|
|
65
|
+
out[key] = sanitizeSlotArg(item, seen);
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return String(value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function sanitizeSlotArgs(args: unknown[]) {
|
|
74
|
+
const seen = new WeakSet<object>();
|
|
75
|
+
return args.map(arg => sanitizeSlotArg(arg, seen));
|
|
76
|
+
}
|