@bleedingdev/modern-js-plugin-tanstack 3.2.0-ultramodern.9 → 3.2.0-ultramodern.90

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.
Files changed (48) hide show
  1. package/dist/cjs/cli/index.js +12 -0
  2. package/dist/cjs/cli/routeSplitting.js +83 -0
  3. package/dist/cjs/cli/tanstackTypes.js +146 -58
  4. package/dist/cjs/runtime/index.js +38 -6
  5. package/dist/cjs/runtime/plugin.js +6 -5
  6. package/dist/cjs/runtime/plugin.node.js +27 -10
  7. package/dist/cjs/runtime/plugin.worker.js +49 -0
  8. package/dist/cjs/runtime/routeTree.js +55 -4
  9. package/dist/cjs/runtime/types.js +27 -1
  10. package/dist/esm/cli/index.mjs +4 -1
  11. package/dist/esm/cli/routeSplitting.mjs +43 -0
  12. package/dist/esm/cli/tanstackTypes.mjs +146 -58
  13. package/dist/esm/runtime/index.mjs +2 -1
  14. package/dist/esm/runtime/plugin.mjs +10 -9
  15. package/dist/esm/runtime/plugin.node.mjs +28 -11
  16. package/dist/esm/runtime/plugin.worker.mjs +1 -0
  17. package/dist/esm/runtime/routeTree.mjs +55 -4
  18. package/dist/esm/runtime/types.mjs +7 -0
  19. package/dist/esm-node/cli/index.mjs +4 -1
  20. package/dist/esm-node/cli/routeSplitting.mjs +44 -0
  21. package/dist/esm-node/cli/tanstackTypes.mjs +146 -58
  22. package/dist/esm-node/runtime/index.mjs +2 -1
  23. package/dist/esm-node/runtime/plugin.mjs +10 -9
  24. package/dist/esm-node/runtime/plugin.node.mjs +28 -11
  25. package/dist/esm-node/runtime/plugin.worker.mjs +2 -0
  26. package/dist/esm-node/runtime/routeTree.mjs +55 -4
  27. package/dist/esm-node/runtime/types.mjs +7 -0
  28. package/dist/types/cli/index.d.ts +4 -0
  29. package/dist/types/cli/routeSplitting.d.ts +29 -0
  30. package/dist/types/runtime/index.d.ts +3 -1
  31. package/dist/types/runtime/plugin.d.ts +1 -1
  32. package/dist/types/runtime/plugin.node.d.ts +1 -1
  33. package/dist/types/runtime/plugin.worker.d.ts +1 -0
  34. package/dist/types/runtime/types.d.ts +7 -0
  35. package/package.json +14 -14
  36. package/src/cli/index.ts +17 -0
  37. package/src/cli/routeSplitting.ts +81 -0
  38. package/src/cli/tanstackTypes.ts +216 -67
  39. package/src/runtime/index.tsx +13 -1
  40. package/src/runtime/plugin.node.tsx +54 -7
  41. package/src/runtime/plugin.tsx +8 -5
  42. package/src/runtime/plugin.worker.tsx +4 -0
  43. package/src/runtime/routeTree.ts +125 -8
  44. package/src/runtime/types.ts +13 -0
  45. package/tests/router/cli.test.ts +239 -0
  46. package/tests/router/fastDefaults.test.ts +25 -0
  47. package/tests/router/routeTree.test.ts +193 -1
  48. package/tests/router/tanstackTypes.test.ts +184 -0
@@ -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,
@@ -75,8 +76,10 @@ type ModernRouteObject = RouteObject & {
75
76
  isClientComponent?: boolean;
76
77
  lazyImport?: () => unknown;
77
78
  loader?: ModernLoader;
79
+ loaderDeps?: unknown;
78
80
  pendingComponent?: unknown;
79
81
  shouldRevalidate?: ModernShouldRevalidate;
82
+ validateSearch?: unknown;
80
83
  };
81
84
 
