@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.4 → 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 (54) hide show
  1. package/dist/cjs/cli/index.js +9 -0
  2. package/dist/cjs/runtime/lifecycle.js +7 -7
  3. package/dist/cjs/runtime/prefetchLink.js +2 -7
  4. package/dist/cjs/runtime/register.js +2 -2
  5. package/dist/cjs/runtime/router.js +25 -9
  6. package/dist/cjs/runtime/state.js +2 -2
  7. package/dist/esm/cli/index.mjs +9 -0
  8. package/dist/esm/runtime/lifecycle.mjs +1 -1
  9. package/dist/esm/runtime/prefetchLink.mjs +1 -6
  10. package/dist/esm/runtime/register.mjs +1 -1
  11. package/dist/esm/runtime/router.mjs +27 -11
  12. package/dist/esm/runtime/state.mjs +1 -1
  13. package/dist/esm-node/cli/index.mjs +9 -0
  14. package/dist/esm-node/runtime/lifecycle.mjs +1 -1
  15. package/dist/esm-node/runtime/prefetchLink.mjs +1 -6
  16. package/dist/esm-node/runtime/register.mjs +1 -1
  17. package/dist/esm-node/runtime/router.mjs +27 -11
  18. package/dist/esm-node/runtime/state.mjs +1 -1
  19. package/dist/types/runtime/lifecycle.d.ts +9 -2
  20. package/dist/types/runtime/register.d.ts +1 -1
  21. package/dist/types/runtime/router.d.ts +2 -13
  22. package/dist/types/runtime/state.d.ts +1 -1
  23. package/dist/types/runtime/types.d.ts +2 -1
  24. package/package.json +12 -10
  25. package/rstest.config.mts +0 -6
  26. package/src/cli/index.ts +10 -0
  27. package/src/runtime/lifecycle.ts +14 -3
  28. package/src/runtime/plugin.node.tsx +1 -1
  29. package/src/runtime/prefetchLink.tsx +1 -23
  30. package/src/runtime/register.ts +1 -1
  31. package/src/runtime/router.ts +25 -40
  32. package/src/runtime/state.ts +1 -1
  33. package/src/runtime/types.ts +2 -2
  34. package/tests/realm-isolation.test.ts +8 -2
  35. package/tests/router/cli.test.ts +105 -8
  36. package/tests/router/dataMutation.test.tsx +17 -121
  37. package/tests/router/flightSerialization.roundtrip.test.tsx +9 -6
  38. package/tests/router/generateRouteArtifacts.test.ts +69 -29
  39. package/tests/router/hooks.test.ts +1 -18
  40. package/tests/router/loaderBridge.test.ts +0 -77
  41. package/tests/router/prefetchLink.test.tsx +28 -16
  42. package/tests/router/prefetchLinkPreload.test.tsx +21 -28
  43. package/tests/router/register.test.ts +1 -1
  44. package/tests/router/routeHooks.test.ts +41 -105
  45. package/tests/router/routeTree.test.ts +0 -212
  46. package/tests/router/routerCompatibility.test.ts +32 -9
  47. package/tests/router/rsc.test.tsx +118 -136
  48. package/tests/router/rscDecoderSecurity.test.ts +24 -53
  49. package/tests/router/tanstackTypes.test.ts +24 -364
  50. package/tests/router/fastDefaults.test.ts +0 -32
  51. package/tests/router/hydrationBoundary.test.tsx +0 -23
  52. package/tests/router/packageSurface.test.ts +0 -24
  53. package/tests/router/preloadRedirect.test.ts +0 -64
  54. package/tests/router/rscPayloadRouterMatrix.test.ts +0 -197
