@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.4 → 3.9.0-ultramodern.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/cli/index.js +9 -0
- package/dist/cjs/runtime/lifecycle.js +7 -7
- package/dist/cjs/runtime/prefetchLink.js +2 -7
- package/dist/cjs/runtime/register.js +0 -2
- package/dist/cjs/runtime/router.js +25 -9
- package/dist/cjs/runtime/state.js +2 -2
- package/dist/esm/cli/index.mjs +9 -0
- package/dist/esm/runtime/lifecycle.mjs +1 -1
- package/dist/esm/runtime/prefetchLink.mjs +1 -6
- package/dist/esm/runtime/register.mjs +0 -2
- package/dist/esm/runtime/router.mjs +27 -11
- package/dist/esm/runtime/state.mjs +1 -1
- package/dist/esm-node/cli/index.mjs +9 -0
- package/dist/esm-node/runtime/lifecycle.mjs +1 -1
- package/dist/esm-node/runtime/prefetchLink.mjs +1 -6
- package/dist/esm-node/runtime/register.mjs +0 -2
- package/dist/esm-node/runtime/router.mjs +27 -11
- package/dist/esm-node/runtime/state.mjs +1 -1
- package/dist/types/runtime/lifecycle.d.ts +9 -2
- package/dist/types/runtime/register.d.ts +1 -9
- package/dist/types/runtime/router.d.ts +2 -13
- package/dist/types/runtime/state.d.ts +1 -1
- package/dist/types/runtime/types.d.ts +2 -1
- package/package.json +12 -10
- package/rstest.config.mts +0 -21
- package/src/cli/index.ts +10 -0
- package/src/runtime/lifecycle.ts +14 -3
- package/src/runtime/plugin.node.tsx +1 -1
- package/src/runtime/prefetchLink.tsx +1 -23
- package/src/runtime/register.ts +1 -12
- package/src/runtime/router.ts +25 -40
- package/src/runtime/state.ts +1 -1
- package/src/runtime/types.ts +2 -2
- package/tests/realm-isolation.test.ts +7 -17
- package/tests/router/cli.test.ts +105 -8
- package/tests/router/dataMutation.test.tsx +17 -121
- package/tests/router/flightSerialization.roundtrip.test.tsx +9 -6
- package/tests/router/generateRouteArtifacts.test.ts +69 -29
- package/tests/router/hooks.test.ts +1 -18
- package/tests/router/loaderBridge.test.ts +0 -77
- package/tests/router/prefetchLink.test.tsx +28 -16
- package/tests/router/prefetchLinkPreload.test.tsx +21 -28
- package/tests/router/routeHooks.test.ts +41 -105
- package/tests/router/routeTree.test.ts +0 -212
- package/tests/router/rsc.test.tsx +118 -136
- package/tests/router/rscDecoderSecurity.test.ts +24 -53
- package/tests/router/tanstackTypes.test.ts +24 -364
- package/tests/router/fastDefaults.test.ts +0 -32
- package/tests/router/fixtures/legacyRuntimeRouter.ts +0 -10
- package/tests/router/hydrationBoundary.test.tsx +0 -23
- package/tests/router/packageSurface.test.ts +0 -24
- package/tests/router/preloadRedirect.test.ts +0 -64
- package/tests/router/register.test.ts +0 -8
- package/tests/router/routerCompatibility.test.ts +0 -239
- 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
|
});
|
|
@@ -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('
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
325
|
-
|
|
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('
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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(
|
|
317
|
+
__setTanstackRscPayloadDecoderForTests(async stream =>
|
|
318
|
+
JSON.parse(await new Response(stream).text()),
|
|
319
|
+
);
|
|
384
320
|
|
|
385
|
-
const
|
|
386
|
-
const [rootData, productData] = await Promise.all([
|
|
321
|
+
const [getData, postData] = await Promise.all([
|
|
387
322
|
loadTanstackRscRouteData({
|
|
388
|
-
loadClientData: async () => ({ fallback: '
|
|
389
|
-
request,
|
|
390
|
-
routeId: '
|
|
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: '
|
|
394
|
-
request,
|
|
395
|
-
routeId: '/
|
|
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(
|
|
400
|
-
expect(
|
|
401
|
-
expect(fetchMock).toHaveBeenCalledTimes(
|
|
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 }));
|