@bleedingdev/modern-js-plugin-tanstack 3.2.0-ultramodern.120 → 3.2.0-ultramodern.122

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 (94) hide show
  1. package/dist/cjs/cli/index.js +47 -27
  2. package/dist/cjs/cli/routeSplitting.js +0 -32
  3. package/dist/cjs/cli/tanstackTypes.js +34 -199
  4. package/dist/cjs/runtime/hooks.js +11 -14
  5. package/dist/cjs/runtime/index.js +107 -319
  6. package/dist/cjs/runtime/lifecycle.js +12 -86
  7. package/dist/cjs/runtime/loaderBridge.js +173 -0
  8. package/dist/cjs/runtime/plugin.js +6 -30
  9. package/dist/cjs/runtime/plugin.node.js +7 -29
  10. package/dist/cjs/runtime/pluginCore.js +55 -0
  11. package/dist/cjs/runtime/register.js +56 -0
  12. package/dist/cjs/runtime/routeTree.js +10 -207
  13. package/dist/cjs/runtime/{DefaultNotFound.js → router.js} +5 -15
  14. package/dist/cjs/runtime/rsc/payloadRouter.js +35 -1
  15. package/dist/cjs/runtime/state.js +45 -0
  16. package/dist/cjs/runtime/utils.js +0 -5
  17. package/dist/esm/cli/index.mjs +52 -26
  18. package/dist/esm/cli/routeSplitting.mjs +1 -30
  19. package/dist/esm/cli/tanstackTypes.mjs +32 -194
  20. package/dist/esm/runtime/hooks.mjs +1 -8
  21. package/dist/esm/runtime/index.mjs +4 -2
  22. package/dist/esm/runtime/lifecycle.mjs +1 -82
  23. package/dist/esm/runtime/loaderBridge.mjs +114 -0
  24. package/dist/esm/runtime/plugin.mjs +8 -32
  25. package/dist/esm/runtime/plugin.node.mjs +10 -32
  26. package/dist/esm/runtime/pluginCore.mjs +14 -0
  27. package/dist/esm/runtime/register.mjs +18 -0
  28. package/dist/esm/runtime/routeTree.mjs +4 -198
  29. package/dist/esm/runtime/router.mjs +2 -0
  30. package/dist/esm/runtime/rsc/payloadRouter.mjs +35 -1
  31. package/dist/esm/runtime/state.mjs +7 -0
  32. package/dist/esm/runtime/utils.mjs +0 -5
  33. package/dist/esm-node/cli/index.mjs +52 -26
  34. package/dist/esm-node/cli/routeSplitting.mjs +1 -30
  35. package/dist/esm-node/cli/tanstackTypes.mjs +32 -194
  36. package/dist/esm-node/runtime/hooks.mjs +1 -8
  37. package/dist/esm-node/runtime/index.mjs +4 -2
  38. package/dist/esm-node/runtime/lifecycle.mjs +1 -82
  39. package/dist/esm-node/runtime/loaderBridge.mjs +115 -0
  40. package/dist/esm-node/runtime/plugin.mjs +8 -32
  41. package/dist/esm-node/runtime/plugin.node.mjs +10 -32
  42. package/dist/esm-node/runtime/pluginCore.mjs +15 -0
  43. package/dist/esm-node/runtime/register.mjs +19 -0
  44. package/dist/esm-node/runtime/routeTree.mjs +4 -198
  45. package/dist/esm-node/runtime/router.mjs +3 -0
  46. package/dist/esm-node/runtime/rsc/payloadRouter.mjs +35 -1
  47. package/dist/esm-node/runtime/state.mjs +8 -0
  48. package/dist/esm-node/runtime/utils.mjs +0 -5
  49. package/dist/types/cli/index.d.ts +9 -2
  50. package/dist/types/cli/routeSplitting.d.ts +6 -15
  51. package/dist/types/cli/tanstackTypes.d.ts +13 -2
  52. package/dist/types/runtime/hooks.d.ts +8 -18
  53. package/dist/types/runtime/index.d.ts +6 -4
  54. package/dist/types/runtime/lifecycle.d.ts +7 -22
  55. package/dist/types/runtime/loaderBridge.d.ts +48 -0
  56. package/dist/types/runtime/plugin.d.ts +1 -14
  57. package/dist/types/runtime/plugin.node.d.ts +1 -14
  58. package/dist/types/runtime/pluginCore.d.ts +21 -0
  59. package/dist/types/runtime/register.d.ts +9 -0
  60. package/dist/types/runtime/routeTree.d.ts +0 -2
  61. package/dist/types/runtime/router.d.ts +14 -0
  62. package/dist/types/runtime/state.d.ts +16 -0
  63. package/dist/types/runtime/types.d.ts +7 -53
  64. package/package.json +31 -29
  65. package/rstest.config.mts +6 -0
  66. package/src/cli/index.ts +111 -29
  67. package/src/cli/routeSplitting.ts +6 -44
  68. package/src/cli/tanstackTypes.ts +78 -214
  69. package/src/runtime/hooks.ts +10 -27
  70. package/src/runtime/index.tsx +12 -107
  71. package/src/runtime/lifecycle.ts +16 -151
  72. package/src/runtime/loaderBridge.ts +257 -0
  73. package/src/runtime/plugin.node.tsx +14 -77
  74. package/src/runtime/plugin.tsx +12 -72
  75. package/src/runtime/pluginCore.ts +48 -0
  76. package/src/runtime/register.ts +58 -0
  77. package/src/runtime/routeTree.ts +8 -370
  78. package/src/runtime/router.ts +15 -0
  79. package/src/runtime/rsc/payloadRouter.ts +45 -2
  80. package/src/runtime/state.ts +29 -0
  81. package/src/runtime/types.ts +20 -67
  82. package/src/runtime/utils.tsx +3 -6
  83. package/tests/router/cli.test.ts +297 -31
  84. package/tests/router/hooks.test.ts +26 -0
  85. package/tests/router/loaderBridge.test.ts +211 -0
  86. package/tests/router/packageSurface.test.ts +24 -0
  87. package/tests/router/register.test.ts +46 -0
  88. package/tests/router/routeTree.test.ts +65 -180
  89. package/tests/router/rsc.test.tsx +70 -0
  90. package/tests/router/tanstackTypes.test.ts +164 -6
  91. package/dist/esm/runtime/DefaultNotFound.mjs +0 -13
  92. package/dist/esm-node/runtime/DefaultNotFound.mjs +0 -14
  93. package/dist/types/runtime/DefaultNotFound.d.ts +0 -2
  94. package/src/runtime/DefaultNotFound.tsx +0 -15
