@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.5 → 3.9.0-ultramodern.7

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.
@@ -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,
@@ -45,195 +20,51 @@ const installedReactRouterPath = path.resolve(
45
20
  '../../node_modules/@tanstack/react-router',
46
21
  );
47
22
 
48
- async function writeRuntimeTestPackage(projectDirectory: string) {
49
- const runtimePackageDirectory = path.join(
50
- projectDirectory,
51
- 'node_modules',
52
- '@modern-js',
53
- 'plugin-tanstack',
54
- );
55
- await mkdir(runtimePackageDirectory, { recursive: true });
56
- await writeFile(
57
- path.join(runtimePackageDirectory, 'package.json'),
58
- JSON.stringify({
59
- name: '@modern-js/plugin-tanstack',
60
- type: 'commonjs',
61
- exports: {
62
- './runtime': {
63
- types: './runtime.d.ts',
64
- default: './runtime.js',
65
- },
66
- },
67
- }),
68
- );
69
- await writeFile(
70
- path.join(runtimePackageDirectory, 'runtime.d.ts'),
71
- [
72
- 'export type ModernRouterContext = { request?: Request; requestContext?: unknown };',
73
- 'export type ModernRouteStaticData = { modernRouteId?: string; modernRouteAction?: unknown; modernRouteLoader?: unknown };',
74
- 'export type RouteOptions = {',
75
- ' component?: unknown;',
76
- ' getParentRoute?: (...args: never[]) => unknown;',
77
- ' id?: string;',
78
- ' loader?: (...args: never[]) => unknown;',
79
- ' loaderDeps?: (...args: never[]) => unknown;',
80
- ' path?: string;',
81
- ' staticData?: ModernRouteStaticData;',
82
- ' validateSearch?: (...args: never[]) => unknown;',
83
- '};',
84
- 'export type Route<TOptions extends RouteOptions = RouteOptions> = {',
85
- ' options: TOptions;',
86
- ' addChildren<const TChildren extends readonly unknown[]>(children: TChildren): Route<TOptions> & { children: TChildren };',
87
- '};',
88
- 'export const runtimeState: { adapterCalls: Array<{ hasSplat: boolean; modernLoader: unknown }> };',
89
- 'export function createMemoryHistory<TOptions>(options: TOptions): TOptions;',
90
- 'export const modernTanstackRouterFastDefaults: { readonly defaultStructuralSharing: true };',
91
- 'export function createRootRouteWithContext<TContext extends ModernRouterContext>(): <const TOptions extends RouteOptions>(options: TOptions) => Route<TOptions>;',
92
- 'export function createRoute<const TOptions extends RouteOptions>(options: TOptions): Route<TOptions>;',
93
- 'export function createRouter<const TOptions extends { context: ModernRouterContext; history: unknown; routeTree: unknown }>(options: TOptions): TOptions;',
94
- 'export function createRouteStaticData<const TData extends ModernRouteStaticData>(data: TData): TData;',
95
- 'export function modernLoaderToTanstack<TLoader extends (...args: never[]) => unknown>(options: { hasSplat: boolean }, modernLoader: TLoader): (context: unknown) => Promise<Awaited<ReturnType<TLoader>>>;',
96
- ].join('\n'),
97
- );
98
- await writeFile(
99
- path.join(runtimePackageDirectory, 'runtime.js'),
100
- [
101
- 'const runtimeState = { adapterCalls: [] };',
102
- 'const createRouteRecord = options => ({',
103
- ' options,',
104
- ' addChildren(children) {',
105
- ' this.children = children;',
106
- ' return this;',
107
- ' },',
108
- '});',
109
- 'const createMemoryHistory = options => options;',
110
- 'const modernTanstackRouterFastDefaults = { defaultStructuralSharing: true };',
111
- 'const createRootRouteWithContext = () => options => createRouteRecord(options);',
112
- 'const createRoute = options => createRouteRecord(options);',
113
- 'const createRouter = options => options;',
114
- 'const createRouteStaticData = data => data;',
115
- 'const modernLoaderToTanstack = (options, modernLoader) => {',
116
- ' runtimeState.adapterCalls.push({ ...options, modernLoader });',
117
- ' return async context => modernLoader(context);',
118
- '};',
119
- 'module.exports = { createMemoryHistory, createRootRouteWithContext, createRoute, createRouter, createRouteStaticData, modernLoaderToTanstack, modernTanstackRouterFastDefaults, runtimeState };',
120
- ].join('\n'),
121
- );
122
- }
123
-
124
- async function writeInstalledRouterRuntimeTestPackage(
23
+ async function writeTsconfig(
125
24
  projectDirectory: string,
25
+ compilerOptions: Record<string, unknown>,
126
26
  ) {
127
- const runtimePackageDirectory = path.join(
128
- projectDirectory,
129
- 'node_modules',
130
- '@modern-js',
131
- 'plugin-tanstack',
132
- );
133
- await mkdir(runtimePackageDirectory, { recursive: true });
134
- await writeFile(
135
- path.join(runtimePackageDirectory, 'package.json'),
136
- JSON.stringify({
137
- name: '@modern-js/plugin-tanstack',
138
- type: 'commonjs',
139
- exports: {
140
- './runtime': {
141
- types: './runtime.d.ts',
142
- default: './runtime.js',
143
- },
144
- },
145
- }),
146
- );
147
- await writeFile(
148
- path.join(runtimePackageDirectory, 'runtime.d.ts'),
149
- [
150
- "export { createMemoryHistory, createRootRouteWithContext, createRoute, createRouter } from '@tanstack/react-router';",
151
- 'export type ModernRouterContext = { request?: Request; requestContext?: unknown };',
152
- 'export const modernTanstackRouterFastDefaults: { readonly defaultStructuralSharing: true };',
153
- 'export function createRouteStaticData<TData extends { modernRouteId?: string; modernRouteAction?: unknown; modernRouteLoader?: unknown }>(data: TData): TData;',
154
- 'export function modernLoaderToTanstack<TLoader extends (...args: any[]) => any>(options: { hasSplat: boolean }, modernLoader: TLoader): (context: unknown) => Promise<Awaited<ReturnType<TLoader>>>;',
155
- ].join('\n'),
156
- );
157
- }
158
-
159
- async function compileAndRunGeneratedRouter(options: {
160
- projectDirectory: string;
161
- routerGenTs: string;
162
- runtimeCheck: string;
163
- }) {
164
- const { projectDirectory, routerGenTs, runtimeCheck } = options;
165
- const generatedDirectory = path.join(
166
- projectDirectory,
167
- 'src',
168
- 'modern-tanstack',
169
- 'index',
170
- );
171
- await mkdir(generatedDirectory, { recursive: true });
172
- await writeFile(path.join(generatedDirectory, 'router.gen.ts'), routerGenTs);
173
- await writeFile(
174
- path.join(projectDirectory, 'src', 'runtime-check.ts'),
175
- runtimeCheck,
176
- );
177
- await writeRuntimeTestPackage(projectDirectory);
178
- await writeFile(
179
- path.join(projectDirectory, 'package.json'),
180
- JSON.stringify({ private: true, type: 'commonjs' }),
181
- );
182
27
  await writeFile(
183
28
  path.join(projectDirectory, 'tsconfig.json'),
184
29
  JSON.stringify(
185
30
  {
186
31
  extends: strictestTsconfigPath,
187
32
  compilerOptions: {
188
- lib: ['ESNext', 'DOM'],
189
33
  jsx: 'react-jsx',
190
- module: 'Node16',
191
- moduleResolution: 'Node16',
192
- outDir: 'dist',
193
- rootDir: 'src',
194
- target: 'ES2022',
34
+ lib: ['ESNext', 'DOM'],
35
+ noEmit: true,
36
+ target: 'ESNext',
195
37
  types: [],
196
- verbatimModuleSyntax: false,
38
+ ...compilerOptions,
197
39
  },
198
- include: ['src/**/*.ts', 'src/**/*.tsx'],
40
+ include: ['src/**/*.ts', 'src/**/*.tsx', 'src/**/*.d.ts'],
199
41
  },
200
42
  null,
201
43
  2,
202
44
  ),
203
45
  );
46
+ }
204
47
 
48
+ /** Runs the same compiler a consumer build runs; surfaces its diagnostics. */
49
+ async function runTsgo(projectDirectory: string) {
205
50
  try {
206
51
  await execFileAsync(
207
52
  process.platform === 'win32' ? 'tsgo.cmd' : 'tsgo',
208
53
  ['-p', 'tsconfig.json'],
209
- {
210
- cwd: projectDirectory,
211
- shell: process.platform === 'win32',
212
- },
54
+ { cwd: projectDirectory, shell: process.platform === 'win32' },
213
55
  );
214
56
  } catch (error) {
215
- if (error && typeof error === 'object') {
216
- const stdout =
217
- 'stdout' in error && typeof error.stdout === 'string'
218
- ? error.stdout
219
- : '';
220
- const stderr =
221
- 'stderr' in error && typeof error.stderr === 'string'
222
- ? error.stderr
223
- : '';
224
- if (stdout || stderr) {
225
- throw new Error([stdout, stderr].filter(Boolean).join('\n'), {
226
- cause: error,
227
- });
228
- }
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
+ });
229
65
  }
230
66
  throw error;
231
67
  }