@@ -1,43 +1,28 @@
1
- /**
2
- * Entry-injected router module for entrypoints that are NOT TanStack
3
- * file-route entrypoints (e.g. apps configuring
4
- * `router: { framework: 'tanstack', createRoutes }` in modern.runtime.ts).
5
- *
6
- * The CLI plugin injects `{ name: 'router', path: '<pkg>/runtime/router' }`
7
- * for those entries. This module creates a framework-resolving wrapper whose
8
- * app-owned provider realm contains this module graph's TanStack factory.
9
- */
10
- import * as runtimeRouter from '@modern-js/runtime/router/internal';
1
+ /** App-local composition for custom entries using runtime router selection. */
2
+ import { routerProviderRegistryHooks } from '@modern-js/runtime/context';
3
+ import { routerPlugin as nativeRouterPlugin } from '@modern-js/runtime/router/internal';
4
+ import { createRouterPlugin } from '@modern-js/runtime-extensions/router-provider';
5
+ import { createRouterStatePlugin } from '@modern-js/runtime-extensions/router-state-plugin';
11
6
  import { tanstackRouterProviderFactory } from './register';
12
7
 
13
- type LegacyRouterPlugin = (typeof runtimeRouter)['routerPlugin'];
14
- type CurrentRouterPluginCreator = (
15
- localProviders: readonly {
16
- name: string;
17
- factory: typeof tanstackRouterProviderFactory;
18
- }[],
19
- ) => LegacyRouterPlugin;
8
+ const createProviderPlugin = createRouterPlugin({
9
+ defaultProvider: { name: 'react-router', factory: nativeRouterPlugin },
10
+ registryHooks: routerProviderRegistryHooks,
11
+ localProviders: [
12
+ { name: 'tanstack', factory: tanstackRouterProviderFactory },
13
+ ],
14
+ });
15
+ const statePlugin = createRouterStatePlugin({
16
+ registryHooks: routerProviderRegistryHooks,
17
+ });
20
18
 
21
- function getRouterPlugin(): LegacyRouterPlugin {
22
- const createRouterPlugin = Reflect.get(runtimeRouter, 'createRouterPlugin') as
23
- | CurrentRouterPluginCreator
24
- | undefined;
25
-
26
- if (typeof createRouterPlugin === 'function') {
27
- return createRouterPlugin([
28
- { name: 'tanstack', factory: tanstackRouterProviderFactory },
29
- ]);
30
- }
31
-
32
- const legacyRouterPlugin = Reflect.get(runtimeRouter, 'routerPlugin');
33
-
34
- if (typeof legacyRouterPlugin === 'function') {
35
- return tanstackRouterProviderFactory as unknown as LegacyRouterPlugin;
36
- }
37
-
38
- throw new Error(
39
- '[@modern-js/plugin-tanstack] The installed @modern-js/runtime/router/internal exports neither createRouterPlugin nor routerPlugin. Install a compatible @modern-js/runtime version.',
40
- );
41
- }
42
-
43
- export const routerPlugin: LegacyRouterPlugin = getRouterPlugin();
19
+ export const routerPlugin: typeof nativeRouterPlugin = (userConfig = {}) => {
20
+ const providerPlugin = createProviderPlugin(userConfig);
21
+ return {
22
+ ...providerPlugin,
23
+ setup(api) {
24
+ statePlugin.setup(api);
25
+ providerPlugin.setup(api);
26
+ },
27
+ };
28
+ };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getRouterRuntimeState,
3
3
  type InternalRouterRuntimeState,
4
- } from '@modern-js/runtime/context';
4
+ } from '@modern-js/runtime-extensions/router-state';
5
5
  import type { AnyRouter } from '@tanstack/react-router';
6
6
 
7
7
  /**
@@ -2,11 +2,11 @@ import type { ModernRoute } from '@modern-js/runtime/context';
2
2
  import type { RouteObject } from '@modern-js/runtime-utils/router';
3
3
  import type React from 'react';
4
4
 
5
+ export type { ModernRoute } from '@modern-js/runtime/context';
5
6
  export type {
6
7
  InternalRouterServerSnapshot,
7
- ModernRoute,
8
8
  RouterFramework,
9
- } from '@modern-js/runtime/context';
9
+ } from '@modern-js/runtime-extensions/router-state';
10
10
 
11
11
  /** TanStack-specific router config. */
