@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.4 → 3.9.0-ultramodern.6

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 (55) 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 +0 -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 +0 -2
  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 +0 -2
  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 -9
  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 -21
  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 -12
  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 +7 -17
  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/routeHooks.test.ts +41 -105
  44. package/tests/router/routeTree.test.ts +0 -212
  45. package/tests/router/rsc.test.tsx +118 -136
  46. package/tests/router/rscDecoderSecurity.test.ts +24 -53
  47. package/tests/router/tanstackTypes.test.ts +24 -364
  48. package/tests/router/fastDefaults.test.ts +0 -32
  49. package/tests/router/fixtures/legacyRuntimeRouter.ts +0 -10
  50. package/tests/router/hydrationBoundary.test.tsx +0 -23
  51. package/tests/router/packageSurface.test.ts +0 -24
  52. package/tests/router/preloadRedirect.test.ts +0 -64
  53. package/tests/router/register.test.ts +0 -8
  54. package/tests/router/routerCompatibility.test.ts +0 -239
  55. 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,17 +1,13 @@
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
 
5
- const REGISTRY_SLOT: unique symbol = Symbol.for(
6
- '@modern-js/runtime:router-providers:v3',
7
- );
8
- const host = globalThis as { [REGISTRY_SLOT]?: unknown };
9
-
10
5
  type RuntimeContextModule = typeof import('@modern-js/runtime/context');
11
6
  type RouterModule = typeof import('../src/runtime/router');
12
7
 
13
8
  type Vertical = {
14
9
  context: RuntimeContextModule;
10
+ providers: typeof import('@modern-js/runtime-extensions/router-provider');
15
11
  factory: RouterProviderFactory;
16
12
  hooks: RouterExtendsHooks;
17
13
  Link: unknown;
@@ -32,8 +28,12 @@ async function loadVertical(): Promise<Vertical> {
32
28
  const { Link } = await import('../src/runtime/prefetchLink');
33
29
  const { useMatches } = await import('../src/runtime/routeHooks');
34
30
 
31
+ const providers = await import(
32
+ '@modern-js/runtime-extensions/router-provider'
33
+ );
35
34
  return {
36
35
  context,
36
+ providers,
37
37
  factory: tanstackRouterProviderFactory,
38
38
  hooks: routerProviderRegistryHooks,
39
39
  Link,
@@ -50,6 +50,7 @@ function setupRouterWrapper(vertical: Vertical) {
50
50
  const plugin = vertical.routerPlugin({ framework: 'tanstack' });
51
51
  plugin.setup?.({
52
52
  getHooks: () => vertical.hooks,
53
+ onAfterCreateRouter: () => undefined,
53
54
  getRuntimeConfig: () => ({ router: { framework: 'tanstack' } }),
54
55
  onBeforeRender: listener => {
55
56
  onBeforeRender = listener;
@@ -65,7 +66,6 @@ function setupRouterWrapper(vertical: Vertical) {
65
66
 
66
67
  describe('TanStack provider runtime-realm isolation', () => {
67
68
  beforeEach(() => {
68
- delete host[REGISTRY_SLOT];
69
69
  rstest.resetModules();
70
70
  (
71
71
  globalThis as typeof globalThis & {
@@ -75,7 +75,6 @@ describe('TanStack provider runtime-realm isolation', () => {
75
75
  });
76
76
 
77
77
  afterEach(() => {
78
- delete host[REGISTRY_SLOT];
79
78
  rstest.resetModules();
80
79
  });
81
80
 
@@ -98,15 +97,6 @@ describe('TanStack provider runtime-realm isolation', () => {
98
97
  expect(routerB.useMatches).toBe(verticalB.useMatches);
99
98
  expect(routerB.Link).not.toBe(verticalA.Link);
100
99
  expect(routerB.useMatches).not.toBe(verticalA.useMatches);
101
-
102
- // Mixed-version callers that do not supply a realm still see the shared
103
- // keep-first fallback, but production wrappers no longer consume it.
104
- expect(verticalB.context.resolveRouterProvider('tanstack')).toBe(
105
- verticalA.factory,
106
- );
107
- expect(warnSpy).toHaveBeenCalledWith(
108
- expect.stringMatching(/keeping the first registration/),
109
- );
110
100
  } finally {
111
101
  warnSpy.mockRestore();
112
102
  }
@@ -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);