@bleedingdev/modern-js-plugin-tanstack 3.5.0-ultramodern.27 → 3.5.0-ultramodern.29
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 +21 -0
- package/dist/cjs/runtime/plugin.node.js +8 -7
- package/dist/cjs/runtime/prefetchLink.js +15 -2
- package/dist/esm/cli/index.mjs +19 -1
- package/dist/esm/runtime/plugin.node.mjs +8 -7
- package/dist/esm/runtime/prefetchLink.mjs +18 -5
- package/dist/esm-node/cli/index.mjs +19 -1
- package/dist/esm-node/runtime/plugin.node.mjs +8 -7
- package/dist/esm-node/runtime/prefetchLink.mjs +18 -5
- package/dist/types/cli/index.d.ts +11 -0
- package/package.json +10 -9
- package/rstest.config.mts +2 -0
- package/src/cli/index.ts +34 -1
- package/src/runtime/plugin.node.tsx +16 -7
- package/src/runtime/prefetchLink.tsx +78 -15
- package/tests/router/cli.test.ts +7 -0
- package/tests/router/generateRouteArtifacts.test.ts +61 -0
- package/tests/router/prefetchLink.test.tsx +106 -59
- package/tests/router/prefetchLinkPreload.test.tsx +85 -0
package/dist/cjs/cli/index.js
CHANGED
|
@@ -55,6 +55,7 @@ var __webpack_exports__ = {};
|
|
|
55
55
|
collectCanonicalRoutesForEntry: ()=>external_tanstackTypes_js_namespaceObject.collectCanonicalRoutesForEntry,
|
|
56
56
|
createTanstackRsbuildRouteSplittingProfile: ()=>external_routeSplitting_js_namespaceObject.createTanstackRsbuildRouteSplittingProfile,
|
|
57
57
|
default: ()=>src_cli,
|
|
58
|
+
generateTanstackRouteArtifacts: ()=>generateTanstackRouteArtifacts,
|
|
58
59
|
generateTanstackRouterTypesSourceForEntry: ()=>external_tanstackTypes_js_namespaceObject.generateTanstackRouterTypesSourceForEntry,
|
|
59
60
|
resolveTanstackRouteCodeSplittingEnabled: ()=>external_routeSplitting_js_namespaceObject.resolveTanstackRouteCodeSplittingEnabled,
|
|
60
61
|
tanstackRouterPlugin: ()=>tanstackRouterPlugin,
|
|
@@ -295,11 +296,30 @@ ${canonicalRoutesAugmentation}`;
|
|
|
295
296
|
}
|
|
296
297
|
};
|
|
297
298
|
}
|
|
299
|
+
async function generateTanstackRouteArtifacts(opts) {
|
|
300
|
+
const [{ createRunOptions }, { cli }, utils] = await Promise.all([
|
|
301
|
+
import("@modern-js/app-tools/cli/run"),
|
|
302
|
+
import("@modern-js/plugin/cli"),
|
|
303
|
+
import("@modern-js/utils")
|
|
304
|
+
]);
|
|
305
|
+
process.env.MODERN_ARGV = 'node modern build';
|
|
306
|
+
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
|
|
307
|
+
const runOptions = await createRunOptions({
|
|
308
|
+
cwd: opts.appDirectory,
|
|
309
|
+
version: opts.version || process.env.MODERN_JS_VERSION || '0.0.0-routes-generate',
|
|
310
|
+
internalPlugins: utils.INTERNAL_RUNTIME_PLUGINS
|
|
311
|
+
});
|
|
312
|
+
await cli.init({
|
|
313
|
+
...runOptions,
|
|
314
|
+
command: 'build'
|
|
315
|
+
});
|
|
316
|
+
}
|
|
298
317
|
const src_cli = tanstackRouterPlugin;
|
|
299
318
|
})();
|
|
300
319
|
exports.collectCanonicalRoutesForEntry = __webpack_exports__.collectCanonicalRoutesForEntry;
|
|
301
320
|
exports.createTanstackRsbuildRouteSplittingProfile = __webpack_exports__.createTanstackRsbuildRouteSplittingProfile;
|
|
302
321
|
exports["default"] = __webpack_exports__["default"];
|
|
322
|
+
exports.generateTanstackRouteArtifacts = __webpack_exports__.generateTanstackRouteArtifacts;
|
|
303
323
|
exports.generateTanstackRouterTypesSourceForEntry = __webpack_exports__.generateTanstackRouterTypesSourceForEntry;
|
|
304
324
|
exports.resolveTanstackRouteCodeSplittingEnabled = __webpack_exports__.resolveTanstackRouteCodeSplittingEnabled;
|
|
305
325
|
exports.tanstackRouterPlugin = __webpack_exports__.tanstackRouterPlugin;
|
|
@@ -309,6 +329,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
|
309
329
|
"collectCanonicalRoutesForEntry",
|
|
310
330
|
"createTanstackRsbuildRouteSplittingProfile",
|
|
311
331
|
"default",
|
|
332
|
+
"generateTanstackRouteArtifacts",
|
|
312
333
|
"generateTanstackRouterTypesSourceForEntry",
|
|
313
334
|
"resolveTanstackRouteCodeSplittingEnabled",
|
|
314
335
|
"tanstackRouterPlugin",
|
|
@@ -167,13 +167,14 @@ const tanstackRouterPlugin = (userConfig = {})=>{
|
|
|
167
167
|
if (!modifiedRouteObjects.length) return;
|
|
168
168
|
const { request, nonce, baseUrl, loaderFailureMode = 'errorBoundary' } = context.ssrContext;
|
|
169
169
|
const _basename = '/' === baseUrl ? (0, external_utils_js_namespaceObject.urlJoin)(baseUrl, basename || '') : baseUrl;
|
|
170
|
-
const
|
|
171
|
-
const
|
|
170
|
+
const rawRequest = request.raw;
|
|
171
|
+
const initialHref = createGetSsrHref(rawRequest);
|
|
172
|
+
const isRSCNavigation = enableRsc && 'true' === rawRequest.headers.get('x-rsc-tree');
|
|
172
173
|
const requestContext = (0, node_namespaceObject.createRequestContext)(context.ssrContext?.loaderContext);
|
|
173
174
|
const controller = new AbortController();
|
|
174
|
-
const ssrRequest = new Request(
|
|
175
|
+
const ssrRequest = new Request(rawRequest.url, {
|
|
175
176
|
method: 'GET',
|
|
176
|
-
headers:
|
|
177
|
+
headers: rawRequest.headers,
|
|
177
178
|
signal: controller.signal
|
|
178
179
|
});
|
|
179
180
|
const routerContext = {
|
|
@@ -201,7 +202,7 @@ const tanstackRouterPlugin = (userConfig = {})=>{
|
|
|
201
202
|
history,
|
|
202
203
|
basepath: '/',
|
|
203
204
|
rewrite,
|
|
204
|
-
origin: new URL(
|
|
205
|
+
origin: new URL(rawRequest.url).origin,
|
|
205
206
|
ssr: {
|
|
206
207
|
nonce
|
|
207
208
|
},
|
|
@@ -230,14 +231,14 @@ const tanstackRouterPlugin = (userConfig = {})=>{
|
|
|
230
231
|
}
|
|
231
232
|
const routerErrors = collectRouterErrors(tanstackRouter);
|
|
232
233
|
if (routerErrors && 'clientRender' === loaderFailureMode) {
|
|
233
|
-
context.ssrContext?.response
|
|
234
|
+
context.ssrContext?.response?.status(200);
|
|
234
235
|
try {
|
|
235
236
|
serverRouter.serverSsr?.cleanup?.();
|
|
236
237
|
} catch {}
|
|
237
238
|
throw Object.values(routerErrors)[0];
|
|
238
239
|
}
|
|
239
240
|
await preloadMatchedRouteComponents(serverRouter);
|
|
240
|
-
context.ssrContext?.response
|
|
241
|
+
context.ssrContext?.response?.status(tanstackRouter.state.statusCode);
|
|
241
242
|
await serverRouter.serverSsr?.dehydrate?.();
|
|
242
243
|
if (isRSCNavigation) {
|
|
243
244
|
await waitForRouterSerialization(serverRouter);
|
|
@@ -40,10 +40,23 @@ function resolvePreloadFromPrefetch(prefetch, preload) {
|
|
|
40
40
|
return 'viewport';
|
|
41
41
|
}
|
|
42
42
|
const LinkComponentImpl = (props)=>{
|
|
43
|
-
const { prefetch, preload, ...rest } = props;
|
|
44
|
-
|
|
43
|
+
const { prefetch, preload, 'aria-current': ariaCurrentProp, ref, children, ...rest } = props;
|
|
44
|
+
const hasAriaCurrentOverride = 'aria-current' in props;
|
|
45
|
+
const linkOptions = {
|
|
45
46
|
...rest,
|
|
46
47
|
preload: resolvePreloadFromPrefetch(prefetch, preload)
|
|
48
|
+
};
|
|
49
|
+
const linkProps = (0, react_router_namespaceObject.useLinkProps)(linkOptions, ref);
|
|
50
|
+
const { disabled: _disabled, type: _type, ...anchorProps } = linkProps;
|
|
51
|
+
if (hasAriaCurrentOverride) if (false === ariaCurrentProp || void 0 === ariaCurrentProp) delete anchorProps['aria-current'];
|
|
52
|
+
else anchorProps['aria-current'] = ariaCurrentProp;
|
|
53
|
+
const resolvedChildren = 'function' == typeof children ? children({
|
|
54
|
+
isActive: 'active' === anchorProps['data-status'],
|
|
55
|
+
isTransitioning: 'transitioning' === anchorProps['data-transitioning']
|
|
56
|
+
}) : children;
|
|
57
|
+
return /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("a", {
|
|
58
|
+
...anchorProps,
|
|
59
|
+
children: resolvedChildren
|
|
47
60
|
});
|
|
48
61
|
};
|
|
49
62
|
const Link = LinkComponentImpl;
|
package/dist/esm/cli/index.mjs
CHANGED
|
@@ -238,6 +238,24 @@ function tanstackRouterPlugin(options = {}) {
|
|
|
238
238
|
}
|
|
239
239
|
};
|
|
240
240
|
}
|
|
241
|
+
async function generateTanstackRouteArtifacts(opts) {
|
|
242
|
+
const [{ createRunOptions }, { cli }, utils] = await Promise.all([
|
|
243
|
+
import("@modern-js/app-tools/cli/run"),
|
|
244
|
+
import("@modern-js/plugin/cli"),
|
|
245
|
+
import("@modern-js/utils")
|
|
246
|
+
]);
|
|
247
|
+
process.env.MODERN_ARGV = 'node modern build';
|
|
248
|
+
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
|
|
249
|
+
const runOptions = await createRunOptions({
|
|
250
|
+
cwd: opts.appDirectory,
|
|
251
|
+
version: opts.version || process.env.MODERN_JS_VERSION || '0.0.0-routes-generate',
|
|
252
|
+
internalPlugins: utils.INTERNAL_RUNTIME_PLUGINS
|
|
253
|
+
});
|
|
254
|
+
await cli.init({
|
|
255
|
+
...runOptions,
|
|
256
|
+
command: 'build'
|
|
257
|
+
});
|
|
258
|
+
}
|
|
241
259
|
const src_cli = tanstackRouterPlugin;
|
|
242
260
|
export default src_cli;
|
|
243
|
-
export { collectCanonicalRoutesForEntry, createTanstackRsbuildRouteSplittingProfile, generateTanstackRouterTypesSourceForEntry, resolveTanstackRouteCodeSplittingEnabled, tanstackRouterPlugin, writeTanstackRegisterFile, writeTanstackRouterTypesForEntries };
|
|
261
|
+
export { collectCanonicalRoutesForEntry, createTanstackRsbuildRouteSplittingProfile, generateTanstackRouteArtifacts, generateTanstackRouterTypesSourceForEntry, resolveTanstackRouteCodeSplittingEnabled, tanstackRouterPlugin, writeTanstackRegisterFile, writeTanstackRouterTypesForEntries };
|
|
@@ -134,13 +134,14 @@ const tanstackRouterPlugin = (userConfig = {})=>{
|
|
|
134
134
|
if (!modifiedRouteObjects.length) return;
|
|
135
135
|
const { request, nonce, baseUrl, loaderFailureMode = 'errorBoundary' } = context.ssrContext;
|
|
136
136
|
const _basename = '/' === baseUrl ? urlJoin(baseUrl, basename || '') : baseUrl;
|
|
137
|
-
const
|
|
138
|
-
const
|
|
137
|
+
const rawRequest = request.raw;
|
|
138
|
+
const initialHref = createGetSsrHref(rawRequest);
|
|
139
|
+
const isRSCNavigation = enableRsc && 'true' === rawRequest.headers.get('x-rsc-tree');
|
|
139
140
|
const requestContext = createRequestContext(context.ssrContext?.loaderContext);
|
|
140
141
|
const controller = new AbortController();
|
|
141
|
-
const ssrRequest = new Request(
|
|
142
|
+
const ssrRequest = new Request(rawRequest.url, {
|
|
142
143
|
method: 'GET',
|
|
143
|
-
headers:
|
|
144
|
+
headers: rawRequest.headers,
|
|
144
145
|
signal: controller.signal
|
|
145
146
|
});
|
|
146
147
|
const routerContext = {
|
|
@@ -168,7 +169,7 @@ const tanstackRouterPlugin = (userConfig = {})=>{
|
|
|
168
169
|
history,
|
|
169
170
|
basepath: '/',
|
|
170
171
|
rewrite,
|
|
171
|
-
origin: new URL(
|
|
172
|
+
origin: new URL(rawRequest.url).origin,
|
|
172
173
|
ssr: {
|
|
173
174
|
nonce
|
|
174
175
|
},
|
|
@@ -197,14 +198,14 @@ const tanstackRouterPlugin = (userConfig = {})=>{
|
|
|
197
198
|
}
|
|
198
199
|
const routerErrors = collectRouterErrors(tanstackRouter);
|
|
199
200
|
if (routerErrors && 'clientRender' === loaderFailureMode) {
|
|
200
|
-
context.ssrContext?.response
|
|
201
|
+
context.ssrContext?.response?.status(200);
|
|
201
202
|
try {
|
|
202
203
|
serverRouter.serverSsr?.cleanup?.();
|
|
203
204
|
} catch {}
|
|
204
205
|
throw Object.values(routerErrors)[0];
|
|
205
206
|
}
|
|
206
207
|
await preloadMatchedRouteComponents(serverRouter);
|
|
207
|
-
context.ssrContext?.response
|
|
208
|
+
context.ssrContext?.response?.status(tanstackRouter.state.statusCode);
|
|
208
209
|
await serverRouter.serverSsr?.dehydrate?.();
|
|
209
210
|
if (isRSCNavigation) {
|
|
210
211
|
await waitForRouterSerialization(serverRouter);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useLinkProps } from "@tanstack/react-router";
|
|
3
3
|
function resolvePreloadFromPrefetch(prefetch, preload) {
|
|
4
4
|
if (void 0 !== preload) return preload;
|
|
5
5
|
if ('none' === prefetch) return false;
|
|
@@ -7,12 +7,25 @@ function resolvePreloadFromPrefetch(prefetch, preload) {
|
|
|
7
7
|
return 'viewport';
|
|
8
8
|
}
|
|
9
9
|
const LinkComponentImpl = (props)=>{
|
|
10
|
-
const { prefetch, preload, ...rest } = props;
|
|
11
|
-
|
|
10
|
+
const { prefetch, preload, 'aria-current': ariaCurrentProp, ref, children, ...rest } = props;
|
|
11
|
+
const hasAriaCurrentOverride = 'aria-current' in props;
|
|
12
|
+
const linkOptions = {
|
|
12
13
|
...rest,
|
|
13
14
|
preload: resolvePreloadFromPrefetch(prefetch, preload)
|
|
15
|
+
};
|
|
16
|
+
const linkProps = useLinkProps(linkOptions, ref);
|
|
17
|
+
const { disabled: _disabled, type: _type, ...anchorProps } = linkProps;
|
|
18
|
+
if (hasAriaCurrentOverride) if (false === ariaCurrentProp || void 0 === ariaCurrentProp) delete anchorProps['aria-current'];
|
|
19
|
+
else anchorProps['aria-current'] = ariaCurrentProp;
|
|
20
|
+
const resolvedChildren = 'function' == typeof children ? children({
|
|
21
|
+
isActive: 'active' === anchorProps['data-status'],
|
|
22
|
+
isTransitioning: 'transitioning' === anchorProps['data-transitioning']
|
|
23
|
+
}) : children;
|
|
24
|
+
return /*#__PURE__*/ jsx("a", {
|
|
25
|
+
...anchorProps,
|
|
26
|
+
children: resolvedChildren
|
|
14
27
|
});
|
|
15
28
|
};
|
|
16
|
-
const
|
|
29
|
+
const Link = LinkComponentImpl;
|
|
17
30
|
const NavLink = LinkComponentImpl;
|
|
18
|
-
export {
|
|
31
|
+
export { Link, NavLink };
|
|
@@ -242,6 +242,24 @@ function tanstackRouterPlugin(options = {}) {
|
|
|
242
242
|
}
|
|
243
243
|
};
|
|
244
244
|
}
|
|
245
|
+
async function generateTanstackRouteArtifacts(opts) {
|
|
246
|
+
const [{ createRunOptions }, { cli }, utils] = await Promise.all([
|
|
247
|
+
import("@modern-js/app-tools/cli/run"),
|
|
248
|
+
import("@modern-js/plugin/cli"),
|
|
249
|
+
import("@modern-js/utils")
|
|
250
|
+
]);
|
|
251
|
+
process.env.MODERN_ARGV = 'node modern build';
|
|
252
|
+
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
|
|
253
|
+
const runOptions = await createRunOptions({
|
|
254
|
+
cwd: opts.appDirectory,
|
|
255
|
+
version: opts.version || process.env.MODERN_JS_VERSION || '0.0.0-routes-generate',
|
|
256
|
+
internalPlugins: utils.INTERNAL_RUNTIME_PLUGINS
|
|
257
|
+
});
|
|
258
|
+
await cli.init({
|
|
259
|
+
...runOptions,
|
|
260
|
+
command: 'build'
|
|
261
|
+
});
|
|
262
|
+
}
|
|
245
263
|
const src_cli = tanstackRouterPlugin;
|
|
246
264
|
export default src_cli;
|
|
247
|
-
export { collectCanonicalRoutesForEntry, createTanstackRsbuildRouteSplittingProfile, generateTanstackRouterTypesSourceForEntry, resolveTanstackRouteCodeSplittingEnabled, tanstackRouterPlugin, writeTanstackRegisterFile, writeTanstackRouterTypesForEntries };
|
|
265
|
+
export { collectCanonicalRoutesForEntry, createTanstackRsbuildRouteSplittingProfile, generateTanstackRouteArtifacts, generateTanstackRouterTypesSourceForEntry, resolveTanstackRouteCodeSplittingEnabled, tanstackRouterPlugin, writeTanstackRegisterFile, writeTanstackRouterTypesForEntries };
|
|
@@ -135,13 +135,14 @@ const tanstackRouterPlugin = (userConfig = {})=>{
|
|
|
135
135
|
if (!modifiedRouteObjects.length) return;
|
|
136
136
|
const { request, nonce, baseUrl, loaderFailureMode = 'errorBoundary' } = context.ssrContext;
|
|
137
137
|
const _basename = '/' === baseUrl ? urlJoin(baseUrl, basename || '') : baseUrl;
|
|
138
|
-
const
|
|
139
|
-
const
|
|
138
|
+
const rawRequest = request.raw;
|
|
139
|
+
const initialHref = createGetSsrHref(rawRequest);
|
|
140
|
+
const isRSCNavigation = enableRsc && 'true' === rawRequest.headers.get('x-rsc-tree');
|
|
140
141
|
const requestContext = createRequestContext(context.ssrContext?.loaderContext);
|
|
141
142
|
const controller = new AbortController();
|
|
142
|
-
const ssrRequest = new Request(
|
|
143
|
+
const ssrRequest = new Request(rawRequest.url, {
|
|
143
144
|
method: 'GET',
|
|
144
|
-
headers:
|
|
145
|
+
headers: rawRequest.headers,
|
|
145
146
|
signal: controller.signal
|
|
146
147
|
});
|
|
147
148
|
const routerContext = {
|
|
@@ -169,7 +170,7 @@ const tanstackRouterPlugin = (userConfig = {})=>{
|
|
|
169
170
|
history,
|
|
170
171
|
basepath: '/',
|
|
171
172
|
rewrite,
|
|
172
|
-
origin: new URL(
|
|
173
|
+
origin: new URL(rawRequest.url).origin,
|
|
173
174
|
ssr: {
|
|
174
175
|
nonce
|
|
175
176
|
},
|
|
@@ -198,14 +199,14 @@ const tanstackRouterPlugin = (userConfig = {})=>{
|
|
|
198
199
|
}
|
|
199
200
|
const routerErrors = collectRouterErrors(tanstackRouter);
|
|
200
201
|
if (routerErrors && 'clientRender' === loaderFailureMode) {
|
|
201
|
-
context.ssrContext?.response
|
|
202
|
+
context.ssrContext?.response?.status(200);
|
|
202
203
|
try {
|
|
203
204
|
serverRouter.serverSsr?.cleanup?.();
|
|
204
205
|
} catch {}
|
|
205
206
|
throw Object.values(routerErrors)[0];
|
|
206
207
|
}
|
|
207
208
|
await preloadMatchedRouteComponents(serverRouter);
|
|
208
|
-
context.ssrContext?.response
|
|
209
|
+
context.ssrContext?.response?.status(tanstackRouter.state.statusCode);
|
|
209
210
|
await serverRouter.serverSsr?.dehydrate?.();
|
|
210
211
|
if (isRSCNavigation) {
|
|
211
212
|
await waitForRouterSerialization(serverRouter);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { useLinkProps } from "@tanstack/react-router";
|
|
4
4
|
function resolvePreloadFromPrefetch(prefetch, preload) {
|
|
5
5
|
if (void 0 !== preload) return preload;
|
|
6
6
|
if ('none' === prefetch) return false;
|
|
@@ -8,12 +8,25 @@ function resolvePreloadFromPrefetch(prefetch, preload) {
|
|
|
8
8
|
return 'viewport';
|
|
9
9
|
}
|
|
10
10
|
const LinkComponentImpl = (props)=>{
|
|
11
|
-
const { prefetch, preload, ...rest } = props;
|
|
12
|
-
|
|
11
|
+
const { prefetch, preload, 'aria-current': ariaCurrentProp, ref, children, ...rest } = props;
|
|
12
|
+
const hasAriaCurrentOverride = 'aria-current' in props;
|
|
13
|
+
const linkOptions = {
|
|
13
14
|
...rest,
|
|
14
15
|
preload: resolvePreloadFromPrefetch(prefetch, preload)
|
|
16
|
+
};
|
|
17
|
+
const linkProps = useLinkProps(linkOptions, ref);
|
|
18
|
+
const { disabled: _disabled, type: _type, ...anchorProps } = linkProps;
|
|
19
|
+
if (hasAriaCurrentOverride) if (false === ariaCurrentProp || void 0 === ariaCurrentProp) delete anchorProps['aria-current'];
|
|
20
|
+
else anchorProps['aria-current'] = ariaCurrentProp;
|
|
21
|
+
const resolvedChildren = 'function' == typeof children ? children({
|
|
22
|
+
isActive: 'active' === anchorProps['data-status'],
|
|
23
|
+
isTransitioning: 'transitioning' === anchorProps['data-transitioning']
|
|
24
|
+
}) : children;
|
|
25
|
+
return /*#__PURE__*/ jsx("a", {
|
|
26
|
+
...anchorProps,
|
|
27
|
+
children: resolvedChildren
|
|
15
28
|
});
|
|
16
29
|
};
|
|
17
|
-
const
|
|
30
|
+
const Link = LinkComponentImpl;
|
|
18
31
|
const NavLink = LinkComponentImpl;
|
|
19
|
-
export {
|
|
32
|
+
export { Link, NavLink };
|
|
@@ -30,4 +30,15 @@ export declare function writeTanstackRouterTypesForEntries(opts: {
|
|
|
30
30
|
i18nPluginInstalled?: boolean;
|
|
31
31
|
}): Promise<void>;
|
|
32
32
|
export declare function tanstackRouterPlugin(options?: TanstackRouterPluginOptions): CliPlugin<AppTools>;
|
|
33
|
+
/**
|
|
34
|
+
* Headless regeneration of `router.gen.ts` / `register.gen.d.ts` for a generated
|
|
35
|
+
* app, without running a full dev/build. Drives the app-tools analyze `onPrepare`
|
|
36
|
+
* (which fires `generateEntryCode`) and returns before any bundling — the app's
|
|
37
|
+
* own registered plugin-tanstack writes the artifacts via its `generateEntryCode`
|
|
38
|
+
* subscriber. Reuses the exact hook dev/build use, so there is no scanning drift.
|
|
39
|
+
*/
|
|
40
|
+
export declare function generateTanstackRouteArtifacts(opts: {
|
|
41
|
+
appDirectory: string;
|
|
42
|
+
version?: string;
|
|
43
|
+
}): Promise<void>;
|
|
33
44
|
export default tanstackRouterPlugin;
|
package/package.json
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"modern.js",
|
|
19
19
|
"tanstack-router"
|
|
20
20
|
],
|
|
21
|
-
"version": "3.5.0-ultramodern.
|
|
21
|
+
"version": "3.5.0-ultramodern.29",
|
|
22
22
|
"engines": {
|
|
23
23
|
"node": ">=20"
|
|
24
24
|
},
|
|
@@ -77,13 +77,14 @@
|
|
|
77
77
|
"@swc/helpers": "^0.5.23",
|
|
78
78
|
"@tanstack/react-router": "1.170.16",
|
|
79
79
|
"@tanstack/router-core": "1.171.13",
|
|
80
|
-
"@modern-js/
|
|
81
|
-
"@modern-js/types": "npm:@bleedingdev/modern-js-types@3.5.0-ultramodern.
|
|
82
|
-
"@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.5.0-ultramodern.
|
|
83
|
-
"@modern-js/
|
|
80
|
+
"@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.5.0-ultramodern.29",
|
|
81
|
+
"@modern-js/types": "npm:@bleedingdev/modern-js-types@3.5.0-ultramodern.29",
|
|
82
|
+
"@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.5.0-ultramodern.29",
|
|
83
|
+
"@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.5.0-ultramodern.29"
|
|
84
84
|
},
|
|
85
85
|
"peerDependencies": {
|
|
86
|
-
"@modern-js/
|
|
86
|
+
"@modern-js/app-tools": "3.5.0-ultramodern.29",
|
|
87
|
+
"@modern-js/runtime": "3.5.0-ultramodern.29",
|
|
87
88
|
"react": "^19.2.7",
|
|
88
89
|
"react-dom": "^19.2.7"
|
|
89
90
|
},
|
|
@@ -98,9 +99,9 @@
|
|
|
98
99
|
"@typescript/native-preview": "7.0.0-dev.20260628.1",
|
|
99
100
|
"react": "^19.2.7",
|
|
100
101
|
"react-dom": "^19.2.7",
|
|
101
|
-
"@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.5.0-ultramodern.
|
|
102
|
-
"@
|
|
103
|
-
"@
|
|
102
|
+
"@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.5.0-ultramodern.29",
|
|
103
|
+
"@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.5.0-ultramodern.29",
|
|
104
|
+
"@scripts/rstest-config": "2.66.0"
|
|
104
105
|
},
|
|
105
106
|
"sideEffects": [
|
|
106
107
|
"./src/runtime/register.ts",
|
package/rstest.config.mts
CHANGED
|
@@ -25,6 +25,7 @@ export default {
|
|
|
25
25
|
include: [
|
|
26
26
|
'tests/router/cli.test.ts',
|
|
27
27
|
'tests/router/fastDefaults.test.ts',
|
|
28
|
+
'tests/router/generateRouteArtifacts.test.ts',
|
|
28
29
|
'tests/router/hooks.test.ts',
|
|
29
30
|
'tests/router/loaderBridge.test.ts',
|
|
30
31
|
'tests/router/packageSurface.test.ts',
|
|
@@ -44,6 +45,7 @@ export default {
|
|
|
44
45
|
'tests/router/dataMutation.test.tsx',
|
|
45
46
|
'tests/router/hydrationBoundary.test.tsx',
|
|
46
47
|
'tests/router/prefetchLink.test.tsx',
|
|
48
|
+
'tests/router/prefetchLinkPreload.test.tsx',
|
|
47
49
|
],
|
|
48
50
|
extends: commonConfig,
|
|
49
51
|
}),
|
package/src/cli/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @effect-diagnostics asyncFunction:off nodeBuiltinImport:off strictBooleanExpressions:off
|
|
1
|
+
// @effect-diagnostics asyncFunction:off nodeBuiltinImport:off strictBooleanExpressions:off processEnv:off processEnvInEffect:off
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import type {
|
|
4
4
|
AppNormalizedConfig,
|
|
@@ -518,4 +518,37 @@ export function tanstackRouterPlugin(
|
|
|
518
518
|
};
|
|
519
519
|
}
|
|
520
520
|
|
|
521
|
+
/**
|
|
522
|
+
* Headless regeneration of `router.gen.ts` / `register.gen.d.ts` for a generated
|
|
523
|
+
* app, without running a full dev/build. Drives the app-tools analyze `onPrepare`
|
|
524
|
+
* (which fires `generateEntryCode`) and returns before any bundling — the app's
|
|
525
|
+
* own registered plugin-tanstack writes the artifacts via its `generateEntryCode`
|
|
526
|
+
* subscriber. Reuses the exact hook dev/build use, so there is no scanning drift.
|
|
527
|
+
*/
|
|
528
|
+
export async function generateTanstackRouteArtifacts(opts: {
|
|
529
|
+
appDirectory: string;
|
|
530
|
+
version?: string;
|
|
531
|
+
}): Promise<void> {
|
|
532
|
+
const [{ createRunOptions }, { cli }, utils] = await Promise.all([
|
|
533
|
+
import('@modern-js/app-tools/cli/run'),
|
|
534
|
+
import('@modern-js/plugin/cli'),
|
|
535
|
+
import('@modern-js/utils'),
|
|
536
|
+
]);
|
|
537
|
+
// Load-bearing, not decoration: app-tools' analyze.onPrepare only fires
|
|
538
|
+
// generateEntryCode (the hook that writes the route artifacts) when
|
|
539
|
+
// checkIsBuildCommands() is true, and getCommand() resolves the command
|
|
540
|
+
// from MODERN_ARGV before process.argv — cli.init's `command` option does
|
|
541
|
+
// not feed that check.
|
|
542
|
+
process.env.MODERN_ARGV = 'node modern build';
|
|
543
|
+
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
|
|
544
|
+
const runOptions = await createRunOptions({
|
|
545
|
+
cwd: opts.appDirectory,
|
|
546
|
+
version:
|
|
547
|
+
opts.version || process.env.MODERN_JS_VERSION || '0.0.0-routes-generate',
|
|
548
|
+
internalPlugins: (utils as { INTERNAL_RUNTIME_PLUGINS: unknown })
|
|
549
|
+
.INTERNAL_RUNTIME_PLUGINS as never,
|
|
550
|
+
});
|
|
551
|
+
await cli.init({ ...runOptions, command: 'build' });
|
|
552
|
+
}
|
|
553
|
+
|
|
521
554
|
export default tanstackRouterPlugin;
|
|
@@ -357,18 +357,19 @@ export const tanstackRouterPlugin = (
|
|
|
357
357
|
const _basename =
|
|
358
358
|
baseUrl === '/' ? urlJoin(baseUrl, basename || '') : baseUrl;
|
|
359
359
|
|
|
360
|
-
const
|
|
360
|
+
const rawRequest = request.raw as Request;
|
|
361
|
+
const initialHref = createGetSsrHref(rawRequest);
|
|
361
362
|
const isRSCNavigation =
|
|
362
|
-
enableRsc &&
|
|
363
|
+
enableRsc && rawRequest.headers.get('x-rsc-tree') === 'true';
|
|
363
364
|
|
|
364
365
|
const requestContext = createRequestContext(
|
|
365
366
|
context.ssrContext?.loaderContext,
|
|
366
367
|
) as RequestContext<Record<string, unknown>>;
|
|
367
368
|
|
|
368
369
|
const controller = new AbortController();
|
|
369
|
-
const ssrRequest = new Request(
|
|
370
|
+
const ssrRequest = new Request(rawRequest.url, {
|
|
370
371
|
method: 'GET',
|
|
371
|
-
headers:
|
|
372
|
+
headers: rawRequest.headers,
|
|
372
373
|
signal: controller.signal,
|
|
373
374
|
});
|
|
374
375
|
|
|
@@ -398,7 +399,7 @@ export const tanstackRouterPlugin = (
|
|
|
398
399
|
history,
|
|
399
400
|
basepath: '/',
|
|
400
401
|
rewrite,
|
|
401
|
-
origin: new URL(
|
|
402
|
+
origin: new URL(rawRequest.url).origin,
|
|
402
403
|
ssr: { nonce },
|
|
403
404
|
context: routerContext as never,
|
|
404
405
|
...(serializationAdapters ? { serializationAdapters } : {}),
|
|
@@ -439,7 +440,11 @@ export const tanstackRouterPlugin = (
|
|
|
439
440
|
|
|
440
441
|
const routerErrors = collectRouterErrors(tanstackRouter);
|
|
441
442
|
if (routerErrors && loaderFailureMode === 'clientRender') {
|
|
442
|
-
|
|
443
|
+
(
|
|
444
|
+
context.ssrContext?.response as
|
|
445
|
+
| { status: (code: number) => void }
|
|
446
|
+
| undefined
|
|
447
|
+
)?.status(200);
|
|
443
448
|
try {
|
|
444
449
|
serverRouter.serverSsr?.cleanup?.();
|
|
445
450
|
} catch {}
|
|
@@ -448,7 +453,11 @@ export const tanstackRouterPlugin = (
|
|
|
448
453
|
|
|
449
454
|
await preloadMatchedRouteComponents(serverRouter);
|
|
450
455
|
|
|
451
|
-
|
|
456
|
+
(
|
|
457
|
+
context.ssrContext?.response as
|
|
458
|
+
| { status: (code: number) => void }
|
|
459
|
+
| undefined
|
|
460
|
+
)?.status(tanstackRouter.state.statusCode);
|
|
452
461
|
|
|
453
462
|
await serverRouter.serverSsr?.dehydrate?.();
|
|
454
463
|
|
|
@@ -2,9 +2,9 @@ import {
|
|
|
2
2
|
type AnyRouter,
|
|
3
3
|
type LinkComponentProps,
|
|
4
4
|
type RegisteredRouter,
|
|
5
|
-
|
|
5
|
+
useLinkProps,
|
|
6
6
|
} from '@tanstack/react-router';
|
|
7
|
-
import type { ReactElement } from 'react';
|
|
7
|
+
import type { AnchorHTMLAttributes, ReactElement, ReactNode, Ref } from 'react';
|
|
8
8
|
|
|
9
9
|
export type PrefetchBehavior = 'intent' | 'render' | 'viewport' | 'none';
|
|
10
10
|
|
|
@@ -65,21 +65,84 @@ type LinkComponentImplProps = LinkProps<
|
|
|
65
65
|
string | undefined,
|
|
66
66
|
string,
|
|
67
67
|
string
|
|
68
|
-
|
|
68
|
+
> & { ref?: Ref<HTMLAnchorElement> };
|
|
69
69
|
|
|
70
70
|
const LinkComponentImpl = (props: LinkComponentImplProps) => {
|
|
71
|
-
const {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
71
|
+
const {
|
|
72
|
+
prefetch,
|
|
73
|
+
preload,
|
|
74
|
+
'aria-current': ariaCurrentProp,
|
|
75
|
+
ref,
|
|
76
|
+
children,
|
|
77
|
+
...rest
|
|
78
|
+
} = props as LinkComponentImplProps & {
|
|
79
|
+
children?:
|
|
80
|
+
| ReactNode
|
|
81
|
+
| ((state: { isActive: boolean; isTransitioning: boolean }) => ReactNode);
|
|
82
|
+
};
|
|
83
|
+
const hasAriaCurrentOverride = 'aria-current' in props;
|
|
84
|
+
|
|
85
|
+
// Children are rendered by us, not by useLinkProps() — the hook filters
|
|
86
|
+
// unknown/function props out of its return value, so anything routed
|
|
87
|
+
// through it would be silently dropped.
|
|
88
|
+
const linkOptions = {
|
|
89
|
+
...rest,
|
|
90
|
+
preload: resolvePreloadFromPrefetch(
|
|
91
|
+
prefetch,
|
|
92
|
+
preload,
|
|
93
|
+
) as LinkComponentImplProps['preload'],
|
|
94
|
+
} as LinkComponentImplProps;
|
|
95
|
+
|
|
96
|
+
// TanStack Router's useLinkProps() unconditionally spreads
|
|
97
|
+
// STATIC_ACTIVE_PROPS = { "data-status": "active", "aria-current": "page" }
|
|
98
|
+
// as the LAST entry of the props object it returns for an active link
|
|
99
|
+
// (@tanstack/react-router dist/esm/link.js:369 `...isActive &&
|
|
100
|
+
// STATIC_ACTIVE_PROPS`, defined at line 379-382, spread after
|
|
101
|
+
// `...propsSafeToSpread`, `...resolvedActiveProps`, and
|
|
102
|
+
// `...resolvedInactiveProps`). That means any caller-supplied aria-current
|
|
103
|
+
// -- whether passed directly or via activeProps -- is always clobbered by
|
|
104
|
+
// the time <Link>/createLink render the anchor; there is no props-merge
|
|
105
|
+
// order that lets a caller win from inside <Link>. So instead of
|
|
106
|
+
// delegating rendering to <Link>, we call useLinkProps() ourselves and
|
|
107
|
+
// re-apply the caller's original aria-current (including `false`, meaning
|
|
108
|
+
// "suppress the attribute entirely") as the final word on the anchor we
|
|
109
|
+
// render.
|
|
110
|
+
const linkProps = useLinkProps(
|
|
111
|
+
linkOptions as Parameters<typeof useLinkProps>[0],
|
|
112
|
+
ref as Parameters<typeof useLinkProps>[1],
|
|
113
|
+
) as AnchorHTMLAttributes<HTMLAnchorElement> & {
|
|
114
|
+
disabled?: boolean;
|
|
115
|
+
type?: string;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// TanStack's own Link strips `type` and `disabled` before rendering the
|
|
119
|
+
// anchor (link.js:449-452) — mirror that.
|
|
120
|
+
const { disabled: _disabled, type: _type, ...anchorProps } = linkProps;
|
|
121
|
+
|
|
122
|
+
if (hasAriaCurrentOverride) {
|
|
123
|
+
if (ariaCurrentProp === false || typeof ariaCurrentProp === 'undefined') {
|
|
124
|
+
delete anchorProps['aria-current'];
|
|
125
|
+
} else {
|
|
126
|
+
anchorProps['aria-current'] = ariaCurrentProp;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// TanStack's Link resolves render-prop children with the active state
|
|
131
|
+
// (link.js:450) — without this, function children reach React as an
|
|
132
|
+
// invalid child and crash the render.
|
|
133
|
+
const resolvedChildren =
|
|
134
|
+
typeof children === 'function'
|
|
135
|
+
? children({
|
|
136
|
+
isActive:
|
|
137
|
+
(anchorProps as Record<string, unknown>)['data-status'] ===
|
|
138
|
+
'active',
|
|
139
|
+
isTransitioning:
|
|
140
|
+
(anchorProps as Record<string, unknown>)['data-transitioning'] ===
|
|
141
|
+
'transitioning',
|
|
142
|
+
})
|
|
143
|
+
: children;
|
|
144
|
+
|
|
145
|
+
return <a {...anchorProps}>{resolvedChildren}</a>;
|
|
83
146
|
};
|
|
84
147
|
|
|
85
148
|
export const Link = LinkComponentImpl as LinkComponent;
|
package/tests/router/cli.test.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { Entrypoint } from '@modern-js/types';
|
|
|
6
6
|
import { fs, NESTED_ROUTE_SPEC_FILE } from '@modern-js/utils';
|
|
7
7
|
import {
|
|
8
8
|
createTanstackRsbuildRouteSplittingProfile,
|
|
9
|
+
generateTanstackRouteArtifacts,
|
|
9
10
|
tanstackRouterPlugin,
|
|
10
11
|
writeTanstackRegisterFile,
|
|
11
12
|
writeTanstackRouterTypesForEntries,
|
|
@@ -977,3 +978,9 @@ describe('tanstack router cli plugin', () => {
|
|
|
977
978
|
});
|
|
978
979
|
});
|
|
979
980
|
});
|
|
981
|
+
|
|
982
|
+
describe('generateTanstackRouteArtifacts export', () => {
|
|
983
|
+
test('is exported as a function', () => {
|
|
984
|
+
expect(typeof generateTanstackRouteArtifacts).toBe('function');
|
|
985
|
+
});
|
|
986
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Behavioral coverage for the headless routes-generate entry point: the
|
|
2
|
+
// export-shape test in cli.test.ts would pass even if the body were
|
|
3
|
+
// reverted, so this file pins the actual prepare-only drive contract —
|
|
4
|
+
// createRunOptions against the app dir, cli.init with build-command
|
|
5
|
+
// semantics, and the MODERN_ARGV gate that makes analyze.onPrepare fire
|
|
6
|
+
// generateEntryCode.
|
|
7
|
+
const createRunOptionsMock = rstest.fn(async (options: unknown) => ({
|
|
8
|
+
mockRunOptions: true,
|
|
9
|
+
received: options,
|
|
10
|
+
}));
|
|
11
|
+
const cliInitMock = rstest.fn(async (options: unknown) => ({
|
|
12
|
+
appContext: {},
|
|
13
|
+
received: options,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
rstest.mock('@modern-js/app-tools/cli/run', () => ({
|
|
17
|
+
createRunOptions: createRunOptionsMock,
|
|
18
|
+
}));
|
|
19
|
+
rstest.mock('@modern-js/plugin/cli', () => ({
|
|
20
|
+
cli: { init: cliInitMock },
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
import { INTERNAL_RUNTIME_PLUGINS } from '@modern-js/utils';
|
|
24
|
+
import { generateTanstackRouteArtifacts } from '../../src/cli';
|
|
25
|
+
|
|
26
|
+
describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
|
|
27
|
+
it('drives a prepare-only cli.init with build-command semantics', async () => {
|
|
28
|
+
const previousArgv = process.env.MODERN_ARGV;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
await generateTanstackRouteArtifacts({
|
|
32
|
+
appDirectory: '/workspace/apps/shell',
|
|
33
|
+
version: '9.9.9-test',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(createRunOptionsMock).toHaveBeenCalledTimes(1);
|
|
37
|
+
const runOptionsArg = createRunOptionsMock.mock.calls[0][0] as Record<
|
|
38
|
+
string,
|
|
39
|
+
unknown
|
|
40
|
+
>;
|
|
41
|
+
expect(runOptionsArg.cwd).toBe('/workspace/apps/shell');
|
|
42
|
+
expect(runOptionsArg.version).toBe('9.9.9-test');
|
|
43
|
+
expect(runOptionsArg.internalPlugins).toBe(INTERNAL_RUNTIME_PLUGINS);
|
|
44
|
+
|
|
45
|
+
expect(cliInitMock).toHaveBeenCalledTimes(1);
|
|
46
|
+
const initArg = cliInitMock.mock.calls[0][0] as Record<string, unknown>;
|
|
47
|
+
expect(initArg.command).toBe('build');
|
|
48
|
+
expect(initArg.mockRunOptions).toBe(true);
|
|
49
|
+
|
|
50
|
+
// checkIsBuildCommands() resolves the command from MODERN_ARGV — this
|
|
51
|
+
// is what lets analyze.onPrepare fire generateEntryCode headlessly.
|
|
52
|
+
expect(process.env.MODERN_ARGV).toBe('node modern build');
|
|
53
|
+
} finally {
|
|
54
|
+
if (previousArgv === undefined) {
|
|
55
|
+
delete process.env.MODERN_ARGV;
|
|
56
|
+
} else {
|
|
57
|
+
process.env.MODERN_ARGV = previousArgv;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -1,79 +1,126 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createMemoryHistory } from '@tanstack/history';
|
|
2
|
+
import {
|
|
3
|
+
createRootRoute,
|
|
4
|
+
createRoute,
|
|
5
|
+
createRouter,
|
|
6
|
+
RouterProvider,
|
|
7
|
+
} from '@tanstack/react-router';
|
|
8
|
+
import { render, waitFor } from '@testing-library/react';
|
|
2
9
|
import React from 'react';
|
|
3
10
|
import { Link, NavLink } from '../../src/runtime/prefetchLink';
|
|
4
11
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
// Real-router harness (no module mocking): STATIC_ACTIVE_PROPS (the
|
|
13
|
+
// TanStack behavior this adapter works around) is injected by
|
|
14
|
+
// useLinkProps() based on live route match state, which a mocked
|
|
15
|
+
// useLinkProps() cannot reproduce faithfully. We tried spying on
|
|
16
|
+
// useLinkProps via rstest.mock()+importActual while re-exporting the rest
|
|
17
|
+
// of the module untouched, but rstest's module-mock hoisting resolves the
|
|
18
|
+
// "actual" import for a module through its own in-progress mock
|
|
19
|
+
// registration, so every named export (createRootRoute included) comes
|
|
20
|
+
// back undefined -- see the sibling prefetchLinkPreload.test.tsx file for
|
|
21
|
+
// the fully-mocked preload-mapping coverage instead. This file renders
|
|
22
|
+
// through an actual createRouter()/RouterProvider tree with in-memory
|
|
23
|
+
// history so the active/inactive aria-current behavior is exercised for
|
|
24
|
+
// real.
|
|
25
|
+
function buildRouter(options: {
|
|
26
|
+
initialPath: string;
|
|
27
|
+
linkProps?: Record<string, unknown>;
|
|
28
|
+
useNavLink?: boolean;
|
|
29
|
+
children?:
|
|
30
|
+
| React.ReactNode
|
|
31
|
+
| ((state: { isActive: boolean }) => React.ReactNode);
|
|
32
|
+
}) {
|
|
33
|
+
// The Link lives on the always-rendered root component (not a route
|
|
34
|
+
// that's only matched at "/settings") so the "inactive link" case -
|
|
35
|
+
// sitting at "/" while linking to "/settings" - actually mounts it.
|
|
36
|
+
const rootRoute = createRootRoute({
|
|
37
|
+
component: () => {
|
|
38
|
+
const Component = options.useNavLink ? NavLink : Link;
|
|
39
|
+
return (
|
|
40
|
+
<Component to="/settings" {...(options.linkProps ?? {})}>
|
|
41
|
+
{options.children ?? 'Settings'}
|
|
42
|
+
</Component>
|
|
43
|
+
);
|
|
44
|
+
},
|
|
22
45
|
});
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
46
|
+
const homeRoute = createRoute({
|
|
47
|
+
getParentRoute: () => rootRoute,
|
|
48
|
+
path: '/',
|
|
49
|
+
component: () => null,
|
|
50
|
+
});
|
|
51
|
+
const settingsRoute = createRoute({
|
|
52
|
+
getParentRoute: () => rootRoute,
|
|
53
|
+
path: '/settings',
|
|
54
|
+
component: () => null,
|
|
28
55
|
});
|
|
29
56
|
|
|
30
|
-
|
|
31
|
-
render(
|
|
32
|
-
<Link to="/settings" prefetch="render" preload="intent">
|
|
33
|
-
Settings
|
|
34
|
-
</Link>,
|
|
35
|
-
);
|
|
57
|
+
const routeTree = rootRoute.addChildren([homeRoute, settingsRoute]);
|
|
36
58
|
|
|
37
|
-
|
|
59
|
+
return createRouter({
|
|
60
|
+
routeTree,
|
|
61
|
+
history: createMemoryHistory({ initialEntries: [options.initialPath] }),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function renderLink(options: Parameters<typeof buildRouter>[0]) {
|
|
66
|
+
const router = buildRouter(options);
|
|
67
|
+
const utils = render(<RouterProvider router={router} />);
|
|
68
|
+
|
|
69
|
+
const anchor = await waitFor(() => {
|
|
70
|
+
const found = utils.container.querySelector('a[href="/settings"]');
|
|
71
|
+
if (!found) {
|
|
72
|
+
throw new Error('anchor not rendered yet');
|
|
73
|
+
}
|
|
74
|
+
return found as HTMLAnchorElement;
|
|
38
75
|
});
|
|
39
76
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<Link to="/settings" prefetch="render" preload={false}>
|
|
43
|
-
Settings
|
|
44
|
-
</Link>,
|
|
45
|
-
);
|
|
77
|
+
return { anchor, ...utils };
|
|
78
|
+
}
|
|
46
79
|
|
|
47
|
-
|
|
80
|
+
describe('tanstack prefetch link adapter - aria-current override', () => {
|
|
81
|
+
it('defaults active link to aria-current="page" when caller passes nothing', async () => {
|
|
82
|
+
const { anchor } = await renderLink({ initialPath: '/settings' });
|
|
83
|
+
expect(anchor.getAttribute('aria-current')).toBe('page');
|
|
48
84
|
});
|
|
49
85
|
|
|
50
|
-
it('
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
expect(capturedPreloads).toEqual([false]);
|
|
86
|
+
it('lets caller aria-current="true" win over the TanStack-forced value', async () => {
|
|
87
|
+
const { anchor } = await renderLink({
|
|
88
|
+
initialPath: '/settings',
|
|
89
|
+
linkProps: { 'aria-current': 'true' },
|
|
90
|
+
});
|
|
91
|
+
expect(anchor.getAttribute('aria-current')).toBe('true');
|
|
92
|
+
expect(anchor.outerHTML.match(/aria-current=/g)).toHaveLength(1);
|
|
58
93
|
});
|
|
59
94
|
|
|
60
|
-
it
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
Settings
|
|
68
|
-
</Link>,
|
|
69
|
-
);
|
|
95
|
+
it('suppresses aria-current entirely when caller passes false', async () => {
|
|
96
|
+
const { anchor } = await renderLink({
|
|
97
|
+
initialPath: '/settings',
|
|
98
|
+
linkProps: { 'aria-current': false },
|
|
99
|
+
});
|
|
100
|
+
expect(anchor.hasAttribute('aria-current')).toBe(false);
|
|
101
|
+
});
|
|
70
102
|
|
|
71
|
-
|
|
103
|
+
it('leaves an inactive link untouched when caller passes nothing', async () => {
|
|
104
|
+
const { anchor } = await renderLink({ initialPath: '/' });
|
|
105
|
+
expect(anchor.hasAttribute('aria-current')).toBe(false);
|
|
106
|
+
expect(anchor.getAttribute('data-status')).not.toBe('active');
|
|
72
107
|
});
|
|
73
108
|
|
|
74
|
-
it('
|
|
75
|
-
|
|
109
|
+
it('resolves render-prop children with the active state like TanStack Link', async () => {
|
|
110
|
+
const { anchor } = await renderLink({
|
|
111
|
+
initialPath: '/settings',
|
|
112
|
+
children: ({ isActive }: { isActive: boolean }) =>
|
|
113
|
+
isActive ? 'Active settings' : 'Settings',
|
|
114
|
+
});
|
|
115
|
+
expect(anchor.textContent).toBe('Active settings');
|
|
116
|
+
});
|
|
76
117
|
|
|
77
|
-
|
|
118
|
+
it('resolves render-prop children as inactive on a non-matching route', async () => {
|
|
119
|
+
const { anchor } = await renderLink({
|
|
120
|
+
initialPath: '/',
|
|
121
|
+
children: ({ isActive }: { isActive: boolean }) =>
|
|
122
|
+
isActive ? 'Active settings' : 'Settings',
|
|
123
|
+
});
|
|
124
|
+
expect(anchor.textContent).toBe('Settings');
|
|
78
125
|
});
|
|
79
126
|
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { render } from '@testing-library/react';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Link, NavLink } from '../../src/runtime/prefetchLink';
|
|
4
|
+
|
|
5
|
+
type CapturedOptions = {
|
|
6
|
+
preload?: unknown;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type MockLinkAnchorProps = Record<string, unknown> & {
|
|
10
|
+
href?: string;
|
|
11
|
+
ref?: unknown;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
let capturedOptions: CapturedOptions[] = [];
|
|
15
|
+
let mockReturnProps: MockLinkAnchorProps = { href: '/settings' };
|
|
16
|
+
|
|
17
|
+
rstest.mock('@tanstack/react-router', () => ({
|
|
18
|
+
useLinkProps: (options: CapturedOptions) => {
|
|
19
|
+
capturedOptions.push(options);
|
|
20
|
+
return mockReturnProps;
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
describe('tanstack prefetch link adapter - preload mapping', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
capturedOptions = [];
|
|
27
|
+
mockReturnProps = { href: '/settings' };
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('defaults TanStack preload to viewport', () => {
|
|
31
|
+
render(<Link to="/settings">Settings</Link>);
|
|
32
|
+
|
|
33
|
+
expect(capturedOptions.map(o => o.preload)).toEqual(['viewport']);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('preserves explicit preload', () => {
|
|
37
|
+
render(
|
|
38
|
+
<Link to="/settings" prefetch="render" preload="intent">
|
|
39
|
+
Settings
|
|
40
|
+
</Link>,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(capturedOptions.map(o => o.preload)).toEqual(['intent']);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('preserves explicit disabled preload', () => {
|
|
47
|
+
render(
|
|
48
|
+
<Link to="/settings" prefetch="render" preload={false}>
|
|
49
|
+
Settings
|
|
50
|
+
</Link>,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
expect(capturedOptions.map(o => o.preload)).toEqual([false]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('maps none prefetch to disabled TanStack preload', () => {
|
|
57
|
+
render(
|
|
58
|
+
<Link to="/settings" prefetch="none">
|
|
59
|
+
Settings
|
|
60
|
+
</Link>,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(capturedOptions.map(o => o.preload)).toEqual([false]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it.each([
|
|
67
|
+
'intent',
|
|
68
|
+
'render',
|
|
69
|
+
'viewport',
|
|
70
|
+
] as const)('maps %s prefetch to TanStack preload', prefetch => {
|
|
71
|
+
render(
|
|
72
|
+
<Link to="/settings" prefetch={prefetch}>
|
|
73
|
+
Settings
|
|
74
|
+
</Link>,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(capturedOptions.map(o => o.preload)).toEqual([prefetch]);
|
|
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']);
|
|
84
|
+
});
|
|
85
|
+
});
|