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

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 (82) 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 +9 -0
  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 +9 -0
  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/runtime/basepathRewrite.d.ts +2 -1
  38. package/dist/types/runtime/lifecycle.d.ts +9 -2
  39. package/dist/types/runtime/register.d.ts +1 -9
  40. package/dist/types/runtime/router.d.ts +2 -13
  41. package/dist/types/runtime/state.d.ts +1 -1
  42. package/dist/types/runtime/types.d.ts +2 -1
  43. package/package.json +18 -15
  44. package/rstest.config.mts +1 -24
  45. package/src/cli/index.ts +10 -0
  46. package/src/cli/tanstackTypes/source.ts +3 -1
  47. package/src/runtime/basepathRewrite.ts +11 -2
  48. package/src/runtime/lifecycle.ts +14 -3
  49. package/src/runtime/plugin.node.tsx +9 -2
  50. package/src/runtime/plugin.tsx +5 -1
  51. package/src/runtime/prefetchLink.tsx +1 -23
  52. package/src/runtime/register.ts +1 -12
  53. package/src/runtime/routeTree/construction.ts +2 -0
  54. package/src/runtime/router.ts +25 -40
  55. package/src/runtime/state.ts +1 -1
  56. package/src/runtime/types.ts +2 -2
  57. package/src/runtime/utils.tsx +3 -1
  58. package/tests/router/cli.test.ts +148 -824
  59. package/tests/router/dataMutation.test.tsx +17 -85
  60. package/tests/router/flightSerialization.roundtrip.test.tsx +9 -6
  61. package/tests/router/generateRouteArtifacts.test.ts +68 -28
  62. package/tests/router/loaderBridge.test.ts +0 -25
  63. package/tests/router/localisedIdentity.test.tsx +95 -0
  64. package/tests/router/prefetchLink.test.tsx +17 -21
  65. package/tests/router/prefetchLinkPreload.test.tsx +11 -31
  66. package/tests/router/routeHooks.test.ts +41 -105
  67. package/tests/router/routeTree.test.ts +144 -836
  68. package/tests/router/rsc.test.tsx +131 -166
  69. package/tests/router/serverPlugin.test.ts +43 -1
  70. package/tests/router/tanstackTypes.test.ts +109 -973
  71. package/tests/realm-isolation.test.ts +0 -114
  72. package/tests/router/fastDefaults.test.ts +0 -32
  73. package/tests/router/fixtures/legacyRuntimeRouter.ts +0 -10
  74. package/tests/router/fixtures/rscDecoderSecurityChild.cjs +0 -162
  75. package/tests/router/hooks.test.ts +0 -40
  76. package/tests/router/hydrationBoundary.test.tsx +0 -23
  77. package/tests/router/packageSurface.test.ts +0 -24
  78. package/tests/router/preloadRedirect.test.ts +0 -64
  79. package/tests/router/register.test.ts +0 -8
  80. package/tests/router/routerCompatibility.test.ts +0 -239
  81. package/tests/router/rscDecoderSecurity.test.ts +0 -113
  82. package/tests/router/rscPayloadRouterMatrix.test.ts +0 -197
@@ -4,31 +4,6 @@ import { tmpdir } from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { promisify } from 'node:util';
6
6
 
7
- const resolverDelays = new Map<string, number>();
8
-
9
- rstest.mock('../../src/cli/tanstackTypes/shared', () => {
10
- const actual = rstest.requireActual(
11
- '../../src/cli/tanstackTypes/shared',
12
- ) as typeof import('../../src/cli/tanstackTypes/shared');
13
-
14
- return {
15
- ...actual,
16
- resolveFileNoExt: async (inputNoExtPath: string) => {
17
- const normalized = inputNoExtPath.replaceAll('\\', '/');
18
- const delay =
19
- [...resolverDelays.entries()].find(([suffix]) =>
20
- normalized.endsWith(suffix),
21
- )?.[1] ?? 0;
22
-
23
- if (delay > 0) {
24
- await new Promise(resolve => setTimeout(resolve, delay));
25
- }
26
-
27
- return actual.resolveFileNoExt(inputNoExtPath);
28
- },
29
- };
30
- });
31
-
32
7
  import { writeTanstackRegisterFile } from '../../src/cli/artifacts';
