@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.3 → 3.9.0-ultramodern.5

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 (80) hide show
  1. package/dist/cjs/cli/index.js +9 -0
  2. package/dist/cjs/cli/tanstackTypes/source.js +3 -1
  3. package/dist/cjs/runtime/basepathRewrite.js +13 -6
  4. package/dist/cjs/runtime/lifecycle.js +7 -7
  5. package/dist/cjs/runtime/plugin.js +1 -1
  6. package/dist/cjs/runtime/plugin.node.js +5 -1
  7. package/dist/cjs/runtime/prefetchLink.js +2 -7
  8. package/dist/cjs/runtime/register.js +2 -2
  9. package/dist/cjs/runtime/routeTree/construction.js +2 -0
  10. package/dist/cjs/runtime/router.js +25 -9
  11. package/dist/cjs/runtime/state.js +2 -2
  12. package/dist/cjs/runtime/utils.js +2 -0
  13. package/dist/esm/cli/index.mjs +9 -0
  14. package/dist/esm/cli/tanstackTypes/source.mjs +3 -1
  15. package/dist/esm/runtime/basepathRewrite.mjs +10 -3
  16. package/dist/esm/runtime/lifecycle.mjs +1 -1
  17. package/dist/esm/runtime/plugin.mjs +1 -1
  18. package/dist/esm/runtime/plugin.node.mjs +5 -1
  19. package/dist/esm/runtime/prefetchLink.mjs +1 -6
  20. package/dist/esm/runtime/register.mjs +1 -1
  21. package/dist/esm/runtime/routeTree/construction.mjs +2 -0
  22. package/dist/esm/runtime/router.mjs +27 -11
  23. package/dist/esm/runtime/state.mjs +1 -1
  24. package/dist/esm/runtime/utils.mjs +2 -0
  25. package/dist/esm-node/cli/index.mjs +9 -0
  26. package/dist/esm-node/cli/tanstackTypes/source.mjs +3 -1
  27. package/dist/esm-node/runtime/basepathRewrite.mjs +10 -3
  28. package/dist/esm-node/runtime/lifecycle.mjs +1 -1
  29. package/dist/esm-node/runtime/plugin.mjs +1 -1
  30. package/dist/esm-node/runtime/plugin.node.mjs +5 -1
  31. package/dist/esm-node/runtime/prefetchLink.mjs +1 -6
  32. package/dist/esm-node/runtime/register.mjs +1 -1
  33. package/dist/esm-node/runtime/routeTree/construction.mjs +2 -0
  34. package/dist/esm-node/runtime/router.mjs +27 -11
  35. package/dist/esm-node/runtime/state.mjs +1 -1
  36. package/dist/esm-node/runtime/utils.mjs +2 -0
  37. package/dist/types/runtime/basepathRewrite.d.ts +2 -1
  38. package/dist/types/runtime/lifecycle.d.ts +9 -2
  39. package/dist/types/runtime/register.d.ts +1 -1
  40. package/dist/types/runtime/router.d.ts +2 -13
  41. package/dist/types/runtime/state.d.ts +1 -1
  42. package/dist/types/runtime/types.d.ts +2 -1
  43. package/package.json +12 -9
  44. package/rstest.config.mts +1 -6
  45. package/src/cli/index.ts +10 -0
  46. package/src/cli/tanstackTypes/source.ts +3 -1
  47. package/src/runtime/basepathRewrite.ts +11 -2
  48. package/src/runtime/lifecycle.ts +14 -3
  49. package/src/runtime/plugin.node.tsx +9 -2
  50. package/src/runtime/plugin.tsx +5 -1
  51. package/src/runtime/prefetchLink.tsx +1 -23
  52. package/src/runtime/register.ts +1 -1
  53. package/src/runtime/routeTree/construction.ts +2 -0
  54. package/src/runtime/router.ts +25 -40
  55. package/src/runtime/state.ts +1 -1
  56. package/src/runtime/types.ts +2 -2
  57. package/src/runtime/utils.tsx +3 -1
  58. package/tests/realm-isolation.test.ts +8 -2
  59. package/tests/router/cli.test.ts +108 -9
  60. package/tests/router/dataMutation.test.tsx +17 -121
  61. package/tests/router/flightSerialization.roundtrip.test.tsx +9 -6
  62. package/tests/router/generateRouteArtifacts.test.ts +69 -29
  63. package/tests/router/hooks.test.ts +1 -18
  64. package/tests/router/loaderBridge.test.ts +0 -77
  65. package/tests/router/localisedIdentity.test.tsx +95 -0
  66. package/tests/router/prefetchLink.test.tsx +28 -16
  67. package/tests/router/prefetchLinkPreload.test.tsx +21 -28
  68. package/tests/router/register.test.ts +1 -1
  69. package/tests/router/routeHooks.test.ts +41 -105
  70. package/tests/router/routeTree.test.ts +0 -212
  71. package/tests/router/routerCompatibility.test.ts +32 -9
  72. package/tests/router/rsc.test.tsx +118 -136
  73. package/tests/router/rscDecoderSecurity.test.ts +24 -53
  74. package/tests/router/serverPlugin.test.ts +43 -1
  75. package/tests/router/tanstackTypes.test.ts +24 -364
  76. package/tests/router/fastDefaults.test.ts +0 -32
  77. package/tests/router/hydrationBoundary.test.tsx +0 -23
  78. package/tests/router/packageSurface.test.ts +0 -24
  79. package/tests/router/preloadRedirect.test.ts +0 -64
  80. package/tests/router/rscPayloadRouterMatrix.test.ts +0 -197
