@bleedingdev/modern-js-plugin-tanstack 3.2.0-ultramodern.12 → 3.2.0-ultramodern.121

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 (136) hide show
  1. package/dist/cjs/cli/index.js +89 -31
  2. package/dist/cjs/cli/routeSplitting.js +55 -0
  3. package/dist/cjs/cli/tanstackTypes.js +172 -170
  4. package/dist/cjs/cli.js +12 -8
  5. package/dist/cjs/runtime/basepathRewrite.js +12 -8
  6. package/dist/cjs/runtime/dataMutation.js +9 -5
  7. package/dist/cjs/runtime/hooks.js +20 -19
  8. package/dist/cjs/runtime/hydrationBoundary.js +48 -0
  9. package/dist/cjs/runtime/index.js +79 -35
  10. package/dist/cjs/runtime/lifecycle.js +21 -91
  11. package/dist/cjs/runtime/loaderBridge.js +173 -0
  12. package/dist/cjs/runtime/outlet.js +58 -0
  13. package/dist/cjs/runtime/plugin.js +195 -114
  14. package/dist/cjs/runtime/plugin.node.js +45 -45
  15. package/dist/cjs/runtime/plugin.worker.js +53 -0
  16. package/dist/cjs/runtime/pluginCore.js +55 -0
  17. package/dist/cjs/runtime/prefetchLink.js +10 -6
  18. package/dist/cjs/runtime/register.js +56 -0
  19. package/dist/cjs/runtime/routeTree.js +74 -207
  20. package/dist/cjs/runtime/router.js +41 -0
  21. package/dist/cjs/runtime/rsc/ClientSlot.js +9 -5
  22. package/dist/cjs/runtime/rsc/CompositeComponent.js +9 -5
  23. package/dist/cjs/runtime/rsc/ReplayableStream.js +14 -9
  24. package/dist/cjs/runtime/rsc/RscNodeRenderer.js +9 -5
  25. package/dist/cjs/runtime/rsc/SlotContext.js +9 -5
  26. package/dist/cjs/runtime/rsc/client.js +9 -5
  27. package/dist/cjs/runtime/rsc/createRscProxy.js +9 -5
  28. package/dist/cjs/runtime/rsc/index.js +9 -5
  29. package/dist/cjs/runtime/rsc/payloadRouter.js +44 -6
  30. package/dist/cjs/runtime/rsc/server.js +9 -5
  31. package/dist/cjs/runtime/rsc/slotUsageSanitizer.js +9 -5
  32. package/dist/cjs/runtime/rsc/symbols.js +20 -15
  33. package/dist/cjs/runtime/state.js +45 -0
  34. package/dist/cjs/runtime/types.js +31 -1
  35. package/dist/cjs/runtime/utils.js +9 -10
  36. package/dist/cjs/runtime.js +9 -5
  37. package/dist/esm/cli/index.mjs +75 -27
  38. package/dist/esm/cli/routeSplitting.mjs +14 -0
  39. package/dist/esm/cli/tanstackTypes.mjs +158 -160
  40. package/dist/esm/runtime/hooks.mjs +1 -8
  41. package/dist/esm/runtime/hydrationBoundary.mjs +10 -0
  42. package/dist/esm/runtime/index.mjs +5 -2
  43. package/dist/esm/runtime/lifecycle.mjs +1 -82
  44. package/dist/esm/runtime/loaderBridge.mjs +114 -0
  45. package/dist/esm/runtime/outlet.mjs +17 -0
  46. package/dist/esm/runtime/plugin.mjs +191 -114
  47. package/dist/esm/runtime/plugin.node.mjs +40 -44
  48. package/dist/esm/runtime/plugin.worker.mjs +1 -0
  49. package/dist/esm/runtime/pluginCore.mjs +14 -0
  50. package/dist/esm/runtime/prefetchLink.mjs +1 -1
  51. package/dist/esm/runtime/register.mjs +18 -0
  52. package/dist/esm/runtime/routeTree.mjs +59 -193
  53. package/dist/esm/runtime/router.mjs +2 -0
  54. package/dist/esm/runtime/rsc/payloadRouter.mjs +35 -1
  55. package/dist/esm/runtime/state.mjs +7 -0
  56. package/dist/esm/runtime/types.mjs +7 -0
  57. package/dist/esm/runtime/utils.mjs +0 -5
  58. package/dist/esm-node/cli/index.mjs +75 -27
  59. package/dist/esm-node/cli/routeSplitting.mjs +15 -0
  60. package/dist/esm-node/cli/tanstackTypes.mjs +158 -160
  61. package/dist/esm-node/runtime/hooks.mjs +1 -8
  62. package/dist/esm-node/runtime/hydrationBoundary.mjs +11 -0
  63. package/dist/esm-node/runtime/index.mjs +5 -2
  64. package/dist/esm-node/runtime/lifecycle.mjs +1 -82
  65. package/dist/esm-node/runtime/loaderBridge.mjs +115 -0
  66. package/dist/esm-node/runtime/outlet.mjs +18 -0
  67. package/dist/esm-node/runtime/plugin.mjs +191 -114
  68. package/dist/esm-node/runtime/plugin.node.mjs +40 -44
  69. package/dist/esm-node/runtime/plugin.worker.mjs +2 -0
  70. package/dist/esm-node/runtime/pluginCore.mjs +15 -0
  71. package/dist/esm-node/runtime/prefetchLink.mjs +1 -1
  72. package/dist/esm-node/runtime/register.mjs +19 -0
  73. package/dist/esm-node/runtime/routeTree.mjs +59 -193
  74. package/dist/esm-node/runtime/router.mjs +3 -0
  75. package/dist/esm-node/runtime/rsc/payloadRouter.mjs +35 -1
  76. package/dist/esm-node/runtime/state.mjs +8 -0
  77. package/dist/esm-node/runtime/types.mjs +7 -0
  78. package/dist/esm-node/runtime/utils.mjs +0 -5
  79. package/dist/types/cli/index.d.ts +14 -1
  80. package/dist/types/cli/routeSplitting.d.ts +20 -0
  81. package/dist/types/cli/tanstackTypes.d.ts +21 -1
  82. package/dist/types/runtime/hooks.d.ts +8 -33
  83. package/dist/types/runtime/hydrationBoundary.d.ts +2 -0
  84. package/dist/types/runtime/index.d.ts +8 -3
  85. package/dist/types/runtime/lifecycle.d.ts +7 -22
  86. package/dist/types/runtime/loaderBridge.d.ts +48 -0
  87. package/dist/types/runtime/outlet.d.ts +2 -0
  88. package/dist/types/runtime/plugin.d.ts +2 -15
  89. package/dist/types/runtime/plugin.node.d.ts +2 -15
  90. package/dist/types/runtime/plugin.worker.d.ts +1 -0
  91. package/dist/types/runtime/pluginCore.d.ts +21 -0
  92. package/dist/types/runtime/register.d.ts +9 -0
  93. package/dist/types/runtime/routeTree.d.ts +0 -2
  94. package/dist/types/runtime/router.d.ts +14 -0
  95. package/dist/types/runtime/state.d.ts +16 -0
  96. package/dist/types/runtime/types.d.ts +14 -53
  97. package/package.json +42 -40
  98. package/rstest.config.mts +6 -0
  99. package/src/cli/index.ts +162 -23
  100. package/src/cli/routeSplitting.ts +43 -0
  101. package/src/cli/tanstackTypes.ts +331 -187
  102. package/src/runtime/hooks.ts +10 -27
  103. package/src/runtime/hydrationBoundary.tsx +12 -0
  104. package/src/runtime/index.tsx +17 -7
  105. package/src/runtime/lifecycle.ts +16 -151
  106. package/src/runtime/loaderBridge.ts +257 -0
  107. package/src/runtime/outlet.tsx +48 -0
  108. package/src/runtime/plugin.node.tsx +72 -85
  109. package/src/runtime/plugin.tsx +361 -206
  110. package/src/runtime/plugin.worker.tsx +4 -0
  111. package/src/runtime/pluginCore.ts +48 -0
  112. package/src/runtime/prefetchLink.tsx +1 -1
  113. package/src/runtime/register.ts +58 -0
  114. package/src/runtime/routeTree.ts +163 -354
  115. package/src/runtime/router.ts +15 -0
  116. package/src/runtime/rsc/payloadRouter.ts +45 -2
  117. package/src/runtime/ssr-shim.d.ts +1 -3
  118. package/src/runtime/state.ts +29 -0
  119. package/src/runtime/types.ts +32 -66
  120. package/src/runtime/utils.tsx +3 -6
  121. package/tests/router/cli.test.ts +586 -5
  122. package/tests/router/fastDefaults.test.ts +25 -0
  123. package/tests/router/hooks.test.ts +26 -0
  124. package/tests/router/hydrationBoundary.test.tsx +23 -0
  125. package/tests/router/loaderBridge.test.ts +211 -0
  126. package/tests/router/packageSurface.test.ts +24 -0
  127. package/tests/router/prefetchLink.test.tsx +43 -7
  128. package/tests/router/register.test.ts +46 -0
  129. package/tests/router/routeTree.test.ts +381 -81
  130. package/tests/router/rsc.test.tsx +70 -0
  131. package/tests/router/tanstackTypes.test.ts +573 -1
  132. package/dist/cjs/runtime/DefaultNotFound.js +0 -47
  133. package/dist/esm/runtime/DefaultNotFound.mjs +0 -13
  134. package/dist/esm-node/runtime/DefaultNotFound.mjs +0 -14
  135. package/dist/types/runtime/DefaultNotFound.d.ts +0 -2
  136. package/src/runtime/DefaultNotFound.tsx +0 -15
