@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
@@ -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,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
  });