@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.1 → 3.9.0-ultramodern.10
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/dist/cjs/cli/index.js +9 -0
- package/dist/cjs/cli/tanstackTypes/source.js +3 -1
- package/dist/cjs/runtime/basepathRewrite.js +13 -6
- package/dist/cjs/runtime/lifecycle.js +7 -7
- package/dist/cjs/runtime/plugin.js +1 -1
- package/dist/cjs/runtime/plugin.node.js +5 -1
- package/dist/cjs/runtime/prefetchLink.js +2 -7
- package/dist/cjs/runtime/register.js +0 -2
- package/dist/cjs/runtime/routeTree/construction.js +2 -0
- package/dist/cjs/runtime/router.js +25 -9
- package/dist/cjs/runtime/state.js +2 -2
- package/dist/cjs/runtime/utils.js +2 -0
- package/dist/esm/cli/index.mjs +9 -0
- package/dist/esm/cli/tanstackTypes/source.mjs +3 -1
- package/dist/esm/runtime/basepathRewrite.mjs +10 -3
- package/dist/esm/runtime/lifecycle.mjs +1 -1
- package/dist/esm/runtime/plugin.mjs +1 -1
- package/dist/esm/runtime/plugin.node.mjs +5 -1
- package/dist/esm/runtime/prefetchLink.mjs +1 -6
- package/dist/esm/runtime/register.mjs +0 -2
- package/dist/esm/runtime/routeTree/construction.mjs +2 -0
- package/dist/esm/runtime/router.mjs +27 -11
- package/dist/esm/runtime/state.mjs +1 -1
- package/dist/esm/runtime/utils.mjs +2 -0
- package/dist/esm-node/cli/index.mjs +9 -0
- package/dist/esm-node/cli/tanstackTypes/source.mjs +3 -1
- package/dist/esm-node/runtime/basepathRewrite.mjs +10 -3
- package/dist/esm-node/runtime/lifecycle.mjs +1 -1
- package/dist/esm-node/runtime/plugin.mjs +1 -1
- package/dist/esm-node/runtime/plugin.node.mjs +5 -1
- package/dist/esm-node/runtime/prefetchLink.mjs +1 -6
- package/dist/esm-node/runtime/register.mjs +0 -2
- package/dist/esm-node/runtime/routeTree/construction.mjs +2 -0
- package/dist/esm-node/runtime/router.mjs +27 -11
- package/dist/esm-node/runtime/state.mjs +1 -1
- package/dist/esm-node/runtime/utils.mjs +2 -0
- package/dist/types/runtime/basepathRewrite.d.ts +2 -1
- package/dist/types/runtime/lifecycle.d.ts +9 -2
- package/dist/types/runtime/register.d.ts +1 -9
- package/dist/types/runtime/router.d.ts +2 -13
- package/dist/types/runtime/state.d.ts +1 -1
- package/dist/types/runtime/types.d.ts +2 -1
- package/package.json +18 -15
- package/rstest.config.mts +1 -24
- package/src/cli/index.ts +10 -0
- package/src/cli/tanstackTypes/source.ts +3 -1
- package/src/runtime/basepathRewrite.ts +11 -2
- package/src/runtime/lifecycle.ts +14 -3
- package/src/runtime/plugin.node.tsx +9 -2
- package/src/runtime/plugin.tsx +5 -1
- package/src/runtime/prefetchLink.tsx +1 -23
- package/src/runtime/register.ts +1 -12
- package/src/runtime/routeTree/construction.ts +2 -0
- package/src/runtime/router.ts +25 -40
- package/src/runtime/state.ts +1 -1
- package/src/runtime/types.ts +2 -2
- package/src/runtime/utils.tsx +3 -1
- package/tests/router/cli.test.ts +148 -824
- package/tests/router/dataMutation.test.tsx +17 -85
- package/tests/router/flightSerialization.roundtrip.test.tsx +9 -6
- package/tests/router/generateRouteArtifacts.test.ts +68 -28
- package/tests/router/loaderBridge.test.ts +0 -25
- package/tests/router/localisedIdentity.test.tsx +95 -0
- package/tests/router/prefetchLink.test.tsx +17 -21
- package/tests/router/prefetchLinkPreload.test.tsx +11 -31
- package/tests/router/routeHooks.test.ts +41 -105
- package/tests/router/routeTree.test.ts +144 -836
- package/tests/router/rsc.test.tsx +131 -166
- package/tests/router/serverPlugin.test.ts +43 -1
- package/tests/router/tanstackTypes.test.ts +109 -973
- package/tests/realm-isolation.test.ts +0 -114
- package/tests/router/fastDefaults.test.ts +0 -32
- package/tests/router/fixtures/legacyRuntimeRouter.ts +0 -10
- package/tests/router/fixtures/rscDecoderSecurityChild.cjs +0 -162
- package/tests/router/hooks.test.ts +0 -40
- package/tests/router/hydrationBoundary.test.tsx +0 -23
- package/tests/router/packageSurface.test.ts +0 -24
- package/tests/router/preloadRedirect.test.ts +0 -64
- package/tests/router/register.test.ts +0 -8
- package/tests/router/routerCompatibility.test.ts +0 -239
- package/tests/router/rscDecoderSecurity.test.ts +0 -113
- package/tests/router/rscPayloadRouterMatrix.test.ts +0 -197
|
@@ -105,54 +105,14 @@ describe('tanstack data mutation fetcher', () => {
|
|
|
105
105
|
states.length = 0;
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
-
test(
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
174
|
-
{
|
|
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
|
|
148
|
+
new Response(JSON.stringify({ count }), {
|
|
186
149
|
headers: {
|
|
187
150
|
'Content-Type': 'application/json',
|
|
188
151
|
},
|
|
@@ -200,7 +163,9 @@ 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(
|
|
166
|
+
expect(screen.getByTestId('data').textContent).toBe(
|
|
167
|
+
JSON.stringify({ count }),
|
|
168
|
+
);
|
|
204
169
|
expect(states).toEqual(['idle', 'submitting', 'loading', 'idle']);
|
|
205
170
|
});
|
|
206
171
|
|
|
@@ -460,37 +425,4 @@ describe('tanstack data mutation fetcher', () => {
|
|
|
460
425
|
globalThis.FormData = NativeFormData;
|
|
461
426
|
}
|
|
462
427
|
});
|
|
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
428
|
});
|
|
@@ -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);
|
|
@@ -1,19 +1,11 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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 (
|
|
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 {
|
|
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('
|
|
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
|
>;
|
|
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.
|
|
64
107
|
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
108
|
expect(runOptionsArg.configFile).toBe(
|
|
73
109
|
path.resolve(appDirectory, 'modern.config'),
|
|
74
110
|
);
|
|
75
111
|
|
|
76
112
|
expect(cliInitMock).toHaveBeenCalledTimes(1);
|
|
77
|
-
const initArg = cliInitMock.mock.calls[0][0] as Record<string, unknown>;
|
|
78
|
-
expect(initArg.command).toBe('build');
|
|
79
|
-
expect(initArg.mockRunOptions).toBe(true);
|
|
80
113
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
114
|
+
expect(
|
|
115
|
+
fs.existsSync(
|
|
116
|
+
path.join(srcDirectory, 'modern-tanstack', 'main', 'router.gen.ts'),
|
|
117
|
+
),
|
|
118
|
+
).toBe(true);
|
|
119
|
+
const registerArtifact = fs.readFileSync(
|
|
120
|
+
path.join(srcDirectory, 'modern-tanstack', 'register.gen.d.ts'),
|
|
121
|
+
'utf-8',
|
|
122
|
+
);
|
|
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
|
|
134
|
+
it('throws a clear error when no config file exists', async () => {
|
|
95
135
|
const appDirectory = makeAppDir(false);
|
|
96
136
|
|
|
97
137
|
try {
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { isNotFound, isRedirect } from '@tanstack/react-router';
|
|
2
2
|
import {
|
|
3
|
-
createRouteStaticData,
|
|
4
|
-
isAbsoluteUrl,
|
|
5
3
|
mapSplatParamsForModernLoader,
|
|
6
4
|
modernLoaderToTanstack,
|
|
7
5
|
throwTanstackRedirect,
|
|
@@ -53,15 +51,6 @@ describe('throwTanstackRedirect', () => {
|
|
|
53
51
|
});
|
|
54
52
|
});
|
|
55
53
|
|
|
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
54
|
describe('mapSplatParamsForModernLoader', () => {
|
|
66
55
|
test('maps TanStack _splat to React Router * only for splat routes', () => {
|
|
67
56
|
expect(
|
|
@@ -76,20 +65,6 @@ describe('mapSplatParamsForModernLoader', () => {
|
|
|
76
65
|
});
|
|
77
66
|
});
|
|
78
67
|
|
|
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
68
|
describe('modernLoaderToTanstack', () => {
|
|
94
69
|
const baseCtx = {
|
|
95
70
|
location: { href: 'http://localhost/products/1' },
|
|
@@ -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,15 +74,10 @@ 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', () => {
|
|
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',
|
|
@@ -100,27 +95,28 @@ describe('tanstack prefetch link adapter - aria-current override', () => {
|
|
|
100
95
|
expect(anchor.hasAttribute('aria-current')).toBe(false);
|
|
101
96
|
});
|
|
102
97
|
|
|
103
|
-
it('
|
|
104
|
-
const { anchor } = await renderLink({
|
|
105
|
-
|
|
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',
|
|
98
|
+
it('updates render-prop children across an inactive-to-active transition', async () => {
|
|
99
|
+
const { anchor, router } = await renderLink({
|
|
100
|
+
initialPath: '/',
|
|
112
101
|
children: ({ isActive }: { isActive: boolean }) =>
|
|
113
102
|
isActive ? 'Active settings' : 'Settings',
|
|
114
103
|
});
|
|
115
|
-
|
|
104
|
+
|
|
105
|
+
expect(anchor.textContent).toBe('Settings');
|
|
106
|
+
|
|
107
|
+
await act(async () => {
|
|
108
|
+
await router.navigate({ to: '/settings' });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await waitFor(() => expect(anchor.textContent).toBe('Active settings'));
|
|
116
112
|
});
|
|
117
113
|
|
|
118
|
-
it('
|
|
114
|
+
it('passes the native anchor ref through the router hook', async () => {
|
|
115
|
+
const ref = React.createRef<HTMLAnchorElement>();
|
|
119
116
|
const { anchor } = await renderLink({
|
|
120
117
|
initialPath: '/',
|
|
121
|
-
|
|
122
|
-
isActive ? 'Active settings' : 'Settings',
|
|
118
|
+
linkProps: { ref, prefetch: 'none' },
|
|
123
119
|
});
|
|
124
|
-
expect(
|
|
120
|
+
expect(ref.current).toBe(anchor);
|
|
125
121
|
});
|
|
126
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,30 +27,20 @@ describe('tanstack prefetch link adapter - preload mapping', () => {
|
|
|
27
27
|
mockReturnProps = { href: '/settings' };
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
it('
|
|
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('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
|
|
37
|
+
expect(capturedOptions[0]?.preload).toBe(false);
|
|
44
38
|
});
|
|
45
39
|
|
|
46
|
-
it('
|
|
47
|
-
render(
|
|
48
|
-
<Link to="/settings" prefetch="render" preload={false}>
|
|
49
|
-
Settings
|
|
50
|
-
</Link>,
|
|
51
|
-
);
|
|
40
|
+
it('defaults to viewport preload when no prefetch is given', () => {
|
|
41
|
+
render(<Link to="/settings">Settings</Link>);
|
|
52
42
|
|
|
53
|
-
expect(capturedOptions
|
|
43
|
+
expect(capturedOptions[0]?.preload).toBe('viewport');
|
|
54
44
|
});
|
|
55
45
|
|
|
56
46
|
it('maps none prefetch to disabled TanStack preload', () => {
|
|
@@ -60,26 +50,16 @@ describe('tanstack prefetch link adapter - preload mapping', () => {
|
|
|
60
50
|
</Link>,
|
|
61
51
|
);
|
|
62
52
|
|
|
63
|
-
expect(capturedOptions
|
|
53
|
+
expect(capturedOptions[0]?.preload).toBe(false);
|
|
64
54
|
});
|
|
65
55
|
|
|
66
|
-
it
|
|
67
|
-
'intent',
|
|
68
|
-
'render',
|
|
69
|
-
'viewport',
|
|
70
|
-
] as const)('maps %s prefetch to TanStack preload', prefetch => {
|
|
56
|
+
it('lets explicit preload re-enable prefetch=none', () => {
|
|
71
57
|
render(
|
|
72
|
-
<Link to="/settings" prefetch=
|
|
58
|
+
<Link to="/settings" prefetch="none" preload="render">
|
|
73
59
|
Settings
|
|
74
60
|
</Link>,
|
|
75
61
|
);
|
|
76
62
|
|
|
77
|
-
expect(capturedOptions.map(o => o.preload)).toEqual([
|
|
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']);
|
|
63
|
+
expect(capturedOptions.map(o => o.preload)).toEqual(['render']);
|
|
84
64
|
});
|
|
85
65
|
});
|