@bleedingdev/modern-js-plugin-tanstack 3.2.0-ultramodern.98 → 3.4.0-ultramodern.0
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 +80 -34
- package/dist/cjs/cli/routeSplitting.js +13 -41
- package/dist/cjs/cli/tanstackTypes.js +114 -200
- package/dist/cjs/cli.js +12 -8
- package/dist/cjs/runtime/basepathRewrite.js +12 -8
- package/dist/cjs/runtime/dataMutation.js +9 -5
- package/dist/cjs/runtime/hooks.js +20 -19
- package/dist/cjs/runtime/hydrationBoundary.js +12 -7
- package/dist/cjs/runtime/index.js +66 -54
- package/dist/cjs/runtime/lifecycle.js +21 -91
- package/dist/cjs/runtime/loaderBridge.js +173 -0
- package/dist/cjs/runtime/outlet.js +58 -0
- package/dist/cjs/runtime/plugin.js +190 -110
- package/dist/cjs/runtime/plugin.node.js +25 -40
- package/dist/cjs/runtime/plugin.worker.js +9 -5
- package/dist/cjs/runtime/pluginCore.js +55 -0
- package/dist/cjs/runtime/prefetchLink.js +10 -6
- package/dist/cjs/runtime/register.js +56 -0
- package/dist/cjs/runtime/routeTree.js +30 -212
- package/dist/cjs/runtime/router.js +41 -0
- package/dist/cjs/runtime/rsc/ClientSlot.js +9 -5
- package/dist/cjs/runtime/rsc/CompositeComponent.js +9 -5
- package/dist/cjs/runtime/rsc/ReplayableStream.js +14 -9
- package/dist/cjs/runtime/rsc/RscNodeRenderer.js +9 -5
- package/dist/cjs/runtime/rsc/SlotContext.js +9 -5
- package/dist/cjs/runtime/rsc/client.js +9 -5
- package/dist/cjs/runtime/rsc/createRscProxy.js +9 -5
- package/dist/cjs/runtime/rsc/index.js +9 -5
- package/dist/cjs/runtime/rsc/payloadRouter.js +44 -6
- package/dist/cjs/runtime/rsc/server.js +9 -5
- package/dist/cjs/runtime/rsc/slotUsageSanitizer.js +9 -5
- package/dist/cjs/runtime/rsc/symbols.js +20 -15
- package/dist/cjs/runtime/state.js +45 -0
- package/dist/cjs/runtime/types.js +13 -9
- package/dist/cjs/runtime/utils.js +9 -11
- package/dist/cjs/runtime.js +9 -5
- package/dist/esm/cli/index.mjs +73 -28
- package/dist/esm/cli/routeSplitting.mjs +1 -30
- package/dist/esm/cli/tanstackTypes.mjs +100 -190
- package/dist/esm/runtime/hooks.mjs +1 -8
- package/dist/esm/runtime/hydrationBoundary.mjs +3 -2
- package/dist/esm/runtime/index.mjs +4 -2
- package/dist/esm/runtime/lifecycle.mjs +1 -82
- package/dist/esm/runtime/loaderBridge.mjs +114 -0
- package/dist/esm/runtime/outlet.mjs +17 -0
- package/dist/esm/runtime/plugin.mjs +183 -107
- package/dist/esm/runtime/plugin.node.mjs +19 -38
- package/dist/esm/runtime/pluginCore.mjs +14 -0
- package/dist/esm/runtime/prefetchLink.mjs +1 -1
- package/dist/esm/runtime/register.mjs +18 -0
- package/dist/esm/runtime/routeTree.mjs +15 -198
- package/dist/esm/runtime/router.mjs +2 -0
- package/dist/esm/runtime/rsc/payloadRouter.mjs +35 -1
- package/dist/esm/runtime/state.mjs +7 -0
- package/dist/esm/runtime/utils.mjs +0 -6
- package/dist/esm-node/cli/index.mjs +73 -28
- package/dist/esm-node/cli/routeSplitting.mjs +1 -30
- package/dist/esm-node/cli/tanstackTypes.mjs +100 -190
- package/dist/esm-node/runtime/hooks.mjs +1 -8
- package/dist/esm-node/runtime/hydrationBoundary.mjs +3 -2
- package/dist/esm-node/runtime/index.mjs +4 -2
- package/dist/esm-node/runtime/lifecycle.mjs +1 -82
- package/dist/esm-node/runtime/loaderBridge.mjs +115 -0
- package/dist/esm-node/runtime/outlet.mjs +18 -0
- package/dist/esm-node/runtime/plugin.mjs +183 -107
- package/dist/esm-node/runtime/plugin.node.mjs +19 -38
- package/dist/esm-node/runtime/pluginCore.mjs +15 -0
- package/dist/esm-node/runtime/prefetchLink.mjs +1 -1
- package/dist/esm-node/runtime/register.mjs +19 -0
- package/dist/esm-node/runtime/routeTree.mjs +15 -198
- package/dist/esm-node/runtime/router.mjs +3 -0
- package/dist/esm-node/runtime/rsc/payloadRouter.mjs +35 -1
- package/dist/esm-node/runtime/state.mjs +8 -0
- package/dist/esm-node/runtime/utils.mjs +0 -6
- package/dist/types/cli/index.d.ts +11 -2
- package/dist/types/cli/routeSplitting.d.ts +6 -15
- package/dist/types/cli/tanstackTypes.d.ts +21 -1
- package/dist/types/runtime/hooks.d.ts +8 -33
- package/dist/types/runtime/index.d.ts +6 -3
- package/dist/types/runtime/lifecycle.d.ts +7 -22
- package/dist/types/runtime/loaderBridge.d.ts +48 -0
- package/dist/types/runtime/outlet.d.ts +2 -0
- package/dist/types/runtime/plugin.d.ts +1 -14
- package/dist/types/runtime/plugin.node.d.ts +1 -14
- package/dist/types/runtime/pluginCore.d.ts +21 -0
- package/dist/types/runtime/register.d.ts +9 -0
- package/dist/types/runtime/routeTree.d.ts +0 -2
- package/dist/types/runtime/router.d.ts +14 -0
- package/dist/types/runtime/state.d.ts +16 -0
- package/dist/types/runtime/types.d.ts +7 -53
- package/package.json +40 -38
- package/rstest.config.mts +6 -0
- package/src/cli/index.ts +146 -24
- package/src/cli/routeSplitting.ts +6 -44
- package/src/cli/tanstackTypes.ts +203 -208
- package/src/runtime/hooks.ts +10 -27
- package/src/runtime/hydrationBoundary.tsx +5 -5
- package/src/runtime/index.tsx +12 -14
- package/src/runtime/lifecycle.ts +16 -151
- package/src/runtime/loaderBridge.ts +257 -0
- package/src/runtime/outlet.tsx +48 -0
- package/src/runtime/plugin.node.tsx +28 -83
- package/src/runtime/plugin.tsx +350 -202
- package/src/runtime/pluginCore.ts +48 -0
- package/src/runtime/prefetchLink.tsx +1 -1
- package/src/runtime/register.ts +58 -0
- package/src/runtime/routeTree.ts +41 -365
- package/src/runtime/router.ts +15 -0
- package/src/runtime/rsc/payloadRouter.ts +45 -2
- package/src/runtime/ssr-shim.d.ts +1 -3
- package/src/runtime/state.ts +29 -0
- package/src/runtime/types.ts +20 -67
- package/src/runtime/utils.tsx +3 -7
- package/tests/router/cli.test.ts +373 -31
- package/tests/router/hooks.test.ts +26 -0
- package/tests/router/loaderBridge.test.ts +211 -0
- package/tests/router/packageSurface.test.ts +24 -0
- package/tests/router/prefetchLink.test.tsx +43 -7
- package/tests/router/register.test.ts +46 -0
- package/tests/router/routeTree.test.ts +176 -139
- package/tests/router/rsc.test.tsx +70 -0
- package/tests/router/tanstackTypes.test.ts +395 -7
- package/dist/cjs/runtime/DefaultNotFound.js +0 -47
- package/dist/esm/runtime/DefaultNotFound.mjs +0 -13
- package/dist/esm-node/runtime/DefaultNotFound.mjs +0 -14
- package/dist/types/runtime/DefaultNotFound.d.ts +0 -2
- 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('
|
|
602
|
-
const
|
|
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
|
-
|
|
698
|
-
|
|
738
|
+
path: '/',
|
|
739
|
+
Component: () => null,
|
|
699
740
|
children: [
|
|
700
741
|
{
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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 =
|
|
712
|
-
const router = await loadRouteTree(routeTree, '/
|
|
713
|
-
const
|
|
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(
|
|
716
|
-
expect(
|
|
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
|
|
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
|
-
|
|
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
|
|
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('
|
|
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: '
|
|
773
|
-
path: '
|
|
774
|
-
|
|
775
|
-
|
|
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
|
|
786
|
-
const
|
|
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(
|
|
796
|
-
expect(
|
|
797
|
-
|
|
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' }),
|