@bleedingdev/modern-js-plugin-tanstack 3.2.0-ultramodern.99 → 3.4.0-ultramodern.1

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 (127) hide show
  1. package/dist/cjs/cli/index.js +80 -34
  2. package/dist/cjs/cli/routeSplitting.js +13 -41
  3. package/dist/cjs/cli/tanstackTypes.js +114 -200
  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 +12 -7
  9. package/dist/cjs/runtime/index.js +66 -54
  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 +190 -110
  14. package/dist/cjs/runtime/plugin.node.js +25 -40
  15. package/dist/cjs/runtime/plugin.worker.js +9 -5
  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 +30 -212
  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 +13 -9
  35. package/dist/cjs/runtime/utils.js +9 -11
  36. package/dist/cjs/runtime.js +9 -5
  37. package/dist/esm/cli/index.mjs +73 -28
  38. package/dist/esm/cli/routeSplitting.mjs +1 -30
  39. package/dist/esm/cli/tanstackTypes.mjs +100 -190
  40. package/dist/esm/runtime/hooks.mjs +1 -8
  41. package/dist/esm/runtime/hydrationBoundary.mjs +3 -2
  42. package/dist/esm/runtime/index.mjs +4 -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 +183 -107
  47. package/dist/esm/runtime/plugin.node.mjs +19 -38
  48. package/dist/esm/runtime/pluginCore.mjs +14 -0
  49. package/dist/esm/runtime/prefetchLink.mjs +1 -1
  50. package/dist/esm/runtime/register.mjs +18 -0
  51. package/dist/esm/runtime/routeTree.mjs +15 -198
  52. package/dist/esm/runtime/router.mjs +2 -0
  53. package/dist/esm/runtime/rsc/payloadRouter.mjs +35 -1
  54. package/dist/esm/runtime/state.mjs +7 -0
  55. package/dist/esm/runtime/utils.mjs +0 -6
  56. package/dist/esm-node/cli/index.mjs +73 -28
  57. package/dist/esm-node/cli/routeSplitting.mjs +1 -30
  58. package/dist/esm-node/cli/tanstackTypes.mjs +100 -190
  59. package/dist/esm-node/runtime/hooks.mjs +1 -8
  60. package/dist/esm-node/runtime/hydrationBoundary.mjs +3 -2
  61. package/dist/esm-node/runtime/index.mjs +4 -2
  62. package/dist/esm-node/runtime/lifecycle.mjs +1 -82
  63. package/dist/esm-node/runtime/loaderBridge.mjs +115 -0
  64. package/dist/esm-node/runtime/outlet.mjs +18 -0
  65. package/dist/esm-node/runtime/plugin.mjs +183 -107
  66. package/dist/esm-node/runtime/plugin.node.mjs +19 -38
  67. package/dist/esm-node/runtime/pluginCore.mjs +15 -0
  68. package/dist/esm-node/runtime/prefetchLink.mjs +1 -1
  69. package/dist/esm-node/runtime/register.mjs +19 -0
  70. package/dist/esm-node/runtime/routeTree.mjs +15 -198
  71. package/dist/esm-node/runtime/router.mjs +3 -0
  72. package/dist/esm-node/runtime/rsc/payloadRouter.mjs +35 -1
  73. package/dist/esm-node/runtime/state.mjs +8 -0
  74. package/dist/esm-node/runtime/utils.mjs +0 -6
  75. package/dist/types/cli/index.d.ts +11 -2
  76. package/dist/types/cli/routeSplitting.d.ts +6 -15
  77. package/dist/types/cli/tanstackTypes.d.ts +21 -1
  78. package/dist/types/runtime/hooks.d.ts +8 -33
  79. package/dist/types/runtime/index.d.ts +6 -3
  80. package/dist/types/runtime/lifecycle.d.ts +7 -22
  81. package/dist/types/runtime/loaderBridge.d.ts +48 -0
  82. package/dist/types/runtime/outlet.d.ts +2 -0
  83. package/dist/types/runtime/plugin.d.ts +1 -14
  84. package/dist/types/runtime/plugin.node.d.ts +1 -14
  85. package/dist/types/runtime/pluginCore.d.ts +21 -0
  86. package/dist/types/runtime/register.d.ts +9 -0
  87. package/dist/types/runtime/routeTree.d.ts +0 -2
  88. package/dist/types/runtime/router.d.ts +14 -0
  89. package/dist/types/runtime/state.d.ts +16 -0
  90. package/dist/types/runtime/types.d.ts +7 -53
  91. package/package.json +41 -39
  92. package/rstest.config.mts +6 -0
  93. package/src/cli/index.ts +146 -24
  94. package/src/cli/routeSplitting.ts +6 -44
  95. package/src/cli/tanstackTypes.ts +203 -208
  96. package/src/runtime/hooks.ts +10 -27
  97. package/src/runtime/hydrationBoundary.tsx +5 -5
  98. package/src/runtime/index.tsx +12 -14
  99. package/src/runtime/lifecycle.ts +16 -151
  100. package/src/runtime/loaderBridge.ts +257 -0
  101. package/src/runtime/outlet.tsx +48 -0
  102. package/src/runtime/plugin.node.tsx +28 -83
  103. package/src/runtime/plugin.tsx +350 -202
  104. package/src/runtime/pluginCore.ts +48 -0
  105. package/src/runtime/prefetchLink.tsx +1 -1
  106. package/src/runtime/register.ts +58 -0
  107. package/src/runtime/routeTree.ts +41 -365
  108. package/src/runtime/router.ts +15 -0
  109. package/src/runtime/rsc/payloadRouter.ts +45 -2
  110. package/src/runtime/ssr-shim.d.ts +1 -3
  111. package/src/runtime/state.ts +29 -0
  112. package/src/runtime/types.ts +20 -67
  113. package/src/runtime/utils.tsx +3 -7
  114. package/tests/router/cli.test.ts +373 -31
  115. package/tests/router/hooks.test.ts +26 -0
  116. package/tests/router/loaderBridge.test.ts +211 -0
  117. package/tests/router/packageSurface.test.ts +24 -0
  118. package/tests/router/prefetchLink.test.tsx +43 -7
  119. package/tests/router/register.test.ts +46 -0
  120. package/tests/router/routeTree.test.ts +176 -139
  121. package/tests/router/rsc.test.tsx +70 -0
  122. package/tests/router/tanstackTypes.test.ts +395 -7
  123. package/dist/cjs/runtime/DefaultNotFound.js +0 -47
  124. package/dist/esm/runtime/DefaultNotFound.mjs +0 -13
  125. package/dist/esm-node/runtime/DefaultNotFound.mjs +0 -14
  126. package/dist/types/runtime/DefaultNotFound.d.ts +0 -2
  127. package/src/runtime/DefaultNotFound.tsx +0 -15
