@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.11 → 3.9.0-ultramodern.13

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 (81) hide show
  1. package/dist/cjs/cli/artifacts.js +2 -1
  2. package/dist/cjs/cli/tanstackTypes/canonicalRoutes.js +7 -7
  3. package/dist/cjs/cli/tanstackTypes/shared.js +0 -14
  4. package/dist/cjs/cli/tanstackTypes/source.js +28 -63
  5. package/dist/cjs/runtime/lifecycle.js +0 -3
  6. package/dist/cjs/runtime/loaderBridge.js +10 -4
  7. package/dist/cjs/runtime/navigation.js +102 -0
  8. package/dist/cjs/runtime/plugin.js +6 -1
  9. package/dist/cjs/runtime/plugin.node.js +81 -63
  10. package/dist/cjs/runtime/routeTree/construction.js +19 -61
  11. package/dist/cjs/runtime/routeTree/loaders.js +1 -1
  12. package/dist/cjs/runtime/routeTree/paths.js +0 -6
  13. package/dist/cjs/runtime/ssrManagedTags.js +2 -12
  14. package/dist/cjs/runtime/utils.js +1 -0
  15. package/dist/cjs/shared/routeDescriptor.js +60 -0
  16. package/dist/esm/cli/artifacts.mjs +2 -1
  17. package/dist/esm/cli/index.mjs +1 -7
  18. package/dist/esm/cli/tanstackTypes/canonicalRoutes.mjs +7 -7
  19. package/dist/esm/cli/tanstackTypes/shared.mjs +1 -9
  20. package/dist/esm/cli/tanstackTypes/source.mjs +29 -64
  21. package/dist/esm/runtime/lifecycle.mjs +1 -1
  22. package/dist/esm/runtime/loaderBridge.mjs +8 -5
  23. package/dist/esm/runtime/navigation.mjs +64 -0
  24. package/dist/esm/runtime/plugin.mjs +6 -1
  25. package/dist/esm/runtime/plugin.node.mjs +83 -65
  26. package/dist/esm/runtime/routeTree/construction.mjs +19 -61
  27. package/dist/esm/runtime/routeTree/loaders.mjs +2 -2
  28. package/dist/esm/runtime/routeTree/paths.mjs +1 -4
  29. package/dist/esm/runtime/ssrManagedTags.mjs +1 -8
  30. package/dist/esm/runtime/utils.mjs +1 -0
  31. package/dist/esm/shared/routeDescriptor.mjs +22 -0
  32. package/dist/esm-node/cli/artifacts.mjs +2 -1
  33. package/dist/esm-node/cli/index.mjs +1 -7
  34. package/dist/esm-node/cli/tanstackTypes/canonicalRoutes.mjs +7 -7
  35. package/dist/esm-node/cli/tanstackTypes/shared.mjs +1 -9
  36. package/dist/esm-node/cli/tanstackTypes/source.mjs +29 -64
  37. package/dist/esm-node/runtime/lifecycle.mjs +1 -1
  38. package/dist/esm-node/runtime/loaderBridge.mjs +8 -5
  39. package/dist/esm-node/runtime/navigation.mjs +65 -0
  40. package/dist/esm-node/runtime/plugin.mjs +6 -1
  41. package/dist/esm-node/runtime/plugin.node.mjs +83 -65
  42. package/dist/esm-node/runtime/routeTree/construction.mjs +19 -61
  43. package/dist/esm-node/runtime/routeTree/loaders.mjs +2 -2
  44. package/dist/esm-node/runtime/routeTree/paths.mjs +1 -4
  45. package/dist/esm-node/runtime/ssrManagedTags.mjs +1 -8
  46. package/dist/esm-node/runtime/utils.mjs +1 -0
  47. package/dist/esm-node/shared/routeDescriptor.mjs +23 -0
  48. package/dist/types/cli/tanstackTypes/shared.d.ts +0 -2
  49. package/dist/types/runtime/lifecycle.d.ts +1 -1
  50. package/dist/types/runtime/loaderBridge.d.ts +1 -0
  51. package/dist/types/runtime/navigation.d.ts +4 -0
  52. package/dist/types/runtime/routeTree/paths.d.ts +0 -2
  53. package/dist/types/runtime/ssrManagedTags.d.ts +0 -1
  54. package/dist/types/runtime/ssrTypes.d.ts +0 -7
  55. package/dist/types/shared/routeDescriptor.d.ts +26 -0
  56. package/package.json +21 -21
  57. package/src/cli/artifacts.ts +6 -1
  58. package/src/cli/tanstackTypes/canonicalRoutes.ts +17 -12
  59. package/src/cli/tanstackTypes/shared.ts +0 -14
  60. package/src/cli/tanstackTypes/source.ts +43 -119
  61. package/src/runtime/lifecycle.ts +0 -1
  62. package/src/runtime/loaderBridge.ts +9 -9
  63. package/src/runtime/navigation.ts +78 -0
  64. package/src/runtime/plugin.node.tsx +111 -91
  65. package/src/runtime/plugin.tsx +6 -0
  66. package/src/runtime/routeTree/construction.ts +32 -112
  67. package/src/runtime/routeTree/loaders.ts +2 -4
  68. package/src/runtime/routeTree/paths.ts +0 -6
  69. package/src/runtime/ssrManagedTags.ts +0 -17
  70. package/src/runtime/ssrTypes.ts +0 -7
  71. package/src/runtime/utils.tsx +1 -0
  72. package/src/shared/routeDescriptor.ts +46 -0
  73. package/tests/router/clientNavigation.ts +13 -0
  74. package/tests/router/generateRouteArtifacts.test.ts +36 -2
  75. package/tests/router/localisedIdentity.test.tsx +7 -2
  76. package/tests/router/prefetchLink.test.tsx +29 -0
  77. package/tests/router/routeHooks.test.ts +2 -1
  78. package/tests/router/serverPlugin.test.ts +150 -0
  79. package/tests/router/tanstackTypes.test.ts +121 -2
  80. package/dist/esm/rslib-runtime~1.mjs +0 -18
  81. package/dist/esm-node/rslib-runtime~0.mjs +0 -19
