@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.9 → 3.9.3-ultramodern.2
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/cjs/cli/artifacts.js +15 -1
- package/dist/cjs/cli/index.js +13 -2
- package/dist/cjs/cli/routeMetadata.js +131 -0
- package/dist/cjs/cli/rscRouteLayer.js +86 -0
- package/dist/cjs/cli/tanstackTypes/canonicalRoutes.js +7 -7
- package/dist/cjs/cli/tanstackTypes/shared.js +0 -14
- package/dist/cjs/cli/tanstackTypes/source.js +28 -63
- package/dist/cjs/runtime/lifecycle.js +0 -3
- package/dist/cjs/runtime/loaderBridge.js +10 -4
- package/dist/cjs/runtime/navigation.js +102 -0
- package/dist/cjs/runtime/plugin.js +10 -1
- package/dist/cjs/runtime/plugin.node.js +89 -64
- package/dist/cjs/runtime/routeTree/construction.js +19 -61
- package/dist/cjs/runtime/routeTree/loaders.js +1 -1
- package/dist/cjs/runtime/routeTree/paths.js +0 -6
- package/dist/cjs/runtime/ssrManagedTags.js +2 -12
- package/dist/cjs/runtime/utils.js +1 -0
- package/dist/cjs/shared/routeDescriptor.js +88 -0
- package/dist/esm/cli/artifacts.mjs +13 -2
- package/dist/esm/cli/index.mjs +13 -11
- package/dist/esm/cli/routeMetadata.mjs +71 -0
- package/dist/esm/cli/rscRouteLayer.mjs +35 -0
- package/dist/esm/cli/tanstackTypes/canonicalRoutes.mjs +7 -7
- package/dist/esm/cli/tanstackTypes/shared.mjs +1 -9
- package/dist/esm/cli/tanstackTypes/source.mjs +29 -64
- package/dist/esm/runtime/lifecycle.mjs +1 -1
- package/dist/esm/runtime/loaderBridge.mjs +8 -5
- package/dist/esm/runtime/navigation.mjs +64 -0
- package/dist/esm/runtime/plugin.mjs +10 -1
- package/dist/esm/runtime/plugin.node.mjs +91 -66
- package/dist/esm/runtime/routeTree/construction.mjs +19 -61
- package/dist/esm/runtime/routeTree/loaders.mjs +2 -2
- package/dist/esm/runtime/routeTree/paths.mjs +1 -4
- package/dist/esm/runtime/ssrManagedTags.mjs +1 -8
- package/dist/esm/runtime/utils.mjs +1 -0
- package/dist/esm/shared/routeDescriptor.mjs +50 -0
- package/dist/esm-node/cli/artifacts.mjs +13 -2
- package/dist/esm-node/cli/index.mjs +13 -11
- package/dist/esm-node/cli/routeMetadata.mjs +72 -0
- package/dist/esm-node/cli/rscRouteLayer.mjs +36 -0
- package/dist/esm-node/cli/tanstackTypes/canonicalRoutes.mjs +7 -7
- package/dist/esm-node/cli/tanstackTypes/shared.mjs +1 -9
- package/dist/esm-node/cli/tanstackTypes/source.mjs +29 -64
- package/dist/esm-node/runtime/lifecycle.mjs +1 -1
- package/dist/esm-node/runtime/loaderBridge.mjs +8 -5
- package/dist/esm-node/runtime/navigation.mjs +65 -0
- package/dist/esm-node/runtime/plugin.mjs +10 -1
- package/dist/esm-node/runtime/plugin.node.mjs +91 -66
- package/dist/esm-node/runtime/routeTree/construction.mjs +19 -61
- package/dist/esm-node/runtime/routeTree/loaders.mjs +2 -2
- package/dist/esm-node/runtime/routeTree/paths.mjs +1 -4
- package/dist/esm-node/runtime/ssrManagedTags.mjs +1 -8
- package/dist/esm-node/runtime/utils.mjs +1 -0
- package/dist/esm-node/shared/routeDescriptor.mjs +51 -0
- package/dist/types/cli/artifacts.d.ts +16 -7
- package/dist/types/cli/index.d.ts +1 -1
- package/dist/types/cli/routeMetadata.d.ts +18 -0
- package/dist/types/cli/rscRouteLayer.d.ts +11 -0
- package/dist/types/cli/tanstackTypes/shared.d.ts +0 -2
- package/dist/types/cli/tanstackTypes/source.d.ts +3 -2
- package/dist/types/runtime/lifecycle.d.ts +1 -1
- package/dist/types/runtime/loaderBridge.d.ts +1 -0
- package/dist/types/runtime/navigation.d.ts +4 -0
- package/dist/types/runtime/pluginShared.d.ts +5 -0
- package/dist/types/runtime/routeTree/paths.d.ts +0 -2
- package/dist/types/runtime/ssrManagedTags.d.ts +0 -1
- package/dist/types/runtime/ssrTypes.d.ts +0 -7
- package/dist/types/shared/routeDescriptor.d.ts +26 -0
- package/package.json +32 -26
- package/src/cli/artifacts.ts +59 -8
- package/src/cli/index.ts +17 -4
- package/src/cli/routeMetadata.ts +121 -0
- package/src/cli/rscRouteLayer.ts +93 -0
- package/src/cli/tanstackTypes/canonicalRoutes.ts +17 -12
- package/src/cli/tanstackTypes/shared.ts +0 -14
- package/src/cli/tanstackTypes/source.ts +48 -120
- package/src/runtime/lifecycle.ts +0 -1
- package/src/runtime/loaderBridge.ts +9 -9
- package/src/runtime/navigation.ts +78 -0
- package/src/runtime/plugin.node.tsx +121 -91
- package/src/runtime/plugin.tsx +10 -0
- package/src/runtime/pluginShared.ts +12 -0
- package/src/runtime/routeTree/construction.ts +32 -112
- package/src/runtime/routeTree/loaders.ts +2 -4
- package/src/runtime/routeTree/paths.ts +0 -6
- package/src/runtime/ssrManagedTags.ts +0 -17
- package/src/runtime/ssrTypes.ts +0 -7
- package/src/runtime/utils.tsx +1 -0
- package/src/shared/routeDescriptor.ts +102 -0
- package/dist/esm/rslib-runtime~1.mjs +0 -18
- package/dist/esm-node/rslib-runtime~0.mjs +0 -19
- package/rslib.config.mts +0 -4
- package/rstest.config.mts +0 -53
- package/tests/router/cli.test.ts +0 -420
- package/tests/router/clientHydration.test.tsx +0 -200
- package/tests/router/dataMutation.test.tsx +0 -428
- package/tests/router/fixtures/router-gen.oxlint.json +0 -5
- package/tests/router/flightSerialization.roundtrip.test.tsx +0 -140
- package/tests/router/generateRouteArtifacts.test.ts +0 -146
- package/tests/router/loaderBridge.test.ts +0 -207
- package/tests/router/localisedIdentity.test.tsx +0 -95
- package/tests/router/prefetchLink.test.tsx +0 -122
- package/tests/router/prefetchLinkPreload.test.tsx +0 -65
- package/tests/router/routeHooks.test.ts +0 -49
- package/tests/router/routeTree.test.ts +0 -240
- package/tests/router/rsc.test.tsx +0 -480
- package/tests/router/serverPlugin.test.ts +0 -157
- package/tests/router/slotUsageSanitizer.test.ts +0 -23
- package/tests/router/ssrPreload.test.ts +0 -30
- package/tests/router/tanstackTypes.test.ts +0 -384
- package/tsconfig.json +0 -12
- package/tsconfig.tsgo.json +0 -6
|
@@ -1,480 +0,0 @@
|
|
|
1
|
-
import { UNSAFE_ErrorResponseImpl as ErrorResponseImpl } from '@modern-js/runtime-utils/router';
|
|
2
|
-
import {
|
|
3
|
-
__setTanstackRscPayloadDecoderForTests,
|
|
4
|
-
createTanstackRscServerPayload,
|
|
5
|
-
handleTanstackRscRedirect,
|
|
6
|
-
loadTanstackRscRouteData,
|
|
7
|
-
} from '../../src/runtime/rsc/payloadRouter';
|
|
8
|
-
import { ReplayableStream } from '../../src/runtime/rsc/ReplayableStream';
|
|
9
|
-
|
|
10
|
-
async function readAll(stream: ReadableStream<Uint8Array>) {
|
|
11
|
-
const reader = stream.getReader();
|
|
12
|
-
const chunks: number[] = [];
|
|
13
|
-
|
|
14
|
-
for (;;) {
|
|
15
|
-
const { done, value } = await reader.read();
|
|
16
|
-
if (done) {
|
|
17
|
-
break;
|
|
18
|
-
}
|
|
19
|
-
chunks.push(...value);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return chunks;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function withNodeEnv<T>(value: string, callback: () => T): T {
|
|
26
|
-
const original = process.env.NODE_ENV;
|
|
27
|
-
process.env.NODE_ENV = value;
|
|
28
|
-
try {
|
|
29
|
-
return callback();
|
|
30
|
-
} finally {
|
|
31
|
-
process.env.NODE_ENV = original;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
describe('tanstack rsc runtime helpers', () => {
|
|
36
|
-
afterEach(() => {
|
|
37
|
-
__setTanstackRscPayloadDecoderForTests();
|
|
38
|
-
rstest.restoreAllMocks();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('ReplayableStream creates independent readers from one source', async () => {
|
|
42
|
-
const source = new ReadableStream<Uint8Array>({
|
|
43
|
-
start(controller) {
|
|
44
|
-
controller.enqueue(new Uint8Array([1, 2]));
|
|
45
|
-
controller.enqueue(new Uint8Array([3]));
|
|
46
|
-
controller.close();
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
const replayable = new ReplayableStream(source);
|
|
50
|
-
|
|
51
|
-
await expect(readAll(replayable.createReplayStream())).resolves.toEqual([
|
|
52
|
-
1, 2, 3,
|
|
53
|
-
]);
|
|
54
|
-
await expect(readAll(replayable.createReplayStream())).resolves.toEqual([
|
|
55
|
-
1, 2, 3,
|
|
56
|
-
]);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('creates TanStack RSC server payload and omits client-loader data during RSC navigation', () => {
|
|
60
|
-
const payload = createTanstackRscServerPayload(
|
|
61
|
-
{
|
|
62
|
-
state: {
|
|
63
|
-
location: { href: '/products' },
|
|
64
|
-
matches: [
|
|
65
|
-
{
|
|
66
|
-
loaderData: { shell: true },
|
|
67
|
-
params: {},
|
|
68
|
-
pathname: '/',
|
|
69
|
-
pathnameBase: '/',
|
|
70
|
-
route: {
|
|
71
|
-
id: '__root__',
|
|
72
|
-
options: {
|
|
73
|
-
staticData: {
|
|
74
|
-
modernRouteHasLoader: true,
|
|
75
|
-
modernRouteId: 'root',
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
routeId: '__root__',
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
loaderData: { product: 1 },
|
|
83
|
-
params: {},
|
|
84
|
-
pathname: '/products',
|
|
85
|
-
pathnameBase: '/products',
|
|
86
|
-
route: {
|
|
87
|
-
id: '/products',
|
|
88
|
-
parentRoute: { id: '__root__' },
|
|
89
|
-
options: {
|
|
90
|
-
path: 'products',
|
|
91
|
-
staticData: {
|
|
92
|
-
modernRouteHandle: { section: 'shop' },
|
|
93
|
-
modernRouteHasClientLoader: true,
|
|
94
|
-
modernRouteHasLoader: true,
|
|
95
|
-
modernRouteId: 'products',
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
},
|
|
99
|
-
routeId: '/products',
|
|
100
|
-
},
|
|
101
|
-
],
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
{ omitClientLoaderData: true },
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
expect(payload).toMatchObject({
|
|
108
|
-
type: 'render',
|
|
109
|
-
loaderData: {
|
|
110
|
-
__root__: { shell: true },
|
|
111
|
-
},
|
|
112
|
-
routes: [
|
|
113
|
-
{
|
|
114
|
-
hasClientLoader: false,
|
|
115
|
-
hasLoader: true,
|
|
116
|
-
id: '__root__',
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
handle: { section: 'shop' },
|
|
120
|
-
hasClientLoader: true,
|
|
121
|
-
hasLoader: true,
|
|
122
|
-
id: '/products',
|
|
123
|
-
parentId: '__root__',
|
|
124
|
-
path: 'products',
|
|
125
|
-
},
|
|
126
|
-
],
|
|
127
|
-
});
|
|
128
|
-
expect(
|
|
129
|
-
(payload.loaderData as Record<string, unknown>)['/products'],
|
|
130
|
-
).toBeUndefined();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test('redacts production TanStack RSC server payload errors (Error, RouteErrorResponse, and plain-object)', () => {
|
|
134
|
-
const routeError = new ErrorResponseImpl(
|
|
135
|
-
500,
|
|
136
|
-
'secret status text',
|
|
137
|
-
'route secret',
|
|
138
|
-
true,
|
|
139
|
-
);
|
|
140
|
-
const serverError = new Error('server secret');
|
|
141
|
-
serverError.stack = 'stack secret';
|
|
142
|
-
|
|
143
|
-
const payload = withNodeEnv('production', () =>
|
|
144
|
-
createTanstackRscServerPayload({
|
|
145
|
-
state: {
|
|
146
|
-
location: { href: '/products' },
|
|
147
|
-
matches: [
|
|
148
|
-
{
|
|
149
|
-
error: serverError,
|
|
150
|
-
params: {},
|
|
151
|
-
pathname: '/',
|
|
152
|
-
pathnameBase: '/',
|
|
153
|
-
route: { id: '__root__' },
|
|
154
|
-
routeId: '__root__',
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
error: routeError,
|
|
158
|
-
params: {},
|
|
159
|
-
pathname: '/products',
|
|
160
|
-
pathnameBase: '/products',
|
|
161
|
-
route: {
|
|
162
|
-
id: '/products',
|
|
163
|
-
parentRoute: { id: '__root__' },
|
|
164
|
-
options: { path: 'products' },
|
|
165
|
-
},
|
|
166
|
-
routeId: '/products',
|
|
167
|
-
},
|
|
168
|
-
{
|
|
169
|
-
error: { message: 'plain secret', token: 'token secret' },
|
|
170
|
-
params: {},
|
|
171
|
-
pathname: '/plain',
|
|
172
|
-
pathnameBase: '/plain',
|
|
173
|
-
route: { id: '/plain', parentRoute: { id: '__root__' } },
|
|
174
|
-
routeId: '/plain',
|
|
175
|
-
},
|
|
176
|
-
],
|
|
177
|
-
},
|
|
178
|
-
}),
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
expect(payload.errors).toMatchObject({
|
|
182
|
-
__root__: {
|
|
183
|
-
message: 'Unexpected Server Error',
|
|
184
|
-
stack: undefined,
|
|
185
|
-
__type: 'Error',
|
|
186
|
-
},
|
|
187
|
-
'/products': {
|
|
188
|
-
status: 500,
|
|
189
|
-
statusText: 'Internal Server Error',
|
|
190
|
-
data: 'Unexpected Server Error',
|
|
191
|
-
__type: 'RouteErrorResponse',
|
|
192
|
-
},
|
|
193
|
-
'/plain': {
|
|
194
|
-
message: 'Unexpected Server Error',
|
|
195
|
-
stack: undefined,
|
|
196
|
-
__type: 'Error',
|
|
197
|
-
},
|
|
198
|
-
});
|
|
199
|
-
expect(JSON.stringify(payload.errors)).not.toContain('server secret');
|
|
200
|
-
expect(JSON.stringify(payload.errors)).not.toContain('route secret');
|
|
201
|
-
expect(JSON.stringify(payload.errors)).not.toContain('secret status text');
|
|
202
|
-
expect(JSON.stringify(payload.errors)).not.toContain('stack secret');
|
|
203
|
-
expect(JSON.stringify(payload.errors)).not.toContain('plain secret');
|
|
204
|
-
expect(JSON.stringify(payload.errors)).not.toContain('token secret');
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
test('preserves RSC redirect response status and headers in TanStack redirects', async () => {
|
|
208
|
-
const scenarios = [
|
|
209
|
-
{ location: '/base', name: 'exact basename', redirect: '/' },
|
|
210
|
-
{
|
|
211
|
-
location: '/base/login?from=%2Fbase',
|
|
212
|
-
name: 'leading basename',
|
|
213
|
-
redirect: '/login?from=%2Fbase',
|
|
214
|
-
},
|
|
215
|
-
{
|
|
216
|
-
location: '/shop/base/login',
|
|
217
|
-
name: 'mid-path basename',
|
|
218
|
-
redirect: '/shop/base/login',
|
|
219
|
-
},
|
|
220
|
-
] as const;
|
|
221
|
-
|
|
222
|
-
for (const scenario of scenarios) {
|
|
223
|
-
const response = handleTanstackRscRedirect(
|
|
224
|
-
new Headers({
|
|
225
|
-
Location: scenario.location,
|
|
226
|
-
'X-Trace': scenario.name,
|
|
227
|
-
}),
|
|
228
|
-
'/base',
|
|
229
|
-
307,
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
expect(response.status).toBe(307);
|
|
233
|
-
expect(response.headers.get('Location')).toBeNull();
|
|
234
|
-
expect(response.headers.get('X-Modernjs-BaseUrl')).toBe('/base');
|
|
235
|
-
expect(response.headers.get('X-Modernjs-Redirect')).toBe(
|
|
236
|
-
scenario.redirect,
|
|
237
|
-
);
|
|
238
|
-
expect(response.headers.get('X-Trace')).toBe(scenario.name);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
rstest.stubGlobal(
|
|
242
|
-
'fetch',
|
|
243
|
-
rstest.fn(() =>
|
|
244
|
-
Promise.resolve(
|
|
245
|
-
new Response(null, {
|
|
246
|
-
headers: {
|
|
247
|
-
'X-Modernjs-Redirect': '/login',
|
|
248
|
-
'X-Trace': 'preserved',
|
|
249
|
-
},
|
|
250
|
-
status: 308,
|
|
251
|
-
}),
|
|
252
|
-
),
|
|
253
|
-
),
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
const thrown = (await loadTanstackRscRouteData({
|
|
257
|
-
loadClientData: async () => ({ fallback: true }),
|
|
258
|
-
request: new Request('http://localhost/products'),
|
|
259
|
-
routeId: '/products',
|
|
260
|
-
}).then(
|
|
261
|
-
() => {
|
|
262
|
-
throw new Error('expected redirect');
|
|
263
|
-
},
|
|
264
|
-
err => err,
|
|
265
|
-
)) as Response & {
|
|
266
|
-
options?: { statusCode?: number; to?: string };
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
expect(thrown).toBeInstanceOf(Response);
|
|
270
|
-
expect(thrown.status).toBe(308);
|
|
271
|
-
expect(thrown.headers.get('X-Trace')).toBe('preserved');
|
|
272
|
-
expect(thrown.options?.statusCode).toBe(308);
|
|
273
|
-
expect(thrown.options?.to).toBe('/login');
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
test('separates RSC payload cache entries by URL and request method', async () => {
|
|
277
|
-
const payloads = [
|
|
278
|
-
{
|
|
279
|
-
type: 'render',
|
|
280
|
-
actionData: null,
|
|
281
|
-
errors: null,
|
|
282
|
-
loaderData: { '/method': { method: 'GET' } },
|
|
283
|
-
location: { href: '/method' },
|
|
284
|
-
routes: [{ id: '/method', hasLoader: true }],
|
|
285
|
-
},
|
|
286
|
-
{
|
|
287
|
-
type: 'render',
|
|
288
|
-
actionData: null,
|
|
289
|
-
errors: null,
|
|
290
|
-
loaderData: { '/method': { method: 'POST' } },
|
|
291
|
-
location: { href: '/method' },
|
|
292
|
-
routes: [{ id: '/method', hasLoader: true }],
|
|
293
|
-
},
|
|
294
|
-
];
|
|
295
|
-
let payloadIndex = 0;
|
|
296
|
-
const fetchMock = rstest.fn(() =>
|
|
297
|
-
Promise.resolve(new Response(JSON.stringify(payloads[payloadIndex++]))),
|
|
298
|
-
);
|
|
299
|
-
rstest.stubGlobal('fetch', fetchMock);
|
|
300
|
-
__setTanstackRscPayloadDecoderForTests(async stream =>
|
|
301
|
-
JSON.parse(await new Response(stream).text()),
|
|
302
|
-
);
|
|
303
|
-
|
|
304
|
-
const [getData, postData] = await Promise.all([
|
|
305
|
-
loadTanstackRscRouteData({
|
|
306
|
-
loadClientData: async () => ({ fallback: 'get' }),
|
|
307
|
-
request: new Request('http://localhost/method', { method: 'GET' }),
|
|
308
|
-
routeId: '/method',
|
|
309
|
-
}),
|
|
310
|
-
loadTanstackRscRouteData({
|
|
311
|
-
loadClientData: async () => ({ fallback: 'post' }),
|
|
312
|
-
request: new Request('http://localhost/method', { method: 'POST' }),
|
|
313
|
-
routeId: '/method',
|
|
314
|
-
}),
|
|
315
|
-
]);
|
|
316
|
-
|
|
317
|
-
expect(getData).toEqual({ method: 'GET' });
|
|
318
|
-
expect(postData).toEqual({ method: 'POST' });
|
|
319
|
-
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
test('separates RSC payload cache entries by forwarded request headers', async () => {
|
|
323
|
-
const payloadByUser: Record<string, unknown> = {
|
|
324
|
-
alice: {
|
|
325
|
-
type: 'render',
|
|
326
|
-
actionData: null,
|
|
327
|
-
errors: null,
|
|
328
|
-
loaderData: {
|
|
329
|
-
'/profile': { user: 'alice' },
|
|
330
|
-
},
|
|
331
|
-
location: { href: '/profile' },
|
|
332
|
-
routes: [{ id: '/profile', hasLoader: true }],
|
|
333
|
-
},
|
|
334
|
-
bob: {
|
|
335
|
-
type: 'render',
|
|
336
|
-
actionData: null,
|
|
337
|
-
errors: null,
|
|
338
|
-
loaderData: {
|
|
339
|
-
'/profile': { user: 'bob' },
|
|
340
|
-
},
|
|
341
|
-
location: { href: '/profile' },
|
|
342
|
-
routes: [{ id: '/profile', hasLoader: true }],
|
|
343
|
-
},
|
|
344
|
-
};
|
|
345
|
-
const fetchMock = rstest.fn(
|
|
346
|
-
(_url: string | URL | Request, init?: RequestInit) => {
|
|
347
|
-
const user =
|
|
348
|
-
init?.headers instanceof Headers ? init.headers.get('x-user') : null;
|
|
349
|
-
return Promise.resolve(
|
|
350
|
-
new Response(JSON.stringify(payloadByUser[user || ''])),
|
|
351
|
-
);
|
|
352
|
-
},
|
|
353
|
-
);
|
|
354
|
-
rstest.stubGlobal('fetch', fetchMock);
|
|
355
|
-
__setTanstackRscPayloadDecoderForTests(async stream =>
|
|
356
|
-
JSON.parse(await new Response(stream).text()),
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
const [aliceData, bobData] = await Promise.all([
|
|
360
|
-
loadTanstackRscRouteData({
|
|
361
|
-
loadClientData: async () => ({ fallback: 'alice' }),
|
|
362
|
-
request: new Request('http://localhost/profile', {
|
|
363
|
-
headers: { 'x-user': 'alice' },
|
|
364
|
-
}),
|
|
365
|
-
routeId: '/profile',
|
|
366
|
-
}),
|
|
367
|
-
loadTanstackRscRouteData({
|
|
368
|
-
loadClientData: async () => ({ fallback: 'bob' }),
|
|
369
|
-
request: new Request('http://localhost/profile', {
|
|
370
|
-
headers: { 'x-user': 'bob' },
|
|
371
|
-
}),
|
|
372
|
-
routeId: '/profile',
|
|
373
|
-
}),
|
|
374
|
-
]);
|
|
375
|
-
|
|
376
|
-
expect(aliceData).toEqual({ user: 'alice' });
|
|
377
|
-
expect(bobData).toEqual({ user: 'bob' });
|
|
378
|
-
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
test('rethrows matched route errors from the decoded payload', async () => {
|
|
382
|
-
const routeError = new Error('route loader failed');
|
|
383
|
-
const payload = {
|
|
384
|
-
type: 'render',
|
|
385
|
-
actionData: null,
|
|
386
|
-
errors: { '/broken': routeError },
|
|
387
|
-
loaderData: {},
|
|
388
|
-
location: { href: '/broken' },
|
|
389
|
-
routes: [{ id: '/broken', hasLoader: true }],
|
|
390
|
-
};
|
|
391
|
-
rstest.stubGlobal(
|
|
392
|
-
'fetch',
|
|
393
|
-
rstest.fn(() => Promise.resolve(new Response('payload'))),
|
|
394
|
-
);
|
|
395
|
-
__setTanstackRscPayloadDecoderForTests(async () => payload);
|
|
396
|
-
|
|
397
|
-
await expect(
|
|
398
|
-
loadTanstackRscRouteData({
|
|
399
|
-
loadClientData: async () => ({ fallback: true }),
|
|
400
|
-
request: new Request('http://localhost/broken'),
|
|
401
|
-
routeId: '/broken',
|
|
402
|
-
}),
|
|
403
|
-
).rejects.toBe(routeError);
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
test('returns undefined for a route id missing from the decoded payload', async () => {
|
|
407
|
-
const loadClientData = rstest.fn(async () => ({ fallback: true }));
|
|
408
|
-
const payload = {
|
|
409
|
-
type: 'render',
|
|
410
|
-
actionData: null,
|
|
411
|
-
errors: null,
|
|
412
|
-
loaderData: { '/known': { known: true } },
|
|
413
|
-
location: { href: '/known' },
|
|
414
|
-
routes: [{ id: '/known', hasLoader: true }],
|
|
415
|
-
};
|
|
416
|
-
rstest.stubGlobal(
|
|
417
|
-
'fetch',
|
|
418
|
-
rstest.fn(() => Promise.resolve(new Response('payload'))),
|
|
419
|
-
);
|
|
420
|
-
__setTanstackRscPayloadDecoderForTests(async () => payload);
|
|
421
|
-
|
|
422
|
-
await expect(
|
|
423
|
-
loadTanstackRscRouteData({
|
|
424
|
-
loadClientData,
|
|
425
|
-
request: new Request('http://localhost/missing'),
|
|
426
|
-
routeId: '/missing',
|
|
427
|
-
}),
|
|
428
|
-
).resolves.toBeUndefined();
|
|
429
|
-
expect(loadClientData).not.toHaveBeenCalled();
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
test('uses client loader data without requesting an RSC payload for client-loader routes', async () => {
|
|
433
|
-
const fetchMock = rstest.fn();
|
|
434
|
-
const loadClientData = rstest.fn(async () => ({ client: true }));
|
|
435
|
-
rstest.stubGlobal('fetch', fetchMock);
|
|
436
|
-
|
|
437
|
-
await expect(
|
|
438
|
-
loadTanstackRscRouteData({
|
|
439
|
-
hasClientLoader: true,
|
|
440
|
-
loadClientData,
|
|
441
|
-
request: new Request('http://localhost/client'),
|
|
442
|
-
routeId: '/client',
|
|
443
|
-
}),
|
|
444
|
-
).resolves.toEqual({ client: true });
|
|
445
|
-
|
|
446
|
-
expect(loadClientData).toHaveBeenCalledTimes(1);
|
|
447
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
test('rethrows serialized notFound payload errors for the matched route', async () => {
|
|
451
|
-
const payload = {
|
|
452
|
-
type: 'render',
|
|
453
|
-
actionData: null,
|
|
454
|
-
errors: {
|
|
455
|
-
'/missing': {
|
|
456
|
-
isNotFound: true,
|
|
457
|
-
},
|
|
458
|
-
},
|
|
459
|
-
loaderData: {},
|
|
460
|
-
location: { href: '/missing' },
|
|
461
|
-
routes: [{ id: '/missing', hasLoader: true }],
|
|
462
|
-
};
|
|
463
|
-
rstest.stubGlobal(
|
|
464
|
-
'fetch',
|
|
465
|
-
rstest.fn(() => Promise.resolve(new Response('payload'))),
|
|
466
|
-
);
|
|
467
|
-
__setTanstackRscPayloadDecoderForTests(async () => payload);
|
|
468
|
-
|
|
469
|
-
await expect(
|
|
470
|
-
loadTanstackRscRouteData({
|
|
471
|
-
loadClientData: async () => ({ fallback: true }),
|
|
472
|
-
request: new Request('http://localhost/missing'),
|
|
473
|
-
routeId: '/missing',
|
|
474
|
-
}),
|
|
475
|
-
).rejects.toMatchObject({
|
|
476
|
-
isNotFound: true,
|
|
477
|
-
routeId: '/missing',
|
|
478
|
-
});
|
|
479
|
-
});
|
|
480
|
-
});
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import type { TInternalRuntimeContext } from '@modern-js/runtime/context';
|
|
2
|
-
import { routerProviderRegistryHooks } from '@modern-js/runtime/context';
|
|
3
|
-
import { type AnyRouter, RouterProvider } from '@tanstack/react-router';
|
|
4
|
-
import { createElement } from 'react';
|
|
5
|
-
import { renderToString } from 'react-dom/server';
|
|
6
|
-
import {
|
|
7
|
-
getRouterRuntimeState,
|
|
8
|
-
getRouterServerSnapshot,
|
|
9
|
-
} from '../../src/runtime/lifecycle';
|
|
10
|
-
import { tanstackRouterPlugin } from '../../src/runtime/plugin.node';
|
|
11
|
-
import type { TanstackRouterPluginAPI } from '../../src/runtime/pluginShared';
|
|
12
|
-
import type { RouterConfig } from '../../src/runtime/types';
|
|
13
|
-
|
|
14
|
-
type BeforeRenderListener = Parameters<
|
|
15
|
-
TanstackRouterPluginAPI['onBeforeRender']
|
|
16
|
-
>[0];
|
|
17
|
-
|
|
18
|
-
function collectBeforeRender(
|
|
19
|
-
createRoutes: NonNullable<RouterConfig['createRoutes']>,
|
|
20
|
-
) {
|
|
21
|
-
let listener: BeforeRenderListener | undefined;
|
|
22
|
-
tanstackRouterPlugin({ createRoutes }).setup?.({
|
|
23
|
-
getRuntimeConfig: () => ({}),
|
|
24
|
-
getHooks: () => routerProviderRegistryHooks,
|
|
25
|
-
onBeforeRender: nextListener => {
|
|
26
|
-
listener = nextListener;
|
|
27
|
-
},
|
|
28
|
-
wrapRoot: () => {},
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
if (!listener) {
|
|
32
|
-
throw new Error('Expected the TanStack server plugin to register a hook');
|
|
33
|
-
}
|
|
34
|
-
return listener;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function createServerContext(pathname: string) {
|
|
38
|
-
const status = rstest.fn();
|
|
39
|
-
const context = {
|
|
40
|
-
ssrContext: {
|
|
41
|
-
baseUrl: '/',
|
|
42
|
-
loaderContext: {},
|
|
43
|
-
mode: 'string',
|
|
44
|
-
request: {
|
|
45
|
-
raw: new Request(`http://localhost${pathname}`),
|
|
46
|
-
},
|
|
47
|
-
response: { status },
|
|
48
|
-
},
|
|
49
|
-
} as unknown as TInternalRuntimeContext;
|
|
50
|
-
|
|
51
|
-
return { context, status };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
describe('tanstack server plugin router results', () => {
|
|
55
|
-
afterEach(() => {
|
|
56
|
-
rstest.restoreAllMocks();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test.each([
|
|
60
|
-
'/cs/login',
|
|
61
|
-
'/en/login',
|
|
62
|
-
])('renders native link active attributes during SSR at %s', async pathname => {
|
|
63
|
-
const { context } = createServerContext(pathname);
|
|
64
|
-
const beforeRender = collectBeforeRender(() => [
|
|
65
|
-
{
|
|
66
|
-
id: 'login',
|
|
67
|
-
path: '/:lang/login',
|
|
68
|
-
Component: () => {
|
|
69
|
-
const Link = context.router?.Link;
|
|
70
|
-
if (!Link) {
|
|
71
|
-
throw new Error('SSR router did not provide its native Link');
|
|
72
|
-
}
|
|
73
|
-
return createElement(Link, { to: '/cs' }, 'Home');
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
]);
|
|
77
|
-
await beforeRender(context, value => value);
|
|
78
|
-
|
|
79
|
-
expect(context.router?.Link).toBeTypeOf('function');
|
|
80
|
-
const html = renderToString(
|
|
81
|
-
createElement(RouterProvider, {
|
|
82
|
-
router: getRouterRuntimeState(context)?.instance as AnyRouter,
|
|
83
|
-
}),
|
|
84
|
-
);
|
|
85
|
-
expect(html).toContain('href="/cs"');
|
|
86
|
-
if (pathname === '/cs/login') {
|
|
87
|
-
expect(html).toContain('data-status="active"');
|
|
88
|
-
expect(html).toContain('aria-current="page"');
|
|
89
|
-
} else {
|
|
90
|
-
expect(html).not.toContain('data-status="active"');
|
|
91
|
-
expect(html).not.toContain('aria-current="page"');
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test('uses the router render result as the HTTP and hydration status', async () => {
|
|
96
|
-
const beforeRender = collectBeforeRender(() => [
|
|
97
|
-
{
|
|
98
|
-
id: 'root',
|
|
99
|
-
path: '/',
|
|
100
|
-
Component: () => null,
|
|
101
|
-
children: [
|
|
102
|
-
{
|
|
103
|
-
id: 'target',
|
|
104
|
-
path: 'target',
|
|
105
|
-
Component: () => null,
|
|
106
|
-
},
|
|
107
|
-
],
|
|
108
|
-
},
|
|
109
|
-
]);
|
|
110
|
-
const { context, status } = createServerContext('/target');
|
|
111
|
-
|
|
112
|
-
await beforeRender(context, value => value);
|
|
113
|
-
|
|
114
|
-
expect(status).toHaveBeenCalledWith(200);
|
|
115
|
-
expect(getRouterServerSnapshot(context)).toMatchObject({
|
|
116
|
-
framework: 'tanstack',
|
|
117
|
-
statusCode: 200,
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test('interrupts SSR with the router redirect response', async () => {
|
|
122
|
-
const beforeRender = collectBeforeRender(() => [
|
|
123
|
-
{
|
|
124
|
-
id: 'root',
|
|
125
|
-
path: '/',
|
|
126
|
-
Component: () => null,
|
|
127
|
-
children: [
|
|
128
|
-
{
|
|
129
|
-
id: 'redirect',
|
|
130
|
-
path: 'redirect',
|
|
131
|
-
loader: () =>
|
|
132
|
-
new Response(null, {
|
|
133
|
-
status: 307,
|
|
134
|
-
headers: { Location: '/target' },
|
|
135
|
-
}),
|
|
136
|
-
Component: () => null,
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
id: 'target',
|
|
140
|
-
path: 'target',
|
|
141
|
-
Component: () => null,
|
|
142
|
-
},
|
|
143
|
-
],
|
|
144
|
-
},
|
|
145
|
-
]);
|
|
146
|
-
const { context, status } = createServerContext('/redirect');
|
|
147
|
-
const interrupt = rstest.fn((value: unknown) => value);
|
|
148
|
-
|
|
149
|
-
const response = await beforeRender(context, interrupt);
|
|
150
|
-
|
|
151
|
-
expect(response).toBeInstanceOf(Response);
|
|
152
|
-
expect((response as Response).status).toBe(307);
|
|
153
|
-
expect((response as Response).headers.get('Location')).toBe('/target');
|
|
154
|
-
expect(interrupt).toHaveBeenCalledWith(response);
|
|
155
|
-
expect(status).not.toHaveBeenCalled();
|
|
156
|
-
});
|
|
157
|
-
});
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { sanitizeSlotArgs } from '../../src/runtime/rsc/slotUsageSanitizer';
|
|
2
|
-
|
|
3
|
-
describe('RSC slot usage sanitizer', () => {
|
|
4
|
-
test('redacts callable slot args without serializing function source', () => {
|
|
5
|
-
function tokenFactory() {
|
|
6
|
-
return 'token-secret';
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const args = sanitizeSlotArgs([
|
|
10
|
-
tokenFactory,
|
|
11
|
-
{ nested: tokenFactory },
|
|
12
|
-
[tokenFactory],
|
|
13
|
-
]);
|
|
14
|
-
|
|
15
|
-
expect(args).toEqual([
|
|
16
|
-
'[Function]',
|
|
17
|
-
{ nested: '[Function]' },
|
|
18
|
-
['[Function]'],
|
|
19
|
-
]);
|
|
20
|
-
expect(JSON.stringify(args)).not.toContain('token-secret');
|
|
21
|
-
expect(JSON.stringify(args)).not.toContain('tokenFactory');
|
|
22
|
-
});
|
|
23
|
-
});
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { preloadMatchedRouteComponents } from '../../src/runtime/ssrPreload';
|
|
2
|
-
|
|
3
|
-
describe('preloadMatchedRouteComponents', () => {
|
|
4
|
-
test('preloads each component identity once per SSR pass', async () => {
|
|
5
|
-
const sharedComponent = Object.assign(() => null, {
|
|
6
|
-
preload: rstest.fn(),
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
await preloadMatchedRouteComponents({
|
|
10
|
-
routesById: {
|
|
11
|
-
child: {
|
|
12
|
-
options: {
|
|
13
|
-
component: sharedComponent,
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
root: {
|
|
17
|
-
options: {
|
|
18
|
-
component: sharedComponent,
|
|
19
|
-
pendingComponent: sharedComponent,
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
state: {
|
|
24
|
-
matches: [{ routeId: 'root' }, { routeId: 'child' }],
|
|
25
|
-
},
|
|
26
|
-
} as never);
|
|
27
|
-
|
|
28
|
-
expect(sharedComponent.preload).toHaveBeenCalledTimes(1);
|
|
29
|
-
});
|
|
30
|
-
});
|