@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.3 → 3.9.0-ultramodern.5

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 (80) hide show
  1. package/dist/cjs/cli/index.js +9 -0
  2. package/dist/cjs/cli/tanstackTypes/source.js +3 -1
  3. package/dist/cjs/runtime/basepathRewrite.js +13 -6
  4. package/dist/cjs/runtime/lifecycle.js +7 -7
  5. package/dist/cjs/runtime/plugin.js +1 -1
  6. package/dist/cjs/runtime/plugin.node.js +5 -1
  7. package/dist/cjs/runtime/prefetchLink.js +2 -7
  8. package/dist/cjs/runtime/register.js +2 -2
  9. package/dist/cjs/runtime/routeTree/construction.js +2 -0
  10. package/dist/cjs/runtime/router.js +25 -9
  11. package/dist/cjs/runtime/state.js +2 -2
  12. package/dist/cjs/runtime/utils.js +2 -0
  13. package/dist/esm/cli/index.mjs +9 -0
  14. package/dist/esm/cli/tanstackTypes/source.mjs +3 -1
  15. package/dist/esm/runtime/basepathRewrite.mjs +10 -3
  16. package/dist/esm/runtime/lifecycle.mjs +1 -1
  17. package/dist/esm/runtime/plugin.mjs +1 -1
  18. package/dist/esm/runtime/plugin.node.mjs +5 -1
  19. package/dist/esm/runtime/prefetchLink.mjs +1 -6
  20. package/dist/esm/runtime/register.mjs +1 -1
  21. package/dist/esm/runtime/routeTree/construction.mjs +2 -0
  22. package/dist/esm/runtime/router.mjs +27 -11
  23. package/dist/esm/runtime/state.mjs +1 -1
  24. package/dist/esm/runtime/utils.mjs +2 -0
  25. package/dist/esm-node/cli/index.mjs +9 -0
  26. package/dist/esm-node/cli/tanstackTypes/source.mjs +3 -1
  27. package/dist/esm-node/runtime/basepathRewrite.mjs +10 -3
  28. package/dist/esm-node/runtime/lifecycle.mjs +1 -1
  29. package/dist/esm-node/runtime/plugin.mjs +1 -1
  30. package/dist/esm-node/runtime/plugin.node.mjs +5 -1
  31. package/dist/esm-node/runtime/prefetchLink.mjs +1 -6
  32. package/dist/esm-node/runtime/register.mjs +1 -1
  33. package/dist/esm-node/runtime/routeTree/construction.mjs +2 -0
  34. package/dist/esm-node/runtime/router.mjs +27 -11
  35. package/dist/esm-node/runtime/state.mjs +1 -1
  36. package/dist/esm-node/runtime/utils.mjs +2 -0
  37. package/dist/types/runtime/basepathRewrite.d.ts +2 -1
  38. package/dist/types/runtime/lifecycle.d.ts +9 -2
  39. package/dist/types/runtime/register.d.ts +1 -1
  40. package/dist/types/runtime/router.d.ts +2 -13
  41. package/dist/types/runtime/state.d.ts +1 -1
  42. package/dist/types/runtime/types.d.ts +2 -1
  43. package/package.json +12 -9
  44. package/rstest.config.mts +1 -6
  45. package/src/cli/index.ts +10 -0
  46. package/src/cli/tanstackTypes/source.ts +3 -1
  47. package/src/runtime/basepathRewrite.ts +11 -2
  48. package/src/runtime/lifecycle.ts +14 -3
  49. package/src/runtime/plugin.node.tsx +9 -2
  50. package/src/runtime/plugin.tsx +5 -1
  51. package/src/runtime/prefetchLink.tsx +1 -23
  52. package/src/runtime/register.ts +1 -1
  53. package/src/runtime/routeTree/construction.ts +2 -0
  54. package/src/runtime/router.ts +25 -40
  55. package/src/runtime/state.ts +1 -1
  56. package/src/runtime/types.ts +2 -2
  57. package/src/runtime/utils.tsx +3 -1
  58. package/tests/realm-isolation.test.ts +8 -2
  59. package/tests/router/cli.test.ts +108 -9
  60. package/tests/router/dataMutation.test.tsx +17 -121
  61. package/tests/router/flightSerialization.roundtrip.test.tsx +9 -6
  62. package/tests/router/generateRouteArtifacts.test.ts +69 -29
  63. package/tests/router/hooks.test.ts +1 -18
  64. package/tests/router/loaderBridge.test.ts +0 -77
  65. package/tests/router/localisedIdentity.test.tsx +95 -0
  66. package/tests/router/prefetchLink.test.tsx +28 -16
  67. package/tests/router/prefetchLinkPreload.test.tsx +21 -28
  68. package/tests/router/register.test.ts +1 -1
  69. package/tests/router/routeHooks.test.ts +41 -105
  70. package/tests/router/routeTree.test.ts +0 -212
  71. package/tests/router/routerCompatibility.test.ts +32 -9
  72. package/tests/router/rsc.test.tsx +118 -136
  73. package/tests/router/rscDecoderSecurity.test.ts +24 -53
  74. package/tests/router/serverPlugin.test.ts +43 -1
  75. package/tests/router/tanstackTypes.test.ts +24 -364
  76. package/tests/router/fastDefaults.test.ts +0 -32
  77. package/tests/router/hydrationBoundary.test.tsx +0 -23
  78. package/tests/router/packageSurface.test.ts +0 -24
  79. package/tests/router/preloadRedirect.test.ts +0 -64
  80. package/tests/router/rscPayloadRouterMatrix.test.ts +0 -197
