@bleedingdev/modern-js-plugin-tanstack 3.5.0-ultramodern.3 → 3.5.0-ultramodern.30

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.
@@ -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
 
@@ -1,5 +1,3 @@
1
- import type { Plugin, RuntimePluginExtends } from '@modern-js/plugin';
2
- import type { RuntimePluginAPI } from '@modern-js/plugin/runtime';
3
1
  import {
4
2
  getGlobalLayoutApp,
5
3
  getGlobalRoutes,
@@ -15,19 +13,27 @@ export type TanstackRouterRuntimeConfig = {
15
13
  [key: string]: unknown;
16
14
  };
17
15
 
18
- type TanstackRouterRuntimeExtends = Required<
19
- RuntimePluginExtends<TanstackRouterRuntimeConfig, TInternalRuntimeContext>
20
- > & {
21
- extendHooks: RouterExtendsHooks;
22
- };
16
+ type RuntimeInterrupt = (value?: unknown) => unknown;
23
17
 
24
- export type TanstackRouterPluginAPI =
25
- RuntimePluginAPI<TanstackRouterRuntimeExtends>;
18
+ export type TanstackRouterPluginAPI = {
19
+ getRuntimeConfig: () => TanstackRouterRuntimeConfig;
20
+ getHooks: () => RouterExtendsHooks;
21
+ onBeforeRender: (
22
+ listener: (
23
+ context: TInternalRuntimeContext,
24
+ interrupt: RuntimeInterrupt,
25
+ ) => unknown,
26
+ ) => void;
27
+ wrapRoot: (listener: (App: any) => any) => void;
28
+ [key: string]: any;
29
+ };
26
30
 
27
- export type TanstackRouterRuntimePlugin = Plugin<
28
- TanstackRouterPluginAPI,
29
- TInternalRuntimeContext
30
- >;
31
+ export type TanstackRouterRuntimePlugin = {
32
+ name?: string;
33
+ registryHooks?: RouterExtendsHooks;
34
+ setup?: (api: TanstackRouterPluginAPI) => unknown;
35
+ [key: string]: unknown;
36
+ };
31
37
 
32
38
  export function getMergedRouterConfig(
33
39
  api: TanstackRouterPluginAPI,
@@ -39,9 +45,11 @@ export function getMergedRouterConfig(
39
45
  return merge(pluginConfig.router || {}, userConfig) as RouterConfig;
40
46
  }
41
47
 
42
- export function getFinalRouteConfig(mergedConfig: RouterConfig) {
48
+ export function getFinalRouteConfig(
49
+ mergedConfig: RouterConfig,
50
+ ): RouterConfig['routesConfig'] {
43
51
  return {
44
- routes: getGlobalRoutes(),
52
+ routes: getGlobalRoutes() as RouterConfig['routesConfig']['routes'],
45
53
  globalApp: getGlobalLayoutApp(),
46
54
  ...mergedConfig.routesConfig,
47
55
  };
@@ -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;
@@ -1,21 +1,51 @@
1
- import type {
2
- RouterFramework,
3
- // The router runtime state types are owned by @modern-js/runtime; they are
4
- // imported through the `/context` seam instead of being copied here so
5
- // upstream fixes propagate to this package automatically.
6
- } from '@modern-js/runtime/context';
7
1
  import type { RouteObject } from '@modern-js/runtime-utils/router';
8
- import type { NestedRoute, PageRoute } from '@modern-js/types';
9
2
  import type React from 'react';
10
3
 
11
- export type {
12
- BuiltInRouterFramework,
13
- InternalRouterRuntimeState,
14
- InternalRouterServerSnapshot,
15
- RouterFramework,
16
- RouterRouteMatchSnapshot,
17
- RouterServerPrepareResult,
18
- } from '@modern-js/runtime/context';
4
+ export type BuiltInRouterFramework = 'react-router' | 'tanstack';
5
+ export type RouterFramework = BuiltInRouterFramework | (string & {});
6
+
7
+ export type ModernRoute = {
8
+ type: 'nested' | 'page';
9
+ path?: string;
10
+ id?: string;
11
+ component?: React.ComponentType | string;
12
+ children?: ModernRoute[];
13
+ [key: string]: any;
14
+ };
15
+
16
+ export type RouterRouteMatchSnapshot = {
17
+ routeId: string;
18
+ assetRouteId?: string;
19
+ pathname?: string;
20
+ params?: Record<string, string>;
21
+ };
22
+
23
+ export type InternalRouterServerSnapshot = {
24
+ framework?: RouterFramework;
25
+ basename?: string;
26
+ statusCode?: number;
27
+ errors?: Record<string, unknown>;
28
+ routerData?: {
29
+ loaderData?: Record<string, unknown>;
30
+ errors?: Record<string, unknown>;
31
+ };
32
+ hydrationScript?: string;
33
+ hydrationScripts?: string[];
34
+ matchedRouteIds?: string[];
35
+ matches?: RouterRouteMatchSnapshot[];
36
+ };
37
+
38
+ export type InternalRouterRuntimeState = {
39
+ framework: RouterFramework;
40
+ basename?: string;
41
+ instance?: unknown;
42
+ hydrationScript?: string;
43
+ hydrationScripts?: string[];
44
+ matchedRouteIds?: string[];
45
+ matches?: RouterRouteMatchSnapshot[];
46
+ serverSnapshot?: InternalRouterServerSnapshot;
47
+ cleanup?: () => void | Promise<void>;
48
+ };
19
49
 
20
50
  /**
21
51
  * TanStack-specific router config. Unlike the react-router provider config,
@@ -26,7 +56,7 @@ export type RouterConfig = {
26
56
  framework?: RouterFramework;
27
57
  routesConfig: {
28
58
  globalApp?: React.ComponentType<any>;
29
- routes?: (NestedRoute | PageRoute)[];
59
+ routes?: ModernRoute[];
30
60
  };
31
61
  serverBase?: string[];
32
62
  supportHtml5History?: boolean;
@@ -1,12 +1,14 @@
1
1
  // @effect-diagnostics strictBooleanExpressions:off
2
2
  import type { RouteObject } from '@modern-js/runtime-utils/router';
3
- import type { NestedRoute, PageRoute, SSRMode } from '@modern-js/types';
4
3
  import React from 'react';
4
+ import type { ModernRoute } from './types';
5
+
6
+ export type SSRMode = 'string' | 'stream';
5
7
 
6
8
  type RouterConfig = {
7
9
  routesConfig: {
8
10
  globalApp?: React.ComponentType<any>;
9
- routes?: (NestedRoute | PageRoute)[];
11
+ routes?: ModernRoute[];
10
12
  };
11
13
  };
12
14
 
@@ -29,7 +31,7 @@ export type ModernRouteObject = RouteObject & {
29
31
  };
30
32
 
31
33
  export function getRouteObjects(
32
- routes: (NestedRoute | PageRoute)[],
34
+ routes: ModernRoute[],
33
35
  {
34
36
  globalApp,
35
37
  ssrMode,
@@ -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,106 @@
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, the MODERN_ARGV gate that makes analyze.onPrepare fire
6
+ // generateEntryCode, and the absolute config-file resolution that keeps
7
+ // headless runs (cwd != appDirectory) from feeding `false` into node's
8
+ // path APIs.
9
+ const createRunOptionsMock = rstest.fn(async (options: unknown) => ({
10
+ mockRunOptions: true,
11
+ received: options,
12
+ }));
13
+ const cliInitMock = rstest.fn(async (options: unknown) => ({
14
+ appContext: {},
15
+ received: options,
16
+ }));
17
+
18
+ rstest.mock('@modern-js/app-tools/cli/run', () => ({
19
+ createRunOptions: createRunOptionsMock,
20
+ }));
21
+ rstest.mock('@modern-js/plugin/cli', () => ({
22
+ cli: { init: cliInitMock },
23
+ }));
24
+
25
+ import fs from 'node:fs';
26
+ import os from 'node:os';
27
+ import path from 'node:path';
28
+ import { INTERNAL_RUNTIME_PLUGINS } from '@modern-js/utils';
29
+ import { generateTanstackRouteArtifacts } from '../../src/cli';
30
+
31
+ function makeAppDir(withConfig: boolean): string {
32
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tanstack-routes-gen-'));
33
+ if (withConfig) {
34
+ fs.writeFileSync(
35
+ path.join(dir, 'modern.config.ts'),
36
+ 'export default {};\n',
37
+ 'utf-8',
38
+ );
39
+ }
40
+ return dir;
41
+ }
42
+
43
+ describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
44
+ beforeEach(() => {
45
+ createRunOptionsMock.mockClear();
46
+ cliInitMock.mockClear();
47
+ });
48
+
49
+ it('drives a prepare-only cli.init with build-command semantics', async () => {
50
+ const previousArgv = process.env.MODERN_ARGV;
51
+ const appDirectory = makeAppDir(true);
52
+
53
+ try {
54
+ await generateTanstackRouteArtifacts({
55
+ appDirectory,
56
+ version: '9.9.9-test',
57
+ });
58
+
59
+ expect(createRunOptionsMock).toHaveBeenCalledTimes(1);
60
+ const runOptionsArg = createRunOptionsMock.mock.calls[0][0] as Record<
61
+ string,
62
+ unknown
63
+ >;
64
+ expect(runOptionsArg.cwd).toBe(appDirectory);
65
+ expect(runOptionsArg.version).toBe('9.9.9-test');
66
+ expect(runOptionsArg.internalPlugins).toBe(INTERNAL_RUNTIME_PLUGINS);
67
+
68
+ // The config file is resolved to an absolute base path against the app
69
+ // directory (not process.cwd()). Without this, a headless run from a
70
+ // different cwd finds no config, `findExists` returns `false`, and that
71
+ // boolean reaches `path.isAbsolute` deep inside plugin/cli.
72
+ expect(runOptionsArg.configFile).toBe(
73
+ path.resolve(appDirectory, 'modern.config'),
74
+ );
75
+
76
+ expect(cliInitMock).toHaveBeenCalledTimes(1);
77
+ const initArg = cliInitMock.mock.calls[0][0] as Record<string, unknown>;
78
+ expect(initArg.command).toBe('build');
79
+ expect(initArg.mockRunOptions).toBe(true);
80
+
81
+ // checkIsBuildCommands() resolves the command from MODERN_ARGV — this
82
+ // is what lets analyze.onPrepare fire generateEntryCode headlessly.
83
+ expect(process.env.MODERN_ARGV).toBe('node modern build');
84
+ } finally {
85
+ fs.rmSync(appDirectory, { force: true, recursive: true });
86
+ if (previousArgv === undefined) {
87
+ delete process.env.MODERN_ARGV;
88
+ } else {
89
+ process.env.MODERN_ARGV = previousArgv;
90
+ }
91
+ }
92
+ });
93
+
94
+ it('throws a clear error (never a boolean path crash) when no config file exists', async () => {
95
+ const appDirectory = makeAppDir(false);
96
+
97
+ try {
98
+ await expect(
99
+ generateTanstackRouteArtifacts({ appDirectory }),
100
+ ).rejects.toThrow(/Unable to locate a Modern\.js config file/u);
101
+ expect(createRunOptionsMock).not.toHaveBeenCalled();
102
+ } finally {
103
+ fs.rmSync(appDirectory, { force: true, recursive: true });
104
+ }
105
+ });
106
+ });
@@ -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
  });