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