@@ -5,7 +5,9 @@ const createRunOptionsMock = rstest.fn(async (options: unknown) => ({
5
5
  mockRunOptions: true,
6
6
  received: options,
7
7
  }));
8
- const cliInitMock = rstest.fn(async () => ({ appContext: {} }));
8
+ const cliInitMock = rstest.fn(async () => ({
9
+ appContext: { hooks: { onBeforeExit: { call: async () => {} } } },
10
+ }));
9
11
 
10
12
  rstest.mock('@modern-js/app-tools/cli/run', () => ({
11
13
  createRunOptions: createRunOptionsMock,
@@ -17,6 +19,7 @@ rstest.mock('@modern-js/plugin/cli', () => ({
17
19
  import fs from 'node:fs';
18
20
  import os from 'node:os';
19
21
  import path from 'node:path';
22
+ import { createAsyncHook } from '@modern-js/plugin';
20
23
  import { INTERNAL_RUNTIME_PLUGINS } from '@modern-js/utils';
21
24
  import {
22
25
  generateTanstackRouteArtifacts,
@@ -63,6 +66,12 @@ describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
63
66
  mockRunOptions: true,
64
67
  received: runOptions,
65
68
  });
69
+ const onBeforeExit = createAsyncHook<() => Promise<void>>();
70
+ let disposed = false;
71
+ onBeforeExit.tap(async () => {
72
+ await Promise.resolve();
73
+ disposed = true;
74
+ });
66
75
  cliInitMock.mockImplementation(async () => {
67
76
  await writeTanstackRouterTypesForEntries({
68
77
  appContext: {
@@ -88,7 +97,7 @@ describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
88
97
  ] as any,
89
98
  },
90
99
  });
91
- return { appContext: {} };
100
+ return { appContext: { hooks: { onBeforeExit } } };
92
101
  });
93
102
 
94
103
  try {
@@ -110,6 +119,7 @@ describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
110
119
  );
111
120
 
112
121
  expect(cliInitMock).toHaveBeenCalledTimes(1);
122
+ expect(disposed).toBe(true);
113
123
 
114
124
  expect(
115
125
  fs.existsSync(
@@ -131,6 +141,30 @@ describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
131
141
  }
132
142
  });
133
143
 
144
+ it('preserves initialization and cleanup failures', async () => {
145
+ const appDirectory = makeAppDir(true);
146
+ const initializationFailure = new Error('prepare failed');
147
+ const cleanupFailure = new Error('dispose failed');
148
+ const onBeforeExit = createAsyncHook<() => Promise<void>>();
149
+ onBeforeExit.tap(async () => {
150
+ throw cleanupFailure;
151
+ });
152
+ try {
153
+ cliInitMock.mockRejectedValueOnce(initializationFailure);
154
+ await expect(
155
+ generateTanstackRouteArtifacts({ appDirectory }),
156
+ ).rejects.toBe(initializationFailure);
157
+ cliInitMock.mockResolvedValueOnce({
158
+ appContext: { hooks: { onBeforeExit } },
159
+ });
160
+ await expect(
161
+ generateTanstackRouteArtifacts({ appDirectory }),
162
+ ).rejects.toBe(cleanupFailure);
163
+ } finally {
164
+ fs.rmSync(appDirectory, { force: true, recursive: true });
165
+ }
166
+ });
167
+
134
168
  it('throws a clear error when no config file exists', async () => {
135
169
  const appDirectory = makeAppDir(false);
136
170
 
@@ -12,6 +12,7 @@ import { renderToString } from 'react-dom/server';
12
12
  import { createModernBasepathRewrite } from '../../src/runtime/basepathRewrite';
13
13
  import { createRouteTreeFromRouteObjects } from '../../src/runtime/routeTree';
14
14
  import { createTanstackRouteObjectsFromConfig } from '../../src/runtime/utils';
15
+ import { navigateOnClient } from './clientNavigation';
15
16
 
16
17
  function SearchPage() {
17
18
  const data = useLoaderData({ from: '/$lang/search' }) as { language: string };
@@ -55,6 +56,7 @@ test('localized URLs preserve native loader and params route identity in SSR and
55
56
  '/search': { en: '/find', cs: '/hledat' },
56
57
  '/resources/:id': { en: '/resources/:id', cs: '/zdroje/:id' },
57
58
  },
59
+ 'canonical',
58
60
  );
59
61
  const routeObjects = createTanstackRouteObjectsFromConfig({
60
62
  routesConfig: { routes },
@@ -79,14 +81,17 @@ test('localized URLs preserve native loader and params route identity in SSR and
79
81
  );
80
82
  expect(history.location.href).toBe('/base/cs/hledat?q=tractor#results');
81
83
 
82
- await router.navigate({ to: '/en/search', search: { q: 'tractor' } });
84
+ await navigateOnClient(router, {
85
+ to: '/en/search',
86
+ search: { q: 'tractor' },
87
+ });
83
88
  expect(history.location.href).toBe('/base/en/find?q=tractor');
84
89
  expect(router.state.matches.at(-1)?.routeId).toBe('/$lang/search');
85
90
  expect(renderToString(createElement(RouterProvider, { router }))).toContain(
86
91
  'search:en',
87
92
  );
88
93
 
89
- await router.navigate({ to: '/cs/zdroje/a%2Fb' });
94
+ await navigateOnClient(router, { to: '/cs/zdroje/a%2Fb' });
90
95
  expect(history.location.href).toBe('/base/cs/zdroje/a%2Fb');
91
96
  expect(router.state.matches.at(-1)?.routeId).toBe('/$lang/resources/$id');
92
97
  expect(renderToString(createElement(RouterProvider, { router }))).toContain(
@@ -7,6 +7,7 @@ import {
7
7
  } from '@tanstack/react-router';
8
8
  import { act, render, waitFor } from '@testing-library/react';
9
9
  import React from 'react';
10
+ import { createTanstackNavigation } from '../../src/runtime/navigation';
10
11
  import { Link, NavLink } from '../../src/runtime/prefetchLink';
11
12
 
12
13
  // Real-router harness (no module mocking): STATIC_ACTIVE_PROPS (the
@@ -120,3 +121,31 @@ describe('tanstack prefetch link adapter - aria-current override', () => {
120
121
  expect(ref.current).toBe(anchor);
121
122
  });
122
123
  });
124
+
125
+ test('provider navigation observes native navigation and preserves history state', async () => {
126
+ const { router, unmount } = await renderLink({ initialPath: '/' });
127
+ const capability = createTanstackNavigation(router);
128
+ const initial = capability.getSnapshot();
129
+ expect(capability.getSnapshot()).toBe(initial);
130
+ const listener = rstest.fn();
131
+ const stop = capability.subscribe(listener);
132
+ await act(() =>
133
+ capability.navigate('/settings?q=two#detail', {
134
+ replace: true,
135
+ state: { custom: true },
136
+ }),
137
+ );
138
+ expect(listener).toHaveBeenCalled();
139
+ expect(capability.getSnapshot().location).toEqual({
140
+ pathname: '/settings',
141
+ search: '?q=two',
142
+ hash: '#detail',
143
+ });
144
+ expect(router.state.location.state).toMatchObject({ custom: true });
145
+ expect(router.history.length).toBe(1);
146
+ stop();
147
+ listener.mockClear();
148
+ await act(() => capability.navigate('/'));
149
+ expect(listener).not.toHaveBeenCalled();
150
+ unmount();
151
+ });
@@ -8,6 +8,7 @@ import {
8
8
  import React from 'react';
9
9
  import { renderToString } from 'react-dom/server';
10
10
  import { useMatches } from '../../src/runtime/routeHooks';
11
+ import { navigateOnClient } from './clientNavigation';
11
12
 
12
13
  function MatchIds() {
13
14
  const matches = useMatches();
@@ -41,7 +42,7 @@ test('updates whole match objects after navigation with the structural-sharing g
41
42
  renderToString(React.createElement(RouterProvider, { router })),
42
43
  ).toContain('__root__');
43
44
 
44
- await router.navigate({ to: '/settings' });
45
+ await navigateOnClient(router, { to: '/settings' });
45
46
 
46
47
  expect(
47
48
  renderToString(React.createElement(RouterProvider, { router })),
@@ -9,6 +9,8 @@ import {
9
9
  } from '../../src/runtime/lifecycle';
10
10
  import { tanstackRouterPlugin } from '../../src/runtime/plugin.node';
11
11
  import type { TanstackRouterPluginAPI } from '../../src/runtime/pluginShared';
12
+ import * as ssrUtils from '../../src/runtime/ssrManagedTags';
13
+ import * as ssrPreload from '../../src/runtime/ssrPreload';
12
14
  import type { RouterConfig } from '../../src/runtime/types';
13
15
 
14
16
  type BeforeRenderListener = Parameters<
@@ -116,6 +118,11 @@ describe('tanstack server plugin router results', () => {
116
118
  framework: 'tanstack',
117
119
  statusCode: 200,
118
120
  });
121
+ // Without the dehydrated router the client re-renders matched remotes
122
+ // from scratch instead of hydrating the server HTML.
123
+ expect(getRouterServerSnapshot(context)?.hydrationScripts).toEqual(
124
+ expect.arrayContaining([expect.stringContaining('$_TSR')]),
125
+ );
119
126
  });
120
127
 
121
128
  test('interrupts SSR with the router redirect response', async () => {
@@ -155,3 +162,146 @@ describe('tanstack server plugin router results', () => {
155
162
  expect(status).not.toHaveBeenCalled();
156
163
  });
157
164
  });
165
+
166
+ describe('TanStack preparation resource lifetime', () => {
167
+ test.each([
168
+ 'attach',
169
+ 'load',
170
+ 'result',
171
+ 'preload',
172
+ 'dehydrate',
173
+ 'abort',
174
+ 'clientRender',
175
+ 'afterCreate',
176
+ ])('disposes exactly once when %s fails', async stage => {
177
+ const failure = new Error(`${stage} failed`);
178
+ const cleanup = rstest.fn();
179
+ const controller = new AbortController();
180
+ const { context } = createServerContext('/');
181
+ context.ssrContext!.request.raw = new Request('http://localhost/', {
182
+ signal: controller.signal,
183
+ });
184
+ const attach = ssrUtils.attachServerSsrUtils;
185
+ rstest
186
+ .spyOn(ssrUtils, 'attachServerSsrUtils')
187
+ .mockImplementation(async router => {
188
+ await attach(router);
189
+ rstest.spyOn(router.serverSsr!, 'cleanup').mockImplementation(cleanup);
190
+ if (stage === 'attach') throw failure;
191
+ if (stage === 'load')
192
+ rstest.spyOn(router, 'load').mockRejectedValue(failure);
193
+ if (stage === 'result')
194
+ rstest.spyOn(router, 'load').mockResolvedValue(undefined);
195
+ if (stage === 'dehydrate')
196
+ rstest
197
+ .spyOn(router.serverSsr!, 'dehydrate')
198
+ .mockRejectedValue(failure);
199
+ if (stage === 'abort') controller.abort(failure);
200
+ });
201
+ if (stage === 'clientRender') {
202
+ context.ssrContext!.loaderFailureMode = 'clientRender';
203
+ rstest
204
+ .spyOn(ssrUtils, 'collectRouterErrors')
205
+ .mockReturnValue({ root: failure });
206
+ }
207
+ if (stage === 'afterCreate')
208
+ rstest
209
+ .spyOn(routerProviderRegistryHooks.onAfterCreateRouter, 'call')
210
+ .mockImplementation(() => {
211
+ throw failure;
212
+ });
213
+ if (stage === 'preload')
214
+ rstest
215
+ .spyOn(ssrPreload, 'preloadMatchedRouteComponents')
216
+ .mockRejectedValue(failure);
217
+ const beforeRender = collectBeforeRender(() => [
218
+ { id: 'root', path: '/', Component: () => null },
219
+ ]);
220
+ await expect(beforeRender(context, value => value)).rejects.toThrow(
221
+ stage === 'result' ? 'without a server result' : failure.message,
222
+ );
223
+ expect(cleanup).toHaveBeenCalledTimes(1);
224
+ await getRouterRuntimeState(context)?.cleanup?.();
225
+ expect(cleanup).toHaveBeenCalledTimes(1);
226
+ });
227
+
228
+ test.each([
229
+ 'string',
230
+ 'stream',
231
+ ] as const)('retains %s router resources for response termination', async mode => {
232
+ const { context } = createServerContext('/products/shoe?q=1#detail');
233
+ context.ssrContext!.mode = mode;
234
+ const beforeRender = collectBeforeRender(() => [
235
+ { id: 'product', path: '/products/:id', Component: () => null },
236
+ ]);
237
+ await beforeRender(context, value => value);
238
+ const state = getRouterRuntimeState(context)!;
239
+ const router = state.instance as AnyRouter;
240
+ const cleanup = rstest.spyOn(router.serverSsr!, 'cleanup');
241
+ const navigation = state.navigation!;
242
+ expect(navigation.getSnapshot()).toMatchObject({
243
+ location: { pathname: '/products/shoe', search: '?q=1', hash: '#detail' },
244
+ params: { id: 'shoe' },
245
+ });
246
+ expect(navigation.getSnapshot()).toBe(navigation.getSnapshot());
247
+ expect(navigation.Link).toBe(context.router?.Link);
248
+ expect(
249
+ navigation.createLinkProps!({
250
+ pathname: '/products/shoe',
251
+ href: '/products/shoe?q=2#detail',
252
+ search: { q: '2' },
253
+ hash: 'detail',
254
+ hashScrollIntoView: false,
255
+ prefetch: 'none',
256
+ }),
257
+ ).toEqual({
258
+ to: '/products/shoe',
259
+ search: { q: '2' },
260
+ hash: 'detail',
261
+ hashScrollIntoView: false,
262
+ preload: false,
263
+ });
264
+ expect(cleanup).not.toHaveBeenCalled();
265
+ await state.cleanup?.();
266
+ await state.cleanup?.();
267
+ expect(cleanup).toHaveBeenCalledTimes(1);
268
+ });
269
+ });
270
+
271
+ test('incoming abort reaches an active Modern loader and disposes its router', async () => {
272
+ const incoming = new AbortController();
273
+ let started!: () => void;
274
+ const loading = new Promise<void>(resolve => {
275
+ started = resolve;
276
+ });
277
+ const { context } = createServerContext('/');
278
+ context.ssrContext!.request.raw = new Request('http://localhost/', {
279
+ signal: incoming.signal,
280
+ });
281
+ const failure = new Error('client disconnected during loader');
282
+ const beforeRender = collectBeforeRender(() => [
283
+ {
284
+ id: 'root',
285
+ path: '/',
286
+ Component: () => null,
287
+ loader: ({ request }: { request: Request }) =>
288
+ new Promise((_resolve, reject) => {
289
+ request.signal.addEventListener(
290
+ 'abort',
291
+ () => reject(request.signal.reason),
292
+ { once: true },
293
+ );
294
+ started();
295
+ }),
296
+ },
297
+ ]);
298
+ const pending = beforeRender(context, value => value);
299
+ await loading;
300
+ const router = getRouterRuntimeState(context)!.instance as AnyRouter;
301
+ const cleanup = rstest.spyOn(router.serverSsr!, 'cleanup');
302
+ incoming.abort(failure);
303
+ await expect(pending).rejects.toBe(failure);
304
+ expect(cleanup).toHaveBeenCalledTimes(1);
305
+ await getRouterRuntimeState(context)?.cleanup?.();
306
+ expect(cleanup).toHaveBeenCalledTimes(1);
307
+ });
@@ -1,14 +1,20 @@
1
1
  import { execFile } from 'node:child_process';
2
- import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
2
+ import { mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import path from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
5
6
  import { promisify } from 'node:util';
6
-
7
+ import { applyLocalisedUrlsToRoutes } from '@modern-js/i18n-runtime-extensions';
8
+ import { createMemoryHistory, createRouter } from '@tanstack/react-router';
7
9
  import { writeTanstackRegisterFile } from '../../src/cli/artifacts';
8
10
  import {
9
11
  collectCanonicalRoutesForEntry,
10
12
  generateTanstackRouterTypesSourceForEntry,
11
13
  } from '../../src/cli/tanstackTypes';
14
+ import { createModernBasepathRewrite } from '../../src/runtime/basepathRewrite';
15
+ import { createRouteTreeFromRouteObjects } from '../../src/runtime/routeTree';
16
+ import { createTanstackRouteObjectsFromConfig } from '../../src/runtime/utils';
17
+ import { navigateOnClient } from './clientNavigation';
12
18
 
13
19
  const execFileAsync = promisify(execFile);
14
20
  const strictestTsconfigPath = path.resolve(
@@ -261,6 +267,119 @@ describe('tanstack router type generation', () => {
261
267
  });
262
268
  });
263
269
 
270
+ test('generated and runtime trees agree on localized splats, root siblings, layouts and params', async () => {
271
+ tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-parity-'));
272
+ const localized = applyLocalisedUrlsToRoutes(
273
+ [
274
+ {
275
+ type: 'nested',
276
+ id: 'lang',
277
+ path: ':lang',
278
+ children: [
279
+ {
280
+ type: 'nested',
281
+ id: 'products',
282
+ path: 'products',
283
+ children: [{ type: 'nested', id: 'product-files', path: '*' }],
284
+ },
285
+ { type: 'nested', id: 'fallback', path: '*' },
286
+ ],
287
+ },
288
+ ],
289
+ ['en', 'cs'],
290
+ { '/products': { en: '/products', cs: '/produkty' } },
291
+ 'canonical',
292
+ );
293
+ const routes = [
294
+ {
295
+ type: 'nested',
296
+ id: 'layout',
297
+ isRoot: true,
298
+ children: [
299
+ ...localized,
300
+ {
301
+ type: 'nested',
302
+ children: [{ type: 'nested', path: 'one/:id?', index: false }],
303
+ },
304
+ { type: 'nested', children: [{ type: 'nested', path: 'two/*' }] },
305
+ { type: 'nested', id: 'index', index: true },
306
+ ],
307
+ },
308
+ { type: 'nested', id: 'sibling', path: 'outside/:lang' },
309
+ ] as any;
310
+ const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
311
+ appContext: {
312
+ srcDirectory: path.join(tempDir, 'src'),
313
+ internalSrcAlias: '@/_',
314
+ } as any,
315
+ entryName: 'parity',
316
+ routes,
317
+ });
318
+ const generated = path.join(tempDir, 'router.gen.ts');
319
+ await writeFile(generated, routerGenTs);
320
+ const modules = path.join(tempDir, 'node_modules', '@modern-js');
321
+ await mkdir(modules, { recursive: true });
322
+ await symlink(
323
+ path.resolve(__dirname, '../..'),
324
+ path.join(modules, 'plugin-tanstack'),
325
+ 'junction',
326
+ );
327
+ const { stdout } = await execFileAsync(process.execPath, [
328
+ '--input-type=module',
329
+ '--eval',
330
+ `
331
+ import { router } from ${JSON.stringify(pathToFileURL(generated).href)};
332
+ import { createModernBasepathRewrite } from ${JSON.stringify(pathToFileURL(path.resolve(__dirname, '../../dist/esm/runtime/basepathRewrite.mjs')).href)};
333
+ router.update({ rewrite: createModernBasepathRewrite('/base', false, ${JSON.stringify(routes)}) });
334
+ router.history.push(router.buildLocation({ to: '/cs/produkty/a/b', search: { q: 'tractor' }, hash: 'files' }).publicHref); router.updateLatestLocation(); await router.load();
335
+ console.log(JSON.stringify({ ids: Object.keys(router.routesById).sort(), match: router.state.matches.at(-1).routeId, params: router.state.matches.at(-1).params, href: router.history.location.href }));
336
+ `,
337
+ ]);
338
+ const routeObjects = createTanstackRouteObjectsFromConfig({
339
+ routesConfig: { routes },
340
+ })!;
341
+ const runtime = createRouter({
342
+ routeTree: createRouteTreeFromRouteObjects(routeObjects),
343
+ history: createMemoryHistory({
344
+ initialEntries: ['/base/cs/produkty/a/b?q=tractor#files'],
345
+ }),
346
+ rewrite: createModernBasepathRewrite('/base', false, routeObjects),
347
+ });
348
+ await runtime.load();
349
+ expect(JSON.parse(stdout)).toEqual({
350
+ ids: Object.keys(runtime.routesById).sort(),
351
+ match: runtime.state.matches.at(-1)?.routeId,
352
+ params: runtime.state.matches.at(-1)?.params,
353
+ href: runtime.history.location.href,
354
+ });
355
+ expect(runtime.state.matches.at(-1)?.routeId).toBe('/$lang/products/$');
356
+ expect(runtime.state.matches.at(-1)?.params).toMatchObject({
357
+ lang: 'cs',
358
+ _splat: 'a/b',
359
+ });
360
+ await navigateOnClient(runtime, { to: '/cs/missing/path' });
361
+ expect(runtime.state.matches.at(-1)?.routeId).toBe('/$lang/$');
362
+ expect(runtime.state.matches.at(-1)?.params).toMatchObject({
363
+ lang: 'cs',
364
+ _splat: 'missing/path',
365
+ });
366
+ await navigateOnClient(runtime, { to: '/outside/cs' });
367
+ expect(runtime.state.matches.at(-1)?.params).toEqual({ lang: 'cs' });
368
+ const canonicalRoutes = collectCanonicalRoutesForEntry(localized as any)!;
369
+ expect(Object.keys(canonicalRoutes)).toEqual(['/$', '/products/$']);
370
+ await typecheckCanonicalRegisterContract({
371
+ projectDirectory: tempDir,
372
+ canonicalRoutes,
373
+ contractLines: [
374
+ "const product: UltramodernCanonicalRoutes['/products/$'] = { _splat: 'a/b' };",
375
+ "const fallback: UltramodernCanonicalRoutes['/$'] = { _splat: 'unknown/path' };",
376
+ '// @ts-expect-error splat remains a string',
377
+ "const invalid: UltramodernCanonicalRoutes['/products/$'] = { _splat: 42 };",
378
+ 'void product; void fallback; void invalid;',
379
+ ],
380
+ });
381
+ });
382
+
264
383
  test('maps a locale-prefixed route tree to the canonical paths consumers navigate to', () => {
265
384
  const routes = [
266
385
  {
@@ -1,18 +0,0 @@
1
- var __webpack_modules__ = {};
2
- var __webpack_module_cache__ = {};
3
- function __webpack_require__(moduleId) {
4
- var cachedModule = __webpack_module_cache__[moduleId];
5
- if (void 0 !== cachedModule) return cachedModule.exports;
6
- var module = __webpack_module_cache__[moduleId] = {
7
- exports: {}
8
- };
9
- __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
10
- return module.exports;
11
- }
12
- __webpack_require__.m = __webpack_modules__;
13
- (()=>{
14
- __webpack_require__.add = function(modules) {
15
- Object.assign(__webpack_require__.m, modules);
16
- };
17
- })();
18
- export { __webpack_require__ };
@@ -1,19 +0,0 @@
1
- import "node:module";
2
- var __webpack_modules__ = {};
3
- var __webpack_module_cache__ = {};
4
- function __webpack_require__(moduleId) {
5
- var cachedModule = __webpack_module_cache__[moduleId];
6
- if (void 0 !== cachedModule) return cachedModule.exports;
7
- var module = __webpack_module_cache__[moduleId] = {
8
- exports: {}
9
- };
10
- __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
11
- return module.exports;
12
- }
13
- __webpack_require__.m = __webpack_modules__;
14
- (()=>{
15
- __webpack_require__.add = function(modules) {
16
- Object.assign(__webpack_require__.m, modules);
17
- };
18
- })();
19
- export { __webpack_require__ };