@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.2 → 3.9.0-ultramodern.21

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.
Files changed (137) hide show
  1. package/dist/cjs/cli/artifacts.js +15 -1
  2. package/dist/cjs/cli/index.js +12 -0
  3. package/dist/cjs/cli/routeMetadata.js +131 -0
  4. package/dist/cjs/cli/tanstackTypes/canonicalRoutes.js +7 -7
  5. package/dist/cjs/cli/tanstackTypes/shared.js +0 -14
  6. package/dist/cjs/cli/tanstackTypes/source.js +27 -60
  7. package/dist/cjs/runtime/basepathRewrite.js +13 -6
  8. package/dist/cjs/runtime/lifecycle.js +6 -9
  9. package/dist/cjs/runtime/loaderBridge.js +10 -4
  10. package/dist/cjs/runtime/navigation.js +102 -0
  11. package/dist/cjs/runtime/plugin.js +7 -2
  12. package/dist/cjs/runtime/plugin.node.js +86 -64
  13. package/dist/cjs/runtime/prefetchLink.js +2 -7
  14. package/dist/cjs/runtime/register.js +0 -2
  15. package/dist/cjs/runtime/routeTree/construction.js +19 -59
  16. package/dist/cjs/runtime/routeTree/loaders.js +1 -1
  17. package/dist/cjs/runtime/routeTree/paths.js +0 -6
  18. package/dist/cjs/runtime/router.js +25 -9
  19. package/dist/cjs/runtime/ssrManagedTags.js +2 -12
  20. package/dist/cjs/runtime/state.js +2 -2
  21. package/dist/cjs/runtime/utils.js +3 -0
  22. package/dist/cjs/shared/routeDescriptor.js +60 -0
  23. package/dist/esm/cli/artifacts.mjs +13 -2
  24. package/dist/esm/cli/index.mjs +12 -9
  25. package/dist/esm/cli/routeMetadata.mjs +71 -0
  26. package/dist/esm/cli/tanstackTypes/canonicalRoutes.mjs +7 -7
  27. package/dist/esm/cli/tanstackTypes/shared.mjs +1 -9
  28. package/dist/esm/cli/tanstackTypes/source.mjs +28 -61
  29. package/dist/esm/runtime/basepathRewrite.mjs +10 -3
  30. package/dist/esm/runtime/lifecycle.mjs +1 -1
  31. package/dist/esm/runtime/loaderBridge.mjs +8 -5
  32. package/dist/esm/runtime/navigation.mjs +64 -0
  33. package/dist/esm/runtime/plugin.mjs +7 -2
  34. package/dist/esm/runtime/plugin.node.mjs +88 -66
  35. package/dist/esm/runtime/prefetchLink.mjs +1 -6
  36. package/dist/esm/runtime/register.mjs +0 -2
  37. package/dist/esm/runtime/routeTree/construction.mjs +19 -59
  38. package/dist/esm/runtime/routeTree/loaders.mjs +2 -2
  39. package/dist/esm/runtime/routeTree/paths.mjs +1 -4
  40. package/dist/esm/runtime/router.mjs +27 -11
  41. package/dist/esm/runtime/ssrManagedTags.mjs +1 -8
  42. package/dist/esm/runtime/state.mjs +1 -1
  43. package/dist/esm/runtime/utils.mjs +3 -0
  44. package/dist/esm/shared/routeDescriptor.mjs +22 -0
  45. package/dist/esm-node/cli/artifacts.mjs +13 -2
  46. package/dist/esm-node/cli/index.mjs +12 -9
  47. package/dist/esm-node/cli/routeMetadata.mjs +72 -0
  48. package/dist/esm-node/cli/tanstackTypes/canonicalRoutes.mjs +7 -7
  49. package/dist/esm-node/cli/tanstackTypes/shared.mjs +1 -9
  50. package/dist/esm-node/cli/tanstackTypes/source.mjs +28 -61
  51. package/dist/esm-node/runtime/basepathRewrite.mjs +10 -3
  52. package/dist/esm-node/runtime/lifecycle.mjs +1 -1
  53. package/dist/esm-node/runtime/loaderBridge.mjs +8 -5
  54. package/dist/esm-node/runtime/navigation.mjs +65 -0
  55. package/dist/esm-node/runtime/plugin.mjs +7 -2
  56. package/dist/esm-node/runtime/plugin.node.mjs +88 -66
  57. package/dist/esm-node/runtime/prefetchLink.mjs +1 -6
  58. package/dist/esm-node/runtime/register.mjs +0 -2
  59. package/dist/esm-node/runtime/routeTree/construction.mjs +19 -59
  60. package/dist/esm-node/runtime/routeTree/loaders.mjs +2 -2
  61. package/dist/esm-node/runtime/routeTree/paths.mjs +1 -4
  62. package/dist/esm-node/runtime/router.mjs +27 -11
  63. package/dist/esm-node/runtime/ssrManagedTags.mjs +1 -8
  64. package/dist/esm-node/runtime/state.mjs +1 -1
  65. package/dist/esm-node/runtime/utils.mjs +3 -0
  66. package/dist/esm-node/shared/routeDescriptor.mjs +23 -0
  67. package/dist/types/cli/artifacts.d.ts +13 -5
  68. package/dist/types/cli/index.d.ts +1 -1
  69. package/dist/types/cli/routeMetadata.d.ts +18 -0
  70. package/dist/types/cli/tanstackTypes/shared.d.ts +0 -2
  71. package/dist/types/runtime/basepathRewrite.d.ts +2 -1
  72. package/dist/types/runtime/lifecycle.d.ts +9 -2
  73. package/dist/types/runtime/loaderBridge.d.ts +1 -0
  74. package/dist/types/runtime/navigation.d.ts +4 -0
  75. package/dist/types/runtime/register.d.ts +1 -9
  76. package/dist/types/runtime/routeTree/paths.d.ts +0 -2
  77. package/dist/types/runtime/router.d.ts +2 -13
  78. package/dist/types/runtime/ssrManagedTags.d.ts +0 -1
  79. package/dist/types/runtime/ssrTypes.d.ts +0 -7
  80. package/dist/types/runtime/state.d.ts +1 -1
  81. package/dist/types/runtime/types.d.ts +2 -1
  82. package/dist/types/shared/routeDescriptor.d.ts +26 -0
  83. package/package.json +22 -19
  84. package/rstest.config.mts +2 -24
  85. package/src/cli/artifacts.ts +53 -6
  86. package/src/cli/index.ts +11 -2
  87. package/src/cli/routeMetadata.ts +121 -0
  88. package/src/cli/tanstackTypes/canonicalRoutes.ts +17 -12
  89. package/src/cli/tanstackTypes/shared.ts +0 -14
  90. package/src/cli/tanstackTypes/source.ts +42 -116
  91. package/src/runtime/basepathRewrite.ts +11 -2
  92. package/src/runtime/lifecycle.ts +14 -4
  93. package/src/runtime/loaderBridge.ts +9 -9
  94. package/src/runtime/navigation.ts +78 -0
  95. package/src/runtime/plugin.node.tsx +120 -93
  96. package/src/runtime/plugin.tsx +11 -1
  97. package/src/runtime/prefetchLink.tsx +1 -23
  98. package/src/runtime/register.ts +1 -12
  99. package/src/runtime/routeTree/construction.ts +32 -110
  100. package/src/runtime/routeTree/loaders.ts +2 -4
  101. package/src/runtime/routeTree/paths.ts +0 -6
  102. package/src/runtime/router.ts +25 -40
  103. package/src/runtime/ssrManagedTags.ts +0 -17
  104. package/src/runtime/ssrTypes.ts +0 -7
  105. package/src/runtime/state.ts +1 -1
  106. package/src/runtime/types.ts +2 -2
  107. package/src/runtime/utils.tsx +4 -1
  108. package/src/shared/routeDescriptor.ts +46 -0
  109. package/tests/router/cli.test.ts +148 -824
  110. package/tests/router/clientNavigation.ts +13 -0
  111. package/tests/router/dataMutation.test.tsx +17 -85
  112. package/tests/router/flightSerialization.roundtrip.test.tsx +9 -6
  113. package/tests/router/generateRouteArtifacts.test.ts +139 -27
  114. package/tests/router/loaderBridge.test.ts +0 -25
  115. package/tests/router/localisedIdentity.test.tsx +100 -0
  116. package/tests/router/prefetchLink.test.tsx +46 -21
  117. package/tests/router/prefetchLinkPreload.test.tsx +11 -32
  118. package/tests/router/routeHooks.test.ts +42 -105
  119. package/tests/router/routeMetadata.test.ts +233 -0
  120. package/tests/router/routeTree.test.ts +144 -836
  121. package/tests/router/rsc.test.tsx +131 -166
  122. package/tests/router/serverPlugin.test.ts +193 -1
  123. package/tests/router/tanstackTypes.test.ts +218 -963
  124. package/dist/esm/rslib-runtime~1.mjs +0 -18
  125. package/dist/esm-node/rslib-runtime~0.mjs +0 -19
  126. package/tests/realm-isolation.test.ts +0 -114
  127. package/tests/router/fastDefaults.test.ts +0 -32
  128. package/tests/router/fixtures/legacyRuntimeRouter.ts +0 -10
  129. package/tests/router/fixtures/rscDecoderSecurityChild.cjs +0 -162
  130. package/tests/router/hooks.test.ts +0 -40
  131. package/tests/router/hydrationBoundary.test.tsx +0 -23
  132. package/tests/router/packageSurface.test.ts +0 -24
  133. package/tests/router/preloadRedirect.test.ts +0 -64
  134. package/tests/router/register.test.ts +0 -8
  135. package/tests/router/routerCompatibility.test.ts +0 -239
  136. package/tests/router/rscDecoderSecurity.test.ts +0 -113
  137. package/tests/router/rscPayloadRouterMatrix.test.ts +0 -197
