@bleedingdev/modern-js-plugin-tanstack 3.2.0-ultramodern.98 → 3.4.0-ultramodern.0

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 (127) hide show
  1. package/dist/cjs/cli/index.js +80 -34
  2. package/dist/cjs/cli/routeSplitting.js +13 -41
  3. package/dist/cjs/cli/tanstackTypes.js +114 -200
  4. package/dist/cjs/cli.js +12 -8
  5. package/dist/cjs/runtime/basepathRewrite.js +12 -8
  6. package/dist/cjs/runtime/dataMutation.js +9 -5
  7. package/dist/cjs/runtime/hooks.js +20 -19
  8. package/dist/cjs/runtime/hydrationBoundary.js +12 -7
  9. package/dist/cjs/runtime/index.js +66 -54
  10. package/dist/cjs/runtime/lifecycle.js +21 -91
  11. package/dist/cjs/runtime/loaderBridge.js +173 -0
  12. package/dist/cjs/runtime/outlet.js +58 -0
  13. package/dist/cjs/runtime/plugin.js +190 -110
  14. package/dist/cjs/runtime/plugin.node.js +25 -40
  15. package/dist/cjs/runtime/plugin.worker.js +9 -5
  16. package/dist/cjs/runtime/pluginCore.js +55 -0
  17. package/dist/cjs/runtime/prefetchLink.js +10 -6
  18. package/dist/cjs/runtime/register.js +56 -0
  19. package/dist/cjs/runtime/routeTree.js +30 -212
  20. package/dist/cjs/runtime/router.js +41 -0
  21. package/dist/cjs/runtime/rsc/ClientSlot.js +9 -5
  22. package/dist/cjs/runtime/rsc/CompositeComponent.js +9 -5
  23. package/dist/cjs/runtime/rsc/ReplayableStream.js +14 -9
  24. package/dist/cjs/runtime/rsc/RscNodeRenderer.js +9 -5
  25. package/dist/cjs/runtime/rsc/SlotContext.js +9 -5
  26. package/dist/cjs/runtime/rsc/client.js +9 -5
  27. package/dist/cjs/runtime/rsc/createRscProxy.js +9 -5
  28. package/dist/cjs/runtime/rsc/index.js +9 -5
  29. package/dist/cjs/runtime/rsc/payloadRouter.js +44 -6
  30. package/dist/cjs/runtime/rsc/server.js +9 -5
  31. package/dist/cjs/runtime/rsc/slotUsageSanitizer.js +9 -5
  32. package/dist/cjs/runtime/rsc/symbols.js +20 -15
  33. package/dist/cjs/runtime/state.js +45 -0
  34. package/dist/cjs/runtime/types.js +13 -9
  35. package/dist/cjs/runtime/utils.js +9 -11
  36. package/dist/cjs/runtime.js +9 -5
  37. package/dist/esm/cli/index.mjs +73 -28
  38. package/dist/esm/cli/routeSplitting.mjs +1 -30
  39. package/dist/esm/cli/tanstackTypes.mjs +100 -190
  40. package/dist/esm/runtime/hooks.mjs +1 -8
  41. package/dist/esm/runtime/hydrationBoundary.mjs +3 -2
  42. package/dist/esm/runtime/index.mjs +4 -2
  43. package/dist/esm/runtime/lifecycle.mjs +1 -82
  44. package/dist/esm/runtime/loaderBridge.mjs +114 -0
  45. package/dist/esm/runtime/outlet.mjs +17 -0
  46. package/dist/esm/runtime/plugin.mjs +183 -107
  47. package/dist/esm/runtime/plugin.node.mjs +19 -38
  48. package/dist/esm/runtime/pluginCore.mjs +14 -0
  49. package/dist/esm/runtime/prefetchLink.mjs +1 -1
  50. package/dist/esm/runtime/register.mjs +18 -0
  51. package/dist/esm/runtime/routeTree.mjs +15 -198
  52. package/dist/esm/runtime/router.mjs +2 -0
  53. package/dist/esm/runtime/rsc/payloadRouter.mjs +35 -1
  54. package/dist/esm/runtime/state.mjs +7 -0
  55. package/dist/esm/runtime/utils.mjs +0 -6
  56. package/dist/esm-node/cli/index.mjs +73 -28
  57. package/dist/esm-node/cli/routeSplitting.mjs +1 -30
  58. package/dist/esm-node/cli/tanstackTypes.mjs +100 -190
  59. package/dist/esm-node/runtime/hooks.mjs +1 -8
  60. package/dist/esm-node/runtime/hydrationBoundary.mjs +3 -2
  61. package/dist/esm-node/runtime/index.mjs +4 -2
  62. package/dist/esm-node/runtime/lifecycle.mjs +1 -82
  63. package/dist/esm-node/runtime/loaderBridge.mjs +115 -0
  64. package/dist/esm-node/runtime/outlet.mjs +18 -0
  65. package/dist/esm-node/runtime/plugin.mjs +183 -107
  66. package/dist/esm-node/runtime/plugin.node.mjs +19 -38
  67. package/dist/esm-node/runtime/pluginCore.mjs +15 -0
  68. package/dist/esm-node/runtime/prefetchLink.mjs +1 -1
  69. package/dist/esm-node/runtime/register.mjs +19 -0
  70. package/dist/esm-node/runtime/routeTree.mjs +15 -198
  71. package/dist/esm-node/runtime/router.mjs +3 -0
  72. package/dist/esm-node/runtime/rsc/payloadRouter.mjs +35 -1
  73. package/dist/esm-node/runtime/state.mjs +8 -0
  74. package/dist/esm-node/runtime/utils.mjs +0 -6
  75. package/dist/types/cli/index.d.ts +11 -2
  76. package/dist/types/cli/routeSplitting.d.ts +6 -15
  77. package/dist/types/cli/tanstackTypes.d.ts +21 -1
  78. package/dist/types/runtime/hooks.d.ts +8 -33
  79. package/dist/types/runtime/index.d.ts +6 -3
  80. package/dist/types/runtime/lifecycle.d.ts +7 -22
  81. package/dist/types/runtime/loaderBridge.d.ts +48 -0
  82. package/dist/types/runtime/outlet.d.ts +2 -0
  83. package/dist/types/runtime/plugin.d.ts +1 -14
  84. package/dist/types/runtime/plugin.node.d.ts +1 -14
  85. package/dist/types/runtime/pluginCore.d.ts +21 -0
  86. package/dist/types/runtime/register.d.ts +9 -0
  87. package/dist/types/runtime/routeTree.d.ts +0 -2
  88. package/dist/types/runtime/router.d.ts +14 -0
  89. package/dist/types/runtime/state.d.ts +16 -0
  90. package/dist/types/runtime/types.d.ts +7 -53
  91. package/package.json +40 -38
  92. package/rstest.config.mts +6 -0
  93. package/src/cli/index.ts +146 -24
  94. package/src/cli/routeSplitting.ts +6 -44
  95. package/src/cli/tanstackTypes.ts +203 -208
  96. package/src/runtime/hooks.ts +10 -27
  97. package/src/runtime/hydrationBoundary.tsx +5 -5
  98. package/src/runtime/index.tsx +12 -14
  99. package/src/runtime/lifecycle.ts +16 -151
  100. package/src/runtime/loaderBridge.ts +257 -0
  101. package/src/runtime/outlet.tsx +48 -0
  102. package/src/runtime/plugin.node.tsx +28 -83
  103. package/src/runtime/plugin.tsx +350 -202
  104. package/src/runtime/pluginCore.ts +48 -0
  105. package/src/runtime/prefetchLink.tsx +1 -1
  106. package/src/runtime/register.ts +58 -0
  107. package/src/runtime/routeTree.ts +41 -365
  108. package/src/runtime/router.ts +15 -0
  109. package/src/runtime/rsc/payloadRouter.ts +45 -2
  110. package/src/runtime/ssr-shim.d.ts +1 -3
  111. package/src/runtime/state.ts +29 -0
  112. package/src/runtime/types.ts +20 -67
  113. package/src/runtime/utils.tsx +3 -7
  114. package/tests/router/cli.test.ts +373 -31
  115. package/tests/router/hooks.test.ts +26 -0
  116. package/tests/router/loaderBridge.test.ts +211 -0
  117. package/tests/router/packageSurface.test.ts +24 -0
  118. package/tests/router/prefetchLink.test.tsx +43 -7
  119. package/tests/router/register.test.ts +46 -0
  120. package/tests/router/routeTree.test.ts +176 -139
  121. package/tests/router/rsc.test.tsx +70 -0
  122. package/tests/router/tanstackTypes.test.ts +395 -7
  123. package/dist/cjs/runtime/DefaultNotFound.js +0 -47
  124. package/dist/esm/runtime/DefaultNotFound.mjs +0 -13
  125. package/dist/esm-node/runtime/DefaultNotFound.mjs +0 -14
  126. package/dist/types/runtime/DefaultNotFound.d.ts +0 -2
  127. package/src/runtime/DefaultNotFound.tsx +0 -15
