@bleedingdev/modern-js-plugin-tanstack 3.5.0-ultramodern.101 → 3.5.0-ultramodern.103

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 (52) hide show
  1. package/dist/cjs/cli/index.js +10 -2
  2. package/dist/cjs/cli/tanstackTypes/source.js +7 -17
  3. package/dist/cjs/runtime/plugin.node.js +12 -5
  4. package/dist/cjs/runtime/pluginShared.js +3 -3
  5. package/dist/cjs/runtime/rsc/payloadErrors.js +2 -2
  6. package/dist/cjs/runtime/rsc/shared.js +1 -1
  7. package/dist/cjs/runtime/rsc/treeGetter.js +5 -9
  8. package/dist/cjs/runtime/submitAction.js +4 -5
  9. package/dist/esm/cli/index.mjs +10 -2
  10. package/dist/esm/cli/tanstackTypes/source.mjs +7 -7
  11. package/dist/esm/runtime/plugin.node.mjs +12 -5
  12. package/dist/esm/runtime/pluginShared.mjs +3 -3
  13. package/dist/esm/runtime/rsc/payloadErrors.mjs +1 -1
  14. package/dist/esm/runtime/rsc/shared.mjs +1 -1
  15. package/dist/esm/runtime/rsc/treeGetter.mjs +5 -9
  16. package/dist/esm/runtime/submitAction.mjs +4 -5
  17. package/dist/esm-node/cli/index.mjs +10 -2
  18. package/dist/esm-node/cli/tanstackTypes/source.mjs +7 -7
  19. package/dist/esm-node/runtime/plugin.node.mjs +12 -5
  20. package/dist/esm-node/runtime/pluginShared.mjs +3 -3
  21. package/dist/esm-node/runtime/rsc/payloadErrors.mjs +1 -1
  22. package/dist/esm-node/runtime/rsc/shared.mjs +1 -1
  23. package/dist/esm-node/runtime/rsc/treeGetter.mjs +5 -9
  24. package/dist/esm-node/runtime/submitAction.mjs +4 -5
  25. package/dist/types/runtime/clientHydration.d.ts +1 -1
  26. package/dist/types/runtime/ssrTypes.d.ts +0 -2
  27. package/package.json +26 -20
  28. package/rstest.config.mts +2 -0
  29. package/src/cli/index.ts +12 -0
  30. package/src/cli/tanstackTypes/canonicalRoutes.ts +0 -1
  31. package/src/cli/tanstackTypes/shared.ts +0 -1
  32. package/src/cli/tanstackTypes/source.ts +1 -1
  33. package/src/runtime/plugin.node.tsx +17 -11
  34. package/src/runtime/plugin.tsx +0 -2
  35. package/src/runtime/pluginShared.ts +8 -7
  36. package/src/runtime/rsc/payloadErrors.ts +1 -1
  37. package/src/runtime/rsc/shared.ts +1 -1
  38. package/src/runtime/rsc/treeGetter.tsx +9 -8
  39. package/src/runtime/ssrManagedTags.ts +0 -2
  40. package/src/runtime/ssrTypes.ts +0 -4
  41. package/src/runtime/submitAction.ts +6 -11
  42. package/src/types/react-server-dom-rspack.d.ts +4 -52
  43. package/tests/router/cli.test.ts +4 -0
  44. package/tests/router/clientHydration.test.tsx +140 -1
  45. package/tests/router/dataMutation.test.tsx +50 -9
  46. package/tests/router/fixtures/rscDecoderSecurityChild.cjs +162 -0
  47. package/tests/router/preloadRedirect.test.ts +3 -8
  48. package/tests/router/routeTree.test.ts +73 -19
  49. package/tests/router/rscDecoderSecurity.test.ts +113 -0
  50. package/tests/router/serverPlugin.test.ts +115 -0
  51. package/tests/router/tanstackTypes.test.ts +351 -26
  52. package/src/runtime/ssr-shim.d.ts +0 -10
@@ -29,6 +29,7 @@ rstest.mock('../../src/cli/tanstackTypes/shared', () => {
29
29
  };
30
30
  });
31
31
 
32
+ import { writeTanstackRegisterFile } from '../../src/cli/artifacts';
32
33
  import {
33
34
  collectCanonicalRoutesForEntry,
34
35
  generateTanstackRouterTypesSourceForEntry,
@@ -48,6 +49,10 @@ const strictestTsconfigPath = path.resolve(
48
49
  __dirname,
49
50
  '../../node_modules/@tsconfig/strictest/tsconfig.json',
50
51
  );
52
+ const installedReactRouterPath = path.resolve(
53
+ __dirname,
54
+ '../../node_modules/@tanstack/react-router',
55
+ );
51
56
 
52
57
  async function writeRuntimeTestPackage(projectDirectory: string) {
53
58
  const runtimePackageDirectory = path.join(
@@ -125,6 +130,41 @@ async function writeRuntimeTestPackage(projectDirectory: string) {
125
130
  );
126
131
  }
127
132
 
133
+ async function writeInstalledRouterRuntimeTestPackage(
134
+ projectDirectory: string,
135
+ ) {
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
+
128
168
  async function compileAndRunGeneratedRouter(options: {
129
169
  projectDirectory: string;
130
170
  routerGenTs: string;
@@ -181,13 +221,20 @@ async function compileAndRunGeneratedRouter(options: {
181
221
  },
182
222
  );
183
223
  } 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 });
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
+ }
191
238
  }
