@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
@@ -0,0 +1,95 @@
1
+ import { applyLocalisedUrlsToRoutes } from '@modern-js/i18n-runtime-extensions';
2
+ import {
3
+ createMemoryHistory,
4
+ createRouter,
5
+ Outlet,
6
+ RouterProvider,
7
+ useLoaderData,
8
+ useParams,
9
+ } from '@tanstack/react-router';
10
+ import { createElement } from 'react';
11
+ import { renderToString } from 'react-dom/server';
12
+ import { createModernBasepathRewrite } from '../../src/runtime/basepathRewrite';
13
+ import { createRouteTreeFromRouteObjects } from '../../src/runtime/routeTree';
14
+ import { createTanstackRouteObjectsFromConfig } from '../../src/runtime/utils';
15
+
16
+ function SearchPage() {
17
+ const data = useLoaderData({ from: '/$lang/search' }) as { language: string };
18
+ return createElement('p', null, `search:${data.language}`);
19
+ }
20
+
21
+ function ResourcePage() {
22
+ const params = useParams({ from: '/$lang/resources/$id' }) as { id: string };
23
+ return createElement('p', null, `resource:${params.id}`);
24
+ }
25
+
26
+ test('localized URLs preserve native loader and params route identity in SSR and navigation', async () => {
27
+ const routes = applyLocalisedUrlsToRoutes(
28
+ [
29
+ {
30
+ type: 'nested',
31
+ id: 'lang',
32
+ path: ':lang',
33
+ component: Outlet,
34
+ children: [
35
+ {
36
+ type: 'nested',
37
+ id: 'search',
38
+ path: 'search',
39
+ component: SearchPage,
40
+ loader: ({ params }: { params: { lang: string } }) => ({
41
+ language: params.lang,
42
+ }),
43
+ },
44
+ {
45
+ type: 'nested',
46
+ id: 'resource',
47
+ path: 'resources/:id',
48
+ component: ResourcePage,
49
+ },
50
+ ],
51
+ },
52
+ ],
53
+ ['en', 'cs'],
54
+ {
55
+ '/search': { en: '/find', cs: '/hledat' },
56
+ '/resources/:id': { en: '/resources/:id', cs: '/zdroje/:id' },
57
+ },
58
+ );
59
+ const routeObjects = createTanstackRouteObjectsFromConfig({
60
+ routesConfig: { routes },
61
+ })!;
62
+ const history = createMemoryHistory({
63
+ initialEntries: ['/base/cs/hledat?q=tractor#results'],
64
+ });
65
+ const router = createRouter({
66
+ context: {
67
+ request: new Request(
68
+ 'https://example.test/base/cs/hledat?q=tractor#results',
69
+ ),
70
+ },
71
+ routeTree: createRouteTreeFromRouteObjects(routeObjects),
72
+ history,
73
+ rewrite: createModernBasepathRewrite('/base', false, routeObjects),
74
+ });
75
+ await router.load();
76
+ expect(router.state.matches.at(-1)?.routeId).toBe('/$lang/search');
77
+ expect(renderToString(createElement(RouterProvider, { router }))).toContain(
78
+ 'search:cs',
79
+ );
80
+ expect(history.location.href).toBe('/base/cs/hledat?q=tractor#results');
81
+
82
+ await router.navigate({ to: '/en/search', search: { q: 'tractor' } });
83
+ expect(history.location.href).toBe('/base/en/find?q=tractor');
84
+ expect(router.state.matches.at(-1)?.routeId).toBe('/$lang/search');
85
+ expect(renderToString(createElement(RouterProvider, { router }))).toContain(
86
+ 'search:en',
87
+ );
88
+
89
+ await router.navigate({ to: '/cs/zdroje/a%2Fb' });
90
+ expect(history.location.href).toBe('/base/cs/zdroje/a%2Fb');
91
+ expect(router.state.matches.at(-1)?.routeId).toBe('/$lang/resources/$id');
92
+ expect(renderToString(createElement(RouterProvider, { router }))).toContain(
93
+ 'resource:a/b',
94
+ );
95
+ });
@@ -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,4 +1,4 @@
1
- import { resolveRouterProvider } from '@modern-js/runtime/context';
1
+ import { resolveRouterProvider } from '@modern-js/runtime-extensions/router-provider';
2
2
  import '../../src/runtime/register';
3
3
 
4
4
  describe("'@modern-js/plugin-tanstack/runtime' import side effects", () => {
@@ -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
  });