@@ -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: {
@@ -66,7 +65,6 @@ export function getRouteObjects(
66
65
  id: route.id,
67
66
  loader: route.loader,
68
67
  action: route.action,
69
- hasErrorBoundary: route.hasErrorBoundary,
70
68
  shouldRevalidate: route.shouldRevalidate,
71
69
  handle: {
72
70
  ...route.handle,
@@ -106,11 +104,9 @@ export function getRouteObjects(
106
104
  }
107
105
  }
108
106
 
109
- routeObjects.push({
110
- path: '*',
111
- element: <DefaultNotFound />,
112
- });
113
-
107
+ // No synthetic `{ path: '*' }` 404 route here: TanStack Router handles
108
+ // not-found matches through the root route's `notFoundComponent`
109
+ // (see routeTree.ts), so the react-router style catch-all is unnecessary.
114
110
  return routeObjects;
115
111
  }
116
112
 
@@ -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',
@@ -123,6 +135,82 @@ describe('tanstack router cli plugin', () => {
123
135
  );
124
136
  });
125
137
 
138
+ test('writes plugin-i18n module augmentation when canonicalRoutes are provided', async () => {
139
+ tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-cli-'));
140
+ const srcDirectory = path.join(tempDir, 'src');
141
+
142
+ await writeTanstackRegisterFile({
143
+ entries: ['main'],
144
+ generatedDirName: 'tanstack',
145
+ srcDirectory,
146
+ canonicalRoutes: {
147
+ '/': 'Record<string, never>',
148
+ '/products/$slug': '{ "slug": string }',
149
+ },
150
+ });
151
+
152
+ const register = await readFile(
153
+ path.join(srcDirectory, 'tanstack', 'register.gen.d.ts'),
154
+ 'utf-8',
155
+ );
156
+
157
+ expect(register).toContain(
158
+ "declare module '@modern-js/plugin-i18n/runtime'",
159
+ );
160
+ expect(register).toContain('interface UltramodernCanonicalRoutes');
161
+ expect(register).toContain("'/': Record<string, never>;");
162
+ expect(register).toContain('\'/products/$slug\': { "slug": string };');
163
+ });
164
+
165
+ test('does not emit plugin-i18n augmentation when canonicalRoutes is absent (back-compat)', async () => {
166
+ tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-cli-'));
167
+ const srcDirectory = path.join(tempDir, 'src');
168
+
169
+ await writeTanstackRegisterFile({
170
+ entries: ['main'],
171
+ generatedDirName: 'tanstack',
172
+ srcDirectory,
173
+ // No canonicalRoutes provided at all — plain TanStack app
174
+ });
175
+
176
+ const register = await readFile(
177
+ path.join(srcDirectory, 'tanstack', 'register.gen.d.ts'),
178
+ 'utf-8',
179
+ );
180
+
181
+ expect(register).not.toContain('plugin-i18n');
182
+ expect(register).not.toContain('UltramodernCanonicalRoutes');
183
+ // But the standard TanStack runtime augmentation must still be present.
184
+ expect(register).toContain(
185
+ "declare module '@modern-js/plugin-tanstack/runtime'",
186
+ );
187
+ });
188
+
189
+ test('uses a custom i18nRuntimeModule when specified', async () => {
190
+ tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-cli-'));
191
+ const srcDirectory = path.join(tempDir, 'src');
192
+
193
+ await writeTanstackRegisterFile({
194
+ entries: ['main'],
195
+ generatedDirName: 'tanstack',
196
+ srcDirectory,
197
+ canonicalRoutes: {
198
+ '/talks': 'Record<string, never>',
199
+ },
200
+ i18nRuntimeModule: '@my-org/i18n/runtime',
201
+ });
202
+
203
+ const register = await readFile(
204
+ path.join(srcDirectory, 'tanstack', 'register.gen.d.ts'),
205
+ 'utf-8',
206
+ );
207
+
208
+ expect(register).toContain("declare module '@my-org/i18n/runtime'");
209
+ expect(register).not.toContain(
210
+ "declare module '@modern-js/plugin-i18n/runtime'",
211
+ );
212
+ });
213
+
126
214
  test('claims custom routes, injects runtime plugin, and merges route specs', async () => {
127
215
  tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-cli-'));
128
216
  const srcDirectory = path.join(tempDir, 'src');
@@ -231,6 +319,278 @@ describe('tanstack router cli plugin', () => {
231
319
  });
232
320
  });
