@bleedingdev/modern-js-plugin-tanstack 3.5.0-ultramodern.102 → 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.
- package/dist/cjs/cli/index.js +4 -0
- package/dist/cjs/cli/tanstackTypes/source.js +7 -17
- package/dist/cjs/runtime/plugin.node.js +12 -5
- package/dist/cjs/runtime/pluginShared.js +3 -3
- package/dist/cjs/runtime/rsc/payloadErrors.js +2 -2
- package/dist/cjs/runtime/rsc/shared.js +1 -1
- package/dist/cjs/runtime/rsc/treeGetter.js +5 -9
- package/dist/cjs/runtime/submitAction.js +4 -5
- package/dist/esm/cli/index.mjs +4 -0
- package/dist/esm/cli/tanstackTypes/source.mjs +7 -7
- package/dist/esm/runtime/plugin.node.mjs +12 -5
- package/dist/esm/runtime/pluginShared.mjs +3 -3
- package/dist/esm/runtime/rsc/payloadErrors.mjs +1 -1
- package/dist/esm/runtime/rsc/shared.mjs +1 -1
- package/dist/esm/runtime/rsc/treeGetter.mjs +5 -9
- package/dist/esm/runtime/submitAction.mjs +4 -5
- package/dist/esm-node/cli/index.mjs +4 -0
- package/dist/esm-node/cli/tanstackTypes/source.mjs +7 -7
- package/dist/esm-node/runtime/plugin.node.mjs +12 -5
- package/dist/esm-node/runtime/pluginShared.mjs +3 -3
- package/dist/esm-node/runtime/rsc/payloadErrors.mjs +1 -1
- package/dist/esm-node/runtime/rsc/shared.mjs +1 -1
- package/dist/esm-node/runtime/rsc/treeGetter.mjs +5 -9
- package/dist/esm-node/runtime/submitAction.mjs +4 -5
- package/dist/types/runtime/clientHydration.d.ts +1 -1
- package/dist/types/runtime/ssrTypes.d.ts +0 -2
- package/package.json +26 -20
- package/rstest.config.mts +2 -0
- package/src/cli/index.ts +6 -0
- package/src/cli/tanstackTypes/canonicalRoutes.ts +0 -1
- package/src/cli/tanstackTypes/shared.ts +0 -1
- package/src/cli/tanstackTypes/source.ts +1 -1
- package/src/runtime/plugin.node.tsx +17 -11
- package/src/runtime/plugin.tsx +0 -2
- package/src/runtime/pluginShared.ts +8 -7
- package/src/runtime/rsc/payloadErrors.ts +1 -1
- package/src/runtime/rsc/shared.ts +1 -1
- package/src/runtime/rsc/treeGetter.tsx +9 -8
- package/src/runtime/ssrManagedTags.ts +0 -2
- package/src/runtime/ssrTypes.ts +0 -4
- package/src/runtime/submitAction.ts +6 -11
- package/src/types/react-server-dom-rspack.d.ts +4 -52
- package/tests/router/cli.test.ts +2 -0
- package/tests/router/clientHydration.test.tsx +140 -1
- package/tests/router/dataMutation.test.tsx +50 -9
- package/tests/router/fixtures/rscDecoderSecurityChild.cjs +162 -0
- package/tests/router/preloadRedirect.test.ts +3 -8
- package/tests/router/routeTree.test.ts +73 -19
- package/tests/router/rscDecoderSecurity.test.ts +113 -0
- package/tests/router/serverPlugin.test.ts +115 -0
- package/tests/router/tanstackTypes.test.ts +351 -26
- 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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
693
|
-
|
|
694
|
-
)
|
|
695
|
-
|
|
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'])
|
|
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!['/'
|
|
753
|
-
|
|
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'])
|
|
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}'])
|
|
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/$'])
|
|
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(
|
|
881
|
-
expect(
|
|
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
|
-
}
|