@@ -1,5 +1,6 @@
1
- // @effect-diagnostics asyncFunction:off globalFetch:off strictBooleanExpressions:off
1
+ // @effect-diagnostics asyncFunction:off globalFetch:off processEnv:off strictBooleanExpressions:off
2
2
  import type { PayloadRoute, ServerPayload } from '@modern-js/runtime/context';
3
+ import { isRouteErrorResponse } from '@modern-js/runtime-utils/router';
3
4
  import { notFound, redirect } from '@tanstack/react-router';
4
5
 
5
6
  type PayloadDecoder = (stream: ReadableStream<Uint8Array>) => Promise<unknown>;
@@ -103,6 +104,48 @@ function toPayloadRoute(match: RouterMatchLike): PayloadRoute | undefined {
103
104
  };
104
105
  }
105
106
 
107
+ function shouldRedactServerError(status = 500) {
108
+ return (
109
+ status >= 500 &&
110
+ process.env.NODE_ENV !== 'development' &&
111
+ process.env.NODE_ENV !== 'test'
112
+ );
113
+ }
114
+
115
+ function serializePayloadError(error: unknown): unknown {
116
+ if (isRouteErrorResponse(error)) {
117
+ if (shouldRedactServerError(error.status)) {
118
+ return {
119
+ status: error.status,
120
+ statusText: 'Internal Server Error',
121
+ data: 'Unexpected Server Error',
122
+ __type: 'RouteErrorResponse',
123
+ };
124
+ }
125
+
126
+ return { ...error, __type: 'RouteErrorResponse' };
127
+ }
128
+
129
+ if (error instanceof Error) {
130
+ if (shouldRedactServerError()) {
131
+ return {
132
+ message: 'Unexpected Server Error',
133
+ stack: undefined,
134
+ __type: 'Error',
135
+ };
136
+ }
137
+
138
+ return {
139
+ message: error.message,
140
+ stack: error.stack,
141
+ __type: 'Error',
142
+ ...(error.name !== 'Error' ? { __subType: error.name } : {}),
143
+ };
144
+ }
145
+
146
+ return error;
147
+ }
148
+
106
149
  export function createTanstackRscServerPayload(
107
150
  router: TanstackPayloadRouterLike,
108
151
  options: {
@@ -133,7 +176,7 @@ export function createTanstackRscServerPayload(
133
176
  }
134
177
 
135
178
  if (typeof match.error !== 'undefined') {
136
- errors[payloadRoute.id] = match.error;
179
+ errors[payloadRoute.id] = serializePayloadError(match.error);
137
180
  }
138
181
  }
139
182
 
@@ -0,0 +1,29 @@
1
+ import { getRouterRuntimeState } from '@modern-js/runtime/context';
2
+ import type { AnyRouter } from '@tanstack/react-router';
3
+ import type { InternalRouterRuntimeState } from './types';
4
+
5
+ /**
6
+ * Router runtime state as published by the TanStack router provider into the
7
+ * runtime-context extension slot.
8
+ */
9
+ export interface TanstackRouterState
10
+ extends Omit<InternalRouterRuntimeState, 'framework' | 'instance'> {
11
+ framework: 'tanstack';
12
+ instance?: AnyRouter;
13
+ }
14
+
15
+ /**
16
+ * Typed accessor for the TanStack router state stored on a Modern.js runtime
17
+ * context. Returns `undefined` when the active router provider is not
18
+ * TanStack (e.g. react-router) or no router has been created yet.
19
+ */
20
+ export function getTanstackRouterState(
21
+ context: object,
22
+ ): TanstackRouterState | undefined {
23
+ const state = getRouterRuntimeState(context);
24
+ if (state === undefined || state.framework !== 'tanstack') {
25
+ return undefined;
26
+ }
27
+
28
+ return state as TanstackRouterState;
29
+ }
@@ -1,25 +1,37 @@
1
- import type { RequestContext } from '@modern-js/runtime-utils/node';
1
+ import type {
2
+ RouterFramework,
3
+ // The router runtime state types are owned by @modern-js/runtime; they are
4
+ // imported through the `/context` seam instead of being copied here so
5
+ // upstream fixes propagate to this package automatically.
6
+ } from '@modern-js/runtime/context';
2
7
  import type { RouteObject } from '@modern-js/runtime-utils/router';
3
8
  import type { NestedRoute, PageRoute } from '@modern-js/types';
4
9
  import type React from 'react';
5
10
 
6
- export type BuiltInRouterFramework = 'react-router' | 'tanstack';
7
- export type RouterFramework = BuiltInRouterFramework | (string & {});
8
-
11
+ export type {
12
+ BuiltInRouterFramework,
13
+ InternalRouterRuntimeState,
14
+ InternalRouterServerSnapshot,
15
+ RouterFramework,
16
+ RouterRouteMatchSnapshot,
17
+ RouterServerPrepareResult,
18
+ } from '@modern-js/runtime/context';
19
+
20
+ /**
21
+ * TanStack-specific router config. Unlike the react-router provider config,
22
+ * this intentionally has no `oldVersion`/`future` fields — those are
23
+ * react-router-only knobs with no meaning here.
24
+ */
9
25
  export type RouterConfig = {
10
26
  framework?: RouterFramework;
11
27
  routesConfig: {
12
28
  globalApp?: React.ComponentType<any>;
13
29
  routes?: (NestedRoute | PageRoute)[];
14
30
  };
15
- oldVersion?: boolean;
16
31
  serverBase?: string[];
17
32
  supportHtml5History?: boolean;
18
33
  basename?: string;
19
34
  createRoutes?: () => RouteObject[];
20
- future?: Partial<{
21
- v7_startTransition: boolean;
22
- }>;
23
35
  defaultStructuralSharing?: boolean;
24
36
  unstable_reloadOnURLMismatch?: boolean;
25
37
  };
@@ -35,62 +47,3 @@ export const getModernTanstackRouterFastDefaults = (
35
47
  config.defaultStructuralSharing ??
36
48
  modernTanstackRouterFastDefaults.defaultStructuralSharing,
37
49
  });
