@bleedingdev/modern-js-plugin-tanstack 3.8.3-ultramodern.6 → 3.9.0-ultramodern.10

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.
Files changed (114) hide show
  1. package/dist/cjs/cli/index.js +9 -0
  2. package/dist/cjs/cli/tanstackTypes/source.js +3 -1
  3. package/dist/cjs/runtime/basepathRewrite.js +13 -6
  4. package/dist/cjs/runtime/lifecycle.js +7 -7
  5. package/dist/cjs/runtime/plugin.js +1 -1
  6. package/dist/cjs/runtime/plugin.node.js +5 -1
  7. package/dist/cjs/runtime/prefetchLink.js +2 -7
  8. package/dist/cjs/runtime/register.js +0 -2
  9. package/dist/cjs/runtime/routeTree/construction.js +2 -0
  10. package/dist/cjs/runtime/router.js +25 -9
  11. package/dist/cjs/runtime/state.js +2 -2
  12. package/dist/cjs/runtime/utils.js +2 -0
  13. package/dist/esm/cli/index.mjs +13 -5
  14. package/dist/esm/cli/tanstackTypes/source.mjs +3 -1
  15. package/dist/esm/runtime/basepathRewrite.mjs +10 -3
  16. package/dist/esm/runtime/lifecycle.mjs +1 -1
  17. package/dist/esm/runtime/plugin.mjs +1 -1
  18. package/dist/esm/runtime/plugin.node.mjs +5 -1
  19. package/dist/esm/runtime/prefetchLink.mjs +1 -6
  20. package/dist/esm/runtime/register.mjs +0 -2
  21. package/dist/esm/runtime/routeTree/construction.mjs +2 -0
  22. package/dist/esm/runtime/router.mjs +27 -11
  23. package/dist/esm/runtime/state.mjs +1 -1
  24. package/dist/esm/runtime/utils.mjs +2 -0
  25. package/dist/esm-node/cli/index.mjs +15 -5
  26. package/dist/esm-node/cli/tanstackTypes/source.mjs +3 -1
  27. package/dist/esm-node/runtime/basepathRewrite.mjs +10 -3
  28. package/dist/esm-node/runtime/lifecycle.mjs +1 -1
  29. package/dist/esm-node/runtime/plugin.mjs +1 -1
  30. package/dist/esm-node/runtime/plugin.node.mjs +5 -1
  31. package/dist/esm-node/runtime/prefetchLink.mjs +1 -6
  32. package/dist/esm-node/runtime/register.mjs +0 -2
  33. package/dist/esm-node/runtime/routeTree/construction.mjs +2 -0
  34. package/dist/esm-node/runtime/router.mjs +27 -11
  35. package/dist/esm-node/runtime/state.mjs +1 -1
  36. package/dist/esm-node/runtime/utils.mjs +2 -0
  37. package/dist/types/cli/index.d.ts +5 -5
  38. package/dist/types/cli/tanstackTypes/shared.d.ts +1 -1
  39. package/dist/types/cli/tanstackTypes.d.ts +3 -3
  40. package/dist/types/cli.d.ts +2 -2
  41. package/dist/types/runtime/basepathRewrite.d.ts +2 -1
  42. package/dist/types/runtime/dataMutation.d.ts +3 -3
  43. package/dist/types/runtime/index.d.ts +14 -14
  44. package/dist/types/runtime/lifecycle.d.ts +9 -2
  45. package/dist/types/runtime/loaderBridge.d.ts +2 -2
  46. package/dist/types/runtime/plugin.d.ts +2 -2
  47. package/dist/types/runtime/plugin.node.d.ts +2 -2
  48. package/dist/types/runtime/plugin.worker.d.ts +1 -1
  49. package/dist/types/runtime/pluginShared.d.ts +2 -2
  50. package/dist/types/runtime/register.d.ts +1 -9
  51. package/dist/types/runtime/routeTree/construction.d.ts +1 -1
  52. package/dist/types/runtime/routeTree/index.d.ts +2 -2
  53. package/dist/types/runtime/routeTree/loaders.d.ts +1 -1
  54. package/dist/types/runtime/routeTree/tanstackRoutes.d.ts +1 -1
  55. package/dist/types/runtime/router.d.ts +2 -13
  56. package/dist/types/runtime/rsc/CompositeComponent.d.ts +1 -1
  57. package/dist/types/runtime/rsc/RscNodeRenderer.d.ts +1 -1
  58. package/dist/types/runtime/rsc/client.d.ts +4 -4
  59. package/dist/types/runtime/rsc/createRscProxy.d.ts +1 -1
  60. package/dist/types/runtime/rsc/payloadFetch.d.ts +1 -1
  61. package/dist/types/runtime/rsc/payloadRouter.d.ts +2 -2
  62. package/dist/types/runtime/rsc/payloadStorage.d.ts +1 -1
  63. package/dist/types/runtime/rsc/server.d.ts +3 -3
  64. package/dist/types/runtime/rsc/shared.d.ts +1 -1
  65. package/dist/types/runtime/rsc/slotUsageSanitizer.d.ts +1 -1
  66. package/dist/types/runtime/ssrManagedTags.d.ts +1 -1
  67. package/dist/types/runtime/ssrPreload.d.ts +1 -1
  68. package/dist/types/runtime/state.d.ts +1 -1
  69. package/dist/types/runtime/submitAction.d.ts +1 -1
  70. package/dist/types/runtime/types.d.ts +2 -1
  71. package/dist/types/runtime/utils.d.ts +1 -1
  72. package/dist/types/runtime.d.ts +1 -1
  73. package/package.json +19 -16
  74. package/rstest.config.mts +1 -24
  75. package/src/cli/index.ts +10 -0
  76. package/src/cli/tanstackTypes/source.ts +3 -1
  77. package/src/runtime/basepathRewrite.ts +11 -2
  78. package/src/runtime/lifecycle.ts +14 -3
  79. package/src/runtime/plugin.node.tsx +9 -2
  80. package/src/runtime/plugin.tsx +5 -1
  81. package/src/runtime/prefetchLink.tsx +1 -23
  82. package/src/runtime/register.ts +1 -12
  83. package/src/runtime/routeTree/construction.ts +2 -0
  84. package/src/runtime/router.ts +25 -40
  85. package/src/runtime/state.ts +1 -1
  86. package/src/runtime/types.ts +2 -2
  87. package/src/runtime/utils.tsx +3 -1
  88. package/tests/router/cli.test.ts +148 -824
  89. package/tests/router/dataMutation.test.tsx +17 -85
  90. package/tests/router/flightSerialization.roundtrip.test.tsx +9 -6
  91. package/tests/router/generateRouteArtifacts.test.ts +68 -28
  92. package/tests/router/loaderBridge.test.ts +0 -25
  93. package/tests/router/localisedIdentity.test.tsx +95 -0
  94. package/tests/router/prefetchLink.test.tsx +17 -21
  95. package/tests/router/prefetchLinkPreload.test.tsx +11 -31
  96. package/tests/router/routeHooks.test.ts +41 -105
  97. package/tests/router/routeTree.test.ts +144 -836
  98. package/tests/router/rsc.test.tsx +131 -166
  99. package/tests/router/serverPlugin.test.ts +43 -1
  100. package/tests/router/tanstackTypes.test.ts +109 -973
  101. package/tests/realm-isolation.test.ts +0 -114
  102. package/tests/router/fastDefaults.test.ts +0 -32
  103. package/tests/router/fixtures/legacyRuntimeRouter.ts +0 -10
  104. package/tests/router/fixtures/rscDecoderSecurityChild.cjs +0 -162
  105. package/tests/router/hooks.test.ts +0 -40
  106. package/tests/router/hydrationBoundary.test.tsx +0 -23
  107. package/tests/router/packageSurface.test.ts +0 -24
  108. package/tests/router/preloadRedirect.test.ts +0 -64
  109. package/tests/router/register.test.ts +0 -8
  110. package/tests/router/routerCompatibility.test.ts +0 -239
  111. package/tests/router/rscDecoderSecurity.test.ts +0 -113
  112. package/tests/router/rscPayloadRouterMatrix.test.ts +0 -197
  113. /package/dist/esm/{rslib-runtime.mjs → rslib-runtime~1.mjs} +0 -0
  114. /package/dist/esm-node/{rslib-runtime.mjs → rslib-runtime~0.mjs} +0 -0
