@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.
@@ -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 initialHref = createGetSsrHref(request.raw);
171
- const isRSCNavigation = enableRsc && 'true' === request.raw.headers.get('x-rsc-tree');
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(request.raw.url, {
175
+ const ssrRequest = new Request(rawRequest.url, {
175
176
  method: 'GET',
176
- headers: request.raw.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(request.raw.url).origin,
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.status(200);
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.status(tanstackRouter.state.statusCode);
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
- return /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(react_router_namespaceObject.Link, {
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;
@@ -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 initialHref = createGetSsrHref(request.raw);
138
- const isRSCNavigation = enableRsc && 'true' === request.raw.headers.get('x-rsc-tree');
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(request.raw.url, {
142
+ const ssrRequest = new Request(rawRequest.url, {
142
143
  method: 'GET',
143
- headers: request.raw.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(request.raw.url).origin,
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.status(200);
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.status(tanstackRouter.state.statusCode);
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 { Link } from "@tanstack/react-router";
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
- return /*#__PURE__*/ jsx(Link, {
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 prefetchLink_Link = LinkComponentImpl;
29
+ const Link = LinkComponentImpl;
17
30
  const NavLink = LinkComponentImpl;
18
- export { NavLink, prefetchLink_Link as Link };
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 initialHref = createGetSsrHref(request.raw);
139
- const isRSCNavigation = enableRsc && 'true' === request.raw.headers.get('x-rsc-tree');
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(request.raw.url, {
143
+ const ssrRequest = new Request(rawRequest.url, {
143
144
  method: 'GET',
144
- headers: request.raw.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(request.raw.url).origin,
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.status(200);
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.status(tanstackRouter.state.statusCode);
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 { Link } from "@tanstack/react-router";
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
- return /*#__PURE__*/ jsx(Link, {
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 prefetchLink_Link = LinkComponentImpl;
30
+ const Link = LinkComponentImpl;
18
31
  const NavLink = LinkComponentImpl;
19
- export { NavLink, prefetchLink_Link as Link };
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.27",
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/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.5.0-ultramodern.27",
81
- "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.5.0-ultramodern.27",
82
- "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.5.0-ultramodern.27",
83
- "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.5.0-ultramodern.27"
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/runtime": "3.5.0-ultramodern.27",
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.27",
102
- "@scripts/rstest-config": "2.66.0",
103
- "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.5.0-ultramodern.27"
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 initialHref = createGetSsrHref(request.raw);
360
+ const rawRequest = request.raw as Request;
361
+ const initialHref = createGetSsrHref(rawRequest);
361
362
  const isRSCNavigation =
362
- enableRsc && request.raw.headers.get('x-rsc-tree') === 'true';
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(request.raw.url, {
370
+ const ssrRequest = new Request(rawRequest.url, {
370
371
  method: 'GET',
371
- headers: request.raw.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(request.raw.url).origin,
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
- context.ssrContext?.response.status(200);
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
- context.ssrContext?.response.status(tanstackRouter.state.statusCode);
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
- Link as TanStackLink,
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 { prefetch, preload, ...rest } = props;
72
- return (
73
- <TanStackLink
74
- {...rest}
75
- preload={
76
- resolvePreloadFromPrefetch(
77
- prefetch,
78
- preload,
79
- ) as LinkComponentImplProps['preload']
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;
@@ -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 { render } from '@testing-library/react';
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
- type MockLinkProps = {
6
- children?: React.ReactNode;
7
- preload?: unknown;
8
- };
9
-
10
- let capturedPreloads: unknown[] = [];
11
-
12
- rstest.mock('@tanstack/react-router', () => ({
13
- Link: (props: MockLinkProps) => {
14
- capturedPreloads.push(props.preload);
15
- return <a href="/settings">{props.children}</a>;
16
- },
17
- }));
18
-
19
- describe('tanstack prefetch link adapter', () => {
20
- beforeEach(() => {
21
- capturedPreloads = [];
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
- it('defaults TanStack preload to viewport', () => {
25
- render(<Link to="/settings">Settings</Link>);
26
-
27
- expect(capturedPreloads).toEqual(['viewport']);
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
- it('preserves explicit preload', () => {
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
- expect(capturedPreloads).toEqual(['intent']);
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
- it('preserves explicit disabled preload', () => {
41
- render(
42
- <Link to="/settings" prefetch="render" preload={false}>
43
- Settings
44
- </Link>,
45
- );
77
+ return { anchor, ...utils };
78
+ }
46
79
 
47
- expect(capturedPreloads).toEqual([false]);
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('maps none prefetch to disabled TanStack preload', () => {
51
- render(
52
- <Link to="/settings" prefetch="none">
53
- Settings
54
- </Link>,
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.each([
61
- 'intent',
62
- 'render',
63
- 'viewport',
64
- ] as const)('maps %s prefetch to TanStack preload', prefetch => {
65
- render(
66
- <Link to="/settings" prefetch={prefetch}>
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
- expect(capturedPreloads).toEqual([prefetch]);
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('defaults NavLink preload to viewport', () => {
75
- render(<NavLink to="/settings">Settings</NavLink>);
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
- expect(capturedPreloads).toEqual(['viewport']);
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
+ });