82
85
  type ModernGeneratedRoute = (NestedRoute | PageRoute) & {
@@ -101,10 +104,12 @@ type ModernGeneratedRoute = (NestedRoute | PageRoute) & {
101
104
  isRoot?: boolean;
102
105
  lazyImport?: () => unknown;
103
106
  loader?: ModernLoader;
107
+ loaderDeps?: unknown;
104
108
  loading?: unknown;
105
109
  pendingComponent?: unknown;
106
110
  path?: string;
107
111
  shouldRevalidate?: ModernShouldRevalidate;
112
+ validateSearch?: unknown;
108
113
  };
109
114
 
110
115
  type MutableTanstackRoute = AnyRoute & {
@@ -118,6 +123,15 @@ type ModernDeferredDataLike = {
118
123
  __modern_deferred?: unknown;
119
124
  data?: unknown;
120
125
  };
126
+ type ModernRouteModule = {
127
+ Component?: unknown;
128
+ default?: unknown;
129
+ };
130
+ type PreloadableComponent = {
131
+ (props: Record<string, unknown>): ReturnType<typeof createElement>;
132
+ load?: () => Promise<unknown>;
133
+ preload?: () => Promise<unknown>;
134
+ };
121
135
  type RouteTreeOptions = {
122
136
  rscPayloadRouter?: boolean;
123
137
  };
@@ -219,6 +233,76 @@ function normalizeModernLoaderResponse(result: unknown): unknown {
219
233
  return normalizeModernLoaderResult(result);
220
234
  }
221
235
 
236
+ function pickRouteModuleComponent(
237
+ routeModule: unknown,
238
+ seen: Set<unknown> = new Set(),
239
+ ): ElementType<Record<string, unknown>> | undefined {
240
+ if (
241
+ typeof routeModule === 'function' ||
242
+ (routeModule &&
243
+ typeof routeModule === 'object' &&
244
+ '$$typeof' in routeModule)
245
+ ) {
246
+ return routeModule as ElementType<Record<string, unknown>>;
247
+ }
248
+
249
+ if (!routeModule || typeof routeModule !== 'object') {
250
+ return undefined;
251
+ }
252
+ if (seen.has(routeModule)) {
253
+ return undefined;
254
+ }
255
+ seen.add(routeModule);
256
+
257
+ const module = routeModule as ModernRouteModule;
258
+ for (const candidate of [module.default, module.Component]) {
259
+ const component = pickRouteModuleComponent(candidate, seen);
260
+ if (component) {
261
+ return component;
262
+ }
263
+ }
264
+
265
+ return undefined;
266
+ }
267
+
268
+ function createServerLazyImportComponent(
269
+ lazyImport: () => unknown,
270
+ fallbackComponent?: unknown,
271
+ ): PreloadableComponent | unknown {
272
+ if (typeof document !== 'undefined') {
273
+ return fallbackComponent;
274
+ }
275
+
276
+ let resolvedComponent: ElementType<Record<string, unknown>> | undefined;
277
+ let pendingLoad: Promise<unknown> | undefined;
278
+
279
+ const load = async () => {
280
+ if (resolvedComponent) {
281
+ return resolvedComponent;
282
+ }
283
+
284
+ const routeModule = await lazyImport();
285
+ const component = pickRouteModuleComponent(routeModule);
286
+ if (component) {
287
+ resolvedComponent = component;
288
+ }
289
+ return resolvedComponent;
290
+ };
291
+
292
+ const Component: PreloadableComponent = props => {
293
+ if (resolvedComponent) {
294
+ return createElement(resolvedComponent, props);
295
+ }
296
+
297
+ pendingLoad ||= load();
298
+ throw pendingLoad;
299
+ };
300
+ Component.load = load;
301
+ Component.preload = load;
302
+
303
+ return Component;
304
+ }
305
+
222
306
  function isAbsoluteUrl(value: string) {
223
307
  try {
224
308
  void new URL(value);
@@ -352,9 +436,10 @@ function wrapModernLoader(
352
436
  ctx?.location?.url?.href ||
353
437
  '';
354
438
 
355
- const request = baseRequest
356
- ? new Request(baseRequest, { signal })
357
- : createModernRequest(href, signal);
439
+ const request =
440
+ baseRequest !== undefined
441
+ ? new Request(baseRequest, { signal })
442
+ : createModernRequest(href, signal);
358
443
  const params = mapParamsForModernLoader({
359
444
  modernRoute,
360
445
  params: ctx.params || {},
@@ -468,9 +553,10 @@ function wrapRouteObjectLoader(
468
553
  ctx?.location?.url?.href ||
469
554
  '';
470
555
 
471
- const request = baseRequest
472
- ? new Request(baseRequest, { signal })
473
- : createModernRequest(href, signal);
556
+ const request =
557
+ baseRequest !== undefined
558
+ ? new Request(baseRequest, { signal })
559
+ : createModernRequest(href, signal);
474
560
 
475
561
  const params = mapParamsForRouteObjectLoader({
476
562
  route,
@@ -519,6 +605,18 @@ function wrapRouteObjectLoader(
519
605
 
520
606
  function toRouteComponent(routeObject: RouteObject): unknown {
521
607
  const route = routeObject as ModernRouteObject;
608
+ const lazyImport =
609
+ typeof route.lazyImport === 'function' ? route.lazyImport : undefined;
610
+ const fallbackComponent = route.Component
611
+ ? route.Component
612
+ : route.element
613
+ ? () => route.element
614
+ : undefined;
615
+
616
+ if (lazyImport && fallbackComponent) {
617
+ return createServerLazyImportComponent(lazyImport, fallbackComponent);
618
+ }
619
+
522
620
  if (route.Component) {
523
621
  return route.Component;
524
622
  }
@@ -529,6 +627,15 @@ function toRouteComponent(routeObject: RouteObject): unknown {
529
627
  return undefined;
530
628
  }
531
629
 
630
+ function toModernRouteComponent(route: ModernGeneratedRoute): unknown {
631
+ const component = route.component || undefined;
632
+ if (typeof route.lazyImport === 'function' && component) {
633
+ return createServerLazyImportComponent(route.lazyImport, component);
634
+ }
635
+
636
+ return component;
637
+ }
638
+
532
639
  function toErrorComponent(routeObject: RouteObject): unknown {
533
640
  const route = routeObject as ModernRouteObject;
534
641
  if (route.ErrorBoundary) {
@@ -632,6 +739,8 @@ function createRouteFromRouteObject(opts: {
632
739
  component: toRouteComponent(routeObject),
633
740
  pendingComponent: toPendingComponent(routeObject),
634
741
  errorComponent: toErrorComponent(routeObject),
742
+ validateSearch: modernRouteObject.validateSearch,
743
+ loaderDeps: modernRouteObject.loaderDeps,
635
744
  wrapInSuspense: true,
636
745
  staticData: createRouteStaticData({
637
746
  modernRouteId: routeObject.id,
@@ -702,7 +811,7 @@ function createRouteFromModernRoute(opts: {
702
811
 
703
812
  const pendingComponent = route.loading || route.pendingComponent;
704
813
  const errorComponent = route.error || route.errorComponent;
705
- const component = route.component;
814
+ const component = toModernRouteComponent(route);
706
815
  const modernLoader = route.loader;
707
816
  const modernAction = route.action;
708
817
  const modernShouldRevalidate = route.shouldRevalidate;
@@ -724,6 +833,8 @@ function createRouteFromModernRoute(opts: {
724
833
  component: component || undefined,
725
834
  pendingComponent: pendingComponent || undefined,
726
835
  errorComponent: errorComponent || undefined,
836
+ validateSearch: route.validateSearch,
837
+ loaderDeps: route.loaderDeps,
727
838
  wrapInSuspense: true,
728
839
  staticData: createRouteStaticData({
729
840
  modernRouteId: modernId,
@@ -788,7 +899,9 @@ export function createRouteTreeFromModernRoutes(
788
899
  (r as ModernGeneratedRoute).isRoot,
789
900
  ) as ModernGeneratedRoute | undefined;
790
901
 
791
- const rootComponent = rootModern?.component;
902
+ const rootComponent = rootModern
903
+ ? toModernRouteComponent(rootModern)
904
+ : undefined;
792
905
  const pendingComponent = rootModern?.loading;
793
906
  const errorComponent = rootModern?.error;
794
907
  const rootLoader = rootModern?.loader;
@@ -805,6 +918,8 @@ export function createRouteTreeFromModernRoutes(
805
918
  component: rootComponent || undefined,
806
919
  pendingComponent: pendingComponent || undefined,
807
920
  errorComponent: errorComponent || undefined,
921
+ validateSearch: rootModern?.validateSearch,
922
+ loaderDeps: rootModern?.loaderDeps,
808
923
  wrapInSuspense: true,
809
924
  notFoundComponent: DefaultNotFound,
810
925
  staticData: createRouteStaticData({
@@ -876,6 +991,8 @@ export function createRouteTreeFromRouteObjects(
876
991
  ? toPendingComponent(rootLikeRoute)
877
992
  : undefined,
878
993
  errorComponent: rootLikeRoute ? toErrorComponent(rootLikeRoute) : undefined,
994
+ validateSearch: rootLikeRoute?.validateSearch,
995
+ loaderDeps: rootLikeRoute?.loaderDeps,
879
996
  wrapInSuspense: true,
880
997
  notFoundComponent: DefaultNotFound,
881
998
  staticData: createRouteStaticData({
@@ -20,9 +20,22 @@ export type RouterConfig = {
20
20
  future?: Partial<{
21
21
  v7_startTransition: boolean;
22
22
  }>;
23
+ defaultStructuralSharing?: boolean;
23
24
  unstable_reloadOnURLMismatch?: boolean;
24
25
  };
25
26
 
27
+ export const modernTanstackRouterFastDefaults = {
28
+ defaultStructuralSharing: true,
29
+ } as const;
30
+
31
+ export const getModernTanstackRouterFastDefaults = (
32
+ config: Partial<Pick<RouterConfig, 'defaultStructuralSharing'>> = {},
33
+ ) => ({
34
+ defaultStructuralSharing:
35
+ config.defaultStructuralSharing ??
36
+ modernTanstackRouterFastDefaults.defaultStructuralSharing,
37
+ });
38
+
26
39
  export interface RouterRouteMatchSnapshot {
27
40
  routeId: string;
28
41
  assetRouteId?: string;
@@ -1,9 +1,12 @@
1
1
  import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises';
2
2
  import { tmpdir } from 'node:os';
3
3
  import path from 'node:path';
4
+ import { mergeConfig } from '@modern-js/plugin/cli';
4
5
  import type { Entrypoint } from '@modern-js/types';
5
6
  import { fs, NESTED_ROUTE_SPEC_FILE } from '@modern-js/utils';
6
7
  import {
8
+ createTanstackRsbuildRouteSplittingProfile,
9
+ isTanstackStartRouteModuleSource,
7
10
  tanstackRouterPlugin,
8
11
  writeTanstackRegisterFile,
9
12
  writeTanstackRouterTypesForEntries,
@@ -193,6 +196,12 @@ describe('tanstack router cli plugin', () => {
193
196
  },
194
197
  ]);
195
198
 
199
+ expect(taps.config()).toMatchObject({
200
+ output: {
201
+ splitRouteChunks: true,
202
+ },
203
+ });
204
+
196
205
  const specPath = path.join(distDirectory, NESTED_ROUTE_SPEC_FILE);
197
206
  await fs.outputJSON(specPath, {
198
207
  existing: [{ id: 'keep-me' }],
@@ -383,4 +392,234 @@ describe('tanstack router cli plugin', () => {
383
392
  }),
384
393
  );
385
394
  });
395
+
396
+ test('can opt out of Modern-owned route code splitting', async () => {
397
+ const taps: Record<string, any> = {};
398
+ const api = {
399
+ getAppContext: () => ({
400
+ srcDirectory: '/tmp/app/src',
401
+ serverRoutes: [],
402
+ }),
403
+ _internalRuntimePlugins: () => {},
404
+ checkEntryPoint: () => {},
405
+ config: (tap: any) => {
406
+ taps.config = tap;
407
+ },
408
+ modifyEntrypoints: () => {},
409
+ generateEntryCode: () => {},
410
+ onFileChanged: () => {},
411
+ modifyFileSystemRoutes: () => {},
412
+ onBeforeGenerateRoutes: () => {},
413
+ };
414
+
415
+ tanstackRouterPlugin({ routeCodeSplitting: false }).setup!(api as any);
416
+
417
+ expect(taps.config()).toMatchObject({
418
+ output: {
419
+ splitRouteChunks: false,
420
+ },
421
+ });
422
+ });
423
+
424
+ test('documents why TanStack Start Rspack splitter is not registered for Modern routes', () => {
425
+ const profile = createTanstackRsbuildRouteSplittingProfile({});
426
+
427
+ expect(profile).toMatchObject({
428
+ defaultConfig: {
429
+ output: {
430
+ splitRouteChunks: true,
431
+ },
432
+ },
433
+ modernRouteChunks: {
434
+ enabled: true,
435
+ owner: 'modern',
436
+ },
437
+ builderChunkSplit: {
438
+ owner: 'modern-rsbuild',
439
+ preserved: true,
440
+ },
441
+ tanstackStartRspackSplitter: {
442
+ compatible: false,
443
+ clientDeleteNodes: ['ssr', 'server', 'headers'],
444
+ },
445
+ });
446
+ expect(
447
+ isTanstackStartRouteModuleSource(
448
+ "export const Route = createFileRoute('/dashboard')({ component })",
449
+ ),
450
+ ).toBe(true);
451
+ expect(
452
+ isTanstackStartRouteModuleSource(
453
+ 'export const route = createRoute({ getParentRoute, path })',
454
+ ),
455
+ ).toBe(false);
456
+ });
457
+
458
+ test('preserves user-selected route and builder chunk splitting modes', () => {
459
+ const pluginDefaults = createTanstackRsbuildRouteSplittingProfile(
460
+ {},
461
+ ).defaultConfig;
462
+ const chunkSplits = [
463
+ { strategy: 'split-by-module' },
464
+ { strategy: 'split-by-experience' },
465
+ { strategy: 'all-in-one' },
466
+ { strategy: 'single-vendor' },
467
+ { strategy: 'split-by-size', minSize: 10_000, maxSize: 60_000 },
468
+ {
469
+ strategy: 'custom',
470
+ splitChunks: {
471
+ chunks: 'all',
472
+ cacheGroups: {
473
+ tractors: {
474
+ name: 'tractors',
475
+ test: /tractors/u,
476
+ },
477
+ },
478
+ },
479
+ },
480
+ ];
481
+
482
+ for (const chunkSplit of chunkSplits) {
483
+ expect(
484
+ mergeConfig([
485
+ pluginDefaults,
486
+ {
487
+ output: {
488
+ splitRouteChunks: false,
489
+ },
490
+ performance: {
491
+ chunkSplit,
492
+ },
493
+ splitChunks: false,
494
+ },
495
+ ]),
496
+ ).toMatchObject({
497
+ output: {
498
+ splitRouteChunks: false,
499
+ },
500
+ performance: {
501
+ chunkSplit,
502
+ },
503
+ splitChunks: false,
504
+ });
505
+ }
506
+
507
+ const pageSplitWithManualAsyncChunks = mergeConfig([
508
+ pluginDefaults,
509
+ {
510
+ performance: {
511
+ chunkSplit: {
512
+ strategy: 'custom',
513
+ splitChunks: {
514
+ chunks: 'async',
515
+ },
516
+ },
517
+ },
518
+ },
519
+ ]);
520
+
521
+ expect(pageSplitWithManualAsyncChunks).toMatchObject({
522
+ output: {
523
+ splitRouteChunks: true,
524
+ },
525
+ performance: {
526
+ chunkSplit: {
527
+ strategy: 'custom',
528
+ splitChunks: {
529
+ chunks: 'async',
530
+ },
531
+ },
532
+ },
533
+ });
534
+ });
535
+
536
+ test('keeps custom cache group details intact', () => {
537
+ const pluginDefaults = createTanstackRsbuildRouteSplittingProfile(
538
+ {},
539
+ ).defaultConfig;
540
+
541
+ const mergedConfig = mergeConfig([
542
+ pluginDefaults,
543
+ {
544
+ performance: {
545
+ chunkSplit: {
546
+ strategy: 'custom',
547
+ splitChunks: {
548
+ chunks: 'all',
549
+ cacheGroups: {
550
+ tractors: {
551
+ name: 'tractors',
552
+ test: /tractors/u,
553
+ },
554
+ },
555
+ },
556
+ },
557
+ },
558
+ },
559
+ ]);
560
+
561
+ expect(
562
+ (
563
+ mergedConfig as {
564
+ performance?: {
565
+ chunkSplit?: {
566
+ splitChunks?: {
567
+ cacheGroups?: {
568
+ tractors?: {
569
+ test?: RegExp;
570
+ };
571
+ };
572
+ };
573
+ };
574
+ };
575
+ }
576
+ ).performance?.chunkSplit?.splitChunks?.cacheGroups?.tractors?.test,
577
+ ).toEqual(/tractors/u);
578
+ expect(mergedConfig).toMatchObject({
579
+ output: {
580
+ splitRouteChunks: true,
581
+ },
582
+ performance: {
583
+ chunkSplit: {
584
+ strategy: 'custom',
585
+ splitChunks: {
586
+ chunks: 'all',
587
+ cacheGroups: {
588
+ tractors: {
589
+ name: 'tractors',
590
+ },
591
+ },
592
+ },
593
+ },
594
+ },
595
+ });
596
+ });
597
+
598
+ test('plugin opt-out can still combine with manual builder chunking', () => {
599
+ const pluginDefaults = createTanstackRsbuildRouteSplittingProfile({
600
+ routeCodeSplitting: false,
601
+ }).defaultConfig;
602
+
603
+ expect(
604
+ mergeConfig([
605
+ pluginDefaults,
606
+ {
607
+ performance: {
608
+ chunkSplit: {
609
+ strategy: 'single-vendor',
610
+ },
611
+ },
612
+ },
613
+ ]),
614
+ ).toMatchObject({
615
+ output: {
616
+ splitRouteChunks: false,
617
+ },
618
+ performance: {
619
+ chunkSplit: {
620
+ strategy: 'single-vendor',
621
+ },
622
+ },
623
+ });
624
+ });
386
625
  });
@@ -0,0 +1,25 @@
1
+ import {
2
+ getModernTanstackRouterFastDefaults,
3
+ modernTanstackRouterFastDefaults,
4
+ } from '../../src/runtime/types';
5
+
6
+ describe('tanstack router fast defaults', () => {
7
+ test('enables structural sharing by default', () => {
8
+ expect(modernTanstackRouterFastDefaults).toEqual({
9
+ defaultStructuralSharing: true,
10
+ });
11
+ expect(getModernTanstackRouterFastDefaults()).toEqual({
12
+ defaultStructuralSharing: true,
13
+ });
14
+ });
15
+
16
+ test('allows explicit structural sharing override', () => {
17
+ expect(
18
+ getModernTanstackRouterFastDefaults({
19
+ defaultStructuralSharing: false,
20
+ }),
21
+ ).toEqual({
22
+ defaultStructuralSharing: false,
23
+ });
24
+ });
25
+ });