@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.
@@ -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 = baseRequest
356
- ? new Request(baseRequest, { signal })
357
- : createModernRequest(href, signal);
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 = baseRequest
472
- ? new Request(baseRequest, { signal })
473
- : createModernRequest(href, signal);
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.component;
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?.component;
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
  });