@@ -105,54 +105,14 @@ describe('tanstack data mutation fetcher', () => {
105
105
  states.length = 0;
106
106
  });
107
107
 
108
- test('tracks submitting and loading phases for mutation submit', async () => {
109
- const actionResult = createDeferred<Response>();
110
- const invalidateResult = createDeferred<void>();
111
-
112
- currentRouter = createRouter({
113
- action: async () => actionResult.promise,
114
- invalidate: async () => invalidateResult.promise,
115
- });
116
-
117
- render(<FetcherHarness />);
118
- expect(screen.getByTestId('state').textContent).toBe('idle');
119
-
120
- let submitPromise: Promise<void> | undefined;
121
- act(() => {
122
- submitPromise = latestFetcher!.submit(
123
- { amount: 2 },
124
- { method: 'post', action: '/mutation' },
125
- );
126
- });
127
-
128
- await waitFor(() => {
129
- expect(screen.getByTestId('state').textContent).toBe('submitting');
130
- });
131
-
132
- actionResult.resolve(
133
- new Response(JSON.stringify({ count: 2 }), {
134
- headers: {
135
- 'Content-Type': 'application/json',
136
- },
137
- }),
138
- );
139
-
140
- await waitFor(() => {
141
- expect(screen.getByTestId('state').textContent).toBe('loading');
142
- });
143
-
144
- invalidateResult.resolve();
145
-
146
- await act(async () => {
147
- await submitPromise;
148
- });
149
-
150
- expect(screen.getByTestId('state').textContent).toBe('idle');
151
- expect(screen.getByTestId('data').textContent).toBe('{"count":2}');
152
- expect(states).toEqual(['idle', 'submitting', 'loading', 'idle']);
153
- });
154
-
155
- test('defaults fetcher submit without method to mutation state', async () => {
108
+ test.each([
109
+ { amount: 2, count: 2, method: 'post' as const, name: 'explicit method' },
110
+ { amount: 1, count: 1, method: undefined, name: 'default method' },
111
+ ])('tracks submitting/loading phases with the $name', async ({
112
+ amount,
113
+ count,
114
+ method,
115
+ }) => {
156
116
  const actionResult = createDeferred<Response>();
157
117
  const invalidateResult = createDeferred<void>();
158
118
  const action = rstest.fn(async () => actionResult.promise);
@@ -170,8 +130,11 @@ describe('tanstack data mutation fetcher', () => {
170
130
  let submitPromise: Promise<void> | undefined;
171
131
  act(() => {
172
132
  submitPromise = latestFetcher!.submit(
173
- { amount: 1 },
174
- { action: '/mutation' },
133
+ { amount },
134
+ {
135
+ ...(method === undefined ? {} : { method }),
136
+ action: '/mutation',
137
+ },
175
138
  );
176
139
  });
177
140
 
@@ -182,7 +145,7 @@ describe('tanstack data mutation fetcher', () => {
182
145
  expect(loader).not.toHaveBeenCalled();
183
146
 
184
147
  actionResult.resolve(
185
- new Response(JSON.stringify({ count: 1 }), {
148
+ new Response(JSON.stringify({ count }), {
186
149
  headers: {
187
150
  'Content-Type': 'application/json',
188
151
  },
@@ -200,7 +163,9 @@ describe('tanstack data mutation fetcher', () => {
200
163
  });
201
164
 
202
165
  expect(screen.getByTestId('state').textContent).toBe('idle');
203
- expect(screen.getByTestId('data').textContent).toBe('{"count":1}');
166
+ expect(screen.getByTestId('data').textContent).toBe(
167
+ JSON.stringify({ count }),
168
+ );
204
169
  expect(states).toEqual(['idle', 'submitting', 'loading', 'idle']);
205
170
  });
206
171
 
@@ -460,37 +425,4 @@ describe('tanstack data mutation fetcher', () => {
460
425
  globalThis.FormData = NativeFormData;
461
426
  }
462
427
  });
463
-
464
- test('does not throw for Form submit with non-2xx response and still invalidates', async () => {
465
- const action = rstest.fn(
466
- async () =>
467
- new Response(JSON.stringify({ message: 'invalid amount' }), {
468
- status: 422,
469
- headers: {
470
- 'Content-Type': 'application/json',
471
- },
472
- }),
473
- );
474
- currentRouter = createRouter({
475
- action,
476
- });
477
-
478
- render(
479
- <Form method="post" action="/mutation">
480
- <button type="submit">submit</button>
481
- </Form>,
482
- );
483
-
484
- const form = document.querySelector('form');
485
- expect(form).toBeTruthy();
486
-
487
- await act(async () => {
488
- fireEvent.submit(form!);
489
- });
490
-
491
- await waitFor(() => {
492
- expect(action).toHaveBeenCalledTimes(1);
493
- expect(currentRouter.invalidate).toHaveBeenCalledTimes(1);
494
- });
495
- });
496
428
  });
@@ -45,21 +45,22 @@ test('round-trips TanStack RSC flight values across supported value types', asyn
45
45
  const circular: Record<string, unknown> = { marker: 'circular' };
46
46
  circular.self = circular;
47
47
 
48
+ const rscProxy = createRscProxy(() => ({ slot: 'server' }), {
49
+ renderable: true,
50
+ stream: createStream(),
51
+ }) as React.ReactElement & Record<PropertyKey, unknown>;
52
+
48
53
  const mapValue = new Map<unknown, unknown>([
49
54
  ['primitive', 1],
50
55
  ['shared', shared],
51
56
  [shared, circular],
57
+ ['rscProxy', rscProxy],
52
58
  ]);
53
59
  mapValue.set('self', mapValue);
54
60
 
55
- const setValue = new Set<unknown>(['set-item', shared, circular]);
61
+ const setValue = new Set<unknown>(['set-item', shared, circular, rscProxy]);
56
62
  setValue.add(setValue);
57
63
 
58
- const rscProxy = createRscProxy(() => ({ slot: 'server' }), {
59
- renderable: true,
60
- stream: createStream(),
61
- }) as React.ReactElement & Record<PropertyKey, unknown>;
62
-
63
64
  const fixture: RoundTripFixture = {
64
65
  bigintValue: 9007199254740993n,
65
66
  booleanValue: true,
@@ -115,12 +116,14 @@ test('round-trips TanStack RSC flight values across supported value types', asyn
115
116
  expect(revived.mapValue.get('shared')).toBe(revived.sharedA);
116
117
  expect(revived.mapValue.get(revived.sharedA)).toBe(revived.circular);
117
118
  expect(revived.mapValue.get('self')).toBe(revived.mapValue);
119
+ expect(isValidElement(revived.mapValue.get('rscProxy'))).toBe(true);
118
120
 
119
121
  expect(revived.setValue).toBeInstanceOf(Set);
120
122
  expect(revived.setValue).not.toBe(setValue);
121
123
  expect(revived.setValue.has('set-item')).toBe(true);
122
124
  expect(revived.setValue.has(revived.sharedA)).toBe(true);
123
125
  expect(revived.setValue.has(revived.circular)).toBe(true);
126
+ expect([...revived.setValue].some(value => isValidElement(value))).toBe(true);
124
127
  expect(revived.setValue.has(revived.setValue)).toBe(true);
125
128
 
126
129
  expect(serialized.rscProxy).not.toBe(rscProxy);
@@ -1,19 +1,11 @@
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.
1
+ // The headless entry must drive the real TanStack artifact writer. Checking
2
+ // only the createRunOptions/cli.init choreography would allow a no-op CLI to
3
+ // pass while leaving router.gen.ts stale or missing.
9
4
  const createRunOptionsMock = rstest.fn(async (options: unknown) => ({
10
5
  mockRunOptions: true,
11
6
  received: options,
12
7
  }));
13
- const cliInitMock = rstest.fn(async (options: unknown) => ({
14
- appContext: {},
15
- received: options,
16
- }));
8
+ const cliInitMock = rstest.fn(async () => ({ appContext: {} }));
17
9
 
18
10
  rstest.mock('@modern-js/app-tools/cli/run', () => ({
19
11
  createRunOptions: createRunOptionsMock,
@@ -26,7 +18,10 @@ import fs from 'node:fs';
26
18
  import os from 'node:os';
27
19
  import path from 'node:path';
28
20
  import { INTERNAL_RUNTIME_PLUGINS } from '@modern-js/utils';
29
- import { generateTanstackRouteArtifacts } from '../../src/cli';
21
+ import {
22
+ generateTanstackRouteArtifacts,
23
+ writeTanstackRouterTypesForEntries,
24
+ } from '../../src/cli';
30
25
 
31
26
  function makeAppDir(withConfig: boolean): string {
32
27
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tanstack-routes-gen-'));
@@ -46,9 +41,55 @@ describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
46
41
  cliInitMock.mockClear();
47
42
  });
48
43
 
49
- it('drives a prepare-only cli.init with build-command semantics', async () => {
44
+ it('writes a real route artifact through the prepare-only CLI drive', async () => {
50
45
  const previousArgv = process.env.MODERN_ARGV;
51
46
  const appDirectory = makeAppDir(true);
47
+ const srcDirectory = path.join(appDirectory, 'src');
48
+ const componentPath = path.join(srcDirectory, 'routes', 'page.tsx');
49
+ fs.mkdirSync(path.dirname(componentPath), { recursive: true });
50
+ fs.writeFileSync(
51
+ componentPath,
52
+ 'export default function Page() { return null; }\n',
53
+ 'utf-8',
54
+ );
55
+
56
+ const runOptions = {
57
+ cwd: appDirectory,
58
+ version: '9.9.9-test',
59
+ internalPlugins: INTERNAL_RUNTIME_PLUGINS,
60
+ configFile: path.resolve(appDirectory, 'modern.config'),
61
+ };
62
+ createRunOptionsMock.mockResolvedValue({
63
+ mockRunOptions: true,
64
+ received: runOptions,
65
+ });
66
+ cliInitMock.mockImplementation(async () => {
67
+ await writeTanstackRouterTypesForEntries({
68
+ appContext: {
69
+ srcDirectory,
70
+ internalSrcAlias: '@/_',
71
+ entrypoints: [{ entryName: 'main', isMainEntry: true }],
72
+ } as any,
73
+ routesByEntry: {
74
+ main: [
75
+ {
76
+ type: 'nested',
77
+ id: 'layout',
78
+ isRoot: true,
79
+ children: [
80
+ {
81
+ type: 'nested',
82
+ id: 'page',
83
+ index: true,
84
+ _component: '@/_/routes/page',
85
+ },
86
+ ],
87
+ },
88
+ ] as any,
89
+ },
90
+ });
91
+ return { appContext: {} };
92
+ });
52
93
 
53
94
  try {
54
95
  await generateTanstackRouteArtifacts({
@@ -61,26 +102,25 @@ describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
61
102
  string,
62
103
  unknown
63
104
  >;
105
+ // The headless entry must resolve cwd/configFile from the target app
106
+ // directory, not the process's own cwd — that was the actual bug.
64
107
  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
108
  expect(runOptionsArg.configFile).toBe(
73
109
  path.resolve(appDirectory, 'modern.config'),
74
110
  );
75
111
 
76
112
  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
113
 
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');
114
+ expect(
115
+ fs.existsSync(
116
+ path.join(srcDirectory, 'modern-tanstack', 'main', 'router.gen.ts'),
117
+ ),
118
+ ).toBe(true);
119
+ const registerArtifact = fs.readFileSync(
120
+ path.join(srcDirectory, 'modern-tanstack', 'register.gen.d.ts'),
121
+ 'utf-8',
122
+ );
123
+ expect(registerArtifact).toContain('./main/router.gen');
84
124
  } finally {
85
125
  fs.rmSync(appDirectory, { force: true, recursive: true });
86
126
  if (previousArgv === undefined) {
@@ -91,7 +131,7 @@ describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
91
131
  }
92
132
  });
93
133
 
94
- it('throws a clear error (never a boolean path crash) when no config file exists', async () => {
134
+ it('throws a clear error when no config file exists', async () => {
95
135
  const appDirectory = makeAppDir(false);
96
136
 
97
137
  try {
@@ -1,7 +1,5 @@
1
1
  import { isNotFound, isRedirect } from '@tanstack/react-router';
2
2
  import {
3
- createRouteStaticData,
4
- isAbsoluteUrl,
5
3
  mapSplatParamsForModernLoader,
6
4
  modernLoaderToTanstack,
7
5
  throwTanstackRedirect,
@@ -53,15 +51,6 @@ describe('throwTanstackRedirect', () => {
53
51
  });
54
52
  });
55
53
 
56
- describe('isAbsoluteUrl', () => {
57
- test('detects absolute and relative URLs', () => {
58
- expect(isAbsoluteUrl('https://example.com/a')).toBe(true);
59
- expect(isAbsoluteUrl('mailto:x@example.com')).toBe(true);
60
- expect(isAbsoluteUrl('/internal/path')).toBe(false);
61
- expect(isAbsoluteUrl('relative')).toBe(false);
62
- });
63
- });
64
-
65
54
  describe('mapSplatParamsForModernLoader', () => {
66
55
  test('maps TanStack _splat to React Router * only for splat routes', () => {
67
56
  expect(
@@ -76,20 +65,6 @@ describe('mapSplatParamsForModernLoader', () => {
76
65
  });
77
66
  });
78
67
 
79
- describe('createRouteStaticData', () => {
80
- test('drops empty fields', () => {
81
- const loader = () => null;
82
- expect(createRouteStaticData({})).toEqual({});
83
- expect(createRouteStaticData({ modernRouteId: '' })).toEqual({});
84
- expect(
85
- createRouteStaticData({
86
- modernRouteId: 'page',
87
- modernRouteLoader: loader,
88
- }),
89
- ).toEqual({ modernRouteId: 'page', modernRouteLoader: loader });
90
- });
91
- });
92
-
93
68
  describe('modernLoaderToTanstack', () => {
94
69
  const baseCtx = {
95
70
  location: { href: 'http://localhost/products/1' },
@@ -0,0 +1,95 @@
1
+ import { applyLocalisedUrlsToRoutes } from '@modern-js/i18n-runtime-extensions';
2
+ import {
3
+ createMemoryHistory,
4
+ createRouter,
5
+ Outlet,
6
+ RouterProvider,
7
+ useLoaderData,
8
+ useParams,
9
+ } from '@tanstack/react-router';
10
+ import { createElement } from 'react';
11
+ import { renderToString } from 'react-dom/server';
12
+ import { createModernBasepathRewrite } from '../../src/runtime/basepathRewrite';
13
+ import { createRouteTreeFromRouteObjects } from '../../src/runtime/routeTree';
14
+ import { createTanstackRouteObjectsFromConfig } from '../../src/runtime/utils';
15
+
16
+ function SearchPage() {
17
+ const data = useLoaderData({ from: '/$lang/search' }) as { language: string };
18
+ return createElement('p', null, `search:${data.language}`);
19
+ }
20
+
21
+ function ResourcePage() {
22
+ const params = useParams({ from: '/$lang/resources/$id' }) as { id: string };
23
+ return createElement('p', null, `resource:${params.id}`);
24
+ }
25
+
26
+ test('localized URLs preserve native loader and params route identity in SSR and navigation', async () => {
27
+ const routes = applyLocalisedUrlsToRoutes(
28
+ [
29
+ {
30
+ type: 'nested',
31
+ id: 'lang',
32
+ path: ':lang',
33
+ component: Outlet,
34
+ children: [
35
+ {
36
+ type: 'nested',
37
+ id: 'search',
38
+ path: 'search',
39
+ component: SearchPage,
40
+ loader: ({ params }: { params: { lang: string } }) => ({
41
+ language: params.lang,
42
+ }),
43
+ },
44
+ {
45
+ type: 'nested',
46
+ id: 'resource',
47
+ path: 'resources/:id',
48
+ component: ResourcePage,
49
+ },
50
+ ],
51
+ },
52
+ ],
53
+ ['en', 'cs'],
54
+ {
55
+ '/search': { en: '/find', cs: '/hledat' },
56
+ '/resources/:id': { en: '/resources/:id', cs: '/zdroje/:id' },
57
+ },
58
+ );
59
+ const routeObjects = createTanstackRouteObjectsFromConfig({
60
+ routesConfig: { routes },
61
+ })!;
62
+ const history = createMemoryHistory({
63
+ initialEntries: ['/base/cs/hledat?q=tractor#results'],
64
+ });
65
+ const router = createRouter({
66
+ context: {
67
+ request: new Request(
68
+ 'https://example.test/base/cs/hledat?q=tractor#results',
69
+ ),
70
+ },
71
+ routeTree: createRouteTreeFromRouteObjects(routeObjects),
72
+ history,
73
+ rewrite: createModernBasepathRewrite('/base', false, routeObjects),
74
+ });
75
+ await router.load();
76
+ expect(router.state.matches.at(-1)?.routeId).toBe('/$lang/search');
77
+ expect(renderToString(createElement(RouterProvider, { router }))).toContain(
78
+ 'search:cs',
79
+ );
80
+ expect(history.location.href).toBe('/base/cs/hledat?q=tractor#results');
81
+
82
+ await router.navigate({ to: '/en/search', search: { q: 'tractor' } });
83
+ expect(history.location.href).toBe('/base/en/find?q=tractor');
84
+ expect(router.state.matches.at(-1)?.routeId).toBe('/$lang/search');
85
+ expect(renderToString(createElement(RouterProvider, { router }))).toContain(
86
+ 'search:en',
87
+ );
88
+
89
+ await router.navigate({ to: '/cs/zdroje/a%2Fb' });
90
+ expect(history.location.href).toBe('/base/cs/zdroje/a%2Fb');
91
+ expect(router.state.matches.at(-1)?.routeId).toBe('/$lang/resources/$id');
92
+ expect(renderToString(createElement(RouterProvider, { router }))).toContain(
93
+ 'resource:a/b',
94
+ );
95
+ });
@@ -5,7 +5,7 @@ import {
5
5
  createRouter,
6
6
  RouterProvider,
7
7
  } from '@tanstack/react-router';
8
- import { render, waitFor } from '@testing-library/react';
8
+ import { act, render, waitFor } from '@testing-library/react';
9
9
  import React from 'react';
10
10
  import { Link, NavLink } from '../../src/runtime/prefetchLink';
11
11
 
@@ -74,15 +74,10 @@ async function renderLink(options: Parameters<typeof buildRouter>[0]) {
74
74
  return found as HTMLAnchorElement;
75
75
  });
76
76
 
77
- return { anchor, ...utils };
77
+ return { anchor, router, ...utils };
78
78
  }
79
79
 
80
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');
84
- });
85
-
86
81
  it('lets caller aria-current="true" win over the TanStack-forced value', async () => {
87
82
  const { anchor } = await renderLink({
88
83
  initialPath: '/settings',
@@ -100,27 +95,28 @@ describe('tanstack prefetch link adapter - aria-current override', () => {
100
95
  expect(anchor.hasAttribute('aria-current')).toBe(false);
101
96
  });
102
97
 
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');
107
- });
108
-
109
- it('resolves render-prop children with the active state like TanStack Link', async () => {
110
- const { anchor } = await renderLink({
111
- initialPath: '/settings',
98
+ it('updates render-prop children across an inactive-to-active transition', async () => {
99
+ const { anchor, router } = await renderLink({
100
+ initialPath: '/',
112
101
  children: ({ isActive }: { isActive: boolean }) =>
113
102
  isActive ? 'Active settings' : 'Settings',
114
103
  });
115
- expect(anchor.textContent).toBe('Active settings');
104
+
105
+ expect(anchor.textContent).toBe('Settings');
106
+
107
+ await act(async () => {
108
+ await router.navigate({ to: '/settings' });
109
+ });
110
+
111
+ await waitFor(() => expect(anchor.textContent).toBe('Active settings'));
116
112
  });
117
113
 
118
- it('resolves render-prop children as inactive on a non-matching route', async () => {
114
+ it('passes the native anchor ref through the router hook', async () => {
115
+ const ref = React.createRef<HTMLAnchorElement>();
119
116
  const { anchor } = await renderLink({
120
117
  initialPath: '/',
121
- children: ({ isActive }: { isActive: boolean }) =>
122
- isActive ? 'Active settings' : 'Settings',
118
+ linkProps: { ref, prefetch: 'none' },
123
119
  });
124
- expect(anchor.textContent).toBe('Settings');
120
+ expect(ref.current).toBe(anchor);
125
121
  });
126
122
  });
@@ -1,6 +1,6 @@
1
1
  import { render } from '@testing-library/react';
2
2
  import React from 'react';
3
- import { Link, NavLink } from '../../src/runtime/prefetchLink';
3
+ import { Link } from '../../src/runtime/prefetchLink';
4
4
 
5
5
  type CapturedOptions = {
6
6
  preload?: unknown;
@@ -27,30 +27,20 @@ describe('tanstack prefetch link adapter - preload mapping', () => {
27
27
  mockReturnProps = { href: '/settings' };
28
28
  });
29
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', () => {
30
+ it('preserves an explicit false preload override', () => {
37
31
  render(
38
- <Link to="/settings" prefetch="render" preload="intent">
32
+ <Link to="/settings" prefetch="render" preload={false}>
39
33
  Settings
40
34
  </Link>,
41
35
  );
42
36
 
43
- expect(capturedOptions.map(o => o.preload)).toEqual(['intent']);
37
+ expect(capturedOptions[0]?.preload).toBe(false);
44
38
  });
45
39
 
46
- it('preserves explicit disabled preload', () => {
47
- render(
48
- <Link to="/settings" prefetch="render" preload={false}>
49
- Settings
50
- </Link>,
51
- );
40
+ it('defaults to viewport preload when no prefetch is given', () => {
41
+ render(<Link to="/settings">Settings</Link>);
52
42
 
53
- expect(capturedOptions.map(o => o.preload)).toEqual([false]);
43
+ expect(capturedOptions[0]?.preload).toBe('viewport');
54
44
  });
55
45
 
56
46
  it('maps none prefetch to disabled TanStack preload', () => {
@@ -60,26 +50,16 @@ describe('tanstack prefetch link adapter - preload mapping', () => {
60
50
  </Link>,
61
51
  );
62
52
 
63
- expect(capturedOptions.map(o => o.preload)).toEqual([false]);
53
+ expect(capturedOptions[0]?.preload).toBe(false);
64
54
  });
65
55
 
66
- it.each([
67
- 'intent',
68
- 'render',
69
- 'viewport',
70
- ] as const)('maps %s prefetch to TanStack preload', prefetch => {
56
+ it('lets explicit preload re-enable prefetch=none', () => {
71
57
  render(
72
- <Link to="/settings" prefetch={prefetch}>
58
+ <Link to="/settings" prefetch="none" preload="render">
73
59
  Settings
74
60
  </Link>,
75
61
  );
76
62
 
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']);
63
+ expect(capturedOptions.map(o => o.preload)).toEqual(['render']);
84
64
  });
85
65
  });