@@ -1,10 +1,18 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import path from 'node:path';
1
3
  import type { RouteObject } from '@modern-js/runtime-utils/router';
2
4
  import type { NestedRoute } from '@modern-js/types';
3
5
  import { createMemoryHistory } from '@tanstack/history';
4
- import { createRouter } from '@tanstack/react-router';
6
+ import { createRouter, Outlet, RouterProvider } from '@tanstack/react-router';
7
+ import type { ComponentType } from 'react';
8
+ import { createElement, lazy } from 'react';
9
+ import { renderToStaticMarkup, renderToString } from 'react-dom/server';
10
+ import * as TanstackRuntime from '../../src/runtime';
11
+ import { Outlet as PublicOutlet } from '../../src/runtime';
12
+ import { Outlet as ModernOutlet } from '../../src/runtime/outlet';
5
13
  import {
6
- createRouteTreeFromModernRoutes,
7
14
  createRouteTreeFromRouteObjects,
15
+ getModernRouteIdsFromMatches,
8
16
  } from '../../src/runtime/routeTree';
9
17
  import { __setTanstackRscPayloadDecoderForTests } from '../../src/runtime/rsc/payloadRouter';
10
18
  import { createRouteObjectsFromConfig } from '../../src/runtime/utils';
@@ -23,6 +31,9 @@ type TestRouteObject = RouteObject & {
23
31
  hasLoader?: boolean;
24
32
  inValidSSRRoute?: boolean;
25
33
  isClientComponent?: boolean;
34
+ lazyImport?: () => Promise<unknown>;
35
+ loaderDeps?: unknown;
36
+ validateSearch?: unknown;
26
37
  };
