@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.6 → 3.9.0-ultramodern.7
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.
- package/package.json +12 -12
- package/rstest.config.mts +0 -3
- package/tests/router/cli.test.ts +85 -860
- package/tests/router/dataMutation.test.tsx +36 -0
- package/tests/router/generateRouteArtifacts.test.ts +11 -11
- package/tests/router/loaderBridge.test.ts +52 -0
- package/tests/router/prefetchLink.test.tsx +0 -16
- package/tests/router/prefetchLinkPreload.test.tsx +12 -25
- package/tests/router/routeTree.test.ts +144 -624
- package/tests/router/rsc.test.tsx +14 -31
- package/tests/router/tanstackTypes.test.ts +92 -616
- package/tests/realm-isolation.test.ts +0 -104
- package/tests/router/fixtures/rscDecoderSecurityChild.cjs +0 -162
- package/tests/router/hooks.test.ts +0 -23
- package/tests/router/rscDecoderSecurity.test.ts +0 -84
|
@@ -169,6 +169,42 @@ describe('tanstack data mutation fetcher', () => {
|
|
|
169
169
|
expect(states).toEqual(['idle', 'submitting', 'loading', 'idle']);
|
|
170
170
|
});
|
|
171
171
|
|
|
172
|
+
test('passes built href and matched route params to mutation actions', async () => {
|
|
173
|
+
const action = rstest.fn(
|
|
174
|
+
async ({ request, params }: Parameters<RouteHandler>[0]) => ({
|
|
175
|
+
params,
|
|
176
|
+
requestUrl: request.url,
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
currentRouter = createRouter({
|
|
180
|
+
action,
|
|
181
|
+
params: { userId: '42' },
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
render(<FetcherHarness />);
|
|
185
|
+
|
|
186
|
+
await act(async () => {
|
|
187
|
+
await latestFetcher!.submit(
|
|
188
|
+
{ amount: 2 },
|
|
189
|
+
{
|
|
190
|
+
method: 'post',
|
|
191
|
+
action: '/users/42?tab=edit#details',
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(action).toHaveBeenCalledTimes(1);
|
|
197
|
+
const actionArgs = action.mock.calls[0][0];
|
|
198
|
+
const expectedRequestUrl = `${window.location.origin}/users/42?tab=edit#details`;
|
|
199
|
+
expect(actionArgs.params).toEqual({ userId: '42' });
|
|
200
|
+
expect(actionArgs.request.url).toBe(expectedRequestUrl);
|
|
201
|
+
expect(latestFetcher?.data).toEqual({
|
|
202
|
+
params: { userId: '42' },
|
|
203
|
+
requestUrl: expectedRequestUrl,
|
|
204
|
+
});
|
|
205
|
+
expect(currentRouter.invalidate).toHaveBeenCalledTimes(1);
|
|
206
|
+
});
|
|
207
|
+
|
|
172
208
|
test('surfaces non-2xx action responses as fetcher errors', async () => {
|
|
173
209
|
const actionResult = createDeferred<Response>();
|
|
174
210
|
currentRouter = createRouter({
|
|
@@ -102,24 +102,24 @@ describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
|
|
|
102
102
|
string,
|
|
103
103
|
unknown
|
|
104
104
|
>;
|
|
105
|
-
|
|
105
|
+
// The headless entry must resolve cwd/configFile from the target app
|
|
106
|
+
// directory, not the process's own cwd — that was the actual bug.
|
|
107
|
+
expect(runOptionsArg.cwd).toBe(appDirectory);
|
|
108
|
+
expect(runOptionsArg.configFile).toBe(
|
|
109
|
+
path.resolve(appDirectory, 'modern.config'),
|
|
110
|
+
);
|
|
106
111
|
|
|
107
112
|
expect(cliInitMock).toHaveBeenCalledTimes(1);
|
|
108
|
-
const initArg = cliInitMock.mock.calls[0][0] as Record<string, unknown>;
|
|
109
|
-
expect(initArg.command).toBe('build');
|
|
110
|
-
expect(initArg.mockRunOptions).toBe(true);
|
|
111
|
-
expect(process.env.MODERN_ARGV).toBe('node modern build');
|
|
112
113
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
expect(
|
|
115
|
+
fs.existsSync(
|
|
116
|
+
path.join(srcDirectory, 'modern-tanstack', 'main', 'router.gen.ts'),
|
|
117
|
+
),
|
|
118
|
+
).toBe(true);
|
|
117
119
|
const registerArtifact = fs.readFileSync(
|
|
118
120
|
path.join(srcDirectory, 'modern-tanstack', 'register.gen.d.ts'),
|
|
119
121
|
'utf-8',
|
|
120
122
|
);
|
|
121
|
-
expect(routerArtifact).toContain('createRootRouteWithContext');
|
|
122
|
-
expect(routerArtifact).toContain('routeTree');
|
|
123
123
|
expect(registerArtifact).toContain('./main/router.gen');
|
|
124
124
|
} finally {
|
|
125
125
|
fs.rmSync(appDirectory, { force: true, recursive: true });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isNotFound, isRedirect } from '@tanstack/react-router';
|
|
2
2
|
import {
|
|
3
|
+
mapSplatParamsForModernLoader,
|
|
3
4
|
modernLoaderToTanstack,
|
|
4
5
|
throwTanstackRedirect,
|
|
5
6
|
} from '../../src/runtime/loaderBridge';
|
|
@@ -20,6 +21,50 @@ function catchThrown(fn: () => unknown): unknown {
|
|
|
20
21
|
throw new Error('expected the function to throw');
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
describe('throwTanstackRedirect', () => {
|
|
25
|
+
test('absolute URLs redirect via href (external), not via to', () => {
|
|
26
|
+
// The old inline codegen handler threw `redirect({ href })` INSIDE a
|
|
27
|
+
// try block whose catch replaced it with `redirect({ to: absoluteUrl })`,
|
|
28
|
+
// making TanStack treat the absolute URL as an internal path.
|
|
29
|
+
const thrown = catchThrown(() =>
|
|
30
|
+
throwTanstackRedirect('https://example.com/external'),
|
|
31
|
+
) as RedirectLike;
|
|
32
|
+
|
|
33
|
+
expect(isRedirect(thrown)).toBe(true);
|
|
34
|
+
expect(thrown.options?.href).toBe('https://example.com/external');
|
|
35
|
+
expect(thrown.options?.to).toBeUndefined();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('relative paths redirect via to so the basepath rewrite applies', () => {
|
|
39
|
+
const thrown = catchThrown(() =>
|
|
40
|
+
throwTanstackRedirect('/dashboard'),
|
|
41
|
+
) as RedirectLike;
|
|
42
|
+
|
|
43
|
+
expect(isRedirect(thrown)).toBe(true);
|
|
44
|
+
expect(thrown.options?.to).toBe('/dashboard');
|
|
45
|
+
expect(thrown.options?.href).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('empty location falls back to /', () => {
|
|
49
|
+
const thrown = catchThrown(() => throwTanstackRedirect('')) as RedirectLike;
|
|
50
|
+
expect(thrown.options?.to).toBe('/');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('mapSplatParamsForModernLoader', () => {
|
|
55
|
+
test('maps TanStack _splat to React Router * only for splat routes', () => {
|
|
56
|
+
expect(
|
|
57
|
+
mapSplatParamsForModernLoader({ _splat: 'a/b', id: '1' }, true),
|
|
58
|
+
).toEqual({ '*': 'a/b', id: '1' });
|
|
59
|
+
expect(
|
|
60
|
+
mapSplatParamsForModernLoader({ _splat: 'a/b', id: '1' }, false),
|
|
61
|
+
).toEqual({ _splat: 'a/b', id: '1' });
|
|
62
|
+
expect(mapSplatParamsForModernLoader({ id: '1' }, true)).toEqual({
|
|
63
|
+
id: '1',
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
23
68
|
describe('modernLoaderToTanstack', () => {
|
|
24
69
|
const baseCtx = {
|
|
25
70
|
location: { href: 'http://localhost/products/1' },
|
|
@@ -119,6 +164,13 @@ describe('modernLoaderToTanstack', () => {
|
|
|
119
164
|
expect(isNotFound(thrown)).toBe(true);
|
|
120
165
|
});
|
|
121
166
|
|
|
167
|
+
test('preserves returned non-404 error Responses as loader results', async () => {
|
|
168
|
+
const response = new Response('loader exploded', { status: 500 });
|
|
169
|
+
const loader = modernLoaderToTanstack({ hasSplat: false }, () => response);
|
|
170
|
+
|
|
171
|
+
await expect(loader(baseCtx)).resolves.toBe(response);
|
|
172
|
+
});
|
|
173
|
+
|
|
122
174
|
test('translates redirect Responses thrown synchronously by the loader', () => {
|
|
123
175
|
const loader = modernLoaderToTanstack({ hasSplat: false }, () => {
|
|
124
176
|
throw new Response(null, {
|
|
@@ -78,11 +78,6 @@ async function renderLink(options: Parameters<typeof buildRouter>[0]) {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
describe('tanstack prefetch link adapter - aria-current override', () => {
|
|
81
|
-
it('defaults active link to aria-current="page" when caller passes nothing', async () => {
|
|
82
|
-
const { anchor } = await renderLink({ initialPath: '/settings' });
|
|
83
|
-
expect(anchor.getAttribute('aria-current')).toBe('page');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
81
|
it('lets caller aria-current="true" win over the TanStack-forced value', async () => {
|
|
87
82
|
const { anchor } = await renderLink({
|
|
88
83
|
initialPath: '/settings',
|
|
@@ -124,15 +119,4 @@ describe('tanstack prefetch link adapter - aria-current override', () => {
|
|
|
124
119
|
});
|
|
125
120
|
expect(ref.current).toBe(anchor);
|
|
126
121
|
});
|
|
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');
|
|
137
|
-
});
|
|
138
122
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { render } from '@testing-library/react';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { Link
|
|
3
|
+
import { Link } from '../../src/runtime/prefetchLink';
|
|
4
4
|
|
|
5
5
|
type CapturedOptions = {
|
|
6
6
|
preload?: unknown;
|
|
@@ -27,20 +27,20 @@ describe('tanstack prefetch link adapter - preload mapping', () => {
|
|
|
27
27
|
mockReturnProps = { href: '/settings' };
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
it
|
|
31
|
-
{ expected: 'intent', preload: 'intent' },
|
|
32
|
-
{ expected: false, preload: false },
|
|
33
|
-
])('preserves an explicit $expected preload override', ({
|
|
34
|
-
expected,
|
|
35
|
-
preload,
|
|
36
|
-
}) => {
|
|
30
|
+
it('preserves an explicit false preload override', () => {
|
|
37
31
|
render(
|
|
38
|
-
<Link to="/settings" prefetch="render" preload={
|
|
32
|
+
<Link to="/settings" prefetch="render" preload={false}>
|
|
39
33
|
Settings
|
|
40
34
|
</Link>,
|
|
41
35
|
);
|
|
42
36
|
|
|
43
|
-
expect(capturedOptions[0]?.preload).toBe(
|
|
37
|
+
expect(capturedOptions[0]?.preload).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('defaults to viewport preload when no prefetch is given', () => {
|
|
41
|
+
render(<Link to="/settings">Settings</Link>);
|
|
42
|
+
|
|
43
|
+
expect(capturedOptions[0]?.preload).toBe('viewport');
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
it('maps none prefetch to disabled TanStack preload', () => {
|
|
@@ -53,26 +53,13 @@ describe('tanstack prefetch link adapter - preload mapping', () => {
|
|
|
53
53
|
expect(capturedOptions[0]?.preload).toBe(false);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
it('
|
|
56
|
+
it('lets explicit preload re-enable prefetch=none', () => {
|
|
57
57
|
render(
|
|
58
|
-
<Link to="/settings" prefetch="
|
|
58
|
+
<Link to="/settings" prefetch="none" preload="render">
|
|
59
59
|
Settings
|
|
60
60
|
</Link>,
|
|
61
61
|
);
|
|
62
62
|
|
|
63
|
-
expect(capturedOptions[0]?.preload).toBe('intent');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it.each([
|
|
67
|
-
Link,
|
|
68
|
-
NavLink,
|
|
69
|
-
])('lets explicit preload re-enable prefetch=none', Component => {
|
|
70
|
-
render(
|
|
71
|
-
<Component to="/settings" prefetch="none" preload="render">
|
|
72
|
-
Settings
|
|
73
|
-
</Component>,
|
|
74
|
-
);
|
|
75
|
-
|
|
76
63
|
expect(capturedOptions.map(o => o.preload)).toEqual(['render']);
|
|
77
64
|
});
|
|
78
65
|
});
|