@@ -4,6 +4,7 @@ import { tmpdir } from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { promisify } from 'node:util';
6
6
  import { mergeConfig } from '@modern-js/plugin/cli';
7
+ import { compile } from '@modern-js/server-utils';
7
8
  import type { Entrypoint } from '@modern-js/types';
8
9
  import { fs, NESTED_ROUTE_SPEC_FILE } from '@modern-js/utils';
9
10
  import {
@@ -20,6 +21,16 @@ const strictestTsconfigPath = path.resolve(
20
21
  '../../node_modules/@tsconfig/strictest/tsconfig.json',
21
22
  );
22
23
 
24
+ function createAppContextApi<T extends object>(initial: T) {
25
+ let context: T & { serverCompileExcludedFiles?: string[] } = { ...initial };
26
+ return {
27
+ getAppContext: () => context,
28
+ updateAppContext: (update: Partial<typeof context>) => {
29
+ context = { ...context, ...update };
30
+ },
31
+ };
32
+ }
33
+
23
34
  async function typecheckGeneratedRegistration(options: {
24
35
  entries: string[];
25
36
  generatedDirName: string;
@@ -201,6 +212,92 @@ describe('tanstack router cli plugin', () => {
201
212
  }
202
213
  });
203
214
 
215
+ test.each([
216
+ { sourceDirectory: 'src', generatedDirName: undefined },
217
+ { sourceDirectory: 'client', generatedDirName: 'routing-types' },
218
+ ])('excludes its generated declaration from server roots only when installed: %s', async ({
219
+ sourceDirectory,
220
+ generatedDirName,
221
+ }) => {
222
+ tempDir = await fs.realpath(
223
+ await mkdtemp(path.join(tmpdir(), 'tanstack-server-roots-')),
224
+ );
225
+ const srcDirectory = path.join(tempDir, sourceDirectory);
226
+ const generatedDirectory = generatedDirName ?? 'modern-tanstack';
227
+ const declaration = path.join(
228
+ srcDirectory,
229
+ generatedDirectory,
230
+ 'register.gen.d.ts',
231
+ );
232
+ const apiDirectory = path.join(tempDir, 'api');
233
+ const compileOptions = {
234
+ sourceDirs: [apiDirectory],
235
+ distDir: path.join(tempDir, 'dist'),
236
+ tsconfigPath: path.join(tempDir, 'tsconfig.json'),
237
+ throwErrorInsteadOfExit: true,
238
+ };
239
+ await fs.outputJSON(compileOptions.tsconfigPath, {
240
+ compilerOptions: {
241
+ module: 'commonjs',
242
+ target: 'ES2022',
243
+ types: [],
244
+ noEmitOnError: true,
245
+ skipLibCheck: false,
246
+ },
247
+ include: ['api', sourceDirectory],
248
+ });
249
+ await fs.outputFile(
250
+ path.join(apiDirectory, 'index.ts'),
251
+ 'export const server = true;\n',
252
+ );
253
+ await fs.outputFile(
254
+ path.join(srcDirectory, 'routes/page.tsx'),
255
+ "import type { Missing } from 'missing-browser-only-types';\nexport const page: Missing = undefined;\n",
256
+ );
257
+ await fs.outputFile(
258
+ path.join(srcDirectory, generatedDirectory, 'main/router.gen.ts'),
259
+ "import { page } from '../../routes/page';\nexport const router = { page };\n",
260
+ );
261
+ await writeTanstackRegisterFile({
262
+ entries: ['main'],
263
+ srcDirectory,
264
+ generatedDirName,
265
+ });
266
+
267
+ await expect(compile(tempDir, {}, compileOptions)).rejects.toThrow(
268
+ /missing-browser-only-types/,
269
+ );
270
+
271
+ const previousExclusions = [path.join(tempDir, 'other-client.d.ts')];
272
+ const api = {
273
+ ...createAppContextApi({
274
+ srcDirectory,
275
+ serverCompileExcludedFiles: previousExclusions,
276
+ }),
277
+ _internalRuntimePlugins: () => {},
278
+ checkEntryPoint: () => {},
279
+ config: () => {},
280
+ modifyEntrypoints: () => {},
281
+ generateEntryCode: () => {},
282
+ onFileChanged: () => {},
283
+ modifyFileSystemRoutes: () => {},
284
+ onBeforeGenerateRoutes: () => {},
285
+ };
286
+ tanstackRouterPlugin({ generatedDirName }).setup!(api as never);
287
+ const excludeFiles = api.getAppContext().serverCompileExcludedFiles;
288
+ expect(excludeFiles).toEqual([...previousExclusions, declaration]);
289
+ expect(previousExclusions).toEqual([
290
+ path.join(tempDir, 'other-client.d.ts'),
291
+ ]);
292
+ await compile(tempDir, {}, { ...compileOptions, excludeFiles });
293
+ expect(
294
+ await fs.pathExists(path.join(compileOptions.distDir, 'api/index.js')),
295
+ ).toBe(true);
296
+ expect(
297
+ await fs.pathExists(path.join(compileOptions.distDir, sourceDirectory)),
298
+ ).toBe(false);
299
+ });
300
+
204
301
  test('typechecks plugin-owned routers and register metadata under strictest', async () => {
205
302
  tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-cli-'));
206
303
  const srcDirectory = path.join(tempDir, 'src');
@@ -345,7 +442,7 @@ describe('tanstack router cli plugin', () => {
345
442
 
346
443
  const taps: Record<string, any> = {};
347
444
  const api = {
348
- getAppContext: () => ({
445
+ ...createAppContextApi({
349
446
  srcDirectory,
350
447
  distDirectory,
351
448
  metaName: 'modern-js',
@@ -446,7 +543,7 @@ describe('tanstack router cli plugin', () => {
446
543
  test('injects the framework-resolving router wrapper for non-file-route entrypoints', async () => {
447
544
  const taps: Record<string, any> = {};
448
545
  const api = {
449
- getAppContext: () => ({
546
+ ...createAppContextApi({
450
547
  srcDirectory: '/tmp/app/src',
451
548
  metaName: 'modern-js',
452
549
  serverRoutes: [{ entryName: 'custom', urlPath: '/' }],
@@ -508,7 +605,7 @@ describe('tanstack router cli plugin', () => {
508
605
  test('leaves built-in and foreign-owned route entrypoints to their own router', () => {
509
606
  const taps: Record<string, any> = {};
510
607
  const api = {
511
- getAppContext: () => ({
608
+ ...createAppContextApi({
512
609
  srcDirectory: '/tmp/app/src',
513
610
  metaName: 'modern-js',
514
611
  serverRoutes: [{ entryName: 'home', urlPath: '/' }],
@@ -588,7 +685,7 @@ describe('tanstack router cli plugin', () => {
588
685
  test('source.include covers the package dist and TanStack runtime deps without string surgery', () => {
589
686
  const taps: Record<string, any> = {};
590
687
  const api = {
591
- getAppContext: () => ({
688
+ ...createAppContextApi({
592
689
  srcDirectory: '/tmp/app/src',
593
690
  serverRoutes: [],
594
691
  }),
@@ -624,7 +721,9 @@ describe('tanstack router cli plugin', () => {
624
721
  // the old `.replace('cjs', 'esm')` never matched the bundled dist/esm
625
722
  // runtime when the CLI was loaded through the ESM condition.
626
723
  expect(stringEntries[0]).toBe(path.resolve(__dirname, '..', '..'));
627
- expect(stringEntries[0]).not.toContain('esm');
724
+ expect(stringEntries[0]).not.toMatch(
725
+ /[\\/]dist[\\/]esm(?:-node)?(?:[\\/]|$)/,
726
+ );
628
727
  });
629
728
 
630
729
  test('emits the plugin-i18n augmentation only when plugin-i18n is registered', async () => {
@@ -666,7 +765,7 @@ describe('tanstack router cli plugin', () => {
666
765
 
667
766
  const taps: Record<string, any> = {};
668
767
  const api = {
669
- getAppContext: () => ({
768
+ ...createAppContextApi({
670
769
  srcDirectory,
671
770
  internalSrcAlias: '@/_',
672
771
  entrypoints: [entrypoint],
@@ -763,7 +862,7 @@ describe('tanstack router cli plugin', () => {
763
862
 
764
863
  const taps: Record<string, any> = {};
765
864
  const api = {
766
- getAppContext: () => ({
865
+ ...createAppContextApi({
767
866
  srcDirectory,
768
867
  internalSrcAlias: '@/_',
769
868
  entrypoints: [entrypoint],
@@ -828,7 +927,7 @@ describe('tanstack router cli plugin', () => {
828
927
  __modernRoutesDir: 'views',
829
928
  } as any as Entrypoint;
830
929
  const api = {
831
- getAppContext: () => ({
930
+ ...createAppContextApi({
832
931
  srcDirectory: '/tmp/app/src',
833
932
  internalSrcAlias: '@/_',
834
933
  entrypoints: [entrypoint],
@@ -876,7 +975,7 @@ describe('tanstack router cli plugin', () => {
876
975
  test('can opt out of Modern-owned route code splitting', async () => {
877
976
  const taps: Record<string, any> = {};
878
977
  const api = {
879
- getAppContext: () => ({
978
+ ...createAppContextApi({
880
979
  srcDirectory: '/tmp/app/src',
881
980
  serverRoutes: [],
882
981
  }),
@@ -105,54 +105,14 @@ describe('tanstack data mutation fetcher', () => {
105
105
  states.length = 0;
106
106
  });
107
107
 
108
- test('tracks submitting and loading phases for mutation submit', async () => {
109
- const actionResult = createDeferred<Response>();
110
- const invalidateResult = createDeferred<void>();
111
-
112
- currentRouter = createRouter({
113
- action: async () => actionResult.promise,
114
- invalidate: async () => invalidateResult.promise,
115
- });
116
-
117
- render(<FetcherHarness />);
118
- expect(screen.getByTestId('state').textContent).toBe('idle');
119
-
120
- let submitPromise: Promise<void> | undefined;
121
- act(() => {
122
- submitPromise = latestFetcher!.submit(
123
- { amount: 2 },
124
- { method: 'post', action: '/mutation' },
125
- );
126
- });
127
-
128
- await waitFor(() => {
129
- expect(screen.getByTestId('state').textContent).toBe('submitting');
130
- });
131
-
132
- actionResult.resolve(
133
- new Response(JSON.stringify({ count: 2 }), {
134
- headers: {
135
- 'Content-Type': 'application/json',
136
- },
137
- }),
138
- );
139
-
140
- await waitFor(() => {
141
- expect(screen.getByTestId('state').textContent).toBe('loading');
142
- });
143
-
144
- invalidateResult.resolve();
145
-
146
- await act(async () => {
147
- await submitPromise;
148
- });
149
-
150
- expect(screen.getByTestId('state').textContent).toBe('idle');
151
- expect(screen.getByTestId('data').textContent).toBe('{"count":2}');
152
- expect(states).toEqual(['idle', 'submitting', 'loading', 'idle']);
153
- });
154
-
155
- test('defaults fetcher submit without method to mutation state', async () => {
108
+ test.each([
109
+ { amount: 2, count: 2, method: 'post' as const, name: 'explicit method' },
110
+ { amount: 1, count: 1, method: undefined, name: 'default method' },
111
+ ])('tracks submitting/loading phases with the $name', async ({
112
+ amount,
113
+ count,
114
+ method,
115
+ }) => {
156
116
  const actionResult = createDeferred<Response>();
157
117
  const invalidateResult = createDeferred<void>();
158
118
  const action = rstest.fn(async () => actionResult.promise);
@@ -170,8 +130,11 @@ describe('tanstack data mutation fetcher', () => {
170
130
  let submitPromise: Promise<void> | undefined;
171
131
  act(() => {
172
132
  submitPromise = latestFetcher!.submit(
173
- { amount: 1 },
174
- { action: '/mutation' },
133
+ { amount },
134
+ {
135
+ ...(method === undefined ? {} : { method }),
136
+ action: '/mutation',
137
+ },
175
138
  );
176
139
  });
177
140
 
@@ -182,7 +145,7 @@ describe('tanstack data mutation fetcher', () => {
182
145
  expect(loader).not.toHaveBeenCalled();
183
146
 
184
147
  actionResult.resolve(
185
- new Response(JSON.stringify({ count: 1 }), {
148
+ new Response(JSON.stringify({ count }), {
186
149
  headers: {
187
150
  'Content-Type': 'application/json',
188
151
  },
@@ -200,44 +163,10 @@ describe('tanstack data mutation fetcher', () => {
200
163
  });
201
164
 
202
165
  expect(screen.getByTestId('state').textContent).toBe('idle');
203
- expect(screen.getByTestId('data').textContent).toBe('{"count":1}');
204
- expect(states).toEqual(['idle', 'submitting', 'loading', 'idle']);
205
- });
206
-
207
- test('passes built href and matched route params to mutation actions', async () => {
208
- const action = rstest.fn(
209
- async ({ request, params }: Parameters<RouteHandler>[0]) => ({
210
- params,
211
- requestUrl: request.url,
212
- }),
166
+ expect(screen.getByTestId('data').textContent).toBe(
167
+ JSON.stringify({ count }),
213
168
  );
214
- currentRouter = createRouter({
215
- action,
216
- params: { userId: '42' },
217
- });
218
-
219
- render(<FetcherHarness />);
220
-
221
- await act(async () => {
222
- await latestFetcher!.submit(
223
- { amount: 2 },
224
- {
225
- method: 'post',
226
- action: '/users/42?tab=edit#details',
227
- },
228
- );
229
- });
230
-
231
- expect(action).toHaveBeenCalledTimes(1);
232
- const actionArgs = action.mock.calls[0][0];
233
- const expectedRequestUrl = `${window.location.origin}/users/42?tab=edit#details`;
234
- expect(actionArgs.params).toEqual({ userId: '42' });
235
- expect(actionArgs.request.url).toBe(expectedRequestUrl);
236
- expect(latestFetcher?.data).toEqual({
237
- params: { userId: '42' },
238
- requestUrl: expectedRequestUrl,
239
- });
240
- expect(currentRouter.invalidate).toHaveBeenCalledTimes(1);
169
+ expect(states).toEqual(['idle', 'submitting', 'loading', 'idle']);
241
170
  });
242
171
 
243
172
  test('surfaces non-2xx action responses as fetcher errors', async () => {
@@ -460,37 +389,4 @@ describe('tanstack data mutation fetcher', () => {
460
389
  globalThis.FormData = NativeFormData;
461
390
  }
462
391
  });
463
-
464
- test('does not throw for Form submit with non-2xx response and still invalidates', async () => {
465
- const action = rstest.fn(
466
- async () =>
467
- new Response(JSON.stringify({ message: 'invalid amount' }), {
468
- status: 422,
469
- headers: {
470
- 'Content-Type': 'application/json',
471
- },
472
- }),
473
- );
474
- currentRouter = createRouter({
475
- action,
476
- });
477
-
478
- render(
479
- <Form method="post" action="/mutation">
480
- <button type="submit">submit</button>
481
- </Form>,
482
- );
483
-
484
- const form = document.querySelector('form');
485
- expect(form).toBeTruthy();
486
-
487
- await act(async () => {
488
- fireEvent.submit(form!);
489
- });
490
-
491
- await waitFor(() => {
492
- expect(action).toHaveBeenCalledTimes(1);
493
- expect(currentRouter.invalidate).toHaveBeenCalledTimes(1);
494
- });
495
- });
496
392
  });
@@ -45,21 +45,22 @@ test('round-trips TanStack RSC flight values across supported value types', asyn
45
45
  const circular: Record<string, unknown> = { marker: 'circular' };
46
46
  circular.self = circular;
47
47
 
48
+ const rscProxy = createRscProxy(() => ({ slot: 'server' }), {
49
+ renderable: true,
50
+ stream: createStream(),
51
+ }) as React.ReactElement & Record<PropertyKey, unknown>;
52
+
48
53
  const mapValue = new Map<unknown, unknown>([
49
54
  ['primitive', 1],
50
55
  ['shared', shared],
51
56
  [shared, circular],
57
+ ['rscProxy', rscProxy],
52
58
  ]);
53
59
  mapValue.set('self', mapValue);
54
60
 
55
- const setValue = new Set<unknown>(['set-item', shared, circular]);
61
+ const setValue = new Set<unknown>(['set-item', shared, circular, rscProxy]);
56
62
  setValue.add(setValue);
57
63
 
58
- const rscProxy = createRscProxy(() => ({ slot: 'server' }), {
59
- renderable: true,
60
- stream: createStream(),
61
- }) as React.ReactElement & Record<PropertyKey, unknown>;
62
-
63
64
  const fixture: RoundTripFixture = {
64
65
  bigintValue: 9007199254740993n,
65
66
  booleanValue: true,
@@ -115,12 +116,14 @@ test('round-trips TanStack RSC flight values across supported value types', asyn
115
116
  expect(revived.mapValue.get('shared')).toBe(revived.sharedA);
116
117
  expect(revived.mapValue.get(revived.sharedA)).toBe(revived.circular);
117
118
  expect(revived.mapValue.get('self')).toBe(revived.mapValue);
119
+ expect(isValidElement(revived.mapValue.get('rscProxy'))).toBe(true);
118
120
 
119
121
  expect(revived.setValue).toBeInstanceOf(Set);
120
122
  expect(revived.setValue).not.toBe(setValue);
121
123
  expect(revived.setValue.has('set-item')).toBe(true);
122
124
  expect(revived.setValue.has(revived.sharedA)).toBe(true);
123
125
  expect(revived.setValue.has(revived.circular)).toBe(true);
126
+ expect([...revived.setValue].some(value => isValidElement(value))).toBe(true);
124
127
  expect(revived.setValue.has(revived.setValue)).toBe(true);
125
128
 
126
129
  expect(serialized.rscProxy).not.toBe(rscProxy);
@@ -1,19 +1,11 @@
1
- // Behavioral coverage for the headless routes-generate entry point: the
2
- // export-shape test in cli.test.ts would pass even if the body were
3
- // reverted, so this file pins the actual prepare-only drive contract —
4
- // createRunOptions against the app dir, cli.init with build-command
5
- // semantics, the MODERN_ARGV gate that makes analyze.onPrepare fire
6
- // generateEntryCode, and the absolute config-file resolution that keeps
7
- // headless runs (cwd != appDirectory) from feeding `false` into node's
8
- // path APIs.
1
+ // The headless entry must drive the real TanStack artifact writer. Checking
2
+ // only the createRunOptions/cli.init choreography would allow a no-op CLI to
3
+ // pass while leaving router.gen.ts stale or missing.
9
4
  const createRunOptionsMock = rstest.fn(async (options: unknown) => ({
10
5
  mockRunOptions: true,
11
6
  received: options,
12
7
  }));
13
- const cliInitMock = rstest.fn(async (options: unknown) => ({
14
- appContext: {},
15
- received: options,
16
- }));
8
+ const cliInitMock = rstest.fn(async () => ({ appContext: {} }));
17
9
 
18
10
  rstest.mock('@modern-js/app-tools/cli/run', () => ({
19
11
  createRunOptions: createRunOptionsMock,
@@ -26,7 +18,10 @@ import fs from 'node:fs';
26
18
  import os from 'node:os';
27
19
  import path from 'node:path';
28
20
  import { INTERNAL_RUNTIME_PLUGINS } from '@modern-js/utils';
29
- import { generateTanstackRouteArtifacts } from '../../src/cli';
21
+ import {
22
+ generateTanstackRouteArtifacts,
23
+ writeTanstackRouterTypesForEntries,
24
+ } from '../../src/cli';
30
25
 
31
26
  function makeAppDir(withConfig: boolean): string {
32
27
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tanstack-routes-gen-'));
@@ -46,9 +41,55 @@ describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
46
41
  cliInitMock.mockClear();
47
42
  });
48
43
 
49
- it('drives a prepare-only cli.init with build-command semantics', async () => {
44
+ it('writes a real route artifact through the prepare-only CLI drive', async () => {
50
45
  const previousArgv = process.env.MODERN_ARGV;
51
46
  const appDirectory = makeAppDir(true);
47
+ const srcDirectory = path.join(appDirectory, 'src');
48
+ const componentPath = path.join(srcDirectory, 'routes', 'page.tsx');
49
+ fs.mkdirSync(path.dirname(componentPath), { recursive: true });
50
+ fs.writeFileSync(
51
+ componentPath,
52
+ 'export default function Page() { return null; }\n',
53
+ 'utf-8',
54
+ );
55
+
56
+ const runOptions = {
57
+ cwd: appDirectory,
58
+ version: '9.9.9-test',
59
+ internalPlugins: INTERNAL_RUNTIME_PLUGINS,
60
+ configFile: path.resolve(appDirectory, 'modern.config'),
61
+ };
62
+ createRunOptionsMock.mockResolvedValue({
63
+ mockRunOptions: true,
64
+ received: runOptions,
65
+ });
66
+ cliInitMock.mockImplementation(async () => {
67
+ await writeTanstackRouterTypesForEntries({
68
+ appContext: {
69
+ srcDirectory,
70
+ internalSrcAlias: '@/_',
71
+ entrypoints: [{ entryName: 'main', isMainEntry: true }],
72
+ } as any,
73
+ routesByEntry: {
74
+ main: [
75
+ {
76
+ type: 'nested',
77
+ id: 'layout',
78
+ isRoot: true,
79
+ children: [
80
+ {
81
+ type: 'nested',
82
+ id: 'page',
83
+ index: true,
84
+ _component: '@/_/routes/page',
85
+ },
86
+ ],
87
+ },
88
+ ] as any,
89
+ },
90
+ });
91
+ return { appContext: {} };
92
+ });
52
93
 
53
94
  try {
54
95
  await generateTanstackRouteArtifacts({
@@ -61,26 +102,25 @@ describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
61
102
  string,
62
103
  unknown
63
104
  >;
64
- expect(runOptionsArg.cwd).toBe(appDirectory);
65
- expect(runOptionsArg.version).toBe('9.9.9-test');
66
- expect(runOptionsArg.internalPlugins).toBe(INTERNAL_RUNTIME_PLUGINS);
67
-
68
- // The config file is resolved to an absolute base path against the app
69
- // directory (not process.cwd()). Without this, a headless run from a
70
- // different cwd finds no config, `findExists` returns `false`, and that
71
- // boolean reaches `path.isAbsolute` deep inside plugin/cli.
72
- expect(runOptionsArg.configFile).toBe(
73
- path.resolve(appDirectory, 'modern.config'),
74
- );
105
+ expect(runOptionsArg).toEqual(runOptions);
75
106
 
76
107
  expect(cliInitMock).toHaveBeenCalledTimes(1);
77
108
  const initArg = cliInitMock.mock.calls[0][0] as Record<string, unknown>;
78
109
  expect(initArg.command).toBe('build');
79
110
  expect(initArg.mockRunOptions).toBe(true);
80
-
81
- // checkIsBuildCommands() resolves the command from MODERN_ARGV — this
82
- // is what lets analyze.onPrepare fire generateEntryCode headlessly.
83
111
  expect(process.env.MODERN_ARGV).toBe('node modern build');
112
+
113
+ const routerArtifact = fs.readFileSync(
114
+ path.join(srcDirectory, 'modern-tanstack', 'main', 'router.gen.ts'),
115
+ 'utf-8',
116
+ );
117
+ const registerArtifact = fs.readFileSync(
118
+ path.join(srcDirectory, 'modern-tanstack', 'register.gen.d.ts'),
119
+ 'utf-8',
120
+ );
121
+ expect(routerArtifact).toContain('createRootRouteWithContext');
122
+ expect(routerArtifact).toContain('routeTree');
123
+ expect(registerArtifact).toContain('./main/router.gen');
84
124
  } finally {
85
125
  fs.rmSync(appDirectory, { force: true, recursive: true });
86
126
  if (previousArgv === undefined) {
@@ -91,7 +131,7 @@ describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
91
131
  }
92
132
  });
93
133
 
94
- it('throws a clear error (never a boolean path crash) when no config file exists', async () => {
134
+ it('throws a clear error when no config file exists', async () => {
95
135
  const appDirectory = makeAppDir(false);
96
136
 
97
137
  try {
@@ -3,13 +3,11 @@ import {
3
3
  routerProviderRegistryHooks,
4
4
  } from '@modern-js/runtime/context';
5
5
  import * as tanstackHooks from '../../src/runtime/hooks';
6
- import { applyRouterRuntimeState } from '../../src/runtime/lifecycle';
7
6
  import { tanstackRouterPlugin as browserPlugin } from '../../src/runtime/plugin';
8
7
  import { tanstackRouterPlugin as nodePlugin } from '../../src/runtime/plugin.node';
9
- import { getTanstackRouterState } from '../../src/runtime/state';
10
8
 
11
9
  describe('tanstack router hooks single declaration source', () => {
12
- test('re-exports the canonical hook instances owned by @modern-js/runtime', () => {
10
+ test('re-exports and registers the canonical hook instances', () => {
13
11
  // Identity matters: separate instances would split the hook registry
14
12
  // between the built-in router wrapper and this provider.
15
13
  expect(tanstackHooks.modifyRoutes).toBe(canonicalModifyRoutes);
@@ -19,22 +17,7 @@ describe('tanstack router hooks single declaration source', () => {
19
17
  expect(tanstackHooks.modifyRoutes).toBe(
20
18
  routerProviderRegistryHooks.modifyRoutes,
21
19
  );
22
- });
23
-
24
- test('both runtime plugins register the canonical hook registry object', () => {
25
20
  expect(browserPlugin().registryHooks).toBe(routerProviderRegistryHooks);
26
21
  expect(nodePlugin().registryHooks).toBe(routerProviderRegistryHooks);
27
22
  });
28
23
  });
29
-
30
- describe('tanstack router runtime state accessor', () => {
31
- test('returns undefined before a tanstack router instance is created', () => {
32
- const context = {};
33
-
34
- applyRouterRuntimeState(context as never, {
35
- framework: 'tanstack',
36
- });
37
-
38
- expect(getTanstackRouterState(context)).toBeUndefined();
39
- });
40
- });
@@ -1,8 +1,5 @@
1
1
  import { isNotFound, isRedirect } from '@tanstack/react-router';
2
2
  import {
3
- createRouteStaticData,
4
- isAbsoluteUrl,
5
- mapSplatParamsForModernLoader,
6
3
  modernLoaderToTanstack,
7
4
  throwTanstackRedirect,
8
5
  } from '../../src/runtime/loaderBridge';
@@ -23,73 +20,6 @@ function catchThrown(fn: () => unknown): unknown {
23
20
  throw new Error('expected the function to throw');
24
21
  }
25
22
 
26
- describe('throwTanstackRedirect', () => {
27
- test('absolute URLs redirect via href (external), not via to', () => {
28
- // The old inline codegen handler threw `redirect({ href })` INSIDE a
29
- // try block whose catch replaced it with `redirect({ to: absoluteUrl })`,
30
- // making TanStack treat the absolute URL as an internal path.
31
- const thrown = catchThrown(() =>
32
- throwTanstackRedirect('https://example.com/external'),
33
- ) as RedirectLike;
34
-
35
- expect(isRedirect(thrown)).toBe(true);
36
- expect(thrown.options?.href).toBe('https://example.com/external');
37
- expect(thrown.options?.to).toBeUndefined();
38
- });
39
-
40
- test('relative paths redirect via to so the basepath rewrite applies', () => {
41
- const thrown = catchThrown(() =>
42
- throwTanstackRedirect('/dashboard'),
43
- ) as RedirectLike;
44
-
45
- expect(isRedirect(thrown)).toBe(true);
46
- expect(thrown.options?.to).toBe('/dashboard');
47
- expect(thrown.options?.href).toBeUndefined();
48
- });
49
-
50
- test('empty location falls back to /', () => {
51
- const thrown = catchThrown(() => throwTanstackRedirect('')) as RedirectLike;
52
- expect(thrown.options?.to).toBe('/');
53
- });
54
- });
55
-
56
- describe('isAbsoluteUrl', () => {
57
- test('detects absolute and relative URLs', () => {
58
- expect(isAbsoluteUrl('https://example.com/a')).toBe(true);
59
- expect(isAbsoluteUrl('mailto:x@example.com')).toBe(true);
60
- expect(isAbsoluteUrl('/internal/path')).toBe(false);
61
- expect(isAbsoluteUrl('relative')).toBe(false);
62
- });
63
- });
64
-
65
- describe('mapSplatParamsForModernLoader', () => {
66
- test('maps TanStack _splat to React Router * only for splat routes', () => {
67
- expect(
68
- mapSplatParamsForModernLoader({ _splat: 'a/b', id: '1' }, true),
69
- ).toEqual({ '*': 'a/b', id: '1' });
70
- expect(
71
- mapSplatParamsForModernLoader({ _splat: 'a/b', id: '1' }, false),
72
- ).toEqual({ _splat: 'a/b', id: '1' });
73
- expect(mapSplatParamsForModernLoader({ id: '1' }, true)).toEqual({
74
- id: '1',
75
- });
76
- });
77
- });
78
-
79
- describe('createRouteStaticData', () => {
80
- test('drops empty fields', () => {
81
- const loader = () => null;
82
- expect(createRouteStaticData({})).toEqual({});
83
- expect(createRouteStaticData({ modernRouteId: '' })).toEqual({});
84
- expect(
85
- createRouteStaticData({
86
- modernRouteId: 'page',
87
- modernRouteLoader: loader,
88
- }),
89
- ).toEqual({ modernRouteId: 'page', modernRouteLoader: loader });
90
- });
91
- });
92
-
93
23
  describe('modernLoaderToTanstack', () => {
94
24
  const baseCtx = {
95
25
  location: { href: 'http://localhost/products/1' },
@@ -189,13 +119,6 @@ describe('modernLoaderToTanstack', () => {
189
119
  expect(isNotFound(thrown)).toBe(true);
190
120
  });
191
121
 
192
- test('preserves returned non-404 error Responses as loader results', async () => {
193
- const response = new Response('loader exploded', { status: 500 });
194
- const loader = modernLoaderToTanstack({ hasSplat: false }, () => response);
195
-
196
- await expect(loader(baseCtx)).resolves.toBe(response);
197
- });
198
-
199
122
  test('translates redirect Responses thrown synchronously by the loader', () => {
200
123
  const loader = modernLoaderToTanstack({ hasSplat: false }, () => {
201
124
  throw new Response(null, {