27
38
 
28
39
  type TestNestedRoute = NestedRoute & {
@@ -30,6 +41,8 @@ type TestNestedRoute = NestedRoute & {
30
41
  hasAction?: boolean;
31
42
  hasClientLoader?: boolean;
32
43
  hasLoader?: boolean;
44
+ loaderDeps?: unknown;
45
+ validateSearch?: unknown;
33
46
  };
34
47
 
35
48
  type ShouldRevalidateArgs = {
@@ -48,20 +61,31 @@ type ShouldReloadArgs = {
48
61
 
49
62
  type TestRoute = {
50
63
  options: {
64
+ component?: unknown;
51
65
  shouldReload?: (args: ShouldReloadArgs) => boolean | undefined;
52
66
  ssr?: boolean;
53
67
  staticData: Record<string, unknown>;
68
+ loaderDeps?: unknown;
69
+ validateSearch?: unknown;
70
+ wrapInSuspense?: unknown;
54
71
  };
55
72
  };
56
73
 
74
+ type PreloadableTestComponent = {
75
+ load?: () => Promise<unknown>;
76
+ preload?: () => Promise<unknown>;
77
+ };
78
+
57
79
  type TestRouter = {
58
80
  load: () => Promise<void>;
59
81
  looseRoutesById: Partial<Record<string, TestRoute>>;
60
82
  state: {
61
83
  matches: Array<{
84
+ error?: unknown;
62
85
  loaderData?: unknown;
63
86
  routeId: string;
64
87
  }>;
88
+ statusCode?: number;
65
89
  };
66
90
  };
67
91
 
@@ -107,6 +131,38 @@ function getLooseRouteByModernRouteId(
107
131
  return route;
108
132
  }
109
133
 
134
+ function countCompletedSuspenseBoundaries(markup: string) {
135
+ return markup.match(/<!--\$-->/g)?.length || 0;
136
+ }
137
+
138
+ describe('tanstack runtime public exports', () => {
139
+ test('exports the Modern Outlet implementation from the runtime entrypoint', () => {
140
+ expect(PublicOutlet).toBe(ModernOutlet);
141
+ });
142
+
143
+ test('does not expose the unowned composite RSC helper API', () => {
144
+ expect('CompositeComponent' in TanstackRuntime).toBe(false);
145
+
146
+ const packageJson = JSON.parse(
147
+ readFileSync(path.join(__dirname, '../../package.json'), 'utf-8'),
148
+ ) as {
149
+ exports: Record<string, unknown>;
150
+ typesVersions?: Record<string, Record<string, string[]>>;
151
+ };
152
+
153
+ expect(packageJson.exports['./runtime/rsc']).toBeUndefined();
154
+ expect(packageJson.exports['./runtime/rsc/client']).toBeUndefined();
155
+ expect(packageJson.exports['./runtime/rsc/server']).toBeUndefined();
156
+ expect(packageJson.typesVersions?.['*']?.['runtime/rsc']).toBeUndefined();
157
+ expect(
158
+ packageJson.typesVersions?.['*']?.['runtime/rsc/client'],
159
+ ).toBeUndefined();
160
+ expect(
161
+ packageJson.typesVersions?.['*']?.['runtime/rsc/server'],
162
+ ).toBeUndefined();
163
+ });
164
+ });
165
+
110
166
  describe('tanstack route tree from RouteObject[]', () => {
111
167
  afterEach(() => {
112
168
  __setTanstackRscPayloadDecoderForTests();
@@ -145,6 +201,173 @@ describe('tanstack route tree from RouteObject[]', () => {
145
201
  expect(userMatch?.loaderData).toEqual({ id: '123' });
146
202
  });
147
203
 
204
+ test('reports native TanStack unknown routes as HTTP 404', async () => {
205
+ const routes: RouteObject[] = [
206
+ {
207
+ id: 'root',
208
+ path: '/',
209
+ Component: () => null,
210
+ children: [
211
+ {
212
+ id: 'known',
213
+ path: 'known',
214
+ Component: () => null,
215
+ },
216
+ ],
217
+ },
218
+ ];
219
+
220
+ const routeTree = createRouteTreeFromRouteObjects(routes);
221
+ const router = await loadRouteTree(routeTree, '/missing');
222
+
223
+ expect(router.state.statusCode).toBe(404);
224
+ });
225
+
226
+ test('does not force Suspense wrappers for ordinary generated routes', async () => {
227
+ const routes: RouteObject[] = [
228
+ {
229
+ id: 'root',
230
+ path: '/',
231
+ Component: () => createElement(Outlet),
232
+ children: [
233
+ {
234
+ id: 'plain',
235
+ path: 'plain',
236
+ Component: () => null,
237
+ },
238
+ ],
239
+ },
240
+ ];
241
+
242
+ const routeTree = createRouteTreeFromRouteObjects(routes);
243
+ const router = await loadRouteTree(routeTree, '/plain');
244
+
245
+ expect(routeTree.options.wrapInSuspense).toBeUndefined();
246
+ expect(
247
+ getLooseRoute(router, '/plain').options.wrapInSuspense,
248
+ ).toBeUndefined();
249
+ });
250
+
251
+ test('renders Modern Outlet through TanStack native outlet', async () => {
252
+ const routes: RouteObject[] = [
253
+ {
254
+ id: 'root',
255
+ path: '/',
256
+ Component: () =>
257
+ createElement('section', null, createElement(ModernOutlet)),
258
+ children: [
259
+ {
260
+ id: 'plain',
261
+ path: 'plain',
262
+ Component: () => createElement('main', null, 'Plain child route'),
263
+ },
264
+ ],
265
+ },
266
+ ];
267
+
268
+ const routeTree = createRouteTreeFromRouteObjects(routes);
269
+ const router = await loadRouteTree(routeTree, '/plain');
270
+ const markup = renderToString(
271
+ createElement(RouterProvider, { router } as never),
272
+ );
273
+ const suspenseBoundaryCount = countCompletedSuspenseBoundaries(markup);
274
+
275
+ expect(markup).toContain('Plain child route');
276
+ expect(suspenseBoundaryCount).toBe(1);
277
+ });
278
+
279
+ test('resolves matched Modern route ids from TanStack route registry fallback', () => {
280
+ const router = {
281
+ state: {
282
+ matches: [
283
+ { routeId: '__root__' },
284
+ { routeId: '/$lang' },
285
+ { routeId: '/$lang/tractors' },
286
+ {
287
+ route: {
288
+ options: {
289
+ staticData: {
290
+ modernRouteId: '(lang)/stores/page',
291
+ },
292
+ },
293
+ },
294
+ routeId: '/$lang/stores',
295
+ },
296
+ ],
297
+ },
298
+ routesById: {
299
+ __root__: {
300
+ options: {
301
+ staticData: {
302
+ modernRouteId: 'layout',
303
+ },
304
+ },
305
+ },
306
+ '/$lang': {
307
+ options: {
308
+ staticData: {
309
+ modernRouteId: '(lang)/page',
310
+ },
311
+ },
312
+ },
313
+ '/$lang/tractors': {
314
+ options: {
315
+ staticData: {
316
+ modernRouteId: '(lang)/tractors/page',
317
+ },
318
+ },
319
+ },
320
+ },
321
+ };
322
+
323
+ expect(getModernRouteIdsFromMatches(router as never)).toEqual([
324
+ 'layout',
325
+ '(lang)/page',
326
+ '(lang)/tractors/page',
327
+ '(lang)/stores/page',
328
+ ]);
329
+ });
330
+
331
+ test('preserves TanStack search contracts from RouteObject routes', () => {
332
+ const rootValidateSearch = (search: unknown) => ({ root: search });
333
+ const rootLoaderDeps = ({ search }: { search: unknown }) => ({ search });
334
+ const childValidateSearch = (search: unknown) => ({ child: search });
335
+ const childLoaderDeps = ({ search }: { search: unknown }) => ({ search });
336
+ const routes: TestRouteObject[] = [
337
+ {
338
+ id: 'root',
339
+ path: '/',
340
+ validateSearch: rootValidateSearch,
341
+ loaderDeps: rootLoaderDeps,
342
+ Component: () => null,
343
+ children: [
344
+ {
345
+ id: 'search',
346
+ path: 'search',
347
+ validateSearch: childValidateSearch,
348
+ loaderDeps: childLoaderDeps,
349
+ Component: () => null,
350
+ },
351
+ ],
352
+ },
353
+ ];
354
+
355
+ const routeTree = createRouteTreeFromRouteObjects(routes);
356
+ const router = createRouter({
357
+ routeTree,
358
+ history: createMemoryHistory({
359
+ initialEntries: ['/search'],
360
+ }),
361
+ context: {},
362
+ }) as unknown as TestRouter;
363
+ const searchRoute = getLooseRoute(router, '/search');
364
+
365
+ expect(routeTree.options.validateSearch).toBe(rootValidateSearch);
366
+ expect(routeTree.options.loaderDeps).toBe(rootLoaderDeps);
367
+ expect(searchRoute.options.validateSearch).toBe(childValidateSearch);
368
+ expect(searchRoute.options.loaderDeps).toBe(childLoaderDeps);
369
+ });
370
+
148
371
  test('uses TanStack route ids when loading RSC payload route data', async () => {
149
372
  const rootLoader = rstest.fn(() => ({ source: 'modern-root' }));
150
373
  const userLoader = rstest.fn(() => ({ source: 'modern-user' }));
@@ -234,6 +457,145 @@ describe('tanstack route tree from RouteObject[]', () => {
234
457
  expect(splatParamValue).toBe('a/b/c');
235
458
  });
236
459
 
460
+ test('preloads lazy Modern route components for server rendering', async () => {
461
+ const LazyRouteComponent = () =>
462
+ createElement('main', null, 'Lazy route ready');
463
+ const lazyImport = rstest.fn(async () => ({
464
+ default: LazyRouteComponent,
465
+ }));
466
+ const routes: TestRouteObject[] = [
467
+ {
468
+ id: 'root',
469
+ path: '/',
470
+ Component: () => null,
471
+ children: [
472
+ {
473
+ id: 'lazy',
474
+ path: 'lazy',
475
+ Component: lazy(lazyImport),
476
+ lazyImport,
477
+ },
478
+ ],
479
+ },
480
+ ];
481
+
482
+ const routeTree = createRouteTreeFromRouteObjects(routes);
483
+ const router = await loadRouteTree(routeTree, '/lazy');
484
+ const lazyRoute = getLooseRoute(router, '/lazy');
485
+ const component = lazyRoute.options.component as ComponentType & {
486
+ preload?: () => Promise<unknown>;
487
+ };
488
+
489
+ await component.preload?.();
490
+
491
+ expect(lazyImport).toHaveBeenCalled();
492
+ });
493
+
494
+ test('renders preloaded lazy child routes through TanStack router SSR', async () => {
495
+ const LazyRouteComponent = () =>
496
+ createElement('main', null, 'Lazy child route ready');
497
+ const lazyImport = rstest.fn(async () => ({
498
+ default: LazyRouteComponent,
499
+ }));
500
+ const routes: TestRouteObject[] = [
501
+ {
502
+ id: 'root',
503
+ path: '/',
504
+ Component: () => createElement('section', null, createElement(Outlet)),
505
+ children: [
506
+ {
507
+ id: 'lazy',
508
+ path: 'lazy',
509
+ Component: lazy(lazyImport),
510
+ lazyImport,
511
+ },
512
+ ],
513
+ },
514
+ ];
515
+
516
+ const routeTree = createRouteTreeFromRouteObjects(routes);
517
+ const router = await loadRouteTree(routeTree, '/lazy');
518
+ const lazyRoute = getLooseRoute(router, '/lazy');
519
+ const lazyComponent = lazyRoute.options
520
+ .component as PreloadableTestComponent;
521
+
522
+ expect(
523
+ renderToStaticMarkup(createElement(RouterProvider, { router } as never)),
524
+ ).toContain('Lazy child route ready');
525
+ expect(typeof lazyComponent.load).toBe('function');
526
+ expect(typeof lazyComponent.preload).toBe('function');
527
+ });
528
+
529
+ test('exposes load-only Modern route components through TanStack preload', async () => {
530
+ const load = rstest.fn(async () => 'route chunk loaded');
531
+ const LoadOnlyRouteComponent = (() =>
532
+ createElement('main', null, 'Load-only route ready')) as ComponentType & {
533
+ load?: () => Promise<unknown>;
534
+ preload?: () => Promise<unknown>;
535
+ };
536
+ LoadOnlyRouteComponent.load = load;
537
+ const routes: TestRouteObject[] = [
538
+ {
539
+ id: 'root',
540
+ path: '/',
541
+ Component: () => createElement('section', null, createElement(Outlet)),
542
+ children: [
543
+ {
544
+ id: 'load-only',
545
+ path: 'load-only',
546
+ Component: LoadOnlyRouteComponent,
547
+ },
548
+ ],
549
+ },
550
+ ];
551
+
552
+ const routeTree = createRouteTreeFromRouteObjects(routes);
553
+ const router = await loadRouteTree(routeTree, '/load-only');
554
+ const loadOnlyRoute = getLooseRoute(router, '/load-only');
555
+ const loadOnlyComponent = loadOnlyRoute.options
556
+ .component as PreloadableTestComponent;
557
+
558
+ expect(typeof loadOnlyComponent.load).toBe('function');
559
+ expect(typeof loadOnlyComponent.preload).toBe('function');
560
+ expect(load).toHaveBeenCalledTimes(1);
561
+ await loadOnlyComponent.preload?.();
562
+ expect(load).toHaveBeenCalledTimes(2);
563
+ });
564
+
565
+ test('unwraps nested ESM route module defaults before server rendering', async () => {
566
+ const LazyRouteComponent = () =>
567
+ createElement('main', null, 'Nested lazy child route ready');
568
+ const lazyImport = rstest.fn(async () => ({
569
+ default: {
570
+ default: LazyRouteComponent,
571
+ },
572
+ }));
573
+ const routes: TestRouteObject[] = [
574
+ {
575
+ id: 'root',
576
+ path: '/',
577
+ Component: () => createElement('section', null, createElement(Outlet)),
578
+ children: [
579
+ {
580
+ id: 'lazy',
581
+ path: 'lazy',
582
+ Component: lazy(
583
+ lazyImport as () => Promise<{ default: ComponentType }>,
584
+ ),
585
+ lazyImport,
586
+ },
587
+ ],
588
+ },
589
+ ];
590
+
591
+ const routeTree = createRouteTreeFromRouteObjects(routes);
592
+ const router = await loadRouteTree(routeTree, '/lazy');
593
+
594
+ expect(
595
+ renderToStaticMarkup(createElement(RouterProvider, { router } as never)),
596
+ ).toContain('Nested lazy child route ready');
597
+ });
598
+
237
599
  test('preserves route handle and maps shouldRevalidate to shouldReload', async () => {
238
600
  const shouldRevalidate = rstest.fn(({ nextUrl }: ShouldRevalidateArgs) =>
239
601
  nextUrl.pathname.endsWith('/456'),
@@ -368,95 +730,33 @@ describe('tanstack route tree from RouteObject[]', () => {
368
730
  await expect(loaderData?.later).resolves.toBe('done');
369
731
  });
370
732
 
371
- test('merges Modern generated route handle into TanStack static data', () => {
372
- const modernRoutes: NestedRoute[] = [
733
+ test('preserves returned non-404 Response loaders as loader data', async () => {
734
+ const response = new Response('route status payload', { status: 500 });
735
+ const routes: TestRouteObject[] = [
373
736
  {
374
- type: 'nested',
375
- origin: 'config',
376
737
  id: 'root',
377
- isRoot: true,
378
- config: {
379
- handle: {
380
- shell: true,
381
- },
382
- },
738
+ path: '/',
739
+ Component: () => null,
383
740
  children: [
384
741
  {
385
- type: 'nested',
386
- origin: 'config',
387
- id: 'dashboard',
388
- path: 'dashboard',
389
- handle: {
390
- section: 'analytics',
391
- },
392
- config: {
393
- handle: {
394
- role: 'admin',
395
- },
396
- },
742
+ id: 'broken',
743
+ path: 'broken',
744
+ loader: () => response,
745
+ Component: () => null,
397
746
  },
398
747
  ],
399
748
  },
400
749
  ];
401
- const routeTree = createRouteTreeFromModernRoutes(modernRoutes);
402
- const router = createRouter({
403
- routeTree,
404
- history: createMemoryHistory({
405
- initialEntries: ['/dashboard'],
406
- }),
407
- context: {},
408
- }) as unknown as TestRouter;
409
- const dashboardRoute = getLooseRoute(router, '/dashboard');
410
750
 
411
- expect(routeTree.options.staticData.modernRouteHandle).toEqual({
412
- shell: true,
413
- });
414
- expect(dashboardRoute.options.staticData.modernRouteHandle).toEqual({
415
- section: 'analytics',
416
- role: 'admin',
417
- });
418
- });
419
-
420
- test('preserves Modern generated client route metadata', () => {
421
- const modernRoutes: TestNestedRoute[] = [
422
- {
423
- type: 'nested',
424
- origin: 'config',
425
- id: 'root',
426
- isRoot: true,
427
- children: [
428
- {
429
- type: 'nested',
430
- origin: 'config',
431
- id: 'client',
432
- path: 'client',
433
- clientData: './client.data',
434
- hasAction: true,
435
- hasClientLoader: true,
436
- hasLoader: true,
437
- inValidSSRRoute: true,
438
- isClientComponent: true,
439
- },
440
- ],
441
- },
442
- ];
443
- const routeTree = createRouteTreeFromModernRoutes(modernRoutes);
444
- const router = createRouter({
445
- routeTree,
446
- history: createMemoryHistory({
447
- initialEntries: ['/client'],
448
- }),
449
- context: {},
450
- }) as unknown as TestRouter;
451
- const clientRoute = getLooseRouteByModernRouteId(router, 'client');
751
+ const routeTree = createRouteTreeFromRouteObjects(routes);
752
+ const router = await loadRouteTree(routeTree, '/broken');
753
+ const brokenMatch = router.state.matches.find(
754
+ match => match.routeId === '/broken',
755
+ );
452
756
 
453
- expect(clientRoute.options.ssr).toBe(false);
454
- expect(clientRoute.options.staticData).toMatchObject({
455
- modernRouteHasAction: true,
456
- modernRouteHasClientLoader: true,
457
- modernRouteHasLoader: true,
458
- modernRouteIsClientComponent: true,
459
- });
757
+ expect(router.state.statusCode).toBe(200);
758
+ expect(brokenMatch?.loaderData).toBe(response);
759
+ expect(brokenMatch?.error).toBeUndefined();
460
760
  });
461
761
 
462
762
  test('preserves generated client metadata through RouteObject conversion', () => {
@@ -1,3 +1,4 @@
1
+ import { UNSAFE_ErrorResponseImpl as ErrorResponseImpl } from '@modern-js/runtime-utils/router';
1
2
  import type React from 'react';
2
3
  import { isValidElement } from 'react';
3
4
  import { createRscProxy } from '../../src/runtime/rsc/createRscProxy';
@@ -30,6 +31,16 @@ async function readAll(stream: ReadableStream<Uint8Array>) {
30
31
  return chunks;
31
32
  }
32
33
 
34
+ function withNodeEnv<T>(value: string, callback: () => T): T {
35
+ const original = process.env.NODE_ENV;
36
+ process.env.NODE_ENV = value;
37
+ try {
38
+ return callback();
39
+ } finally {
40
+ process.env.NODE_ENV = original;
41
+ }
42
+ }
43
+
33
44
  describe('tanstack rsc runtime helpers', () => {
34
45
  afterEach(() => {
35
46
  __setTanstackRscPayloadDecoderForTests();
@@ -149,6 +160,65 @@ describe('tanstack rsc runtime helpers', () => {
149
160
  ).toBeUndefined();
150
161
  });
151
162
 
163
+ test('redacts production TanStack RSC server payload errors', () => {
164
+ const routeError = new ErrorResponseImpl(
165
+ 500,
166
+ 'secret status text',
167
+ 'route secret',
168
+ true,
169
+ );
170
+ const serverError = new Error('server secret');
171
+ serverError.stack = 'stack secret';
172
+
173
+ const payload = withNodeEnv('production', () =>
174
+ createTanstackRscServerPayload({
175
+ state: {
176
+ location: { href: '/products' },
177
+ matches: [
178
+ {
179
+ error: serverError,
180
+ params: {},
181
+ pathname: '/',
182
+ pathnameBase: '/',
183
+ route: { id: '__root__' },
184
+ routeId: '__root__',
185
+ },
186
+ {
187
+ error: routeError,
188
+ params: {},
189
+ pathname: '/products',
190
+ pathnameBase: '/products',
191
+ route: {
192
+ id: '/products',
193
+ parentRoute: { id: '__root__' },
194
+ options: { path: 'products' },
195
+ },
196
+ routeId: '/products',
197
+ },
198
+ ],
199
+ },
200
+ }),
201
+ );
202
+
203
+ expect(payload.errors).toMatchObject({
204
+ __root__: {
205
+ message: 'Unexpected Server Error',
206
+ stack: undefined,
207
+ __type: 'Error',
208
+ },
209
+ '/products': {
210
+ status: 500,
211
+ statusText: 'Internal Server Error',
212
+ data: 'Unexpected Server Error',
213
+ __type: 'RouteErrorResponse',
214
+ },
215
+ });
216
+ expect(JSON.stringify(payload.errors)).not.toContain('server secret');
217
+ expect(JSON.stringify(payload.errors)).not.toContain('route secret');
218
+ expect(JSON.stringify(payload.errors)).not.toContain('secret status text');
219
+ expect(JSON.stringify(payload.errors)).not.toContain('stack secret');
220
+ });
221
+
152
222
  test('converts TanStack RSC redirects to Modern RSC navigation headers', () => {
153
223
  const response = handleTanstackRscRedirect(
154
224
  new Headers({ Location: '/base/login' }),