@@ -1,7 +1,4 @@
1
- import { readFileSync } from 'node:fs';
2
- import path from 'node:path';
3
1
  import type { RouteObject } from '@modern-js/runtime-utils/router';
4
- import type { NestedRoute } from '@modern-js/types';
5
2
  import { createMemoryHistory } from '@tanstack/history';
6
3
  import {
7
4
  createRouter,
@@ -12,107 +9,39 @@ import {
12
9
  import type { ComponentType } from 'react';
13
10
  import { createElement, lazy } from 'react';
14
11
  import { renderToStaticMarkup, renderToString } from 'react-dom/server';
15
- import * as TanstackRuntime from '../../src/runtime';
16
- import { Outlet as PublicOutlet } from '../../src/runtime';
17
12
  import { Outlet as ModernOutlet } from '../../src/runtime/outlet';
18
- import {
19
- createRouteTreeFromRouteObjects,
20
- getModernRouteIdsFromMatches,
21
- } from '../../src/runtime/routeTree';
22
- import { __setTanstackRscPayloadDecoderForTests } from '../../src/runtime/rsc/payloadRouter';
23
- import { createTanstackRouteObjectsFromConfig } from '../../src/runtime/utils';
13
+ import { createRouteTreeFromRouteObjects } from '../../src/runtime/routeTree';
24
14
 
25
- type LoaderArgs = {
26
- params: Record<string, string>;
27
- };
15
+ type LoaderArgs = { params: Record<string, string> };
28
16
 
29
17
  type TestRouteObject = RouteObject & {
30
18
  children?: TestRouteObject[];
31
- config?: {
32
- handle?: Record<string, unknown>;
33
- };
34
- hasAction?: boolean;
35
- hasClientLoader?: boolean;
36
- hasLoader?: boolean;
37
- inValidSSRRoute?: boolean;
38
- isClientComponent?: boolean;
39
19
  lazyImport?: () => Promise<unknown>;
40
- loaderDeps?: unknown;
41
- validateSearch?: unknown;
42
- };
43
-
44
- type TestNestedRoute = NestedRoute & {
45
- children?: TestNestedRoute[];
46
- hasAction?: boolean;
47
- hasClientLoader?: boolean;
48
- hasLoader?: boolean;
49
- loaderDeps?: unknown;
50
- validateSearch?: unknown;
51
- };
52
-
53
- type ShouldRevalidateArgs = {
54
- nextUrl: URL;
55
- };
56
-
57
- type ShouldReloadArgs = {
58
- context: {
59
- request: Request;
60
- };
61
- location: {
62
- href: string;
63
- };
64
- params: Record<string, string>;
65
- };
66
-
67
- type TestRoute = {
68
- options: {
69
- component?: unknown;
70
- shouldReload?: (args: ShouldReloadArgs) => boolean | undefined;
71
- ssr?: boolean;
72
- staticData: Record<string, unknown>;
73
- loaderDeps?: unknown;
74
- validateSearch?: unknown;
75
- wrapInSuspense?: unknown;
76
- };
77
- };
78
-
79
- type PreloadableTestComponent = {
80
- load?: () => Promise<unknown>;
81
- preload?: () => Promise<unknown>;
82
20
  };
83
21
 
84
22
  type TestRouter = {
85
23
  _serverResult?:
86
- | {
87
- type: 'redirect';
88
- redirect: Response;
89
- }
90
- | {
91
- type: 'render';
92
- status: 200 | 404 | 500;
93
- };
94
- load: () => Promise<void>;
95
- routesById: Partial<Record<string, TestRoute>>;
24
+ | { type: 'redirect'; redirect: Response }
25
+ | { type: 'render'; status: number };
96
26
  state: {
97
- matches: Array<{
98
- error?: unknown;
99
- loaderData?: unknown;
100
- routeId: string;
101
- }>;
27
+ matches: Array<{ error?: unknown; loaderData?: unknown; routeId: string }>;
102
28
  };
103
29
  };
104
30
 
105
- type TestRouteTree = ReturnType<typeof createRouteTreeFromRouteObjects>;
31
+ const nullComponent = () => null;
32
+
33
+ const root = (
34
+ children: TestRouteObject[],
35
+ Component: RouteObject['Component'] = nullComponent,
36
+ ): TestRouteObject[] => [{ id: 'root', path: '/', Component, children }];
106
37
 
107
38
  async function loadRouteTree(
108
- routeTree: TestRouteTree,
39
+ routes: TestRouteObject[],
109
40
  pathname: string,
110
41
  ): Promise<TestRouter> {
111
42
  const router = createRouter({
112
- routeTree,
113
- history: createMemoryHistory({
114
- initialEntries: [pathname],
115
- }),
43
+ routeTree: createRouteTreeFromRouteObjects(routes as RouteObject[]),
44
+ history: createMemoryHistory({ initialEntries: [pathname] }),
116
45
  context: {
117
46
  request: new Request(`http://localhost${pathname}`),
118
47
  requestContext: {},
@@ -123,185 +52,85 @@ async function loadRouteTree(
123
52
  return router as unknown as TestRouter;
124
53
  }
125
54
 
126
- function getRoute(router: TestRouter, id: string): TestRoute {
127
- const route = router.routesById[id];
128
- if (!route) {
129
- throw new Error(`Expected TanStack route ${id} to exist`);
130
- }
131
- return route;
132
- }
133
-
134
- function getRouteByModernRouteId(
135
- router: TestRouter,
136
- modernRouteId: string,
137
- ): TestRoute {
138
- const route = Object.values(router.routesById).find(
139
- route => route?.options.staticData?.modernRouteId === modernRouteId,
140
- );
141
- if (!route) {
142
- throw new Error(`Expected Modern route ${modernRouteId} to exist`);
143
- }
144
- return route;
145
- }
146
-
147
- function countCompletedSuspenseBoundaries(markup: string) {
148
- return markup.match(/<!--\$-->/g)?.length || 0;
149
- }
150
-
151
- describe('tanstack runtime public exports', () => {
152
- test('exports the Modern Outlet implementation from the runtime entrypoint', () => {
153
- expect(PublicOutlet).toBe(ModernOutlet);
154
- });
155
-
156
- test('does not expose the unowned composite RSC helper API', () => {
157
- expect('CompositeComponent' in TanstackRuntime).toBe(false);
158
-
159
- const packageJson = JSON.parse(
160
- readFileSync(path.join(__dirname, '../../package.json'), 'utf-8'),
161
- ) as {
162
- exports: Record<string, unknown>;
163
- typesVersions?: Record<string, Record<string, string[]>>;
164
- };
165
-
166
- expect(packageJson.exports['./runtime/rsc']).toBeUndefined();
167
- expect(packageJson.exports['./runtime/rsc/client']).toBeDefined();
168
- expect(packageJson.exports['./runtime/rsc/server']).toBeDefined();
169
- expect(packageJson.typesVersions?.['*']?.['runtime/rsc']).toBeUndefined();
170
- expect(packageJson.typesVersions?.['*']?.['runtime/rsc/client']).toEqual([
171
- './dist/types/runtime/rsc/client.d.ts',
172
- ]);
173
- expect(packageJson.typesVersions?.['*']?.['runtime/rsc/server']).toEqual([
174
- './dist/types/runtime/rsc/server.d.ts',
175
- ]);
176
- });
177
- });
55
+ const matchOf = (router: TestRouter, routeId: string) =>
56
+ router.state.matches.find(match => match.routeId === routeId);
178
57
 
179
58
  describe('tanstack route tree from RouteObject[]', () => {
180
- afterEach(() => {
181
- __setTanstackRscPayloadDecoderForTests();
182
- rstest.restoreAllMocks();
183
- });
184
-
185
- test('maps root loader and dynamic params', async () => {
186
- const routes: RouteObject[] = [
187
- {
188
- id: 'root',
189
- path: '/',
190
- loader: () => ({ root: 'ok' }),
191
- Component: () => null,
192
- children: [
193
- {
194
- id: 'user',
195
- path: 'user/:id',
196
- loader: ({ params }: LoaderArgs) => ({ id: params.id }),
197
- Component: () => null,
198
- },
199
- ],
200
- },
201
- ];
202
-
203
- const routeTree = createRouteTreeFromRouteObjects(routes);
204
- const router = await loadRouteTree(routeTree, '/user/123');
205
-
206
- const rootMatch = router.state.matches.find(
207
- match => match.routeId === '__root__',
208
- );
209
- const userMatch = router.state.matches.find(
210
- 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,
211
76
  );
77
+ routes[0].loader = () => ({ root: 'ok' });
78
+
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' });
212
82
 
213
- expect(rootMatch?.loaderData).toEqual({ root: 'ok' });
214
- expect(userMatch?.loaderData).toEqual({ id: '123' });
83
+ const splatRouter = await loadRouteTree(routes, '/files/a/b/c');
84
+ expect(matchOf(splatRouter, '/files/$')?.loaderData).toEqual({
85
+ value: 'a/b/c',
86
+ });
215
87
  });
216
88
 
217
89
  test('keeps unnamed sibling pathless layouts distinct', async () => {
218
- const routes: RouteObject[] = [
219
- {
220
- Component: () => null,
221
- children: [
222
- {
223
- path: 'alpha',
224
- loader: () => ({ value: 'alpha' }),
225
- Component: () => null,
226
- },
227
- ],
228
- },
229
- {
230
- Component: () => null,
231
- children: [
232
- {
233
- path: 'beta',
234
- loader: () => ({ value: 'beta' }),
235
- Component: () => null,
236
- },
237
- ],
238
- },
239
- ];
240
-
241
- const routeTree = createRouteTreeFromRouteObjects(routes);
242
- 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
+ });
243
96
 
244
- const matchedLoaderData = router.state.matches
97
+ const router = await loadRouteTree(
98
+ [pathless('alpha'), pathless('beta')],
99
+ '/beta',
100
+ );
101
+ const loaderData = router.state.matches
245
102
  .map(match => match.loaderData)
246
103
  .filter(Boolean);
247
104
 
248
- expect(matchedLoaderData).toContainEqual({ value: 'beta' });
249
- expect(matchedLoaderData).not.toContainEqual({ value: 'alpha' });
105
+ expect(loaderData).toContainEqual({ value: 'beta' });
106
+ expect(loaderData).not.toContainEqual({ value: 'alpha' });
250
107
  });
251
108
 
252
109
  test('reports native TanStack unknown routes as HTTP 404', async () => {
253
- const routes: RouteObject[] = [
254
- {
255
- id: 'root',
256
- path: '/',
257
- Component: () => null,
258
- children: [
259
- {
260
- id: 'known',
261
- path: 'known',
262
- Component: () => null,
263
- },
264
- ],
265
- },
266
- ];
267
-
268
- const routeTree = createRouteTreeFromRouteObjects(routes);
269
- const router = await loadRouteTree(routeTree, '/missing');
110
+ const router = await loadRouteTree(
111
+ root([{ id: 'known', path: 'known', Component: nullComponent }]),
112
+ '/missing',
113
+ );
270
114
 
271
- expect(router._serverResult).toMatchObject({
272
- type: 'render',
273
- status: 404,
274
- });
115
+ expect(router._serverResult).toMatchObject({ type: 'render', status: 404 });
275
116
  });
276
117
 
277
118
  test('reports native TanStack loader redirects as server responses', async () => {
278
- const routes: RouteObject[] = [
279
- {
280
- id: 'root',
281
- path: '/',
282
- Component: () => null,
283
- children: [
284
- {
285
- id: 'redirect',
286
- path: 'redirect',
287
- loader: () => {
288
- throw redirect({ to: '/target' });
289
- },
290
- Component: () => null,
291
- },
292
- {
293
- id: 'target',
294
- path: 'target',
295
- Component: () => null,
296
- },
297
- ],
298
- },
299
- ];
300
-
301
- const routeTree = createRouteTreeFromRouteObjects(routes);
302
- 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
+ );
303
133
 
304
- expect(router._serverResult?.type).toBe('redirect');
305
134
  if (router._serverResult?.type !== 'redirect') {
306
135
  throw new Error('Expected a TanStack redirect server result');
307
136
  }
@@ -311,504 +140,78 @@ describe('tanstack route tree from RouteObject[]', () => {
311
140
  );
312
141
  });
313
142
 
314
- test('does not force Suspense wrappers for ordinary generated routes', async () => {
315
- const routes: RouteObject[] = [
316
- {
317
- id: 'root',
318
- path: '/',
319
- Component: () => createElement(Outlet),
320
- children: [
321
- {
322
- id: 'plain',
323
- path: 'plain',
324
- Component: () => null,
325
- },
326
- ],
327
- },
328
- ];
329
-
330
- const routeTree = createRouteTreeFromRouteObjects(routes);
331
- const router = await loadRouteTree(routeTree, '/plain');
332
-
333
- expect(routeTree.options.wrapInSuspense).toBeUndefined();
334
- expect(getRoute(router, '/plain').options.wrapInSuspense).toBeUndefined();
335
- });
336
-
337
143
  test('renders Modern Outlet through TanStack native outlet', async () => {
338
- const routes: RouteObject[] = [
339
- {
340
- id: 'root',
341
- path: '/',
342
- Component: () =>
343
- createElement('section', null, createElement(ModernOutlet)),
344
- children: [
144
+ const router = await loadRouteTree(
145
+ root(
146
+ [
345
147
  {
346
148
  id: 'plain',
347
149
  path: 'plain',
348
150
  Component: () => createElement('main', null, 'Plain child route'),
349
151
  },
350
152
  ],
351
- },
352
- ];
353
-
354
- const routeTree = createRouteTreeFromRouteObjects(routes);
355
- const router = await loadRouteTree(routeTree, '/plain');
356
- const markup = renderToString(
357
- createElement(RouterProvider, { router } as never),
358
- );
359
- const suspenseBoundaryCount = countCompletedSuspenseBoundaries(markup);
360
-
361
- expect(markup).toContain('Plain child route');
362
- expect(suspenseBoundaryCount).toBe(1);
363
- });
364
-
365
- test('resolves matched Modern route ids from TanStack route registry fallback', () => {
366
- const router = {
367
- state: {
368
- matches: [
369
- { routeId: '__root__' },
370
- { routeId: '/$lang' },
371
- { routeId: '/$lang/tractors' },
372
- {
373
- route: {
374
- options: {
375
- staticData: {
376
- modernRouteId: '(lang)/stores/page',
377
- },
378
- },
379
- },
380
- routeId: '/$lang/stores',
381
- },
382
- ],
383
- },
384
- routesById: {
385
- __root__: {
386
- options: {
387
- staticData: {
388
- modernRouteId: 'layout',
389
- },
390
- },
391
- },
392
- '/$lang': {
393
- options: {
394
- staticData: {
395
- modernRouteId: '(lang)/page',
396
- },
397
- },
398
- },
399
- '/$lang/tractors': {
400
- options: {
401
- staticData: {
402
- modernRouteId: '(lang)/tractors/page',
403
- },
404
- },
405
- },
406
- },
407
- };
408
-
409
- expect(getModernRouteIdsFromMatches(router as never)).toEqual([
410
- 'layout',
411
- '(lang)/page',
412
- '(lang)/tractors/page',
413
- '(lang)/stores/page',
414
- ]);
415
- });
416
-
417
- test('preserves TanStack search contracts from RouteObject routes', () => {
418
- const rootValidateSearch = (search: unknown) => ({ root: search });
419
- const rootLoaderDeps = ({ search }: { search: unknown }) => ({ search });
420
- const childValidateSearch = (search: unknown) => ({ child: search });
421
- const childLoaderDeps = ({ search }: { search: unknown }) => ({ search });
422
- const routes: TestRouteObject[] = [
423
- {
424
- id: 'root',
425
- path: '/',
426
- validateSearch: rootValidateSearch,
427
- loaderDeps: rootLoaderDeps,
428
- Component: () => null,
429
- children: [
430
- {
431
- id: 'search',
432
- path: 'search',
433
- validateSearch: childValidateSearch,
434
- loaderDeps: childLoaderDeps,
435
- Component: () => null,
436
- },
437
- ],
438
- },
439
- ];
440
-
441
- const routeTree = createRouteTreeFromRouteObjects(routes);
442
- const router = createRouter({
443
- routeTree,
444
- history: createMemoryHistory({
445
- initialEntries: ['/search'],
446
- }),
447
- context: {},
448
- }) as unknown as TestRouter;
449
- const searchRoute = getRoute(router, '/search');
450
-
451
- expect(routeTree.options.validateSearch).toBe(rootValidateSearch);
452
- expect(routeTree.options.loaderDeps).toBe(rootLoaderDeps);
453
- expect(searchRoute.options.validateSearch).toBe(childValidateSearch);
454
- expect(searchRoute.options.loaderDeps).toBe(childLoaderDeps);
455
- });
456
-
457
- test('uses TanStack route ids when loading RSC payload route data', async () => {
458
- const rootLoader = rstest.fn(() => ({ source: 'modern-root' }));
459
- const userLoader = rstest.fn(() => ({ source: 'modern-user' }));
460
- const routes: RouteObject[] = [
461
- {
462
- id: 'root',
463
- path: '/',
464
- loader: rootLoader,
465
- Component: () => null,
466
- children: [
467
- {
468
- id: 'user',
469
- path: 'user/:id',
470
- loader: userLoader,
471
- Component: () => null,
472
- },
473
- ],
474
- },
475
- ];
476
- const payload = {
477
- type: 'render',
478
- actionData: null,
479
- errors: null,
480
- loaderData: {
481
- __root__: { source: 'rsc-root' },
482
- '/user/$id': { source: 'rsc-user' },
483
- },
484
- location: { href: '/user/123' },
485
- routes: [
486
- { id: '__root__', hasLoader: true },
487
- { id: '/user/$id', hasLoader: true },
488
- ],
489
- };
490
- const fetchMock = rstest.fn(() => Promise.resolve(new Response('payload')));
491
- const decodeMock = rstest.fn(async () => payload);
492
- rstest.stubGlobal('fetch', fetchMock);
493
- rstest.stubGlobal('window', { origin: 'http://localhost' });
494
- __setTanstackRscPayloadDecoderForTests(decodeMock);
495
-
496
- const routeTree = createRouteTreeFromRouteObjects(routes, {
497
- rscPayloadRouter: true,
498
- });
499
- const router = await loadRouteTree(routeTree, '/user/123');
500
-
501
- const rootMatch = router.state.matches.find(
502
- match => match.routeId === '__root__',
503
- );
504
- const userMatch = router.state.matches.find(
505
- match => match.routeId === '/user/$id',
506
- );
507
- expect(rootMatch?.loaderData).toEqual({ source: 'rsc-root' });
508
- expect(userMatch?.loaderData).toEqual({ source: 'rsc-user' });
509
- expect(rootLoader).not.toHaveBeenCalled();
510
- expect(userLoader).not.toHaveBeenCalled();
511
- expect(fetchMock).toHaveBeenCalledTimes(1);
512
- expect(decodeMock).toHaveBeenCalledTimes(1);
513
- });
514
-
515
- test('maps splat params', async () => {
516
- let splatParamValue = '';
517
- const routes: TestRouteObject[] = [
518
- {
519
- id: 'root',
520
- path: '/',
521
- Component: () => null,
522
- children: [
523
- {
524
- id: 'files',
525
- path: 'files/*',
526
- loader: ({ params }: LoaderArgs) => {
527
- splatParamValue = String(params['*'] || '');
528
- return { value: params['*'] };
529
- },
530
- Component: () => null,
531
- },
532
- ],
533
- },
534
- ];
535
-
536
- const routeTree = createRouteTreeFromRouteObjects(routes);
537
-
538
- const splatRouter = await loadRouteTree(routeTree, '/files/a/b/c');
539
- const filesMatch = splatRouter.state.matches.find(
540
- match => match.routeId === '/files/$',
153
+ () => createElement('section', null, createElement(ModernOutlet)),
154
+ ),
155
+ '/plain',
541
156
  );
542
- expect(filesMatch?.loaderData).toEqual({ value: 'a/b/c' });
543
- expect(splatParamValue).toBe('a/b/c');
544
- });
545
-
546
- test('preloads lazy Modern route components for server rendering', async () => {
547
- const LazyRouteComponent = () =>
548
- createElement('main', null, 'Lazy route ready');
549
- const lazyImport = rstest.fn(async () => ({
550
- default: LazyRouteComponent,
551
- }));
552
- const routes: TestRouteObject[] = [
553
- {
554
- id: 'root',
555
- path: '/',
556
- Component: () => null,
557
- children: [
558
- {
559
- id: 'lazy',
560
- path: 'lazy',
561
- Component: lazy(lazyImport),
562
- lazyImport,
563
- },
564
- ],
565
- },
566
- ];
567
-
568
- const routeTree = createRouteTreeFromRouteObjects(routes);
569
- const router = await loadRouteTree(routeTree, '/lazy');
570
- const lazyRoute = getRoute(router, '/lazy');
571
- const component = lazyRoute.options.component as ComponentType & {
572
- preload?: () => Promise<unknown>;
573
- };
574
-
575
- await component.preload?.();
576
-
577
- expect(lazyImport).toHaveBeenCalled();
578
- });
579
-
580
- test('renders preloaded lazy child routes through TanStack router SSR', async () => {
581
- const LazyRouteComponent = () =>
582
- createElement('main', null, 'Lazy child route ready');
583
- const lazyImport = rstest.fn(async () => ({
584
- default: LazyRouteComponent,
585
- }));
586
- const routes: TestRouteObject[] = [
587
- {
588
- id: 'root',
589
- path: '/',
590
- Component: () => createElement('section', null, createElement(Outlet)),
591
- children: [
592
- {
593
- id: 'lazy',
594
- path: 'lazy',
595
- Component: lazy(lazyImport),
596
- lazyImport,
597
- },
598
- ],
599
- },
600
- ];
601
-
602
- const routeTree = createRouteTreeFromRouteObjects(routes);
603
- const router = await loadRouteTree(routeTree, '/lazy');
604
- const lazyRoute = getRoute(router, '/lazy');
605
- const lazyComponent = lazyRoute.options
606
- .component as PreloadableTestComponent;
607
-
608
- expect(
609
- renderToStaticMarkup(createElement(RouterProvider, { router } as never)),
610
- ).toContain('Lazy child route ready');
611
- expect(typeof lazyComponent.load).toBe('function');
612
- expect(typeof lazyComponent.preload).toBe('function');
613
- });
614
-
615
- test('exposes load-only Modern route components through TanStack preload', async () => {
616
- const load = rstest.fn(async () => 'route chunk loaded');
617
- const LoadOnlyRouteComponent = (() =>
618
- createElement('main', null, 'Load-only route ready')) as ComponentType & {
619
- load?: () => Promise<unknown>;
620
- preload?: () => Promise<unknown>;
621
- };
622
- LoadOnlyRouteComponent.load = load;
623
- const routes: TestRouteObject[] = [
624
- {
625
- id: 'root',
626
- path: '/',
627
- Component: () => createElement('section', null, createElement(Outlet)),
628
- children: [
629
- {
630
- id: 'load-only',
631
- path: 'load-only',
632
- Component: LoadOnlyRouteComponent,
633
- },
634
- ],
635
- },
636
- ];
637
-
638
- const routeTree = createRouteTreeFromRouteObjects(routes);
639
- const router = await loadRouteTree(routeTree, '/load-only');
640
- const loadOnlyRoute = getRoute(router, '/load-only');
641
- const loadOnlyComponent = loadOnlyRoute.options
642
- .component as PreloadableTestComponent;
643
-
644
- expect(typeof loadOnlyComponent.load).toBe('function');
645
- expect(typeof loadOnlyComponent.preload).toBe('function');
646
- expect(load).toHaveBeenCalledTimes(1);
647
- await loadOnlyComponent.preload?.();
648
- expect(load).toHaveBeenCalledTimes(2);
649
- });
650
-
651
- test('unwraps nested ESM route module defaults before server rendering', async () => {
652
- const LazyRouteComponent = () =>
653
- createElement('main', null, 'Nested lazy child route ready');
654
- const lazyImport = rstest.fn(async () => ({
655
- default: {
656
- default: LazyRouteComponent,
657
- },
658
- }));
659
- const routes: TestRouteObject[] = [
660
- {
661
- id: 'root',
662
- path: '/',
663
- Component: () => createElement('section', null, createElement(Outlet)),
664
- children: [
665
- {
666
- id: 'lazy',
667
- path: 'lazy',
668
- Component: lazy(
669
- lazyImport as () => Promise<{ default: ComponentType }>,
670
- ),
671
- lazyImport,
672
- },
673
- ],
674
- },
675
- ];
676
-
677
- const routeTree = createRouteTreeFromRouteObjects(routes);
678
- const router = await loadRouteTree(routeTree, '/lazy');
679
157
 
680
158
  expect(
681
- renderToStaticMarkup(createElement(RouterProvider, { router } as never)),
682
- ).toContain('Nested lazy child route ready');
683
- });
684
-
685
- test('preserves route handle and maps shouldRevalidate to shouldReload', async () => {
686
- const shouldRevalidate = rstest.fn(({ nextUrl }: ShouldRevalidateArgs) =>
687
- nextUrl.pathname.endsWith('/456'),
688
- );
689
- const routes: RouteObject[] = [
690
- {
691
- id: 'root',
692
- path: '/',
693
- config: {
694
- handle: {
695
- shell: true,
696
- },
697
- },
698
- Component: () => null,
699
- children: [
700
- {
701
- id: 'user',
702
- path: 'user/:id',
703
- handle: { auth: true },
704
- config: {
705
- handle: {
706
- role: 'admin',
707
- },
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,
708
184
  },
709
- shouldRevalidate,
710
- loader: ({ params }: LoaderArgs) => ({ id: params.id }),
711
- Component: () => null,
712
- },
713
- ],
714
- },
715
- ];
716
-
717
- const routeTree = createRouteTreeFromRouteObjects(routes);
718
- const router = await loadRouteTree(routeTree, '/user/123');
719
- const userRoute = getRoute(router, '/user/$id');
720
-
721
- expect(routeTree.options.staticData.modernRouteHandle).toEqual({
722
- shell: true,
723
- });
724
- expect(userRoute.options.staticData.modernRouteHandle).toEqual({
725
- auth: true,
726
- role: 'admin',
727
- });
728
- expect(userRoute.options.staticData.modernRouteShouldRevalidate).toBe(
729
- shouldRevalidate,
730
- );
731
- expect(
732
- userRoute.options.shouldReload?.({
733
- location: { href: '/user/456' },
734
- params: { id: '456' },
735
- context: {
736
- request: new Request('http://localhost/user/456'),
737
- },
738
- }),
739
- ).toBe(true);
740
- expect(shouldRevalidate).toHaveBeenCalledWith(
741
- expect.objectContaining({
742
- currentParams: { id: '123' },
743
- nextParams: { id: '456' },
744
- }),
745
- );
746
- });
747
-
748
- test('preserves client route metadata and disables invalid SSR routes', async () => {
749
- const routes: TestRouteObject[] = [
750
- {
751
- id: 'root',
752
- path: '/',
753
- Component: () => null,
754
- children: [
755
- {
756
- id: 'client',
757
- path: 'client',
758
- hasAction: true,
759
- hasClientLoader: true,
760
- hasLoader: true,
761
- inValidSSRRoute: true,
762
- isClientComponent: true,
763
- loader: () => ({ ok: true }),
764
- Component: () => null,
765
- },
766
- ],
767
- },
768
- ];
769
-
770
- const routeTree = createRouteTreeFromRouteObjects(routes);
771
- const router = await loadRouteTree(routeTree, '/client');
772
- const clientRoute = getRoute(router, '/client');
773
-
774
- expect(clientRoute.options.ssr).toBe(false);
775
- expect(clientRoute.options.staticData).toMatchObject({
776
- modernRouteHasAction: true,
777
- modernRouteHasClientLoader: true,
778
- modernRouteHasLoader: true,
779
- modernRouteIsClientComponent: true,
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');
780
196
  });
781
- });
197
+ }
782
198
 
783
199
  test('normalizes Modern deferred loader data for TanStack SSR', async () => {
784
- const routes: TestRouteObject[] = [
785
- {
786
- id: 'root',
787
- path: '/',
788
- Component: () => null,
789
- children: [
790
- {
791
- id: 'deferred',
792
- path: 'deferred',
793
- loader: () => ({
794
- __modern_deferred: true,
795
- data: {
796
- immediate: 'ok',
797
- later: Promise.resolve('done'),
798
- },
799
- }),
800
- Component: () => null,
801
- },
802
- ],
803
- },
804
- ];
805
-
806
- const routeTree = createRouteTreeFromRouteObjects(routes);
807
- const router = await loadRouteTree(routeTree, '/deferred');
808
- const deferredMatch = router.state.matches.find(
809
- 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',
810
213
  );
811
- const loaderData = deferredMatch?.loaderData as
214
+ const loaderData = matchOf(router, '/deferred')?.loaderData as
812
215
  | { immediate: string; later: Promise<string> }
813
216
  | undefined;
814
217
 
@@ -818,115 +221,20 @@ describe('tanstack route tree from RouteObject[]', () => {
818
221
 
819
222
  test('preserves returned non-404 Response loaders as loader data', async () => {
820
223
  const response = new Response('route status payload', { status: 500 });
821
- const routes: TestRouteObject[] = [
822
- {
823
- id: 'root',
824
- path: '/',
825
- Component: () => null,
826
- children: [
827
- {
828
- id: 'broken',
829
- path: 'broken',
830
- loader: () => response,
831
- Component: () => null,
832
- },
833
- ],
834
- },
835
- ];
836
-
837
- const routeTree = createRouteTreeFromRouteObjects(routes);
838
- const router = await loadRouteTree(routeTree, '/broken');
839
- const brokenMatch = router.state.matches.find(
840
- 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',
841
234
  );
842
235
 
843
- expect(router._serverResult).toMatchObject({
844
- type: 'render',
845
- status: 200,
846
- });
847
- expect(brokenMatch?.loaderData).toBe(response);
848
- expect(brokenMatch?.error).toBeUndefined();
849
- });
850
-
851
- test('preserves generated client metadata through RouteObject conversion', () => {
852
- const modernRoutes: TestNestedRoute[] = [
853
- {
854
- type: 'nested',
855
- origin: 'config',
856
- id: 'root',
857
- isRoot: true,
858
- children: [
859
- {
860
- type: 'nested',
861
- origin: 'config',
862
- id: 'client',
863
- path: 'client',
864
- clientData: './client.data',
865
- data: './data',
866
- hasAction: true,
867
- hasClientLoader: true,
868
- hasLoader: true,
869
- inValidSSRRoute: true,
870
- isClientComponent: true,
871
- },
872
- ],
873
- },
874
- ];
875
- const routeObjects = createTanstackRouteObjectsFromConfig({
876
- routesConfig: { routes: modernRoutes },
877
- });
878
- const routeTree = createRouteTreeFromRouteObjects(routeObjects || []);
879
- const router = createRouter({
880
- routeTree,
881
- history: createMemoryHistory({
882
- initialEntries: ['/client'],
883
- }),
884
- context: {},
885
- }) as unknown as TestRouter;
886
- const clientRoute = getRouteByModernRouteId(router, 'client');
887
-
888
- expect(clientRoute.options.ssr).toBe(false);
889
- expect(clientRoute.options.staticData).toMatchObject({
890
- modernRouteHasAction: true,
891
- modernRouteHasClientLoader: true,
892
- modernRouteHasLoader: true,
893
- modernRouteIsClientComponent: true,
894
- });
895
- });
896
-
897
- test('does not copy hasErrorBoundary into React Router route objects', () => {
898
- function ErrorPage() {
899
- return null;
900
- }
901
-
902
- const modernRoutes: TestNestedRoute[] = [
903
- {
904
- type: 'nested',
905
- origin: 'config',
906
- id: 'root',
907
- isRoot: true,
908
- hasErrorBoundary: true,
909
- children: [
910
- {
911
- type: 'nested',
912
- origin: 'config',
913
- id: 'child',
914
- path: 'child',
915
- hasErrorBoundary: true,
916
- error: ErrorPage,
917
- },
918
- ],
919
- },
920
- ];
921
-
922
- const routeObjects = createTanstackRouteObjectsFromConfig({
923
- routesConfig: { routes: modernRoutes },
924
- });
925
- const rootRoute = routeObjects?.[0];
926
- const childRoute = rootRoute?.children?.[0];
927
-
928
- expect(rootRoute).not.toHaveProperty('hasErrorBoundary');
929
- expect(childRoute).not.toHaveProperty('hasErrorBoundary');
930
- expect(childRoute?.errorElement).toBeDefined();
236
+ expect(router._serverResult).toMatchObject({ type: 'render', status: 200 });
237
+ expect(matchOf(router, '/broken')?.loaderData).toBe(response);
238
+ expect(matchOf(router, '/broken')?.error).toBeUndefined();
931
239
  });
932
240
  });