@bleedingdev/modern-js-plugin-tanstack 3.5.0-ultramodern.100 → 3.5.0-ultramodern.101
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/tanstackTypes/source.js +2 -3
- package/dist/esm/cli/tanstackTypes/source.mjs +2 -3
- package/dist/esm-node/cli/tanstackTypes/source.mjs +2 -3
- package/package.json +10 -9
- package/src/cli/tanstackTypes/source.ts +2 -3
- package/tests/router/cli.test.ts +200 -96
- package/tests/router/routerCompatibility.test.ts +0 -6
- package/tests/router/tanstackTypes.test.ts +333 -346
- package/tests/castRatchet.test.ts +0 -85
- package/tests/router/fixtures/router-gen.golden.txt +0 -71
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import { mkdir, mkdtemp,
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
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
|
|
120
|
-
await
|
|
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('
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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('
|
|
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/
|
|
249
|
-
'routes/page.tsx',
|
|
250
|
-
'routes/
|
|
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(
|
|
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
|
-
|
|
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: '
|
|
279
|
-
path: '
|
|
280
|
-
_component: '@/_/routes/
|
|
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: '
|
|
286
|
-
path: '
|
|
287
|
-
_component: '@/_/routes/
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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('
|
|
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
|
|
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
|
|
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('
|
|
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
|
|
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('
|
|
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
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
|