192
239
  throw error;
193
240
  }
@@ -198,6 +245,160 @@ async function compileAndRunGeneratedRouter(options: {
198
245
  );
199
246
  }
200
247
 
248
+ async function compileGeneratedRouterAgainstInstalledDeclarations(options: {
249
+ projectDirectory: string;
250
+ routerGenTs: string;
251
+ }) {
252
+ const { projectDirectory, routerGenTs } = options;
253
+ const generatedDirectory = path.join(
254
+ projectDirectory,
255
+ 'src',
256
+ 'modern-tanstack',
257
+ 'golden',
258
+ );
259
+ await mkdir(generatedDirectory, { recursive: true });
260
+ 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' }),
265
+ );
266
+ 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'],
285
+ },
286
+ null,
287
+ 2,
288
+ ),
289
+ );
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
+ }
318
+ }
319
+
320
+ async function typecheckCanonicalRegisterContract(options: {
321
+ canonicalRoutes: Record<string, string>;
322
+ contractLines: string[];
323
+ projectDirectory: string;
324
+ }) {
325
+ const { canonicalRoutes, contractLines, projectDirectory } = options;
326
+ 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 });
330
+ await writeFile(
331
+ path.join(routerDirectory, 'router.gen.ts'),
332
+ 'export const router = { context: {} };',
333
+ );
334
+ await writeTanstackRegisterFile({
335
+ canonicalRoutes,
336
+ entries: ['index'],
337
+ srcDirectory,
338
+ });
339
+ await writeFile(
340
+ path.join(srcDirectory, 'runtime-shims.d.ts'),
341
+ [
342
+ "declare module '@modern-js/plugin-tanstack/runtime' { export interface Register {} }",
343
+ "declare module '@modern-js/plugin-i18n/runtime' { export interface UltramodernCanonicalRoutes {} }",
344
+ ].join('\n'),
345
+ );
346
+ await writeFile(
347
+ path.join(srcDirectory, 'canonical-routes-contract.ts'),
348
+ [
349
+ "import type { UltramodernCanonicalRoutes } from '@modern-js/plugin-i18n/runtime';",
350
+ ...contractLines,
351
+ ].join('\n'),
352
+ );
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
+ }
400
+ }
401
+
201
402
  async function writeComprehensiveRouterFixture(srcDirectory: string) {
202
403
  const files = new Map([
203
404
  [
@@ -539,6 +740,17 @@ describe('tanstack router type generation', () => {
539
740
  });
540
741
  });
541
742
 
743
+ test('strictly compiles generated routes against installed router declarations', async () => {
744
+ 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
+
542
754
  test('passes the generated-artifact lint contract', async () => {
543
755
  tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
544
756
  const srcDirectory = path.join(tempDir, 'src');
@@ -636,6 +848,15 @@ describe('tanstack router type generation', () => {
636
848
  });
637
849
 
638
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
+
639
860
  test('returns null for a route tree with no locale param and no canonical metadata', () => {
640
861
  const result = collectCanonicalRoutesForEntry([
641
862
  {
@@ -660,7 +881,7 @@ describe('collectCanonicalRoutesForEntry', () => {
660
881
  expect(result).toBeNull();
661
882
  });
662
883
 
663
- test('ignores a leading :lang param when the locale-param heuristic is disabled (no plugin-i18n)', () => {
884
+ test('ignores a leading :lang param when the locale-param heuristic is disabled (no plugin-i18n)', async () => {
664
885
  const routes = [
665
886
  {
666
887
  type: 'nested',
@@ -689,14 +910,27 @@ describe('collectCanonicalRoutesForEntry', () => {
689
910
  collectCanonicalRoutesForEntry(routes, { localeParamHeuristic: false }),
690
911
  ).toBeNull();
691
912
  // With plugin-i18n installed the heuristic strips the locale prefix.
692
- expect(
693
- collectCanonicalRoutesForEntry(routes, { localeParamHeuristic: true }),
694
- ).toEqual({
695
- '/about': 'Record<string, never>',
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,
696
930
  });
697
931
  });
698
932
 
699
- test('still honors modernCanonicalPath metadata when the heuristic is disabled', () => {
933
+ test('still honors modernCanonicalPath metadata when the heuristic is disabled', async () => {
700
934
  const result = collectCanonicalRoutesForEntry(
701
935
  [
702
936
  {
@@ -717,10 +951,26 @@ describe('collectCanonicalRoutesForEntry', () => {
717
951
  );
718
952
 
719
953
  expect(result).not.toBeNull();
720
- expect(result!['/products/$slug']).toBe('{ "slug": string }');
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
+ });
721
971
  });
722
972
 
723
- test('strips leading :lang param and maps index under :lang to "/"', () => {
973
+ test('strips leading :lang param and maps index under :lang to "/"', async () => {
724
974
  const result = collectCanonicalRoutesForEntry([
725
975
  {
726
976
  type: 'nested',
@@ -749,11 +999,25 @@ describe('collectCanonicalRoutesForEntry', () => {
749
999
  ] as any);
750
1000
 
751
1001
  expect(result).not.toBeNull();
752
- expect(result!['/']).toBe('Record<string, never>');
753
- expect(result!['/about']).toBe('Record<string, never>');
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
+ });
754
1018
  });
755
1019
 
756
- test('converts :slug to $slug with required params type', () => {
1020
+ test('converts :slug to $slug with required params type', async () => {
757
1021
  const result = collectCanonicalRoutesForEntry([
758
1022
  {
759
1023
  type: 'nested',
@@ -777,10 +1041,23 @@ describe('collectCanonicalRoutesForEntry', () => {
777
1041
  ] as any);
778
1042
 
779
1043
  expect(result).not.toBeNull();
780
- expect(result!['/products/$slug']).toBe('{ "slug": string }');
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
+ });
781
1058
  });
782
1059
 
783
- test('converts :slug? to optional {-$slug} with optional params type', () => {
1060
+ test('converts :slug? to optional {-$slug} with optional params type', async () => {
784
1061
  const result = collectCanonicalRoutesForEntry([
785
1062
  {
786
1063
  type: 'nested',
@@ -804,10 +1081,25 @@ describe('collectCanonicalRoutesForEntry', () => {
804
1081
  ] as any);
805
1082
 
806
1083
  expect(result).not.toBeNull();
807
- expect(result!['/optional/{-$slug}']).toBe('{ "slug"?: string }');
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
+ });
808
1100
  });
809
1101
 
810
- test('converts * splat to $ with optional _splat param', () => {
1102
+ test('converts * splat to $ with optional _splat param', async () => {
811
1103
  const result = collectCanonicalRoutesForEntry([
812
1104
  {
813
1105
  type: 'nested',
@@ -831,10 +1123,25 @@ describe('collectCanonicalRoutesForEntry', () => {
831
1123
  ] as any);
832
1124
 
833
1125
  expect(result).not.toBeNull();
834
- expect(result!['/files/$']).toBe("{ '_splat'?: string }");
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
+ });
835
1142
  });
836
1143
 
837
- test('collapses localized variants with shared modernCanonicalPath to one canonical key', () => {
1144
+ test('collapses localized variants with shared modernCanonicalPath to one canonical key', async () => {
838
1145
  // This reuses the same fixture shape as the 'preserves typed child trees'
839
1146
  // test but adds modernCanonicalPath fields as plugin-i18n now emits.
840
1147
  const result = collectCanonicalRoutesForEntry([
@@ -877,10 +1184,28 @@ describe('collectCanonicalRoutesForEntry', () => {
877
1184
  const keys = Object.keys(result!);
878
1185
  // /products/$slug and /optional/{-$slug} — exactly 2 keys with params
879
1186
  expect(keys.filter(k => k.startsWith('/products'))).toHaveLength(1);
880
- expect(result!['/products/$slug']).toBe('{ "slug": string }');
881
- expect(result!['/optional/{-$slug}']).toBe('{ "slug"?: string }');
1187
+ expect(keys).toHaveLength(2);
1188
+ expect(keys).toEqual(['/optional/{-$slug}', '/products/$slug']);
882
1189
  // The Czech localized path must not appear as a separate key.
883
1190
  expect('/produkty/$slug' in result!).toBe(false);
1191
+
1192
+ tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-canonical-'));
1193
+ await typecheckCanonicalRegisterContract({
1194
+ canonicalRoutes: result!,
1195
+ contractLines: [
1196
+ "const productParams: UltramodernCanonicalRoutes['/products/$slug'] = { slug: 'tractor' };",
1197
+ "const optionalParams: UltramodernCanonicalRoutes['/optional/{-$slug}'] = {};",
1198
+ '// @ts-expect-error localized physical variants do not become canonical keys',
1199
+ "declare const localizedParams: UltramodernCanonicalRoutes['/produkty/$slug'];",
1200
+ '// @ts-expect-error the deduplicated product route still requires its param',
1201
+ "const missingProductParams: UltramodernCanonicalRoutes['/products/$slug'] = {};",
1202
+ 'void productParams;',
1203
+ 'void optionalParams;',
1204
+ 'void localizedParams;',
1205
+ 'void missingProductParams;',
1206
+ ],
1207
+ projectDirectory: tempDir,
1208
+ });
884
1209
  });
885
1210
 
886
1211
  test('output is sorted alphabetically by canonical key', () => {
@@ -1,10 +0,0 @@
1
- declare module '@tanstack/react-router/ssr/client' {
2
- export function hydrate(router: unknown): Promise<unknown>;
3
- }
4
-
5
- declare module '@tanstack/react-router/ssr/server' {
6
- export function attachRouterServerSsrUtils(opts: {
7
- router: unknown;
8
- manifest?: unknown;
9
- }): void;
10
- }