@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.6 → 3.9.0-ultramodern.7
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/package.json +12 -12
- package/rstest.config.mts +0 -3
- package/tests/router/cli.test.ts +85 -860
- package/tests/router/dataMutation.test.tsx +36 -0
- package/tests/router/generateRouteArtifacts.test.ts +11 -11
- package/tests/router/loaderBridge.test.ts +52 -0
- package/tests/router/prefetchLink.test.tsx +0 -16
- package/tests/router/prefetchLinkPreload.test.tsx +12 -25
- package/tests/router/routeTree.test.ts +144 -624
- package/tests/router/rsc.test.tsx +14 -31
- package/tests/router/tanstackTypes.test.ts +92 -616
- package/tests/realm-isolation.test.ts +0 -104
- package/tests/router/fixtures/rscDecoderSecurityChild.cjs +0 -162
- package/tests/router/hooks.test.ts +0 -23
- package/tests/router/rscDecoderSecurity.test.ts +0 -84
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { RouteObject } from '@modern-js/runtime-utils/router';
|
|
2
|
-
import type { NestedRoute } from '@modern-js/types';
|
|
3
2
|
import { createMemoryHistory } from '@tanstack/history';
|
|
4
3
|
import {
|
|
5
4
|
createRouter,
|
|
@@ -11,104 +10,38 @@ import type { ComponentType } from 'react';
|
|
|
11
10
|
import { createElement, lazy } from 'react';
|
|
12
11
|
import { renderToStaticMarkup, renderToString } from 'react-dom/server';
|
|
13
12
|
import { Outlet as ModernOutlet } from '../../src/runtime/outlet';
|
|
14
|
-
import {
|
|
15
|
-
createRouteTreeFromRouteObjects,
|
|
16
|
-
getModernRouteIdsFromMatches,
|
|
17
|
-
} from '../../src/runtime/routeTree';
|
|
18
|
-
import { __setTanstackRscPayloadDecoderForTests } from '../../src/runtime/rsc/payloadRouter';
|
|
19
|
-
import { createTanstackRouteObjectsFromConfig } from '../../src/runtime/utils';
|
|
13
|
+
import { createRouteTreeFromRouteObjects } from '../../src/runtime/routeTree';
|
|
20
14
|
|
|
21
|
-
type LoaderArgs = {
|
|
22
|
-
params: Record<string, string>;
|
|
23
|
-
};
|
|
15
|
+
type LoaderArgs = { params: Record<string, string> };
|
|
24
16
|
|
|
25
17
|
type TestRouteObject = RouteObject & {
|
|
26
18
|
children?: TestRouteObject[];
|
|
27
|
-
config?: {
|
|
28
|
-
handle?: Record<string, unknown>;
|
|
29
|
-
};
|
|
30
|
-
hasAction?: boolean;
|
|
31
|
-
hasClientLoader?: boolean;
|
|
32
|
-
hasLoader?: boolean;
|
|
33
|
-
inValidSSRRoute?: boolean;
|
|
34
|
-
isClientComponent?: boolean;
|
|
35
19
|
lazyImport?: () => Promise<unknown>;
|
|
36
|
-
loaderDeps?: unknown;
|
|
37
|
-
validateSearch?: unknown;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
type TestNestedRoute = NestedRoute & {
|
|
41
|
-
children?: TestNestedRoute[];
|
|
42
|
-
hasAction?: boolean;
|
|
43
|
-
hasClientLoader?: boolean;
|
|
44
|
-
hasLoader?: boolean;
|
|
45
|
-
loaderDeps?: unknown;
|
|
46
|
-
validateSearch?: unknown;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
type ShouldRevalidateArgs = {
|
|
50
|
-
nextUrl: URL;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
type ShouldReloadArgs = {
|
|
54
|
-
context: {
|
|
55
|
-
request: Request;
|
|
56
|
-
};
|
|
57
|
-
location: {
|
|
58
|
-
href: string;
|
|
59
|
-
};
|
|
60
|
-
params: Record<string, string>;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
type TestRoute = {
|
|
64
|
-
options: {
|
|
65
|
-
component?: unknown;
|
|
66
|
-
shouldReload?: (args: ShouldReloadArgs) => boolean | undefined;
|
|
67
|
-
ssr?: boolean;
|
|
68
|
-
staticData: Record<string, unknown>;
|
|
69
|
-
loaderDeps?: unknown;
|
|
70
|
-
validateSearch?: unknown;
|
|
71
|
-
wrapInSuspense?: unknown;
|
|
72
|
-
};
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
type PreloadableTestComponent = {
|
|
76
|
-
load?: () => Promise<unknown>;
|
|
77
|
-
preload?: () => Promise<unknown>;
|
|
78
20
|
};
|
|
79
21
|
|
|
80
22
|
type TestRouter = {
|
|
81
23
|
_serverResult?:
|
|
82
|
-
| {
|
|
83
|
-
|
|
84
|
-
redirect: Response;
|
|
85
|
-
}
|
|
86
|
-
| {
|
|
87
|
-
type: 'render';
|
|
88
|
-
status: 200 | 404 | 500;
|
|
89
|
-
};
|
|
90
|
-
load: () => Promise<void>;
|
|
91
|
-
routesById: Partial<Record<string, TestRoute>>;
|
|
24
|
+
| { type: 'redirect'; redirect: Response }
|
|
25
|
+
| { type: 'render'; status: number };
|
|
92
26
|
state: {
|
|
93
|
-
matches: Array<{
|
|
94
|
-
error?: unknown;
|
|
95
|
-
loaderData?: unknown;
|
|
96
|
-
routeId: string;
|
|
97
|
-
}>;
|
|
27
|
+
matches: Array<{ error?: unknown; loaderData?: unknown; routeId: string }>;
|
|
98
28
|
};
|
|
99
29
|
};
|
|
100
30
|
|
|
101
|
-
|
|
31
|
+
const nullComponent = () => null;
|
|
32
|
+
|
|
33
|
+
const root = (
|
|
34
|
+
children: TestRouteObject[],
|
|
35
|
+
Component: RouteObject['Component'] = nullComponent,
|
|
36
|
+
): TestRouteObject[] => [{ id: 'root', path: '/', Component, children }];
|
|
102
37
|
|
|
103
38
|
async function loadRouteTree(
|
|
104
|
-
|
|
39
|
+
routes: TestRouteObject[],
|
|
105
40
|
pathname: string,
|
|
106
41
|
): Promise<TestRouter> {
|
|
107
42
|
const router = createRouter({
|
|
108
|
-
routeTree,
|
|
109
|
-
history: createMemoryHistory({
|
|
110
|
-
initialEntries: [pathname],
|
|
111
|
-
}),
|
|
43
|
+
routeTree: createRouteTreeFromRouteObjects(routes as RouteObject[]),
|
|
44
|
+
history: createMemoryHistory({ initialEntries: [pathname] }),
|
|
112
45
|
context: {
|
|
113
46
|
request: new Request(`http://localhost${pathname}`),
|
|
114
47
|
requestContext: {},
|
|
@@ -119,153 +52,85 @@ async function loadRouteTree(
|
|
|
119
52
|
return router as unknown as TestRouter;
|
|
120
53
|
}
|
|
121
54
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (!route) {
|
|
125
|
-
throw new Error(`Expected TanStack route ${id} to exist`);
|
|
126
|
-
}
|
|
127
|
-
return route;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function getRouteByModernRouteId(
|
|
131
|
-
router: TestRouter,
|
|
132
|
-
modernRouteId: string,
|
|
133
|
-
): TestRoute {
|
|
134
|
-
const route = Object.values(router.routesById).find(
|
|
135
|
-
route => route?.options.staticData?.modernRouteId === modernRouteId,
|
|
136
|
-
);
|
|
137
|
-
if (!route) {
|
|
138
|
-
throw new Error(`Expected Modern route ${modernRouteId} to exist`);
|
|
139
|
-
}
|
|
140
|
-
return route;
|
|
141
|
-
}
|
|
55
|
+
const matchOf = (router: TestRouter, routeId: string) =>
|
|
56
|
+
router.state.matches.find(match => match.routeId === routeId);
|
|
142
57
|
|
|
143
58
|
describe('tanstack route tree from RouteObject[]', () => {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
Component: () => null,
|
|
162
|
-
},
|
|
163
|
-
],
|
|
164
|
-
},
|
|
165
|
-
];
|
|
166
|
-
|
|
167
|
-
const routeTree = createRouteTreeFromRouteObjects(routes);
|
|
168
|
-
const router = await loadRouteTree(routeTree, '/user/123');
|
|
169
|
-
|
|
170
|
-
const rootMatch = router.state.matches.find(
|
|
171
|
-
match => match.routeId === '__root__',
|
|
172
|
-
);
|
|
173
|
-
const userMatch = router.state.matches.find(
|
|
174
|
-
match => match.routeId === '/user/$id',
|
|
59
|
+
test('resolves loader data for root, dynamic and splat params', async () => {
|
|
60
|
+
const routes: TestRouteObject[] = root(
|
|
61
|
+
[
|
|
62
|
+
{
|
|
63
|
+
id: 'user',
|
|
64
|
+
path: 'user/:id',
|
|
65
|
+
loader: ({ params }: LoaderArgs) => ({ id: params.id }),
|
|
66
|
+
Component: nullComponent,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'files',
|
|
70
|
+
path: 'files/*',
|
|
71
|
+
loader: ({ params }: LoaderArgs) => ({ value: params['*'] }),
|
|
72
|
+
Component: nullComponent,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
nullComponent,
|
|
175
76
|
);
|
|
77
|
+
routes[0].loader = () => ({ root: 'ok' });
|
|
176
78
|
|
|
177
|
-
|
|
178
|
-
expect(
|
|
79
|
+
const userRouter = await loadRouteTree(routes, '/user/123');
|
|
80
|
+
expect(matchOf(userRouter, '__root__')?.loaderData).toEqual({ root: 'ok' });
|
|
81
|
+
expect(matchOf(userRouter, '/user/$id')?.loaderData).toEqual({ id: '123' });
|
|
82
|
+
|
|
83
|
+
const splatRouter = await loadRouteTree(routes, '/files/a/b/c');
|
|
84
|
+
expect(matchOf(splatRouter, '/files/$')?.loaderData).toEqual({
|
|
85
|
+
value: 'a/b/c',
|
|
86
|
+
});
|
|
179
87
|
});
|
|
180
88
|
|
|
181
89
|
test('keeps unnamed sibling pathless layouts distinct', async () => {
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
loader: () => ({ value: 'alpha' }),
|
|
189
|
-
Component: () => null,
|
|
190
|
-
},
|
|
191
|
-
],
|
|
192
|
-
},
|
|
193
|
-
{
|
|
194
|
-
Component: () => null,
|
|
195
|
-
children: [
|
|
196
|
-
{
|
|
197
|
-
path: 'beta',
|
|
198
|
-
loader: () => ({ value: 'beta' }),
|
|
199
|
-
Component: () => null,
|
|
200
|
-
},
|
|
201
|
-
],
|
|
202
|
-
},
|
|
203
|
-
];
|
|
204
|
-
|
|
205
|
-
const routeTree = createRouteTreeFromRouteObjects(routes);
|
|
206
|
-
const router = await loadRouteTree(routeTree, '/beta');
|
|
90
|
+
const pathless = (path: string): TestRouteObject => ({
|
|
91
|
+
Component: nullComponent,
|
|
92
|
+
children: [
|
|
93
|
+
{ path, loader: () => ({ value: path }), Component: nullComponent },
|
|
94
|
+
],
|
|
95
|
+
});
|
|
207
96
|
|
|
208
|
-
const
|
|
97
|
+
const router = await loadRouteTree(
|
|
98
|
+
[pathless('alpha'), pathless('beta')],
|
|
99
|
+
'/beta',
|
|
100
|
+
);
|
|
101
|
+
const loaderData = router.state.matches
|
|
209
102
|
.map(match => match.loaderData)
|
|
210
103
|
.filter(Boolean);
|
|
211
104
|
|
|
212
|
-
expect(
|
|
213
|
-
expect(
|
|
105
|
+
expect(loaderData).toContainEqual({ value: 'beta' });
|
|
106
|
+
expect(loaderData).not.toContainEqual({ value: 'alpha' });
|
|
214
107
|
});
|
|
215
108
|
|
|
216
109
|
test('reports native TanStack unknown routes as HTTP 404', async () => {
|
|
217
|
-
const
|
|
218
|
-
{
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
Component: () => null,
|
|
222
|
-
children: [
|
|
223
|
-
{
|
|
224
|
-
id: 'known',
|
|
225
|
-
path: 'known',
|
|
226
|
-
Component: () => null,
|
|
227
|
-
},
|
|
228
|
-
],
|
|
229
|
-
},
|
|
230
|
-
];
|
|
231
|
-
|
|
232
|
-
const routeTree = createRouteTreeFromRouteObjects(routes);
|
|
233
|
-
const router = await loadRouteTree(routeTree, '/missing');
|
|
110
|
+
const router = await loadRouteTree(
|
|
111
|
+
root([{ id: 'known', path: 'known', Component: nullComponent }]),
|
|
112
|
+
'/missing',
|
|
113
|
+
);
|
|
234
114
|
|
|
235
|
-
expect(router._serverResult).toMatchObject({
|
|
236
|
-
type: 'render',
|
|
237
|
-
status: 404,
|
|
238
|
-
});
|
|
115
|
+
expect(router._serverResult).toMatchObject({ type: 'render', status: 404 });
|
|
239
116
|
});
|
|
240
117
|
|
|
241
118
|
test('reports native TanStack loader redirects as server responses', async () => {
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
{
|
|
257
|
-
id: 'target',
|
|
258
|
-
path: 'target',
|
|
259
|
-
Component: () => null,
|
|
260
|
-
},
|
|
261
|
-
],
|
|
262
|
-
},
|
|
263
|
-
];
|
|
264
|
-
|
|
265
|
-
const routeTree = createRouteTreeFromRouteObjects(routes);
|
|
266
|
-
const router = await loadRouteTree(routeTree, '/redirect');
|
|
119
|
+
const router = await loadRouteTree(
|
|
120
|
+
root([
|
|
121
|
+
{
|
|
122
|
+
id: 'redirect',
|
|
123
|
+
path: 'redirect',
|
|
124
|
+
loader: () => {
|
|
125
|
+
throw redirect({ to: '/target' });
|
|
126
|
+
},
|
|
127
|
+
Component: nullComponent,
|
|
128
|
+
},
|
|
129
|
+
{ id: 'target', path: 'target', Component: nullComponent },
|
|
130
|
+
]),
|
|
131
|
+
'/redirect',
|
|
132
|
+
);
|
|
267
133
|
|
|
268
|
-
expect(router._serverResult?.type).toBe('redirect');
|
|
269
134
|
if (router._serverResult?.type !== 'redirect') {
|
|
270
135
|
throw new Error('Expected a TanStack redirect server result');
|
|
271
136
|
}
|
|
@@ -276,363 +141,77 @@ describe('tanstack route tree from RouteObject[]', () => {
|
|
|
276
141
|
});
|
|
277
142
|
|
|
278
143
|
test('renders Modern Outlet through TanStack native outlet', async () => {
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
path: '/',
|
|
283
|
-
Component: () =>
|
|
284
|
-
createElement('section', null, createElement(ModernOutlet)),
|
|
285
|
-
children: [
|
|
144
|
+
const router = await loadRouteTree(
|
|
145
|
+
root(
|
|
146
|
+
[
|
|
286
147
|
{
|
|
287
148
|
id: 'plain',
|
|
288
149
|
path: 'plain',
|
|
289
150
|
Component: () => createElement('main', null, 'Plain child route'),
|
|
290
151
|
},
|
|
291
152
|
],
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const routeTree = createRouteTreeFromRouteObjects(routes);
|
|
296
|
-
const router = await loadRouteTree(routeTree, '/plain');
|
|
297
|
-
const markup = renderToString(
|
|
298
|
-
createElement(RouterProvider, { router } as never),
|
|
299
|
-
);
|
|
300
|
-
expect(markup).toContain('Plain child route');
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
test('resolves matched Modern route ids from TanStack route registry fallback', () => {
|
|
304
|
-
const router = {
|
|
305
|
-
state: {
|
|
306
|
-
matches: [
|
|
307
|
-
{ routeId: '__root__' },
|
|
308
|
-
{ routeId: '/$lang' },
|
|
309
|
-
{ routeId: '/$lang/tractors' },
|
|
310
|
-
{
|
|
311
|
-
route: {
|
|
312
|
-
options: {
|
|
313
|
-
staticData: {
|
|
314
|
-
modernRouteId: '(lang)/stores/page',
|
|
315
|
-
},
|
|
316
|
-
},
|
|
317
|
-
},
|
|
318
|
-
routeId: '/$lang/stores',
|
|
319
|
-
},
|
|
320
|
-
],
|
|
321
|
-
},
|
|
322
|
-
routesById: {
|
|
323
|
-
__root__: {
|
|
324
|
-
options: {
|
|
325
|
-
staticData: {
|
|
326
|
-
modernRouteId: 'layout',
|
|
327
|
-
},
|
|
328
|
-
},
|
|
329
|
-
},
|
|
330
|
-
'/$lang': {
|
|
331
|
-
options: {
|
|
332
|
-
staticData: {
|
|
333
|
-
modernRouteId: '(lang)/page',
|
|
334
|
-
},
|
|
335
|
-
},
|
|
336
|
-
},
|
|
337
|
-
'/$lang/tractors': {
|
|
338
|
-
options: {
|
|
339
|
-
staticData: {
|
|
340
|
-
modernRouteId: '(lang)/tractors/page',
|
|
341
|
-
},
|
|
342
|
-
},
|
|
343
|
-
},
|
|
344
|
-
},
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
expect(getModernRouteIdsFromMatches(router as never)).toEqual([
|
|
348
|
-
'layout',
|
|
349
|
-
'(lang)/page',
|
|
350
|
-
'(lang)/tractors/page',
|
|
351
|
-
'(lang)/stores/page',
|
|
352
|
-
]);
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
test('uses TanStack route ids when loading RSC payload route data', async () => {
|
|
356
|
-
const rootLoader = rstest.fn(() => ({ source: 'modern-root' }));
|
|
357
|
-
const userLoader = rstest.fn(() => ({ source: 'modern-user' }));
|
|
358
|
-
const routes: RouteObject[] = [
|
|
359
|
-
{
|
|
360
|
-
id: 'root',
|
|
361
|
-
path: '/',
|
|
362
|
-
loader: rootLoader,
|
|
363
|
-
Component: () => null,
|
|
364
|
-
children: [
|
|
365
|
-
{
|
|
366
|
-
id: 'user',
|
|
367
|
-
path: 'user/:id',
|
|
368
|
-
loader: userLoader,
|
|
369
|
-
Component: () => null,
|
|
370
|
-
},
|
|
371
|
-
],
|
|
372
|
-
},
|
|
373
|
-
];
|
|
374
|
-
const payload = {
|
|
375
|
-
type: 'render',
|
|
376
|
-
actionData: null,
|
|
377
|
-
errors: null,
|
|
378
|
-
loaderData: {
|
|
379
|
-
__root__: { source: 'rsc-root' },
|
|
380
|
-
'/user/$id': { source: 'rsc-user' },
|
|
381
|
-
},
|
|
382
|
-
location: { href: '/user/123' },
|
|
383
|
-
routes: [
|
|
384
|
-
{ id: '__root__', hasLoader: true },
|
|
385
|
-
{ id: '/user/$id', hasLoader: true },
|
|
386
|
-
],
|
|
387
|
-
};
|
|
388
|
-
const fetchMock = rstest.fn(() => Promise.resolve(new Response('payload')));
|
|
389
|
-
const decodeMock = rstest.fn(async () => payload);
|
|
390
|
-
rstest.stubGlobal('fetch', fetchMock);
|
|
391
|
-
rstest.stubGlobal('window', { origin: 'http://localhost' });
|
|
392
|
-
__setTanstackRscPayloadDecoderForTests(decodeMock);
|
|
393
|
-
|
|
394
|
-
const routeTree = createRouteTreeFromRouteObjects(routes, {
|
|
395
|
-
rscPayloadRouter: true,
|
|
396
|
-
});
|
|
397
|
-
const router = await loadRouteTree(routeTree, '/user/123');
|
|
398
|
-
|
|
399
|
-
const rootMatch = router.state.matches.find(
|
|
400
|
-
match => match.routeId === '__root__',
|
|
153
|
+
() => createElement('section', null, createElement(ModernOutlet)),
|
|
154
|
+
),
|
|
155
|
+
'/plain',
|
|
401
156
|
);
|
|
402
|
-
const userMatch = router.state.matches.find(
|
|
403
|
-
match => match.routeId === '/user/$id',
|
|
404
|
-
);
|
|
405
|
-
expect(rootMatch?.loaderData).toEqual({ source: 'rsc-root' });
|
|
406
|
-
expect(userMatch?.loaderData).toEqual({ source: 'rsc-user' });
|
|
407
|
-
expect(rootLoader).not.toHaveBeenCalled();
|
|
408
|
-
expect(userLoader).not.toHaveBeenCalled();
|
|
409
|
-
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
410
|
-
expect(decodeMock).toHaveBeenCalledTimes(1);
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
test('maps splat params', async () => {
|
|
414
|
-
let splatParamValue = '';
|
|
415
|
-
const routes: TestRouteObject[] = [
|
|
416
|
-
{
|
|
417
|
-
id: 'root',
|
|
418
|
-
path: '/',
|
|
419
|
-
Component: () => null,
|
|
420
|
-
children: [
|
|
421
|
-
{
|
|
422
|
-
id: 'files',
|
|
423
|
-
path: 'files/*',
|
|
424
|
-
loader: ({ params }: LoaderArgs) => {
|
|
425
|
-
splatParamValue = String(params['*'] || '');
|
|
426
|
-
return { value: params['*'] };
|
|
427
|
-
},
|
|
428
|
-
Component: () => null,
|
|
429
|
-
},
|
|
430
|
-
],
|
|
431
|
-
},
|
|
432
|
-
];
|
|
433
|
-
|
|
434
|
-
const routeTree = createRouteTreeFromRouteObjects(routes);
|
|
435
|
-
|
|
436
|
-
const splatRouter = await loadRouteTree(routeTree, '/files/a/b/c');
|
|
437
|
-
const filesMatch = splatRouter.state.matches.find(
|
|
438
|
-
match => match.routeId === '/files/$',
|
|
439
|
-
);
|
|
440
|
-
expect(filesMatch?.loaderData).toEqual({ value: 'a/b/c' });
|
|
441
|
-
expect(splatParamValue).toBe('a/b/c');
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
test('renders preloaded lazy child routes through TanStack router SSR', async () => {
|
|
445
|
-
const LazyRouteComponent = () =>
|
|
446
|
-
createElement('main', null, 'Lazy child route ready');
|
|
447
|
-
const lazyImport = rstest.fn(async () => ({
|
|
448
|
-
default: LazyRouteComponent,
|
|
449
|
-
}));
|
|
450
|
-
const routes: TestRouteObject[] = [
|
|
451
|
-
{
|
|
452
|
-
id: 'root',
|
|
453
|
-
path: '/',
|
|
454
|
-
Component: () => createElement('section', null, createElement(Outlet)),
|
|
455
|
-
children: [
|
|
456
|
-
{
|
|
457
|
-
id: 'lazy',
|
|
458
|
-
path: 'lazy',
|
|
459
|
-
Component: lazy(lazyImport),
|
|
460
|
-
lazyImport,
|
|
461
|
-
},
|
|
462
|
-
],
|
|
463
|
-
},
|
|
464
|
-
];
|
|
465
|
-
|
|
466
|
-
const routeTree = createRouteTreeFromRouteObjects(routes);
|
|
467
|
-
const router = await loadRouteTree(routeTree, '/lazy');
|
|
468
157
|
|
|
469
158
|
expect(
|
|
470
|
-
|
|
471
|
-
).toContain('
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
];
|
|
496
|
-
|
|
497
|
-
const routeTree = createRouteTreeFromRouteObjects(routes);
|
|
498
|
-
const router = await loadRouteTree(routeTree, '/load-only');
|
|
499
|
-
const loadOnlyRoute = getRoute(router, '/load-only');
|
|
500
|
-
const loadOnlyComponent = loadOnlyRoute.options
|
|
501
|
-
.component as PreloadableTestComponent;
|
|
502
|
-
|
|
503
|
-
expect(typeof loadOnlyComponent.load).toBe('function');
|
|
504
|
-
expect(typeof loadOnlyComponent.preload).toBe('function');
|
|
505
|
-
expect(load).toHaveBeenCalledTimes(1);
|
|
506
|
-
await loadOnlyComponent.preload?.();
|
|
507
|
-
expect(load).toHaveBeenCalledTimes(2);
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
test('unwraps nested ESM route module defaults before server rendering', async () => {
|
|
511
|
-
const LazyRouteComponent = () =>
|
|
512
|
-
createElement('main', null, 'Nested lazy child route ready');
|
|
513
|
-
const lazyImport = rstest.fn(async () => ({
|
|
514
|
-
default: {
|
|
515
|
-
default: LazyRouteComponent,
|
|
516
|
-
},
|
|
517
|
-
}));
|
|
518
|
-
const routes: TestRouteObject[] = [
|
|
519
|
-
{
|
|
520
|
-
id: 'root',
|
|
521
|
-
path: '/',
|
|
522
|
-
Component: () => createElement('section', null, createElement(Outlet)),
|
|
523
|
-
children: [
|
|
524
|
-
{
|
|
525
|
-
id: 'lazy',
|
|
526
|
-
path: 'lazy',
|
|
527
|
-
Component: lazy(
|
|
528
|
-
lazyImport as () => Promise<{ default: ComponentType }>,
|
|
529
|
-
),
|
|
530
|
-
lazyImport,
|
|
531
|
-
},
|
|
532
|
-
],
|
|
533
|
-
},
|
|
534
|
-
];
|
|
535
|
-
|
|
536
|
-
const routeTree = createRouteTreeFromRouteObjects(routes);
|
|
537
|
-
const router = await loadRouteTree(routeTree, '/lazy');
|
|
538
|
-
|
|
539
|
-
expect(
|
|
540
|
-
renderToStaticMarkup(createElement(RouterProvider, { router } as never)),
|
|
541
|
-
).toContain('Nested lazy child route ready');
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
test('preserves route handle and maps shouldRevalidate to shouldReload', async () => {
|
|
545
|
-
const shouldRevalidate = rstest.fn(({ nextUrl }: ShouldRevalidateArgs) =>
|
|
546
|
-
nextUrl.pathname.endsWith('/456'),
|
|
547
|
-
);
|
|
548
|
-
const routes: RouteObject[] = [
|
|
549
|
-
{
|
|
550
|
-
id: 'root',
|
|
551
|
-
path: '/',
|
|
552
|
-
config: {
|
|
553
|
-
handle: {
|
|
554
|
-
shell: true,
|
|
555
|
-
},
|
|
556
|
-
},
|
|
557
|
-
Component: () => null,
|
|
558
|
-
children: [
|
|
559
|
-
{
|
|
560
|
-
id: 'user',
|
|
561
|
-
path: 'user/:id',
|
|
562
|
-
handle: { auth: true },
|
|
563
|
-
config: {
|
|
564
|
-
handle: {
|
|
565
|
-
role: 'admin',
|
|
566
|
-
},
|
|
159
|
+
renderToString(createElement(RouterProvider, { router } as never)),
|
|
160
|
+
).toContain('Plain child route');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
for (const [shape, wrap] of [
|
|
164
|
+
['flat', (Component: ComponentType) => ({ default: Component })],
|
|
165
|
+
[
|
|
166
|
+
'nested',
|
|
167
|
+
(Component: ComponentType) => ({ default: { default: Component } }),
|
|
168
|
+
],
|
|
169
|
+
] as const) {
|
|
170
|
+
test(`server renders lazy child routes from ${shape} ESM module defaults`, async () => {
|
|
171
|
+
const LazyRouteComponent = () =>
|
|
172
|
+
createElement('main', null, 'Lazy child route ready');
|
|
173
|
+
const lazyImport = () => Promise.resolve(wrap(LazyRouteComponent));
|
|
174
|
+
const router = await loadRouteTree(
|
|
175
|
+
root(
|
|
176
|
+
[
|
|
177
|
+
{
|
|
178
|
+
id: 'lazy',
|
|
179
|
+
path: 'lazy',
|
|
180
|
+
Component: lazy(
|
|
181
|
+
lazyImport as () => Promise<{ default: ComponentType }>,
|
|
182
|
+
),
|
|
183
|
+
lazyImport,
|
|
567
184
|
},
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
expect(routeTree.options.staticData.modernRouteHandle).toEqual({
|
|
581
|
-
shell: true,
|
|
582
|
-
});
|
|
583
|
-
expect(userRoute.options.staticData.modernRouteHandle).toEqual({
|
|
584
|
-
auth: true,
|
|
585
|
-
role: 'admin',
|
|
185
|
+
],
|
|
186
|
+
() => createElement('section', null, createElement(Outlet)),
|
|
187
|
+
),
|
|
188
|
+
'/lazy',
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
expect(
|
|
192
|
+
renderToStaticMarkup(
|
|
193
|
+
createElement(RouterProvider, { router } as never),
|
|
194
|
+
),
|
|
195
|
+
).toContain('Lazy child route ready');
|
|
586
196
|
});
|
|
587
|
-
|
|
588
|
-
shouldRevalidate,
|
|
589
|
-
);
|
|
590
|
-
expect(
|
|
591
|
-
userRoute.options.shouldReload?.({
|
|
592
|
-
location: { href: '/user/456' },
|
|
593
|
-
params: { id: '456' },
|
|
594
|
-
context: {
|
|
595
|
-
request: new Request('http://localhost/user/456'),
|
|
596
|
-
},
|
|
597
|
-
}),
|
|
598
|
-
).toBe(true);
|
|
599
|
-
expect(shouldRevalidate).toHaveBeenCalledWith(
|
|
600
|
-
expect.objectContaining({
|
|
601
|
-
currentParams: { id: '123' },
|
|
602
|
-
nextParams: { id: '456' },
|
|
603
|
-
}),
|
|
604
|
-
);
|
|
605
|
-
});
|
|
197
|
+
}
|
|
606
198
|
|
|
607
199
|
test('normalizes Modern deferred loader data for TanStack SSR', async () => {
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
later: Promise.resolve('done'),
|
|
622
|
-
},
|
|
623
|
-
}),
|
|
624
|
-
Component: () => null,
|
|
625
|
-
},
|
|
626
|
-
],
|
|
627
|
-
},
|
|
628
|
-
];
|
|
629
|
-
|
|
630
|
-
const routeTree = createRouteTreeFromRouteObjects(routes);
|
|
631
|
-
const router = await loadRouteTree(routeTree, '/deferred');
|
|
632
|
-
const deferredMatch = router.state.matches.find(
|
|
633
|
-
match => match.routeId === '/deferred',
|
|
200
|
+
const router = await loadRouteTree(
|
|
201
|
+
root([
|
|
202
|
+
{
|
|
203
|
+
id: 'deferred',
|
|
204
|
+
path: 'deferred',
|
|
205
|
+
loader: () => ({
|
|
206
|
+
__modern_deferred: true,
|
|
207
|
+
data: { immediate: 'ok', later: Promise.resolve('done') },
|
|
208
|
+
}),
|
|
209
|
+
Component: nullComponent,
|
|
210
|
+
},
|
|
211
|
+
]),
|
|
212
|
+
'/deferred',
|
|
634
213
|
);
|
|
635
|
-
const loaderData =
|
|
214
|
+
const loaderData = matchOf(router, '/deferred')?.loaderData as
|
|
636
215
|
| { immediate: string; later: Promise<string> }
|
|
637
216
|
| undefined;
|
|
638
217
|
|
|
@@ -642,79 +221,20 @@ describe('tanstack route tree from RouteObject[]', () => {
|
|
|
642
221
|
|
|
643
222
|
test('preserves returned non-404 Response loaders as loader data', async () => {
|
|
644
223
|
const response = new Response('route status payload', { status: 500 });
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
Component: () => null,
|
|
656
|
-
},
|
|
657
|
-
],
|
|
658
|
-
},
|
|
659
|
-
];
|
|
660
|
-
|
|
661
|
-
const routeTree = createRouteTreeFromRouteObjects(routes);
|
|
662
|
-
const router = await loadRouteTree(routeTree, '/broken');
|
|
663
|
-
const brokenMatch = router.state.matches.find(
|
|
664
|
-
match => match.routeId === '/broken',
|
|
224
|
+
const router = await loadRouteTree(
|
|
225
|
+
root([
|
|
226
|
+
{
|
|
227
|
+
id: 'broken',
|
|
228
|
+
path: 'broken',
|
|
229
|
+
loader: () => response,
|
|
230
|
+
Component: nullComponent,
|
|
231
|
+
},
|
|
232
|
+
]),
|
|
233
|
+
'/broken',
|
|
665
234
|
);
|
|
666
235
|
|
|
667
|
-
expect(router._serverResult).toMatchObject({
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
});
|
|
671
|
-
expect(brokenMatch?.loaderData).toBe(response);
|
|
672
|
-
expect(brokenMatch?.error).toBeUndefined();
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
test('preserves generated client metadata through RouteObject conversion', () => {
|
|
676
|
-
const modernRoutes: TestNestedRoute[] = [
|
|
677
|
-
{
|
|
678
|
-
type: 'nested',
|
|
679
|
-
origin: 'config',
|
|
680
|
-
id: 'root',
|
|
681
|
-
isRoot: true,
|
|
682
|
-
children: [
|
|
683
|
-
{
|
|
684
|
-
type: 'nested',
|
|
685
|
-
origin: 'config',
|
|
686
|
-
id: 'client',
|
|
687
|
-
path: 'client',
|
|
688
|
-
clientData: './client.data',
|
|
689
|
-
data: './data',
|
|
690
|
-
hasAction: true,
|
|
691
|
-
hasClientLoader: true,
|
|
692
|
-
hasLoader: true,
|
|
693
|
-
inValidSSRRoute: true,
|
|
694
|
-
isClientComponent: true,
|
|
695
|
-
},
|
|
696
|
-
],
|
|
697
|
-
},
|
|
698
|
-
];
|
|
699
|
-
const routeObjects = createTanstackRouteObjectsFromConfig({
|
|
700
|
-
routesConfig: { routes: modernRoutes },
|
|
701
|
-
});
|
|
702
|
-
const routeTree = createRouteTreeFromRouteObjects(routeObjects || []);
|
|
703
|
-
const router = createRouter({
|
|
704
|
-
routeTree,
|
|
705
|
-
history: createMemoryHistory({
|
|
706
|
-
initialEntries: ['/client'],
|
|
707
|
-
}),
|
|
708
|
-
context: {},
|
|
709
|
-
}) as unknown as TestRouter;
|
|
710
|
-
const clientRoute = getRouteByModernRouteId(router, 'client');
|
|
711
|
-
|
|
712
|
-
expect(clientRoute.options.ssr).toBe(false);
|
|
713
|
-
expect(clientRoute.options.staticData).toMatchObject({
|
|
714
|
-
modernRouteHasAction: true,
|
|
715
|
-
modernRouteHasClientLoader: true,
|
|
716
|
-
modernRouteHasLoader: true,
|
|
717
|
-
modernRouteIsClientComponent: true,
|
|
718
|
-
});
|
|
236
|
+
expect(router._serverResult).toMatchObject({ type: 'render', status: 200 });
|
|
237
|
+
expect(matchOf(router, '/broken')?.loaderData).toBe(response);
|
|
238
|
+
expect(matchOf(router, '/broken')?.error).toBeUndefined();
|
|
719
239
|
});
|
|
720
240
|
});
|