38
-
39
- export interface RouterRouteMatchSnapshot {
40
- routeId: string;
41
- assetRouteId?: string;
42
- pathname?: string;
43
- params?: Record<string, string>;
44
- }
45
-
46
- export interface InternalRouterServerSnapshot {
47
- framework?: RouterFramework;
48
- basename?: string;
49
- statusCode?: number;
50
- errors?: Record<string, unknown>;
51
- routerData?: {
52
- loaderData?: Record<string, unknown>;
53
- errors?: Record<string, unknown>;
54
- };
55
- hydrationScript?: string;
56
- hydrationScripts?: string[];
57
- matchedRouteIds?: string[];
58
- matches?: RouterRouteMatchSnapshot[];
59
- }
60
-
61
- export interface InternalRouterRuntimeState {
62
- framework: RouterFramework;
63
- basename?: string;
64
- instance?: unknown;
65
- hydrationScript?: string;
66
- hydrationScripts?: string[];
67
- matchedRouteIds?: string[];
68
- matches?: RouterRouteMatchSnapshot[];
69
- serverSnapshot?: InternalRouterServerSnapshot;
70
- cleanup?: () => void | Promise<void>;
71
- }
72
-
73
- export interface RouterServerPrepareResult {
74
- state: InternalRouterRuntimeState;
75
- snapshot?: InternalRouterServerSnapshot;
76
- redirect?: Response;
77
- cleanup?: () => void | Promise<void>;
78
- }
79
-
80
- interface DataFunctionArgs<D = any> {
81
- request: Request;
82
- params: Record<string, string>;
83
- context?: D;
84
- }
85
-
86
- export type LoaderFunctionArgs<
87
- P extends Record<string, unknown> = Record<string, unknown>,
88
- > = DataFunctionArgs<RequestContext<P>>;
89
-
90
- type DataFunctionValue = Response | NonNullable<unknown> | null;
91
-
92
- export type LoaderFunction = <
93
- P extends Record<string, unknown> = Record<string, unknown>,
94
- >(
95
- args: LoaderFunctionArgs<P>,
96
- ) => Promise<DataFunctionValue> | DataFunctionValue;
@@ -2,7 +2,6 @@
2
2
  import type { RouteObject } from '@modern-js/runtime-utils/router';
