@bleedingdev/modern-js-plugin-tanstack 3.5.0-ultramodern.100 → 3.5.0-ultramodern.102

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.
@@ -1,9 +1,8 @@
1
1
  import { execFile } from 'node:child_process';
2
- import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { promisify } from 'node:util';
6
- import { isRedirect } from '@tanstack/react-router';
7
6
 
8
7
  const resolverDelays = new Map<string, number>();
9
8
 
@@ -34,14 +33,8 @@ import {
34
33
  collectCanonicalRoutesForEntry,
35
34
  generateTanstackRouterTypesSourceForEntry,
36
35
  } from '../../src/cli/tanstackTypes';
37
- import { throwTanstackRedirect } from '../../src/runtime/loaderBridge';
38
36
 
39
37
  const execFileAsync = promisify(execFile);
40
- const routerGenGoldenPath = path.join(
41
- __dirname,
42
- 'fixtures',
43
- 'router-gen.golden.txt',
44
- );
45
38
  const routerGenOxlintConfigPath = path.join(
46
39
  __dirname,
47
40
  'fixtures',
@@ -51,35 +44,161 @@ const oxlintCliPath = path.resolve(
51
44
  __dirname,
52
45
  '../../../../toolkit/code-tools/node_modules/oxlint/bin/oxlint',
53
46
  );
47
+ const strictestTsconfigPath = path.resolve(
48
+ __dirname,
49
+ '../../node_modules/@tsconfig/strictest/tsconfig.json',
50
+ );
54
51
 
55
- type RedirectLike = {
56
- options?: {
57
- href?: string;
58
- to?: string;
59
- };
60
- };
61
-
62
- function catchThrown(fn: () => unknown): unknown {
63
- try {
64
- fn();
65
- } catch (err) {
66
- return err;
67
- }
68
- throw new Error('expected function to throw');
52
+ async function writeRuntimeTestPackage(projectDirectory: string) {
53
+ const runtimePackageDirectory = path.join(
54
+ projectDirectory,
55
+ 'node_modules',
56
+ '@modern-js',
57
+ 'plugin-tanstack',
58
+ );
59
+ await mkdir(runtimePackageDirectory, { recursive: true });
60
+ await writeFile(
61
+ path.join(runtimePackageDirectory, 'package.json'),
62
+ JSON.stringify({
63
+ name: '@modern-js/plugin-tanstack',
64
+ type: 'commonjs',
65
+ exports: {
66
+ './runtime': {
67
+ types: './runtime.d.ts',
68
+ default: './runtime.js',
69
+ },
70
+ },
71
+ }),
72
+ );
73
+ await writeFile(
74
+ path.join(runtimePackageDirectory, 'runtime.d.ts'),
75
+ [
76
+ 'export type ModernRouterContext = { request?: Request; requestContext?: unknown };',
77
+ 'export type ModernRouteStaticData = { modernRouteId?: string; modernRouteAction?: unknown; modernRouteLoader?: unknown };',
78
+ 'export type RouteOptions = {',
79
+ ' component?: unknown;',
80
+ ' getParentRoute?: (...args: never[]) => unknown;',
81
+ ' id?: string;',
82
+ ' loader?: (...args: never[]) => unknown;',
83
+ ' loaderDeps?: (...args: never[]) => unknown;',
84
+ ' path?: string;',
85
+ ' staticData?: ModernRouteStaticData;',
86
+ ' validateSearch?: (...args: never[]) => unknown;',
87
+ '};',
88
+ 'export type Route<TOptions extends RouteOptions = RouteOptions> = {',
89
+ ' options: TOptions;',
90
+ ' addChildren<const TChildren extends readonly unknown[]>(children: TChildren): Route<TOptions> & { children: TChildren };',
91
+ '};',
92
+ 'export const runtimeState: { adapterCalls: Array<{ hasSplat: boolean; modernLoader: unknown }> };',
93
+ 'export function createMemoryHistory<TOptions>(options: TOptions): TOptions;',
94
+ 'export const modernTanstackRouterFastDefaults: { defaultPreload: "intent" };',
95
+ 'export function createRootRouteWithContext<TContext extends ModernRouterContext>(): <const TOptions extends RouteOptions>(options: TOptions) => Route<TOptions>;',
96
+ 'export function createRoute<const TOptions extends RouteOptions>(options: TOptions): Route<TOptions>;',
97
+ 'export function createRouter<const TOptions extends { context: ModernRouterContext; history: unknown; routeTree: unknown }>(options: TOptions): TOptions;',
98
+ 'export function createRouteStaticData<const TData extends ModernRouteStaticData>(data: TData): TData;',
99
+ 'export function modernLoaderToTanstack<TLoader extends (...args: never[]) => unknown>(options: { hasSplat: boolean }, modernLoader: TLoader): (context: unknown) => Promise<Awaited<ReturnType<TLoader>>>;',
100
+ ].join('\n'),
101
+ );
102
+ await writeFile(
103
+ path.join(runtimePackageDirectory, 'runtime.js'),
104
+ [
105
+ 'const runtimeState = { adapterCalls: [] };',
106
+ 'const createRouteRecord = options => ({',
107
+ ' options,',
108
+ ' addChildren(children) {',
109
+ ' this.children = children;',
110
+ ' return this;',
111
+ ' },',
112
+ '});',
113
+ 'const createMemoryHistory = options => options;',
114
+ 'const modernTanstackRouterFastDefaults = { defaultPreload: "intent" };',
115
+ 'const createRootRouteWithContext = () => options => createRouteRecord(options);',
116
+ 'const createRoute = options => createRouteRecord(options);',
117
+ 'const createRouter = options => options;',
118
+ 'const createRouteStaticData = data => data;',
119
+ 'const modernLoaderToTanstack = (options, modernLoader) => {',
120
+ ' runtimeState.adapterCalls.push({ ...options, modernLoader });',
121
+ ' return async context => modernLoader(context);',
122
+ '};',
123
+ 'module.exports = { createMemoryHistory, createRootRouteWithContext, createRoute, createRouter, createRouteStaticData, modernLoaderToTanstack, modernTanstackRouterFastDefaults, runtimeState };',
124
+ ].join('\n'),
125
+ );
69
126
  }
70
127
 
71
- function extractRouteGenerationOrder(source: string) {
72
- return source.split('\n').filter(line => {
73
- return (
74
- line.startsWith('import component_') ||
75
- line.startsWith('const route_') ||
76
- line.includes('component: component_') ||
77
- line.startsWith('export const routeTree')
128
+ async function compileAndRunGeneratedRouter(options: {
129
+ projectDirectory: string;
130
+ routerGenTs: string;
131
+ runtimeCheck: string;
132
+ }) {
133
+ const { projectDirectory, routerGenTs, runtimeCheck } = options;
134
+ const generatedDirectory = path.join(
135
+ projectDirectory,
136
+ 'src',
137
+ 'modern-tanstack',
138
+ 'index',
139
+ );
140
+ await mkdir(generatedDirectory, { recursive: true });
141
+ await writeFile(path.join(generatedDirectory, 'router.gen.ts'), routerGenTs);
142
+ await writeFile(
143
+ path.join(projectDirectory, 'src', 'runtime-check.ts'),
144
+ runtimeCheck,
145
+ );
146
+ await writeRuntimeTestPackage(projectDirectory);
147
+ await writeFile(
148
+ path.join(projectDirectory, 'package.json'),
149
+ JSON.stringify({ private: true, type: 'commonjs' }),
150
+ );
151
+ await writeFile(
152
+ path.join(projectDirectory, 'tsconfig.json'),
153
+ JSON.stringify(
154
+ {
155
+ extends: strictestTsconfigPath,
156
+ compilerOptions: {
157
+ lib: ['ESNext', 'DOM'],
158
+ jsx: 'react-jsx',
159
+ module: 'Node16',
160
+ moduleResolution: 'Node16',
161
+ outDir: 'dist',
162
+ rootDir: 'src',
163
+ target: 'ES2022',
164
+ types: [],
165
+ verbatimModuleSyntax: false,
166
+ },
167
+ include: ['src/**/*.ts', 'src/**/*.tsx'],
168
+ },
169
+ null,
170
+ 2,
171
+ ),
172
+ );
173
+
174
+ try {
175
+ await execFileAsync(
176
+ process.platform === 'win32' ? 'tsgo.cmd' : 'tsgo',
177
+ ['-p', 'tsconfig.json'],
178
+ {
179
+ cwd: projectDirectory,
180
+ shell: process.platform === 'win32',
181
+ },
78
182
  );
79
- });
183
+ } catch (error) {
184
+ if (
185
+ error &&
186
+ typeof error === 'object' &&
187
+ 'stdout' in error &&
188
+ typeof error.stdout === 'string'
189
+ ) {
190
+ throw new Error(error.stdout, { cause: error });
191
+ }
192
+ throw error;
193
+ }
194
+ await execFileAsync(
195
+ process.execPath,
196
+ [path.join(projectDirectory, 'dist', 'runtime-check.js')],
197
+ { cwd: projectDirectory },
198
+ );
80
199
  }
81
200
 
82
- async function writeGoldenRouterFixture(srcDirectory: string) {
201
+ async function writeComprehensiveRouterFixture(srcDirectory: string) {
83
202
  const files = new Map([
84
203
  [
85
204
  'routes/(app)/layout.tsx',
@@ -116,8 +235,8 @@ async function writeGoldenRouterFixture(srcDirectory: string) {
116
235
  }
117
236
  }
118
237
 
119
- async function generateGoldenRouterGen(srcDirectory: string) {
120
- await writeGoldenRouterFixture(srcDirectory);
238
+ async function generateComprehensiveRouterGen(srcDirectory: string) {
239
+ await writeComprehensiveRouterFixture(srcDirectory);
121
240
 
122
241
  const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
123
242
  appContext: {
@@ -173,7 +292,79 @@ describe('tanstack router type generation', () => {
173
292
  }
174
293
  });
175
294
 
176
- test('emits inline data actions into route static data', async () => {
295
+ test('strictly compiles and executes a loader-free conventional route tree', async () => {
296
+ tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
297
+ const srcDirectory = path.join(tempDir, 'src');
298
+ const files = new Map([
299
+ [
300
+ 'routes/layout.tsx',
301
+ 'export default function Layout() { return null; }',
302
+ ],
303
+ ['routes/page.tsx', 'export default function Home() { return null; }'],
304
+ [
305
+ 'routes/reviews/[scanId]/page.tsx',
306
+ 'export default function Review() { return null; }',
307
+ ],
308
+ ]);
309
+
310
+ for (const [relativePath, contents] of files) {
311
+ const filePath = path.join(srcDirectory, relativePath);
312
+ await mkdir(path.dirname(filePath), { recursive: true });
313
+ await writeFile(filePath, contents);
314
+ }
315
+
316
+ const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
317
+ appContext: {
318
+ srcDirectory,
319
+ internalSrcAlias: '@/_',
320
+ } as any,
321
+ entryName: 'index',
322
+ routes: [
323
+ {
324
+ type: 'nested',
325
+ id: 'layout',
326
+ isRoot: true,
327
+ _component: '@/_/routes/layout',
328
+ children: [
329
+ {
330
+ type: 'nested',
331
+ id: 'page',
332
+ index: true,
333
+ _component: '@/_/routes/page',
334
+ },
335
+ {
336
+ type: 'nested',
337
+ id: 'reviews/(scanId)/page',
338
+ path: 'reviews/:scanId',
339
+ _component: '@/_/routes/reviews/[scanId]/page',
340
+ },
341
+ ],
342
+ },
343
+ ] as any,
344
+ });
345
+
346
+ await compileAndRunGeneratedRouter({
347
+ projectDirectory: tempDir,
348
+ routerGenTs,
349
+ runtimeCheck: [
350
+ "import { runtimeState } from '@modern-js/plugin-tanstack/runtime';",
351
+ "import Layout from './routes/layout';",
352
+ "import Home from './routes/page';",
353
+ "import Review from './routes/reviews/[scanId]/page';",
354
+ "import { rootRoute, routeTree, router } from './modern-tanstack/index/router.gen';",
355
+ '',
356
+ 'if (runtimeState.adapterCalls.length !== 0) throw new Error("loader adapter registered without a loader");',
357
+ 'if (Object.keys(router.context).length !== 0) throw new Error("router context is not empty");',
358
+ 'if (rootRoute.options.component !== Layout) throw new Error("root component was not wired");',
359
+ 'const [homeRoute, reviewRoute] = routeTree.children;',
360
+ 'if (homeRoute.options.component !== Home) throw new Error("index component was not wired");',
361
+ 'if (reviewRoute.options.component !== Review) throw new Error("review component was not wired");',
362
+ 'if (reviewRoute.options.path !== "reviews/$scanId") throw new Error("route params were not translated");',
363
+ ].join('\n'),
364
+ });
365
+ });
366
+
367
+ test('executes generated loader and action wiring through route static data', async () => {
177
368
  tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
178
369
  const srcDirectory = path.join(tempDir, 'src');
179
370
  const routeDir = path.join(srcDirectory, 'routes', 'mf');
@@ -210,50 +401,46 @@ describe('tanstack router type generation', () => {
210
401
  ] as any,
211
402
  });
212
403
 
213
- expect(routerGenTs).toContain(
214
- 'import { loader as loader_0, action as action_0 } from "../../routes/mf/page.data";',
215
- );
216
- expect(routerGenTs).toContain('modernRouteLoader: loader_0');
217
- expect(routerGenTs).toContain('modernRouteAction: action_0');
218
- expect(routerGenTs).toContain(
219
- "} from '@modern-js/plugin-tanstack/runtime';",
220
- );
221
- expect(routerGenTs).toContain('modernTanstackRouterFastDefaults,');
222
- expect(routerGenTs).toContain('...modernTanstackRouterFastDefaults,');
223
-
224
- // The loader-bridge helpers are imported from the package runtime instead
225
- // of being inlined into every generated file (bugfixes ship via package
226
- // update, and the broken inline absolute-redirect handler is gone).
227
- expect(routerGenTs).toContain('createRouteStaticData,');
228
- expect(routerGenTs).toContain('modernLoaderToTanstack,');
229
- expect(routerGenTs).toContain('type ModernRouterContext,');
230
- expect(routerGenTs).not.toContain('function modernLoaderToTanstack');
231
- expect(routerGenTs).not.toContain('function createRouteStaticData');
232
- expect(routerGenTs).not.toContain('function throwTanstackRedirect');
233
- expect(routerGenTs).not.toContain('new URL(target)');
234
- expect(routerGenTs).not.toContain('redirect({ to: target })');
235
-
236
- const redirectError = catchThrown(() =>
237
- throwTanstackRedirect('https://example.com/absolute'),
238
- ) as RedirectLike;
239
- expect(isRedirect(redirectError)).toBe(true);
240
- expect(redirectError.options?.href).toBe('https://example.com/absolute');
241
- expect(redirectError.options?.to).toBeUndefined();
404
+ await compileAndRunGeneratedRouter({
405
+ projectDirectory: tempDir,
406
+ routerGenTs,
407
+ runtimeCheck: [
408
+ "import { runtimeState } from '@modern-js/plugin-tanstack/runtime';",
409
+ "import { action, loader } from './routes/mf/page.data';",
410
+ "import { routeTree } from './modern-tanstack/index/router.gen';",
411
+ '',
412
+ 'async function main() {',
413
+ 'const [mfRoute] = routeTree.children;',
414
+ 'if (runtimeState.adapterCalls.length !== 1) throw new Error("loader adapter was not registered exactly once");',
415
+ 'if (runtimeState.adapterCalls[0]?.modernLoader !== loader) throw new Error("wrong loader was adapted");',
416
+ 'const result = await mfRoute.options.loader({});',
417
+ 'if (result.count !== 0) throw new Error("adapted loader returned the wrong value");',
418
+ 'if (mfRoute.options.staticData.modernRouteLoader !== loader) throw new Error("loader static data was not wired");',
419
+ 'if (mfRoute.options.staticData.modernRouteAction !== action) throw new Error("action static data was not wired");',
420
+ '}',
421
+ 'void main();',
422
+ ].join('\n'),
423
+ });
242
424
  });
243
425
 
244
- test('emits resolvable relative component imports for routes carrying _component', async () => {
426
+ test('executes component sharing correctly when module resolution races', async () => {
245
427
  tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
246
428
  const srcDirectory = path.join(tempDir, 'src');
429
+
247
430
  for (const componentFile of [
248
- 'routes/layout.tsx',
249
- 'routes/page.tsx',
250
- 'routes/about/page.tsx',
431
+ 'routes/slow-page.tsx',
432
+ 'routes/fast-page.tsx',
433
+ 'routes/shared-page.tsx',
251
434
  ]) {
252
435
  const componentPath = path.join(srcDirectory, componentFile);
253
436
  await mkdir(path.dirname(componentPath), { recursive: true });
254
- await writeFile(componentPath, 'export default () => null;');
437
+ await writeFile(
438
+ componentPath,
439
+ 'export default function Page() { return null; }',
440
+ );
255
441
  }
256
442
 
443
+ resolverDelays.set('/routes/slow-page', 25);
257
444
  const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
258
445
  appContext: {
259
446
  srcDirectory,
@@ -265,29 +452,32 @@ describe('tanstack router type generation', () => {
265
452
  type: 'nested',
266
453
  id: 'layout',
267
454
  isRoot: true,
268
- _component: '@/_/routes/layout',
269
455
  children: [
270
456
  {
271
457
  type: 'nested',
272
- id: 'page',
273
- index: true,
274
- _component: '@/_/routes/page',
458
+ id: 'slow/page',
459
+ path: 'slow',
460
+ _component: '@/_/routes/slow-page',
275
461
  },
276
462
  {
277
463
  type: 'nested',
278
- id: 'about/page',
279
- path: 'about',
280
- _component: '@/_/routes/about/page',
464
+ id: 'fast/page',
465
+ path: 'fast',
466
+ _component: '@/_/routes/fast-page',
281
467
  },
282
468
  {
283
- // Shares the page component module: the import must be reused.
284
469
  type: 'nested',
285
- id: 'about-alias/page',
286
- path: 'about-alias',
287
- _component: '@/_/routes/about/page',
470
+ id: 'shared-a/page',
471
+ path: 'shared-a',
472
+ _component: '@/_/routes/shared-page',
473
+ },
474
+ {
475
+ type: 'nested',
476
+ id: 'shared-b/page',
477
+ path: 'shared-b',
478
+ _component: '@/_/routes/shared-page',
288
479
  },
289
480
  {
290
- // No _component: no component option may be emitted.
291
481
  type: 'nested',
292
482
  id: 'data-only/page',
293
483
  path: 'data-only',
@@ -297,142 +487,62 @@ describe('tanstack router type generation', () => {
297
487
  ] as any,
298
488
  });
299
489
 
300
- // Children are emitted first, the root route component import last.
301
- // Imports are relative (resolved like loader modules) — the raw
302
- // `@/_` internal alias is not mapped by app tsconfigs.
303
- expect(routerGenTs).toContain(
304
- 'import component_0 from "../../routes/page";',
305
- );
306
- expect(routerGenTs).toContain(
307
- 'import component_1 from "../../routes/about/page";',
308
- );
309
- expect(routerGenTs).toContain(
310
- 'import component_2 from "../../routes/layout";',
311
- );
312
- expect(routerGenTs).not.toContain('@/_/routes');
313
- // The shared module is imported exactly once.
314
- expect(routerGenTs).not.toContain('component_3');
315
- expect(
316
- routerGenTs.match(/from "\.\.\/\.\.\/routes\/about\/page";/g),
317
- ).toHaveLength(1);
318
-
319
- expect(routerGenTs).toContain('component: component_0,');
320
- // The shared component import is referenced by both aliased routes.
321
- expect(routerGenTs.match(/component: component_1,/g)).toHaveLength(2);
322
- // The root route gets its component option.
323
- expect(routerGenTs).toContain('component: component_2,');
324
-
325
- const dataOnlyRoute = routerGenTs
326
- .split('const ')
327
- .find(block => block.includes('path: "data-only",'));
328
- expect(dataOnlyRoute).toBeDefined();
329
- expect(dataOnlyRoute).not.toContain('component:');
330
- });
331
-
332
- test('keeps generated source stable when route module resolution races', async () => {
333
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
334
- const srcDirectory = path.join(tempDir, 'src');
335
-
336
- for (const componentFile of [
337
- 'routes/slow-page.tsx',
338
- 'routes/fast-page.tsx',
339
- 'routes/shared-page.tsx',
340
- ]) {
341
- const componentPath = path.join(srcDirectory, componentFile);
342
- await mkdir(path.dirname(componentPath), { recursive: true });
343
- await writeFile(
344
- componentPath,
345
- 'export default function Page() { return null; }',
346
- );
347
- }
348
-
349
- const routes = [
350
- {
351
- type: 'nested',
352
- id: 'layout',
353
- isRoot: true,
354
- children: [
355
- {
356
- type: 'nested',
357
- id: 'slow/page',
358
- path: 'slow',
359
- _component: '@/_/routes/slow-page',
360
- },
361
- {
362
- type: 'nested',
363
- id: 'fast/page',
364
- path: 'fast',
365
- _component: '@/_/routes/fast-page',
366
- },
367
- {
368
- type: 'nested',
369
- id: 'shared-a/page',
370
- path: 'shared-a',
371
- _component: '@/_/routes/shared-page',
372
- },
373
- {
374
- type: 'nested',
375
- id: 'shared-b/page',
376
- path: 'shared-b',
377
- _component: '@/_/routes/shared-page',
378
- },
379
- ],
380
- },
381
- ] as any;
382
-
383
- const generateWithResolverDelays = async (
384
- delays: Record<string, number>,
385
- ) => {
386
- resolverDelays.clear();
387
- for (const [suffix, delay] of Object.entries(delays)) {
388
- resolverDelays.set(suffix, delay);
389
- }
390
-
391
- const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
392
- appContext: {
393
- srcDirectory,
394
- internalSrcAlias: '@/_',
395
- } as any,
396
- entryName: 'index',
397
- routes,
398
- });
399
-
400
- return routerGenTs;
401
- };
402
-
403
- const sequentialOutput = await generateWithResolverDelays({});
404
- const racedOutput = await generateWithResolverDelays({
405
- '/routes/slow-page': 25,
490
+ await compileAndRunGeneratedRouter({
491
+ projectDirectory: tempDir,
492
+ routerGenTs,
493
+ runtimeCheck: [
494
+ "import SlowPage from './routes/slow-page';",
495
+ "import FastPage from './routes/fast-page';",
496
+ "import SharedPage from './routes/shared-page';",
497
+ "import { routeTree } from './modern-tanstack/index/router.gen';",
498
+ '',
499
+ 'const [slowRoute, fastRoute, sharedA, sharedB, dataOnly] = routeTree.children;',
500
+ 'if (slowRoute.options.component !== SlowPage) throw new Error("slow component was crossed during resolution");',
501
+ 'if (fastRoute.options.component !== FastPage) throw new Error("fast component was crossed during resolution");',
502
+ 'if (sharedA.options.component !== SharedPage || sharedB.options.component !== SharedPage) throw new Error("shared component identity was not preserved");',
503
+ 'if (Reflect.has(dataOnly.options, "component")) throw new Error("data-only route gained a component");',
504
+ ].join('\n'),
406
505
  });
407
-
408
- expect(
409
- racedOutput.match(/from "\.\.\/\.\.\/routes\/shared-page";/g),
410
- ).toHaveLength(1);
411
- expect(extractRouteGenerationOrder(racedOutput)).toEqual(
412
- extractRouteGenerationOrder(sequentialOutput),
413
- );
414
- expect(racedOutput).toBe(sequentialOutput);
415
506
  });
416
507
 
417
- test('matches checked-in router.gen golden output', async () => {
508
+ test('strictly compiles and executes nested loader and search contracts', async () => {
418
509
  tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
419
510
  const srcDirectory = path.join(tempDir, 'src');
420
- const routerGenTs = await generateGoldenRouterGen(srcDirectory);
421
-
422
- if (process.env.UPDATE_TANSTACK_ROUTER_GOLDEN === '1') {
423
- await mkdir(path.dirname(routerGenGoldenPath), { recursive: true });
424
- await writeFile(routerGenGoldenPath, routerGenTs);
425
- }
511
+ const routerGenTs = await generateComprehensiveRouterGen(srcDirectory);
426
512
 
427
- await expect(readFile(routerGenGoldenPath, 'utf8')).resolves.toBe(
513
+ await compileAndRunGeneratedRouter({
514
+ projectDirectory: tempDir,
428
515
  routerGenTs,
429
- );
516
+ runtimeCheck: [
517
+ "import { runtimeState } from '@modern-js/plugin-tanstack/runtime';",
518
+ "import { action, loader } from './routes/(app)/users/(userId)/page.data';",
519
+ "import { loaderDeps, validateSearch } from './routes/search.contract';",
520
+ "import { routeTree } from './modern-tanstack/index/router.gen';",
521
+ '',
522
+ 'async function main() {',
523
+ 'const [appRoute] = routeTree.children;',
524
+ 'const [userRoute, docsRoute] = appRoute.children;',
525
+ 'if (runtimeState.adapterCalls.length !== 1) throw new Error("nested loader was not adapted");',
526
+ 'if (userRoute.options.validateSearch !== validateSearch) throw new Error("search validator was not wired");',
527
+ 'if (userRoute.options.loaderDeps !== loaderDeps) throw new Error("loader dependencies were not wired");',
528
+ 'if (userRoute.options.staticData.modernRouteLoader !== loader) throw new Error("nested static loader was not wired");',
529
+ 'if (userRoute.options.staticData.modernRouteAction !== action) throw new Error("nested static action was not wired");',
530
+ 'const user = await userRoute.options.loader({});',
531
+ 'if (user.userId !== "42") throw new Error("nested loader result was lost");',
532
+ 'if (userRoute.options.path !== "users/$userId") throw new Error("required route param was not translated");',
533
+ 'if (docsRoute.options.path !== "docs/$") throw new Error("splat route was not translated");',
534
+ 'const search = userRoute.options.validateSearch({});',
535
+ 'if (search.tab !== "overview") throw new Error("search contract returned the wrong value");',
536
+ '}',
537
+ 'void main();',
538
+ ].join('\n'),
539
+ });
430
540
  });
431
541
 
432
- test('emits suppression-free router source that passes the generated-artifact lint fixture', async () => {
542
+ test('passes the generated-artifact lint contract', async () => {
433
543
  tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
434
544
  const srcDirectory = path.join(tempDir, 'src');
435
- const routerGenTs = await generateGoldenRouterGen(srcDirectory);
545
+ const routerGenTs = await generateComprehensiveRouterGen(srcDirectory);
436
546
  const routerGenPath = path.join(
437
547
  srcDirectory,
438
548
  'modern-tanstack',
@@ -443,8 +553,6 @@ describe('tanstack router type generation', () => {
443
553
  await mkdir(path.dirname(routerGenPath), { recursive: true });
444
554
  await writeFile(routerGenPath, routerGenTs);
445
555
 
446
- expect(routerGenTs).not.toContain('eslint-disable');
447
-
448
556
  const { stderr, stdout } = await execFileAsync(
449
557
  process.execPath,
450
558
  [
@@ -464,130 +572,7 @@ describe('tanstack router type generation', () => {
464
572
  expect(`${stdout}${stderr}`).toBe('');
465
573
  });
466
574
 
467
- test('typechecks generated TanStack search contracts', async () => {
468
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
469
- const srcDirectory = path.join(tempDir, 'src');
470
- const routeDir = path.join(srcDirectory, 'routes');
471
- const generatedDir = path.join(srcDirectory, 'modern-tanstack', 'index');
472
- await mkdir(routeDir, { recursive: true });
473
- await mkdir(generatedDir, { recursive: true });
474
- await writeFile(
475
- path.join(routeDir, 'search.contract.ts'),
476
- [
477
- 'export const validateSearch = (search: { q?: string }) => ({ q: search.q ?? "" });',
478
- 'export const loaderDeps = ({ search }: { search: { q: string } }) => ({ q: search.q });',
479
- ].join('\n'),
480
- );
481
-
482
- const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
483
- appContext: {
484
- srcDirectory,
485
- internalSrcAlias: '@/_',
486
- } as any,
487
- entryName: 'index',
488
- routes: [
489
- {
490
- type: 'nested',
491
- id: 'layout',
492
- isRoot: true,
493
- validateSearch: '@/_/routes/search.contract',
494
- loaderDeps: '@/_/routes/search.contract',
495
- children: [
496
- {
497
- type: 'nested',
498
- id: 'search/page',
499
- path: 'search',
500
- validateSearch: '@/_/routes/search.contract',
501
- loaderDeps: '@/_/routes/search.contract',
502
- },
503
- ],
504
- },
505
- ] as any,
506
- });
507
-
508
- await writeFile(path.join(generatedDir, 'router.gen.ts'), routerGenTs);
509
- await writeFile(
510
- path.join(srcDirectory, 'runtime-shim.d.ts'),
511
- [
512
- "declare module '@modern-js/plugin-tanstack/runtime' {",
513
- ' type RouteOptions = {',
514
- ' getParentRoute?: () => unknown;',
515
- ' id?: string;',
516
- ' loader?: unknown;',
517
- ' loaderDeps?: unknown;',
518
- ' path?: string;',
519
- ' staticData?: unknown;',
520
- ' validateSearch?: unknown;',
521
- ' };',
522
- ' type Route<TOptions extends RouteOptions> = {',
523
- ' options: TOptions;',
524
- ' addChildren<TChildren extends readonly unknown[]>(children: TChildren): Route<TOptions> & { children: TChildren };',
525
- ' };',
526
- ' export type ModernRouterContext = {',
527
- ' request?: Request;',
528
- ' requestContext?: unknown;',
529
- ' };',
530
- ' export function createMemoryHistory(options: unknown): unknown;',
531
- ' export const modernTanstackRouterFastDefaults: Record<string, unknown>;',
532
- ' export function createRootRouteWithContext<TContext>(): <TOptions extends RouteOptions>(options: TOptions) => Route<TOptions>;',
533
- ' export function createRoute<TOptions extends RouteOptions>(options: TOptions): Route<TOptions>;',
534
- ' export function createRouter<TOptions extends Record<string, unknown>>(options: TOptions): TOptions;',
535
- ' export function createRouteStaticData(opts: { modernRouteId?: string; modernRouteAction?: unknown; modernRouteLoader?: unknown }): Record<string, unknown>;',
536
- ' export function modernLoaderToTanstack<TLoader extends (args: never) => unknown>(opts: { hasSplat: boolean }, modernLoader: TLoader): (ctx: unknown) => Promise<Awaited<ReturnType<TLoader>>>;',
537
- '}',
538
- ].join('\n'),
539
- );
540
- await writeFile(
541
- path.join(srcDirectory, 'assert-search-contracts.ts'),
542
- [
543
- "import { rootRoute, routeTree } from './modern-tanstack/index/router.gen';",
544
- "import { loaderDeps, validateSearch } from './routes/search.contract';",
545
- '',
546
- 'const rootValidateSearch: typeof validateSearch = rootRoute.options.validateSearch;',
547
- 'const rootLoaderDeps: typeof loaderDeps = rootRoute.options.loaderDeps;',
548
- 'const childValidateSearch: typeof validateSearch = routeTree.children[0].options.validateSearch;',
549
- 'const childLoaderDeps: typeof loaderDeps = routeTree.children[0].options.loaderDeps;',
550
- '',
551
- "rootValidateSearch({ q: 'root' });",
552
- "rootLoaderDeps({ search: { q: 'root' } });",
553
- "childValidateSearch({ q: 'child' });",
554
- "childLoaderDeps({ search: { q: 'child' } });",
555
- ].join('\n'),
556
- );
557
- await writeFile(
558
- path.join(tempDir, 'tsconfig.json'),
559
- JSON.stringify(
560
- {
561
- compilerOptions: {
562
- lib: ['ESNext', 'DOM'],
563
- module: 'Preserve',
564
- moduleResolution: 'Bundler',
565
- noEmit: true,
566
- skipLibCheck: true,
567
- strict: true,
568
- target: 'ESNext',
569
- types: [],
570
- },
571
- include: ['src/**/*.ts', 'src/**/*.d.ts'],
572
- },
573
- null,
574
- 2,
575
- ),
576
- );
577
-
578
- await expect(
579
- execFileAsync(
580
- process.platform === 'win32' ? 'tsgo.cmd' : 'tsgo',
581
- ['--noEmit', '-p', 'tsconfig.json'],
582
- {
583
- cwd: tempDir,
584
- shell: process.platform === 'win32',
585
- },
586
- ),
587
- ).resolves.toBeDefined();
588
- });
589
-
590
- test('preserves typed child trees for localized nested route aliases', async () => {
575
+ test('executes localized aliases with typed children and correct parents', async () => {
591
576
  tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
592
577
  const srcDirectory = path.join(tempDir, 'src');
593
578
 
@@ -630,21 +615,23 @@ describe('tanstack router type generation', () => {
630
615
  ] as any,
631
616
  });
632
617
 
633
- expect(routerGenTs).toContain(
634
- 'const route__lang__layout__base = createRoute({',
635
- );
636
- expect(routerGenTs).toContain(
637
- 'getParentRoute: () => route__lang__layout__base,',
638
- );
639
- expect(routerGenTs).toContain('path: "produkty/$slug",');
640
- expect(routerGenTs).toContain('path: "volitelne/{-$slug}",');
641
- expect(routerGenTs).toContain(
642
- 'const route__lang__layout = route__lang__layout__base.addChildren([route__lang__products__slug__page, route__lang__products__slug__page__localised_produkty_slug, route__lang__optional__slug$__page__localised_volitelne_slug]);',
643
- );
644
- expect(routerGenTs).toContain(
645
- 'export const routeTree = rootRoute.addChildren([route__lang__layout]);',
646
- );
647
- expect(routerGenTs).not.toContain('route__lang__layout.addChildren([');
618
+ await compileAndRunGeneratedRouter({
619
+ projectDirectory: tempDir,
620
+ routerGenTs,
621
+ runtimeCheck: [
622
+ "import { routeTree } from './modern-tanstack/index/router.gen';",
623
+ '',
624
+ 'const [localeRoute] = routeTree.children;',
625
+ 'const [productRoute, localizedProductRoute, optionalRoute] = localeRoute.children;',
626
+ 'if (localeRoute.options.path !== "$lang") throw new Error("locale param was not translated");',
627
+ 'if (productRoute.options.path !== "products/$slug") throw new Error("product param was not translated");',
628
+ 'if (localizedProductRoute.options.path !== "produkty/$slug") throw new Error("localized param was not translated");',
629
+ 'if (optionalRoute.options.path !== "volitelne/{-$slug}") throw new Error("optional param was not translated");',
630
+ 'for (const child of localeRoute.children) {',
631
+ ' if (child.options.getParentRoute() !== localeRoute) throw new Error("localized child points at the wrong parent");',
632
+ '}',
633
+ ].join('\n'),
634
+ });
648
635
  });
649
636
  });
650
637