@@ -1,5 +1,3 @@
1
- import { readFileSync } from 'node:fs';
2
- import path from 'node:path';
3
1
  import type { RouteObject } from '@modern-js/runtime-utils/router';
4
2
  import type { NestedRoute } from '@modern-js/types';
5
3
  import { createMemoryHistory } from '@tanstack/history';
@@ -12,8 +10,6 @@ import {
12
10
  import type { ComponentType } from 'react';
13
11
  import { createElement, lazy } from 'react';
14
12
  import { renderToStaticMarkup, renderToString } from 'react-dom/server';
15
- import * as TanstackRuntime from '../../src/runtime';
16
- import { Outlet as PublicOutlet } from '../../src/runtime';
17
13
  import { Outlet as ModernOutlet } from '../../src/runtime/outlet';
18
14
  import {
19
15
  createRouteTreeFromRouteObjects,
@@ -144,38 +140,6 @@ function getRouteByModernRouteId(
144
140
  return route;
145
141
  }
146
142
 
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
- });
178
-
179
143
  describe('tanstack route tree from RouteObject[]', () => {
180
144
  afterEach(() => {
181
145
  __setTanstackRscPayloadDecoderForTests();
@@ -311,29 +275,6 @@ describe('tanstack route tree from RouteObject[]', () => {
311
275
  );
312
276
  });
313
277
 
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
278
  test('renders Modern Outlet through TanStack native outlet', async () => {
338
279
  const routes: RouteObject[] = [
339
280
  {
@@ -356,10 +297,7 @@ describe('tanstack route tree from RouteObject[]', () => {
356
297
  const markup = renderToString(
357
298
  createElement(RouterProvider, { router } as never),
358
299
  );
359
- const suspenseBoundaryCount = countCompletedSuspenseBoundaries(markup);
360
-
361
300
  expect(markup).toContain('Plain child route');
362
- expect(suspenseBoundaryCount).toBe(1);
363
301
  });
364
302
 
365
303
  test('resolves matched Modern route ids from TanStack route registry fallback', () => {
@@ -414,46 +352,6 @@ describe('tanstack route tree from RouteObject[]', () => {
414
352
  ]);
415
353
  });
416
354
 
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
355
  test('uses TanStack route ids when loading RSC payload route data', async () => {
458
356
  const rootLoader = rstest.fn(() => ({ source: 'modern-root' }));
459
357
  const userLoader = rstest.fn(() => ({ source: 'modern-user' }));
@@ -543,40 +441,6 @@ describe('tanstack route tree from RouteObject[]', () => {
543
441
  expect(splatParamValue).toBe('a/b/c');
544
442
  });
545
443
 
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
444
  test('renders preloaded lazy child routes through TanStack router SSR', async () => {
581
445
  const LazyRouteComponent = () =>
582
446
  createElement('main', null, 'Lazy child route ready');
@@ -601,15 +465,10 @@ describe('tanstack route tree from RouteObject[]', () => {
601
465
 
602
466
  const routeTree = createRouteTreeFromRouteObjects(routes);
603
467
  const router = await loadRouteTree(routeTree, '/lazy');
604
- const lazyRoute = getRoute(router, '/lazy');
605
- const lazyComponent = lazyRoute.options
606
- .component as PreloadableTestComponent;
607
468
 
608
469
  expect(
609
470
  renderToStaticMarkup(createElement(RouterProvider, { router } as never)),
610
471
  ).toContain('Lazy child route ready');
611
- expect(typeof lazyComponent.load).toBe('function');
612
- expect(typeof lazyComponent.preload).toBe('function');
613
472
  });
614
473
 
615
474
  test('exposes load-only Modern route components through TanStack preload', async () => {
@@ -745,41 +604,6 @@ describe('tanstack route tree from RouteObject[]', () => {
745
604
  );
746
605
  });
747
606
 
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,
780
- });
781
- });
782
-
783
607
  test('normalizes Modern deferred loader data for TanStack SSR', async () => {
784
608
  const routes: TestRouteObject[] = [
785
609
  {
@@ -893,40 +717,4 @@ describe('tanstack route tree from RouteObject[]', () => {
893
717
  modernRouteIsClientComponent: true,
894
718
  });
895
719
  });
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();
931
- });
932
720
  });
@@ -27,6 +27,8 @@ type LegacyRuntimeRouterModule = {
27
27
 
28
28
  type CompatibilityGraph = {
29
29
  factory: (...args: any[]) => any;
30
+ Link: unknown;
31
+ hooks: Record<string, unknown>;
30
32
  legacyRuntimeRouter: LegacyRuntimeRouterModule;
31
33
  resolveRouterProvider: (
32
34
  framework?: string,
@@ -45,16 +47,41 @@ async function loadCompatibilityGraph(): Promise<CompatibilityGraph> {
45
47
  const { tanstackRouterProviderFactory } = await import(
46
48
  '../../src/runtime/register'
47
49
  );
48
- const { resolveRouterProvider } = await import('@modern-js/runtime/context');
50
+ const { resolveRouterProvider } = await import(
51
+ '@modern-js/runtime-extensions/router-provider'
52
+ );
53
+ const { routerProviderRegistryHooks } = await import(
54
+ '../../src/runtime/hooks'
55
+ );
56
+ const { Link } = await import('../../src/runtime/prefetchLink');
49
57
 
50
58
  return {
51
59
  factory: tanstackRouterProviderFactory,
60
+ Link,
61
+ hooks: routerProviderRegistryHooks,
52
62
  legacyRuntimeRouter,
53
63
  resolveRouterProvider,
54
64
  routerPlugin,
55
65
  };
56
66
  }
57
67
 
68
+ function initializeRouter(graph: CompatibilityGraph) {
69
+ const beforeRender: ((context: any, interrupt: () => void) => unknown)[] = [];
70
+ const plugin = graph.routerPlugin({ framework: 'tanstack' });
71
+ plugin.setup({
72
+ getHooks: () => graph.hooks,
73
+ getRuntimeConfig: () => ({ router: { framework: 'tanstack' } }),
74
+ onAfterCreateRouter: () => undefined,
75
+ onBeforeRender: (
76
+ callback: (context: any, interrupt: () => void) => unknown,
77
+ ) => beforeRender.push(callback),
78
+ wrapRoot: () => undefined,
79
+ });
80
+ const context: any = {};
81
+ for (const callback of beforeRender) callback(context, () => {});
82
+ return context.router as { Link: unknown };
83
+ }
84
+
58
85
  function resolveTsgoBin(): string {
59
86
  const packageJsonPath = require.resolve(
60
87
  '@typescript/native-preview/package.json',
@@ -103,10 +130,7 @@ describe('runtime router compatibility', () => {
103
130
  const graph = await loadCompatibilityGraph();
104
131
 
105
132
  expect('createRouterPlugin' in graph.legacyRuntimeRouter).toBe(false);
106
- expect(graph.routerPlugin).toBe(graph.factory);
107
- expect(graph.routerPlugin({ framework: 'tanstack' })).toMatchObject({
108
- name: '@modern-js/plugin-router-tanstack',
109
- });
133
+ expect(initializeRouter(graph).Link).toBe(graph.Link);
110
134
  expect(
111
135
  graph.legacyRuntimeRouter.getLegacyRouterPluginInvocationCount(),
112
136
  ).toBe(0);
@@ -117,11 +141,10 @@ describe('runtime router compatibility', () => {
117
141
  const graphB = await loadCompatibilityGraph();
118
142
 
119
143
  expect(graphB.factory).not.toBe(graphA.factory);
120
- expect(graphB.routerPlugin).toBe(graphB.factory);
144
+ expect(graphB.routerPlugin).not.toBe(graphA.routerPlugin);
121
145
  expect(graphB.resolveRouterProvider('tanstack')).toBe(graphA.factory);
122
- expect(graphB.routerPlugin({ framework: 'tanstack' })).toMatchObject({
123
- name: '@modern-js/plugin-router-tanstack',
124
- });
146
+ expect(initializeRouter(graphA).Link).toBe(graphA.Link);
147
+ expect(initializeRouter(graphB).Link).toBe(graphB.Link);
125
148
  expect(
126
149
  graphA.legacyRuntimeRouter.getLegacyRouterPluginInvocationCount(),
127
150
  ).toBe(0);
@@ -1,11 +1,4 @@
1
1
  import { UNSAFE_ErrorResponseImpl as ErrorResponseImpl } from '@modern-js/runtime-utils/router';
2
- import type React from 'react';
3
- import { isValidElement } from 'react';
4
- import { createRscProxy } from '../../src/runtime/rsc/createRscProxy';
5
- import {
6
- reviveTanstackRscFlightValues,
7
- serializeTanstackRscFlightValues,
8
- } from '../../src/runtime/rsc/flightSerialization';
9
2
  import {
10
3
  __setTanstackRscPayloadDecoderForTests,
11
4
  createTanstackRscServerPayload,
@@ -13,12 +6,6 @@ import {
13
6
  loadTanstackRscRouteData,
14
7
  } from '../../src/runtime/rsc/payloadRouter';
15
8
  import { ReplayableStream } from '../../src/runtime/rsc/ReplayableStream';
16
- import {
17
- RENDERABLE_RSC,
18
- RSC_PROXY_PATH,
19
- SERVER_COMPONENT_STREAM,
20
- type ServerComponentStream,
21
- } from '../../src/runtime/rsc/symbols';
22
9
 
23
10
  async function readAll(stream: ReadableStream<Uint8Array>) {
24
11
  const reader = stream.getReader();
@@ -69,75 +56,6 @@ describe('tanstack rsc runtime helpers', () => {
69
56
  ]);
70
57
  });
71
58
 
72
- test('revives shared structures that the Flight serializer preserves', async () => {
73
- const shared = { label: 'shared' };
74
- const serialized = serializeTanstackRscFlightValues({
75
- left: shared,
76
- right: shared,
77
- }) as { left: unknown; right: unknown };
78
-
79
- const revived = (await reviveTanstackRscFlightValues(serialized)) as {
80
- left: unknown;
81
- right: unknown;
82
- };
83
-
84
- expect(serialized.left).toBe(serialized.right);
85
- expect(revived.left).toBe(revived.right);
86
- });
87
-
88
- test('serializes TanStack RSC flight values nested in map and set containers', () => {
89
- const stream: ServerComponentStream = {
90
- createReplayStream: () => new ReadableStream<Uint8Array>(),
91
- };
92
- const proxy = createRscProxy(() => null, {
93
- renderable: true,
94
- stream,
95
- }) as React.ReactElement & Record<PropertyKey, unknown>;
96
-
97
- const serialized = serializeTanstackRscFlightValues({
98
- map: new Map([['slot', proxy]]),
99
- set: new Set([proxy]),
100
- }) as {
101
- map: Map<string, Record<string, unknown>>;
102
- set: Set<Record<string, unknown>>;
103
- };
104
-
105
- const mapValue = serialized.map.get('slot');
106
- const [setValue] = serialized.set;
107
-
108
- expect(mapValue).not.toBe(proxy);
109
- expect(setValue).not.toBe(proxy);
110
- expect(mapValue).toMatchObject({
111
- __modernTanstackRsc: true,
112
- kind: 'renderable',
113
- });
114
- expect(setValue).toMatchObject({
115
- __modernTanstackRsc: true,
116
- kind: 'renderable',
117
- });
118
- });
119
-
120
- test('renderable RSC proxies preserve React element behavior and metadata', () => {
121
- const stream: ServerComponentStream = {
122
- createReplayStream: () => new ReadableStream<Uint8Array>(),
123
- };
124
- const proxy = createRscProxy(() => ({ sidebar: 'ok' }), {
125
- renderable: true,
126
- stream,
127
- }) as React.ReactElement & Record<PropertyKey, unknown>;
128
-
129
- expect(isValidElement(proxy)).toBe(true);
130
- expect(proxy[SERVER_COMPONENT_STREAM]).toBe(stream);
131
- expect(proxy[RENDERABLE_RSC]).toBe(true);
132
- expect(proxy.then).toBeUndefined();
133
- expect('__SEROVAL_STREAM__' in proxy).toBe(false);
134
- expect('__SEROVAL_SEQUENCE__' in proxy).toBe(false);
135
- expect(Symbol.iterator in proxy).toBe(false);
136
-
137
- const sidebar = proxy.sidebar as Record<PropertyKey, unknown>;
138
- expect(sidebar[RSC_PROXY_PATH]).toEqual(['sidebar']);
139
- });
140
-
141
59
  test('creates TanStack RSC server payload and omits client-loader data during RSC navigation', () => {
142
60
  const payload = createTanstackRscServerPayload(
143
61
  {
@@ -301,29 +219,39 @@ describe('tanstack rsc runtime helpers', () => {
301
219
  expect(JSON.stringify(payload.errors)).not.toContain('token secret');
302
220
  });
303
221
 
304
- test('converts TanStack RSC redirects to Modern RSC navigation headers', () => {
305
- const response = handleTanstackRscRedirect(
306
- new Headers({ Location: '/base/login' }),
307
- '/base',
308
- 302,
309
- );
310
-
311
- expect(response.status).toBe(302);
312
- expect(response.headers.get('Location')).toBeNull();
313
- expect(response.headers.get('X-Modernjs-Redirect')).toBe('/login');
314
- expect(response.headers.get('X-Modernjs-BaseUrl')).toBe('/base');
315
- });
316
-
317
- test('preserves TanStack RSC redirect paths that do not start with the basename', () => {
318
- const response = handleTanstackRscRedirect(
319
- new Headers({ Location: '/shop/base/login' }),
320
- '/base',
321
- 302,
322
- );
222
+ test('normalizes redirect basenames while preserving status and headers', () => {
223
+ const scenarios = [
224
+ { location: '/base', name: 'exact basename', redirect: '/' },
225
+ {
226
+ location: '/base/login?from=%2Fbase',
227
+ name: 'leading basename',
228
+ redirect: '/login?from=%2Fbase',
229
+ },
230
+ {
231
+ location: '/shop/base/login',
232
+ name: 'mid-path basename',
233
+ redirect: '/shop/base/login',
234
+ },
235
+ ] as const;
323
236
 
324
- expect(response.headers.get('X-Modernjs-Redirect')).toBe(
325
- '/shop/base/login',
326
- );
237
+ for (const scenario of scenarios) {
238
+ const response = handleTanstackRscRedirect(
239
+ new Headers({
240
+ Location: scenario.location,
241
+ 'X-Trace': scenario.name,
242
+ }),
243
+ '/base',
244
+ 307,
245
+ );
246
+
247
+ expect(response.status).toBe(307);
248
+ expect(response.headers.get('Location')).toBeNull();
249
+ expect(response.headers.get('X-Modernjs-BaseUrl')).toBe('/base');
250
+ expect(response.headers.get('X-Modernjs-Redirect')).toBe(
251
+ scenario.redirect,
252
+ );
253
+ expect(response.headers.get('X-Trace')).toBe(scenario.name);
254
+ }
327
255
  });
328
256
 
329
257
  test('preserves RSC redirect response status and headers in TanStack redirects', async () => {
@@ -362,47 +290,50 @@ describe('tanstack rsc runtime helpers', () => {
362
290
  expect(thrown.options?.to).toBe('/login');
363
291
  });
364
292
 
365
- test('loads one RSC payload for multiple server route loaders', async () => {
366
- const payload = {
367
- type: 'render',
368
- actionData: null,
369
- errors: null,
370
- loaderData: {
371
- __root__: { shell: true },
372
- '/products': { product: 1 },
293
+ test('separates RSC payload cache entries by URL and request method', async () => {
294
+ const payloads = [
295
+ {
296
+ type: 'render',
297
+ actionData: null,
298
+ errors: null,
299
+ loaderData: { '/method': { method: 'GET' } },
300
+ location: { href: '/method' },
301
+ routes: [{ id: '/method', hasLoader: true }],
373
302
  },
374
- location: { href: '/products' },
375
- routes: [
376
- { id: '__root__', hasLoader: true },
377
- { id: '/products', hasLoader: true },
378
- ],
379
- };
380
- const fetchMock = rstest.fn(() => Promise.resolve(new Response('payload')));
381
- const decodeMock = rstest.fn(async () => payload);
303
+ {
304
+ type: 'render',
305
+ actionData: null,
306
+ errors: null,
307
+ loaderData: { '/method': { method: 'POST' } },
308
+ location: { href: '/method' },
309
+ routes: [{ id: '/method', hasLoader: true }],
310
+ },
311
+ ];
312
+ let payloadIndex = 0;
313
+ const fetchMock = rstest.fn(() =>
314
+ Promise.resolve(new Response(JSON.stringify(payloads[payloadIndex++]))),
315
+ );
382
316
  rstest.stubGlobal('fetch', fetchMock);
383
- __setTanstackRscPayloadDecoderForTests(decodeMock);
317
+ __setTanstackRscPayloadDecoderForTests(async stream =>
318
+ JSON.parse(await new Response(stream).text()),
319
+ );
384
320
 
385
- const request = new Request('http://localhost/products');
386
- const [rootData, productData] = await Promise.all([
321
+ const [getData, postData] = await Promise.all([
387
322
  loadTanstackRscRouteData({
388
- loadClientData: async () => ({ fallback: 'root' }),
389
- request,
390
- routeId: '__root__',
323
+ loadClientData: async () => ({ fallback: 'get' }),
324
+ request: new Request('http://localhost/method', { method: 'GET' }),
325
+ routeId: '/method',
391
326
  }),
392
327
  loadTanstackRscRouteData({
393
- loadClientData: async () => ({ fallback: 'product' }),
394
- request,
395
- routeId: '/products',
328
+ loadClientData: async () => ({ fallback: 'post' }),
329
+ request: new Request('http://localhost/method', { method: 'POST' }),
330
+ routeId: '/method',
396
331
  }),
397
332
  ]);
398
333
 
399
- expect(rootData).toEqual({ shell: true });
400
- expect(productData).toEqual({ product: 1 });
401
- expect(fetchMock).toHaveBeenCalledTimes(1);
402
- expect(fetchMock.mock.calls[0]?.[1]?.headers.get('x-rsc-tree')).toBe(
403
- 'true',
404
- );
405
- expect(decodeMock).toHaveBeenCalledTimes(1);
334
+ expect(getData).toEqual({ method: 'GET' });
335
+ expect(postData).toEqual({ method: 'POST' });
336
+ expect(fetchMock).toHaveBeenCalledTimes(2);
406
337
  });
407
338
 
408
339
  test('separates RSC payload cache entries by forwarded request headers', async () => {
@@ -464,6 +395,57 @@ describe('tanstack rsc runtime helpers', () => {
464
395
  expect(fetchMock).toHaveBeenCalledTimes(2);
465
396
  });
466
397
 
398
+ test('rethrows matched route errors from the decoded payload', async () => {
399
+ const routeError = new Error('route loader failed');
400
+ const payload = {
401
+ type: 'render',
402
+ actionData: null,
403
+ errors: { '/broken': routeError },
404
+ loaderData: {},
405
+ location: { href: '/broken' },
406
+ routes: [{ id: '/broken', hasLoader: true }],
407
+ };
408
+ rstest.stubGlobal(
409
+ 'fetch',
410
+ rstest.fn(() => Promise.resolve(new Response('payload'))),
411
+ );
412
+ __setTanstackRscPayloadDecoderForTests(async () => payload);
413
+
414
+ await expect(
415
+ loadTanstackRscRouteData({
416
+ loadClientData: async () => ({ fallback: true }),
417
+ request: new Request('http://localhost/broken'),
418
+ routeId: '/broken',
419
+ }),
420
+ ).rejects.toBe(routeError);
421
+ });
422
+
423
+ test('returns undefined for a route id missing from the decoded payload', async () => {
424
+ const loadClientData = rstest.fn(async () => ({ fallback: true }));
425
+ const payload = {
426
+ type: 'render',
427
+ actionData: null,
428
+ errors: null,
429
+ loaderData: { '/known': { known: true } },
430
+ location: { href: '/known' },
431
+ routes: [{ id: '/known', hasLoader: true }],
432
+ };
433
+ rstest.stubGlobal(
434
+ 'fetch',
435
+ rstest.fn(() => Promise.resolve(new Response('payload'))),
436
+ );
437
+ __setTanstackRscPayloadDecoderForTests(async () => payload);
438
+
439
+ await expect(
440
+ loadTanstackRscRouteData({
441
+ loadClientData,
442
+ request: new Request('http://localhost/missing'),
443
+ routeId: '/missing',
444
+ }),
445
+ ).resolves.toBeUndefined();
446
+ expect(loadClientData).not.toHaveBeenCalled();
447
+ });
448
+
467
449
  test('uses client loader data without requesting an RSC payload for client-loader routes', async () => {
468
450
  const fetchMock = rstest.fn();
469
451
  const loadClientData = rstest.fn(async () => ({ client: true }));