3
3
  import type { NestedRoute, PageRoute, SSRMode } from '@modern-js/types';
4
4
  import React from 'react';
5
- import { DefaultNotFound } from './DefaultNotFound';
6
5
 
7
6
  type RouterConfig = {
8
7
  routesConfig: {
@@ -106,11 +105,9 @@ export function getRouteObjects(
106
105
  }
107
106
  }
108
107
 
109
- routeObjects.push({
110
- path: '*',
111
- element: <DefaultNotFound />,
112
- });
113
-
108
+ // No synthetic `{ path: '*' }` 404 route here: TanStack Router handles
109
+ // not-found matches through the root route's `notFoundComponent`
110
+ // (see routeTree.ts), so the react-router style catch-all is unnecessary.
114
111
  return routeObjects;
115
112
  }
116
113
 
@@ -6,7 +6,6 @@ import type { Entrypoint } from '@modern-js/types';
6
6
  import { fs, NESTED_ROUTE_SPEC_FILE } from '@modern-js/utils';
7
7
  import {
8
8
  createTanstackRsbuildRouteSplittingProfile,
9
- isTanstackStartRouteModuleSource,
10
9
  tanstackRouterPlugin,
11
10
  writeTanstackRegisterFile,
12
11
  writeTanstackRouterTypesForEntries,
@@ -19,16 +18,29 @@ const runtimeCliMocks = {
19
18
 
20
19
  rstest.mock('@modern-js/runtime/cli', () => {
21
20
  const routesDirMetaKey = '__modernRoutesDir';
21
+ // The codegen helpers are pure — forward to the real implementations.
22
+ const actualCli = rstest.requireActual('@modern-js/runtime/cli') as {
23
+ getPathWithoutExt: (filename: string) => string;
24
+ makeLegalIdentifier: (value: string) => string;
25
+ };
22
26
 
23
27
  return {
24
28
  __esModule: true,
29
+ getPathWithoutExt: actualCli.getPathWithoutExt,
30
+ makeLegalIdentifier: actualCli.makeLegalIdentifier,
25
31
  getEntrypointRoutesDir: (entrypoint: any) =>
26
32
  entrypoint[routesDirMetaKey] ||
27
33
  (entrypoint.nestedRoutesEntry
28
34
  ? path.basename(entrypoint.nestedRoutesEntry)
29
35
  : null),
30
- handleFileChange: runtimeCliMocks.handleFileChange,
31
- handleGeneratorEntryCode: runtimeCliMocks.handleGeneratorEntryCode,
36
+ getEntrypointRoutesOwner: (entrypoint: any) =>
37
+ entrypoint.__modernRoutesOwner || null,
38
+ // Forward through arrows: the mock factory is hoisted above the
39
+ // `runtimeCliMocks` initializer, so it must not dereference it eagerly.
40
+ handleFileChange: (...args: unknown[]) =>
41
+ runtimeCliMocks.handleFileChange(...args),
42
+ handleGeneratorEntryCode: (...args: unknown[]) =>
43
+ runtimeCliMocks.handleGeneratorEntryCode(...args),
32
44
  handleModifyEntrypoints: async (
33
45
  entrypoints: Entrypoint[],
34
46
  routesDir = 'routes',
@@ -307,6 +319,278 @@ describe('tanstack router cli plugin', () => {
307
319
  });
308
320
  });
309
321
 
322
+ test('injects the framework-resolving router wrapper for non-file-route entrypoints', async () => {
323
+ const taps: Record<string, any> = {};
324
+ const api = {
325
+ getAppContext: () => ({
326
+ srcDirectory: '/tmp/app/src',
327
+ metaName: 'modern-js',
328
+ serverRoutes: [{ entryName: 'custom', urlPath: '/' }],
329
+ }),
330
+ _internalRuntimePlugins: (tap: any) => {
331
+ taps.internalRuntimePlugins = tap;
332
+ },
333
+ checkEntryPoint: () => {},
334
+ config: () => {},
335
+ modifyEntrypoints: () => {},
336
+ generateEntryCode: () => {},
337
+ onFileChanged: () => {},
338
+ modifyFileSystemRoutes: () => {},
339
+ onBeforeGenerateRoutes: () => {},
340
+ };
341
+
342
+ tanstackRouterPlugin().setup!(api as any);
343
+
344
+ // Custom entry without a routes dir (e.g. `createRoutes` in
345
+ // modern.runtime.ts): installing the plugin is the explicit opt-in, no
346
+ // source sniffing — the wrapper plus the provider registration is
347
+ // injected through the package's own runtime/router module.
348
+ const customEntrypoint = {
349
+ entryName: 'custom',
350
+ isAutoMount: true,
351
+ } as Entrypoint;
352
+ expect(
353
+ taps.internalRuntimePlugins({ entrypoint: customEntrypoint, plugins: [] })
354
+ .plugins,
355
+ ).toEqual([
356
+ {
357
+ name: 'router',
358
+ path: '@modern-js/plugin-tanstack/runtime/router',
359
+ config: { serverBase: ['/'] },
360
+ },
361
+ ]);
362
+
363
+ // If the built-in router CLI already installed the internal router for
364
+ // this custom entry (explicit `runtime.router` config), only the module
365
+ // path is redirected so the TanStack provider registration is
366
+ // value-imported with it.
367
+ const existingRouterPlugin = {
368
+ name: 'router',
369
+ path: '@modern-js/runtime/router/internal',
370
+ config: { serverBase: ['/'] },
371
+ };
372
+ const { plugins } = taps.internalRuntimePlugins({
373
+ entrypoint: customEntrypoint,
374
+ plugins: [existingRouterPlugin],
375
+ });
376
+ expect(plugins).toHaveLength(1);
377
+ expect(plugins[0]).toEqual({
378
+ name: 'router',
379
+ path: '@modern-js/plugin-tanstack/runtime/router',
380
+ config: { serverBase: ['/'] },
381
+ });
382
+ });
383
+
384
+ test('leaves built-in and foreign-owned route entrypoints to their own router', () => {
385
+ const taps: Record<string, any> = {};
386
+ const api = {
387
+ getAppContext: () => ({
388
+ srcDirectory: '/tmp/app/src',
389
+ metaName: 'modern-js',
390
+ serverRoutes: [{ entryName: 'home', urlPath: '/' }],
391
+ }),
392
+ _internalRuntimePlugins: (tap: any) => {
393
+ taps.internalRuntimePlugins = tap;
394
+ },
395
+ checkEntryPoint: () => {},
396
+ config: () => {},
397
+ modifyEntrypoints: () => {},
398
+ generateEntryCode: () => {},
399
+ onFileChanged: () => {},
400
+ modifyFileSystemRoutes: () => {},
401
+ onBeforeGenerateRoutes: () => {},
402
+ };
403
+
404
+ tanstackRouterPlugin({ routesDir: 'ts-routes' }).setup!(api as any);
405
+
406
+ // A classic react-router file-route entry (src/<entry>/routes) living
407
+ // next to the TanStack entries: its internal router plugin must be left
408
+ // untouched — redirecting it through the TanStack wrapper would pull
409
+ // @tanstack/react-router into a pure react-router bundle.
410
+ const builtInEntrypoint = {
411
+ entryName: 'home',
412
+ isAutoMount: true,
413
+ nestedRoutesEntry: '/tmp/app/src/home/routes',
414
+ __modernRoutesDir: 'routes',
415
+ } as Entrypoint;
416
+ const { plugins: builtInPlugins } = taps.internalRuntimePlugins({
417
+ entrypoint: builtInEntrypoint,
418
+ plugins: [
419
+ {
420
+ name: 'router',
421
+ path: '@modern-js/runtime/router/internal',
422
+ config: { serverBase: ['/'] },
423
+ },
424
+ ],
425
+ });
426
+ expect(builtInPlugins).toEqual([
427
+ {
428
+ name: 'router',
429
+ path: '@modern-js/runtime/router/internal',
430
+ config: { serverBase: ['/'] },
431
+ },
432
+ ]);
433
+
434
+ // An entry tagged by another routes-owner plugin: nothing is pushed.
435
+ // The replaced sniffing path in @modern-js/runtime excluded
436
+ // plugin-owned entrypoints for the same reason — pushing a second
437
+ // `router` plugin can install two routers for one entry.
438
+ const foreignEntrypoint = {
439
+ entryName: 'home',
440
+ isAutoMount: true,
441
+ nestedRoutesEntry: '/tmp/app/src/home/acme-routes',
442
+ __modernRoutesDir: 'acme-routes',
443
+ __modernRoutesOwner: '@acme/plugin-file-router',
444
+ } as Entrypoint;
445
+ expect(
446
+ taps.internalRuntimePlugins({
447
+ entrypoint: foreignEntrypoint,
448
+ plugins: [],
449
+ }).plugins,
450
+ ).toEqual([]);
451
+
452
+ // The built-in pages/ convention is foreign too.
453
+ const pagesEntrypoint = {
454
+ entryName: 'home',
455
+ isAutoMount: true,
456
+ pageRoutesEntry: '/tmp/app/src/home/pages',
457
+ } as Entrypoint;
458
+ expect(
459
+ taps.internalRuntimePlugins({ entrypoint: pagesEntrypoint, plugins: [] })
460
+ .plugins,
461
+ ).toEqual([]);
462
+ });
463
+
464
+ test('source.include covers the package dist and TanStack runtime deps without string surgery', () => {
465
+ const taps: Record<string, any> = {};
466
+ const api = {
467
+ getAppContext: () => ({
468
+ srcDirectory: '/tmp/app/src',
469
+ serverRoutes: [],
470
+ }),
471
+ _internalRuntimePlugins: () => {},
472
+ checkEntryPoint: () => {},
473
+ config: (tap: any) => {
474
+ taps.config = tap;
475
+ },
476
+ modifyEntrypoints: () => {},
477
+ generateEntryCode: () => {},
478
+ onFileChanged: () => {},
479
+ modifyFileSystemRoutes: () => {},
480
+ onBeforeGenerateRoutes: () => {},
481
+ };
482
+
483
+ tanstackRouterPlugin().setup!(api as any);
484
+
485
+ const include = taps.config().source.include as Array<RegExp | string>;
486
+ const regexes = include.filter(
487
+ (entry): entry is RegExp => entry instanceof RegExp,
488
+ );
489
+ for (const dep of ['react-router', 'router-core', 'react-store']) {
490
+ const sample = `/repo/node_modules/@tanstack/${dep}/dist/esm/index.js`;
491
+ expect(regexes.some(regex => regex.test(sample))).toBe(true);
492
+ }
493
+
494
+ const stringEntries = include.filter(
495
+ (entry): entry is string => typeof entry === 'string',
496
+ );
497
+ expect(stringEntries).toHaveLength(1);
498
+ // The include must point at the package root (two levels above the cli
499
+ // build dir) so dist/esm, dist/esm-node and dist/cjs are all covered —
500
+ // the old `.replace('cjs', 'esm')` never matched the bundled dist/esm
501
+ // runtime when the CLI was loaded through the ESM condition.
502
+ expect(stringEntries[0]).toBe(path.resolve(__dirname, '..', '..'));
503
+ expect(stringEntries[0]).not.toContain('esm');
504
+ });
505
+
506
+ test('emits the plugin-i18n augmentation only when plugin-i18n is registered', async () => {
507
+ const langRoutes = [
508
+ {
509
+ type: 'nested',
510
+ id: 'layout',
511
+ isRoot: true,
512
+ children: [
513
+ {
514
+ type: 'nested',
515
+ id: '(lang)/layout',
516
+ path: ':lang',
517
+ children: [
518
+ {
519
+ type: 'nested',
520
+ id: '(lang)/about/page',
521
+ path: 'about',
522
+ },
523
+ ],
524
+ },
525
+ ],
526
+ },
527
+ ];
528
+
529
+ const runGenerate = async (registeredPlugins: Array<{ name: string }>) => {
530
+ const dir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-cli-'));
531
+ const srcDirectory = path.join(dir, 'src');
532
+ const entrypoint = {
533
+ entryName: 'main',
534
+ isAutoMount: true,
535
+ isMainEntry: true,
536
+ nestedRoutesEntry: path.join(srcDirectory, 'routes'),
537
+ __modernRoutesDir: 'routes',
538
+ } as Entrypoint;
539
+ runtimeCliMocks.handleGeneratorEntryCode.mockResolvedValueOnce({
540
+ main: langRoutes,
541
+ });
542
+
543
+ const taps: Record<string, any> = {};
544
+ const api = {
545
+ getAppContext: () => ({
546
+ srcDirectory,
547
+ internalSrcAlias: '@/_',
548
+ entrypoints: [entrypoint],
549
+ plugins: registeredPlugins,
550
+ }),
551
+ _internalRuntimePlugins: () => {},
552
+ checkEntryPoint: () => {},
553
+ config: () => {},
554
+ modifyEntrypoints: () => {},
555
+ generateEntryCode: (tap: any) => {
556
+ taps.generateEntryCode = tap;
557
+ },
558
+ onFileChanged: () => {},
559
+ modifyFileSystemRoutes: () => {},
560
+ onBeforeGenerateRoutes: () => {},
561
+ };
562
+
563
+ tanstackRouterPlugin().setup!(api as any);
564
+ await taps.generateEntryCode({ entrypoints: [entrypoint] });
565
+
566
+ const register = await readFile(
567
+ path.join(srcDirectory, 'modern-tanstack', 'register.gen.d.ts'),
568
+ 'utf-8',
569
+ );
570
+ await rm(dir, { recursive: true, force: true });
571
+ return register;
572
+ };
573
+
574
+ // A hand-rolled `/:lang/` app WITHOUT plugin-i18n must not get the
575
+ // augmentation — it would reference an unresolvable module (TS2664).
576
+ const withoutI18n = await runGenerate([{ name: '@modern-js/app-tools' }]);
577
+ expect(withoutI18n).not.toContain('plugin-i18n');
578
+ expect(withoutI18n).not.toContain('UltramodernCanonicalRoutes');
579
+ expect(withoutI18n).toContain(
580
+ "declare module '@modern-js/plugin-tanstack/runtime'",
581
+ );
582
+
583
+ // With plugin-i18n registered the canonical route map is emitted.
584
+ const withI18n = await runGenerate([
585
+ { name: '@modern-js/app-tools' },
586
+ { name: '@modern-js/plugin-i18n' },
587
+ ]);
588
+ expect(withI18n).toContain(
589
+ "declare module '@modern-js/plugin-i18n/runtime'",
590
+ );
591
+ expect(withI18n).toContain("'/about': Record<string, never>;");
592
+ });
593
+
310
594
  test('generates plugin-owned TanStack route files through core route generation', async () => {
311
595
  tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-cli-'));
312
596
  const srcDirectory = path.join(tempDir, 'src');
@@ -392,9 +676,6 @@ describe('tanstack router cli plugin', () => {
392
676
  [entrypoint],
393
677
  {
394
678
  entrypointsKey: '@modern-js/plugin-tanstack',
395
- generateCodeOptions: {
396
- enableTanstackTypes: false,
397
- },
398
679
  },
399
680
  );
400
681
 
@@ -497,38 +778,23 @@ describe('tanstack router cli plugin', () => {
497
778
  });
498
779
  });
499
780
 
500
- test('documents why TanStack Start Rspack splitter is not registered for Modern routes', () => {
501
- const profile = createTanstackRsbuildRouteSplittingProfile({});
502
-
503
- expect(profile).toMatchObject({
781
+ test('route splitting profile carries only the rsbuild config production consumes', () => {
782
+ expect(createTanstackRsbuildRouteSplittingProfile({})).toEqual({
504
783
  defaultConfig: {
505
784
  output: {
506
785
  splitRouteChunks: true,
507
786
  },
508
787
  },
509
- modernRouteChunks: {
510
- enabled: true,
511
- owner: 'modern',
512
- },
513
- builderChunkSplit: {
514
- owner: 'modern-rsbuild',
515
- preserved: true,
516
- },
517
- tanstackStartRspackSplitter: {
518
- compatible: false,
519
- clientDeleteNodes: ['ssr', 'server', 'headers'],
520
- },
521
788
  });
522
789
  expect(
523
- isTanstackStartRouteModuleSource(
524
- "export const Route = createFileRoute('/dashboard')({ component })",
525
- ),
526
- ).toBe(true);
527
- expect(
528
- isTanstackStartRouteModuleSource(
529
- 'export const route = createRoute({ getParentRoute, path })',
530
- ),
531
- ).toBe(false);
790
+ createTanstackRsbuildRouteSplittingProfile({ routeCodeSplitting: false }),
791
+ ).toEqual({
792
+ defaultConfig: {
793
+ output: {
794
+ splitRouteChunks: false,
795
+ },
796
+ },
797
+ });
532
798
  });
533
799
 
534
800
  test('preserves user-selected route and builder chunk splitting modes', () => {
@@ -0,0 +1,26 @@
1
+ import {
2
+ modifyRoutes as canonicalModifyRoutes,
3
+ routerProviderRegistryHooks,
4
+ } from '@modern-js/runtime/context';
5
+ import * as tanstackHooks from '../../src/runtime/hooks';
6
+ import { tanstackRouterPlugin as browserPlugin } from '../../src/runtime/plugin';
7
+ import { tanstackRouterPlugin as nodePlugin } from '../../src/runtime/plugin.node';
8
+
9
+ describe('tanstack router hooks single declaration source', () => {
10
+ test('re-exports the canonical hook instances owned by @modern-js/runtime', () => {
11
+ // Identity matters: separate instances would split the hook registry
12
+ // between the built-in router wrapper and this provider.
13
+ expect(tanstackHooks.modifyRoutes).toBe(canonicalModifyRoutes);
14
+ expect(tanstackHooks.routerProviderRegistryHooks).toBe(
15
+ routerProviderRegistryHooks,
16
+ );
17
+ expect(tanstackHooks.modifyRoutes).toBe(
18
+ routerProviderRegistryHooks.modifyRoutes,
19
+ );
20
+ });
21
+
22
+ test('both runtime plugins register the canonical hook registry object', () => {
23
+ expect(browserPlugin().registryHooks).toBe(routerProviderRegistryHooks);
24
+ expect(nodePlugin().registryHooks).toBe(routerProviderRegistryHooks);
25
+ });
26
+ });