33
8
  import {
34
9
  collectCanonicalRoutesForEntry,
@@ -36,15 +11,6 @@ import {
36
11
  } from '../../src/cli/tanstackTypes';
37
12
 
38
13
  const execFileAsync = promisify(execFile);
39
- const routerGenOxlintConfigPath = path.join(
40
- __dirname,
41
- 'fixtures',
42
- 'router-gen.oxlint.json',
43
- );
44
- const oxlintCliPath = path.resolve(
45
- __dirname,
46
- '../../../../toolkit/code-tools/node_modules/oxlint/bin/oxlint',
47
- );
48
14
  const strictestTsconfigPath = path.resolve(
49
15
  __dirname,
50
16
  '../../node_modules/@tsconfig/strictest/tsconfig.json',
@@ -54,195 +20,51 @@ const installedReactRouterPath = path.resolve(
54
20
  '../../node_modules/@tanstack/react-router',
55
21
  );
56
22
 
57
- async function writeRuntimeTestPackage(projectDirectory: string) {
58
- const runtimePackageDirectory = path.join(
59
- projectDirectory,
60
- 'node_modules',
61
- '@modern-js',
62
- 'plugin-tanstack',
63
- );
64
- await mkdir(runtimePackageDirectory, { recursive: true });
65
- await writeFile(
66
- path.join(runtimePackageDirectory, 'package.json'),
67
- JSON.stringify({
68
- name: '@modern-js/plugin-tanstack',
69
- type: 'commonjs',
70
- exports: {
71
- './runtime': {
72
- types: './runtime.d.ts',
73
- default: './runtime.js',
74
- },
75
- },
76
- }),
77
- );
78
- await writeFile(
79
- path.join(runtimePackageDirectory, 'runtime.d.ts'),
80
- [
81
- 'export type ModernRouterContext = { request?: Request; requestContext?: unknown };',
82
- 'export type ModernRouteStaticData = { modernRouteId?: string; modernRouteAction?: unknown; modernRouteLoader?: unknown };',
83
- 'export type RouteOptions = {',
84
- ' component?: unknown;',
85
- ' getParentRoute?: (...args: never[]) => unknown;',
86
- ' id?: string;',
87
- ' loader?: (...args: never[]) => unknown;',
88
- ' loaderDeps?: (...args: never[]) => unknown;',
89
- ' path?: string;',
90
- ' staticData?: ModernRouteStaticData;',
91
- ' validateSearch?: (...args: never[]) => unknown;',
92
- '};',
93
- 'export type Route<TOptions extends RouteOptions = RouteOptions> = {',
94
- ' options: TOptions;',
95
- ' addChildren<const TChildren extends readonly unknown[]>(children: TChildren): Route<TOptions> & { children: TChildren };',
96
- '};',
97
- 'export const runtimeState: { adapterCalls: Array<{ hasSplat: boolean; modernLoader: unknown }> };',
98
- 'export function createMemoryHistory<TOptions>(options: TOptions): TOptions;',
99
- 'export const modernTanstackRouterFastDefaults: { defaultPreload: "intent" };',
100
- 'export function createRootRouteWithContext<TContext extends ModernRouterContext>(): <const TOptions extends RouteOptions>(options: TOptions) => Route<TOptions>;',
101
- 'export function createRoute<const TOptions extends RouteOptions>(options: TOptions): Route<TOptions>;',
102
- 'export function createRouter<const TOptions extends { context: ModernRouterContext; history: unknown; routeTree: unknown }>(options: TOptions): TOptions;',
103
- 'export function createRouteStaticData<const TData extends ModernRouteStaticData>(data: TData): TData;',
104
- 'export function modernLoaderToTanstack<TLoader extends (...args: never[]) => unknown>(options: { hasSplat: boolean }, modernLoader: TLoader): (context: unknown) => Promise<Awaited<ReturnType<TLoader>>>;',
105
- ].join('\n'),
106
- );
107
- await writeFile(
108
- path.join(runtimePackageDirectory, 'runtime.js'),
109
- [
110
- 'const runtimeState = { adapterCalls: [] };',
111
- 'const createRouteRecord = options => ({',
112
- ' options,',
113
- ' addChildren(children) {',
114
- ' this.children = children;',
115
- ' return this;',
116
- ' },',
117
- '});',
118
- 'const createMemoryHistory = options => options;',
119
- 'const modernTanstackRouterFastDefaults = { defaultPreload: "intent" };',
120
- 'const createRootRouteWithContext = () => options => createRouteRecord(options);',
121
- 'const createRoute = options => createRouteRecord(options);',
122
- 'const createRouter = options => options;',
123
- 'const createRouteStaticData = data => data;',
124
- 'const modernLoaderToTanstack = (options, modernLoader) => {',
125
- ' runtimeState.adapterCalls.push({ ...options, modernLoader });',
126
- ' return async context => modernLoader(context);',
127
- '};',
128
- 'module.exports = { createMemoryHistory, createRootRouteWithContext, createRoute, createRouter, createRouteStaticData, modernLoaderToTanstack, modernTanstackRouterFastDefaults, runtimeState };',
129
- ].join('\n'),
130
- );
131
- }
132
-
133
- async function writeInstalledRouterRuntimeTestPackage(
23
+ async function writeTsconfig(
134
24
  projectDirectory: string,
25
+ compilerOptions: Record<string, unknown>,
135
26
  ) {
136
- const runtimePackageDirectory = path.join(
137
- projectDirectory,
138
- 'node_modules',
139
- '@modern-js',
140
- 'plugin-tanstack',
141
- );
142
- await mkdir(runtimePackageDirectory, { recursive: true });
143
- await writeFile(
144
- path.join(runtimePackageDirectory, 'package.json'),
145
- JSON.stringify({
146
- name: '@modern-js/plugin-tanstack',
147
- type: 'commonjs',
148
- exports: {
149
- './runtime': {
150
- types: './runtime.d.ts',
151
- default: './runtime.js',
152
- },
153
- },
154
- }),
155
- );
156
- await writeFile(
157
- path.join(runtimePackageDirectory, 'runtime.d.ts'),
158
- [
159
- "export { createMemoryHistory, createRootRouteWithContext, createRoute, createRouter } from '@tanstack/react-router';",
160
- 'export type ModernRouterContext = { request?: Request; requestContext?: unknown };',
161
- 'export const modernTanstackRouterFastDefaults: { readonly defaultStructuralSharing: true };',
162
- 'export function createRouteStaticData<TData extends { modernRouteId?: string; modernRouteAction?: unknown; modernRouteLoader?: unknown }>(data: TData): TData;',
163
- 'export function modernLoaderToTanstack<TLoader extends (...args: any[]) => any>(options: { hasSplat: boolean }, modernLoader: TLoader): (context: unknown) => Promise<Awaited<ReturnType<TLoader>>>;',
164
- ].join('\n'),
165
- );
166
- }
167
-
168
- async function compileAndRunGeneratedRouter(options: {
169
- projectDirectory: string;
170
- routerGenTs: string;
171
- runtimeCheck: string;
172
- }) {
173
- const { projectDirectory, routerGenTs, runtimeCheck } = options;
174
- const generatedDirectory = path.join(
175
- projectDirectory,
176
- 'src',
177
- 'modern-tanstack',
178
- 'index',
179
- );
180
- await mkdir(generatedDirectory, { recursive: true });
181
- await writeFile(path.join(generatedDirectory, 'router.gen.ts'), routerGenTs);
182
- await writeFile(
183
- path.join(projectDirectory, 'src', 'runtime-check.ts'),
184
- runtimeCheck,
185
- );
186
- await writeRuntimeTestPackage(projectDirectory);
187
- await writeFile(
188
- path.join(projectDirectory, 'package.json'),
189
- JSON.stringify({ private: true, type: 'commonjs' }),
190
- );
191
27
  await writeFile(
192
28
  path.join(projectDirectory, 'tsconfig.json'),
193
29
  JSON.stringify(
194
30
  {
195
31
  extends: strictestTsconfigPath,
196
32
  compilerOptions: {
197
- lib: ['ESNext', 'DOM'],
198
33
  jsx: 'react-jsx',
199
- module: 'Node16',
200
- moduleResolution: 'Node16',
201
- outDir: 'dist',
202
- rootDir: 'src',
203
- target: 'ES2022',
34
+ lib: ['ESNext', 'DOM'],
35
+ noEmit: true,
36
+ target: 'ESNext',
204
37
  types: [],
205
- verbatimModuleSyntax: false,
38
+ ...compilerOptions,
206
39
  },
207
- include: ['src/**/*.ts', 'src/**/*.tsx'],
40
+ include: ['src/**/*.ts', 'src/**/*.tsx', 'src/**/*.d.ts'],
208
41
  },
209
42
  null,
210
43
  2,
211
44
  ),
212
45
  );
46
+ }
213
47
 
48
+ /** Runs the same compiler a consumer build runs; surfaces its diagnostics. */
49
+ async function runTsgo(projectDirectory: string) {
214
50
  try {
215
51
  await execFileAsync(
216
52
  process.platform === 'win32' ? 'tsgo.cmd' : 'tsgo',
217
53
  ['-p', 'tsconfig.json'],
218
- {
219
- cwd: projectDirectory,
220
- shell: process.platform === 'win32',
221
- },
54
+ { cwd: projectDirectory, shell: process.platform === 'win32' },
222
55
  );
223
56
  } catch (error) {
224
- if (error && typeof error === 'object') {
225
- const stdout =
226
- 'stdout' in error && typeof error.stdout === 'string'
227
- ? error.stdout
228
- : '';
229
- const stderr =
230
- 'stderr' in error && typeof error.stderr === 'string'
231
- ? error.stderr
232
- : '';
233
- if (stdout || stderr) {
234
- throw new Error([stdout, stderr].filter(Boolean).join('\n'), {
235
- cause: error,
236
- });
237
- }
57
+ const { stdout, stderr } = (error ?? {}) as {
58
+ stdout?: string;
59
+ stderr?: string;
60
+ };
61
+ if (stdout || stderr) {
62
+ throw new Error([stdout, stderr].filter(Boolean).join('\n'), {
63
+ cause: error,
64
+ });
238
65
  }
239
66
  throw error;
240
67
  }
241
- await execFileAsync(
242
- process.execPath,
243
- [path.join(projectDirectory, 'dist', 'runtime-check.js')],
244
- { cwd: projectDirectory },
245
- );
246
68
  }
247
69
 
248
70
  async function compileGeneratedRouterAgainstInstalledDeclarations(options: {
@@ -258,63 +80,48 @@ async function compileGeneratedRouterAgainstInstalledDeclarations(options: {
258
80
  );
259
81
  await mkdir(generatedDirectory, { recursive: true });
260
82
  await writeFile(path.join(generatedDirectory, 'router.gen.ts'), routerGenTs);
261
- await writeInstalledRouterRuntimeTestPackage(projectDirectory);
262
- await writeFile(
263
- path.join(projectDirectory, 'package.json'),
264
- JSON.stringify({ private: true, type: 'commonjs' }),
83
+
84
+ // The runtime package re-exports the *installed* @tanstack/react-router
85
+ // declarations, so the generated file is checked against the real router
86
+ // types a consumer resolves — not a stand-in.
87
+ const runtimePackageDirectory = path.join(
88
+ projectDirectory,
89
+ 'node_modules',
90
+ '@modern-js',
91
+ 'plugin-tanstack',
265
92
  );
93
+ await mkdir(runtimePackageDirectory, { recursive: true });
266
94
  await writeFile(
267
- path.join(projectDirectory, 'tsconfig.json'),
268
- JSON.stringify(
269
- {
270
- extends: strictestTsconfigPath,
271
- compilerOptions: {
272
- jsx: 'react-jsx',
273
- lib: ['ESNext', 'DOM'],
274
- module: 'Node16',
275
- moduleResolution: 'Node16',
276
- noEmit: true,
277
- paths: {
278
- '@tanstack/react-router': [installedReactRouterPath],
279
- },
280
- target: 'ES2022',
281
- types: [],
282
- verbatimModuleSyntax: false,
283
- },
284
- include: ['src/**/*.ts', 'src/**/*.tsx'],
95
+ path.join(runtimePackageDirectory, 'package.json'),
96
+ JSON.stringify({
97
+ name: '@modern-js/plugin-tanstack',
98
+ type: 'commonjs',
99
+ exports: {
100
+ './runtime': { types: './runtime.d.ts', default: './runtime.js' },
285
101
  },
286
- null,
287
- 2,
288
- ),
102
+ }),
289
103
  );
290
-
291
- try {
292
- await execFileAsync(
293
- process.platform === 'win32' ? 'tsgo.cmd' : 'tsgo',
294
- ['-p', 'tsconfig.json'],
295
- {
296
- cwd: projectDirectory,
297
- shell: process.platform === 'win32',
298
- },
299
- );
300
- } catch (error) {
301
- if (error && typeof error === 'object') {
302
- const stdout =
303
- 'stdout' in error && typeof error.stdout === 'string'
304
- ? error.stdout
305
- : '';
306
- const stderr =
307
- 'stderr' in error && typeof error.stderr === 'string'
308
- ? error.stderr
309
- : '';
310
- if (stdout || stderr) {
311
- throw new Error([stdout, stderr].filter(Boolean).join('\n'), {
312
- cause: error,
313
- });
314
- }
315
- }
316
- throw error;
317
- }
104
+ await writeFile(
105
+ path.join(runtimePackageDirectory, 'runtime.d.ts'),
106
+ [
107
+ "export { createMemoryHistory, createRootRouteWithContext, createRoute, createRouter } from '@tanstack/react-router';",
108
+ 'export type ModernRouterContext = { request?: Request; requestContext?: unknown };',
109
+ 'export const modernTanstackRouterFastDefaults: { readonly defaultStructuralSharing: true };',
110
+ 'export function createRouteStaticData<TData extends { modernRouteId?: string; modernRouteAction?: unknown; modernRouteLoader?: unknown }>(data: TData): TData;',
111
+ 'export function modernLoaderToTanstack<TLoader extends (...args: any[]) => any>(options: { hasSplat: boolean }, modernLoader: TLoader): (context: unknown) => Promise<Awaited<ReturnType<TLoader>>>;',
112
+ ].join('\n'),
113
+ );
114
+ await writeFile(
115
+ path.join(projectDirectory, 'package.json'),
116
+ JSON.stringify({ private: true, type: 'commonjs' }),
117
+ );
118
+ await writeTsconfig(projectDirectory, {
119
+ module: 'Node16',
120
+ moduleResolution: 'Node16',
121
+ paths: { '@tanstack/react-router': [installedReactRouterPath] },
122
+ verbatimModuleSyntax: false,
123
+ });
124
+ await runTsgo(projectDirectory);
318
125
  }
319
126
 
320
127
  async function typecheckCanonicalRegisterContract(options: {
@@ -324,11 +131,11 @@ async function typecheckCanonicalRegisterContract(options: {
324
131
  }) {
325
132
  const { canonicalRoutes, contractLines, projectDirectory } = options;
326
133
  const srcDirectory = path.join(projectDirectory, 'src');
327
- const generatedDirectory = path.join(srcDirectory, 'modern-tanstack');
328
- const routerDirectory = path.join(generatedDirectory, 'index');
329
- await mkdir(routerDirectory, { recursive: true });
134
+ await mkdir(path.join(srcDirectory, 'modern-tanstack', 'index'), {
135
+ recursive: true,
136
+ });
330
137
  await writeFile(
331
- path.join(routerDirectory, 'router.gen.ts'),
138
+ path.join(srcDirectory, 'modern-tanstack', 'index', 'router.gen.ts'),
332
139
  'export const router = { context: {} };',
333
140
  );
334
141
  await writeTanstackRegisterFile({
@@ -350,56 +157,14 @@ async function typecheckCanonicalRegisterContract(options: {
350
157
  ...contractLines,
351
158
  ].join('\n'),
352
159
  );
353
- await writeFile(
354
- path.join(projectDirectory, 'tsconfig.json'),
355
- JSON.stringify(
356
- {
357
- extends: strictestTsconfigPath,
358
- compilerOptions: {
359
- lib: ['ESNext', 'DOM'],
360
- module: 'Preserve',
361
- moduleResolution: 'Bundler',
362
- noEmit: true,
363
- target: 'ESNext',
364
- types: [],
365
- },
366
- include: ['src/**/*.ts', 'src/**/*.d.ts'],
367
- },
368
- null,
369
- 2,
370
- ),
371
- );
372
-
373
- try {
374
- await execFileAsync(
375
- process.platform === 'win32' ? 'tsgo.cmd' : 'tsgo',
376
- ['-p', 'tsconfig.json'],
377
- {
378
- cwd: projectDirectory,
379
- shell: process.platform === 'win32',
380
- },
381
- );
382
- } catch (error) {
383
- if (error && typeof error === 'object') {
384
- const stdout =
385
- 'stdout' in error && typeof error.stdout === 'string'
386
- ? error.stdout
387
- : '';
388
- const stderr =
389
- 'stderr' in error && typeof error.stderr === 'string'
390
- ? error.stderr
391
- : '';
392
- if (stdout || stderr) {
393
- throw new Error([stdout, stderr].filter(Boolean).join('\n'), {
394
- cause: error,
395
- });
396
- }
397
- }
398
- throw error;
399
- }
160
+ await writeTsconfig(projectDirectory, {
161
+ module: 'Preserve',
162
+ moduleResolution: 'Bundler',
163
+ });
164
+ await runTsgo(projectDirectory);
400
165
  }
401
166
 
402
- async function writeComprehensiveRouterFixture(srcDirectory: string) {
167
+ async function generateComprehensiveRouterGen(srcDirectory: string) {
403
168
  const files = new Map([
404
169
  [
405
170
  'routes/(app)/layout.tsx',
@@ -428,22 +193,14 @@ async function writeComprehensiveRouterFixture(srcDirectory: string) {
428
193
  ].join('\n'),
429
194
  ],
430
195
  ]);
431
-
432
196
  for (const [relativePath, contents] of files) {
433
197
  const filePath = path.join(srcDirectory, relativePath);
434
198
  await mkdir(path.dirname(filePath), { recursive: true });
435
199
  await writeFile(filePath, contents);
436
200
  }
437
- }
438
-
439
- async function generateComprehensiveRouterGen(srcDirectory: string) {
440
- await writeComprehensiveRouterFixture(srcDirectory);
441
201
 
442
202
  const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
443
- appContext: {
444
- srcDirectory,
445
- internalSrcAlias: '@/_',
446
- } as any,
203
+ appContext: { srcDirectory, internalSrcAlias: '@/_' } as any,
447
204
  entryName: 'golden',
448
205
  routes: [
449
206
  {
@@ -486,402 +243,25 @@ describe('tanstack router type generation', () => {
486
243
  let tempDir: string | undefined;
487
244
 
488
245
  afterEach(async () => {
489
- resolverDelays.clear();
490
246
  if (tempDir) {
491
247
  await rm(tempDir, { recursive: true, force: true });
492
248
  tempDir = undefined;
493
249
  }
494
250
  });
495
251
 
496
- test('strictly compiles and executes a loader-free conventional route tree', async () => {
497
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
498
- const srcDirectory = path.join(tempDir, 'src');
499
- const files = new Map([
500
- [
501
- 'routes/layout.tsx',
502
- 'export default function Layout() { return null; }',
503
- ],
504
- ['routes/page.tsx', 'export default function Home() { return null; }'],
505
- [
506
- 'routes/reviews/[scanId]/page.tsx',
507
- 'export default function Review() { return null; }',
508
- ],
509
- ]);
510
-
511
- for (const [relativePath, contents] of files) {
512
- const filePath = path.join(srcDirectory, relativePath);
513
- await mkdir(path.dirname(filePath), { recursive: true });
514
- await writeFile(filePath, contents);
515
- }
516
-
517
- const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
518
- appContext: {
519
- srcDirectory,
520
- internalSrcAlias: '@/_',
521
- } as any,
522
- entryName: 'index',
523
- routes: [
524
- {
525
- type: 'nested',
526
- id: 'layout',
527
- isRoot: true,
528
- _component: '@/_/routes/layout',
529
- children: [
530
- {
531
- type: 'nested',
532
- id: 'page',
533
- index: true,
534
- _component: '@/_/routes/page',
535
- },
536
- {
537
- type: 'nested',
538
- id: 'reviews/(scanId)/page',
539
- path: 'reviews/:scanId',
540
- _component: '@/_/routes/reviews/[scanId]/page',
541
- },
542
- ],
543
- },
544
- ] as any,
545
- });
546
-
547
- await compileAndRunGeneratedRouter({
548
- projectDirectory: tempDir,
549
- routerGenTs,
550
- runtimeCheck: [
551
- "import { runtimeState } from '@modern-js/plugin-tanstack/runtime';",
552
- "import Layout from './routes/layout';",
553
- "import Home from './routes/page';",
554
- "import Review from './routes/reviews/[scanId]/page';",
555
- "import { rootRoute, routeTree, router } from './modern-tanstack/index/router.gen';",
556
- '',
557
- 'if (runtimeState.adapterCalls.length !== 0) throw new Error("loader adapter registered without a loader");',
558
- 'if (Object.keys(router.context).length !== 0) throw new Error("router context is not empty");',
559
- 'if (rootRoute.options.component !== Layout) throw new Error("root component was not wired");',
560
- 'const [homeRoute, reviewRoute] = routeTree.children;',
561
- 'if (homeRoute.options.component !== Home) throw new Error("index component was not wired");',
562
- 'if (reviewRoute.options.component !== Review) throw new Error("review component was not wired");',
563
- 'if (reviewRoute.options.path !== "reviews/$scanId") throw new Error("route params were not translated");',
564
- ].join('\n'),
565
- });
566
- });
567
-
568
- test('executes generated loader and action wiring through route static data', async () => {
569
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
570
- const srcDirectory = path.join(tempDir, 'src');
571
- const routeDir = path.join(srcDirectory, 'routes', 'mf');
572
- await mkdir(routeDir, { recursive: true });
573
- await writeFile(
574
- path.join(routeDir, 'page.data.ts'),
575
- [
576
- 'export const loader = () => ({ count: 0 });',
577
- 'export const action = () => Response.json({ count: 1 });',
578
- ].join('\n'),
579
- );
580
-
581
- const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
582
- appContext: {
583
- srcDirectory,
584
- internalSrcAlias: '@/_',
585
- } as any,
586
- entryName: 'index',
587
- routes: [
588
- {
589
- type: 'nested',
590
- id: 'layout',
591
- isRoot: true,
592
- children: [
593
- {
594
- type: 'nested',
595
- id: 'mf/page',
596
- path: 'mf',
597
- data: '@/_/routes/mf/page.data',
598
- action: '@/_/routes/mf/page.data',
599
- },
600
- ],
601
- },
602
- ] as any,
603
- });
604
-
605
- await compileAndRunGeneratedRouter({
606
- projectDirectory: tempDir,
607
- routerGenTs,
608
- runtimeCheck: [
609
- "import { runtimeState } from '@modern-js/plugin-tanstack/runtime';",
610
- "import { action, loader } from './routes/mf/page.data';",
611
- "import { routeTree } from './modern-tanstack/index/router.gen';",
612
- '',
613
- 'async function main() {',
614
- 'const [mfRoute] = routeTree.children;',
615
- 'if (runtimeState.adapterCalls.length !== 1) throw new Error("loader adapter was not registered exactly once");',
616
- 'if (runtimeState.adapterCalls[0]?.modernLoader !== loader) throw new Error("wrong loader was adapted");',
617
- 'const result = await mfRoute.options.loader({});',
618
- 'if (result.count !== 0) throw new Error("adapted loader returned the wrong value");',
619
- 'if (mfRoute.options.staticData.modernRouteLoader !== loader) throw new Error("loader static data was not wired");',
620
- 'if (mfRoute.options.staticData.modernRouteAction !== action) throw new Error("action static data was not wired");',
621
- '}',
622
- 'void main();',
623
- ].join('\n'),
624
- });
625
- });
626
-
627
- test('executes component sharing correctly when module resolution races', async () => {
628
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
629
- const srcDirectory = path.join(tempDir, 'src');
630
-
631
- for (const componentFile of [
632
- 'routes/slow-page.tsx',
633
- 'routes/fast-page.tsx',
634
- 'routes/shared-page.tsx',
635
- ]) {
636
- const componentPath = path.join(srcDirectory, componentFile);
637
- await mkdir(path.dirname(componentPath), { recursive: true });
638
- await writeFile(
639
- componentPath,
640
- 'export default function Page() { return null; }',
641
- );
642
- }
643
-
644
- resolverDelays.set('/routes/slow-page', 25);
645
- const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
646
- appContext: {
647
- srcDirectory,
648
- internalSrcAlias: '@/_',
649
- } as any,
650
- entryName: 'index',
651
- routes: [
652
- {
653
- type: 'nested',
654
- id: 'layout',
655
- isRoot: true,
656
- children: [
657
- {
658
- type: 'nested',
659
- id: 'slow/page',
660
- path: 'slow',
661
- _component: '@/_/routes/slow-page',
662
- },
663
- {
664
- type: 'nested',
665
- id: 'fast/page',
666
- path: 'fast',
667
- _component: '@/_/routes/fast-page',
668
- },
669
- {
670
- type: 'nested',
671
- id: 'shared-a/page',
672
- path: 'shared-a',
673
- _component: '@/_/routes/shared-page',
674
- },
675
- {
676
- type: 'nested',
677
- id: 'shared-b/page',
678
- path: 'shared-b',
679
- _component: '@/_/routes/shared-page',
680
- },
681
- {
682
- type: 'nested',
683
- id: 'data-only/page',
684
- path: 'data-only',
685
- },
686
- ],
687
- },
688
- ] as any,
689
- });
690
-
691
- await compileAndRunGeneratedRouter({
692
- projectDirectory: tempDir,
693
- routerGenTs,
694
- runtimeCheck: [
695
- "import SlowPage from './routes/slow-page';",
696
- "import FastPage from './routes/fast-page';",
697
- "import SharedPage from './routes/shared-page';",
698
- "import { routeTree } from './modern-tanstack/index/router.gen';",
699
- '',
700
- 'const [slowRoute, fastRoute, sharedA, sharedB, dataOnly] = routeTree.children;',
701
- 'if (slowRoute.options.component !== SlowPage) throw new Error("slow component was crossed during resolution");',
702
- 'if (fastRoute.options.component !== FastPage) throw new Error("fast component was crossed during resolution");',
703
- 'if (sharedA.options.component !== SharedPage || sharedB.options.component !== SharedPage) throw new Error("shared component identity was not preserved");',
704
- 'if (Reflect.has(dataOnly.options, "component")) throw new Error("data-only route gained a component");',
705
- ].join('\n'),
706
- });
707
- });
708
-
709
- test('strictly compiles and executes nested loader and search contracts', async () => {
710
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
711
- const srcDirectory = path.join(tempDir, 'src');
712
- const routerGenTs = await generateComprehensiveRouterGen(srcDirectory);
713
-
714
- await compileAndRunGeneratedRouter({
715
- projectDirectory: tempDir,
716
- routerGenTs,
717
- runtimeCheck: [
718
- "import { runtimeState } from '@modern-js/plugin-tanstack/runtime';",
719
- "import { action, loader } from './routes/(app)/users/(userId)/page.data';",
720
- "import { loaderDeps, validateSearch } from './routes/search.contract';",
721
- "import { routeTree } from './modern-tanstack/index/router.gen';",
722
- '',
723
- 'async function main() {',
724
- 'const [appRoute] = routeTree.children;',
725
- 'const [userRoute, docsRoute] = appRoute.children;',
726
- 'if (runtimeState.adapterCalls.length !== 1) throw new Error("nested loader was not adapted");',
727
- 'if (userRoute.options.validateSearch !== validateSearch) throw new Error("search validator was not wired");',
728
- 'if (userRoute.options.loaderDeps !== loaderDeps) throw new Error("loader dependencies were not wired");',
729
- 'if (userRoute.options.staticData.modernRouteLoader !== loader) throw new Error("nested static loader was not wired");',
730
- 'if (userRoute.options.staticData.modernRouteAction !== action) throw new Error("nested static action was not wired");',
731
- 'const user = await userRoute.options.loader({});',
732
- 'if (user.userId !== "42") throw new Error("nested loader result was lost");',
733
- 'if (userRoute.options.path !== "users/$userId") throw new Error("required route param was not translated");',
734
- 'if (docsRoute.options.path !== "docs/$") throw new Error("splat route was not translated");',
735
- 'const search = userRoute.options.validateSearch({});',
736
- 'if (search.tab !== "overview") throw new Error("search contract returned the wrong value");',
737
- '}',
738
- 'void main();',
739
- ].join('\n'),
740
- });
741
- });
742
-
743
252
  test('strictly compiles generated routes against installed router declarations', async () => {
744
253
  tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
745
- const srcDirectory = path.join(tempDir, 'src');
746
- const routerGenTs = await generateComprehensiveRouterGen(srcDirectory);
747
-
748
- await compileGeneratedRouterAgainstInstalledDeclarations({
749
- projectDirectory: tempDir,
750
- routerGenTs,
751
- });
752
- });
753
-
754
- test('passes the generated-artifact lint contract', async () => {
755
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
756
- const srcDirectory = path.join(tempDir, 'src');
757
- const routerGenTs = await generateComprehensiveRouterGen(srcDirectory);
758
- const routerGenPath = path.join(
759
- srcDirectory,
760
- 'modern-tanstack',
761
- 'golden',
762
- 'router.gen.ts',
254
+ const routerGenTs = await generateComprehensiveRouterGen(
255
+ path.join(tempDir, 'src'),
763
256
  );
764
257
 
765
- await mkdir(path.dirname(routerGenPath), { recursive: true });
766
- await writeFile(routerGenPath, routerGenTs);
767
-
768
- const { stderr, stdout } = await execFileAsync(
769
- process.execPath,
770
- [
771
- oxlintCliPath,
772
- routerGenPath,
773
- '--config',
774
- routerGenOxlintConfigPath,
775
- '--no-ignore',
776
- '--report-unused-disable-directives-severity',
777
- 'error',
778
- '--format',
779
- 'unix',
780
- ],
781
- { cwd: tempDir },
782
- );
783
-
784
- expect(`${stdout}${stderr}`).toBe('');
785
- });
786
-
787
- test('executes localized aliases with typed children and correct parents', async () => {
788
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
789
- const srcDirectory = path.join(tempDir, 'src');
790
-
791
- const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
792
- appContext: {
793
- srcDirectory,
794
- internalSrcAlias: '@/_',
795
- } as any,
796
- entryName: 'index',
797
- routes: [
798
- {
799
- type: 'nested',
800
- id: 'layout',
801
- isRoot: true,
802
- children: [
803
- {
804
- type: 'nested',
805
- id: '(lang)/layout',
806
- path: ':lang',
807
- children: [
808
- {
809
- type: 'nested',
810
- id: '(lang)/products/(slug)/page',
811
- path: 'products/:slug',
812
- },
813
- {
814
- type: 'nested',
815
- id: '(lang)/products/(slug)/page__localised_produkty_slug',
816
- path: 'produkty/:slug',
817
- },
818
- {
819
- type: 'nested',
820
- id: '(lang)/optional/(slug$)/page__localised_volitelne_slug',
821
- path: 'volitelne/:slug?',
822
- },
823
- ],
824
- },
825
- ],
826
- },
827
- ] as any,
828
- });
829
-
830
- await compileAndRunGeneratedRouter({
258
+ await compileGeneratedRouterAgainstInstalledDeclarations({
831
259
  projectDirectory: tempDir,
832
260
  routerGenTs,
833
- runtimeCheck: [
834
- "import { routeTree } from './modern-tanstack/index/router.gen';",
835
- '',
836
- 'const [localeRoute] = routeTree.children;',
837
- 'const [productRoute, localizedProductRoute, optionalRoute] = localeRoute.children;',
838
- 'if (localeRoute.options.path !== "$lang") throw new Error("locale param was not translated");',
839
- 'if (productRoute.options.path !== "products/$slug") throw new Error("product param was not translated");',
840
- 'if (localizedProductRoute.options.path !== "produkty/$slug") throw new Error("localized param was not translated");',
841
- 'if (optionalRoute.options.path !== "volitelne/{-$slug}") throw new Error("optional param was not translated");',
842
- 'for (const child of localeRoute.children) {',
843
- ' if (child.options.getParentRoute() !== localeRoute) throw new Error("localized child points at the wrong parent");',
844
- '}',
845
- ].join('\n'),
846
261
  });
847
262
  });
848
- });
849
-
850
- describe('collectCanonicalRoutesForEntry', () => {
851
- let tempDir: string | undefined;
852
-
853
- afterEach(async () => {
854
- if (tempDir) {
855
- await rm(tempDir, { recursive: true, force: true });
856
- tempDir = undefined;
857
- }
858
- });
859
263
 
860
- test('returns null for a route tree with no locale param and no canonical metadata', () => {
861
- const result = collectCanonicalRoutesForEntry([
862
- {
863
- type: 'nested',
864
- id: 'layout',
865
- isRoot: true,
866
- children: [
867
- {
868
- type: 'nested',
869
- id: 'about/page',
870
- path: 'about',
871
- },
872
- {
873
- type: 'nested',
874
- id: 'contact/page',
875
- path: 'contact',
876
- },
877
- ],
878
- },
879
- ] as any);
880
-
881
- expect(result).toBeNull();
882
- });
883
-
884
- test('ignores a leading :lang param when the locale-param heuristic is disabled (no plugin-i18n)', async () => {
264
+ test('maps a locale-prefixed route tree to the canonical paths consumers navigate to', () => {
885
265
  const routes = [
886
266
  {
887
267
  type: 'nested',
@@ -893,257 +273,38 @@ describe('collectCanonicalRoutesForEntry', () => {
893
273
  id: '(lang)/layout',
894
274
  path: ':lang',
895
275
  children: [
276
+ { type: 'nested', id: '(lang)/page', index: true },
277
+ { type: 'nested', id: '(lang)/about/page', path: 'about' },
896
278
  {
897
279
  type: 'nested',
898
- id: '(lang)/about/page',
899
- path: 'about',
280
+ id: '(lang)/optional/(slug$)/page',
281
+ path: 'optional/:slug?',
900
282
  },
283
+ { type: 'nested', id: '(lang)/files/page', path: 'files/*' },
901
284
  ],
902
285
  },
903
286
  ],
904
287
  },
905
288
  ] as any;
906
289
 
907
- // Without plugin-i18n, a hand-rolled `/:lang/` param must NOT trigger the
908
- // i18n surface (the emitted module augmentation would break typechecking).
290
+ // Without plugin-i18n installed, a hand-rolled `/:lang/` param must NOT
291
+ // produce a canonical surface — the emitted module augmentation would
292
+ // break typechecking in an app that never opted into i18n.
909
293
  expect(
910
294
  collectCanonicalRoutesForEntry(routes, { localeParamHeuristic: false }),
911
295
  ).toBeNull();
912
- // With plugin-i18n installed the heuristic strips the locale prefix.
913
- const result = collectCanonicalRoutesForEntry(routes, {
914
- localeParamHeuristic: true,
915
- });
916
- expect(result).not.toBeNull();
917
- expect(Object.keys(result!)).toEqual(['/about']);
918
-
919
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-canonical-'));
920
- await typecheckCanonicalRegisterContract({
921
- canonicalRoutes: result!,
922
- contractLines: [
923
- "const aboutParams: UltramodernCanonicalRoutes['/about'] = {};",
924
- '// @ts-expect-error canonical routes without params reject unexpected properties',
925
- "const invalidAboutParams: UltramodernCanonicalRoutes['/about'] = { slug: 'unexpected' };",
926
- 'void aboutParams;',
927
- 'void invalidAboutParams;',
928
- ],
929
- projectDirectory: tempDir,
930
- });
931
- });
932
-
933
- test('still honors modernCanonicalPath metadata when the heuristic is disabled', async () => {
934
- const result = collectCanonicalRoutesForEntry(
935
- [
936
- {
937
- type: 'nested',
938
- id: 'layout',
939
- isRoot: true,
940
- children: [
941
- {
942
- type: 'nested',
943
- id: '(lang)/products/(slug)/page',
944
- path: 'products/:slug',
945
- modernCanonicalPath: '/products/:slug',
946
- },
947
- ],
948
- },
949
- ] as any,
950
- { localeParamHeuristic: false },
951
- );
952
-
953
- expect(result).not.toBeNull();
954
- expect(Object.keys(result!)).toEqual(['/products/$slug']);
955
-
956
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-canonical-'));
957
- await typecheckCanonicalRegisterContract({
958
- canonicalRoutes: result!,
959
- contractLines: [
960
- "const productParams: UltramodernCanonicalRoutes['/products/$slug'] = { slug: 'tractor' };",
961
- '// @ts-expect-error a required canonical route param cannot be omitted',
962
- "const missingProductParams: UltramodernCanonicalRoutes['/products/$slug'] = {};",
963
- '// @ts-expect-error canonical route params retain their string type',
964
- "const invalidProductParams: UltramodernCanonicalRoutes['/products/$slug'] = { slug: 42 };",
965
- 'void productParams;',
966
- 'void missingProductParams;',
967
- 'void invalidProductParams;',
968
- ],
969
- projectDirectory: tempDir,
970
- });
971
- });
972
-
973
- test('strips leading :lang param and maps index under :lang to "/"', async () => {
974
- const result = collectCanonicalRoutesForEntry([
975
- {
976
- type: 'nested',
977
- id: 'layout',
978
- isRoot: true,
979
- children: [
980
- {
981
- type: 'nested',
982
- id: '(lang)/layout',
983
- path: ':lang',
984
- children: [
985
- {
986
- type: 'nested',
987
- id: '(lang)/page',
988
- index: true,
989
- },
990
- {
991
- type: 'nested',
992
- id: '(lang)/about/page',
993
- path: 'about',
994
- },
995
- ],
996
- },
997
- ],
998
- },
999
- ] as any);
1000
296
 
297
+ const result = collectCanonicalRoutesForEntry(routes);
1001
298
  expect(result).not.toBeNull();
1002
- expect(Object.keys(result!)).toEqual(['/', '/about']);
1003
-
1004
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-canonical-'));
1005
- await typecheckCanonicalRegisterContract({
1006
- canonicalRoutes: result!,
1007
- contractLines: [
1008
- "const rootParams: UltramodernCanonicalRoutes['/'] = {};",
1009
- "const aboutParams: UltramodernCanonicalRoutes['/about'] = {};",
1010
- '// @ts-expect-error index canonical routes reject unexpected params',
1011
- "const invalidRootParams: UltramodernCanonicalRoutes['/'] = { slug: 'unexpected' };",
1012
- 'void rootParams;',
1013
- 'void aboutParams;',
1014
- 'void invalidRootParams;',
1015
- ],
1016
- projectDirectory: tempDir,
1017
- });
1018
- });
1019
-
1020
- test('converts :slug to $slug with required params type', async () => {
1021
- const result = collectCanonicalRoutesForEntry([
1022
- {
1023
- type: 'nested',
1024
- id: 'layout',
1025
- isRoot: true,
1026
- children: [
1027
- {
1028
- type: 'nested',
1029
- id: '(lang)/layout',
1030
- path: ':lang',
1031
- children: [
1032
- {
1033
- type: 'nested',
1034
- id: '(lang)/products/(slug)/page',
1035
- path: 'products/:slug',
1036
- },
1037
- ],
1038
- },
1039
- ],
1040
- },
1041
- ] as any);
1042
-
1043
- expect(result).not.toBeNull();
1044
- expect(Object.keys(result!)).toEqual(['/products/$slug']);
1045
-
1046
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-canonical-'));
1047
- await typecheckCanonicalRegisterContract({
1048
- canonicalRoutes: result!,
1049
- contractLines: [
1050
- "const productParams: UltramodernCanonicalRoutes['/products/$slug'] = { slug: 'tractor' };",
1051
- '// @ts-expect-error a required slug cannot be omitted',
1052
- "const missingProductParams: UltramodernCanonicalRoutes['/products/$slug'] = {};",
1053
- 'void productParams;',
1054
- 'void missingProductParams;',
1055
- ],
1056
- projectDirectory: tempDir,
1057
- });
1058
- });
1059
-
1060
- test('converts :slug? to optional {-$slug} with optional params type', async () => {
1061
- const result = collectCanonicalRoutesForEntry([
1062
- {
1063
- type: 'nested',
1064
- id: 'layout',
1065
- isRoot: true,
1066
- children: [
1067
- {
1068
- type: 'nested',
1069
- id: '(lang)/layout',
1070
- path: ':lang',
1071
- children: [
1072
- {
1073
- type: 'nested',
1074
- id: '(lang)/optional/(slug$)/page',
1075
- path: 'optional/:slug?',
1076
- },
1077
- ],
1078
- },
1079
- ],
1080
- },
1081
- ] as any);
1082
-
1083
- expect(result).not.toBeNull();
1084
- expect(Object.keys(result!)).toEqual(['/optional/{-$slug}']);
1085
-
1086
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-canonical-'));
1087
- await typecheckCanonicalRegisterContract({
1088
- canonicalRoutes: result!,
1089
- contractLines: [
1090
- "const omittedOptionalParams: UltramodernCanonicalRoutes['/optional/{-$slug}'] = {};",
1091
- "const presentOptionalParams: UltramodernCanonicalRoutes['/optional/{-$slug}'] = { slug: 'tractor' };",
1092
- '// @ts-expect-error optional canonical params still retain their string type',
1093
- "const invalidOptionalParams: UltramodernCanonicalRoutes['/optional/{-$slug}'] = { slug: 42 };",
1094
- 'void omittedOptionalParams;',
1095
- 'void presentOptionalParams;',
1096
- 'void invalidOptionalParams;',
1097
- ],
1098
- projectDirectory: tempDir,
1099
- });
1100
- });
1101
-
1102
- test('converts * splat to $ with optional _splat param', async () => {
1103
- const result = collectCanonicalRoutesForEntry([
1104
- {
1105
- type: 'nested',
1106
- id: 'layout',
1107
- isRoot: true,
1108
- children: [
1109
- {
1110
- type: 'nested',
1111
- id: '(lang)/layout',
1112
- path: ':lang',
1113
- children: [
1114
- {
1115
- type: 'nested',
1116
- id: '(lang)/files/page',
1117
- path: 'files/*',
1118
- },
1119
- ],
1120
- },
1121
- ],
1122
- },
1123
- ] as any);
1124
-
1125
- expect(result).not.toBeNull();
1126
- expect(Object.keys(result!)).toEqual(['/files/$']);
1127
-
1128
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-canonical-'));
1129
- await typecheckCanonicalRegisterContract({
1130
- canonicalRoutes: result!,
1131
- contractLines: [
1132
- "const emptySplatParams: UltramodernCanonicalRoutes['/files/$'] = {};",
1133
- "const splatParams: UltramodernCanonicalRoutes['/files/$'] = { _splat: 'guides/intro' };",
1134
- '// @ts-expect-error splat params retain their string type',
1135
- "const invalidSplatParams: UltramodernCanonicalRoutes['/files/$'] = { _splat: 42 };",
1136
- 'void emptySplatParams;',
1137
- 'void splatParams;',
1138
- 'void invalidSplatParams;',
1139
- ],
1140
- projectDirectory: tempDir,
1141
- });
299
+ expect(Object.keys(result!)).toEqual([
300
+ '/',
301
+ '/about',
302
+ '/files/$',
303
+ '/optional/{-$slug}',
304
+ ]);
1142
305
  });
1143
306
 
1144
- test('collapses localized variants with shared modernCanonicalPath to one canonical key', async () => {
1145
- // This reuses the same fixture shape as the 'preserves typed child trees'
1146
- // test but adds modernCanonicalPath fields as plugin-i18n now emits.
307
+ test('collapses localized variants to one canonical key with typed params', async () => {
1147
308
  const result = collectCanonicalRoutesForEntry([
1148
309
  {
1149
310
  type: 'nested',
@@ -1180,69 +341,44 @@ describe('collectCanonicalRoutesForEntry', () => {
1180
341
  ] as any);
1181
342
 
1182
343
  expect(result).not.toBeNull();
1183
- // Two physical variants share the same canonical path — only one entry.
1184
- const keys = Object.keys(result!);
1185
- // /products/$slug and /optional/{-$slug} — exactly 2 keys with params
1186
- expect(keys.filter(k => k.startsWith('/products'))).toHaveLength(1);
1187
- expect(keys).toHaveLength(2);
1188
- expect(keys).toEqual(['/optional/{-$slug}', '/products/$slug']);
1189
- // The Czech localized path must not appear as a separate key.
344
+ expect(Object.keys(result!)).toEqual([
345
+ '/optional/{-$slug}',
346
+ '/products/$slug',
347
+ ]);
1190
348
  expect('/produkty/$slug' in result!).toBe(false);
1191
349
 
1192
350
  tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-canonical-'));
1193
351
  await typecheckCanonicalRegisterContract({
1194
- canonicalRoutes: result!,
352
+ canonicalRoutes: {
353
+ '/': 'Record<string, never>',
354
+ '/files/$': '{ _splat?: string }',
355
+ ...result!,
356
+ },
1195
357
  contractLines: [
358
+ "const rootParams: UltramodernCanonicalRoutes['/'] = {};",
359
+ '// @ts-expect-error index canonical routes reject unexpected params',
360
+ "const invalidRootParams: UltramodernCanonicalRoutes['/'] = { slug: 'unexpected' };",
1196
361
  "const productParams: UltramodernCanonicalRoutes['/products/$slug'] = { slug: 'tractor' };",
1197
362
  "const optionalParams: UltramodernCanonicalRoutes['/optional/{-$slug}'] = {};",
363
+ "const presentOptionalParams: UltramodernCanonicalRoutes['/optional/{-$slug}'] = { slug: 'tractor' };",
364
+ '// @ts-expect-error optional canonical params still retain their string type',
365
+ "const invalidOptionalParams: UltramodernCanonicalRoutes['/optional/{-$slug}'] = { slug: 42 };",
366
+ "const splatParams: UltramodernCanonicalRoutes['/files/$'] = { _splat: 'guides/intro' };",
1198
367
  '// @ts-expect-error localized physical variants do not become canonical keys',
1199
368
  "declare const localizedParams: UltramodernCanonicalRoutes['/produkty/$slug'];",
1200
369
  '// @ts-expect-error the deduplicated product route still requires its param',
1201
370
  "const missingProductParams: UltramodernCanonicalRoutes['/products/$slug'] = {};",
371
+ 'void rootParams;',
1202
372
  'void productParams;',
1203
373
  'void optionalParams;',
374
+ 'void presentOptionalParams;',
375
+ 'void splatParams;',
376
+ 'void invalidRootParams;',
377
+ 'void invalidOptionalParams;',
1204
378
  'void localizedParams;',
1205
379
  'void missingProductParams;',
1206
380
  ],
1207
381
  projectDirectory: tempDir,
1208
382
  });
1209
383
  });
1210
-
1211
- test('output is sorted alphabetically by canonical key', () => {
1212
- const result = collectCanonicalRoutesForEntry([
1213
- {
1214
- type: 'nested',
1215
- id: 'layout',
1216
- isRoot: true,
1217
- children: [
1218
- {
1219
- type: 'nested',
1220
- id: '(lang)/layout',
1221
- path: ':lang',
1222
- children: [
1223
- {
1224
- type: 'nested',
1225
- id: '(lang)/products/(slug)/page',
1226
- path: 'products/:slug',
1227
- },
1228
- {
1229
- type: 'nested',
1230
- id: '(lang)/about/page',
1231
- path: 'about',
1232
- },
1233
- {
1234
- type: 'nested',
1235
- id: '(lang)/page',
1236
- index: true,
1237
- },
1238
- ],
1239
- },
1240
- ],
1241
- },
1242
- ] as any);
1243
-
1244
- expect(result).not.toBeNull();
1245
- const keys = Object.keys(result!);
1246
- expect(keys).toEqual([...keys].sort((a, b) => a.localeCompare(b)));
1247
- });
1248
384
  });