12
12
  export type RouterConfig = {
@@ -1,4 +1,4 @@
1
- import type { RouterProviderFactory } from '@modern-js/runtime/context';
1
+ import type { RouterProviderFactory } from '@modern-js/runtime-extensions/router-provider';
2
2
  import { rstest } from '@rstest/core';
3
3
  import type { RouterExtendsHooks } from '../src/runtime/hooks';
4
4
 
@@ -12,6 +12,7 @@ type RouterModule = typeof import('../src/runtime/router');
12
12
 
13
13
  type Vertical = {
14
14
  context: RuntimeContextModule;
15
+ providers: typeof import('@modern-js/runtime-extensions/router-provider');
15
16
  factory: RouterProviderFactory;
16
17
  hooks: RouterExtendsHooks;
17
18
  Link: unknown;
@@ -32,8 +33,12 @@ async function loadVertical(): Promise<Vertical> {
32
33
  const { Link } = await import('../src/runtime/prefetchLink');
33
34
  const { useMatches } = await import('../src/runtime/routeHooks');
34
35
 
36
+ const providers = await import(
37
+ '@modern-js/runtime-extensions/router-provider'
38
+ );
35
39
  return {
36
40
  context,
41
+ providers,
37
42
  factory: tanstackRouterProviderFactory,
38
43
  hooks: routerProviderRegistryHooks,
39
44
  Link,
@@ -50,6 +55,7 @@ function setupRouterWrapper(vertical: Vertical) {
50
55
  const plugin = vertical.routerPlugin({ framework: 'tanstack' });
51
56
  plugin.setup?.({
52
57
  getHooks: () => vertical.hooks,
58
+ onAfterCreateRouter: () => undefined,
53
59
  getRuntimeConfig: () => ({ router: { framework: 'tanstack' } }),
54
60
  onBeforeRender: listener => {
55
61
  onBeforeRender = listener;
@@ -101,7 +107,7 @@ describe('TanStack provider runtime-realm isolation', () => {
101
107
 
102
108
  // Mixed-version callers that do not supply a realm still see the shared
103
109
  // keep-first fallback, but production wrappers no longer consume it.
104
- expect(verticalB.context.resolveRouterProvider('tanstack')).toBe(
110
+ expect(verticalB.providers.resolveRouterProvider('tanstack')).toBe(
105
111
  verticalA.factory,
106
112
  );
107
113
  expect(warnSpy).toHaveBeenCalledWith(
@@ -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
  }),
@@ -668,7 +765,7 @@ describe('tanstack router cli plugin', () => {
668
765
 
669
766
  const taps: Record<string, any> = {};
670
767
  const api = {
671
- getAppContext: () => ({
768
+ ...createAppContextApi({
672
769
  srcDirectory,
673
770
  internalSrcAlias: '@/_',
674
771
  entrypoints: [entrypoint],
@@ -765,7 +862,7 @@ describe('tanstack router cli plugin', () => {
765
862
 
766
863
  const taps: Record<string, any> = {};
767
864
  const api = {
768
- getAppContext: () => ({
865
+ ...createAppContextApi({
769
866
  srcDirectory,
770
867
  internalSrcAlias: '@/_',
771
868
  entrypoints: [entrypoint],
@@ -830,7 +927,7 @@ describe('tanstack router cli plugin', () => {
830
927
  __modernRoutesDir: 'views',
831
928
  } as any as Entrypoint;
832
929
  const api = {
833
- getAppContext: () => ({
930
+ ...createAppContextApi({
834
931
  srcDirectory: '/tmp/app/src',
835
932
  internalSrcAlias: '@/_',
836
933
  entrypoints: [entrypoint],
@@ -878,7 +975,7 @@ describe('tanstack router cli plugin', () => {
878
975
  test('can opt out of Modern-owned route code splitting', async () => {
879
976
  const taps: Record<string, any> = {};
880
977
  const api = {
881
- getAppContext: () => ({
978
+ ...createAppContextApi({
882
979
  srcDirectory: '/tmp/app/src',
883
980
  serverRoutes: [],
884
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 {