@bleedingdev/modern-js-plugin-tanstack 3.2.0-ultramodern.8 → 3.2.0-ultramodern.81
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/tanstackTypes.js +101 -51
- package/dist/cjs/runtime/plugin.js +4 -5
- package/dist/cjs/runtime/plugin.node.js +25 -10
- package/dist/cjs/runtime/plugin.worker.js +49 -0
- package/dist/cjs/runtime/routeTree.js +40 -4
- package/dist/esm/cli/tanstackTypes.mjs +101 -51
- package/dist/esm/runtime/plugin.mjs +8 -9
- package/dist/esm/runtime/plugin.node.mjs +26 -11
- package/dist/esm/runtime/plugin.worker.mjs +1 -0
- package/dist/esm/runtime/routeTree.mjs +40 -4
- package/dist/esm-node/cli/tanstackTypes.mjs +101 -51
- package/dist/esm-node/runtime/plugin.mjs +8 -9
- package/dist/esm-node/runtime/plugin.node.mjs +26 -11
- package/dist/esm-node/runtime/plugin.worker.mjs +2 -0
- package/dist/esm-node/runtime/routeTree.mjs +40 -4
- package/dist/types/runtime/plugin.worker.d.ts +1 -0
- package/package.json +13 -13
- package/src/cli/tanstackTypes.ts +114 -54
- package/src/runtime/plugin.node.tsx +48 -6
- package/src/runtime/plugin.tsx +3 -4
- package/src/runtime/plugin.worker.tsx +4 -0
- package/src/runtime/routeTree.ts +109 -8
- package/tests/router/routeTree.test.ts +72 -1
- package/tests/router/tanstackTypes.test.ts +64 -0
package/src/runtime/routeTree.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
notFound,
|
|
13
13
|
redirect,
|
|
14
14
|
} from '@tanstack/react-router';
|
|
15
|
+
import { createElement, type ElementType } from 'react';
|
|
15
16
|
import { DefaultNotFound } from './DefaultNotFound';
|
|
16
17
|
import {
|
|
17
18
|
isTanstackRscPayloadNavigationEnabled,
|
|
@@ -118,6 +119,15 @@ type ModernDeferredDataLike = {
|
|
|
118
119
|
__modern_deferred?: unknown;
|
|
119
120
|
data?: unknown;
|
|
120
121
|
};
|
|
122
|
+
type ModernRouteModule = {
|
|
123
|
+
Component?: unknown;
|
|
124
|
+
default?: unknown;
|
|
125
|
+
};
|
|
126
|
+
type PreloadableComponent = {
|
|
127
|
+
(props: Record<string, unknown>): ReturnType<typeof createElement>;
|
|
128
|
+
load?: () => Promise<unknown>;
|
|
129
|
+
preload?: () => Promise<unknown>;
|
|
130
|
+
};
|
|
121
131
|
type RouteTreeOptions = {
|
|
122
132
|
rscPayloadRouter?: boolean;
|
|
123
133
|
};
|
|
@@ -219,6 +229,72 @@ function normalizeModernLoaderResponse(result: unknown): unknown {
|
|
|
219
229
|
return normalizeModernLoaderResult(result);
|
|
220
230
|
}
|
|
221
231
|
|
|
232
|
+
function pickRouteModuleComponent(
|
|
233
|
+
routeModule: unknown,
|
|
234
|
+
): ElementType<Record<string, unknown>> | undefined {
|
|
235
|
+
if (
|
|
236
|
+
typeof routeModule === 'function' ||
|
|
237
|
+
(routeModule &&
|
|
238
|
+
typeof routeModule === 'object' &&
|
|
239
|
+
'$$typeof' in routeModule)
|
|
240
|
+
) {
|
|
241
|
+
return routeModule as ElementType<Record<string, unknown>>;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!routeModule || typeof routeModule !== 'object') {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const module = routeModule as ModernRouteModule;
|
|
249
|
+
const component = module.default || module.Component;
|
|
250
|
+
if (
|
|
251
|
+
typeof component === 'function' ||
|
|
252
|
+
(component && typeof component === 'object' && '$$typeof' in component)
|
|
253
|
+
) {
|
|
254
|
+
return component as ElementType<Record<string, unknown>>;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function createServerLazyImportComponent(
|
|
261
|
+
lazyImport: () => unknown,
|
|
262
|
+
fallbackComponent?: unknown,
|
|
263
|
+
): PreloadableComponent | unknown {
|
|
264
|
+
if (typeof document !== 'undefined') {
|
|
265
|
+
return fallbackComponent;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let resolvedComponent: ElementType<Record<string, unknown>> | undefined;
|
|
269
|
+
let pendingLoad: Promise<unknown> | undefined;
|
|
270
|
+
|
|
271
|
+
const load = async () => {
|
|
272
|
+
if (resolvedComponent) {
|
|
273
|
+
return resolvedComponent;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const routeModule = await lazyImport();
|
|
277
|
+
const component = pickRouteModuleComponent(routeModule);
|
|
278
|
+
if (component) {
|
|
279
|
+
resolvedComponent = component;
|
|
280
|
+
}
|
|
281
|
+
return resolvedComponent;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const Component: PreloadableComponent = props => {
|
|
285
|
+
if (resolvedComponent) {
|
|
286
|
+
return createElement(resolvedComponent, props);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
pendingLoad ||= load();
|
|
290
|
+
throw pendingLoad;
|
|
291
|
+
};
|
|
292
|
+
Component.load = load;
|
|
293
|
+
Component.preload = load;
|
|
294
|
+
|
|
295
|
+
return Component;
|
|
296
|
+
}
|
|
297
|
+
|
|
222
298
|
function isAbsoluteUrl(value: string) {
|
|
223
299
|
try {
|
|
224
300
|
void new URL(value);
|
|
@@ -352,9 +428,10 @@ function wrapModernLoader(
|
|
|
352
428
|
ctx?.location?.url?.href ||
|
|
353
429
|
'';
|
|
354
430
|
|
|
355
|
-
const request =
|
|
356
|
-
|
|
357
|
-
|
|
431
|
+
const request =
|
|
432
|
+
baseRequest !== undefined
|
|
433
|
+
? new Request(baseRequest, { signal })
|
|
434
|
+
: createModernRequest(href, signal);
|
|
358
435
|
const params = mapParamsForModernLoader({
|
|
359
436
|
modernRoute,
|
|
360
437
|
params: ctx.params || {},
|
|
@@ -468,9 +545,10 @@ function wrapRouteObjectLoader(
|
|
|
468
545
|
ctx?.location?.url?.href ||
|
|
469
546
|
'';
|
|
470
547
|
|
|
471
|
-
const request =
|
|
472
|
-
|
|
473
|
-
|
|
548
|
+
const request =
|
|
549
|
+
baseRequest !== undefined
|
|
550
|
+
? new Request(baseRequest, { signal })
|
|
551
|
+
: createModernRequest(href, signal);
|
|
474
552
|
|
|
475
553
|
const params = mapParamsForRouteObjectLoader({
|
|
476
554
|
route,
|
|
@@ -519,6 +597,18 @@ function wrapRouteObjectLoader(
|
|
|
519
597
|
|
|
520
598
|
function toRouteComponent(routeObject: RouteObject): unknown {
|
|
521
599
|
const route = routeObject as ModernRouteObject;
|
|
600
|
+
const lazyImport =
|
|
601
|
+
typeof route.lazyImport === 'function' ? route.lazyImport : undefined;
|
|
602
|
+
const fallbackComponent = route.Component
|
|
603
|
+
? route.Component
|
|
604
|
+
: route.element
|
|
605
|
+
? () => route.element
|
|
606
|
+
: undefined;
|
|
607
|
+
|
|
608
|
+
if (lazyImport && fallbackComponent) {
|
|
609
|
+
return createServerLazyImportComponent(lazyImport, fallbackComponent);
|
|
610
|
+
}
|
|
611
|
+
|
|
522
612
|
if (route.Component) {
|
|
523
613
|
return route.Component;
|
|
524
614
|
}
|
|
@@ -529,6 +619,15 @@ function toRouteComponent(routeObject: RouteObject): unknown {
|
|
|
529
619
|
return undefined;
|
|
530
620
|
}
|
|
531
621
|
|
|
622
|
+
function toModernRouteComponent(route: ModernGeneratedRoute): unknown {
|
|
623
|
+
const component = route.component || undefined;
|
|
624
|
+
if (typeof route.lazyImport === 'function' && component) {
|
|
625
|
+
return createServerLazyImportComponent(route.lazyImport, component);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return component;
|
|
629
|
+
}
|
|
630
|
+
|
|
532
631
|
function toErrorComponent(routeObject: RouteObject): unknown {
|
|
533
632
|
const route = routeObject as ModernRouteObject;
|
|
534
633
|
if (route.ErrorBoundary) {
|
|
@@ -702,7 +801,7 @@ function createRouteFromModernRoute(opts: {
|
|
|
702
801
|
|
|
703
802
|
const pendingComponent = route.loading || route.pendingComponent;
|
|
704
803
|
const errorComponent = route.error || route.errorComponent;
|
|
705
|
-
const component = route
|
|
804
|
+
const component = toModernRouteComponent(route);
|
|
706
805
|
const modernLoader = route.loader;
|
|
707
806
|
const modernAction = route.action;
|
|
708
807
|
const modernShouldRevalidate = route.shouldRevalidate;
|
|
@@ -788,7 +887,9 @@ export function createRouteTreeFromModernRoutes(
|
|
|
788
887
|
(r as ModernGeneratedRoute).isRoot,
|
|
789
888
|
) as ModernGeneratedRoute | undefined;
|
|
790
889
|
|
|
791
|
-
const rootComponent = rootModern
|
|
890
|
+
const rootComponent = rootModern
|
|
891
|
+
? toModernRouteComponent(rootModern)
|
|
892
|
+
: undefined;
|
|
792
893
|
const pendingComponent = rootModern?.loading;
|
|
793
894
|
const errorComponent = rootModern?.error;
|
|
794
895
|
const rootLoader = rootModern?.loader;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { RouteObject } from '@modern-js/runtime-utils/router';
|
|
2
2
|
import type { NestedRoute } from '@modern-js/types';
|
|
3
3
|
import { createMemoryHistory } from '@tanstack/history';
|
|
4
|
-
import { createRouter } from '@tanstack/react-router';
|
|
4
|
+
import { createRouter, Outlet, RouterProvider } from '@tanstack/react-router';
|
|
5
|
+
import type { ComponentType } from 'react';
|
|
6
|
+
import { createElement, lazy } from 'react';
|
|
7
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
5
8
|
import {
|
|
6
9
|
createRouteTreeFromModernRoutes,
|
|
7
10
|
createRouteTreeFromRouteObjects,
|
|
@@ -23,6 +26,7 @@ type TestRouteObject = RouteObject & {
|
|
|
23
26
|
hasLoader?: boolean;
|
|
24
27
|
inValidSSRRoute?: boolean;
|
|
25
28
|
isClientComponent?: boolean;
|
|
29
|
+
lazyImport?: () => Promise<{ default: ComponentType }>;
|
|
26
30
|
};
|
|
27
31
|
|
|
28
32
|
type TestNestedRoute = NestedRoute & {
|
|
@@ -234,6 +238,73 @@ describe('tanstack route tree from RouteObject[]', () => {
|
|
|
234
238
|
expect(splatParamValue).toBe('a/b/c');
|
|
235
239
|
});
|
|
236
240
|
|
|
241
|
+
test('preloads lazy Modern route components for server rendering', async () => {
|
|
242
|
+
const LazyRouteComponent = () =>
|
|
243
|
+
createElement('main', null, 'Lazy route ready');
|
|
244
|
+
const lazyImport = rstest.fn(async () => ({
|
|
245
|
+
default: LazyRouteComponent,
|
|
246
|
+
}));
|
|
247
|
+
const routes: TestRouteObject[] = [
|
|
248
|
+
{
|
|
249
|
+
id: 'root',
|
|
250
|
+
path: '/',
|
|
251
|
+
Component: () => null,
|
|
252
|
+
children: [
|
|
253
|
+
{
|
|
254
|
+
id: 'lazy',
|
|
255
|
+
path: 'lazy',
|
|
256
|
+
Component: lazy(lazyImport),
|
|
257
|
+
lazyImport,
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
},
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
const routeTree = createRouteTreeFromRouteObjects(routes);
|
|
264
|
+
const router = await loadRouteTree(routeTree, '/lazy');
|
|
265
|
+
const lazyRoute = getLooseRoute(router, '/lazy');
|
|
266
|
+
const component = lazyRoute.options.component as ComponentType & {
|
|
267
|
+
preload?: () => Promise<unknown>;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
await component.preload?.();
|
|
271
|
+
|
|
272
|
+
expect(renderToStaticMarkup(createElement(component))).toContain(
|
|
273
|
+
'Lazy route ready',
|
|
274
|
+
);
|
|
275
|
+
expect(lazyImport).toHaveBeenCalled();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('renders preloaded lazy child routes through TanStack router SSR', async () => {
|
|
279
|
+
const LazyRouteComponent = () =>
|
|
280
|
+
createElement('main', null, 'Lazy child route ready');
|
|
281
|
+
const lazyImport = rstest.fn(async () => ({
|
|
282
|
+
default: LazyRouteComponent,
|
|
283
|
+
}));
|
|
284
|
+
const routes: TestRouteObject[] = [
|
|
285
|
+
{
|
|
286
|
+
id: 'root',
|
|
287
|
+
path: '/',
|
|
288
|
+
Component: () => createElement('section', null, createElement(Outlet)),
|
|
289
|
+
children: [
|
|
290
|
+
{
|
|
291
|
+
id: 'lazy',
|
|
292
|
+
path: 'lazy',
|
|
293
|
+
Component: lazy(lazyImport),
|
|
294
|
+
lazyImport,
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
},
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
const routeTree = createRouteTreeFromRouteObjects(routes);
|
|
301
|
+
const router = await loadRouteTree(routeTree, '/lazy');
|
|
302
|
+
|
|
303
|
+
expect(
|
|
304
|
+
renderToStaticMarkup(createElement(RouterProvider, { router } as never)),
|
|
305
|
+
).toContain('Lazy child route ready');
|
|
306
|
+
});
|
|
307
|
+
|
|
237
308
|
test('preserves route handle and maps shouldRevalidate to shouldReload', async () => {
|
|
238
309
|
const shouldRevalidate = rstest.fn(({ nextUrl }: ShouldRevalidateArgs) =>
|
|
239
310
|
nextUrl.pathname.endsWith('/456'),
|
|
@@ -55,8 +55,72 @@ describe('tanstack router type generation', () => {
|
|
|
55
55
|
);
|
|
56
56
|
expect(routerGenTs).toContain('modernRouteLoader: loader_0');
|
|
57
57
|
expect(routerGenTs).toContain('modernRouteAction: action_0');
|
|
58
|
+
expect(routerGenTs).toContain('modernRouteId?: string;');
|
|
59
|
+
expect(routerGenTs).not.toContain(
|
|
60
|
+
'return Object.keys(staticData).length > 0 ? staticData : undefined;',
|
|
61
|
+
);
|
|
58
62
|
expect(routerGenTs).toContain(
|
|
59
63
|
"} from '@modern-js/plugin-tanstack/runtime';",
|
|
60
64
|
);
|
|
61
65
|
});
|
|
66
|
+
|
|
67
|
+
test('preserves typed child trees for localized nested route aliases', async () => {
|
|
68
|
+
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
|
|
69
|
+
const srcDirectory = path.join(tempDir, 'src');
|
|
70
|
+
|
|
71
|
+
const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
|
|
72
|
+
appContext: {
|
|
73
|
+
srcDirectory,
|
|
74
|
+
internalSrcAlias: '@/_',
|
|
75
|
+
} as any,
|
|
76
|
+
entryName: 'index',
|
|
77
|
+
routes: [
|
|
78
|
+
{
|
|
79
|
+
type: 'nested',
|
|
80
|
+
id: 'layout',
|
|
81
|
+
isRoot: true,
|
|
82
|
+
children: [
|
|
83
|
+
{
|
|
84
|
+
type: 'nested',
|
|
85
|
+
id: '(lang)/layout',
|
|
86
|
+
path: ':lang',
|
|
87
|
+
children: [
|
|
88
|
+
{
|
|
89
|
+
type: 'nested',
|
|
90
|
+
id: '(lang)/products/(slug)/page',
|
|
91
|
+
path: 'products/:slug',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
type: 'nested',
|
|
95
|
+
id: '(lang)/products/(slug)/page__localised_produkty_slug',
|
|
96
|
+
path: 'produkty/:slug',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
type: 'nested',
|
|
100
|
+
id: '(lang)/optional/(slug$)/page__localised_volitelne_slug',
|
|
101
|
+
path: 'volitelne/:slug?',
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
] as any,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(routerGenTs).toContain(
|
|
111
|
+
'const route__lang__layout__base = createRoute({',
|
|
112
|
+
);
|
|
113
|
+
expect(routerGenTs).toContain(
|
|
114
|
+
'getParentRoute: () => route__lang__layout__base,',
|
|
115
|
+
);
|
|
116
|
+
expect(routerGenTs).toContain('path: "produkty/$slug",');
|
|
117
|
+
expect(routerGenTs).toContain('path: "volitelne/{-$slug}",');
|
|
118
|
+
expect(routerGenTs).toContain(
|
|
119
|
+
'const route__lang__layout = route__lang__layout__base.addChildren([route__lang__products__slug__page, route__lang__products__slug__page__localised_produkty_slug, route__lang__optional__slug$__page__localised_volitelne_slug]);',
|
|
120
|
+
);
|
|
121
|
+
expect(routerGenTs).toContain(
|
|
122
|
+
'export const routeTree = rootRoute.addChildren([route__lang__layout]);',
|
|
123
|
+
);
|
|
124
|
+
expect(routerGenTs).not.toContain('route__lang__layout.addChildren([');
|
|
125
|
+
});
|
|
62
126
|
});
|