@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.2 → 3.9.0-ultramodern.21
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/artifacts.js +15 -1
- package/dist/cjs/cli/index.js +12 -0
- package/dist/cjs/cli/routeMetadata.js +131 -0
- package/dist/cjs/cli/tanstackTypes/canonicalRoutes.js +7 -7
- package/dist/cjs/cli/tanstackTypes/shared.js +0 -14
- package/dist/cjs/cli/tanstackTypes/source.js +27 -60
- package/dist/cjs/runtime/basepathRewrite.js +13 -6
- package/dist/cjs/runtime/lifecycle.js +6 -9
- package/dist/cjs/runtime/loaderBridge.js +10 -4
- package/dist/cjs/runtime/navigation.js +102 -0
- package/dist/cjs/runtime/plugin.js +7 -2
- package/dist/cjs/runtime/plugin.node.js +86 -64
- package/dist/cjs/runtime/prefetchLink.js +2 -7
- package/dist/cjs/runtime/register.js +0 -2
- package/dist/cjs/runtime/routeTree/construction.js +19 -59
- package/dist/cjs/runtime/routeTree/loaders.js +1 -1
- package/dist/cjs/runtime/routeTree/paths.js +0 -6
- package/dist/cjs/runtime/router.js +25 -9
- package/dist/cjs/runtime/ssrManagedTags.js +2 -12
- package/dist/cjs/runtime/state.js +2 -2
- package/dist/cjs/runtime/utils.js +3 -0
- package/dist/cjs/shared/routeDescriptor.js +60 -0
- package/dist/esm/cli/artifacts.mjs +13 -2
- package/dist/esm/cli/index.mjs +12 -9
- package/dist/esm/cli/routeMetadata.mjs +71 -0
- package/dist/esm/cli/tanstackTypes/canonicalRoutes.mjs +7 -7
- package/dist/esm/cli/tanstackTypes/shared.mjs +1 -9
- package/dist/esm/cli/tanstackTypes/source.mjs +28 -61
- package/dist/esm/runtime/basepathRewrite.mjs +10 -3
- package/dist/esm/runtime/lifecycle.mjs +1 -1
- package/dist/esm/runtime/loaderBridge.mjs +8 -5
- package/dist/esm/runtime/navigation.mjs +64 -0
- package/dist/esm/runtime/plugin.mjs +7 -2
- package/dist/esm/runtime/plugin.node.mjs +88 -66
- package/dist/esm/runtime/prefetchLink.mjs +1 -6
- package/dist/esm/runtime/register.mjs +0 -2
- package/dist/esm/runtime/routeTree/construction.mjs +19 -59
- package/dist/esm/runtime/routeTree/loaders.mjs +2 -2
- package/dist/esm/runtime/routeTree/paths.mjs +1 -4
- package/dist/esm/runtime/router.mjs +27 -11
- package/dist/esm/runtime/ssrManagedTags.mjs +1 -8
- package/dist/esm/runtime/state.mjs +1 -1
- package/dist/esm/runtime/utils.mjs +3 -0
- package/dist/esm/shared/routeDescriptor.mjs +22 -0
- package/dist/esm-node/cli/artifacts.mjs +13 -2
- package/dist/esm-node/cli/index.mjs +12 -9
- package/dist/esm-node/cli/routeMetadata.mjs +72 -0
- package/dist/esm-node/cli/tanstackTypes/canonicalRoutes.mjs +7 -7
- package/dist/esm-node/cli/tanstackTypes/shared.mjs +1 -9
- package/dist/esm-node/cli/tanstackTypes/source.mjs +28 -61
- package/dist/esm-node/runtime/basepathRewrite.mjs +10 -3
- package/dist/esm-node/runtime/lifecycle.mjs +1 -1
- package/dist/esm-node/runtime/loaderBridge.mjs +8 -5
- package/dist/esm-node/runtime/navigation.mjs +65 -0
- package/dist/esm-node/runtime/plugin.mjs +7 -2
- package/dist/esm-node/runtime/plugin.node.mjs +88 -66
- 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 +19 -59
- package/dist/esm-node/runtime/routeTree/loaders.mjs +2 -2
- package/dist/esm-node/runtime/routeTree/paths.mjs +1 -4
- package/dist/esm-node/runtime/router.mjs +27 -11
- package/dist/esm-node/runtime/ssrManagedTags.mjs +1 -8
- package/dist/esm-node/runtime/state.mjs +1 -1
- package/dist/esm-node/runtime/utils.mjs +3 -0
- package/dist/esm-node/shared/routeDescriptor.mjs +23 -0
- package/dist/types/cli/artifacts.d.ts +13 -5
- package/dist/types/cli/index.d.ts +1 -1
- package/dist/types/cli/routeMetadata.d.ts +18 -0
- package/dist/types/cli/tanstackTypes/shared.d.ts +0 -2
- package/dist/types/runtime/basepathRewrite.d.ts +2 -1
- package/dist/types/runtime/lifecycle.d.ts +9 -2
- package/dist/types/runtime/loaderBridge.d.ts +1 -0
- package/dist/types/runtime/navigation.d.ts +4 -0
- package/dist/types/runtime/register.d.ts +1 -9
- package/dist/types/runtime/routeTree/paths.d.ts +0 -2
- package/dist/types/runtime/router.d.ts +2 -13
- package/dist/types/runtime/ssrManagedTags.d.ts +0 -1
- package/dist/types/runtime/ssrTypes.d.ts +0 -7
- package/dist/types/runtime/state.d.ts +1 -1
- package/dist/types/runtime/types.d.ts +2 -1
- package/dist/types/shared/routeDescriptor.d.ts +26 -0
- package/package.json +22 -19
- package/rstest.config.mts +2 -24
- package/src/cli/artifacts.ts +53 -6
- package/src/cli/index.ts +11 -2
- package/src/cli/routeMetadata.ts +121 -0
- package/src/cli/tanstackTypes/canonicalRoutes.ts +17 -12
- package/src/cli/tanstackTypes/shared.ts +0 -14
- package/src/cli/tanstackTypes/source.ts +42 -116
- package/src/runtime/basepathRewrite.ts +11 -2
- package/src/runtime/lifecycle.ts +14 -4
- package/src/runtime/loaderBridge.ts +9 -9
- package/src/runtime/navigation.ts +78 -0
- package/src/runtime/plugin.node.tsx +120 -93
- package/src/runtime/plugin.tsx +11 -1
- package/src/runtime/prefetchLink.tsx +1 -23
- package/src/runtime/register.ts +1 -12
- package/src/runtime/routeTree/construction.ts +32 -110
- package/src/runtime/routeTree/loaders.ts +2 -4
- package/src/runtime/routeTree/paths.ts +0 -6
- package/src/runtime/router.ts +25 -40
- package/src/runtime/ssrManagedTags.ts +0 -17
- package/src/runtime/ssrTypes.ts +0 -7
- package/src/runtime/state.ts +1 -1
- package/src/runtime/types.ts +2 -2
- package/src/runtime/utils.tsx +4 -1
- package/src/shared/routeDescriptor.ts +46 -0
- package/tests/router/cli.test.ts +148 -824
- package/tests/router/clientNavigation.ts +13 -0
- package/tests/router/dataMutation.test.tsx +17 -85
- package/tests/router/flightSerialization.roundtrip.test.tsx +9 -6
- package/tests/router/generateRouteArtifacts.test.ts +139 -27
- package/tests/router/loaderBridge.test.ts +0 -25
- package/tests/router/localisedIdentity.test.tsx +100 -0
- package/tests/router/prefetchLink.test.tsx +46 -21
- package/tests/router/prefetchLinkPreload.test.tsx +11 -32
- package/tests/router/routeHooks.test.ts +42 -105
- package/tests/router/routeMetadata.test.ts +233 -0
- package/tests/router/routeTree.test.ts +144 -836
- package/tests/router/rsc.test.tsx +131 -166
- package/tests/router/serverPlugin.test.ts +193 -1
- package/tests/router/tanstackTypes.test.ts +218 -963
- package/dist/esm/rslib-runtime~1.mjs +0 -18
- package/dist/esm-node/rslib-runtime~0.mjs +0 -19
- 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
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AnyRouter } from '@tanstack/react-router';
|
|
2
|
+
|
|
3
|
+
// Router-core navigates only on the client. Node tests perform the same
|
|
4
|
+
// transition a mounted RouterProvider runs: push the rewritten public URL,
|
|
5
|
+
// then load it.
|
|
6
|
+
export async function navigateOnClient(
|
|
7
|
+
router: AnyRouter,
|
|
8
|
+
options: Parameters<AnyRouter['buildLocation']>[0],
|
|
9
|
+
) {
|
|
10
|
+
router.history.push(router.buildLocation(options).publicHref);
|
|
11
|
+
router.updateLatestLocation();
|
|
12
|
+
await router.load();
|
|
13
|
+
}
|
|
@@ -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,18 +1,12 @@
|
|
|
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,
|
|
8
|
+
const cliInitMock = rstest.fn(async () => ({
|
|
9
|
+
appContext: { hooks: { onBeforeExit: { call: async () => {} } } },
|
|
16
10
|
}));
|
|
17
11
|
|
|
18
12
|
rstest.mock('@modern-js/app-tools/cli/run', () => ({
|
|
@@ -25,8 +19,12 @@ rstest.mock('@modern-js/plugin/cli', () => ({
|
|
|
25
19
|
import fs from 'node:fs';
|
|
26
20
|
import os from 'node:os';
|
|
27
21
|
import path from 'node:path';
|
|
22
|
+
import { createAsyncHook } from '@modern-js/plugin';
|
|
28
23
|
import { INTERNAL_RUNTIME_PLUGINS } from '@modern-js/utils';
|
|
29
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
generateTanstackRouteArtifacts,
|
|
26
|
+
writeTanstackRouterTypesForEntries,
|
|
27
|
+
} from '../../src/cli';
|
|
30
28
|
|
|
31
29
|
function makeAppDir(withConfig: boolean): string {
|
|
32
30
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tanstack-routes-gen-'));
|
|
@@ -46,9 +44,61 @@ describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
|
|
|
46
44
|
cliInitMock.mockClear();
|
|
47
45
|
});
|
|
48
46
|
|
|
49
|
-
it('
|
|
47
|
+
it('writes a real route artifact through the prepare-only CLI drive', async () => {
|
|
50
48
|
const previousArgv = process.env.MODERN_ARGV;
|
|
51
49
|
const appDirectory = makeAppDir(true);
|
|
50
|
+
const srcDirectory = path.join(appDirectory, 'src');
|
|
51
|
+
const componentPath = path.join(srcDirectory, 'routes', 'page.tsx');
|
|
52
|
+
fs.mkdirSync(path.dirname(componentPath), { recursive: true });
|
|
53
|
+
fs.writeFileSync(
|
|
54
|
+
componentPath,
|
|
55
|
+
'export default function Page() { return null; }\n',
|
|
56
|
+
'utf-8',
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const runOptions = {
|
|
60
|
+
cwd: appDirectory,
|
|
61
|
+
version: '9.9.9-test',
|
|
62
|
+
internalPlugins: INTERNAL_RUNTIME_PLUGINS,
|
|
63
|
+
configFile: path.resolve(appDirectory, 'modern.config'),
|
|
64
|
+
};
|
|
65
|
+
createRunOptionsMock.mockResolvedValue({
|
|
66
|
+
mockRunOptions: true,
|
|
67
|
+
received: runOptions,
|
|
68
|
+
});
|
|
69
|
+
const onBeforeExit = createAsyncHook<() => Promise<void>>();
|
|
70
|
+
let disposed = false;
|
|
71
|
+
onBeforeExit.tap(async () => {
|
|
72
|
+
await Promise.resolve();
|
|
73
|
+
disposed = true;
|
|
74
|
+
});
|
|
75
|
+
cliInitMock.mockImplementation(async () => {
|
|
76
|
+
await writeTanstackRouterTypesForEntries({
|
|
77
|
+
appContext: {
|
|
78
|
+
srcDirectory,
|
|
79
|
+
internalSrcAlias: '@/_',
|
|
80
|
+
entrypoints: [{ entryName: 'main', isMainEntry: true }],
|
|
81
|
+
} as any,
|
|
82
|
+
routesByEntry: {
|
|
83
|
+
main: [
|
|
84
|
+
{
|
|
85
|
+
type: 'nested',
|
|
86
|
+
id: 'layout',
|
|
87
|
+
isRoot: true,
|
|
88
|
+
children: [
|
|
89
|
+
{
|
|
90
|
+
type: 'nested',
|
|
91
|
+
id: 'page',
|
|
92
|
+
index: true,
|
|
93
|
+
_component: '@/_/routes/page',
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
] as any,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
return { appContext: { hooks: { onBeforeExit } } };
|
|
101
|
+
});
|
|
52
102
|
|
|
53
103
|
try {
|
|
54
104
|
await generateTanstackRouteArtifacts({
|
|
@@ -61,26 +111,26 @@ describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
|
|
|
61
111
|
string,
|
|
62
112
|
unknown
|
|
63
113
|
>;
|
|
114
|
+
// The headless entry must resolve cwd/configFile from the target app
|
|
115
|
+
// directory, not the process's own cwd — that was the actual bug.
|
|
64
116
|
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
117
|
expect(runOptionsArg.configFile).toBe(
|
|
73
118
|
path.resolve(appDirectory, 'modern.config'),
|
|
74
119
|
);
|
|
75
120
|
|
|
76
121
|
expect(cliInitMock).toHaveBeenCalledTimes(1);
|
|
77
|
-
|
|
78
|
-
expect(initArg.command).toBe('build');
|
|
79
|
-
expect(initArg.mockRunOptions).toBe(true);
|
|
122
|
+
expect(disposed).toBe(true);
|
|
80
123
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
124
|
+
expect(
|
|
125
|
+
fs.existsSync(
|
|
126
|
+
path.join(srcDirectory, 'modern-tanstack', 'main', 'router.gen.ts'),
|
|
127
|
+
),
|
|
128
|
+
).toBe(true);
|
|
129
|
+
const registerArtifact = fs.readFileSync(
|
|
130
|
+
path.join(srcDirectory, 'modern-tanstack', 'register.gen.d.ts'),
|
|
131
|
+
'utf-8',
|
|
132
|
+
);
|
|
133
|
+
expect(registerArtifact).toContain('./main/router.gen');
|
|
84
134
|
} finally {
|
|
85
135
|
fs.rmSync(appDirectory, { force: true, recursive: true });
|
|
86
136
|
if (previousArgv === undefined) {
|
|
@@ -91,7 +141,69 @@ describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
|
|
|
91
141
|
}
|
|
92
142
|
});
|
|
93
143
|
|
|
94
|
-
it('
|
|
144
|
+
it('preserves initialization and cleanup failures', async () => {
|
|
145
|
+
const appDirectory = makeAppDir(true);
|
|
146
|
+
const initializationFailure = new Error('prepare failed');
|
|
147
|
+
const cleanupFailure = new Error('dispose failed');
|
|
148
|
+
const onBeforeExit = createAsyncHook<() => Promise<void>>();
|
|
149
|
+
onBeforeExit.tap(async () => {
|
|
150
|
+
throw cleanupFailure;
|
|
151
|
+
});
|
|
152
|
+
try {
|
|
153
|
+
cliInitMock.mockRejectedValueOnce(initializationFailure);
|
|
154
|
+
await expect(
|
|
155
|
+
generateTanstackRouteArtifacts({ appDirectory }),
|
|
156
|
+
).rejects.toBe(initializationFailure);
|
|
157
|
+
cliInitMock.mockResolvedValueOnce({
|
|
158
|
+
appContext: { hooks: { onBeforeExit } },
|
|
159
|
+
});
|
|
160
|
+
await expect(
|
|
161
|
+
generateTanstackRouteArtifacts({ appDirectory }),
|
|
162
|
+
).rejects.toBe(cleanupFailure);
|
|
163
|
+
} finally {
|
|
164
|
+
fs.rmSync(appDirectory, { force: true, recursive: true });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('regenerates the route metadata manifest before loading the config', async () => {
|
|
169
|
+
const appDirectory = makeAppDir(true);
|
|
170
|
+
const routesDirectory = path.join(appDirectory, 'src', 'routes');
|
|
171
|
+
const manifestPath = path.join(
|
|
172
|
+
routesDirectory,
|
|
173
|
+
'ultramodern-route-metadata.ts',
|
|
174
|
+
);
|
|
175
|
+
fs.mkdirSync(path.join(routesDirectory, '[lang]'), { recursive: true });
|
|
176
|
+
fs.writeFileSync(
|
|
177
|
+
path.join(routesDirectory, '[lang]', 'route.meta.ts'),
|
|
178
|
+
"export const routeMeta = { namespace: 'app' } as const;\n",
|
|
179
|
+
);
|
|
180
|
+
// A manifest that still imports a deleted route.meta.ts makes the config
|
|
181
|
+
// (which imports the manifest) fail to load.
|
|
182
|
+
fs.writeFileSync(
|
|
183
|
+
manifestPath,
|
|
184
|
+
"import { routeMeta as route0 } from './[lang]/deleted/route.meta';\n",
|
|
185
|
+
);
|
|
186
|
+
const configLoadFailure = new Error(
|
|
187
|
+
"Cannot find module './[lang]/deleted/route.meta'",
|
|
188
|
+
);
|
|
189
|
+
cliInitMock.mockImplementationOnce(async () => {
|
|
190
|
+
throw configLoadFailure;
|
|
191
|
+
});
|
|
192
|
+
try {
|
|
193
|
+
await expect(
|
|
194
|
+
generateTanstackRouteArtifacts({ appDirectory }),
|
|
195
|
+
).rejects.toBe(configLoadFailure);
|
|
196
|
+
const manifest = fs.readFileSync(manifestPath, 'utf8');
|
|
197
|
+
expect(manifest).toContain(
|
|
198
|
+
"import { routeMeta as route0 } from './[lang]/route.meta';",
|
|
199
|
+
);
|
|
200
|
+
expect(manifest).not.toContain('deleted');
|
|
201
|
+
} finally {
|
|
202
|
+
fs.rmSync(appDirectory, { force: true, recursive: true });
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('throws a clear error when no config file exists', async () => {
|
|
95
207
|
const appDirectory = makeAppDir(false);
|
|
96
208
|
|
|
97
209
|
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,100 @@
|
|
|
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
|
+
import { navigateOnClient } from './clientNavigation';
|
|
16
|
+
|
|
17
|
+
function SearchPage() {
|
|
18
|
+
const data = useLoaderData({ from: '/$lang/search' }) as { language: string };
|
|
19
|
+
return createElement('p', null, `search:${data.language}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function ResourcePage() {
|
|
23
|
+
const params = useParams({ from: '/$lang/resources/$id' }) as { id: string };
|
|
24
|
+
return createElement('p', null, `resource:${params.id}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test('localized URLs preserve native loader and params route identity in SSR and navigation', async () => {
|
|
28
|
+
const routes = applyLocalisedUrlsToRoutes(
|
|
29
|
+
[
|
|
30
|
+
{
|
|
31
|
+
type: 'nested',
|
|
32
|
+
id: 'lang',
|
|
33
|
+
path: ':lang',
|
|
34
|
+
component: Outlet,
|
|
35
|
+
children: [
|
|
36
|
+
{
|
|
37
|
+
type: 'nested',
|
|
38
|
+
id: 'search',
|
|
39
|
+
path: 'search',
|
|
40
|
+
component: SearchPage,
|
|
41
|
+
loader: ({ params }: { params: { lang: string } }) => ({
|
|
42
|
+
language: params.lang,
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
type: 'nested',
|
|
47
|
+
id: 'resource',
|
|
48
|
+
path: 'resources/:id',
|
|
49
|
+
component: ResourcePage,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
['en', 'cs'],
|
|
55
|
+
{
|
|
56
|
+
'/search': { en: '/find', cs: '/hledat' },
|
|
57
|
+
'/resources/:id': { en: '/resources/:id', cs: '/zdroje/:id' },
|
|
58
|
+
},
|
|
59
|
+
'canonical',
|
|
60
|
+
);
|
|
61
|
+
const routeObjects = createTanstackRouteObjectsFromConfig({
|
|
62
|
+
routesConfig: { routes },
|
|
63
|
+
})!;
|
|
64
|
+
const history = createMemoryHistory({
|
|
65
|
+
initialEntries: ['/base/cs/hledat?q=tractor#results'],
|
|
66
|
+
});
|
|
67
|
+
const router = createRouter({
|
|
68
|
+
context: {
|
|
69
|
+
request: new Request(
|
|
70
|
+
'https://example.test/base/cs/hledat?q=tractor#results',
|
|
71
|
+
),
|
|
72
|
+
},
|
|
73
|
+
routeTree: createRouteTreeFromRouteObjects(routeObjects),
|
|
74
|
+
history,
|
|
75
|
+
rewrite: createModernBasepathRewrite('/base', false, routeObjects),
|
|
76
|
+
});
|
|
77
|
+
await router.load();
|
|
78
|
+
expect(router.state.matches.at(-1)?.routeId).toBe('/$lang/search');
|
|
79
|
+
expect(renderToString(createElement(RouterProvider, { router }))).toContain(
|
|
80
|
+
'search:cs',
|
|
81
|
+
);
|
|
82
|
+
expect(history.location.href).toBe('/base/cs/hledat?q=tractor#results');
|
|
83
|
+
|
|
84
|
+
await navigateOnClient(router, {
|
|
85
|
+
to: '/en/search',
|
|
86
|
+
search: { q: 'tractor' },
|
|
87
|
+
});
|
|
88
|
+
expect(history.location.href).toBe('/base/en/find?q=tractor');
|
|
89
|
+
expect(router.state.matches.at(-1)?.routeId).toBe('/$lang/search');
|
|
90
|
+
expect(renderToString(createElement(RouterProvider, { router }))).toContain(
|
|
91
|
+
'search:en',
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
await navigateOnClient(router, { to: '/cs/zdroje/a%2Fb' });
|
|
95
|
+
expect(history.location.href).toBe('/base/cs/zdroje/a%2Fb');
|
|
96
|
+
expect(router.state.matches.at(-1)?.routeId).toBe('/$lang/resources/$id');
|
|
97
|
+
expect(renderToString(createElement(RouterProvider, { router }))).toContain(
|
|
98
|
+
'resource:a/b',
|
|
99
|
+
);
|
|
100
|
+
});
|
|
@@ -5,8 +5,9 @@ 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
|
+
import { createTanstackNavigation } from '../../src/runtime/navigation';
|
|
10
11
|
import { Link, NavLink } from '../../src/runtime/prefetchLink';
|
|
11
12
|
|
|
12
13
|
// Real-router harness (no module mocking): STATIC_ACTIVE_PROPS (the
|
|
@@ -74,15 +75,10 @@ async function renderLink(options: Parameters<typeof buildRouter>[0]) {
|
|
|
74
75
|
return found as HTMLAnchorElement;
|
|
75
76
|
});
|
|
76
77
|
|
|
77
|
-
return { anchor, ...utils };
|
|
78
|
+
return { anchor, router, ...utils };
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
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
82
|
it('lets caller aria-current="true" win over the TanStack-forced value', async () => {
|
|
87
83
|
const { anchor } = await renderLink({
|
|
88
84
|
initialPath: '/settings',
|
|
@@ -100,27 +96,56 @@ describe('tanstack prefetch link adapter - aria-current override', () => {
|
|
|
100
96
|
expect(anchor.hasAttribute('aria-current')).toBe(false);
|
|
101
97
|
});
|
|
102
98
|
|
|
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',
|
|
99
|
+
it('updates render-prop children across an inactive-to-active transition', async () => {
|
|
100
|
+
const { anchor, router } = await renderLink({
|
|
101
|
+
initialPath: '/',
|
|
112
102
|
children: ({ isActive }: { isActive: boolean }) =>
|
|
113
103
|
isActive ? 'Active settings' : 'Settings',
|
|
114
104
|
});
|
|
115
|
-
|
|
105
|
+
|
|
106
|
+
expect(anchor.textContent).toBe('Settings');
|
|
107
|
+
|
|
108
|
+
await act(async () => {
|
|
109
|
+
await router.navigate({ to: '/settings' });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await waitFor(() => expect(anchor.textContent).toBe('Active settings'));
|
|
116
113
|
});
|
|
117
114
|
|
|
118
|
-
it('
|
|
115
|
+
it('passes the native anchor ref through the router hook', async () => {
|
|
116
|
+
const ref = React.createRef<HTMLAnchorElement>();
|
|
119
117
|
const { anchor } = await renderLink({
|
|
120
118
|
initialPath: '/',
|
|
121
|
-
|
|
122
|
-
isActive ? 'Active settings' : 'Settings',
|
|
119
|
+
linkProps: { ref, prefetch: 'none' },
|
|
123
120
|
});
|
|
124
|
-
expect(
|
|
121
|
+
expect(ref.current).toBe(anchor);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('provider navigation observes native navigation and preserves history state', async () => {
|
|
126
|
+
const { router, unmount } = await renderLink({ initialPath: '/' });
|
|
127
|
+
const capability = createTanstackNavigation(router);
|
|
128
|
+
const initial = capability.getSnapshot();
|
|
129
|
+
expect(capability.getSnapshot()).toBe(initial);
|
|
130
|
+
const listener = rstest.fn();
|
|
131
|
+
const stop = capability.subscribe(listener);
|
|
132
|
+
await act(() =>
|
|
133
|
+
capability.navigate('/settings?q=two#detail', {
|
|
134
|
+
replace: true,
|
|
135
|
+
state: { custom: true },
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
expect(listener).toHaveBeenCalled();
|
|
139
|
+
expect(capability.getSnapshot().location).toEqual({
|
|
140
|
+
pathname: '/settings',
|
|
141
|
+
search: '?q=two',
|
|
142
|
+
hash: '#detail',
|
|
125
143
|
});
|
|
144
|
+
expect(router.state.location.state).toMatchObject({ custom: true });
|
|
145
|
+
expect(router.history.length).toBe(1);
|
|
146
|
+
stop();
|
|
147
|
+
listener.mockClear();
|
|
148
|
+
await act(() => capability.navigate('/'));
|
|
149
|
+
expect(listener).not.toHaveBeenCalled();
|
|
150
|
+
unmount();
|
|
126
151
|
});
|