233
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
+
234
594
  test('generates plugin-owned TanStack route files through core route generation', async () => {
235
595
  tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-cli-'));
236
596
  const srcDirectory = path.join(tempDir, 'src');
@@ -316,9 +676,6 @@ describe('tanstack router cli plugin', () => {
316
676
  [entrypoint],
317
677
  {
318
678
  entrypointsKey: '@modern-js/plugin-tanstack',
319
- generateCodeOptions: {
320
- enableTanstackTypes: false,
321
- },
322
679
  },
323
680
  );
324
681
 
@@ -421,38 +778,23 @@ describe('tanstack router cli plugin', () => {
421
778
  });
422
779
  });
423
780
 
424
- test('documents why TanStack Start Rspack splitter is not registered for Modern routes', () => {
425
- const profile = createTanstackRsbuildRouteSplittingProfile({});
426
-
427
- expect(profile).toMatchObject({
781
+ test('route splitting profile carries only the rsbuild config production consumes', () => {
782
+ expect(createTanstackRsbuildRouteSplittingProfile({})).toEqual({
428
783
  defaultConfig: {
429
784
  output: {
430
785
  splitRouteChunks: true,
431
786
  },
432
787
  },
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
788
  });
446
789
  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);
790
+ createTanstackRsbuildRouteSplittingProfile({ routeCodeSplitting: false }),
791
+ ).toEqual({
792
+ defaultConfig: {
793
+ output: {
794
+ splitRouteChunks: false,
795
+ },
796
+ },
797
+ });
456
798
  });
457
799
 
458
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
+ });