@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,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, {
@@ -5,7 +5,7 @@ import {
5
5
  createRouter,
6
6
  RouterProvider,
7
7
  } from '@tanstack/react-router';
8
- import { render, waitFor } from '@testing-library/react';
8
+ import { act, render, waitFor } from '@testing-library/react';
9
9
  import React from 'react';
10
10
  import { Link, NavLink } from '../../src/runtime/prefetchLink';
11
11
 
@@ -74,7 +74,7 @@ async function renderLink(options: Parameters<typeof buildRouter>[0]) {
74
74
  return found as HTMLAnchorElement;
75
75
  });
76
76
 
77
- return { anchor, ...utils };
77
+ return { anchor, router, ...utils };
78
78
  }
79
79
 
80
80
  describe('tanstack prefetch link adapter - aria-current override', () => {
@@ -100,27 +100,39 @@ describe('tanstack prefetch link adapter - aria-current override', () => {
100
100
  expect(anchor.hasAttribute('aria-current')).toBe(false);
101
101
  });
102
102
 
103
- it('leaves an inactive link untouched when caller passes nothing', async () => {
104
- const { anchor } = await renderLink({ initialPath: '/' });
105
- expect(anchor.hasAttribute('aria-current')).toBe(false);
106
- expect(anchor.getAttribute('data-status')).not.toBe('active');
107
- });
108
-
109
- it('resolves render-prop children with the active state like TanStack Link', async () => {
110
- const { anchor } = await renderLink({
111
- initialPath: '/settings',
103
+ it('updates render-prop children across an inactive-to-active transition', async () => {
104
+ const { anchor, router } = await renderLink({
105
+ initialPath: '/',
112
106
  children: ({ isActive }: { isActive: boolean }) =>
113
107
  isActive ? 'Active settings' : 'Settings',
114
108
  });
115
- expect(anchor.textContent).toBe('Active settings');
109
+
110
+ expect(anchor.textContent).toBe('Settings');
111
+
112
+ await act(async () => {
113
+ await router.navigate({ to: '/settings' });
114
+ });
115
+
116
+ await waitFor(() => expect(anchor.textContent).toBe('Active settings'));
116
117
  });
117
118
 
118
- it('resolves render-prop children as inactive on a non-matching route', async () => {
119
+ it('passes the native anchor ref through the router hook', async () => {
120
+ const ref = React.createRef<HTMLAnchorElement>();
119
121
  const { anchor } = await renderLink({
120
122
  initialPath: '/',
121
- children: ({ isActive }: { isActive: boolean }) =>
122
- isActive ? 'Active settings' : 'Settings',
123
+ linkProps: { ref, prefetch: 'none' },
123
124
  });
124
- expect(anchor.textContent).toBe('Settings');
125
+ expect(ref.current).toBe(anchor);
126
+ });
127
+
128
+ it('preserves NavLink render children and aria-current overrides', async () => {
129
+ const { anchor } = await renderLink({
130
+ initialPath: '/settings',
131
+ useNavLink: true,
132
+ linkProps: { 'aria-current': 'step', preload: false },
133
+ children: ({ isActive }) => (isActive ? 'Current step' : 'Other step'),
134
+ });
135
+ expect(anchor.textContent).toBe('Current step');
136
+ expect(anchor.getAttribute('aria-current')).toBe('step');
125
137
  });
126
138
  });
@@ -27,59 +27,52 @@ describe('tanstack prefetch link adapter - preload mapping', () => {
27
27
  mockReturnProps = { href: '/settings' };
28
28
  });
29
29
 
30
- it('defaults TanStack preload to viewport', () => {
31
- render(<Link to="/settings">Settings</Link>);
32
-
33
- expect(capturedOptions.map(o => o.preload)).toEqual(['viewport']);
34
- });
35
-
36
- it('preserves explicit preload', () => {
30
+ it.each([
31
+ { expected: 'intent', preload: 'intent' },
32
+ { expected: false, preload: false },
33
+ ])('preserves an explicit $expected preload override', ({
34
+ expected,
35
+ preload,
36
+ }) => {
37
37
  render(
38
- <Link to="/settings" prefetch="render" preload="intent">
38
+ <Link to="/settings" prefetch="render" preload={preload}>
39
39
  Settings
40
40
  </Link>,
41
41
  );
42
42
 
43
- expect(capturedOptions.map(o => o.preload)).toEqual(['intent']);
43
+ expect(capturedOptions[0]?.preload).toBe(expected);
44
44
  });
45
45
 
46
- it('preserves explicit disabled preload', () => {
46
+ it('maps none prefetch to disabled TanStack preload', () => {
47
47
  render(
48
- <Link to="/settings" prefetch="render" preload={false}>
48
+ <Link to="/settings" prefetch="none">
49
49
  Settings
50
50
  </Link>,
51
51
  );
52
52
 
53
- expect(capturedOptions.map(o => o.preload)).toEqual([false]);
53
+ expect(capturedOptions[0]?.preload).toBe(false);
54
54
  });
55
55
 
56
- it('maps none prefetch to disabled TanStack preload', () => {
56
+ it('forwards a supported prefetch mode to TanStack preload', () => {
57
57
  render(
58
- <Link to="/settings" prefetch="none">
58
+ <Link to="/settings" prefetch="intent">
59
59
  Settings
60
60
  </Link>,
61
61
  );
62
62
 
63
- expect(capturedOptions.map(o => o.preload)).toEqual([false]);
63
+ expect(capturedOptions[0]?.preload).toBe('intent');
64
64
  });
65
65
 
66
66
  it.each([
67
- 'intent',
68
- 'render',
69
- 'viewport',
70
- ] as const)('maps %s prefetch to TanStack preload', prefetch => {
67
+ Link,
68
+ NavLink,
69
+ ])('lets explicit preload re-enable prefetch=none', Component => {
71
70
  render(
72
- <Link to="/settings" prefetch={prefetch}>
71
+ <Component to="/settings" prefetch="none" preload="render">
73
72
  Settings
74
- </Link>,
73
+ </Component>,
75
74
  );
76
75
 
77
- expect(capturedOptions.map(o => o.preload)).toEqual([prefetch]);
78
- });
79
-
80
- it('defaults NavLink preload to viewport', () => {
81
- render(<NavLink to="/settings">Settings</NavLink>);
82
-
83
- expect(capturedOptions.map(o => o.preload)).toEqual(['viewport']);
76
+ expect(capturedOptions.map(o => o.preload)).toEqual(['render']);
84
77
  });
85
78
  });
@@ -1,113 +1,49 @@
1
- const calls: Record<string, unknown[]> = {
2
- useChildMatches: [],
3
- useMatch: [],
4
- useMatches: [],
5
- useParentMatches: [],
6
- useRouterState: [],
7
- };
8
-
9
- rstest.mock('@tanstack/react-router', () => ({
10
- useChildMatches: (options: unknown) => {
11
- calls.useChildMatches.push(options);
12
- return options;
13
- },
14
- useMatch: (options: unknown) => {
15
- calls.useMatch.push(options);
16
- return options;
17
- },
18
- useMatches: (options: unknown) => {
19
- calls.useMatches.push(options);
20
- return options;
21
- },
22
- useParentMatches: (options: unknown) => {
23
- calls.useParentMatches.push(options);
24
- return options;
25
- },
26
- useRouterState: (options: unknown) => {
27
- calls.useRouterState.push(options);
28
- return options;
29
- },
30
- }));
31
-
1
+ import { createMemoryHistory } from '@tanstack/history';
32
2
  import {
33
- useChildMatches,
34
- useMatch,
35
- useMatches,
36
- useParentMatches,
37
- useRouterState,
38
- } from '../../src/runtime/routeHooks';
39
-
40
- const getLastCall = (hookName: keyof typeof calls) => {
41
- const callList = calls[hookName];
42
- return callList[callList.length - 1];
43
- };
44
-
45
- describe('tanstack route hook structural sharing guards', () => {
46
- beforeEach(() => {
47
- for (const hookName of Object.keys(calls)) {
48
- calls[hookName] = [];
49
- }
3
+ createRootRoute,
4
+ createRoute,
5
+ createRouter,
6
+ RouterProvider,
7
+ } from '@tanstack/react-router';
8
+ import React from 'react';
9
+ import { renderToString } from 'react-dom/server';
10
+ import { useMatches } from '../../src/runtime/routeHooks';
11
+
12
+ function MatchIds() {
13
+ const matches = useMatches();
14
+ return React.createElement(
15
+ 'output',
16
+ { 'data-testid': 'route-ids' },
17
+ matches.map(match => match.routeId).join(','),
18
+ );
19
+ }
20
+
21
+ test('updates whole match objects after navigation with the structural-sharing guard', async () => {
22
+ const rootRoute = createRootRoute({ component: MatchIds });
23
+ const homeRoute = createRoute({
24
+ getParentRoute: () => rootRoute,
25
+ path: '/',
26
+ component: () => null,
50
27
  });
51
-
52
- test('disables structural sharing for full internal router objects', () => {
53
- useMatch({ from: '/' } as never);
54
- useMatches();
55
- useParentMatches();
56
- useChildMatches();
57
- useRouterState();
58
-
59
- expect(getLastCall('useMatch')).toEqual({
60
- from: '/',
61
- structuralSharing: false,
62
- });
63
- expect(getLastCall('useMatches')).toEqual({
64
- structuralSharing: false,
65
- });
66
- expect(getLastCall('useParentMatches')).toEqual({
67
- structuralSharing: false,
68
- });
69
- expect(getLastCall('useChildMatches')).toEqual({
70
- structuralSharing: false,
71
- });
72
- expect(getLastCall('useRouterState')).toEqual({
73
- structuralSharing: false,
74
- });
28
+ const settingsRoute = createRoute({
29
+ getParentRoute: () => rootRoute,
30
+ path: '/settings',
31
+ component: () => null,
75
32
  });
76
-
77
- test('keeps selected data on the router default', () => {
78
- const select = (value: unknown) => value;
79
- const matchOptions = { from: '/', select };
80
- const matchesOptions = { select };
81
- const stateOptions = { select };
82
-
83
- useMatch(matchOptions as never);
84
- useMatches(matchesOptions as never);
85
- useParentMatches(matchesOptions as never);
86
- useChildMatches(matchesOptions as never);
87
- useRouterState(stateOptions as never);
88
-
89
- expect(getLastCall('useMatch')).toBe(matchOptions);
90
- expect(getLastCall('useMatches')).toBe(matchesOptions);
91
- expect(getLastCall('useParentMatches')).toBe(matchesOptions);
92
- expect(getLastCall('useChildMatches')).toBe(matchesOptions);
93
- expect(getLastCall('useRouterState')).toBe(stateOptions);
33
+ const router = createRouter({
34
+ routeTree: rootRoute.addChildren([homeRoute, settingsRoute]),
35
+ history: createMemoryHistory({ initialEntries: ['/'] }),
36
+ defaultStructuralSharing: true,
94
37
  });
95
38
 
96
- test('honors explicit structural sharing choices', () => {
97
- const enabled = { structuralSharing: true };
98
- const disabled = { structuralSharing: false };
99
- const matchEnabled = { from: '/', structuralSharing: true };
39
+ await router.load();
40
+ expect(
41
+ renderToString(React.createElement(RouterProvider, { router })),
42
+ ).toContain('__root__');
100
43
 
101
- useMatch(matchEnabled as never);
102
- useMatches(enabled as never);
103
- useParentMatches(disabled as never);
104
- useChildMatches(enabled as never);
105
- useRouterState(disabled as never);
44
+ await router.navigate({ to: '/settings' });
106
45
 
107
- expect(getLastCall('useMatch')).toBe(matchEnabled);
108
- expect(getLastCall('useMatches')).toBe(enabled);
109
- expect(getLastCall('useParentMatches')).toBe(disabled);
110
- expect(getLastCall('useChildMatches')).toBe(enabled);
111
- expect(getLastCall('useRouterState')).toBe(disabled);
112
- });
46
+ expect(
47
+ renderToString(React.createElement(RouterProvider, { router })),
48
+ ).toContain('/settings');
113
49
  });