@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.10 → 3.9.0-ultramodern.12
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 +82 -64
- 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/utils.js +1 -0
- package/dist/cjs/shared/routeDescriptor.js +60 -0
- package/dist/esm/cli/artifacts.mjs +2 -1
- 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/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/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/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/shared/routeDescriptor.d.ts +26 -0
- package/package.json +12 -12
- 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 -89
- 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/utils.tsx +1 -0
- package/src/shared/routeDescriptor.ts +46 -0
- package/tests/router/generateRouteArtifacts.test.ts +36 -2
- package/tests/router/localisedIdentity.test.tsx +1 -0
- package/tests/router/prefetchLink.test.tsx +29 -0
- package/tests/router/serverPlugin.test.ts +145 -0
- package/tests/router/tanstackTypes.test.ts +120 -2
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// @effect-diagnostics asyncFunction:off strictBooleanExpressions:off
|
|
2
2
|
|
|
3
|
-
import { canonicaliseLocalisedRoutes } from '@modern-js/i18n-runtime-extensions';
|
|
4
3
|
import { DefaultNotFound } from '@modern-js/runtime/context';
|
|
5
4
|
import type { RouteObject } from '@modern-js/runtime-utils/router';
|
|
6
5
|
import type { AnyRoute, AnyRouter } from '@tanstack/react-router';
|
|
7
6
|
import { rootRouteId } from '@tanstack/react-router';
|
|
8
7
|
|
|
8
|
+
import {
|
|
9
|
+
describeRouteTree,
|
|
10
|
+
type RouteDescriptor,
|
|
11
|
+
} from '../../shared/routeDescriptor';
|
|
9
12
|
import { createRouteStaticData } from '../../shared/routeStaticData';
|
|
10
13
|
import { withModernRouteMatchContext } from '../outlet';
|
|
11
14
|
import {
|
|
@@ -14,7 +17,6 @@ import {
|
|
|
14
17
|
toRouteComponent,
|
|
15
18
|
} from './components';
|
|
16
19
|
import { createModernShouldReload, wrapRouteObjectLoader } from './loaders';
|
|
17
|
-
import { isRouteObjectPathlessLayout, toTanstackPath } from './paths';
|
|
18
20
|
import { mergeModernRouteHandle } from './staticData';
|
|
19
21
|
import {
|
|
20
22
|
createTanstackRootRoute,
|
|
@@ -24,25 +26,16 @@ import {
|
|
|
24
26
|
import type {
|
|
25
27
|
ModernRouteObject,
|
|
26
28
|
ModernTanstackRootRoute,
|
|
27
|
-
MutableTanstackRoute,
|
|
28
29
|
RouteRevalidationState,
|
|
29
30
|
RouteTreeOptions,
|
|
30
31
|
TanstackRootRouteOptions,
|
|
31
32
|
TanstackRouteOptions,
|
|
32
33
|
} from './types';
|
|
33
34
|
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
routeObject: RouteObject;
|
|
39
|
-
}): AnyRoute {
|
|
40
|
-
const {
|
|
41
|
-
options = {},
|
|
42
|
-
parent,
|
|
43
|
-
pathlessFallbackId = 'pathless',
|
|
44
|
-
routeObject,
|
|
45
|
-
} = opts;
|
|
35
|
+
function createRouteOptions(
|
|
36
|
+
routeObject: RouteObject,
|
|
37
|
+
options: RouteTreeOptions,
|
|
38
|
+
): TanstackRouteOptions {
|
|
46
39
|
const modernRouteObject = routeObject as ModernRouteObject;
|
|
47
40
|
const revalidationState: RouteRevalidationState = {};
|
|
48
41
|
const shouldRevalidate = modernRouteObject.shouldRevalidate;
|
|
@@ -50,13 +43,8 @@ function createRouteFromRouteObject(opts: {
|
|
|
50
43
|
shouldRevalidate,
|
|
51
44
|
revalidationState,
|
|
52
45
|
);
|
|
53
|
-
|
|
54
|
-
const stableFallbackId =
|
|
55
|
-
routeObject.id || modernRouteObject.file || pathlessFallbackId;
|
|
56
|
-
|
|
57
46
|
const component = toRouteComponent(routeObject);
|
|
58
47
|
const base: TanstackRouteOptions = {
|
|
59
|
-
getParentRoute: () => parent,
|
|
60
48
|
component,
|
|
61
49
|
pendingComponent: toPendingComponent(routeObject),
|
|
62
50
|
errorComponent: toErrorComponent(routeObject),
|
|
@@ -87,117 +75,49 @@ function createRouteFromRouteObject(opts: {
|
|
|
87
75
|
base.shouldReload = shouldReload;
|
|
88
76
|
}
|
|
89
77
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
} else {
|
|
93
|
-
base.path = routeObject.index
|
|
94
|
-
? '/'
|
|
95
|
-
: toTanstackPath((routeObject.path as string) || '');
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const route = createTanstackRoute(base);
|
|
99
|
-
wrapRouteComponentWithModernContext(route, component, routeObject.id);
|
|
78
|
+
return base;
|
|
79
|
+
}
|
|
100
80
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
81
|
+
function createRouteFromDescriptor(
|
|
82
|
+
descriptor: RouteDescriptor<RouteObject>,
|
|
83
|
+
parent: AnyRoute,
|
|
84
|
+
options: RouteTreeOptions,
|
|
85
|
+
): AnyRoute {
|
|
86
|
+
const { source, location, children } = descriptor;
|
|
87
|
+
const routeOptions = createRouteOptions(source, options);
|
|
88
|
+
const route = createTanstackRoute({
|
|
89
|
+
...routeOptions,
|
|
90
|
+
...location,
|
|
91
|
+
getParentRoute: () => parent,
|
|
92
|
+
});
|
|
93
|
+
wrapRouteComponentWithModernContext(route, routeOptions.component, source.id);
|
|
94
|
+
if (children.length) {
|
|
95
|
+
route.addChildren(
|
|
96
|
+
children.map(child => createRouteFromDescriptor(child, route, options)),
|
|
110
97
|
);
|
|
111
|
-
route.addChildren(childRoutes);
|
|
112
98
|
}
|
|
113
|
-
|
|
114
99
|
return route;
|
|
115
100
|
}
|
|
116
101
|
|
|
117
|
-
function getRootLikeRouteObject(routes: RouteObject[]) {
|
|
118
|
-
return routes.find(route => route.path === '/' && !route.index);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
102
|
export function createRouteTreeFromRouteObjects(
|
|
122
103
|
routes: RouteObject[],
|
|
123
104
|
options: RouteTreeOptions = {},
|
|
124
105
|
): ModernTanstackRootRoute {
|
|
125
|
-
|
|
126
|
-
const rootLikeRoute = getRootLikeRouteObject(routes) as
|
|
127
|
-
| ModernRouteObject
|
|
128
|
-
| undefined;
|
|
129
|
-
const rootRevalidationState: RouteRevalidationState = {};
|
|
130
|
-
const rootShouldRevalidate = rootLikeRoute?.shouldRevalidate;
|
|
131
|
-
const rootShouldReload = createModernShouldReload(
|
|
132
|
-
rootShouldRevalidate,
|
|
133
|
-
rootRevalidationState,
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
const rootComponent = rootLikeRoute
|
|
137
|
-
? toRouteComponent(rootLikeRoute)
|
|
138
|
-
: undefined;
|
|
106
|
+
const { root, children } = describeRouteTree(routes);
|
|
139
107
|
const rootRouteOptions: TanstackRootRouteOptions = {
|
|
140
|
-
|
|
141
|
-
pendingComponent: rootLikeRoute
|
|
142
|
-
? toPendingComponent(rootLikeRoute)
|
|
143
|
-
: undefined,
|
|
144
|
-
errorComponent: rootLikeRoute ? toErrorComponent(rootLikeRoute) : undefined,
|
|
145
|
-
validateSearch: rootLikeRoute?.validateSearch,
|
|
146
|
-
loaderDeps: rootLikeRoute?.loaderDeps,
|
|
108
|
+
...(root ? createRouteOptions(root, options) : {}),
|
|
147
109
|
notFoundComponent: DefaultNotFound,
|
|
148
|
-
staticData: createRouteStaticData({
|
|
149
|
-
modernRouteId: rootLikeRoute?.id,
|
|
150
|
-
modernRouteAction: rootLikeRoute?.action,
|
|
151
|
-
modernRouteHandle: rootLikeRoute
|
|
152
|
-
? mergeModernRouteHandle(rootLikeRoute)
|
|
153
|
-
: undefined,
|
|
154
|
-
modernRouteHasAction:
|
|
155
|
-
rootLikeRoute?.hasAction || Boolean(rootLikeRoute?.action),
|
|
156
|
-
modernRouteHasClientLoader:
|
|
157
|
-
rootLikeRoute?.hasClientLoader ||
|
|
158
|
-
typeof rootLikeRoute?.clientData !== 'undefined',
|
|
159
|
-
modernRouteHasLoader:
|
|
160
|
-
rootLikeRoute?.hasLoader || typeof rootLikeRoute?.loader === 'function',
|
|
161
|
-
modernRouteIsClientComponent: rootLikeRoute?.isClientComponent,
|
|
162
|
-
modernRouteLoader: rootLikeRoute?.loader,
|
|
163
|
-
modernRouteShouldRevalidate: rootShouldRevalidate,
|
|
164
|
-
}),
|
|
165
|
-
loader: rootLikeRoute
|
|
166
|
-
? wrapRouteObjectLoader(rootLikeRoute, rootRevalidationState, options)
|
|
167
|
-
: undefined,
|
|
168
110
|
};
|
|
169
|
-
if (rootShouldReload) {
|
|
170
|
-
rootRouteOptions.shouldReload = rootShouldReload;
|
|
171
|
-
}
|
|
172
|
-
if (rootLikeRoute?.inValidSSRRoute) {
|
|
173
|
-
rootRouteOptions.ssr = false;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
111
|
const rootRoute = createTanstackRootRoute(rootRouteOptions);
|
|
177
|
-
if (
|
|
112
|
+
if (rootRouteOptions.component) {
|
|
178
113
|
rootRoute.options.component = withModernRouteMatchContext(
|
|
179
|
-
|
|
114
|
+
rootRouteOptions.component,
|
|
180
115
|
rootRouteId,
|
|
181
116
|
) as typeof rootRoute.options.component;
|
|
182
117
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
? [
|
|
186
|
-
...((rootLikeRoute.children as RouteObject[] | undefined) || []),
|
|
187
|
-
...routes.filter(route => route !== rootLikeRoute),
|
|
188
|
-
]
|
|
189
|
-
: routes;
|
|
190
|
-
|
|
191
|
-
const childRoutes = topLevel.map((routeObject, index) =>
|
|
192
|
-
createRouteFromRouteObject({
|
|
193
|
-
options,
|
|
194
|
-
parent: rootRoute,
|
|
195
|
-
pathlessFallbackId: `pathless-${index}`,
|
|
196
|
-
routeObject,
|
|
197
|
-
}),
|
|
118
|
+
rootRoute.addChildren(
|
|
119
|
+
children.map(child => createRouteFromDescriptor(child, rootRoute, options)),
|
|
198
120
|
);
|
|
199
|
-
|
|
200
|
-
rootRoute.addChildren(childRoutes);
|
|
201
121
|
return rootRoute as unknown as ModernTanstackRootRoute;
|
|
202
122
|
}
|
|
203
123
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { RouteObject } from '@modern-js/runtime-utils/router';
|
|
4
4
|
import {
|
|
5
5
|
getLoaderHref,
|
|
6
|
+
getLoaderSignal,
|
|
6
7
|
handleModernLoaderError,
|
|
7
8
|
handleModernLoaderResult,
|
|
8
9
|
mapSplatParamsForModernLoader,
|
|
@@ -118,10 +119,7 @@ export function wrapRouteObjectLoader(
|
|
|
118
119
|
} catch {}
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
const signal
|
|
122
|
-
ctx?.abortController?.signal ||
|
|
123
|
-
ctx?.signal ||
|
|
124
|
-
new AbortController().signal;
|
|
122
|
+
const signal = getLoaderSignal(ctx);
|
|
125
123
|
const baseRequest: Request | undefined =
|
|
126
124
|
ctx?.context?.request instanceof Request
|
|
127
125
|
? ctx.context.request
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
// @effect-diagnostics asyncFunction:off strictBooleanExpressions:off
|
|
2
2
|
|
|
3
|
-
import type { RouteObject } from '@modern-js/runtime-utils/router';
|
|
4
|
-
|
|
5
3
|
export function toTanstackPath(pathname: string): string {
|
|
6
4
|
// TanStack Router uses `$param` and `$` (splat) style params.
|
|
7
5
|
// Modern's conventional routing currently generates React Router style params (e.g. `:id`, `*`).
|
|
@@ -30,7 +28,3 @@ export function toTanstackPath(pathname: string): string {
|
|
|
30
28
|
})
|
|
31
29
|
.join('/');
|
|
32
30
|
}
|
|
33
|
-
|
|
34
|
-
export function isRouteObjectPathlessLayout(route: RouteObject) {
|
|
35
|
-
return !route.path && !route.index;
|
|
36
|
-
}
|
package/src/runtime/utils.tsx
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { toTanstackPath } from '../runtime/routeTree/paths';
|
|
2
|
+
|
|
3
|
+
type RouteShape = {
|
|
4
|
+
id?: string;
|
|
5
|
+
file?: string;
|
|
6
|
+
path?: string;
|
|
7
|
+
index?: boolean;
|
|
8
|
+
isRoot?: boolean;
|
|
9
|
+
children?: RouteShape[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Browser-safe structure shared by the executable tree and concrete TS emitter. */
|
|
13
|
+
export type RouteDescriptor<T extends RouteShape> = {
|
|
14
|
+
source: T;
|
|
15
|
+
location: { id: string; path?: never } | { path: string; id?: never };
|
|
16
|
+
hasSplat: boolean;
|
|
17
|
+
children: RouteDescriptor<T>[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function describeRouteTree<T extends RouteShape>(routes: T[]) {
|
|
21
|
+
const root = routes.find(
|
|
22
|
+
route =>
|
|
23
|
+
route.isRoot === true || (route.path === '/' && route.index !== true),
|
|
24
|
+
);
|
|
25
|
+
const describe = (route: T, index: number): RouteDescriptor<T> => ({
|
|
26
|
+
source: route,
|
|
27
|
+
location:
|
|
28
|
+
(route.path ?? '').length === 0 && route.index !== true
|
|
29
|
+
? { id: route.id || route.file || `pathless-${index}` }
|
|
30
|
+
: {
|
|
31
|
+
path: route.index === true ? '/' : toTanstackPath(route.path || ''),
|
|
32
|
+
},
|
|
33
|
+
hasSplat: Boolean(route.path?.includes('*')),
|
|
34
|
+
children: ((route.children ?? []) as T[]).map(describe),
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
root,
|
|
38
|
+
children: (root !== undefined
|
|
39
|
+
? [
|
|
40
|
+
...((root.children ?? []) as T[]),
|
|
41
|
+
...routes.filter(route => route !== root),
|
|
42
|
+
]
|
|
43
|
+
: routes
|
|
44
|
+
).map(describe),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -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
|
|
|
@@ -55,6 +55,7 @@ test('localized URLs preserve native loader and params route identity in SSR and
|
|
|
55
55
|
'/search': { en: '/find', cs: '/hledat' },
|
|
56
56
|
'/resources/:id': { en: '/resources/:id', cs: '/zdroje/:id' },
|
|
57
57
|
},
|
|
58
|
+
'canonical',
|
|
58
59
|
);
|
|
59
60
|
const routeObjects = createTanstackRouteObjectsFromConfig({
|
|
60
61
|
routesConfig: { routes },
|
|
@@ -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
|
+
});
|
|
@@ -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<
|
|
@@ -155,3 +157,146 @@ describe('tanstack server plugin router results', () => {
|
|
|
155
157
|
expect(status).not.toHaveBeenCalled();
|
|
156
158
|
});
|
|
157
159
|
});
|
|
160
|
+
|
|
161
|
+
describe('TanStack preparation resource lifetime', () => {
|
|
162
|
+
test.each([
|
|
163
|
+
'attach',
|
|
164
|
+
'load',
|
|
165
|
+
'result',
|
|
166
|
+
'preload',
|
|
167
|
+
'dehydrate',
|
|
168
|
+
'abort',
|
|
169
|
+
'clientRender',
|
|
170
|
+
'afterCreate',
|
|
171
|
+
])('disposes exactly once when %s fails', async stage => {
|
|
172
|
+
const failure = new Error(`${stage} failed`);
|
|
173
|
+
const cleanup = rstest.fn();
|
|
174
|
+
const controller = new AbortController();
|
|
175
|
+
const { context } = createServerContext('/');
|
|
176
|
+
context.ssrContext!.request.raw = new Request('http://localhost/', {
|
|
177
|
+
signal: controller.signal,
|
|
178
|
+
});
|
|
179
|
+
const attach = ssrUtils.attachServerSsrUtils;
|
|
180
|
+
rstest
|
|
181
|
+
.spyOn(ssrUtils, 'attachServerSsrUtils')
|
|
182
|
+
.mockImplementation(async router => {
|
|
183
|
+
await attach(router);
|
|
184
|
+
rstest.spyOn(router.serverSsr!, 'cleanup').mockImplementation(cleanup);
|
|
185
|
+
if (stage === 'attach') throw failure;
|
|
186
|
+
if (stage === 'load')
|
|
187
|
+
rstest.spyOn(router, 'load').mockRejectedValue(failure);
|
|
188
|
+
if (stage === 'result')
|
|
189
|
+
rstest.spyOn(router, 'load').mockResolvedValue(undefined);
|
|
190
|
+
if (stage === 'dehydrate')
|
|
191
|
+
rstest
|
|
192
|
+
.spyOn(router.serverSsr!, 'dehydrate')
|
|
193
|
+
.mockRejectedValue(failure);
|
|
194
|
+
if (stage === 'abort') controller.abort(failure);
|
|
195
|
+
});
|
|
196
|
+
if (stage === 'clientRender') {
|
|
197
|
+
context.ssrContext!.loaderFailureMode = 'clientRender';
|
|
198
|
+
rstest
|
|
199
|
+
.spyOn(ssrUtils, 'collectRouterErrors')
|
|
200
|
+
.mockReturnValue({ root: failure });
|
|
201
|
+
}
|
|
202
|
+
if (stage === 'afterCreate')
|
|
203
|
+
rstest
|
|
204
|
+
.spyOn(routerProviderRegistryHooks.onAfterCreateRouter, 'call')
|
|
205
|
+
.mockImplementation(() => {
|
|
206
|
+
throw failure;
|
|
207
|
+
});
|
|
208
|
+
if (stage === 'preload')
|
|
209
|
+
rstest
|
|
210
|
+
.spyOn(ssrPreload, 'preloadMatchedRouteComponents')
|
|
211
|
+
.mockRejectedValue(failure);
|
|
212
|
+
const beforeRender = collectBeforeRender(() => [
|
|
213
|
+
{ id: 'root', path: '/', Component: () => null },
|
|
214
|
+
]);
|
|
215
|
+
await expect(beforeRender(context, value => value)).rejects.toThrow(
|
|
216
|
+
stage === 'result' ? 'without a server result' : failure.message,
|
|
217
|
+
);
|
|
218
|
+
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
219
|
+
await getRouterRuntimeState(context)?.cleanup?.();
|
|
220
|
+
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test.each([
|
|
224
|
+
'string',
|
|
225
|
+
'stream',
|
|
226
|
+
] as const)('retains %s router resources for response termination', async mode => {
|
|
227
|
+
const { context } = createServerContext('/products/shoe?q=1#detail');
|
|
228
|
+
context.ssrContext!.mode = mode;
|
|
229
|
+
const beforeRender = collectBeforeRender(() => [
|
|
230
|
+
{ id: 'product', path: '/products/:id', Component: () => null },
|
|
231
|
+
]);
|
|
232
|
+
await beforeRender(context, value => value);
|
|
233
|
+
const state = getRouterRuntimeState(context)!;
|
|
234
|
+
const router = state.instance as AnyRouter;
|
|
235
|
+
const cleanup = rstest.spyOn(router.serverSsr!, 'cleanup');
|
|
236
|
+
const navigation = state.navigation!;
|
|
237
|
+
expect(navigation.getSnapshot()).toMatchObject({
|
|
238
|
+
location: { pathname: '/products/shoe', search: '?q=1', hash: '#detail' },
|
|
239
|
+
params: { id: 'shoe' },
|
|
240
|
+
});
|
|
241
|
+
expect(navigation.getSnapshot()).toBe(navigation.getSnapshot());
|
|
242
|
+
expect(navigation.Link).toBe(context.router?.Link);
|
|
243
|
+
expect(
|
|
244
|
+
navigation.createLinkProps!({
|
|
245
|
+
pathname: '/products/shoe',
|
|
246
|
+
href: '/products/shoe?q=2#detail',
|
|
247
|
+
search: { q: '2' },
|
|
248
|
+
hash: 'detail',
|
|
249
|
+
hashScrollIntoView: false,
|
|
250
|
+
prefetch: 'none',
|
|
251
|
+
}),
|
|
252
|
+
).toEqual({
|
|
253
|
+
to: '/products/shoe',
|
|
254
|
+
search: { q: '2' },
|
|
255
|
+
hash: 'detail',
|
|
256
|
+
hashScrollIntoView: false,
|
|
257
|
+
preload: false,
|
|
258
|
+
});
|
|
259
|
+
expect(cleanup).not.toHaveBeenCalled();
|
|
260
|
+
await state.cleanup?.();
|
|
261
|
+
await state.cleanup?.();
|
|
262
|
+
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('incoming abort reaches an active Modern loader and disposes its router', async () => {
|
|
267
|
+
const incoming = new AbortController();
|
|
268
|
+
let started!: () => void;
|
|
269
|
+
const loading = new Promise<void>(resolve => {
|
|
270
|
+
started = resolve;
|
|
271
|
+
});
|
|
272
|
+
const { context } = createServerContext('/');
|
|
273
|
+
context.ssrContext!.request.raw = new Request('http://localhost/', {
|
|
274
|
+
signal: incoming.signal,
|
|
275
|
+
});
|
|
276
|
+
const failure = new Error('client disconnected during loader');
|
|
277
|
+
const beforeRender = collectBeforeRender(() => [
|
|
278
|
+
{
|
|
279
|
+
id: 'root',
|
|
280
|
+
path: '/',
|
|
281
|
+
Component: () => null,
|
|
282
|
+
loader: ({ request }: { request: Request }) =>
|
|
283
|
+
new Promise((_resolve, reject) => {
|
|
284
|
+
request.signal.addEventListener(
|
|
285
|
+
'abort',
|
|
286
|
+
() => reject(request.signal.reason),
|
|
287
|
+
{ once: true },
|
|
288
|
+
);
|
|
289
|
+
started();
|
|
290
|
+
}),
|
|
291
|
+
},
|
|
292
|
+
]);
|
|
293
|
+
const pending = beforeRender(context, value => value);
|
|
294
|
+
await loading;
|
|
295
|
+
const router = getRouterRuntimeState(context)!.instance as AnyRouter;
|
|
296
|
+
const cleanup = rstest.spyOn(router.serverSsr!, 'cleanup');
|
|
297
|
+
incoming.abort(failure);
|
|
298
|
+
await expect(pending).rejects.toBe(failure);
|
|
299
|
+
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
300
|
+
await getRouterRuntimeState(context)?.cleanup?.();
|
|
301
|
+
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
302
|
+
});
|