232
- await execFileAsync(
233
- process.execPath,
234
- [path.join(projectDirectory, 'dist', 'runtime-check.js')],
235
- { cwd: projectDirectory },
236
- );
237
68
  }
238
69
 
239
70
  async function compileGeneratedRouterAgainstInstalledDeclarations(options: {
@@ -249,63 +80,48 @@ async function compileGeneratedRouterAgainstInstalledDeclarations(options: {
249
80
  );
250
81
  await mkdir(generatedDirectory, { recursive: true });
251
82
  await writeFile(path.join(generatedDirectory, 'router.gen.ts'), routerGenTs);
252
- await writeInstalledRouterRuntimeTestPackage(projectDirectory);
253
- await writeFile(
254
- path.join(projectDirectory, 'package.json'),
255
- 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',
256
92
  );
93
+ await mkdir(runtimePackageDirectory, { recursive: true });
257
94
  await writeFile(
258
- path.join(projectDirectory, 'tsconfig.json'),
259
- JSON.stringify(
260
- {
261
- extends: strictestTsconfigPath,
262
- compilerOptions: {
263
- jsx: 'react-jsx',
264
- lib: ['ESNext', 'DOM'],
265
- module: 'Node16',
266
- moduleResolution: 'Node16',
267
- noEmit: true,
268
- paths: {
269
- '@tanstack/react-router': [installedReactRouterPath],
270
- },
271
- target: 'ES2022',
272
- types: [],
273
- verbatimModuleSyntax: false,
274
- },
275
- 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' },
276
101
  },
277
- null,
278
- 2,
279
- ),
102
+ }),
280
103
  );
281
-
282
- try {
283
- await execFileAsync(
284
- process.platform === 'win32' ? 'tsgo.cmd' : 'tsgo',
285
- ['-p', 'tsconfig.json'],
286
- {
287
- cwd: projectDirectory,
288
- shell: process.platform === 'win32',
289
- },
290
- );
291
- } catch (error) {
292
- if (error && typeof error === 'object') {
293
- const stdout =
294
- 'stdout' in error && typeof error.stdout === 'string'
295
- ? error.stdout
296
- : '';
297
- const stderr =
298
- 'stderr' in error && typeof error.stderr === 'string'
299
- ? error.stderr
300
- : '';
301
- if (stdout || stderr) {
302
- throw new Error([stdout, stderr].filter(Boolean).join('\n'), {
303
- cause: error,
304
- });
305
- }
306
- }
307
- throw error;
308
- }
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);
309
125
  }
