@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.1 → 3.9.0-ultramodern.11
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 +9 -0
- package/dist/cjs/cli/tanstackTypes/source.js +3 -1
- package/dist/cjs/runtime/basepathRewrite.js +13 -6
- package/dist/cjs/runtime/lifecycle.js +7 -7
- package/dist/cjs/runtime/plugin.js +1 -1
- package/dist/cjs/runtime/plugin.node.js +5 -1
- package/dist/cjs/runtime/prefetchLink.js +2 -7
- package/dist/cjs/runtime/register.js +0 -2
- package/dist/cjs/runtime/routeTree/construction.js +2 -0
- package/dist/cjs/runtime/router.js +25 -9
- package/dist/cjs/runtime/state.js +2 -2
- package/dist/cjs/runtime/utils.js +2 -0
- package/dist/esm/cli/index.mjs +9 -0
- package/dist/esm/cli/tanstackTypes/source.mjs +3 -1
- package/dist/esm/runtime/basepathRewrite.mjs +10 -3
- package/dist/esm/runtime/lifecycle.mjs +1 -1
- package/dist/esm/runtime/plugin.mjs +1 -1
- package/dist/esm/runtime/plugin.node.mjs +5 -1
- package/dist/esm/runtime/prefetchLink.mjs +1 -6
- package/dist/esm/runtime/register.mjs +0 -2
- package/dist/esm/runtime/routeTree/construction.mjs +2 -0
- package/dist/esm/runtime/router.mjs +27 -11
- package/dist/esm/runtime/state.mjs +1 -1
- package/dist/esm/runtime/utils.mjs +2 -0
- package/dist/esm-node/cli/index.mjs +9 -0
- package/dist/esm-node/cli/tanstackTypes/source.mjs +3 -1
- package/dist/esm-node/runtime/basepathRewrite.mjs +10 -3
- package/dist/esm-node/runtime/lifecycle.mjs +1 -1
- package/dist/esm-node/runtime/plugin.mjs +1 -1
- package/dist/esm-node/runtime/plugin.node.mjs +5 -1
- package/dist/esm-node/runtime/prefetchLink.mjs +1 -6
- package/dist/esm-node/runtime/register.mjs +0 -2
- package/dist/esm-node/runtime/routeTree/construction.mjs +2 -0
- package/dist/esm-node/runtime/router.mjs +27 -11
- package/dist/esm-node/runtime/state.mjs +1 -1
- package/dist/esm-node/runtime/utils.mjs +2 -0
- package/dist/types/runtime/basepathRewrite.d.ts +2 -1
- package/dist/types/runtime/lifecycle.d.ts +9 -2
- package/dist/types/runtime/register.d.ts +1 -9
- package/dist/types/runtime/router.d.ts +2 -13
- package/dist/types/runtime/state.d.ts +1 -1
- package/dist/types/runtime/types.d.ts +2 -1
- package/package.json +18 -15
- package/rstest.config.mts +1 -24
- package/src/cli/index.ts +10 -0
- package/src/cli/tanstackTypes/source.ts +3 -1
- package/src/runtime/basepathRewrite.ts +11 -2
- package/src/runtime/lifecycle.ts +14 -3
- package/src/runtime/plugin.node.tsx +9 -2
- package/src/runtime/plugin.tsx +5 -1
- package/src/runtime/prefetchLink.tsx +1 -23
- package/src/runtime/register.ts +1 -12
- package/src/runtime/routeTree/construction.ts +2 -0
- package/src/runtime/router.ts +25 -40
- package/src/runtime/state.ts +1 -1
- package/src/runtime/types.ts +2 -2
- package/src/runtime/utils.tsx +3 -1
- package/tests/router/cli.test.ts +148 -824
- package/tests/router/dataMutation.test.tsx +17 -85
- package/tests/router/flightSerialization.roundtrip.test.tsx +9 -6
- package/tests/router/generateRouteArtifacts.test.ts +68 -28
- package/tests/router/loaderBridge.test.ts +0 -25
- package/tests/router/localisedIdentity.test.tsx +95 -0
- package/tests/router/prefetchLink.test.tsx +17 -21
- package/tests/router/prefetchLinkPreload.test.tsx +11 -31
- package/tests/router/routeHooks.test.ts +41 -105
- package/tests/router/routeTree.test.ts +144 -836
- package/tests/router/rsc.test.tsx +131 -166
- package/tests/router/serverPlugin.test.ts +43 -1
- package/tests/router/tanstackTypes.test.ts +109 -973
- package/tests/realm-isolation.test.ts +0 -114
- package/tests/router/fastDefaults.test.ts +0 -32
- package/tests/router/fixtures/legacyRuntimeRouter.ts +0 -10
- package/tests/router/fixtures/rscDecoderSecurityChild.cjs +0 -162
- package/tests/router/hooks.test.ts +0 -40
- package/tests/router/hydrationBoundary.test.tsx +0 -23
- package/tests/router/packageSurface.test.ts +0 -24
- package/tests/router/preloadRedirect.test.ts +0 -64
- package/tests/router/register.test.ts +0 -8
- package/tests/router/routerCompatibility.test.ts +0 -239
- package/tests/router/rscDecoderSecurity.test.ts +0 -113
- package/tests/router/rscPayloadRouterMatrix.test.ts +0 -197
|
@@ -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,
|
|
@@ -36,15 +11,6 @@ import {
|
|
|
36
11
|
} from '../../src/cli/tanstackTypes';
|
|
37
12
|
|
|
38
13
|
const execFileAsync = promisify(execFile);
|
|
39
|
-
const routerGenOxlintConfigPath = path.join(
|
|
40
|
-
__dirname,
|
|
41
|
-
'fixtures',
|
|
42
|
-
'router-gen.oxlint.json',
|
|
43
|
-
);
|
|
44
|
-
const oxlintCliPath = path.resolve(
|
|
45
|
-
__dirname,
|
|
46
|
-
'../../../../toolkit/code-tools/node_modules/oxlint/bin/oxlint',
|
|
47
|
-
);
|
|
48
14
|
const strictestTsconfigPath = path.resolve(
|
|
49
15
|
__dirname,
|
|
50
16
|
'../../node_modules/@tsconfig/strictest/tsconfig.json',
|
|
@@ -54,195 +20,51 @@ const installedReactRouterPath = path.resolve(
|
|
|
54
20
|
'../../node_modules/@tanstack/react-router',
|
|
55
21
|
);
|
|
56
22
|
|
|
57
|
-
async function
|
|
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(
|
|
23
|
+
async function writeTsconfig(
|
|
134
24
|
projectDirectory: string,
|
|
25
|
+
compilerOptions: Record<string, unknown>,
|
|
135
26
|
) {
|
|
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
27
|
await writeFile(
|
|
192
28
|
path.join(projectDirectory, 'tsconfig.json'),
|
|
193
29
|
JSON.stringify(
|
|
194
30
|
{
|
|
195
31
|
extends: strictestTsconfigPath,
|
|
196
32
|
compilerOptions: {
|
|
197
|
-
lib: ['ESNext', 'DOM'],
|
|
198
33
|
jsx: 'react-jsx',
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
rootDir: 'src',
|
|
203
|
-
target: 'ES2022',
|
|
34
|
+
lib: ['ESNext', 'DOM'],
|
|
35
|
+
noEmit: true,
|
|
36
|
+
target: 'ESNext',
|
|
204
37
|
types: [],
|
|
205
|
-
|
|
38
|
+
...compilerOptions,
|
|
206
39
|
},
|
|
207
|
-
include: ['src/**/*.ts', 'src/**/*.tsx'],
|
|
40
|
+
include: ['src/**/*.ts', 'src/**/*.tsx', 'src/**/*.d.ts'],
|
|
208
41
|
},
|
|
209
42
|
null,
|
|
210
43
|
2,
|
|
211
44
|
),
|
|
212
45
|
);
|
|
46
|
+
}
|
|
213
47
|
|
|
48
|
+
/** Runs the same compiler a consumer build runs; surfaces its diagnostics. */
|
|
49
|
+
async function runTsgo(projectDirectory: string) {
|
|
214
50
|
try {
|
|
215
51
|
await execFileAsync(
|
|
216
52
|
process.platform === 'win32' ? 'tsgo.cmd' : 'tsgo',
|
|
217
53
|
['-p', 'tsconfig.json'],
|
|
218
|
-
{
|
|
219
|
-
cwd: projectDirectory,
|
|
220
|
-
shell: process.platform === 'win32',
|
|
221
|
-
},
|
|
54
|
+
{ cwd: projectDirectory, shell: process.platform === 'win32' },
|
|
222
55
|
);
|
|
223
56
|
} catch (error) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
: '';
|
|
233
|
-
if (stdout || stderr) {
|
|
234
|
-
throw new Error([stdout, stderr].filter(Boolean).join('\n'), {
|
|
235
|
-
cause: error,
|
|
236
|
-
});
|
|
237
|
-
}
|
|
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
|
+
});
|
|
238
65
|
}
|
|
239
66
|
throw error;
|
|
240
67
|
}
|
|
241
|
-
await execFileAsync(
|
|
242
|
-
process.execPath,
|
|
243
|
-
[path.join(projectDirectory, 'dist', 'runtime-check.js')],
|
|
244
|
-
{ cwd: projectDirectory },
|
|
245
|
-
);
|
|
246
68
|
}
|
|
247
69
|
|
|
248
70
|
async function compileGeneratedRouterAgainstInstalledDeclarations(options: {
|
|
@@ -258,63 +80,48 @@ async function compileGeneratedRouterAgainstInstalledDeclarations(options: {
|
|
|
258
80
|
);
|
|
259
81
|
await mkdir(generatedDirectory, { recursive: true });
|
|
260
82
|
await writeFile(path.join(generatedDirectory, 'router.gen.ts'), routerGenTs);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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',
|
|
265
92
|
);
|
|
93
|
+
await mkdir(runtimePackageDirectory, { recursive: true });
|
|
266
94
|
await writeFile(
|
|
267
|
-
path.join(
|
|
268
|
-
JSON.stringify(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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'],
|
|
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' },
|
|
285
101
|
},
|
|
286
|
-
|
|
287
|
-
2,
|
|
288
|
-
),
|
|
102
|
+
}),
|
|
289
103
|
);
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
{
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
throw new Error([stdout, stderr].filter(Boolean).join('\n'), {
|
|
312
|
-
cause: error,
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
throw error;
|
|
317
|
-
}
|
|
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);
|
|
318
125
|
}
|
|
319
126
|
|
|
320
127
|
async function typecheckCanonicalRegisterContract(options: {
|
|
@@ -324,11 +131,11 @@ async function typecheckCanonicalRegisterContract(options: {
|
|
|
324
131
|
}) {
|
|
325
132
|
const { canonicalRoutes, contractLines, projectDirectory } = options;
|
|
326
133
|
const srcDirectory = path.join(projectDirectory, 'src');
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
134
|
+
await mkdir(path.join(srcDirectory, 'modern-tanstack', 'index'), {
|
|
135
|
+
recursive: true,
|
|
136
|
+
});
|
|
330
137
|
await writeFile(
|
|
331
|
-
path.join(
|
|
138
|
+
path.join(srcDirectory, 'modern-tanstack', 'index', 'router.gen.ts'),
|
|
332
139
|
'export const router = { context: {} };',
|
|
333
140
|
);
|
|
334
141
|
await writeTanstackRegisterFile({
|
|
@@ -350,56 +157,14 @@ async function typecheckCanonicalRegisterContract(options: {
|
|
|
350
157
|
...contractLines,
|
|
351
158
|
].join('\n'),
|
|
352
159
|
);
|
|
353
|
-
await
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
}
|
|
160
|
+
await writeTsconfig(projectDirectory, {
|
|
161
|
+
module: 'Preserve',
|
|
162
|
+
moduleResolution: 'Bundler',
|
|
163
|
+
});
|
|
164
|
+
await runTsgo(projectDirectory);
|
|
400
165
|
}
|
|
401
166
|
|
|
402
|
-
async function
|
|
167
|
+
async function generateComprehensiveRouterGen(srcDirectory: string) {
|
|
403
168
|
const files = new Map([
|
|
404
169
|
[
|
|
405
170
|
'routes/(app)/layout.tsx',
|
|
@@ -428,22 +193,14 @@ async function writeComprehensiveRouterFixture(srcDirectory: string) {
|
|
|
428
193
|
].join('\n'),
|
|
429
194
|
],
|
|
430
195
|
]);
|
|
431
|
-
|
|
432
196
|
for (const [relativePath, contents] of files) {
|
|
433
197
|
const filePath = path.join(srcDirectory, relativePath);
|
|
434
198
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
435
199
|
await writeFile(filePath, contents);
|
|
436
200
|
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
async function generateComprehensiveRouterGen(srcDirectory: string) {
|
|
440
|
-
await writeComprehensiveRouterFixture(srcDirectory);
|
|
441
201
|
|
|
442
202
|
const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
|
|
443
|
-
appContext: {
|
|
444
|
-
srcDirectory,
|
|
445
|
-
internalSrcAlias: '@/_',
|
|
446
|
-
} as any,
|
|
203
|
+
appContext: { srcDirectory, internalSrcAlias: '@/_' } as any,
|
|
447
204
|
entryName: 'golden',
|
|
448
205
|
routes: [
|
|
449
206
|
{
|
|
@@ -486,402 +243,25 @@ describe('tanstack router type generation', () => {
|
|
|
486
243
|
let tempDir: string | undefined;
|
|
487
244
|
|
|
488
245
|
afterEach(async () => {
|
|
489
|
-
resolverDelays.clear();
|
|
490
246
|
if (tempDir) {
|
|
491
247
|
await rm(tempDir, { recursive: true, force: true });
|
|
492
248
|
tempDir = undefined;
|
|
493
249
|
}
|
|
494
250
|
});
|
|
495
251
|
|
|
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 () => {
|
|
569
|
-
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
|
|
570
|
-
const srcDirectory = path.join(tempDir, 'src');
|
|
571
|
-
const routeDir = path.join(srcDirectory, 'routes', 'mf');
|
|
572
|
-
await mkdir(routeDir, { recursive: true });
|
|
573
|
-
await writeFile(
|
|
574
|
-
path.join(routeDir, 'page.data.ts'),
|
|
575
|
-
[
|
|
576
|
-
'export const loader = () => ({ count: 0 });',
|
|
577
|
-
'export const action = () => Response.json({ count: 1 });',
|
|
578
|
-
].join('\n'),
|
|
579
|
-
);
|
|
580
|
-
|
|
581
|
-
const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
|
|
582
|
-
appContext: {
|
|
583
|
-
srcDirectory,
|
|
584
|
-
internalSrcAlias: '@/_',
|
|
585
|
-
} as any,
|
|
586
|
-
entryName: 'index',
|
|
587
|
-
routes: [
|
|
588
|
-
{
|
|
589
|
-
type: 'nested',
|
|
590
|
-
id: 'layout',
|
|
591
|
-
isRoot: true,
|
|
592
|
-
children: [
|
|
593
|
-
{
|
|
594
|
-
type: 'nested',
|
|
595
|
-
id: 'mf/page',
|
|
596
|
-
path: 'mf',
|
|
597
|
-
data: '@/_/routes/mf/page.data',
|
|
598
|
-
action: '@/_/routes/mf/page.data',
|
|
599
|
-
},
|
|
600
|
-
],
|
|
601
|
-
},
|
|
602
|
-
] as any,
|
|
603
|
-
});
|
|
604
|
-
|
|
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
|
-
});
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
test('executes component sharing correctly when module resolution races', async () => {
|
|
628
|
-
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
|
|
629
|
-
const srcDirectory = path.join(tempDir, 'src');
|
|
630
|
-
|
|
631
|
-
for (const componentFile of [
|
|
632
|
-
'routes/slow-page.tsx',
|
|
633
|
-
'routes/fast-page.tsx',
|
|
634
|
-
'routes/shared-page.tsx',
|
|
635
|
-
]) {
|
|
636
|
-
const componentPath = path.join(srcDirectory, componentFile);
|
|
637
|
-
await mkdir(path.dirname(componentPath), { recursive: true });
|
|
638
|
-
await writeFile(
|
|
639
|
-
componentPath,
|
|
640
|
-
'export default function Page() { return null; }',
|
|
641
|
-
);
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
resolverDelays.set('/routes/slow-page', 25);
|
|
645
|
-
const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
|
|
646
|
-
appContext: {
|
|
647
|
-
srcDirectory,
|
|
648
|
-
internalSrcAlias: '@/_',
|
|
649
|
-
} as any,
|
|
650
|
-
entryName: 'index',
|
|
651
|
-
routes: [
|
|
652
|
-
{
|
|
653
|
-
type: 'nested',
|
|
654
|
-
id: 'layout',
|
|
655
|
-
isRoot: true,
|
|
656
|
-
children: [
|
|
657
|
-
{
|
|
658
|
-
type: 'nested',
|
|
659
|
-
id: 'slow/page',
|
|
660
|
-
path: 'slow',
|
|
661
|
-
_component: '@/_/routes/slow-page',
|
|
662
|
-
},
|
|
663
|
-
{
|
|
664
|
-
type: 'nested',
|
|
665
|
-
id: 'fast/page',
|
|
666
|
-
path: 'fast',
|
|
667
|
-
_component: '@/_/routes/fast-page',
|
|
668
|
-
},
|
|
669
|
-
{
|
|
670
|
-
type: 'nested',
|
|
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',
|
|
680
|
-
},
|
|
681
|
-
{
|
|
682
|
-
type: 'nested',
|
|
683
|
-
id: 'data-only/page',
|
|
684
|
-
path: 'data-only',
|
|
685
|
-
},
|
|
686
|
-
],
|
|
687
|
-
},
|
|
688
|
-
] as any,
|
|
689
|
-
});
|
|
690
|
-
|
|
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
|
-
});
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
test('strictly compiles and executes nested loader and search contracts', async () => {
|
|
710
|
-
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
|
|
711
|
-
const srcDirectory = path.join(tempDir, 'src');
|
|
712
|
-
const routerGenTs = await generateComprehensiveRouterGen(srcDirectory);
|
|
713
|
-
|
|
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'),
|
|
740
|
-
});
|
|
741
|
-
});
|
|
742
|
-
|
|
743
252
|
test('strictly compiles generated routes against installed router declarations', async () => {
|
|
744
253
|
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
await compileGeneratedRouterAgainstInstalledDeclarations({
|
|
749
|
-
projectDirectory: tempDir,
|
|
750
|
-
routerGenTs,
|
|
751
|
-
});
|
|
752
|
-
});
|
|
753
|
-
|
|
754
|
-
test('passes the generated-artifact lint contract', async () => {
|
|
755
|
-
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
|
|
756
|
-
const srcDirectory = path.join(tempDir, 'src');
|
|
757
|
-
const routerGenTs = await generateComprehensiveRouterGen(srcDirectory);
|
|
758
|
-
const routerGenPath = path.join(
|
|
759
|
-
srcDirectory,
|
|
760
|
-
'modern-tanstack',
|
|
761
|
-
'golden',
|
|
762
|
-
'router.gen.ts',
|
|
254
|
+
const routerGenTs = await generateComprehensiveRouterGen(
|
|
255
|
+
path.join(tempDir, 'src'),
|
|
763
256
|
);
|
|
764
257
|
|
|
765
|
-
await
|
|
766
|
-
await writeFile(routerGenPath, routerGenTs);
|
|
767
|
-
|
|
768
|
-
const { stderr, stdout } = await execFileAsync(
|
|
769
|
-
process.execPath,
|
|
770
|
-
[
|
|
771
|
-
oxlintCliPath,
|
|
772
|
-
routerGenPath,
|
|
773
|
-
'--config',
|
|
774
|
-
routerGenOxlintConfigPath,
|
|
775
|
-
'--no-ignore',
|
|
776
|
-
'--report-unused-disable-directives-severity',
|
|
777
|
-
'error',
|
|
778
|
-
'--format',
|
|
779
|
-
'unix',
|
|
780
|
-
],
|
|
781
|
-
{ cwd: tempDir },
|
|
782
|
-
);
|
|
783
|
-
|
|
784
|
-
expect(`${stdout}${stderr}`).toBe('');
|
|
785
|
-
});
|
|
786
|
-
|
|
787
|
-
test('executes localized aliases with typed children and correct parents', async () => {
|
|
788
|
-
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
|
|
789
|
-
const srcDirectory = path.join(tempDir, 'src');
|
|
790
|
-
|
|
791
|
-
const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
|
|
792
|
-
appContext: {
|
|
793
|
-
srcDirectory,
|
|
794
|
-
internalSrcAlias: '@/_',
|
|
795
|
-
} as any,
|
|
796
|
-
entryName: 'index',
|
|
797
|
-
routes: [
|
|
798
|
-
{
|
|
799
|
-
type: 'nested',
|
|
800
|
-
id: 'layout',
|
|
801
|
-
isRoot: true,
|
|
802
|
-
children: [
|
|
803
|
-
{
|
|
804
|
-
type: 'nested',
|
|
805
|
-
id: '(lang)/layout',
|
|
806
|
-
path: ':lang',
|
|
807
|
-
children: [
|
|
808
|
-
{
|
|
809
|
-
type: 'nested',
|
|
810
|
-
id: '(lang)/products/(slug)/page',
|
|
811
|
-
path: 'products/:slug',
|
|
812
|
-
},
|
|
813
|
-
{
|
|
814
|
-
type: 'nested',
|
|
815
|
-
id: '(lang)/products/(slug)/page__localised_produkty_slug',
|
|
816
|
-
path: 'produkty/:slug',
|
|
817
|
-
},
|
|
818
|
-
{
|
|
819
|
-
type: 'nested',
|
|
820
|
-
id: '(lang)/optional/(slug$)/page__localised_volitelne_slug',
|
|
821
|
-
path: 'volitelne/:slug?',
|
|
822
|
-
},
|
|
823
|
-
],
|
|
824
|
-
},
|
|
825
|
-
],
|
|
826
|
-
},
|
|
827
|
-
] as any,
|
|
828
|
-
});
|
|
829
|
-
|
|
830
|
-
await compileAndRunGeneratedRouter({
|
|
258
|
+
await compileGeneratedRouterAgainstInstalledDeclarations({
|
|
831
259
|
projectDirectory: tempDir,
|
|
832
260
|
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
261
|
});
|
|
847
262
|
});
|
|
848
|
-
});
|
|
849
|
-
|
|
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
263
|
|
|
860
|
-
test('
|
|
861
|
-
const result = collectCanonicalRoutesForEntry([
|
|
862
|
-
{
|
|
863
|
-
type: 'nested',
|
|
864
|
-
id: 'layout',
|
|
865
|
-
isRoot: true,
|
|
866
|
-
children: [
|
|
867
|
-
{
|
|
868
|
-
type: 'nested',
|
|
869
|
-
id: 'about/page',
|
|
870
|
-
path: 'about',
|
|
871
|
-
},
|
|
872
|
-
{
|
|
873
|
-
type: 'nested',
|
|
874
|
-
id: 'contact/page',
|
|
875
|
-
path: 'contact',
|
|
876
|
-
},
|
|
877
|
-
],
|
|
878
|
-
},
|
|
879
|
-
] as any);
|
|
880
|
-
|
|
881
|
-
expect(result).toBeNull();
|
|
882
|
-
});
|
|
883
|
-
|
|
884
|
-
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', () => {
|
|
885
265
|
const routes = [
|
|
886
266
|
{
|
|
887
267
|
type: 'nested',
|
|
@@ -893,257 +273,38 @@ describe('collectCanonicalRoutesForEntry', () => {
|
|
|
893
273
|
id: '(lang)/layout',
|
|
894
274
|
path: ':lang',
|
|
895
275
|
children: [
|
|
276
|
+
{ type: 'nested', id: '(lang)/page', index: true },
|
|
277
|
+
{ type: 'nested', id: '(lang)/about/page', path: 'about' },
|
|
896
278
|
{
|
|
897
279
|
type: 'nested',
|
|
898
|
-
id: '(lang)/
|
|
899
|
-
path: '
|
|
280
|
+
id: '(lang)/optional/(slug$)/page',
|
|
281
|
+
path: 'optional/:slug?',
|
|
900
282
|
},
|
|
283
|
+
{ type: 'nested', id: '(lang)/files/page', path: 'files/*' },
|
|
901
284
|
],
|
|
902
285
|
},
|
|
903
286
|
],
|
|
904
287
|
},
|
|
905
288
|
] as any;
|
|
906
289
|
|
|
907
|
-
// Without plugin-i18n, a hand-rolled `/:lang/` param must NOT
|
|
908
|
-
//
|
|
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.
|
|
909
293
|
expect(
|
|
910
294
|
collectCanonicalRoutesForEntry(routes, { localeParamHeuristic: false }),
|
|
911
295
|
).toBeNull();
|
|
912
|
-
// With plugin-i18n installed the heuristic strips the locale prefix.
|
|
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,
|
|
930
|
-
});
|
|
931
|
-
});
|
|
932
|
-
|
|
933
|
-
test('still honors modernCanonicalPath metadata when the heuristic is disabled', async () => {
|
|
934
|
-
const result = collectCanonicalRoutesForEntry(
|
|
935
|
-
[
|
|
936
|
-
{
|
|
937
|
-
type: 'nested',
|
|
938
|
-
id: 'layout',
|
|
939
|
-
isRoot: true,
|
|
940
|
-
children: [
|
|
941
|
-
{
|
|
942
|
-
type: 'nested',
|
|
943
|
-
id: '(lang)/products/(slug)/page',
|
|
944
|
-
path: 'products/:slug',
|
|
945
|
-
modernCanonicalPath: '/products/:slug',
|
|
946
|
-
},
|
|
947
|
-
],
|
|
948
|
-
},
|
|
949
|
-
] as any,
|
|
950
|
-
{ localeParamHeuristic: false },
|
|
951
|
-
);
|
|
952
|
-
|
|
953
|
-
expect(result).not.toBeNull();
|
|
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
|
-
});
|
|
971
|
-
});
|
|
972
|
-
|
|
973
|
-
test('strips leading :lang param and maps index under :lang to "/"', async () => {
|
|
974
|
-
const result = collectCanonicalRoutesForEntry([
|
|
975
|
-
{
|
|
976
|
-
type: 'nested',
|
|
977
|
-
id: 'layout',
|
|
978
|
-
isRoot: true,
|
|
979
|
-
children: [
|
|
980
|
-
{
|
|
981
|
-
type: 'nested',
|
|
982
|
-
id: '(lang)/layout',
|
|
983
|
-
path: ':lang',
|
|
984
|
-
children: [
|
|
985
|
-
{
|
|
986
|
-
type: 'nested',
|
|
987
|
-
id: '(lang)/page',
|
|
988
|
-
index: true,
|
|
989
|
-
},
|
|
990
|
-
{
|
|
991
|
-
type: 'nested',
|
|
992
|
-
id: '(lang)/about/page',
|
|
993
|
-
path: 'about',
|
|
994
|
-
},
|
|
995
|
-
],
|
|
996
|
-
},
|
|
997
|
-
],
|
|
998
|
-
},
|
|
999
|
-
] as any);
|
|
1000
296
|
|
|
297
|
+
const result = collectCanonicalRoutesForEntry(routes);
|
|
1001
298
|
expect(result).not.toBeNull();
|
|
1002
|
-
expect(Object.keys(result!)).toEqual([
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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
|
-
});
|
|
1018
|
-
});
|
|
1019
|
-
|
|
1020
|
-
test('converts :slug to $slug with required params type', async () => {
|
|
1021
|
-
const result = collectCanonicalRoutesForEntry([
|
|
1022
|
-
{
|
|
1023
|
-
type: 'nested',
|
|
1024
|
-
id: 'layout',
|
|
1025
|
-
isRoot: true,
|
|
1026
|
-
children: [
|
|
1027
|
-
{
|
|
1028
|
-
type: 'nested',
|
|
1029
|
-
id: '(lang)/layout',
|
|
1030
|
-
path: ':lang',
|
|
1031
|
-
children: [
|
|
1032
|
-
{
|
|
1033
|
-
type: 'nested',
|
|
1034
|
-
id: '(lang)/products/(slug)/page',
|
|
1035
|
-
path: 'products/:slug',
|
|
1036
|
-
},
|
|
1037
|
-
],
|
|
1038
|
-
},
|
|
1039
|
-
],
|
|
1040
|
-
},
|
|
1041
|
-
] as any);
|
|
1042
|
-
|
|
1043
|
-
expect(result).not.toBeNull();
|
|
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
|
-
});
|
|
1058
|
-
});
|
|
1059
|
-
|
|
1060
|
-
test('converts :slug? to optional {-$slug} with optional params type', async () => {
|
|
1061
|
-
const result = collectCanonicalRoutesForEntry([
|
|
1062
|
-
{
|
|
1063
|
-
type: 'nested',
|
|
1064
|
-
id: 'layout',
|
|
1065
|
-
isRoot: true,
|
|
1066
|
-
children: [
|
|
1067
|
-
{
|
|
1068
|
-
type: 'nested',
|
|
1069
|
-
id: '(lang)/layout',
|
|
1070
|
-
path: ':lang',
|
|
1071
|
-
children: [
|
|
1072
|
-
{
|
|
1073
|
-
type: 'nested',
|
|
1074
|
-
id: '(lang)/optional/(slug$)/page',
|
|
1075
|
-
path: 'optional/:slug?',
|
|
1076
|
-
},
|
|
1077
|
-
],
|
|
1078
|
-
},
|
|
1079
|
-
],
|
|
1080
|
-
},
|
|
1081
|
-
] as any);
|
|
1082
|
-
|
|
1083
|
-
expect(result).not.toBeNull();
|
|
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
|
-
});
|
|
1100
|
-
});
|
|
1101
|
-
|
|
1102
|
-
test('converts * splat to $ with optional _splat param', async () => {
|
|
1103
|
-
const result = collectCanonicalRoutesForEntry([
|
|
1104
|
-
{
|
|
1105
|
-
type: 'nested',
|
|
1106
|
-
id: 'layout',
|
|
1107
|
-
isRoot: true,
|
|
1108
|
-
children: [
|
|
1109
|
-
{
|
|
1110
|
-
type: 'nested',
|
|
1111
|
-
id: '(lang)/layout',
|
|
1112
|
-
path: ':lang',
|
|
1113
|
-
children: [
|
|
1114
|
-
{
|
|
1115
|
-
type: 'nested',
|
|
1116
|
-
id: '(lang)/files/page',
|
|
1117
|
-
path: 'files/*',
|
|
1118
|
-
},
|
|
1119
|
-
],
|
|
1120
|
-
},
|
|
1121
|
-
],
|
|
1122
|
-
},
|
|
1123
|
-
] as any);
|
|
1124
|
-
|
|
1125
|
-
expect(result).not.toBeNull();
|
|
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
|
-
});
|
|
299
|
+
expect(Object.keys(result!)).toEqual([
|
|
300
|
+
'/',
|
|
301
|
+
'/about',
|
|
302
|
+
'/files/$',
|
|
303
|
+
'/optional/{-$slug}',
|
|
304
|
+
]);
|
|
1142
305
|
});
|
|
1143
306
|
|
|
1144
|
-
test('collapses localized variants
|
|
1145
|
-
// This reuses the same fixture shape as the 'preserves typed child trees'
|
|
1146
|
-
// test but adds modernCanonicalPath fields as plugin-i18n now emits.
|
|
307
|
+
test('collapses localized variants to one canonical key with typed params', async () => {
|
|
1147
308
|
const result = collectCanonicalRoutesForEntry([
|
|
1148
309
|
{
|
|
1149
310
|
type: 'nested',
|
|
@@ -1180,69 +341,44 @@ describe('collectCanonicalRoutesForEntry', () => {
|
|
|
1180
341
|
] as any);
|
|
1181
342
|
|
|
1182
343
|
expect(result).not.toBeNull();
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
expect(keys).toHaveLength(2);
|
|
1188
|
-
expect(keys).toEqual(['/optional/{-$slug}', '/products/$slug']);
|
|
1189
|
-
// The Czech localized path must not appear as a separate key.
|
|
344
|
+
expect(Object.keys(result!)).toEqual([
|
|
345
|
+
'/optional/{-$slug}',
|
|
346
|
+
'/products/$slug',
|
|
347
|
+
]);
|
|
1190
348
|
expect('/produkty/$slug' in result!).toBe(false);
|
|
1191
349
|
|
|
1192
350
|
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-canonical-'));
|
|
1193
351
|
await typecheckCanonicalRegisterContract({
|
|
1194
|
-
canonicalRoutes:
|
|
352
|
+
canonicalRoutes: {
|
|
353
|
+
'/': 'Record<string, never>',
|
|
354
|
+
'/files/$': '{ _splat?: string }',
|
|
355
|
+
...result!,
|
|
356
|
+
},
|
|
1195
357
|
contractLines: [
|
|
358
|
+
"const rootParams: UltramodernCanonicalRoutes['/'] = {};",
|
|
359
|
+
'// @ts-expect-error index canonical routes reject unexpected params',
|
|
360
|
+
"const invalidRootParams: UltramodernCanonicalRoutes['/'] = { slug: 'unexpected' };",
|
|
1196
361
|
"const productParams: UltramodernCanonicalRoutes['/products/$slug'] = { slug: 'tractor' };",
|
|
1197
362
|
"const optionalParams: UltramodernCanonicalRoutes['/optional/{-$slug}'] = {};",
|
|
363
|
+
"const presentOptionalParams: UltramodernCanonicalRoutes['/optional/{-$slug}'] = { slug: 'tractor' };",
|
|
364
|
+
'// @ts-expect-error optional canonical params still retain their string type',
|
|
365
|
+
"const invalidOptionalParams: UltramodernCanonicalRoutes['/optional/{-$slug}'] = { slug: 42 };",
|
|
366
|
+
"const splatParams: UltramodernCanonicalRoutes['/files/$'] = { _splat: 'guides/intro' };",
|
|
1198
367
|
'// @ts-expect-error localized physical variants do not become canonical keys',
|
|
1199
368
|
"declare const localizedParams: UltramodernCanonicalRoutes['/produkty/$slug'];",
|
|
1200
369
|
'// @ts-expect-error the deduplicated product route still requires its param',
|
|
1201
370
|
"const missingProductParams: UltramodernCanonicalRoutes['/products/$slug'] = {};",
|
|
371
|
+
'void rootParams;',
|
|
1202
372
|
'void productParams;',
|
|
1203
373
|
'void optionalParams;',
|
|
374
|
+
'void presentOptionalParams;',
|
|
375
|
+
'void splatParams;',
|
|
376
|
+
'void invalidRootParams;',
|
|
377
|
+
'void invalidOptionalParams;',
|
|
1204
378
|
'void localizedParams;',
|
|
1205
379
|
'void missingProductParams;',
|
|
1206
380
|
],
|
|
1207
381
|
projectDirectory: tempDir,
|
|
1208
382
|
});
|
|
1209
383
|
});
|
|
1210
|
-
|
|
1211
|
-
test('output is sorted alphabetically by canonical key', () => {
|
|
1212
|
-
const result = collectCanonicalRoutesForEntry([
|
|
1213
|
-
{
|
|
1214
|
-
type: 'nested',
|
|
1215
|
-
id: 'layout',
|
|
1216
|
-
isRoot: true,
|
|
1217
|
-
children: [
|
|
1218
|
-
{
|
|
1219
|
-
type: 'nested',
|
|
1220
|
-
id: '(lang)/layout',
|
|
1221
|
-
path: ':lang',
|
|
1222
|
-
children: [
|
|
1223
|
-
{
|
|
1224
|
-
type: 'nested',
|
|
1225
|
-
id: '(lang)/products/(slug)/page',
|
|
1226
|
-
path: 'products/:slug',
|
|
1227
|
-
},
|
|
1228
|
-
{
|
|
1229
|
-
type: 'nested',
|
|
1230
|
-
id: '(lang)/about/page',
|
|
1231
|
-
path: 'about',
|
|
1232
|
-
},
|
|
1233
|
-
{
|
|
1234
|
-
type: 'nested',
|
|
1235
|
-
id: '(lang)/page',
|
|
1236
|
-
index: true,
|
|
1237
|
-
},
|
|
1238
|
-
],
|
|
1239
|
-
},
|
|
1240
|
-
],
|
|
1241
|
-
},
|
|
1242
|
-
] as any);
|
|
1243
|
-
|
|
1244
|
-
expect(result).not.toBeNull();
|
|
1245
|
-
const keys = Object.keys(result!);
|
|
1246
|
-
expect(keys).toEqual([...keys].sort((a, b) => a.localeCompare(b)));
|
|
1247
|
-
});
|
|
1248
384
|
});
|