@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.
- package/dist/cjs/cli/index.js +9 -0
- package/dist/cjs/runtime/lifecycle.js +7 -7
- package/dist/cjs/runtime/prefetchLink.js +2 -7
- package/dist/cjs/runtime/register.js +0 -2
- package/dist/cjs/runtime/router.js +25 -9
- package/dist/cjs/runtime/state.js +2 -2
- package/dist/esm/cli/index.mjs +9 -0
- package/dist/esm/runtime/lifecycle.mjs +1 -1
- package/dist/esm/runtime/prefetchLink.mjs +1 -6
- package/dist/esm/runtime/register.mjs +0 -2
- package/dist/esm/runtime/router.mjs +27 -11
- package/dist/esm/runtime/state.mjs +1 -1
- package/dist/esm-node/cli/index.mjs +9 -0
- package/dist/esm-node/runtime/lifecycle.mjs +1 -1
- package/dist/esm-node/runtime/prefetchLink.mjs +1 -6
- package/dist/esm-node/runtime/register.mjs +0 -2
- package/dist/esm-node/runtime/router.mjs +27 -11
- package/dist/esm-node/runtime/state.mjs +1 -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 +12 -10
- package/rstest.config.mts +0 -21
- package/src/cli/index.ts +10 -0
- package/src/runtime/lifecycle.ts +14 -3
- package/src/runtime/plugin.node.tsx +1 -1
- package/src/runtime/prefetchLink.tsx +1 -23
- package/src/runtime/register.ts +1 -12
- package/src/runtime/router.ts +25 -40
- package/src/runtime/state.ts +1 -1
- package/src/runtime/types.ts +2 -2
- package/tests/realm-isolation.test.ts +7 -17
- package/tests/router/cli.test.ts +105 -8
- package/tests/router/dataMutation.test.tsx +17 -121
- package/tests/router/flightSerialization.roundtrip.test.tsx +9 -6
- package/tests/router/generateRouteArtifacts.test.ts +69 -29
- package/tests/router/hooks.test.ts +1 -18
- package/tests/router/loaderBridge.test.ts +0 -77
- package/tests/router/prefetchLink.test.tsx +28 -16
- package/tests/router/prefetchLinkPreload.test.tsx +21 -28
- package/tests/router/routeHooks.test.ts +41 -105
- package/tests/router/routeTree.test.ts +0 -212
- package/tests/router/rsc.test.tsx +118 -136
- package/tests/router/rscDecoderSecurity.test.ts +24 -53
- package/tests/router/tanstackTypes.test.ts +24 -364
- package/tests/router/fastDefaults.test.ts +0 -32
- package/tests/router/fixtures/legacyRuntimeRouter.ts +0 -10
- 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/rscPayloadRouterMatrix.test.ts +0 -197
|
@@ -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
|
>;
|
|
64
|
-
expect(runOptionsArg
|
|
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
|
|
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
|
|
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('
|
|
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',
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
122
|
-
isActive ? 'Active settings' : 'Settings',
|
|
123
|
+
linkProps: { ref, prefetch: 'none' },
|
|
123
124
|
});
|
|
124
|
-
expect(
|
|
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(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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=
|
|
38
|
+
<Link to="/settings" prefetch="render" preload={preload}>
|
|
39
39
|
Settings
|
|
40
40
|
</Link>,
|
|
41
41
|
);
|
|
42
42
|
|
|
43
|
-
expect(capturedOptions
|
|
43
|
+
expect(capturedOptions[0]?.preload).toBe(expected);
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
it('
|
|
46
|
+
it('maps none prefetch to disabled TanStack preload', () => {
|
|
47
47
|
render(
|
|
48
|
-
<Link to="/settings" prefetch="
|
|
48
|
+
<Link to="/settings" prefetch="none">
|
|
49
49
|
Settings
|
|
50
50
|
</Link>,
|
|
51
51
|
);
|
|
52
52
|
|
|
53
|
-
expect(capturedOptions
|
|
53
|
+
expect(capturedOptions[0]?.preload).toBe(false);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
it('
|
|
56
|
+
it('forwards a supported prefetch mode to TanStack preload', () => {
|
|
57
57
|
render(
|
|
58
|
-
<Link to="/settings" prefetch="
|
|
58
|
+
<Link to="/settings" prefetch="intent">
|
|
59
59
|
Settings
|
|
60
60
|
</Link>,
|
|
61
61
|
);
|
|
62
62
|
|
|
63
|
-
expect(capturedOptions
|
|
63
|
+
expect(capturedOptions[0]?.preload).toBe('intent');
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
it.each([
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
<
|
|
71
|
+
<Component to="/settings" prefetch="none" preload="render">
|
|
73
72
|
Settings
|
|
74
|
-
</
|
|
73
|
+
</Component>,
|
|
75
74
|
);
|
|
76
75
|
|
|
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']);
|
|
76
|
+
expect(capturedOptions.map(o => o.preload)).toEqual(['render']);
|
|
84
77
|
});
|
|
85
78
|
});
|
|
@@ -1,113 +1,49 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
39
|
+
await router.load();
|
|
40
|
+
expect(
|
|
41
|
+
renderToString(React.createElement(RouterProvider, { router })),
|
|
42
|
+
).toContain('__root__');
|
|
100
43
|
|
|
101
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
});
|