310
126
 
311
127
  async function typecheckCanonicalRegisterContract(options: {
@@ -315,11 +131,11 @@ async function typecheckCanonicalRegisterContract(options: {
315
131
  }) {
316
132
  const { canonicalRoutes, contractLines, projectDirectory } = options;
317
133
  const srcDirectory = path.join(projectDirectory, 'src');
318
- const generatedDirectory = path.join(srcDirectory, 'modern-tanstack');
319
- const routerDirectory = path.join(generatedDirectory, 'index');
320
- await mkdir(routerDirectory, { recursive: true });
134
+ await mkdir(path.join(srcDirectory, 'modern-tanstack', 'index'), {
135
+ recursive: true,
136
+ });
321
137
  await writeFile(
322
- path.join(routerDirectory, 'router.gen.ts'),
138
+ path.join(srcDirectory, 'modern-tanstack', 'index', 'router.gen.ts'),
323
139
  'export const router = { context: {} };',
324
140
  );
325
141
  await writeTanstackRegisterFile({
@@ -341,56 +157,14 @@ async function typecheckCanonicalRegisterContract(options: {
341
157
  ...contractLines,
342
158
  ].join('\n'),
343
159
  );
344
- await writeFile(
345
- path.join(projectDirectory, 'tsconfig.json'),
346
- JSON.stringify(
347
- {
348
- extends: strictestTsconfigPath,
349
- compilerOptions: {
350
- lib: ['ESNext', 'DOM'],
351
- module: 'Preserve',
352
- moduleResolution: 'Bundler',
353
- noEmit: true,
354
- target: 'ESNext',
355
- types: [],
356
- },
357
- include: ['src/**/*.ts', 'src/**/*.d.ts'],
358
- },
359
- null,
360
- 2,
361
- ),
362
- );
363
-
364
- try {
365
- await execFileAsync(
366
- process.platform === 'win32' ? 'tsgo.cmd' : 'tsgo',
367
- ['-p', 'tsconfig.json'],
368
- {
369
- cwd: projectDirectory,
370
- shell: process.platform === 'win32',
371
- },
372
- );
373
- } catch (error) {
374
- if (error && typeof error === 'object') {
375
- const stdout =
376
- 'stdout' in error && typeof error.stdout === 'string'
377
- ? error.stdout
378
- : '';
379
- const stderr =
380
- 'stderr' in error && typeof error.stderr === 'string'
381
- ? error.stderr
382
- : '';
383
- if (stdout || stderr) {
384
- throw new Error([stdout, stderr].filter(Boolean).join('\n'), {
385
- cause: error,
386
- });
387
- }
388
- }
389
- throw error;
390
- }
160
+ await writeTsconfig(projectDirectory, {
161
+ module: 'Preserve',
162
+ moduleResolution: 'Bundler',
163
+ });
164
+ await runTsgo(projectDirectory);
391
165
  }
392
166
 
393
- async function writeComprehensiveRouterFixture(srcDirectory: string) {
167
+ async function generateComprehensiveRouterGen(srcDirectory: string) {
394
168
  const files = new Map([
395
169
  [
396
170
  'routes/(app)/layout.tsx',
@@ -419,22 +193,14 @@ async function writeComprehensiveRouterFixture(srcDirectory: string) {
419
193
  ].join('\n'),
420
194
  ],
421
195
  ]);
422
-
423
196
  for (const [relativePath, contents] of files) {
424
197
  const filePath = path.join(srcDirectory, relativePath);
425
198
  await mkdir(path.dirname(filePath), { recursive: true });
426
199
  await writeFile(filePath, contents);
427
200
  }
428
- }
429
-
430
- async function generateComprehensiveRouterGen(srcDirectory: string) {
431
- await writeComprehensiveRouterFixture(srcDirectory);
432
201
 
433
202
  const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
434
- appContext: {
435
- srcDirectory,
436
- internalSrcAlias: '@/_',
437
- } as any,
203
+ appContext: { srcDirectory, internalSrcAlias: '@/_' } as any,
438
204
  entryName: 'golden',
439
205
  routes: [
440
206
  {
@@ -477,176 +243,25 @@ describe('tanstack router type generation', () => {
477
243
  let tempDir: string | undefined;
478
244
 
479
245
  afterEach(async () => {
480
- resolverDelays.clear();
481
246
  if (tempDir) {
482
247
  await rm(tempDir, { recursive: true, force: true });
483
248
  tempDir = undefined;
484
249
  }
485
250
  });
486
251
 
487
- test('executes component sharing correctly when module resolution races', async () => {
488
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
489
- const srcDirectory = path.join(tempDir, 'src');
490
-
491
- for (const componentFile of [
492
- 'routes/slow-page.tsx',
493
- 'routes/fast-page.tsx',
494
- 'routes/shared-page.tsx',
495
- ]) {
496
- const componentPath = path.join(srcDirectory, componentFile);
497
- await mkdir(path.dirname(componentPath), { recursive: true });
498
- await writeFile(
499
- componentPath,
500
- 'export default function Page() { return null; }',
501
- );
502
- }
503
-
504
- resolverDelays.set('/routes/slow-page', 25);
505
- const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
506
- appContext: {
507
- srcDirectory,
508
- internalSrcAlias: '@/_',
509
- } as any,
510
- entryName: 'index',
511
- routes: [
512
- {
513
- type: 'nested',
514
- id: 'layout',
515
- isRoot: true,
516
- children: [
517
- {
518
- type: 'nested',
519
- id: 'slow/page',
520
- path: 'slow',
521
- _component: '@/_/routes/slow-page',
522
- },
523
- {
524
- type: 'nested',
525
- id: 'fast/page',
526
- path: 'fast',
527
- _component: '@/_/routes/fast-page',
528
- },
529
- {
530
- type: 'nested',
531
- id: 'shared-a/page',
532
- path: 'shared-a',
533
- _component: '@/_/routes/shared-page',
534
- },
535
- {
536
- type: 'nested',
537
- id: 'shared-b/page',
538
- path: 'shared-b',
539
- _component: '@/_/routes/shared-page',
540
- },
541
- {
542
- type: 'nested',
543
- id: 'data-only/page',
544
- path: 'data-only',
545
- },
546
- ],
547
- },
548
- ] as any,
549
- });
550
-
551
- await compileAndRunGeneratedRouter({
552
- projectDirectory: tempDir,
553
- routerGenTs,
554
- runtimeCheck: [
555
- "import SlowPage from './routes/slow-page';",
556
- "import FastPage from './routes/fast-page';",
557
- "import SharedPage from './routes/shared-page';",
558
- "import { routeTree } from './modern-tanstack/index/router.gen';",
559
- '',
560
- 'const [slowRoute, fastRoute, sharedA, sharedB, dataOnly] = routeTree.children;',
561
- 'if (slowRoute.options.component !== SlowPage) throw new Error("slow component was crossed during resolution");',
562
- 'if (fastRoute.options.component !== FastPage) throw new Error("fast component was crossed during resolution");',
563
- 'if (sharedA.options.component !== SharedPage || sharedB.options.component !== SharedPage) throw new Error("shared component identity was not preserved");',
564
- 'if (Reflect.has(dataOnly.options, "component")) throw new Error("data-only route gained a component");',
565
- ].join('\n'),
566
- });
567
- });
568
-
569
- test('strictly compiles and executes nested loader and search contracts', async () => {
570
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
571
- const srcDirectory = path.join(tempDir, 'src');
572
- const routerGenTs = await generateComprehensiveRouterGen(srcDirectory);
573
-
574
- await compileAndRunGeneratedRouter({
575
- projectDirectory: tempDir,
576
- routerGenTs,
577
- runtimeCheck: [
578
- "import { runtimeState } from '@modern-js/plugin-tanstack/runtime';",
579
- "import { action, loader } from './routes/(app)/users/(userId)/page.data';",
580
- "import { loaderDeps, validateSearch } from './routes/search.contract';",
581
- "import { routeTree } from './modern-tanstack/index/router.gen';",
582
- '',
583
- 'async function main() {',
584
- 'const [appRoute] = routeTree.children;',
585
- 'const [userRoute, docsRoute] = appRoute.children;',
586
- 'if (runtimeState.adapterCalls.length !== 1) throw new Error("nested loader was not adapted");',
587
- 'if (userRoute.options.validateSearch !== validateSearch) throw new Error("search validator was not wired");',
588
- 'if (userRoute.options.loaderDeps !== loaderDeps) throw new Error("loader dependencies were not wired");',
589
- 'if (userRoute.options.staticData.modernRouteLoader !== loader) throw new Error("nested static loader was not wired");',
590
- 'if (userRoute.options.staticData.modernRouteAction !== action) throw new Error("nested static action was not wired");',
591
- 'const user = await userRoute.options.loader({});',
592
- 'if (user.userId !== "42") throw new Error("nested loader result was lost");',
593
- 'if (userRoute.options.path !== "users/$userId") throw new Error("required route param was not translated");',
594
- 'if (docsRoute.options.path !== "docs/$") throw new Error("splat route was not translated");',
595
- 'const search = userRoute.options.validateSearch({});',
596
- 'if (search.tab !== "overview") throw new Error("search contract returned the wrong value");',
597
- '}',
598
- 'void main();',
599
- ].join('\n'),
600
- });
601
- });
602
-
603
252
  test('strictly compiles generated routes against installed router declarations', async () => {
604
253
  tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
605
- const srcDirectory = path.join(tempDir, 'src');
606
- const routerGenTs = await generateComprehensiveRouterGen(srcDirectory);
254
+ const routerGenTs = await generateComprehensiveRouterGen(
255
+ path.join(tempDir, 'src'),
256
+ );
607
257
 
608
258
  await compileGeneratedRouterAgainstInstalledDeclarations({
609
259
  projectDirectory: tempDir,
610
260
  routerGenTs,
611
261
  });
612
262
  });
613
- });
614
-
615
- describe('collectCanonicalRoutesForEntry', () => {
616
- let tempDir: string | undefined;
617
263
 
618
- afterEach(async () => {
619
- if (tempDir) {
620
- await rm(tempDir, { recursive: true, force: true });
621
- tempDir = undefined;
622
- }
623
- });
624
-
625
- test('returns null for a route tree with no locale param and no canonical metadata', () => {
626
- const result = collectCanonicalRoutesForEntry([
627
- {
628
- type: 'nested',
629
- id: 'layout',
630
- isRoot: true,
631
- children: [
632
- {
633
- type: 'nested',
634
- id: 'about/page',
635
- path: 'about',
636
- },
637
- {
638
- type: 'nested',
639
- id: 'contact/page',
640
- path: 'contact',
641
- },
642
- ],
643
- },
644
- ] as any);
645
-
646
- expect(result).toBeNull();
647
- });
648
-
649
- 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', () => {
650
265
  const routes = [
651
266
  {
652
267
  type: 'nested',
@@ -658,172 +273,38 @@ describe('collectCanonicalRoutesForEntry', () => {
658
273
  id: '(lang)/layout',
659
274
  path: ':lang',
660
275
  children: [
276
+ { type: 'nested', id: '(lang)/page', index: true },
277
+ { type: 'nested', id: '(lang)/about/page', path: 'about' },
661
278
  {
662
279
  type: 'nested',
663
- id: '(lang)/about/page',
664
- path: 'about',
280
+ id: '(lang)/optional/(slug$)/page',
281
+ path: 'optional/:slug?',
665
282
  },
283
+ { type: 'nested', id: '(lang)/files/page', path: 'files/*' },
666
284
  ],
667
285
  },
668
286
  ],
669
287
  },
670
288
  ] as any;
671
289
 
672
- // Without plugin-i18n, a hand-rolled `/:lang/` param must NOT trigger the
673
- // 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.
674
293
  expect(
675
294
  collectCanonicalRoutesForEntry(routes, { localeParamHeuristic: false }),
676
295
  ).toBeNull();
677
- // With plugin-i18n installed the heuristic strips the locale prefix.
678
- const result = collectCanonicalRoutesForEntry(routes, {
679
- localeParamHeuristic: true,
680
- });
681
- expect(result).not.toBeNull();
682
- expect(Object.keys(result!)).toEqual(['/about']);
683
-
684
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-canonical-'));
685
- await typecheckCanonicalRegisterContract({
686
- canonicalRoutes: result!,
687
- contractLines: [
688
- "const aboutParams: UltramodernCanonicalRoutes['/about'] = {};",
689
- '// @ts-expect-error canonical routes without params reject unexpected properties',
690
- "const invalidAboutParams: UltramodernCanonicalRoutes['/about'] = { slug: 'unexpected' };",
691
- 'void aboutParams;',
692
- 'void invalidAboutParams;',
693
- ],
694
- projectDirectory: tempDir,
695
- });
696
- });
697
-
698
- test('still honors modernCanonicalPath metadata when the heuristic is disabled', async () => {
699
- const result = collectCanonicalRoutesForEntry(
700
- [
701
- {
702
- type: 'nested',
703
- id: 'layout',
704
- isRoot: true,
705
- children: [
706
- {
707
- type: 'nested',
708
- id: '(lang)/products/(slug)/page',
709
- path: 'products/:slug',
710
- modernCanonicalPath: '/products/:slug',
711
- },
712
- ],
713
- },
714
- ] as any,
715
- { localeParamHeuristic: false },
716
- );
717
-
718
- expect(result).not.toBeNull();
719
- expect(Object.keys(result!)).toEqual(['/products/$slug']);
720
-
721
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-canonical-'));
722
- await typecheckCanonicalRegisterContract({
723
- canonicalRoutes: result!,
724
- contractLines: [
725
- "const productParams: UltramodernCanonicalRoutes['/products/$slug'] = { slug: 'tractor' };",
726
- '// @ts-expect-error a required canonical route param cannot be omitted',
727
- "const missingProductParams: UltramodernCanonicalRoutes['/products/$slug'] = {};",
728
- '// @ts-expect-error canonical route params retain their string type',
729
- "const invalidProductParams: UltramodernCanonicalRoutes['/products/$slug'] = { slug: 42 };",
730
- 'void productParams;',
731
- 'void missingProductParams;',
732
- 'void invalidProductParams;',
733
- ],
734
- projectDirectory: tempDir,
735
- });
736
- });
737
-
738
- test('strips leading :lang param and maps index under :lang to "/"', async () => {
739
- const result = collectCanonicalRoutesForEntry([
740
- {
741
- type: 'nested',
742
- id: 'layout',
743
- isRoot: true,
744
- children: [
745
- {
746
- type: 'nested',
747
- id: '(lang)/layout',
748
- path: ':lang',
749
- children: [
750
- {
751
- type: 'nested',
752
- id: '(lang)/page',
753
- index: true,
754
- },
755
- {
756
- type: 'nested',
757
- id: '(lang)/about/page',
758
- path: 'about',
759
- },
760
- ],
761
- },
762
- ],
763
- },
764
- ] as any);
765
-
766
- expect(result).not.toBeNull();
767
- expect(Object.keys(result!)).toEqual(['/', '/about']);
768
- });
769
-
770
- test('converts :slug? to optional {-$slug} with optional params type', async () => {
771
- const result = collectCanonicalRoutesForEntry([
772
- {
773
- type: 'nested',
774
- id: 'layout',
775
- isRoot: true,
776
- children: [
777
- {
778
- type: 'nested',
779
- id: '(lang)/layout',
780
- path: ':lang',
781
- children: [
782
- {
783
- type: 'nested',
784
- id: '(lang)/optional/(slug$)/page',
785
- path: 'optional/:slug?',
786
- },
787
- ],
788
- },
789
- ],
790
- },
791
- ] as any);
792
-
793
- expect(result).not.toBeNull();
794
- expect(Object.keys(result!)).toEqual(['/optional/{-$slug}']);
795
- });
796
-
797
- test('converts * splat to $ with optional _splat param', async () => {
798
- const result = collectCanonicalRoutesForEntry([
799
- {
800
- type: 'nested',
801
- id: 'layout',
802
- isRoot: true,
803
- children: [
804
- {
805
- type: 'nested',
806
- id: '(lang)/layout',
807
- path: ':lang',
808
- children: [
809
- {
810
- type: 'nested',
811
- id: '(lang)/files/page',
812
- path: 'files/*',
813
- },
814
- ],
815
- },
816
- ],
817
- },
818
- ] as any);
819
296
 
297
+ const result = collectCanonicalRoutesForEntry(routes);
820
298
  expect(result).not.toBeNull();
821
- expect(Object.keys(result!)).toEqual(['/files/$']);
299
+ expect(Object.keys(result!)).toEqual([
300
+ '/',
301
+ '/about',
302
+ '/files/$',
303
+ '/optional/{-$slug}',
304
+ ]);
822
305
  });
823
306
 
824
- test('collapses localized variants with shared modernCanonicalPath to one canonical key', async () => {
825
- // This reuses the same fixture shape as the 'preserves typed child trees'
826
- // test but adds modernCanonicalPath fields as plugin-i18n now emits.
307
+ test('collapses localized variants to one canonical key with typed params', async () => {
827
308
  const result = collectCanonicalRoutesForEntry([
828
309
  {
829
310
  type: 'nested',
@@ -860,10 +341,10 @@ describe('collectCanonicalRoutesForEntry', () => {
860
341
  ] as any);
861
342
 
862
343
  expect(result).not.toBeNull();
863
- // Two physical variants share the same canonical path — only one entry.
864
- const keys = Object.keys(result!);
865
- expect(keys).toEqual(['/optional/{-$slug}', '/products/$slug']);
866
- // The Czech localized path must not appear as a separate key.
344
+ expect(Object.keys(result!)).toEqual([
345
+ '/optional/{-$slug}',
346
+ '/products/$slug',
347
+ ]);
867
348
  expect('/produkty/$slug' in result!).toBe(false);
868
349
 
869
350
  tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-canonical-'));
@@ -882,10 +363,7 @@ describe('collectCanonicalRoutesForEntry', () => {
882
363
  "const presentOptionalParams: UltramodernCanonicalRoutes['/optional/{-$slug}'] = { slug: 'tractor' };",
883
364
  '// @ts-expect-error optional canonical params still retain their string type',
884
365
  "const invalidOptionalParams: UltramodernCanonicalRoutes['/optional/{-$slug}'] = { slug: 42 };",
885
- "const emptySplatParams: UltramodernCanonicalRoutes['/files/$'] = {};",
886
366
  "const splatParams: UltramodernCanonicalRoutes['/files/$'] = { _splat: 'guides/intro' };",
887
- '// @ts-expect-error splat params retain their string type',
888
- "const invalidSplatParams: UltramodernCanonicalRoutes['/files/$'] = { _splat: 42 };",
889
367
  '// @ts-expect-error localized physical variants do not become canonical keys',
890
368
  "declare const localizedParams: UltramodernCanonicalRoutes['/produkty/$slug'];",
891
369
  '// @ts-expect-error the deduplicated product route still requires its param',
@@ -894,11 +372,9 @@ describe('collectCanonicalRoutesForEntry', () => {
894
372
  'void productParams;',
895
373
  'void optionalParams;',
896
374
  'void presentOptionalParams;',
897
- 'void emptySplatParams;',
898
375
  'void splatParams;',
899
376
  'void invalidRootParams;',
900
377
  'void invalidOptionalParams;',
901
- 'void invalidSplatParams;',
902
378
  'void localizedParams;',
903
379
  'void missingProductParams;',
904
380
  ],