@bleedingdev/modern-js-plugin-tanstack 3.5.0-ultramodern.98 → 3.8.1-ultramodern.1
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 +10 -2
- package/dist/cjs/cli/tanstackTypes/source.js +9 -20
- 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 +10 -2
- package/dist/esm/cli/tanstackTypes/source.mjs +9 -10
- 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 +10 -2
- package/dist/esm-node/cli/tanstackTypes/source.mjs +9 -10
- 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 +27 -20
- package/rstest.config.mts +2 -0
- package/src/cli/index.ts +12 -0
- package/src/cli/tanstackTypes/canonicalRoutes.ts +0 -1
- package/src/cli/tanstackTypes/shared.ts +0 -1
- package/src/cli/tanstackTypes/source.ts +3 -4
- 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 +204 -96
- 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/routerCompatibility.test.ts +0 -6
- package/tests/router/rscDecoderSecurity.test.ts +113 -0
- package/tests/router/serverPlugin.test.ts +115 -0
- package/tests/router/tanstackTypes.test.ts +668 -356
- package/src/runtime/ssr-shim.d.ts +0 -10
- 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
|
|
|
@@ -30,18 +29,13 @@ rstest.mock('../../src/cli/tanstackTypes/shared', () => {
|
|
|
30
29
|
};
|
|
31
30
|
});
|
|
32
31
|
|
|
32
|
+
import { writeTanstackRegisterFile } from '../../src/cli/artifacts';
|
|
33
33
|
import {
|
|
34
34
|
collectCanonicalRoutesForEntry,
|
|
35
35
|
generateTanstackRouterTypesSourceForEntry,
|
|
36
36
|
} from '../../src/cli/tanstackTypes';
|
|
37
|
-
import { throwTanstackRedirect } from '../../src/runtime/loaderBridge';
|
|
38
37
|
|
|
39
38
|
const execFileAsync = promisify(execFile);
|
|
40
|
-
const routerGenGoldenPath = path.join(
|
|
41
|
-
__dirname,
|
|
42
|
-
'fixtures',
|
|
43
|
-
'router-gen.golden.txt',
|
|
44
|
-
);
|
|
45
39
|
const routerGenOxlintConfigPath = path.join(
|
|
46
40
|
__dirname,
|
|
47
41
|
'fixtures',
|
|
@@ -51,35 +45,361 @@ const oxlintCliPath = path.resolve(
|
|
|
51
45
|
__dirname,
|
|
52
46
|
'../../../../toolkit/code-tools/node_modules/oxlint/bin/oxlint',
|
|
53
47
|
);
|
|
48
|
+
const strictestTsconfigPath = path.resolve(
|
|
49
|
+
__dirname,
|
|
50
|
+
'../../node_modules/@tsconfig/strictest/tsconfig.json',
|
|
51
|
+
);
|
|
52
|
+
const installedReactRouterPath = path.resolve(
|
|
53
|
+
__dirname,
|
|
54
|
+
'../../node_modules/@tanstack/react-router',
|
|
55
|
+
);
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
async function writeRuntimeTestPackage(projectDirectory: string) {
|
|
58
|
+
const runtimePackageDirectory = path.join(
|
|
59
|
+
projectDirectory,
|
|
60
|
+
'node_modules',
|
|
61
|
+
'@modern-js',
|
|
62
|
+
'plugin-tanstack',
|
|
63
|
+
);
|
|
64
|
+
await mkdir(runtimePackageDirectory, { recursive: true });
|
|
65
|
+
await writeFile(
|
|
66
|
+
path.join(runtimePackageDirectory, 'package.json'),
|
|
67
|
+
JSON.stringify({
|
|
68
|
+
name: '@modern-js/plugin-tanstack',
|
|
69
|
+
type: 'commonjs',
|
|
70
|
+
exports: {
|
|
71
|
+
'./runtime': {
|
|
72
|
+
types: './runtime.d.ts',
|
|
73
|
+
default: './runtime.js',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
await writeFile(
|
|
79
|
+
path.join(runtimePackageDirectory, 'runtime.d.ts'),
|
|
80
|
+
[
|
|
81
|
+
'export type ModernRouterContext = { request?: Request; requestContext?: unknown };',
|
|
82
|
+
'export type ModernRouteStaticData = { modernRouteId?: string; modernRouteAction?: unknown; modernRouteLoader?: unknown };',
|
|
83
|
+
'export type RouteOptions = {',
|
|
84
|
+
' component?: unknown;',
|
|
85
|
+
' getParentRoute?: (...args: never[]) => unknown;',
|
|
86
|
+
' id?: string;',
|
|
87
|
+
' loader?: (...args: never[]) => unknown;',
|
|
88
|
+
' loaderDeps?: (...args: never[]) => unknown;',
|
|
89
|
+
' path?: string;',
|
|
90
|
+
' staticData?: ModernRouteStaticData;',
|
|
91
|
+
' validateSearch?: (...args: never[]) => unknown;',
|
|
92
|
+
'};',
|
|
93
|
+
'export type Route<TOptions extends RouteOptions = RouteOptions> = {',
|
|
94
|
+
' options: TOptions;',
|
|
95
|
+
' addChildren<const TChildren extends readonly unknown[]>(children: TChildren): Route<TOptions> & { children: TChildren };',
|
|
96
|
+
'};',
|
|
97
|
+
'export const runtimeState: { adapterCalls: Array<{ hasSplat: boolean; modernLoader: unknown }> };',
|
|
98
|
+
'export function createMemoryHistory<TOptions>(options: TOptions): TOptions;',
|
|
99
|
+
'export const modernTanstackRouterFastDefaults: { defaultPreload: "intent" };',
|
|
100
|
+
'export function createRootRouteWithContext<TContext extends ModernRouterContext>(): <const TOptions extends RouteOptions>(options: TOptions) => Route<TOptions>;',
|
|
101
|
+
'export function createRoute<const TOptions extends RouteOptions>(options: TOptions): Route<TOptions>;',
|
|
102
|
+
'export function createRouter<const TOptions extends { context: ModernRouterContext; history: unknown; routeTree: unknown }>(options: TOptions): TOptions;',
|
|
103
|
+
'export function createRouteStaticData<const TData extends ModernRouteStaticData>(data: TData): TData;',
|
|
104
|
+
'export function modernLoaderToTanstack<TLoader extends (...args: never[]) => unknown>(options: { hasSplat: boolean }, modernLoader: TLoader): (context: unknown) => Promise<Awaited<ReturnType<TLoader>>>;',
|
|
105
|
+
].join('\n'),
|
|
106
|
+
);
|
|
107
|
+
await writeFile(
|
|
108
|
+
path.join(runtimePackageDirectory, 'runtime.js'),
|
|
109
|
+
[
|
|
110
|
+
'const runtimeState = { adapterCalls: [] };',
|
|
111
|
+
'const createRouteRecord = options => ({',
|
|
112
|
+
' options,',
|
|
113
|
+
' addChildren(children) {',
|
|
114
|
+
' this.children = children;',
|
|
115
|
+
' return this;',
|
|
116
|
+
' },',
|
|
117
|
+
'});',
|
|
118
|
+
'const createMemoryHistory = options => options;',
|
|
119
|
+
'const modernTanstackRouterFastDefaults = { defaultPreload: "intent" };',
|
|
120
|
+
'const createRootRouteWithContext = () => options => createRouteRecord(options);',
|
|
121
|
+
'const createRoute = options => createRouteRecord(options);',
|
|
122
|
+
'const createRouter = options => options;',
|
|
123
|
+
'const createRouteStaticData = data => data;',
|
|
124
|
+
'const modernLoaderToTanstack = (options, modernLoader) => {',
|
|
125
|
+
' runtimeState.adapterCalls.push({ ...options, modernLoader });',
|
|
126
|
+
' return async context => modernLoader(context);',
|
|
127
|
+
'};',
|
|
128
|
+
'module.exports = { createMemoryHistory, createRootRouteWithContext, createRoute, createRouter, createRouteStaticData, modernLoaderToTanstack, modernTanstackRouterFastDefaults, runtimeState };',
|
|
129
|
+
].join('\n'),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
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
|
+
|
|
168
|
+
async function compileAndRunGeneratedRouter(options: {
|
|
169
|
+
projectDirectory: string;
|
|
170
|
+
routerGenTs: string;
|
|
171
|
+
runtimeCheck: string;
|
|
172
|
+
}) {
|
|
173
|
+
const { projectDirectory, routerGenTs, runtimeCheck } = options;
|
|
174
|
+
const generatedDirectory = path.join(
|
|
175
|
+
projectDirectory,
|
|
176
|
+
'src',
|
|
177
|
+
'modern-tanstack',
|
|
178
|
+
'index',
|
|
179
|
+
);
|
|
180
|
+
await mkdir(generatedDirectory, { recursive: true });
|
|
181
|
+
await writeFile(path.join(generatedDirectory, 'router.gen.ts'), routerGenTs);
|
|
182
|
+
await writeFile(
|
|
183
|
+
path.join(projectDirectory, 'src', 'runtime-check.ts'),
|
|
184
|
+
runtimeCheck,
|
|
185
|
+
);
|
|
186
|
+
await writeRuntimeTestPackage(projectDirectory);
|
|
187
|
+
await writeFile(
|
|
188
|
+
path.join(projectDirectory, 'package.json'),
|
|
189
|
+
JSON.stringify({ private: true, type: 'commonjs' }),
|
|
190
|
+
);
|
|
191
|
+
await writeFile(
|
|
192
|
+
path.join(projectDirectory, 'tsconfig.json'),
|
|
193
|
+
JSON.stringify(
|
|
194
|
+
{
|
|
195
|
+
extends: strictestTsconfigPath,
|
|
196
|
+
compilerOptions: {
|
|
197
|
+
lib: ['ESNext', 'DOM'],
|
|
198
|
+
jsx: 'react-jsx',
|
|
199
|
+
module: 'Node16',
|
|
200
|
+
moduleResolution: 'Node16',
|
|
201
|
+
outDir: 'dist',
|
|
202
|
+
rootDir: 'src',
|
|
203
|
+
target: 'ES2022',
|
|
204
|
+
types: [],
|
|
205
|
+
verbatimModuleSyntax: false,
|
|
206
|
+
},
|
|
207
|
+
include: ['src/**/*.ts', 'src/**/*.tsx'],
|
|
208
|
+
},
|
|
209
|
+
null,
|
|
210
|
+
2,
|
|
211
|
+
),
|
|
212
|
+
);
|
|
61
213
|
|
|
62
|
-
function catchThrown(fn: () => unknown): unknown {
|
|
63
214
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
215
|
+
await execFileAsync(
|
|
216
|
+
process.platform === 'win32' ? 'tsgo.cmd' : 'tsgo',
|
|
217
|
+
['-p', 'tsconfig.json'],
|
|
218
|
+
{
|
|
219
|
+
cwd: projectDirectory,
|
|
220
|
+
shell: process.platform === 'win32',
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
} catch (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
|
+
}
|
|
238
|
+
}
|
|
239
|
+
throw error;
|
|
67
240
|
}
|
|
68
|
-
|
|
241
|
+
await execFileAsync(
|
|
242
|
+
process.execPath,
|
|
243
|
+
[path.join(projectDirectory, 'dist', 'runtime-check.js')],
|
|
244
|
+
{ cwd: projectDirectory },
|
|
245
|
+
);
|
|
69
246
|
}
|
|
70
247
|
|
|
71
|
-
function
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
},
|
|
78
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,
|
|
79
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
|
+
}
|
|
80
400
|
}
|
|
81
401
|
|
|
82
|
-
async function
|
|
402
|
+
async function writeComprehensiveRouterFixture(srcDirectory: string) {
|
|
83
403
|
const files = new Map([
|
|
84
404
|
[
|
|
85
405
|
'routes/(app)/layout.tsx',
|
|
@@ -116,8 +436,8 @@ async function writeGoldenRouterFixture(srcDirectory: string) {
|
|
|
116
436
|
}
|
|
117
437
|
}
|
|
118
438
|
|
|
119
|
-
async function
|
|
120
|
-
await
|
|
439
|
+
async function generateComprehensiveRouterGen(srcDirectory: string) {
|
|
440
|
+
await writeComprehensiveRouterFixture(srcDirectory);
|
|
121
441
|
|
|
122
442
|
const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
|
|
123
443
|
appContext: {
|
|
@@ -173,7 +493,79 @@ describe('tanstack router type generation', () => {
|
|
|
173
493
|
}
|
|
174
494
|
});
|
|
175
495
|
|
|
176
|
-
test('
|
|
496
|
+
test('strictly compiles and executes a loader-free conventional route tree', async () => {
|
|
497
|
+
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
|
|
498
|
+
const srcDirectory = path.join(tempDir, 'src');
|
|
499
|
+
const files = new Map([
|
|
500
|
+
[
|
|
501
|
+
'routes/layout.tsx',
|
|
502
|
+
'export default function Layout() { return null; }',
|
|
503
|
+
],
|
|
504
|
+
['routes/page.tsx', 'export default function Home() { return null; }'],
|
|
505
|
+
[
|
|
506
|
+
'routes/reviews/[scanId]/page.tsx',
|
|
507
|
+
'export default function Review() { return null; }',
|
|
508
|
+
],
|
|
509
|
+
]);
|
|
510
|
+
|
|
511
|
+
for (const [relativePath, contents] of files) {
|
|
512
|
+
const filePath = path.join(srcDirectory, relativePath);
|
|
513
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
514
|
+
await writeFile(filePath, contents);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
|
|
518
|
+
appContext: {
|
|
519
|
+
srcDirectory,
|
|
520
|
+
internalSrcAlias: '@/_',
|
|
521
|
+
} as any,
|
|
522
|
+
entryName: 'index',
|
|
523
|
+
routes: [
|
|
524
|
+
{
|
|
525
|
+
type: 'nested',
|
|
526
|
+
id: 'layout',
|
|
527
|
+
isRoot: true,
|
|
528
|
+
_component: '@/_/routes/layout',
|
|
529
|
+
children: [
|
|
530
|
+
{
|
|
531
|
+
type: 'nested',
|
|
532
|
+
id: 'page',
|
|
533
|
+
index: true,
|
|
534
|
+
_component: '@/_/routes/page',
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
type: 'nested',
|
|
538
|
+
id: 'reviews/(scanId)/page',
|
|
539
|
+
path: 'reviews/:scanId',
|
|
540
|
+
_component: '@/_/routes/reviews/[scanId]/page',
|
|
541
|
+
},
|
|
542
|
+
],
|
|
543
|
+
},
|
|
544
|
+
] as any,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
await compileAndRunGeneratedRouter({
|
|
548
|
+
projectDirectory: tempDir,
|
|
549
|
+
routerGenTs,
|
|
550
|
+
runtimeCheck: [
|
|
551
|
+
"import { runtimeState } from '@modern-js/plugin-tanstack/runtime';",
|
|
552
|
+
"import Layout from './routes/layout';",
|
|
553
|
+
"import Home from './routes/page';",
|
|
554
|
+
"import Review from './routes/reviews/[scanId]/page';",
|
|
555
|
+
"import { rootRoute, routeTree, router } from './modern-tanstack/index/router.gen';",
|
|
556
|
+
'',
|
|
557
|
+
'if (runtimeState.adapterCalls.length !== 0) throw new Error("loader adapter registered without a loader");',
|
|
558
|
+
'if (Object.keys(router.context).length !== 0) throw new Error("router context is not empty");',
|
|
559
|
+
'if (rootRoute.options.component !== Layout) throw new Error("root component was not wired");',
|
|
560
|
+
'const [homeRoute, reviewRoute] = routeTree.children;',
|
|
561
|
+
'if (homeRoute.options.component !== Home) throw new Error("index component was not wired");',
|
|
562
|
+
'if (reviewRoute.options.component !== Review) throw new Error("review component was not wired");',
|
|
563
|
+
'if (reviewRoute.options.path !== "reviews/$scanId") throw new Error("route params were not translated");',
|
|
564
|
+
].join('\n'),
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
test('executes generated loader and action wiring through route static data', async () => {
|
|
177
569
|
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
|
|
178
570
|
const srcDirectory = path.join(tempDir, 'src');
|
|
179
571
|
const routeDir = path.join(srcDirectory, 'routes', 'mf');
|
|
@@ -210,50 +602,46 @@ describe('tanstack router type generation', () => {
|
|
|
210
602
|
] as any,
|
|
211
603
|
});
|
|
212
604
|
|
|
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();
|
|
605
|
+
await compileAndRunGeneratedRouter({
|
|
606
|
+
projectDirectory: tempDir,
|
|
607
|
+
routerGenTs,
|
|
608
|
+
runtimeCheck: [
|
|
609
|
+
"import { runtimeState } from '@modern-js/plugin-tanstack/runtime';",
|
|
610
|
+
"import { action, loader } from './routes/mf/page.data';",
|
|
611
|
+
"import { routeTree } from './modern-tanstack/index/router.gen';",
|
|
612
|
+
'',
|
|
613
|
+
'async function main() {',
|
|
614
|
+
'const [mfRoute] = routeTree.children;',
|
|
615
|
+
'if (runtimeState.adapterCalls.length !== 1) throw new Error("loader adapter was not registered exactly once");',
|
|
616
|
+
'if (runtimeState.adapterCalls[0]?.modernLoader !== loader) throw new Error("wrong loader was adapted");',
|
|
617
|
+
'const result = await mfRoute.options.loader({});',
|
|
618
|
+
'if (result.count !== 0) throw new Error("adapted loader returned the wrong value");',
|
|
619
|
+
'if (mfRoute.options.staticData.modernRouteLoader !== loader) throw new Error("loader static data was not wired");',
|
|
620
|
+
'if (mfRoute.options.staticData.modernRouteAction !== action) throw new Error("action static data was not wired");',
|
|
621
|
+
'}',
|
|
622
|
+
'void main();',
|
|
623
|
+
].join('\n'),
|
|
624
|
+
});
|
|
242
625
|
});
|
|
243
626
|
|
|
244
|
-
test('
|
|
627
|
+
test('executes component sharing correctly when module resolution races', async () => {
|
|
245
628
|
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
|
|
246
629
|
const srcDirectory = path.join(tempDir, 'src');
|
|
630
|
+
|
|
247
631
|
for (const componentFile of [
|
|
248
|
-
'routes/
|
|
249
|
-
'routes/page.tsx',
|
|
250
|
-
'routes/
|
|
632
|
+
'routes/slow-page.tsx',
|
|
633
|
+
'routes/fast-page.tsx',
|
|
634
|
+
'routes/shared-page.tsx',
|
|
251
635
|
]) {
|
|
252
636
|
const componentPath = path.join(srcDirectory, componentFile);
|
|
253
637
|
await mkdir(path.dirname(componentPath), { recursive: true });
|
|
254
|
-
await writeFile(
|
|
638
|
+
await writeFile(
|
|
639
|
+
componentPath,
|
|
640
|
+
'export default function Page() { return null; }',
|
|
641
|
+
);
|
|
255
642
|
}
|
|
256
643
|
|
|
644
|
+
resolverDelays.set('/routes/slow-page', 25);
|
|
257
645
|
const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
|
|
258
646
|
appContext: {
|
|
259
647
|
srcDirectory,
|
|
@@ -265,29 +653,32 @@ describe('tanstack router type generation', () => {
|
|
|
265
653
|
type: 'nested',
|
|
266
654
|
id: 'layout',
|
|
267
655
|
isRoot: true,
|
|
268
|
-
_component: '@/_/routes/layout',
|
|
269
656
|
children: [
|
|
270
657
|
{
|
|
271
658
|
type: 'nested',
|
|
272
|
-
id: 'page',
|
|
273
|
-
|
|
274
|
-
_component: '@/_/routes/page',
|
|
659
|
+
id: 'slow/page',
|
|
660
|
+
path: 'slow',
|
|
661
|
+
_component: '@/_/routes/slow-page',
|
|
275
662
|
},
|
|
276
663
|
{
|
|
277
664
|
type: 'nested',
|
|
278
|
-
id: '
|
|
279
|
-
path: '
|
|
280
|
-
_component: '@/_/routes/
|
|
665
|
+
id: 'fast/page',
|
|
666
|
+
path: 'fast',
|
|
667
|
+
_component: '@/_/routes/fast-page',
|
|
281
668
|
},
|
|
282
669
|
{
|
|
283
|
-
// Shares the page component module: the import must be reused.
|
|
284
670
|
type: 'nested',
|
|
285
|
-
id: '
|
|
286
|
-
path: '
|
|
287
|
-
_component: '@/_/routes/
|
|
671
|
+
id: 'shared-a/page',
|
|
672
|
+
path: 'shared-a',
|
|
673
|
+
_component: '@/_/routes/shared-page',
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
type: 'nested',
|
|
677
|
+
id: 'shared-b/page',
|
|
678
|
+
path: 'shared-b',
|
|
679
|
+
_component: '@/_/routes/shared-page',
|
|
288
680
|
},
|
|
289
681
|
{
|
|
290
|
-
// No _component: no component option may be emitted.
|
|
291
682
|
type: 'nested',
|
|
292
683
|
id: 'data-only/page',
|
|
293
684
|
path: 'data-only',
|
|
@@ -297,142 +688,73 @@ describe('tanstack router type generation', () => {
|
|
|
297
688
|
] as any,
|
|
298
689
|
});
|
|
299
690
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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:');
|
|
691
|
+
await compileAndRunGeneratedRouter({
|
|
692
|
+
projectDirectory: tempDir,
|
|
693
|
+
routerGenTs,
|
|
694
|
+
runtimeCheck: [
|
|
695
|
+
"import SlowPage from './routes/slow-page';",
|
|
696
|
+
"import FastPage from './routes/fast-page';",
|
|
697
|
+
"import SharedPage from './routes/shared-page';",
|
|
698
|
+
"import { routeTree } from './modern-tanstack/index/router.gen';",
|
|
699
|
+
'',
|
|
700
|
+
'const [slowRoute, fastRoute, sharedA, sharedB, dataOnly] = routeTree.children;',
|
|
701
|
+
'if (slowRoute.options.component !== SlowPage) throw new Error("slow component was crossed during resolution");',
|
|
702
|
+
'if (fastRoute.options.component !== FastPage) throw new Error("fast component was crossed during resolution");',
|
|
703
|
+
'if (sharedA.options.component !== SharedPage || sharedB.options.component !== SharedPage) throw new Error("shared component identity was not preserved");',
|
|
704
|
+
'if (Reflect.has(dataOnly.options, "component")) throw new Error("data-only route gained a component");',
|
|
705
|
+
].join('\n'),
|
|
706
|
+
});
|
|
330
707
|
});
|
|
331
708
|
|
|
332
|
-
test('
|
|
709
|
+
test('strictly compiles and executes nested loader and search contracts', async () => {
|
|
333
710
|
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
|
|
334
711
|
const srcDirectory = path.join(tempDir, 'src');
|
|
712
|
+
const routerGenTs = await generateComprehensiveRouterGen(srcDirectory);
|
|
335
713
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
'
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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,
|
|
714
|
+
await compileAndRunGeneratedRouter({
|
|
715
|
+
projectDirectory: tempDir,
|
|
716
|
+
routerGenTs,
|
|
717
|
+
runtimeCheck: [
|
|
718
|
+
"import { runtimeState } from '@modern-js/plugin-tanstack/runtime';",
|
|
719
|
+
"import { action, loader } from './routes/(app)/users/(userId)/page.data';",
|
|
720
|
+
"import { loaderDeps, validateSearch } from './routes/search.contract';",
|
|
721
|
+
"import { routeTree } from './modern-tanstack/index/router.gen';",
|
|
722
|
+
'',
|
|
723
|
+
'async function main() {',
|
|
724
|
+
'const [appRoute] = routeTree.children;',
|
|
725
|
+
'const [userRoute, docsRoute] = appRoute.children;',
|
|
726
|
+
'if (runtimeState.adapterCalls.length !== 1) throw new Error("nested loader was not adapted");',
|
|
727
|
+
'if (userRoute.options.validateSearch !== validateSearch) throw new Error("search validator was not wired");',
|
|
728
|
+
'if (userRoute.options.loaderDeps !== loaderDeps) throw new Error("loader dependencies were not wired");',
|
|
729
|
+
'if (userRoute.options.staticData.modernRouteLoader !== loader) throw new Error("nested static loader was not wired");',
|
|
730
|
+
'if (userRoute.options.staticData.modernRouteAction !== action) throw new Error("nested static action was not wired");',
|
|
731
|
+
'const user = await userRoute.options.loader({});',
|
|
732
|
+
'if (user.userId !== "42") throw new Error("nested loader result was lost");',
|
|
733
|
+
'if (userRoute.options.path !== "users/$userId") throw new Error("required route param was not translated");',
|
|
734
|
+
'if (docsRoute.options.path !== "docs/$") throw new Error("splat route was not translated");',
|
|
735
|
+
'const search = userRoute.options.validateSearch({});',
|
|
736
|
+
'if (search.tab !== "overview") throw new Error("search contract returned the wrong value");',
|
|
737
|
+
'}',
|
|
738
|
+
'void main();',
|
|
739
|
+
].join('\n'),
|
|
406
740
|
});
|
|
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
741
|
});
|
|
416
742
|
|
|
417
|
-
test('
|
|
743
|
+
test('strictly compiles generated routes against installed router declarations', async () => {
|
|
418
744
|
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
|
|
419
745
|
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
|
-
}
|
|
746
|
+
const routerGenTs = await generateComprehensiveRouterGen(srcDirectory);
|
|
426
747
|
|
|
427
|
-
await
|
|
748
|
+
await compileGeneratedRouterAgainstInstalledDeclarations({
|
|
749
|
+
projectDirectory: tempDir,
|
|
428
750
|
routerGenTs,
|
|
429
|
-
);
|
|
751
|
+
});
|
|
430
752
|
});
|
|
431
753
|
|
|
432
|
-
test('
|
|
754
|
+
test('passes the generated-artifact lint contract', async () => {
|
|
433
755
|
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
|
|
434
756
|
const srcDirectory = path.join(tempDir, 'src');
|
|
435
|
-
const routerGenTs = await
|
|
757
|
+
const routerGenTs = await generateComprehensiveRouterGen(srcDirectory);
|
|
436
758
|
const routerGenPath = path.join(
|
|
437
759
|
srcDirectory,
|
|
438
760
|
'modern-tanstack',
|
|
@@ -443,8 +765,6 @@ describe('tanstack router type generation', () => {
|
|
|
443
765
|
await mkdir(path.dirname(routerGenPath), { recursive: true });
|
|
444
766
|
await writeFile(routerGenPath, routerGenTs);
|
|
445
767
|
|
|
446
|
-
expect(routerGenTs).not.toContain('eslint-disable');
|
|
447
|
-
|
|
448
768
|
const { stderr, stdout } = await execFileAsync(
|
|
449
769
|
process.execPath,
|
|
450
770
|
[
|
|
@@ -464,130 +784,7 @@ describe('tanstack router type generation', () => {
|
|
|
464
784
|
expect(`${stdout}${stderr}`).toBe('');
|
|
465
785
|
});
|
|
466
786
|
|
|
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 () => {
|
|
787
|
+
test('executes localized aliases with typed children and correct parents', async () => {
|
|
591
788
|
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
|
|
592
789
|
const srcDirectory = path.join(tempDir, 'src');
|
|
593
790
|
|
|
@@ -630,25 +827,36 @@ describe('tanstack router type generation', () => {
|
|
|
630
827
|
] as any,
|
|
631
828
|
});
|
|
632
829
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
830
|
+
await compileAndRunGeneratedRouter({
|
|
831
|
+
projectDirectory: tempDir,
|
|
832
|
+
routerGenTs,
|
|
833
|
+
runtimeCheck: [
|
|
834
|
+
"import { routeTree } from './modern-tanstack/index/router.gen';",
|
|
835
|
+
'',
|
|
836
|
+
'const [localeRoute] = routeTree.children;',
|
|
837
|
+
'const [productRoute, localizedProductRoute, optionalRoute] = localeRoute.children;',
|
|
838
|
+
'if (localeRoute.options.path !== "$lang") throw new Error("locale param was not translated");',
|
|
839
|
+
'if (productRoute.options.path !== "products/$slug") throw new Error("product param was not translated");',
|
|
840
|
+
'if (localizedProductRoute.options.path !== "produkty/$slug") throw new Error("localized param was not translated");',
|
|
841
|
+
'if (optionalRoute.options.path !== "volitelne/{-$slug}") throw new Error("optional param was not translated");',
|
|
842
|
+
'for (const child of localeRoute.children) {',
|
|
843
|
+
' if (child.options.getParentRoute() !== localeRoute) throw new Error("localized child points at the wrong parent");',
|
|
844
|
+
'}',
|
|
845
|
+
].join('\n'),
|
|
846
|
+
});
|
|
648
847
|
});
|
|
649
848
|
});
|
|
650
849
|
|
|
651
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
|
+
|
|
652
860
|
test('returns null for a route tree with no locale param and no canonical metadata', () => {
|
|
653
861
|
const result = collectCanonicalRoutesForEntry([
|
|
654
862
|
{
|
|
@@ -673,7 +881,7 @@ describe('collectCanonicalRoutesForEntry', () => {
|
|
|
673
881
|
expect(result).toBeNull();
|
|
674
882
|
});
|
|
675
883
|
|
|
676
|
-
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 () => {
|
|
677
885
|
const routes = [
|
|
678
886
|
{
|
|
679
887
|
type: 'nested',
|
|
@@ -702,14 +910,27 @@ describe('collectCanonicalRoutesForEntry', () => {
|
|
|
702
910
|
collectCanonicalRoutesForEntry(routes, { localeParamHeuristic: false }),
|
|
703
911
|
).toBeNull();
|
|
704
912
|
// With plugin-i18n installed the heuristic strips the locale prefix.
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
)
|
|
708
|
-
|
|
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,
|
|
709
930
|
});
|
|
710
931
|
});
|
|
711
932
|
|
|
712
|
-
test('still honors modernCanonicalPath metadata when the heuristic is disabled', () => {
|
|
933
|
+
test('still honors modernCanonicalPath metadata when the heuristic is disabled', async () => {
|
|
713
934
|
const result = collectCanonicalRoutesForEntry(
|
|
714
935
|
[
|
|
715
936
|
{
|
|
@@ -730,10 +951,26 @@ describe('collectCanonicalRoutesForEntry', () => {
|
|
|
730
951
|
);
|
|
731
952
|
|
|
732
953
|
expect(result).not.toBeNull();
|
|
733
|
-
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
|
+
});
|
|
734
971
|
});
|
|
735
972
|
|
|
736
|
-
test('strips leading :lang param and maps index under :lang to "/"', () => {
|
|
973
|
+
test('strips leading :lang param and maps index under :lang to "/"', async () => {
|
|
737
974
|
const result = collectCanonicalRoutesForEntry([
|
|
738
975
|
{
|
|
739
976
|
type: 'nested',
|
|
@@ -762,11 +999,25 @@ describe('collectCanonicalRoutesForEntry', () => {
|
|
|
762
999
|
] as any);
|
|
763
1000
|
|
|
764
1001
|
expect(result).not.toBeNull();
|
|
765
|
-
expect(result!['/'
|
|
766
|
-
|
|
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
|
+
});
|
|
767
1018
|
});
|
|
768
1019
|
|
|
769
|
-
test('converts :slug to $slug with required params type', () => {
|
|
1020
|
+
test('converts :slug to $slug with required params type', async () => {
|
|
770
1021
|
const result = collectCanonicalRoutesForEntry([
|
|
771
1022
|
{
|
|
772
1023
|
type: 'nested',
|
|
@@ -790,10 +1041,23 @@ describe('collectCanonicalRoutesForEntry', () => {
|
|
|
790
1041
|
] as any);
|
|
791
1042
|
|
|
792
1043
|
expect(result).not.toBeNull();
|
|
793
|
-
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
|
+
});
|
|
794
1058
|
});
|
|
795
1059
|
|
|
796
|
-
test('converts :slug? to optional {-$slug} with optional params type', () => {
|
|
1060
|
+
test('converts :slug? to optional {-$slug} with optional params type', async () => {
|
|
797
1061
|
const result = collectCanonicalRoutesForEntry([
|
|
798
1062
|
{
|
|
799
1063
|
type: 'nested',
|
|
@@ -817,10 +1081,25 @@ describe('collectCanonicalRoutesForEntry', () => {
|
|
|
817
1081
|
] as any);
|
|
818
1082
|
|
|
819
1083
|
expect(result).not.toBeNull();
|
|
820
|
-
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
|
+
});
|
|
821
1100
|
});
|
|
822
1101
|
|
|
823
|
-
test('converts * splat to $ with optional _splat param', () => {
|
|
1102
|
+
test('converts * splat to $ with optional _splat param', async () => {
|
|
824
1103
|
const result = collectCanonicalRoutesForEntry([
|
|
825
1104
|
{
|
|
826
1105
|
type: 'nested',
|
|
@@ -844,10 +1123,25 @@ describe('collectCanonicalRoutesForEntry', () => {
|
|
|
844
1123
|
] as any);
|
|
845
1124
|
|
|
846
1125
|
expect(result).not.toBeNull();
|
|
847
|
-
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
|
+
});
|
|
848
1142
|
});
|
|
849
1143
|
|
|
850
|
-
test('collapses localized variants with shared modernCanonicalPath to one canonical key', () => {
|
|
1144
|
+
test('collapses localized variants with shared modernCanonicalPath to one canonical key', async () => {
|
|
851
1145
|
// This reuses the same fixture shape as the 'preserves typed child trees'
|
|
852
1146
|
// test but adds modernCanonicalPath fields as plugin-i18n now emits.
|
|
853
1147
|
const result = collectCanonicalRoutesForEntry([
|
|
@@ -890,10 +1184,28 @@ describe('collectCanonicalRoutesForEntry', () => {
|
|
|
890
1184
|
const keys = Object.keys(result!);
|
|
891
1185
|
// /products/$slug and /optional/{-$slug} — exactly 2 keys with params
|
|
892
1186
|
expect(keys.filter(k => k.startsWith('/products'))).toHaveLength(1);
|
|
893
|
-
expect(
|
|
894
|
-
expect(
|
|
1187
|
+
expect(keys).toHaveLength(2);
|
|
1188
|
+
expect(keys).toEqual(['/optional/{-$slug}', '/products/$slug']);
|
|
895
1189
|
// The Czech localized path must not appear as a separate key.
|
|
896
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
|
+
});
|
|
897
1209
|
});
|
|
898
1210
|
|
|
899
1211
|
test('output is sorted alphabetically by canonical key', () => {
|