@@ -1,12 +1,16 @@
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
6
  import { createRouter, Outlet, RouterProvider } from '@tanstack/react-router';
5
7
  import type { ComponentType } from 'react';
6
8
  import { createElement, lazy } from 'react';
7
- import { renderToStaticMarkup } from 'react-dom/server';
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';
8
13
  import {
9
- createRouteTreeFromModernRoutes,
10
14
  createRouteTreeFromRouteObjects,
11
15
  getModernRouteIdsFromMatches,
12
16
  } from '../../src/runtime/routeTree';
@@ -57,6 +61,7 @@ type ShouldReloadArgs = {
57
61
 
58
62
  type TestRoute = {
59
63
  options: {
64
+ component?: unknown;
60
65
  shouldReload?: (args: ShouldReloadArgs) => boolean | undefined;
61
66
  ssr?: boolean;
62
67
  staticData: Record<string, unknown>;
@@ -66,14 +71,21 @@ type TestRoute = {
66
71
  };
67
72
  };
68
73
 
74
+ type PreloadableTestComponent = {
75
+ load?: () => Promise<unknown>;
76
+ preload?: () => Promise<unknown>;
77
+ };
78
+
69
79
  type TestRouter = {
70
80
  load: () => Promise<void>;
71
81
  looseRoutesById: Partial<Record<string, TestRoute>>;
72
82
  state: {
73
83
  matches: Array<{
84
+ error?: unknown;
74
85
  loaderData?: unknown;
75
86
  routeId: string;
76
87
  }>;
88
+ statusCode?: number;
77
89
  };
78
90
  };
79
91
 
@@ -119,6 +131,38 @@ function getLooseRouteByModernRouteId(
119
131
  return route;
120
132
  }
121
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
+
122
166
  describe('tanstack route tree from RouteObject[]', () => {
123
167
  afterEach(() => {
124
168
  __setTanstackRscPayloadDecoderForTests();
@@ -157,6 +201,28 @@ describe('tanstack route tree from RouteObject[]', () => {
157
201
  expect(userMatch?.loaderData).toEqual({ id: '123' });
158
202
  });
159
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
+
160
226
  test('does not force Suspense wrappers for ordinary generated routes', async () => {
161
227
  const routes: RouteObject[] = [
162
228
  {
@@ -182,6 +248,34 @@ describe('tanstack route tree from RouteObject[]', () => {
182
248
  ).toBeUndefined();
183
249
  });
184
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
+
185
279
  test('resolves matched Modern route ids from TanStack route registry fallback', () => {
186
280
  const router = {
187
281
  state: {
@@ -394,9 +488,6 @@ describe('tanstack route tree from RouteObject[]', () => {
394
488
 
395
489
  await component.preload?.();
396
490
 
397
- expect(renderToStaticMarkup(createElement(component))).toContain(
398
- 'Lazy route ready',
399
- );
400
491
  expect(lazyImport).toHaveBeenCalled();
401
492
  });
402
493
 
@@ -424,10 +515,51 @@ describe('tanstack route tree from RouteObject[]', () => {
424
515
 
425
516
  const routeTree = createRouteTreeFromRouteObjects(routes);
426
517
  const router = await loadRouteTree(routeTree, '/lazy');
518
+ const lazyRoute = getLooseRoute(router, '/lazy');
519
+ const lazyComponent = lazyRoute.options
520
+ .component as PreloadableTestComponent;
427
521
 
428
522
  expect(
429
523
  renderToStaticMarkup(createElement(RouterProvider, { router } as never)),
430
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);
431
563
  });
432
564
 
433
565
  test('unwraps nested ESM route module defaults before server rendering', async () => {
@@ -598,125 +730,36 @@ describe('tanstack route tree from RouteObject[]', () => {
598
730
  await expect(loaderData?.later).resolves.toBe('done');
599
731
  });
600
732
 
601
- test('merges Modern generated route handle into TanStack static data', () => {
602
- const modernRoutes: NestedRoute[] = [
603
- {
604
- type: 'nested',
605
- origin: 'config',
606
- id: 'root',
607
- isRoot: true,
608
- config: {
609
- handle: {
610
- shell: true,
611
- },
612
- },
613
- children: [
614
- {
615
- type: 'nested',
616
- origin: 'config',
617
- id: 'dashboard',
618
- path: 'dashboard',
619
- handle: {
620
- section: 'analytics',
621
- },
622
- config: {
623
- handle: {
624
- role: 'admin',
625
- },
626
- },
627
- },
628
- ],
629
- },
630
- ];
631
- const routeTree = createRouteTreeFromModernRoutes(modernRoutes);
632
- const router = createRouter({
633
- routeTree,
634
- history: createMemoryHistory({
635
- initialEntries: ['/dashboard'],
636
- }),
637
- context: {},
638
- }) as unknown as TestRouter;
639
- const dashboardRoute = getLooseRoute(router, '/dashboard');
640
-
641
- expect(routeTree.options.staticData.modernRouteHandle).toEqual({
642
- shell: true,
643
- });
644
- expect(dashboardRoute.options.staticData.modernRouteHandle).toEqual({
645
- section: 'analytics',
646
- role: 'admin',
647
- });
648
- });
649
-
650
- test('preserves TanStack search contracts from Modern generated routes', () => {
651
- const rootValidateSearch = (search: unknown) => ({ root: search });
652
- const rootLoaderDeps = ({ search }: { search: unknown }) => ({ search });
653
- const childValidateSearch = (search: unknown) => ({ child: search });
654
- const childLoaderDeps = ({ search }: { search: unknown }) => ({ search });
655
- const modernRoutes: TestNestedRoute[] = [
656
- {
657
- type: 'nested',
658
- origin: 'config',
659
- id: 'root',
660
- isRoot: true,
661
- validateSearch: rootValidateSearch,
662
- loaderDeps: rootLoaderDeps,
663
- children: [
664
- {
665
- type: 'nested',
666
- origin: 'config',
667
- id: 'search',
668
- path: 'search',
669
- validateSearch: childValidateSearch,
670
- loaderDeps: childLoaderDeps,
671
- },
672
- ],
673
- },
674
- ];
675
- const routeTree = createRouteTreeFromModernRoutes(modernRoutes);
676
- const router = createRouter({
677
- routeTree,
678
- history: createMemoryHistory({
679
- initialEntries: ['/search'],
680
- }),
681
- context: {},
682
- }) as unknown as TestRouter;
683
- const searchRoute = getLooseRouteByModernRouteId(router, 'search');
684
-
685
- expect(routeTree.options.validateSearch).toBe(rootValidateSearch);
686
- expect(routeTree.options.loaderDeps).toBe(rootLoaderDeps);
687
- expect(searchRoute.options.validateSearch).toBe(childValidateSearch);
688
- expect(searchRoute.options.loaderDeps).toBe(childLoaderDeps);
689
- });
690
-
691
- test('does not force Suspense wrappers for ordinary Modern routes', async () => {
692
- const modernRoutes: TestNestedRoute[] = [
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[] = [
693
736
  {
694
- type: 'nested',
695
- origin: 'config',
696
737
  id: 'root',
697
- isRoot: true,
698
- component: () => createElement(Outlet),
738
+ path: '/',
739
+ Component: () => null,
699
740
  children: [
700
741
  {
701
- type: 'nested',
702
- origin: 'config',
703
- id: 'plain',
704
- path: 'plain',
705
- component: () => null,
742
+ id: 'broken',
743
+ path: 'broken',
744
+ loader: () => response,
745
+ Component: () => null,
706
746
  },
707
747
  ],
708
748
  },
709
749
  ];
710
750
 
711
- const routeTree = createRouteTreeFromModernRoutes(modernRoutes);
712
- const router = await loadRouteTree(routeTree, '/plain');
713
- const plain = getLooseRouteByModernRouteId(router, 'plain');
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
+ );
714
756
 
715
- expect(routeTree.options.wrapInSuspense).toBeUndefined();
716
- expect(plain.options.wrapInSuspense).toBeUndefined();
757
+ expect(router.state.statusCode).toBe(200);
758
+ expect(brokenMatch?.loaderData).toBe(response);
759
+ expect(brokenMatch?.error).toBeUndefined();
717
760
  });
718
761
 
719
- test('preserves Modern generated client route metadata', () => {
762
+ test('preserves generated client metadata through RouteObject conversion', () => {
720
763
  const modernRoutes: TestNestedRoute[] = [
721
764
  {
722
765
  type: 'nested',
@@ -730,16 +773,17 @@ describe('tanstack route tree from RouteObject[]', () => {
730
773
  id: 'client',
731
774
  path: 'client',
732
775
  clientData: './client.data',
733
- hasAction: true,
734
- hasClientLoader: true,
735
- hasLoader: true,
776
+ data: './data',
736
777
  inValidSSRRoute: true,
737
778
  isClientComponent: true,
738
779
  },
739
780
  ],
740
781
  },
741
782
  ];
742
- const routeTree = createRouteTreeFromModernRoutes(modernRoutes);
783
+ const routeObjects = createRouteObjectsFromConfig({
784
+ routesConfig: { routes: modernRoutes },
785
+ });
786
+ const routeTree = createRouteTreeFromRouteObjects(routeObjects || []);
743
787
  const router = createRouter({
744
788
  routeTree,
745
789
  history: createMemoryHistory({
@@ -751,51 +795,44 @@ describe('tanstack route tree from RouteObject[]', () => {
751
795
 
752
796
  expect(clientRoute.options.ssr).toBe(false);
753
797
  expect(clientRoute.options.staticData).toMatchObject({
754
- modernRouteHasAction: true,
755
798
  modernRouteHasClientLoader: true,
756
- modernRouteHasLoader: true,
757
799
  modernRouteIsClientComponent: true,
758
800
  });
759
801
  });
760
802
 
761
- test('preserves generated client metadata through RouteObject conversion', () => {
803
+ test('does not copy hasErrorBoundary into React Router route objects', () => {
804
+ function ErrorPage() {
805
+ return null;
806
+ }
807
+
762
808
  const modernRoutes: TestNestedRoute[] = [
763
809
  {
764
810
  type: 'nested',
765
811
  origin: 'config',
766
812
  id: 'root',
767
813
  isRoot: true,
814
+ hasErrorBoundary: true,
768
815
  children: [
769
816
  {
770
817
  type: 'nested',
771
818
  origin: 'config',
772
- id: 'client',
773
- path: 'client',
774
- clientData: './client.data',
775
- data: './data',
776
- inValidSSRRoute: true,
777
- isClientComponent: true,
819
+ id: 'child',
820
+ path: 'child',
821
+ hasErrorBoundary: true,
822
+ error: ErrorPage,
778
823
  },
779
824
  ],
780
825
  },
781
826
  ];
827
+
782
828
  const routeObjects = createRouteObjectsFromConfig({
783
829
  routesConfig: { routes: modernRoutes },
784
830
  });
785
- const routeTree = createRouteTreeFromRouteObjects(routeObjects || []);
786
- const router = createRouter({
787
- routeTree,
788
- history: createMemoryHistory({
789
- initialEntries: ['/client'],
790
- }),
791
- context: {},
792
- }) as unknown as TestRouter;
793
- const clientRoute = getLooseRouteByModernRouteId(router, 'client');
831
+ const rootRoute = routeObjects?.[0];
832
+ const childRoute = rootRoute?.children?.[0];
794
833
 
795
- expect(clientRoute.options.ssr).toBe(false);
796
- expect(clientRoute.options.staticData).toMatchObject({
797
- modernRouteHasClientLoader: true,
798
- modernRouteIsClientComponent: true,
799
- });
834
+ expect(rootRoute).not.toHaveProperty('hasErrorBoundary');
835
+ expect(childRoute).not.toHaveProperty('hasErrorBoundary');
836
+ expect(childRoute?.errorElement).toBeDefined();
800
837
  });
801
838
  });
@@ -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' }),