@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.5 → 3.9.0-ultramodern.7
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/runtime/register.js +0 -2
- package/dist/esm/runtime/register.mjs +0 -2
- package/dist/esm-node/runtime/register.mjs +0 -2
- package/dist/types/runtime/register.d.ts +0 -8
- package/package.json +12 -12
- package/rstest.config.mts +0 -18
- package/src/runtime/register.ts +0 -11
- package/tests/router/cli.test.ts +85 -860
- package/tests/router/dataMutation.test.tsx +36 -0
- package/tests/router/generateRouteArtifacts.test.ts +11 -11
- package/tests/router/loaderBridge.test.ts +52 -0
- package/tests/router/prefetchLink.test.tsx +0 -16
- package/tests/router/prefetchLinkPreload.test.tsx +12 -25
- package/tests/router/routeTree.test.ts +144 -624
- package/tests/router/rsc.test.tsx +14 -31
- package/tests/router/tanstackTypes.test.ts +92 -616
- package/tests/realm-isolation.test.ts +0 -120
- package/tests/router/fixtures/legacyRuntimeRouter.ts +0 -10
- package/tests/router/fixtures/rscDecoderSecurityChild.cjs +0 -162
- package/tests/router/hooks.test.ts +0 -23
- package/tests/router/register.test.ts +0 -8
- package/tests/router/routerCompatibility.test.ts +0 -262
- package/tests/router/rscDecoderSecurity.test.ts +0 -84
|
@@ -4,31 +4,6 @@ import { tmpdir } from 'node:os';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
|
|
7
|
-
const resolverDelays = new Map<string, number>();
|
|
8
|
-
|
|
9
|
-
rstest.mock('../../src/cli/tanstackTypes/shared', () => {
|
|
10
|
-
const actual = rstest.requireActual(
|
|
11
|
-
'../../src/cli/tanstackTypes/shared',
|
|
12
|
-
) as typeof import('../../src/cli/tanstackTypes/shared');
|
|
13
|
-
|
|
14
|
-
return {
|
|
15
|
-
...actual,
|
|
16
|
-
resolveFileNoExt: async (inputNoExtPath: string) => {
|
|
17
|
-
const normalized = inputNoExtPath.replaceAll('\\', '/');
|
|
18
|
-
const delay =
|
|
19
|
-
[...resolverDelays.entries()].find(([suffix]) =>
|
|
20
|
-
normalized.endsWith(suffix),
|
|
21
|
-
)?.[1] ?? 0;
|
|
22
|
-
|
|
23
|
-
if (delay > 0) {
|
|
24
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return actual.resolveFileNoExt(inputNoExtPath);
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
});
|
|
31
|
-
|
|
32
7
|
import { writeTanstackRegisterFile } from '../../src/cli/artifacts';
|
|
33
8
|
import {
|
|
34
9
|
collectCanonicalRoutesForEntry,
|
|
@@ -45,195 +20,51 @@ const installedReactRouterPath = path.resolve(
|
|
|
45
20
|
'../../node_modules/@tanstack/react-router',
|
|
46
21
|
);
|
|
47
22
|
|
|
48
|
-
async function
|
|
49
|
-
const runtimePackageDirectory = path.join(
|
|
50
|
-
projectDirectory,
|
|
51
|
-
'node_modules',
|
|
52
|
-
'@modern-js',
|
|
53
|
-
'plugin-tanstack',
|
|
54
|
-
);
|
|
55
|
-
await mkdir(runtimePackageDirectory, { recursive: true });
|
|
56
|
-
await writeFile(
|
|
57
|
-
path.join(runtimePackageDirectory, 'package.json'),
|
|
58
|
-
JSON.stringify({
|
|
59
|
-
name: '@modern-js/plugin-tanstack',
|
|
60
|
-
type: 'commonjs',
|
|
61
|
-
exports: {
|
|
62
|
-
'./runtime': {
|
|
63
|
-
types: './runtime.d.ts',
|
|
64
|
-
default: './runtime.js',
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
}),
|
|
68
|
-
);
|
|
69
|
-
await writeFile(
|
|
70
|
-
path.join(runtimePackageDirectory, 'runtime.d.ts'),
|
|
71
|
-
[
|
|
72
|
-
'export type ModernRouterContext = { request?: Request; requestContext?: unknown };',
|
|
73
|
-
'export type ModernRouteStaticData = { modernRouteId?: string; modernRouteAction?: unknown; modernRouteLoader?: unknown };',
|
|
74
|
-
'export type RouteOptions = {',
|
|
75
|
-
' component?: unknown;',
|
|
76
|
-
' getParentRoute?: (...args: never[]) => unknown;',
|
|
77
|
-
' id?: string;',
|
|
78
|
-
' loader?: (...args: never[]) => unknown;',
|
|
79
|
-
' loaderDeps?: (...args: never[]) => unknown;',
|
|
80
|
-
' path?: string;',
|
|
81
|
-
' staticData?: ModernRouteStaticData;',
|
|
82
|
-
' validateSearch?: (...args: never[]) => unknown;',
|
|
83
|
-
'};',
|
|
84
|
-
'export type Route<TOptions extends RouteOptions = RouteOptions> = {',
|
|
85
|
-
' options: TOptions;',
|
|
86
|
-
' addChildren<const TChildren extends readonly unknown[]>(children: TChildren): Route<TOptions> & { children: TChildren };',
|
|
87
|
-
'};',
|
|
88
|
-
'export const runtimeState: { adapterCalls: Array<{ hasSplat: boolean; modernLoader: unknown }> };',
|
|
89
|
-
'export function createMemoryHistory<TOptions>(options: TOptions): TOptions;',
|
|
90
|
-
'export const modernTanstackRouterFastDefaults: { readonly defaultStructuralSharing: true };',
|
|
91
|
-
'export function createRootRouteWithContext<TContext extends ModernRouterContext>(): <const TOptions extends RouteOptions>(options: TOptions) => Route<TOptions>;',
|
|
92
|
-
'export function createRoute<const TOptions extends RouteOptions>(options: TOptions): Route<TOptions>;',
|
|
93
|
-
'export function createRouter<const TOptions extends { context: ModernRouterContext; history: unknown; routeTree: unknown }>(options: TOptions): TOptions;',
|
|
94
|
-
'export function createRouteStaticData<const TData extends ModernRouteStaticData>(data: TData): TData;',
|
|
95
|
-
'export function modernLoaderToTanstack<TLoader extends (...args: never[]) => unknown>(options: { hasSplat: boolean }, modernLoader: TLoader): (context: unknown) => Promise<Awaited<ReturnType<TLoader>>>;',
|
|
96
|
-
].join('\n'),
|
|
97
|
-
);
|
|
98
|
-
await writeFile(
|
|
99
|
-
path.join(runtimePackageDirectory, 'runtime.js'),
|
|
100
|
-
[
|
|
101
|
-
'const runtimeState = { adapterCalls: [] };',
|
|
102
|
-
'const createRouteRecord = options => ({',
|
|
103
|
-
' options,',
|
|
104
|
-
' addChildren(children) {',
|
|
105
|
-
' this.children = children;',
|
|
106
|
-
' return this;',
|
|
107
|
-
' },',
|
|
108
|
-
'});',
|
|
109
|
-
'const createMemoryHistory = options => options;',
|
|
110
|
-
'const modernTanstackRouterFastDefaults = { defaultStructuralSharing: true };',
|
|
111
|
-
'const createRootRouteWithContext = () => options => createRouteRecord(options);',
|
|
112
|
-
'const createRoute = options => createRouteRecord(options);',
|
|
113
|
-
'const createRouter = options => options;',
|
|
114
|
-
'const createRouteStaticData = data => data;',
|
|
115
|
-
'const modernLoaderToTanstack = (options, modernLoader) => {',
|
|
116
|
-
' runtimeState.adapterCalls.push({ ...options, modernLoader });',
|
|
117
|
-
' return async context => modernLoader(context);',
|
|
118
|
-
'};',
|
|
119
|
-
'module.exports = { createMemoryHistory, createRootRouteWithContext, createRoute, createRouter, createRouteStaticData, modernLoaderToTanstack, modernTanstackRouterFastDefaults, runtimeState };',
|
|
120
|
-
].join('\n'),
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function writeInstalledRouterRuntimeTestPackage(
|
|
23
|
+
async function writeTsconfig(
|
|
125
24
|
projectDirectory: string,
|
|
25
|
+
compilerOptions: Record<string, unknown>,
|
|
126
26
|
) {
|
|
127
|
-
const runtimePackageDirectory = path.join(
|
|
128
|
-
projectDirectory,
|
|
129
|
-
'node_modules',
|
|
130
|
-
'@modern-js',
|
|
131
|
-
'plugin-tanstack',
|
|
132
|
-
);
|
|
133
|
-
await mkdir(runtimePackageDirectory, { recursive: true });
|
|
134
|
-
await writeFile(
|
|
135
|
-
path.join(runtimePackageDirectory, 'package.json'),
|
|
136
|
-
JSON.stringify({
|
|
137
|
-
name: '@modern-js/plugin-tanstack',
|
|
138
|
-
type: 'commonjs',
|
|
139
|
-
exports: {
|
|
140
|
-
'./runtime': {
|
|
141
|
-
types: './runtime.d.ts',
|
|
142
|
-
default: './runtime.js',
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
}),
|
|
146
|
-
);
|
|
147
|
-
await writeFile(
|
|
148
|
-
path.join(runtimePackageDirectory, 'runtime.d.ts'),
|
|
149
|
-
[
|
|
150
|
-
"export { createMemoryHistory, createRootRouteWithContext, createRoute, createRouter } from '@tanstack/react-router';",
|
|
151
|
-
'export type ModernRouterContext = { request?: Request; requestContext?: unknown };',
|
|
152
|
-
'export const modernTanstackRouterFastDefaults: { readonly defaultStructuralSharing: true };',
|
|
153
|
-
'export function createRouteStaticData<TData extends { modernRouteId?: string; modernRouteAction?: unknown; modernRouteLoader?: unknown }>(data: TData): TData;',
|
|
154
|
-
'export function modernLoaderToTanstack<TLoader extends (...args: any[]) => any>(options: { hasSplat: boolean }, modernLoader: TLoader): (context: unknown) => Promise<Awaited<ReturnType<TLoader>>>;',
|
|
155
|
-
].join('\n'),
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function compileAndRunGeneratedRouter(options: {
|
|
160
|
-
projectDirectory: string;
|
|
161
|
-
routerGenTs: string;
|
|
162
|
-
runtimeCheck: string;
|
|
163
|
-
}) {
|
|
164
|
-
const { projectDirectory, routerGenTs, runtimeCheck } = options;
|
|
165
|
-
const generatedDirectory = path.join(
|
|
166
|
-
projectDirectory,
|
|
167
|
-
'src',
|
|
168
|
-
'modern-tanstack',
|
|
169
|
-
'index',
|
|
170
|
-
);
|
|
171
|
-
await mkdir(generatedDirectory, { recursive: true });
|
|
172
|
-
await writeFile(path.join(generatedDirectory, 'router.gen.ts'), routerGenTs);
|
|
173
|
-
await writeFile(
|
|
174
|
-
path.join(projectDirectory, 'src', 'runtime-check.ts'),
|
|
175
|
-
runtimeCheck,
|
|
176
|
-
);
|
|
177
|
-
await writeRuntimeTestPackage(projectDirectory);
|
|
178
|
-
await writeFile(
|
|
179
|
-
path.join(projectDirectory, 'package.json'),
|
|
180
|
-
JSON.stringify({ private: true, type: 'commonjs' }),
|
|
181
|
-
);
|
|
182
27
|
await writeFile(
|
|
183
28
|
path.join(projectDirectory, 'tsconfig.json'),
|
|
184
29
|
JSON.stringify(
|
|
185
30
|
{
|
|
186
31
|
extends: strictestTsconfigPath,
|
|
187
32
|
compilerOptions: {
|
|
188
|
-
lib: ['ESNext', 'DOM'],
|
|
189
33
|
jsx: 'react-jsx',
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
rootDir: 'src',
|
|
194
|
-
target: 'ES2022',
|
|
34
|
+
lib: ['ESNext', 'DOM'],
|
|
35
|
+
noEmit: true,
|
|
36
|
+
target: 'ESNext',
|
|
195
37
|
types: [],
|
|
196
|
-
|
|
38
|
+
...compilerOptions,
|
|
197
39
|
},
|
|
198
|
-
include: ['src/**/*.ts', 'src/**/*.tsx'],
|
|
40
|
+
include: ['src/**/*.ts', 'src/**/*.tsx', 'src/**/*.d.ts'],
|
|
199
41
|
},
|
|
200
42
|
null,
|
|
201
43
|
2,
|
|
202
44
|
),
|
|
203
45
|
);
|
|
46
|
+
}
|
|
204
47
|
|
|
48
|
+
/** Runs the same compiler a consumer build runs; surfaces its diagnostics. */
|
|
49
|
+
async function runTsgo(projectDirectory: string) {
|
|
205
50
|
try {
|
|
206
51
|
await execFileAsync(
|
|
207
52
|
process.platform === 'win32' ? 'tsgo.cmd' : 'tsgo',
|
|
208
53
|
['-p', 'tsconfig.json'],
|
|
209
|
-
{
|
|
210
|
-
cwd: projectDirectory,
|
|
211
|
-
shell: process.platform === 'win32',
|
|
212
|
-
},
|
|
54
|
+
{ cwd: projectDirectory, shell: process.platform === 'win32' },
|
|
213
55
|
);
|
|
214
56
|
} catch (error) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
: '';
|
|
224
|
-
if (stdout || stderr) {
|
|
225
|
-
throw new Error([stdout, stderr].filter(Boolean).join('\n'), {
|
|
226
|
-
cause: error,
|
|
227
|
-
});
|
|
228
|
-
}
|
|
57
|
+
const { stdout, stderr } = (error ?? {}) as {
|
|
58
|
+
stdout?: string;
|
|
59
|
+
stderr?: string;
|
|
60
|
+
};
|
|
61
|
+
if (stdout || stderr) {
|
|
62
|
+
throw new Error([stdout, stderr].filter(Boolean).join('\n'), {
|
|
63
|
+
cause: error,
|
|
64
|
+
});
|
|
229
65
|
}
|
|
230
66
|
throw error;
|
|
231
67
|
}
|
|
232
|
-
await execFileAsync(
|
|
233
|
-
process.execPath,
|
|
234
|
-
[path.join(projectDirectory, 'dist', 'runtime-check.js')],
|
|
235
|
-
{ cwd: projectDirectory },
|
|
236
|
-
);
|
|
237
68
|
}
|
|
238
69
|
|
|
239
70
|
async function compileGeneratedRouterAgainstInstalledDeclarations(options: {
|
|
@@ -249,63 +80,48 @@ async function compileGeneratedRouterAgainstInstalledDeclarations(options: {
|
|
|
249
80
|
);
|
|
250
81
|
await mkdir(generatedDirectory, { recursive: true });
|
|
251
82
|
await writeFile(path.join(generatedDirectory, 'router.gen.ts'), routerGenTs);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
83
|
+
|
|
84
|
+
// The runtime package re-exports the *installed* @tanstack/react-router
|
|
85
|
+
// declarations, so the generated file is checked against the real router
|
|
86
|
+
// types a consumer resolves — not a stand-in.
|
|
87
|
+
const runtimePackageDirectory = path.join(
|
|
88
|
+
projectDirectory,
|
|
89
|
+
'node_modules',
|
|
90
|
+
'@modern-js',
|
|
91
|
+
'plugin-tanstack',
|
|
256
92
|
);
|
|
93
|
+
await mkdir(runtimePackageDirectory, { recursive: true });
|
|
257
94
|
await writeFile(
|
|
258
|
-
path.join(
|
|
259
|
-
JSON.stringify(
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
lib: ['ESNext', 'DOM'],
|
|
265
|
-
module: 'Node16',
|
|
266
|
-
moduleResolution: 'Node16',
|
|
267
|
-
noEmit: true,
|
|
268
|
-
paths: {
|
|
269
|
-
'@tanstack/react-router': [installedReactRouterPath],
|
|
270
|
-
},
|
|
271
|
-
target: 'ES2022',
|
|
272
|
-
types: [],
|
|
273
|
-
verbatimModuleSyntax: false,
|
|
274
|
-
},
|
|
275
|
-
include: ['src/**/*.ts', 'src/**/*.tsx'],
|
|
95
|
+
path.join(runtimePackageDirectory, 'package.json'),
|
|
96
|
+
JSON.stringify({
|
|
97
|
+
name: '@modern-js/plugin-tanstack',
|
|
98
|
+
type: 'commonjs',
|
|
99
|
+
exports: {
|
|
100
|
+
'./runtime': { types: './runtime.d.ts', default: './runtime.js' },
|
|
276
101
|
},
|
|
277
|
-
|
|
278
|
-
2,
|
|
279
|
-
),
|
|
102
|
+
}),
|
|
280
103
|
);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
{
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
throw new Error([stdout, stderr].filter(Boolean).join('\n'), {
|
|
303
|
-
cause: error,
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
throw error;
|
|
308
|
-
}
|
|
104
|
+
await writeFile(
|
|
105
|
+
path.join(runtimePackageDirectory, 'runtime.d.ts'),
|
|
106
|
+
[
|
|
107
|
+
"export { createMemoryHistory, createRootRouteWithContext, createRoute, createRouter } from '@tanstack/react-router';",
|
|
108
|
+
'export type ModernRouterContext = { request?: Request; requestContext?: unknown };',
|
|
109
|
+
'export const modernTanstackRouterFastDefaults: { readonly defaultStructuralSharing: true };',
|
|
110
|
+
'export function createRouteStaticData<TData extends { modernRouteId?: string; modernRouteAction?: unknown; modernRouteLoader?: unknown }>(data: TData): TData;',
|
|
111
|
+
'export function modernLoaderToTanstack<TLoader extends (...args: any[]) => any>(options: { hasSplat: boolean }, modernLoader: TLoader): (context: unknown) => Promise<Awaited<ReturnType<TLoader>>>;',
|
|
112
|
+
].join('\n'),
|
|
113
|
+
);
|
|
114
|
+
await writeFile(
|
|
115
|
+
path.join(projectDirectory, 'package.json'),
|
|
116
|
+
JSON.stringify({ private: true, type: 'commonjs' }),
|
|
117
|
+
);
|
|
118
|
+
await writeTsconfig(projectDirectory, {
|
|
119
|
+
module: 'Node16',
|
|
120
|
+
moduleResolution: 'Node16',
|
|
121
|
+
paths: { '@tanstack/react-router': [installedReactRouterPath] },
|
|
122
|
+
verbatimModuleSyntax: false,
|
|
123
|
+
});
|
|
124
|
+
await runTsgo(projectDirectory);
|
|
309
125
|
}
|
|
310
126
|
|
|
311
127
|
async function typecheckCanonicalRegisterContract(options: {
|
|
@@ -315,11 +131,11 @@ async function typecheckCanonicalRegisterContract(options: {
|
|
|
315
131
|
}) {
|
|
316
132
|
const { canonicalRoutes, contractLines, projectDirectory } = options;
|
|
317
133
|
const srcDirectory = path.join(projectDirectory, 'src');
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
134
|
+
await mkdir(path.join(srcDirectory, 'modern-tanstack', 'index'), {
|
|
135
|
+
recursive: true,
|
|
136
|
+
});
|
|
321
137
|
await writeFile(
|
|
322
|
-
path.join(
|
|
138
|
+
path.join(srcDirectory, 'modern-tanstack', 'index', 'router.gen.ts'),
|
|
323
139
|
'export const router = { context: {} };',
|
|
324
140
|
);
|
|
325
141
|
await writeTanstackRegisterFile({
|
|
@@ -341,56 +157,14 @@ async function typecheckCanonicalRegisterContract(options: {
|
|
|
341
157
|
...contractLines,
|
|
342
158
|
].join('\n'),
|
|
343
159
|
);
|
|
344
|
-
await
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
compilerOptions: {
|
|
350
|
-
lib: ['ESNext', 'DOM'],
|
|
351
|
-
module: 'Preserve',
|
|
352
|
-
moduleResolution: 'Bundler',
|
|
353
|
-
noEmit: true,
|
|
354
|
-
target: 'ESNext',
|
|
355
|
-
types: [],
|
|
356
|
-
},
|
|
357
|
-
include: ['src/**/*.ts', 'src/**/*.d.ts'],
|
|
358
|
-
},
|
|
359
|
-
null,
|
|
360
|
-
2,
|
|
361
|
-
),
|
|
362
|
-
);
|
|
363
|
-
|
|
364
|
-
try {
|
|
365
|
-
await execFileAsync(
|
|
366
|
-
process.platform === 'win32' ? 'tsgo.cmd' : 'tsgo',
|
|
367
|
-
['-p', 'tsconfig.json'],
|
|
368
|
-
{
|
|
369
|
-
cwd: projectDirectory,
|
|
370
|
-
shell: process.platform === 'win32',
|
|
371
|
-
},
|
|
372
|
-
);
|
|
373
|
-
} catch (error) {
|
|
374
|
-
if (error && typeof error === 'object') {
|
|
375
|
-
const stdout =
|
|
376
|
-
'stdout' in error && typeof error.stdout === 'string'
|
|
377
|
-
? error.stdout
|
|
378
|
-
: '';
|
|
379
|
-
const stderr =
|
|
380
|
-
'stderr' in error && typeof error.stderr === 'string'
|
|
381
|
-
? error.stderr
|
|
382
|
-
: '';
|
|
383
|
-
if (stdout || stderr) {
|
|
384
|
-
throw new Error([stdout, stderr].filter(Boolean).join('\n'), {
|
|
385
|
-
cause: error,
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
throw error;
|
|
390
|
-
}
|
|
160
|
+
await writeTsconfig(projectDirectory, {
|
|
161
|
+
module: 'Preserve',
|
|
162
|
+
moduleResolution: 'Bundler',
|
|
163
|
+
});
|
|
164
|
+
await runTsgo(projectDirectory);
|
|
391
165
|
}
|
|
392
166
|
|
|
393
|
-
async function
|
|
167
|
+
async function generateComprehensiveRouterGen(srcDirectory: string) {
|
|
394
168
|
const files = new Map([
|
|
395
169
|
[
|
|
396
170
|
'routes/(app)/layout.tsx',
|
|
@@ -419,22 +193,14 @@ async function writeComprehensiveRouterFixture(srcDirectory: string) {
|
|
|
419
193
|
].join('\n'),
|
|
420
194
|
],
|
|
421
195
|
]);
|
|
422
|
-
|
|
423
196
|
for (const [relativePath, contents] of files) {
|
|
424
197
|
const filePath = path.join(srcDirectory, relativePath);
|
|
425
198
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
426
199
|
await writeFile(filePath, contents);
|
|
427
200
|
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
async function generateComprehensiveRouterGen(srcDirectory: string) {
|
|
431
|
-
await writeComprehensiveRouterFixture(srcDirectory);
|
|
432
201
|
|
|
433
202
|
const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
|
|
434
|
-
appContext: {
|
|
435
|
-
srcDirectory,
|
|
436
|
-
internalSrcAlias: '@/_',
|
|
437
|
-
} as any,
|
|
203
|
+
appContext: { srcDirectory, internalSrcAlias: '@/_' } as any,
|
|
438
204
|
entryName: 'golden',
|
|
439
205
|
routes: [
|
|
440
206
|
{
|
|
@@ -477,176 +243,25 @@ describe('tanstack router type generation', () => {
|
|
|
477
243
|
let tempDir: string | undefined;
|
|
478
244
|
|
|
479
245
|
afterEach(async () => {
|
|
480
|
-
resolverDelays.clear();
|
|
481
246
|
if (tempDir) {
|
|
482
247
|
await rm(tempDir, { recursive: true, force: true });
|
|
483
248
|
tempDir = undefined;
|
|
484
249
|
}
|
|
485
250
|
});
|
|
486
251
|
|
|
487
|
-
test('executes component sharing correctly when module resolution races', async () => {
|
|
488
|
-
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
|
|
489
|
-
const srcDirectory = path.join(tempDir, 'src');
|
|
490
|
-
|
|
491
|
-
for (const componentFile of [
|
|
492
|
-
'routes/slow-page.tsx',
|
|
493
|
-
'routes/fast-page.tsx',
|
|
494
|
-
'routes/shared-page.tsx',
|
|
495
|
-
]) {
|
|
496
|
-
const componentPath = path.join(srcDirectory, componentFile);
|
|
497
|
-
await mkdir(path.dirname(componentPath), { recursive: true });
|
|
498
|
-
await writeFile(
|
|
499
|
-
componentPath,
|
|
500
|
-
'export default function Page() { return null; }',
|
|
501
|
-
);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
resolverDelays.set('/routes/slow-page', 25);
|
|
505
|
-
const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
|
|
506
|
-
appContext: {
|
|
507
|
-
srcDirectory,
|
|
508
|
-
internalSrcAlias: '@/_',
|
|
509
|
-
} as any,
|
|
510
|
-
entryName: 'index',
|
|
511
|
-
routes: [
|
|
512
|
-
{
|
|
513
|
-
type: 'nested',
|
|
514
|
-
id: 'layout',
|
|
515
|
-
isRoot: true,
|
|
516
|
-
children: [
|
|
517
|
-
{
|
|
518
|
-
type: 'nested',
|
|
519
|
-
id: 'slow/page',
|
|
520
|
-
path: 'slow',
|
|
521
|
-
_component: '@/_/routes/slow-page',
|
|
522
|
-
},
|
|
523
|
-
{
|
|
524
|
-
type: 'nested',
|
|
525
|
-
id: 'fast/page',
|
|
526
|
-
path: 'fast',
|
|
527
|
-
_component: '@/_/routes/fast-page',
|
|
528
|
-
},
|
|
529
|
-
{
|
|
530
|
-
type: 'nested',
|
|
531
|
-
id: 'shared-a/page',
|
|
532
|
-
path: 'shared-a',
|
|
533
|
-
_component: '@/_/routes/shared-page',
|
|
534
|
-
},
|
|
535
|
-
{
|
|
536
|
-
type: 'nested',
|
|
537
|
-
id: 'shared-b/page',
|
|
538
|
-
path: 'shared-b',
|
|
539
|
-
_component: '@/_/routes/shared-page',
|
|
540
|
-
},
|
|
541
|
-
{
|
|
542
|
-
type: 'nested',
|
|
543
|
-
id: 'data-only/page',
|
|
544
|
-
path: 'data-only',
|
|
545
|
-
},
|
|
546
|
-
],
|
|
547
|
-
},
|
|
548
|
-
] as any,
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
await compileAndRunGeneratedRouter({
|
|
552
|
-
projectDirectory: tempDir,
|
|
553
|
-
routerGenTs,
|
|
554
|
-
runtimeCheck: [
|
|
555
|
-
"import SlowPage from './routes/slow-page';",
|
|
556
|
-
"import FastPage from './routes/fast-page';",
|
|
557
|
-
"import SharedPage from './routes/shared-page';",
|
|
558
|
-
"import { routeTree } from './modern-tanstack/index/router.gen';",
|
|
559
|
-
'',
|
|
560
|
-
'const [slowRoute, fastRoute, sharedA, sharedB, dataOnly] = routeTree.children;',
|
|
561
|
-
'if (slowRoute.options.component !== SlowPage) throw new Error("slow component was crossed during resolution");',
|
|
562
|
-
'if (fastRoute.options.component !== FastPage) throw new Error("fast component was crossed during resolution");',
|
|
563
|
-
'if (sharedA.options.component !== SharedPage || sharedB.options.component !== SharedPage) throw new Error("shared component identity was not preserved");',
|
|
564
|
-
'if (Reflect.has(dataOnly.options, "component")) throw new Error("data-only route gained a component");',
|
|
565
|
-
].join('\n'),
|
|
566
|
-
});
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
test('strictly compiles and executes nested loader and search contracts', async () => {
|
|
570
|
-
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
|
|
571
|
-
const srcDirectory = path.join(tempDir, 'src');
|
|
572
|
-
const routerGenTs = await generateComprehensiveRouterGen(srcDirectory);
|
|
573
|
-
|
|
574
|
-
await compileAndRunGeneratedRouter({
|
|
575
|
-
projectDirectory: tempDir,
|
|
576
|
-
routerGenTs,
|
|
577
|
-
runtimeCheck: [
|
|
578
|
-
"import { runtimeState } from '@modern-js/plugin-tanstack/runtime';",
|
|
579
|
-
"import { action, loader } from './routes/(app)/users/(userId)/page.data';",
|
|
580
|
-
"import { loaderDeps, validateSearch } from './routes/search.contract';",
|
|
581
|
-
"import { routeTree } from './modern-tanstack/index/router.gen';",
|
|
582
|
-
'',
|
|
583
|
-
'async function main() {',
|
|
584
|
-
'const [appRoute] = routeTree.children;',
|
|
585
|
-
'const [userRoute, docsRoute] = appRoute.children;',
|
|
586
|
-
'if (runtimeState.adapterCalls.length !== 1) throw new Error("nested loader was not adapted");',
|
|
587
|
-
'if (userRoute.options.validateSearch !== validateSearch) throw new Error("search validator was not wired");',
|
|
588
|
-
'if (userRoute.options.loaderDeps !== loaderDeps) throw new Error("loader dependencies were not wired");',
|
|
589
|
-
'if (userRoute.options.staticData.modernRouteLoader !== loader) throw new Error("nested static loader was not wired");',
|
|
590
|
-
'if (userRoute.options.staticData.modernRouteAction !== action) throw new Error("nested static action was not wired");',
|
|
591
|
-
'const user = await userRoute.options.loader({});',
|
|
592
|
-
'if (user.userId !== "42") throw new Error("nested loader result was lost");',
|
|
593
|
-
'if (userRoute.options.path !== "users/$userId") throw new Error("required route param was not translated");',
|
|
594
|
-
'if (docsRoute.options.path !== "docs/$") throw new Error("splat route was not translated");',
|
|
595
|
-
'const search = userRoute.options.validateSearch({});',
|
|
596
|
-
'if (search.tab !== "overview") throw new Error("search contract returned the wrong value");',
|
|
597
|
-
'}',
|
|
598
|
-
'void main();',
|
|
599
|
-
].join('\n'),
|
|
600
|
-
});
|
|
601
|
-
});
|
|
602
|
-
|
|
603
252
|
test('strictly compiles generated routes against installed router declarations', async () => {
|
|
604
253
|
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
|
|
605
|
-
const
|
|
606
|
-
|
|
254
|
+
const routerGenTs = await generateComprehensiveRouterGen(
|
|
255
|
+
path.join(tempDir, 'src'),
|
|
256
|
+
);
|
|
607
257
|
|
|
608
258
|
await compileGeneratedRouterAgainstInstalledDeclarations({
|
|
609
259
|
projectDirectory: tempDir,
|
|
610
260
|
routerGenTs,
|
|
611
261
|
});
|
|
612
262
|
});
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
describe('collectCanonicalRoutesForEntry', () => {
|
|
616
|
-
let tempDir: string | undefined;
|
|
617
263
|
|
|
618
|
-
|
|
619
|
-
if (tempDir) {
|
|
620
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
621
|
-
tempDir = undefined;
|
|
622
|
-
}
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
test('returns null for a route tree with no locale param and no canonical metadata', () => {
|
|
626
|
-
const result = collectCanonicalRoutesForEntry([
|
|
627
|
-
{
|
|
628
|
-
type: 'nested',
|
|
629
|
-
id: 'layout',
|
|
630
|
-
isRoot: true,
|
|
631
|
-
children: [
|
|
632
|
-
{
|
|
633
|
-
type: 'nested',
|
|
634
|
-
id: 'about/page',
|
|
635
|
-
path: 'about',
|
|
636
|
-
},
|
|
637
|
-
{
|
|
638
|
-
type: 'nested',
|
|
639
|
-
id: 'contact/page',
|
|
640
|
-
path: 'contact',
|
|
641
|
-
},
|
|
642
|
-
],
|
|
643
|
-
},
|
|
644
|
-
] as any);
|
|
645
|
-
|
|
646
|
-
expect(result).toBeNull();
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
test('ignores a leading :lang param when the locale-param heuristic is disabled (no plugin-i18n)', async () => {
|
|
264
|
+
test('maps a locale-prefixed route tree to the canonical paths consumers navigate to', () => {
|
|
650
265
|
const routes = [
|
|
651
266
|
{
|
|
652
267
|
type: 'nested',
|
|
@@ -658,172 +273,38 @@ describe('collectCanonicalRoutesForEntry', () => {
|
|
|
658
273
|
id: '(lang)/layout',
|
|
659
274
|
path: ':lang',
|
|
660
275
|
children: [
|
|
276
|
+
{ type: 'nested', id: '(lang)/page', index: true },
|
|
277
|
+
{ type: 'nested', id: '(lang)/about/page', path: 'about' },
|
|
661
278
|
{
|
|
662
279
|
type: 'nested',
|
|
663
|
-
id: '(lang)/
|
|
664
|
-
path: '
|
|
280
|
+
id: '(lang)/optional/(slug$)/page',
|
|
281
|
+
path: 'optional/:slug?',
|
|
665
282
|
},
|
|
283
|
+
{ type: 'nested', id: '(lang)/files/page', path: 'files/*' },
|
|
666
284
|
],
|
|
667
285
|
},
|
|
668
286
|
],
|
|
669
287
|
},
|
|
670
288
|
] as any;
|
|
671
289
|
|
|
672
|
-
// Without plugin-i18n, a hand-rolled `/:lang/` param must NOT
|
|
673
|
-
//
|
|
290
|
+
// Without plugin-i18n installed, a hand-rolled `/:lang/` param must NOT
|
|
291
|
+
// produce a canonical surface — the emitted module augmentation would
|
|
292
|
+
// break typechecking in an app that never opted into i18n.
|
|
674
293
|
expect(
|
|
675
294
|
collectCanonicalRoutesForEntry(routes, { localeParamHeuristic: false }),
|
|
676
295
|
).toBeNull();
|
|
677
|
-
// With plugin-i18n installed the heuristic strips the locale prefix.
|
|
678
|
-
const result = collectCanonicalRoutesForEntry(routes, {
|
|
679
|
-
localeParamHeuristic: true,
|
|
680
|
-
});
|
|
681
|
-
expect(result).not.toBeNull();
|
|
682
|
-
expect(Object.keys(result!)).toEqual(['/about']);
|
|
683
|
-
|
|
684
|
-
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-canonical-'));
|
|
685
|
-
await typecheckCanonicalRegisterContract({
|
|
686
|
-
canonicalRoutes: result!,
|
|
687
|
-
contractLines: [
|
|
688
|
-
"const aboutParams: UltramodernCanonicalRoutes['/about'] = {};",
|
|
689
|
-
'// @ts-expect-error canonical routes without params reject unexpected properties',
|
|
690
|
-
"const invalidAboutParams: UltramodernCanonicalRoutes['/about'] = { slug: 'unexpected' };",
|
|
691
|
-
'void aboutParams;',
|
|
692
|
-
'void invalidAboutParams;',
|
|
693
|
-
],
|
|
694
|
-
projectDirectory: tempDir,
|
|
695
|
-
});
|
|
696
|
-
});
|
|
697
|
-
|
|
698
|
-
test('still honors modernCanonicalPath metadata when the heuristic is disabled', async () => {
|
|
699
|
-
const result = collectCanonicalRoutesForEntry(
|
|
700
|
-
[
|
|
701
|
-
{
|
|
702
|
-
type: 'nested',
|
|
703
|
-
id: 'layout',
|
|
704
|
-
isRoot: true,
|
|
705
|
-
children: [
|
|
706
|
-
{
|
|
707
|
-
type: 'nested',
|
|
708
|
-
id: '(lang)/products/(slug)/page',
|
|
709
|
-
path: 'products/:slug',
|
|
710
|
-
modernCanonicalPath: '/products/:slug',
|
|
711
|
-
},
|
|
712
|
-
],
|
|
713
|
-
},
|
|
714
|
-
] as any,
|
|
715
|
-
{ localeParamHeuristic: false },
|
|
716
|
-
);
|
|
717
|
-
|
|
718
|
-
expect(result).not.toBeNull();
|
|
719
|
-
expect(Object.keys(result!)).toEqual(['/products/$slug']);
|
|
720
|
-
|
|
721
|
-
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-canonical-'));
|
|
722
|
-
await typecheckCanonicalRegisterContract({
|
|
723
|
-
canonicalRoutes: result!,
|
|
724
|
-
contractLines: [
|
|
725
|
-
"const productParams: UltramodernCanonicalRoutes['/products/$slug'] = { slug: 'tractor' };",
|
|
726
|
-
'// @ts-expect-error a required canonical route param cannot be omitted',
|
|
727
|
-
"const missingProductParams: UltramodernCanonicalRoutes['/products/$slug'] = {};",
|
|
728
|
-
'// @ts-expect-error canonical route params retain their string type',
|
|
729
|
-
"const invalidProductParams: UltramodernCanonicalRoutes['/products/$slug'] = { slug: 42 };",
|
|
730
|
-
'void productParams;',
|
|
731
|
-
'void missingProductParams;',
|
|
732
|
-
'void invalidProductParams;',
|
|
733
|
-
],
|
|
734
|
-
projectDirectory: tempDir,
|
|
735
|
-
});
|
|
736
|
-
});
|
|
737
|
-
|
|
738
|
-
test('strips leading :lang param and maps index under :lang to "/"', async () => {
|
|
739
|
-
const result = collectCanonicalRoutesForEntry([
|
|
740
|
-
{
|
|
741
|
-
type: 'nested',
|
|
742
|
-
id: 'layout',
|
|
743
|
-
isRoot: true,
|
|
744
|
-
children: [
|
|
745
|
-
{
|
|
746
|
-
type: 'nested',
|
|
747
|
-
id: '(lang)/layout',
|
|
748
|
-
path: ':lang',
|
|
749
|
-
children: [
|
|
750
|
-
{
|
|
751
|
-
type: 'nested',
|
|
752
|
-
id: '(lang)/page',
|
|
753
|
-
index: true,
|
|
754
|
-
},
|
|
755
|
-
{
|
|
756
|
-
type: 'nested',
|
|
757
|
-
id: '(lang)/about/page',
|
|
758
|
-
path: 'about',
|
|
759
|
-
},
|
|
760
|
-
],
|
|
761
|
-
},
|
|
762
|
-
],
|
|
763
|
-
},
|
|
764
|
-
] as any);
|
|
765
|
-
|
|
766
|
-
expect(result).not.toBeNull();
|
|
767
|
-
expect(Object.keys(result!)).toEqual(['/', '/about']);
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
test('converts :slug? to optional {-$slug} with optional params type', async () => {
|
|
771
|
-
const result = collectCanonicalRoutesForEntry([
|
|
772
|
-
{
|
|
773
|
-
type: 'nested',
|
|
774
|
-
id: 'layout',
|
|
775
|
-
isRoot: true,
|
|
776
|
-
children: [
|
|
777
|
-
{
|
|
778
|
-
type: 'nested',
|
|
779
|
-
id: '(lang)/layout',
|
|
780
|
-
path: ':lang',
|
|
781
|
-
children: [
|
|
782
|
-
{
|
|
783
|
-
type: 'nested',
|
|
784
|
-
id: '(lang)/optional/(slug$)/page',
|
|
785
|
-
path: 'optional/:slug?',
|
|
786
|
-
},
|
|
787
|
-
],
|
|
788
|
-
},
|
|
789
|
-
],
|
|
790
|
-
},
|
|
791
|
-
] as any);
|
|
792
|
-
|
|
793
|
-
expect(result).not.toBeNull();
|
|
794
|
-
expect(Object.keys(result!)).toEqual(['/optional/{-$slug}']);
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
test('converts * splat to $ with optional _splat param', async () => {
|
|
798
|
-
const result = collectCanonicalRoutesForEntry([
|
|
799
|
-
{
|
|
800
|
-
type: 'nested',
|
|
801
|
-
id: 'layout',
|
|
802
|
-
isRoot: true,
|
|
803
|
-
children: [
|
|
804
|
-
{
|
|
805
|
-
type: 'nested',
|
|
806
|
-
id: '(lang)/layout',
|
|
807
|
-
path: ':lang',
|
|
808
|
-
children: [
|
|
809
|
-
{
|
|
810
|
-
type: 'nested',
|
|
811
|
-
id: '(lang)/files/page',
|
|
812
|
-
path: 'files/*',
|
|
813
|
-
},
|
|
814
|
-
],
|
|
815
|
-
},
|
|
816
|
-
],
|
|
817
|
-
},
|
|
818
|
-
] as any);
|
|
819
296
|
|
|
297
|
+
const result = collectCanonicalRoutesForEntry(routes);
|
|
820
298
|
expect(result).not.toBeNull();
|
|
821
|
-
expect(Object.keys(result!)).toEqual([
|
|
299
|
+
expect(Object.keys(result!)).toEqual([
|
|
300
|
+
'/',
|
|
301
|
+
'/about',
|
|
302
|
+
'/files/$',
|
|
303
|
+
'/optional/{-$slug}',
|
|
304
|
+
]);
|
|
822
305
|
});
|
|
823
306
|
|
|
824
|
-
test('collapses localized variants
|
|
825
|
-
// This reuses the same fixture shape as the 'preserves typed child trees'
|
|
826
|
-
// test but adds modernCanonicalPath fields as plugin-i18n now emits.
|
|
307
|
+
test('collapses localized variants to one canonical key with typed params', async () => {
|
|
827
308
|
const result = collectCanonicalRoutesForEntry([
|
|
828
309
|
{
|
|
829
310
|
type: 'nested',
|
|
@@ -860,10 +341,10 @@ describe('collectCanonicalRoutesForEntry', () => {
|
|
|
860
341
|
] as any);
|
|
861
342
|
|
|
862
343
|
expect(result).not.toBeNull();
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
344
|
+
expect(Object.keys(result!)).toEqual([
|
|
345
|
+
'/optional/{-$slug}',
|
|
346
|
+
'/products/$slug',
|
|
347
|
+
]);
|
|
867
348
|
expect('/produkty/$slug' in result!).toBe(false);
|
|
868
349
|
|
|
869
350
|
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-canonical-'));
|
|
@@ -882,10 +363,7 @@ describe('collectCanonicalRoutesForEntry', () => {
|
|
|
882
363
|
"const presentOptionalParams: UltramodernCanonicalRoutes['/optional/{-$slug}'] = { slug: 'tractor' };",
|
|
883
364
|
'// @ts-expect-error optional canonical params still retain their string type',
|
|
884
365
|
"const invalidOptionalParams: UltramodernCanonicalRoutes['/optional/{-$slug}'] = { slug: 42 };",
|
|
885
|
-
"const emptySplatParams: UltramodernCanonicalRoutes['/files/$'] = {};",
|
|
886
366
|
"const splatParams: UltramodernCanonicalRoutes['/files/$'] = { _splat: 'guides/intro' };",
|
|
887
|
-
'// @ts-expect-error splat params retain their string type',
|
|
888
|
-
"const invalidSplatParams: UltramodernCanonicalRoutes['/files/$'] = { _splat: 42 };",
|
|
889
367
|
'// @ts-expect-error localized physical variants do not become canonical keys',
|
|
890
368
|
"declare const localizedParams: UltramodernCanonicalRoutes['/produkty/$slug'];",
|
|
891
369
|
'// @ts-expect-error the deduplicated product route still requires its param',
|
|
@@ -894,11 +372,9 @@ describe('collectCanonicalRoutesForEntry', () => {
|
|
|
894
372
|
'void productParams;',
|
|
895
373
|
'void optionalParams;',
|
|
896
374
|
'void presentOptionalParams;',
|
|
897
|
-
'void emptySplatParams;',
|
|
898
375
|
'void splatParams;',
|
|
899
376
|
'void invalidRootParams;',
|
|
900
377
|
'void invalidOptionalParams;',
|
|
901
|
-
'void invalidSplatParams;',
|
|
902
378
|
'void localizedParams;',
|
|
903
379
|
'void missingProductParams;',
|
|
904
380
|
],
|