@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.11 → 3.9.0-ultramodern.13
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 +2 -1
- 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 +28 -63
- package/dist/cjs/runtime/lifecycle.js +0 -3
- package/dist/cjs/runtime/loaderBridge.js +10 -4
- package/dist/cjs/runtime/navigation.js +102 -0
- package/dist/cjs/runtime/plugin.js +6 -1
- package/dist/cjs/runtime/plugin.node.js +81 -63
- package/dist/cjs/runtime/routeTree/construction.js +19 -61
- package/dist/cjs/runtime/routeTree/loaders.js +1 -1
- package/dist/cjs/runtime/routeTree/paths.js +0 -6
- package/dist/cjs/runtime/ssrManagedTags.js +2 -12
- package/dist/cjs/runtime/utils.js +1 -0
- package/dist/cjs/shared/routeDescriptor.js +60 -0
- package/dist/esm/cli/artifacts.mjs +2 -1
- package/dist/esm/cli/index.mjs +1 -7
- 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 +29 -64
- 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 +6 -1
- package/dist/esm/runtime/plugin.node.mjs +83 -65
- package/dist/esm/runtime/routeTree/construction.mjs +19 -61
- package/dist/esm/runtime/routeTree/loaders.mjs +2 -2
- package/dist/esm/runtime/routeTree/paths.mjs +1 -4
- package/dist/esm/runtime/ssrManagedTags.mjs +1 -8
- package/dist/esm/runtime/utils.mjs +1 -0
- package/dist/esm/shared/routeDescriptor.mjs +22 -0
- package/dist/esm-node/cli/artifacts.mjs +2 -1
- package/dist/esm-node/cli/index.mjs +1 -7
- 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 +29 -64
- 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 +6 -1
- package/dist/esm-node/runtime/plugin.node.mjs +83 -65
- package/dist/esm-node/runtime/routeTree/construction.mjs +19 -61
- 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/ssrManagedTags.mjs +1 -8
- package/dist/esm-node/runtime/utils.mjs +1 -0
- package/dist/esm-node/shared/routeDescriptor.mjs +23 -0
- package/dist/types/cli/tanstackTypes/shared.d.ts +0 -2
- package/dist/types/runtime/lifecycle.d.ts +1 -1
- package/dist/types/runtime/loaderBridge.d.ts +1 -0
- package/dist/types/runtime/navigation.d.ts +4 -0
- package/dist/types/runtime/routeTree/paths.d.ts +0 -2
- package/dist/types/runtime/ssrManagedTags.d.ts +0 -1
- package/dist/types/runtime/ssrTypes.d.ts +0 -7
- package/dist/types/shared/routeDescriptor.d.ts +26 -0
- package/package.json +21 -21
- package/src/cli/artifacts.ts +6 -1
- package/src/cli/tanstackTypes/canonicalRoutes.ts +17 -12
- package/src/cli/tanstackTypes/shared.ts +0 -14
- package/src/cli/tanstackTypes/source.ts +43 -119
- package/src/runtime/lifecycle.ts +0 -1
- package/src/runtime/loaderBridge.ts +9 -9
- package/src/runtime/navigation.ts +78 -0
- package/src/runtime/plugin.node.tsx +111 -91
- package/src/runtime/plugin.tsx +6 -0
- package/src/runtime/routeTree/construction.ts +32 -112
- package/src/runtime/routeTree/loaders.ts +2 -4
- package/src/runtime/routeTree/paths.ts +0 -6
- package/src/runtime/ssrManagedTags.ts +0 -17
- package/src/runtime/ssrTypes.ts +0 -7
- package/src/runtime/utils.tsx +1 -0
- package/src/shared/routeDescriptor.ts +46 -0
- package/tests/router/clientNavigation.ts +13 -0
- package/tests/router/generateRouteArtifacts.test.ts +36 -2
- package/tests/router/localisedIdentity.test.tsx +7 -2
- package/tests/router/prefetchLink.test.tsx +29 -0
- package/tests/router/routeHooks.test.ts +2 -1
- package/tests/router/serverPlugin.test.ts +150 -0
- package/tests/router/tanstackTypes.test.ts +121 -2
- package/dist/esm/rslib-runtime~1.mjs +0 -18
- package/dist/esm-node/rslib-runtime~0.mjs +0 -19
|
@@ -5,7 +5,9 @@ const createRunOptionsMock = rstest.fn(async (options: unknown) => ({
|
|
|
5
5
|
mockRunOptions: true,
|
|
6
6
|
received: options,
|
|
7
7
|
}));
|
|
8
|
-
const cliInitMock = rstest.fn(async () => ({
|
|
8
|
+
const cliInitMock = rstest.fn(async () => ({
|
|
9
|
+
appContext: { hooks: { onBeforeExit: { call: async () => {} } } },
|
|
10
|
+
}));
|
|
9
11
|
|
|
10
12
|
rstest.mock('@modern-js/app-tools/cli/run', () => ({
|
|
11
13
|
createRunOptions: createRunOptionsMock,
|
|
@@ -17,6 +19,7 @@ rstest.mock('@modern-js/plugin/cli', () => ({
|
|
|
17
19
|
import fs from 'node:fs';
|
|
18
20
|
import os from 'node:os';
|
|
19
21
|
import path from 'node:path';
|
|
22
|
+
import { createAsyncHook } from '@modern-js/plugin';
|
|
20
23
|
import { INTERNAL_RUNTIME_PLUGINS } from '@modern-js/utils';
|
|
21
24
|
import {
|
|
22
25
|
generateTanstackRouteArtifacts,
|
|
@@ -63,6 +66,12 @@ describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
|
|
|
63
66
|
mockRunOptions: true,
|
|
64
67
|
received: runOptions,
|
|
65
68
|
});
|
|
69
|
+
const onBeforeExit = createAsyncHook<() => Promise<void>>();
|
|
70
|
+
let disposed = false;
|
|
71
|
+
onBeforeExit.tap(async () => {
|
|
72
|
+
await Promise.resolve();
|
|
73
|
+
disposed = true;
|
|
74
|
+
});
|
|
66
75
|
cliInitMock.mockImplementation(async () => {
|
|
67
76
|
await writeTanstackRouterTypesForEntries({
|
|
68
77
|
appContext: {
|
|
@@ -88,7 +97,7 @@ describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
|
|
|
88
97
|
] as any,
|
|
89
98
|
},
|
|
90
99
|
});
|
|
91
|
-
return { appContext: {} };
|
|
100
|
+
return { appContext: { hooks: { onBeforeExit } } };
|
|
92
101
|
});
|
|
93
102
|
|
|
94
103
|
try {
|
|
@@ -110,6 +119,7 @@ describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
|
|
|
110
119
|
);
|
|
111
120
|
|
|
112
121
|
expect(cliInitMock).toHaveBeenCalledTimes(1);
|
|
122
|
+
expect(disposed).toBe(true);
|
|
113
123
|
|
|
114
124
|
expect(
|
|
115
125
|
fs.existsSync(
|
|
@@ -131,6 +141,30 @@ describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
|
|
|
131
141
|
}
|
|
132
142
|
});
|
|
133
143
|
|
|
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
|
+
|
|
134
168
|
it('throws a clear error when no config file exists', async () => {
|
|
135
169
|
const appDirectory = makeAppDir(false);
|
|
136
170
|
|
|
@@ -12,6 +12,7 @@ import { renderToString } from 'react-dom/server';
|
|
|
12
12
|
import { createModernBasepathRewrite } from '../../src/runtime/basepathRewrite';
|
|
13
13
|
import { createRouteTreeFromRouteObjects } from '../../src/runtime/routeTree';
|
|
14
14
|
import { createTanstackRouteObjectsFromConfig } from '../../src/runtime/utils';
|
|
15
|
+
import { navigateOnClient } from './clientNavigation';
|
|
15
16
|
|
|
16
17
|
function SearchPage() {
|
|
17
18
|
const data = useLoaderData({ from: '/$lang/search' }) as { language: string };
|
|
@@ -55,6 +56,7 @@ test('localized URLs preserve native loader and params route identity in SSR and
|
|
|
55
56
|
'/search': { en: '/find', cs: '/hledat' },
|
|
56
57
|
'/resources/:id': { en: '/resources/:id', cs: '/zdroje/:id' },
|
|
57
58
|
},
|
|
59
|
+
'canonical',
|
|
58
60
|
);
|
|
59
61
|
const routeObjects = createTanstackRouteObjectsFromConfig({
|
|
60
62
|
routesConfig: { routes },
|
|
@@ -79,14 +81,17 @@ test('localized URLs preserve native loader and params route identity in SSR and
|
|
|
79
81
|
);
|
|
80
82
|
expect(history.location.href).toBe('/base/cs/hledat?q=tractor#results');
|
|
81
83
|
|
|
82
|
-
await router
|
|
84
|
+
await navigateOnClient(router, {
|
|
85
|
+
to: '/en/search',
|
|
86
|
+
search: { q: 'tractor' },
|
|
87
|
+
});
|
|
83
88
|
expect(history.location.href).toBe('/base/en/find?q=tractor');
|
|
84
89
|
expect(router.state.matches.at(-1)?.routeId).toBe('/$lang/search');
|
|
85
90
|
expect(renderToString(createElement(RouterProvider, { router }))).toContain(
|
|
86
91
|
'search:en',
|
|
87
92
|
);
|
|
88
93
|
|
|
89
|
-
await router
|
|
94
|
+
await navigateOnClient(router, { to: '/cs/zdroje/a%2Fb' });
|
|
90
95
|
expect(history.location.href).toBe('/base/cs/zdroje/a%2Fb');
|
|
91
96
|
expect(router.state.matches.at(-1)?.routeId).toBe('/$lang/resources/$id');
|
|
92
97
|
expect(renderToString(createElement(RouterProvider, { router }))).toContain(
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from '@tanstack/react-router';
|
|
8
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
|
|
@@ -120,3 +121,31 @@ describe('tanstack prefetch link adapter - aria-current override', () => {
|
|
|
120
121
|
expect(ref.current).toBe(anchor);
|
|
121
122
|
});
|
|
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',
|
|
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();
|
|
151
|
+
});
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import React from 'react';
|
|
9
9
|
import { renderToString } from 'react-dom/server';
|
|
10
10
|
import { useMatches } from '../../src/runtime/routeHooks';
|
|
11
|
+
import { navigateOnClient } from './clientNavigation';
|
|
11
12
|
|
|
12
13
|
function MatchIds() {
|
|
13
14
|
const matches = useMatches();
|
|
@@ -41,7 +42,7 @@ test('updates whole match objects after navigation with the structural-sharing g
|
|
|
41
42
|
renderToString(React.createElement(RouterProvider, { router })),
|
|
42
43
|
).toContain('__root__');
|
|
43
44
|
|
|
44
|
-
await router
|
|
45
|
+
await navigateOnClient(router, { to: '/settings' });
|
|
45
46
|
|
|
46
47
|
expect(
|
|
47
48
|
renderToString(React.createElement(RouterProvider, { router })),
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
} from '../../src/runtime/lifecycle';
|
|
10
10
|
import { tanstackRouterPlugin } from '../../src/runtime/plugin.node';
|
|
11
11
|
import type { TanstackRouterPluginAPI } from '../../src/runtime/pluginShared';
|
|
12
|
+
import * as ssrUtils from '../../src/runtime/ssrManagedTags';
|
|
13
|
+
import * as ssrPreload from '../../src/runtime/ssrPreload';
|
|
12
14
|
import type { RouterConfig } from '../../src/runtime/types';
|
|
13
15
|
|
|
14
16
|
type BeforeRenderListener = Parameters<
|
|
@@ -116,6 +118,11 @@ describe('tanstack server plugin router results', () => {
|
|
|
116
118
|
framework: 'tanstack',
|
|
117
119
|
statusCode: 200,
|
|
118
120
|
});
|
|
121
|
+
// Without the dehydrated router the client re-renders matched remotes
|
|
122
|
+
// from scratch instead of hydrating the server HTML.
|
|
123
|
+
expect(getRouterServerSnapshot(context)?.hydrationScripts).toEqual(
|
|
124
|
+
expect.arrayContaining([expect.stringContaining('$_TSR')]),
|
|
125
|
+
);
|
|
119
126
|
});
|
|
120
127
|
|
|
121
128
|
test('interrupts SSR with the router redirect response', async () => {
|
|
@@ -155,3 +162,146 @@ describe('tanstack server plugin router results', () => {
|
|
|
155
162
|
expect(status).not.toHaveBeenCalled();
|
|
156
163
|
});
|
|
157
164
|
});
|
|
165
|
+
|
|
166
|
+
describe('TanStack preparation resource lifetime', () => {
|
|
167
|
+
test.each([
|
|
168
|
+
'attach',
|
|
169
|
+
'load',
|
|
170
|
+
'result',
|
|
171
|
+
'preload',
|
|
172
|
+
'dehydrate',
|
|
173
|
+
'abort',
|
|
174
|
+
'clientRender',
|
|
175
|
+
'afterCreate',
|
|
176
|
+
])('disposes exactly once when %s fails', async stage => {
|
|
177
|
+
const failure = new Error(`${stage} failed`);
|
|
178
|
+
const cleanup = rstest.fn();
|
|
179
|
+
const controller = new AbortController();
|
|
180
|
+
const { context } = createServerContext('/');
|
|
181
|
+
context.ssrContext!.request.raw = new Request('http://localhost/', {
|
|
182
|
+
signal: controller.signal,
|
|
183
|
+
});
|
|
184
|
+
const attach = ssrUtils.attachServerSsrUtils;
|
|
185
|
+
rstest
|
|
186
|
+
.spyOn(ssrUtils, 'attachServerSsrUtils')
|
|
187
|
+
.mockImplementation(async router => {
|
|
188
|
+
await attach(router);
|
|
189
|
+
rstest.spyOn(router.serverSsr!, 'cleanup').mockImplementation(cleanup);
|
|
190
|
+
if (stage === 'attach') throw failure;
|
|
191
|
+
if (stage === 'load')
|
|
192
|
+
rstest.spyOn(router, 'load').mockRejectedValue(failure);
|
|
193
|
+
if (stage === 'result')
|
|
194
|
+
rstest.spyOn(router, 'load').mockResolvedValue(undefined);
|
|
195
|
+
if (stage === 'dehydrate')
|
|
196
|
+
rstest
|
|
197
|
+
.spyOn(router.serverSsr!, 'dehydrate')
|
|
198
|
+
.mockRejectedValue(failure);
|
|
199
|
+
if (stage === 'abort') controller.abort(failure);
|
|
200
|
+
});
|
|
201
|
+
if (stage === 'clientRender') {
|
|
202
|
+
context.ssrContext!.loaderFailureMode = 'clientRender';
|
|
203
|
+
rstest
|
|
204
|
+
.spyOn(ssrUtils, 'collectRouterErrors')
|
|
205
|
+
.mockReturnValue({ root: failure });
|
|
206
|
+
}
|
|
207
|
+
if (stage === 'afterCreate')
|
|
208
|
+
rstest
|
|
209
|
+
.spyOn(routerProviderRegistryHooks.onAfterCreateRouter, 'call')
|
|
210
|
+
.mockImplementation(() => {
|
|
211
|
+
throw failure;
|
|
212
|
+
});
|
|
213
|
+
if (stage === 'preload')
|
|
214
|
+
rstest
|
|
215
|
+
.spyOn(ssrPreload, 'preloadMatchedRouteComponents')
|
|
216
|
+
.mockRejectedValue(failure);
|
|
217
|
+
const beforeRender = collectBeforeRender(() => [
|
|
218
|
+
{ id: 'root', path: '/', Component: () => null },
|
|
219
|
+
]);
|
|
220
|
+
await expect(beforeRender(context, value => value)).rejects.toThrow(
|
|
221
|
+
stage === 'result' ? 'without a server result' : failure.message,
|
|
222
|
+
);
|
|
223
|
+
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
224
|
+
await getRouterRuntimeState(context)?.cleanup?.();
|
|
225
|
+
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test.each([
|
|
229
|
+
'string',
|
|
230
|
+
'stream',
|
|
231
|
+
] as const)('retains %s router resources for response termination', async mode => {
|
|
232
|
+
const { context } = createServerContext('/products/shoe?q=1#detail');
|
|
233
|
+
context.ssrContext!.mode = mode;
|
|
234
|
+
const beforeRender = collectBeforeRender(() => [
|
|
235
|
+
{ id: 'product', path: '/products/:id', Component: () => null },
|
|
236
|
+
]);
|
|
237
|
+
await beforeRender(context, value => value);
|
|
238
|
+
const state = getRouterRuntimeState(context)!;
|
|
239
|
+
const router = state.instance as AnyRouter;
|
|
240
|
+
const cleanup = rstest.spyOn(router.serverSsr!, 'cleanup');
|
|
241
|
+
const navigation = state.navigation!;
|
|
242
|
+
expect(navigation.getSnapshot()).toMatchObject({
|
|
243
|
+
location: { pathname: '/products/shoe', search: '?q=1', hash: '#detail' },
|
|
244
|
+
params: { id: 'shoe' },
|
|
245
|
+
});
|
|
246
|
+
expect(navigation.getSnapshot()).toBe(navigation.getSnapshot());
|
|
247
|
+
expect(navigation.Link).toBe(context.router?.Link);
|
|
248
|
+
expect(
|
|
249
|
+
navigation.createLinkProps!({
|
|
250
|
+
pathname: '/products/shoe',
|
|
251
|
+
href: '/products/shoe?q=2#detail',
|
|
252
|
+
search: { q: '2' },
|
|
253
|
+
hash: 'detail',
|
|
254
|
+
hashScrollIntoView: false,
|
|
255
|
+
prefetch: 'none',
|
|
256
|
+
}),
|
|
257
|
+
).toEqual({
|
|
258
|
+
to: '/products/shoe',
|
|
259
|
+
search: { q: '2' },
|
|
260
|
+
hash: 'detail',
|
|
261
|
+
hashScrollIntoView: false,
|
|
262
|
+
preload: false,
|
|
263
|
+
});
|
|
264
|
+
expect(cleanup).not.toHaveBeenCalled();
|
|
265
|
+
await state.cleanup?.();
|
|
266
|
+
await state.cleanup?.();
|
|
267
|
+
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('incoming abort reaches an active Modern loader and disposes its router', async () => {
|
|
272
|
+
const incoming = new AbortController();
|
|
273
|
+
let started!: () => void;
|
|
274
|
+
const loading = new Promise<void>(resolve => {
|
|
275
|
+
started = resolve;
|
|
276
|
+
});
|
|
277
|
+
const { context } = createServerContext('/');
|
|
278
|
+
context.ssrContext!.request.raw = new Request('http://localhost/', {
|
|
279
|
+
signal: incoming.signal,
|
|
280
|
+
});
|
|
281
|
+
const failure = new Error('client disconnected during loader');
|
|
282
|
+
const beforeRender = collectBeforeRender(() => [
|
|
283
|
+
{
|
|
284
|
+
id: 'root',
|
|
285
|
+
path: '/',
|
|
286
|
+
Component: () => null,
|
|
287
|
+
loader: ({ request }: { request: Request }) =>
|
|
288
|
+
new Promise((_resolve, reject) => {
|
|
289
|
+
request.signal.addEventListener(
|
|
290
|
+
'abort',
|
|
291
|
+
() => reject(request.signal.reason),
|
|
292
|
+
{ once: true },
|
|
293
|
+
);
|
|
294
|
+
started();
|
|
295
|
+
}),
|
|
296
|
+
},
|
|
297
|
+
]);
|
|
298
|
+
const pending = beforeRender(context, value => value);
|
|
299
|
+
await loading;
|
|
300
|
+
const router = getRouterRuntimeState(context)!.instance as AnyRouter;
|
|
301
|
+
const cleanup = rstest.spyOn(router.serverSsr!, 'cleanup');
|
|
302
|
+
incoming.abort(failure);
|
|
303
|
+
await expect(pending).rejects.toBe(failure);
|
|
304
|
+
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
305
|
+
await getRouterRuntimeState(context)?.cleanup?.();
|
|
306
|
+
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
307
|
+
});
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
5
6
|
import { promisify } from 'node:util';
|
|
6
|
-
|
|
7
|
+
import { applyLocalisedUrlsToRoutes } from '@modern-js/i18n-runtime-extensions';
|
|
8
|
+
import { createMemoryHistory, createRouter } from '@tanstack/react-router';
|
|
7
9
|
import { writeTanstackRegisterFile } from '../../src/cli/artifacts';
|
|
8
10
|
import {
|
|
9
11
|
collectCanonicalRoutesForEntry,
|
|
10
12
|
generateTanstackRouterTypesSourceForEntry,
|
|
11
13
|
} from '../../src/cli/tanstackTypes';
|
|
14
|
+
import { createModernBasepathRewrite } from '../../src/runtime/basepathRewrite';
|
|
15
|
+
import { createRouteTreeFromRouteObjects } from '../../src/runtime/routeTree';
|
|
16
|
+
import { createTanstackRouteObjectsFromConfig } from '../../src/runtime/utils';
|
|
17
|
+
import { navigateOnClient } from './clientNavigation';
|
|
12
18
|
|
|
13
19
|
const execFileAsync = promisify(execFile);
|
|
14
20
|
const strictestTsconfigPath = path.resolve(
|
|
@@ -261,6 +267,119 @@ describe('tanstack router type generation', () => {
|
|
|
261
267
|
});
|
|
262
268
|
});
|
|
263
269
|
|
|
270
|
+
test('generated and runtime trees agree on localized splats, root siblings, layouts and params', async () => {
|
|
271
|
+
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-parity-'));
|
|
272
|
+
const localized = applyLocalisedUrlsToRoutes(
|
|
273
|
+
[
|
|
274
|
+
{
|
|
275
|
+
type: 'nested',
|
|
276
|
+
id: 'lang',
|
|
277
|
+
path: ':lang',
|
|
278
|
+
children: [
|
|
279
|
+
{
|
|
280
|
+
type: 'nested',
|
|
281
|
+
id: 'products',
|
|
282
|
+
path: 'products',
|
|
283
|
+
children: [{ type: 'nested', id: 'product-files', path: '*' }],
|
|
284
|
+
},
|
|
285
|
+
{ type: 'nested', id: 'fallback', path: '*' },
|
|
286
|
+
],
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
['en', 'cs'],
|
|
290
|
+
{ '/products': { en: '/products', cs: '/produkty' } },
|
|
291
|
+
'canonical',
|
|
292
|
+
);
|
|
293
|
+
const routes = [
|
|
294
|
+
{
|
|
295
|
+
type: 'nested',
|
|
296
|
+
id: 'layout',
|
|
297
|
+
isRoot: true,
|
|
298
|
+
children: [
|
|
299
|
+
...localized,
|
|
300
|
+
{
|
|
301
|
+
type: 'nested',
|
|
302
|
+
children: [{ type: 'nested', path: 'one/:id?', index: false }],
|
|
303
|
+
},
|
|
304
|
+
{ type: 'nested', children: [{ type: 'nested', path: 'two/*' }] },
|
|
305
|
+
{ type: 'nested', id: 'index', index: true },
|
|
306
|
+
],
|
|
307
|
+
},
|
|
308
|
+
{ type: 'nested', id: 'sibling', path: 'outside/:lang' },
|
|
309
|
+
] as any;
|
|
310
|
+
const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
|
|
311
|
+
appContext: {
|
|
312
|
+
srcDirectory: path.join(tempDir, 'src'),
|
|
313
|
+
internalSrcAlias: '@/_',
|
|
314
|
+
} as any,
|
|
315
|
+
entryName: 'parity',
|
|
316
|
+
routes,
|
|
317
|
+
});
|
|
318
|
+
const generated = path.join(tempDir, 'router.gen.ts');
|
|
319
|
+
await writeFile(generated, routerGenTs);
|
|
320
|
+
const modules = path.join(tempDir, 'node_modules', '@modern-js');
|
|
321
|
+
await mkdir(modules, { recursive: true });
|
|
322
|
+
await symlink(
|
|
323
|
+
path.resolve(__dirname, '../..'),
|
|
324
|
+
path.join(modules, 'plugin-tanstack'),
|
|
325
|
+
'junction',
|
|
326
|
+
);
|
|
327
|
+
const { stdout } = await execFileAsync(process.execPath, [
|
|
328
|
+
'--input-type=module',
|
|
329
|
+
'--eval',
|
|
330
|
+
`
|
|
331
|
+
import { router } from ${JSON.stringify(pathToFileURL(generated).href)};
|
|
332
|
+
import { createModernBasepathRewrite } from ${JSON.stringify(pathToFileURL(path.resolve(__dirname, '../../dist/esm/runtime/basepathRewrite.mjs')).href)};
|
|
333
|
+
router.update({ rewrite: createModernBasepathRewrite('/base', false, ${JSON.stringify(routes)}) });
|
|
334
|
+
router.history.push(router.buildLocation({ to: '/cs/produkty/a/b', search: { q: 'tractor' }, hash: 'files' }).publicHref); router.updateLatestLocation(); await router.load();
|
|
335
|
+
console.log(JSON.stringify({ ids: Object.keys(router.routesById).sort(), match: router.state.matches.at(-1).routeId, params: router.state.matches.at(-1).params, href: router.history.location.href }));
|
|
336
|
+
`,
|
|
337
|
+
]);
|
|
338
|
+
const routeObjects = createTanstackRouteObjectsFromConfig({
|
|
339
|
+
routesConfig: { routes },
|
|
340
|
+
})!;
|
|
341
|
+
const runtime = createRouter({
|
|
342
|
+
routeTree: createRouteTreeFromRouteObjects(routeObjects),
|
|
343
|
+
history: createMemoryHistory({
|
|
344
|
+
initialEntries: ['/base/cs/produkty/a/b?q=tractor#files'],
|
|
345
|
+
}),
|
|
346
|
+
rewrite: createModernBasepathRewrite('/base', false, routeObjects),
|
|
347
|
+
});
|
|
348
|
+
await runtime.load();
|
|
349
|
+
expect(JSON.parse(stdout)).toEqual({
|
|
350
|
+
ids: Object.keys(runtime.routesById).sort(),
|
|
351
|
+
match: runtime.state.matches.at(-1)?.routeId,
|
|
352
|
+
params: runtime.state.matches.at(-1)?.params,
|
|
353
|
+
href: runtime.history.location.href,
|
|
354
|
+
});
|
|
355
|
+
expect(runtime.state.matches.at(-1)?.routeId).toBe('/$lang/products/$');
|
|
356
|
+
expect(runtime.state.matches.at(-1)?.params).toMatchObject({
|
|
357
|
+
lang: 'cs',
|
|
358
|
+
_splat: 'a/b',
|
|
359
|
+
});
|
|
360
|
+
await navigateOnClient(runtime, { to: '/cs/missing/path' });
|
|
361
|
+
expect(runtime.state.matches.at(-1)?.routeId).toBe('/$lang/$');
|
|
362
|
+
expect(runtime.state.matches.at(-1)?.params).toMatchObject({
|
|
363
|
+
lang: 'cs',
|
|
364
|
+
_splat: 'missing/path',
|
|
365
|
+
});
|
|
366
|
+
await navigateOnClient(runtime, { to: '/outside/cs' });
|
|
367
|
+
expect(runtime.state.matches.at(-1)?.params).toEqual({ lang: 'cs' });
|
|
368
|
+
const canonicalRoutes = collectCanonicalRoutesForEntry(localized as any)!;
|
|
369
|
+
expect(Object.keys(canonicalRoutes)).toEqual(['/$', '/products/$']);
|
|
370
|
+
await typecheckCanonicalRegisterContract({
|
|
371
|
+
projectDirectory: tempDir,
|
|
372
|
+
canonicalRoutes,
|
|
373
|
+
contractLines: [
|
|
374
|
+
"const product: UltramodernCanonicalRoutes['/products/$'] = { _splat: 'a/b' };",
|
|
375
|
+
"const fallback: UltramodernCanonicalRoutes['/$'] = { _splat: 'unknown/path' };",
|
|
376
|
+
'// @ts-expect-error splat remains a string',
|
|
377
|
+
"const invalid: UltramodernCanonicalRoutes['/products/$'] = { _splat: 42 };",
|
|
378
|
+
'void product; void fallback; void invalid;',
|
|
379
|
+
],
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
264
383
|
test('maps a locale-prefixed route tree to the canonical paths consumers navigate to', () => {
|
|
265
384
|
const routes = [
|
|
266
385
|
{
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
var __webpack_modules__ = {};
|
|
2
|
-
var __webpack_module_cache__ = {};
|
|
3
|
-
function __webpack_require__(moduleId) {
|
|
4
|
-
var cachedModule = __webpack_module_cache__[moduleId];
|
|
5
|
-
if (void 0 !== cachedModule) return cachedModule.exports;
|
|
6
|
-
var module = __webpack_module_cache__[moduleId] = {
|
|
7
|
-
exports: {}
|
|
8
|
-
};
|
|
9
|
-
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
|
|
10
|
-
return module.exports;
|
|
11
|
-
}
|
|
12
|
-
__webpack_require__.m = __webpack_modules__;
|
|
13
|
-
(()=>{
|
|
14
|
-
__webpack_require__.add = function(modules) {
|
|
15
|
-
Object.assign(__webpack_require__.m, modules);
|
|
16
|
-
};
|
|
17
|
-
})();
|
|
18
|
-
export { __webpack_require__ };
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import "node:module";
|
|
2
|
-
var __webpack_modules__ = {};
|
|
3
|
-
var __webpack_module_cache__ = {};
|
|
4
|
-
function __webpack_require__(moduleId) {
|
|
5
|
-
var cachedModule = __webpack_module_cache__[moduleId];
|
|
6
|
-
if (void 0 !== cachedModule) return cachedModule.exports;
|
|
7
|
-
var module = __webpack_module_cache__[moduleId] = {
|
|
8
|
-
exports: {}
|
|
9
|
-
};
|
|
10
|
-
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
|
|
11
|
-
return module.exports;
|
|
12
|
-
}
|
|
13
|
-
__webpack_require__.m = __webpack_modules__;
|
|
14
|
-
(()=>{
|
|
15
|
-
__webpack_require__.add = function(modules) {
|
|
16
|
-
Object.assign(__webpack_require__.m, modules);
|
|
17
|
-
};
|
|
18
|
-
})();
|
|
19
|
-
export { __webpack_require__ };
|