@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.2 → 3.9.0-ultramodern.21
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/artifacts.js +15 -1
- package/dist/cjs/cli/index.js +12 -0
- package/dist/cjs/cli/routeMetadata.js +131 -0
- package/dist/cjs/cli/tanstackTypes/canonicalRoutes.js +7 -7
- package/dist/cjs/cli/tanstackTypes/shared.js +0 -14
- package/dist/cjs/cli/tanstackTypes/source.js +27 -60
- package/dist/cjs/runtime/basepathRewrite.js +13 -6
- package/dist/cjs/runtime/lifecycle.js +6 -9
- package/dist/cjs/runtime/loaderBridge.js +10 -4
- package/dist/cjs/runtime/navigation.js +102 -0
- package/dist/cjs/runtime/plugin.js +7 -2
- package/dist/cjs/runtime/plugin.node.js +86 -64
- package/dist/cjs/runtime/prefetchLink.js +2 -7
- package/dist/cjs/runtime/register.js +0 -2
- package/dist/cjs/runtime/routeTree/construction.js +19 -59
- package/dist/cjs/runtime/routeTree/loaders.js +1 -1
- package/dist/cjs/runtime/routeTree/paths.js +0 -6
- package/dist/cjs/runtime/router.js +25 -9
- package/dist/cjs/runtime/ssrManagedTags.js +2 -12
- package/dist/cjs/runtime/state.js +2 -2
- package/dist/cjs/runtime/utils.js +3 -0
- package/dist/cjs/shared/routeDescriptor.js +60 -0
- package/dist/esm/cli/artifacts.mjs +13 -2
- package/dist/esm/cli/index.mjs +12 -9
- package/dist/esm/cli/routeMetadata.mjs +71 -0
- package/dist/esm/cli/tanstackTypes/canonicalRoutes.mjs +7 -7
- package/dist/esm/cli/tanstackTypes/shared.mjs +1 -9
- package/dist/esm/cli/tanstackTypes/source.mjs +28 -61
- package/dist/esm/runtime/basepathRewrite.mjs +10 -3
- package/dist/esm/runtime/lifecycle.mjs +1 -1
- package/dist/esm/runtime/loaderBridge.mjs +8 -5
- package/dist/esm/runtime/navigation.mjs +64 -0
- package/dist/esm/runtime/plugin.mjs +7 -2
- package/dist/esm/runtime/plugin.node.mjs +88 -66
- package/dist/esm/runtime/prefetchLink.mjs +1 -6
- package/dist/esm/runtime/register.mjs +0 -2
- package/dist/esm/runtime/routeTree/construction.mjs +19 -59
- package/dist/esm/runtime/routeTree/loaders.mjs +2 -2
- package/dist/esm/runtime/routeTree/paths.mjs +1 -4
- package/dist/esm/runtime/router.mjs +27 -11
- package/dist/esm/runtime/ssrManagedTags.mjs +1 -8
- package/dist/esm/runtime/state.mjs +1 -1
- package/dist/esm/runtime/utils.mjs +3 -0
- package/dist/esm/shared/routeDescriptor.mjs +22 -0
- package/dist/esm-node/cli/artifacts.mjs +13 -2
- package/dist/esm-node/cli/index.mjs +12 -9
- package/dist/esm-node/cli/routeMetadata.mjs +72 -0
- package/dist/esm-node/cli/tanstackTypes/canonicalRoutes.mjs +7 -7
- package/dist/esm-node/cli/tanstackTypes/shared.mjs +1 -9
- package/dist/esm-node/cli/tanstackTypes/source.mjs +28 -61
- package/dist/esm-node/runtime/basepathRewrite.mjs +10 -3
- package/dist/esm-node/runtime/lifecycle.mjs +1 -1
- package/dist/esm-node/runtime/loaderBridge.mjs +8 -5
- package/dist/esm-node/runtime/navigation.mjs +65 -0
- package/dist/esm-node/runtime/plugin.mjs +7 -2
- package/dist/esm-node/runtime/plugin.node.mjs +88 -66
- 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 +19 -59
- package/dist/esm-node/runtime/routeTree/loaders.mjs +2 -2
- package/dist/esm-node/runtime/routeTree/paths.mjs +1 -4
- package/dist/esm-node/runtime/router.mjs +27 -11
- package/dist/esm-node/runtime/ssrManagedTags.mjs +1 -8
- package/dist/esm-node/runtime/state.mjs +1 -1
- package/dist/esm-node/runtime/utils.mjs +3 -0
- package/dist/esm-node/shared/routeDescriptor.mjs +23 -0
- package/dist/types/cli/artifacts.d.ts +13 -5
- package/dist/types/cli/index.d.ts +1 -1
- package/dist/types/cli/routeMetadata.d.ts +18 -0
- package/dist/types/cli/tanstackTypes/shared.d.ts +0 -2
- package/dist/types/runtime/basepathRewrite.d.ts +2 -1
- package/dist/types/runtime/lifecycle.d.ts +9 -2
- package/dist/types/runtime/loaderBridge.d.ts +1 -0
- package/dist/types/runtime/navigation.d.ts +4 -0
- package/dist/types/runtime/register.d.ts +1 -9
- package/dist/types/runtime/routeTree/paths.d.ts +0 -2
- package/dist/types/runtime/router.d.ts +2 -13
- package/dist/types/runtime/ssrManagedTags.d.ts +0 -1
- package/dist/types/runtime/ssrTypes.d.ts +0 -7
- package/dist/types/runtime/state.d.ts +1 -1
- package/dist/types/runtime/types.d.ts +2 -1
- package/dist/types/shared/routeDescriptor.d.ts +26 -0
- package/package.json +22 -19
- package/rstest.config.mts +2 -24
- package/src/cli/artifacts.ts +53 -6
- package/src/cli/index.ts +11 -2
- package/src/cli/routeMetadata.ts +121 -0
- package/src/cli/tanstackTypes/canonicalRoutes.ts +17 -12
- package/src/cli/tanstackTypes/shared.ts +0 -14
- package/src/cli/tanstackTypes/source.ts +42 -116
- package/src/runtime/basepathRewrite.ts +11 -2
- package/src/runtime/lifecycle.ts +14 -4
- package/src/runtime/loaderBridge.ts +9 -9
- package/src/runtime/navigation.ts +78 -0
- package/src/runtime/plugin.node.tsx +120 -93
- package/src/runtime/plugin.tsx +11 -1
- package/src/runtime/prefetchLink.tsx +1 -23
- package/src/runtime/register.ts +1 -12
- package/src/runtime/routeTree/construction.ts +32 -110
- package/src/runtime/routeTree/loaders.ts +2 -4
- package/src/runtime/routeTree/paths.ts +0 -6
- package/src/runtime/router.ts +25 -40
- package/src/runtime/ssrManagedTags.ts +0 -17
- package/src/runtime/ssrTypes.ts +0 -7
- package/src/runtime/state.ts +1 -1
- package/src/runtime/types.ts +2 -2
- package/src/runtime/utils.tsx +4 -1
- package/src/shared/routeDescriptor.ts +46 -0
- package/tests/router/cli.test.ts +148 -824
- package/tests/router/clientNavigation.ts +13 -0
- package/tests/router/dataMutation.test.tsx +17 -85
- package/tests/router/flightSerialization.roundtrip.test.tsx +9 -6
- package/tests/router/generateRouteArtifacts.test.ts +139 -27
- package/tests/router/loaderBridge.test.ts +0 -25
- package/tests/router/localisedIdentity.test.tsx +100 -0
- package/tests/router/prefetchLink.test.tsx +46 -21
- package/tests/router/prefetchLinkPreload.test.tsx +11 -32
- package/tests/router/routeHooks.test.ts +42 -105
- package/tests/router/routeMetadata.test.ts +233 -0
- package/tests/router/routeTree.test.ts +144 -836
- package/tests/router/rsc.test.tsx +131 -166
- package/tests/router/serverPlugin.test.ts +193 -1
- package/tests/router/tanstackTypes.test.ts +218 -963
- package/dist/esm/rslib-runtime~1.mjs +0 -18
- package/dist/esm-node/rslib-runtime~0.mjs +0 -19
- 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
|
@@ -1,50 +1,22 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
5
6
|
import { promisify } from 'node:util';
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
7
|
+
import { applyLocalisedUrlsToRoutes } from '@modern-js/i18n-runtime-extensions';
|
|
8
|
+
import { createMemoryHistory, createRouter } from '@tanstack/react-router';
|
|
32
9
|
import { writeTanstackRegisterFile } from '../../src/cli/artifacts';
|
|
33
10
|
import {
|
|
34
11
|
collectCanonicalRoutesForEntry,
|
|
35
12
|
generateTanstackRouterTypesSourceForEntry,
|
|
36
13
|
} from '../../src/cli/tanstackTypes';
|
|
14
|
+
import { createModernBasepathRewrite } from '../../src/runtime/basepathRewrite';
|
|
15
|
+
import { createRouteTreeFromRouteObjects } from '../../src/runtime/routeTree';
|
|
16
|
+
import { createTanstackRouteObjectsFromConfig } from '../../src/runtime/utils';
|
|
17
|
+
import { navigateOnClient } from './clientNavigation';
|
|
37
18
|
|
|
38
19
|
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
20
|
const strictestTsconfigPath = path.resolve(
|
|
49
21
|
__dirname,
|
|
50
22
|
'../../node_modules/@tsconfig/strictest/tsconfig.json',
|
|
@@ -54,195 +26,51 @@ const installedReactRouterPath = path.resolve(
|
|
|
54
26
|
'../../node_modules/@tanstack/react-router',
|
|
55
27
|
);
|
|
56
28
|
|
|
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(
|
|
29
|
+
async function writeTsconfig(
|
|
134
30
|
projectDirectory: string,
|
|
31
|
+
compilerOptions: Record<string, unknown>,
|
|
135
32
|
) {
|
|
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
33
|
await writeFile(
|
|
192
34
|
path.join(projectDirectory, 'tsconfig.json'),
|
|
193
35
|
JSON.stringify(
|
|
194
36
|
{
|
|
195
37
|
extends: strictestTsconfigPath,
|
|
196
38
|
compilerOptions: {
|
|
197
|
-
lib: ['ESNext', 'DOM'],
|
|
198
39
|
jsx: 'react-jsx',
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
rootDir: 'src',
|
|
203
|
-
target: 'ES2022',
|
|
40
|
+
lib: ['ESNext', 'DOM'],
|
|
41
|
+
noEmit: true,
|
|
42
|
+
target: 'ESNext',
|
|
204
43
|
types: [],
|
|
205
|
-
|
|
44
|
+
...compilerOptions,
|
|
206
45
|
},
|
|
207
|
-
include: ['src/**/*.ts', 'src/**/*.tsx'],
|
|
46
|
+
include: ['src/**/*.ts', 'src/**/*.tsx', 'src/**/*.d.ts'],
|
|
208
47
|
},
|
|
209
48
|
null,
|
|
210
49
|
2,
|
|
211
50
|
),
|
|
212
51
|
);
|
|
52
|
+
}
|
|
213
53
|
|
|
54
|
+
/** Runs the same compiler a consumer build runs; surfaces its diagnostics. */
|
|
55
|
+
async function runTsgo(projectDirectory: string) {
|
|
214
56
|
try {
|
|
215
57
|
await execFileAsync(
|
|
216
58
|
process.platform === 'win32' ? 'tsgo.cmd' : 'tsgo',
|
|
217
59
|
['-p', 'tsconfig.json'],
|
|
218
|
-
{
|
|
219
|
-
cwd: projectDirectory,
|
|
220
|
-
shell: process.platform === 'win32',
|
|
221
|
-
},
|
|
60
|
+
{ cwd: projectDirectory, shell: process.platform === 'win32' },
|
|
222
61
|
);
|
|
223
62
|
} 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
|
-
}
|
|
63
|
+
const { stdout, stderr } = (error ?? {}) as {
|
|
64
|
+
stdout?: string;
|
|
65
|
+
stderr?: string;
|
|
66
|
+
};
|
|
67
|
+
if (stdout || stderr) {
|
|
68
|
+
throw new Error([stdout, stderr].filter(Boolean).join('\n'), {
|
|
69
|
+
cause: error,
|
|
70
|
+
});
|
|
238
71
|
}
|
|
239
72
|
throw error;
|
|
240
73
|
}
|
|
241
|
-
await execFileAsync(
|
|
242
|
-
process.execPath,
|
|
243
|
-
[path.join(projectDirectory, 'dist', 'runtime-check.js')],
|
|
244
|
-
{ cwd: projectDirectory },
|
|
245
|
-
);
|
|
246
74
|
}
|
|
247
75
|
|
|
248
76
|
async function compileGeneratedRouterAgainstInstalledDeclarations(options: {
|
|
@@ -258,63 +86,48 @@ async function compileGeneratedRouterAgainstInstalledDeclarations(options: {
|
|
|
258
86
|
);
|
|
259
87
|
await mkdir(generatedDirectory, { recursive: true });
|
|
260
88
|
await writeFile(path.join(generatedDirectory, 'router.gen.ts'), routerGenTs);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
89
|
+
|
|
90
|
+
// The runtime package re-exports the *installed* @tanstack/react-router
|
|
91
|
+
// declarations, so the generated file is checked against the real router
|
|
92
|
+
// types a consumer resolves — not a stand-in.
|
|
93
|
+
const runtimePackageDirectory = path.join(
|
|
94
|
+
projectDirectory,
|
|
95
|
+
'node_modules',
|
|
96
|
+
'@modern-js',
|
|
97
|
+
'plugin-tanstack',
|
|
265
98
|
);
|
|
99
|
+
await mkdir(runtimePackageDirectory, { recursive: true });
|
|
266
100
|
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'],
|
|
101
|
+
path.join(runtimePackageDirectory, 'package.json'),
|
|
102
|
+
JSON.stringify({
|
|
103
|
+
name: '@modern-js/plugin-tanstack',
|
|
104
|
+
type: 'commonjs',
|
|
105
|
+
exports: {
|
|
106
|
+
'./runtime': { types: './runtime.d.ts', default: './runtime.js' },
|
|
285
107
|
},
|
|
286
|
-
|
|
287
|
-
2,
|
|
288
|
-
),
|
|
108
|
+
}),
|
|
289
109
|
);
|
|
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
|
-
}
|
|
110
|
+
await writeFile(
|
|
111
|
+
path.join(runtimePackageDirectory, 'runtime.d.ts'),
|
|
112
|
+
[
|
|
113
|
+
"export { createMemoryHistory, createRootRouteWithContext, createRoute, createRouter } from '@tanstack/react-router';",
|
|
114
|
+
'export type ModernRouterContext = { request?: Request; requestContext?: unknown };',
|
|
115
|
+
'export const modernTanstackRouterFastDefaults: { readonly defaultStructuralSharing: true };',
|
|
116
|
+
'export function createRouteStaticData<TData extends { modernRouteId?: string; modernRouteAction?: unknown; modernRouteLoader?: unknown }>(data: TData): TData;',
|
|
117
|
+
'export function modernLoaderToTanstack<TLoader extends (...args: any[]) => any>(options: { hasSplat: boolean }, modernLoader: TLoader): (context: unknown) => Promise<Awaited<ReturnType<TLoader>>>;',
|
|
118
|
+
].join('\n'),
|
|
119
|
+
);
|
|
120
|
+
await writeFile(
|
|
121
|
+
path.join(projectDirectory, 'package.json'),
|
|
122
|
+
JSON.stringify({ private: true, type: 'commonjs' }),
|
|
123
|
+
);
|
|
124
|
+
await writeTsconfig(projectDirectory, {
|
|
125
|
+
module: 'Node16',
|
|
126
|
+
moduleResolution: 'Node16',
|
|
127
|
+
paths: { '@tanstack/react-router': [installedReactRouterPath] },
|
|
128
|
+
verbatimModuleSyntax: false,
|
|
129
|
+
});
|
|
130
|
+
await runTsgo(projectDirectory);
|
|
318
131
|
}
|
|
319
132
|
|
|
320
133
|
async function typecheckCanonicalRegisterContract(options: {
|
|
@@ -324,11 +137,11 @@ async function typecheckCanonicalRegisterContract(options: {
|
|
|
324
137
|
}) {
|
|
325
138
|
const { canonicalRoutes, contractLines, projectDirectory } = options;
|
|
326
139
|
const srcDirectory = path.join(projectDirectory, 'src');
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
140
|
+
await mkdir(path.join(srcDirectory, 'modern-tanstack', 'index'), {
|
|
141
|
+
recursive: true,
|
|
142
|
+
});
|
|
330
143
|
await writeFile(
|
|
331
|
-
path.join(
|
|
144
|
+
path.join(srcDirectory, 'modern-tanstack', 'index', 'router.gen.ts'),
|
|
332
145
|
'export const router = { context: {} };',
|
|
333
146
|
);
|
|
334
147
|
await writeTanstackRegisterFile({
|
|
@@ -350,56 +163,14 @@ async function typecheckCanonicalRegisterContract(options: {
|
|
|
350
163
|
...contractLines,
|
|
351
164
|
].join('\n'),
|
|
352
165
|
);
|
|
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
|
-
}
|
|
166
|
+
await writeTsconfig(projectDirectory, {
|
|
167
|
+
module: 'Preserve',
|
|
168
|
+
moduleResolution: 'Bundler',
|
|
169
|
+
});
|
|
170
|
+
await runTsgo(projectDirectory);
|
|
400
171
|
}
|
|
401
172
|
|
|
402
|
-
async function
|
|
173
|
+
async function generateComprehensiveRouterGen(srcDirectory: string) {
|
|
403
174
|
const files = new Map([
|
|
404
175
|
[
|
|
405
176
|
'routes/(app)/layout.tsx',
|
|
@@ -428,22 +199,14 @@ async function writeComprehensiveRouterFixture(srcDirectory: string) {
|
|
|
428
199
|
].join('\n'),
|
|
429
200
|
],
|
|
430
201
|
]);
|
|
431
|
-
|
|
432
202
|
for (const [relativePath, contents] of files) {
|
|
433
203
|
const filePath = path.join(srcDirectory, relativePath);
|
|
434
204
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
435
205
|
await writeFile(filePath, contents);
|
|
436
206
|
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
async function generateComprehensiveRouterGen(srcDirectory: string) {
|
|
440
|
-
await writeComprehensiveRouterFixture(srcDirectory);
|
|
441
207
|
|
|
442
208
|
const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
|
|
443
|
-
appContext: {
|
|
444
|
-
srcDirectory,
|
|
445
|
-
internalSrcAlias: '@/_',
|
|
446
|
-
} as any,
|
|
209
|
+
appContext: { srcDirectory, internalSrcAlias: '@/_' } as any,
|
|
447
210
|
entryName: 'golden',
|
|
448
211
|
routes: [
|
|
449
212
|
{
|
|
@@ -486,402 +249,138 @@ describe('tanstack router type generation', () => {
|
|
|
486
249
|
let tempDir: string | undefined;
|
|
487
250
|
|
|
488
251
|
afterEach(async () => {
|
|
489
|
-
resolverDelays.clear();
|
|
490
252
|
if (tempDir) {
|
|
491
253
|
await rm(tempDir, { recursive: true, force: true });
|
|
492
254
|
tempDir = undefined;
|
|
493
255
|
}
|
|
494
256
|
});
|
|
495
257
|
|
|
496
|
-
test('strictly compiles
|
|
258
|
+
test('strictly compiles generated routes against installed router declarations', async () => {
|
|
497
259
|
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
|
|
498
|
-
const
|
|
499
|
-
|
|
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
|
-
});
|
|
260
|
+
const routerGenTs = await generateComprehensiveRouterGen(
|
|
261
|
+
path.join(tempDir, 'src'),
|
|
262
|
+
);
|
|
546
263
|
|
|
547
|
-
await
|
|
264
|
+
await compileGeneratedRouterAgainstInstalledDeclarations({
|
|
548
265
|
projectDirectory: tempDir,
|
|
549
266
|
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
267
|
});
|
|
566
268
|
});
|
|
567
269
|
|
|
568
|
-
test('
|
|
569
|
-
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-
|
|
570
|
-
const
|
|
571
|
-
const routeDir = path.join(srcDirectory, 'routes', 'mf');
|
|
572
|
-
await mkdir(routeDir, { recursive: true });
|
|
573
|
-
await writeFile(
|
|
574
|
-
path.join(routeDir, 'page.data.ts'),
|
|
270
|
+
test('generated and runtime trees agree on localized splats, root siblings, layouts and params', async () => {
|
|
271
|
+
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-parity-'));
|
|
272
|
+
const localized = applyLocalisedUrlsToRoutes(
|
|
575
273
|
[
|
|
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
274
|
{
|
|
589
275
|
type: 'nested',
|
|
590
|
-
id: '
|
|
591
|
-
|
|
276
|
+
id: 'lang',
|
|
277
|
+
path: ':lang',
|
|
592
278
|
children: [
|
|
593
279
|
{
|
|
594
280
|
type: 'nested',
|
|
595
|
-
id: '
|
|
596
|
-
path: '
|
|
597
|
-
|
|
598
|
-
action: '@/_/routes/mf/page.data',
|
|
281
|
+
id: 'products',
|
|
282
|
+
path: 'products',
|
|
283
|
+
children: [{ type: 'nested', id: 'product-files', path: '*' }],
|
|
599
284
|
},
|
|
285
|
+
{ type: 'nested', id: 'fallback', path: '*' },
|
|
600
286
|
],
|
|
601
287
|
},
|
|
602
|
-
]
|
|
288
|
+
],
|
|
289
|
+
['en', 'cs'],
|
|
290
|
+
{ '/products': { en: '/products', cs: '/produkty' } },
|
|
291
|
+
'canonical',
|
|
292
|
+
);
|
|
293
|
+
const routes = [
|
|
294
|
+
{
|
|
295
|
+
type: 'nested',
|
|
296
|
+
id: 'layout',
|
|
297
|
+
isRoot: true,
|
|
298
|
+
children: [
|
|
299
|
+
...localized,
|
|
300
|
+
{
|
|
301
|
+
type: 'nested',
|
|
302
|
+
children: [{ type: 'nested', path: 'one/:id?', index: false }],
|
|
303
|
+
},
|
|
304
|
+
{ type: 'nested', children: [{ type: 'nested', path: 'two/*' }] },
|
|
305
|
+
{ type: 'nested', id: 'index', index: true },
|
|
306
|
+
],
|
|
307
|
+
},
|
|
308
|
+
{ type: 'nested', id: 'sibling', path: 'outside/:lang' },
|
|
309
|
+
] as any;
|
|
310
|
+
const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
|
|
311
|
+
appContext: {
|
|
312
|
+
srcDirectory: path.join(tempDir, 'src'),
|
|
313
|
+
internalSrcAlias: '@/_',
|
|
314
|
+
} as any,
|
|
315
|
+
entryName: 'parity',
|
|
316
|
+
routes,
|
|
603
317
|
});
|
|
604
|
-
|
|
605
|
-
await
|
|
318
|
+
const generated = path.join(tempDir, 'router.gen.ts');
|
|
319
|
+
await writeFile(generated, routerGenTs);
|
|
320
|
+
const modules = path.join(tempDir, 'node_modules', '@modern-js');
|
|
321
|
+
await mkdir(modules, { recursive: true });
|
|
322
|
+
await symlink(
|
|
323
|
+
path.resolve(__dirname, '../..'),
|
|
324
|
+
path.join(modules, 'plugin-tanstack'),
|
|
325
|
+
'junction',
|
|
326
|
+
);
|
|
327
|
+
const { stdout } = await execFileAsync(process.execPath, [
|
|
328
|
+
'--input-type=module',
|
|
329
|
+
'--eval',
|
|
330
|
+
`
|
|
331
|
+
import { router } from ${JSON.stringify(pathToFileURL(generated).href)};
|
|
332
|
+
import { createModernBasepathRewrite } from ${JSON.stringify(pathToFileURL(path.resolve(__dirname, '../../dist/esm/runtime/basepathRewrite.mjs')).href)};
|
|
333
|
+
router.update({ rewrite: createModernBasepathRewrite('/base', false, ${JSON.stringify(routes)}) });
|
|
334
|
+
router.history.push(router.buildLocation({ to: '/cs/produkty/a/b', search: { q: 'tractor' }, hash: 'files' }).publicHref); router.updateLatestLocation(); await router.load();
|
|
335
|
+
console.log(JSON.stringify({ ids: Object.keys(router.routesById).sort(), match: router.state.matches.at(-1).routeId, params: router.state.matches.at(-1).params, href: router.history.location.href }));
|
|
336
|
+
`,
|
|
337
|
+
]);
|
|
338
|
+
const routeObjects = createTanstackRouteObjectsFromConfig({
|
|
339
|
+
routesConfig: { routes },
|
|
340
|
+
})!;
|
|
341
|
+
const runtime = createRouter({
|
|
342
|
+
routeTree: createRouteTreeFromRouteObjects(routeObjects),
|
|
343
|
+
history: createMemoryHistory({
|
|
344
|
+
initialEntries: ['/base/cs/produkty/a/b?q=tractor#files'],
|
|
345
|
+
}),
|
|
346
|
+
rewrite: createModernBasepathRewrite('/base', false, routeObjects),
|
|
347
|
+
});
|
|
348
|
+
await runtime.load();
|
|
349
|
+
expect(JSON.parse(stdout)).toEqual({
|
|
350
|
+
ids: Object.keys(runtime.routesById).sort(),
|
|
351
|
+
match: runtime.state.matches.at(-1)?.routeId,
|
|
352
|
+
params: runtime.state.matches.at(-1)?.params,
|
|
353
|
+
href: runtime.history.location.href,
|
|
354
|
+
});
|
|
355
|
+
expect(runtime.state.matches.at(-1)?.routeId).toBe('/$lang/products/$');
|
|
356
|
+
expect(runtime.state.matches.at(-1)?.params).toMatchObject({
|
|
357
|
+
lang: 'cs',
|
|
358
|
+
_splat: 'a/b',
|
|
359
|
+
});
|
|
360
|
+
await navigateOnClient(runtime, { to: '/cs/missing/path' });
|
|
361
|
+
expect(runtime.state.matches.at(-1)?.routeId).toBe('/$lang/$');
|
|
362
|
+
expect(runtime.state.matches.at(-1)?.params).toMatchObject({
|
|
363
|
+
lang: 'cs',
|
|
364
|
+
_splat: 'missing/path',
|
|
365
|
+
});
|
|
366
|
+
await navigateOnClient(runtime, { to: '/outside/cs' });
|
|
367
|
+
expect(runtime.state.matches.at(-1)?.params).toEqual({ lang: 'cs' });
|
|
368
|
+
const canonicalRoutes = collectCanonicalRoutesForEntry(localized as any)!;
|
|
369
|
+
expect(Object.keys(canonicalRoutes)).toEqual(['/$', '/products/$']);
|
|
370
|
+
await typecheckCanonicalRegisterContract({
|
|
606
371
|
projectDirectory: tempDir,
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
"
|
|
610
|
-
"
|
|
611
|
-
|
|
612
|
-
'',
|
|
613
|
-
'
|
|
614
|
-
|
|
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'),
|
|
372
|
+
canonicalRoutes,
|
|
373
|
+
contractLines: [
|
|
374
|
+
"const product: UltramodernCanonicalRoutes['/products/$'] = { _splat: 'a/b' };",
|
|
375
|
+
"const fallback: UltramodernCanonicalRoutes['/$'] = { _splat: 'unknown/path' };",
|
|
376
|
+
'// @ts-expect-error splat remains a string',
|
|
377
|
+
"const invalid: UltramodernCanonicalRoutes['/products/$'] = { _splat: 42 };",
|
|
378
|
+
'void product; void fallback; void invalid;',
|
|
379
|
+
],
|
|
624
380
|
});
|
|
625
381
|
});
|
|
626
382
|
|
|
627
|
-
test('
|
|
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
|
-
test('strictly compiles generated routes against installed router declarations', async () => {
|
|
744
|
-
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
|
|
745
|
-
const srcDirectory = path.join(tempDir, 'src');
|
|
746
|
-
const routerGenTs = await generateComprehensiveRouterGen(srcDirectory);
|
|
747
|
-
|
|
748
|
-
await compileGeneratedRouterAgainstInstalledDeclarations({
|
|
749
|
-
projectDirectory: tempDir,
|
|
750
|
-
routerGenTs,
|
|
751
|
-
});
|
|
752
|
-
});
|
|
753
|
-
|
|
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',
|
|
763
|
-
);
|
|
764
|
-
|
|
765
|
-
await mkdir(path.dirname(routerGenPath), { recursive: true });
|
|
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({
|
|
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
|
-
});
|
|
847
|
-
});
|
|
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
|
-
|
|
860
|
-
test('returns null for a route tree with no locale param and no canonical metadata', () => {
|
|
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 () => {
|
|
383
|
+
test('maps a locale-prefixed route tree to the canonical paths consumers navigate to', () => {
|
|
885
384
|
const routes = [
|
|
886
385
|
{
|
|
887
386
|
type: 'nested',
|
|
@@ -893,257 +392,38 @@ describe('collectCanonicalRoutesForEntry', () => {
|
|
|
893
392
|
id: '(lang)/layout',
|
|
894
393
|
path: ':lang',
|
|
895
394
|
children: [
|
|
395
|
+
{ type: 'nested', id: '(lang)/page', index: true },
|
|
396
|
+
{ type: 'nested', id: '(lang)/about/page', path: 'about' },
|
|
896
397
|
{
|
|
897
398
|
type: 'nested',
|
|
898
|
-
id: '(lang)/
|
|
899
|
-
path: '
|
|
399
|
+
id: '(lang)/optional/(slug$)/page',
|
|
400
|
+
path: 'optional/:slug?',
|
|
900
401
|
},
|
|
402
|
+
{ type: 'nested', id: '(lang)/files/page', path: 'files/*' },
|
|
901
403
|
],
|
|
902
404
|
},
|
|
903
405
|
],
|
|
904
406
|
},
|
|
905
407
|
] as any;
|
|
906
408
|
|
|
907
|
-
// Without plugin-i18n, a hand-rolled `/:lang/` param must NOT
|
|
908
|
-
//
|
|
409
|
+
// Without plugin-i18n installed, a hand-rolled `/:lang/` param must NOT
|
|
410
|
+
// produce a canonical surface — the emitted module augmentation would
|
|
411
|
+
// break typechecking in an app that never opted into i18n.
|
|
909
412
|
expect(
|
|
910
413
|
collectCanonicalRoutesForEntry(routes, { localeParamHeuristic: false }),
|
|
911
414
|
).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
415
|
|
|
416
|
+
const result = collectCanonicalRoutesForEntry(routes);
|
|
953
417
|
expect(result).not.toBeNull();
|
|
954
|
-
expect(Object.keys(result!)).toEqual([
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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
|
-
|
|
1001
|
-
expect(result).not.toBeNull();
|
|
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
|
-
});
|
|
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
|
-
});
|
|
418
|
+
expect(Object.keys(result!)).toEqual([
|
|
419
|
+
'/',
|
|
420
|
+
'/about',
|
|
421
|
+
'/files/$',
|
|
422
|
+
'/optional/{-$slug}',
|
|
423
|
+
]);
|
|
1142
424
|
});
|
|
1143
425
|
|
|
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.
|
|
426
|
+
test('collapses localized variants to one canonical key with typed params', async () => {
|
|
1147
427
|
const result = collectCanonicalRoutesForEntry([
|
|
1148
428
|
{
|
|
1149
429
|
type: 'nested',
|
|
@@ -1180,69 +460,44 @@ describe('collectCanonicalRoutesForEntry', () => {
|
|
|
1180
460
|
] as any);
|
|
1181
461
|
|
|
1182
462
|
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.
|
|
463
|
+
expect(Object.keys(result!)).toEqual([
|
|
464
|
+
'/optional/{-$slug}',
|
|
465
|
+
'/products/$slug',
|
|
466
|
+
]);
|
|
1190
467
|
expect('/produkty/$slug' in result!).toBe(false);
|
|
1191
468
|
|
|
1192
469
|
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-canonical-'));
|
|
1193
470
|
await typecheckCanonicalRegisterContract({
|
|
1194
|
-
canonicalRoutes:
|
|
471
|
+
canonicalRoutes: {
|
|
472
|
+
'/': 'Record<string, never>',
|
|
473
|
+
'/files/$': '{ _splat?: string }',
|
|
474
|
+
...result!,
|
|
475
|
+
},
|
|
1195
476
|
contractLines: [
|
|
477
|
+
"const rootParams: UltramodernCanonicalRoutes['/'] = {};",
|
|
478
|
+
'// @ts-expect-error index canonical routes reject unexpected params',
|
|
479
|
+
"const invalidRootParams: UltramodernCanonicalRoutes['/'] = { slug: 'unexpected' };",
|
|
1196
480
|
"const productParams: UltramodernCanonicalRoutes['/products/$slug'] = { slug: 'tractor' };",
|
|
1197
481
|
"const optionalParams: UltramodernCanonicalRoutes['/optional/{-$slug}'] = {};",
|
|
482
|
+
"const presentOptionalParams: UltramodernCanonicalRoutes['/optional/{-$slug}'] = { slug: 'tractor' };",
|
|
483
|
+
'// @ts-expect-error optional canonical params still retain their string type',
|
|
484
|
+
"const invalidOptionalParams: UltramodernCanonicalRoutes['/optional/{-$slug}'] = { slug: 42 };",
|
|
485
|
+
"const splatParams: UltramodernCanonicalRoutes['/files/$'] = { _splat: 'guides/intro' };",
|
|
1198
486
|
'// @ts-expect-error localized physical variants do not become canonical keys',
|
|
1199
487
|
"declare const localizedParams: UltramodernCanonicalRoutes['/produkty/$slug'];",
|
|
1200
488
|
'// @ts-expect-error the deduplicated product route still requires its param',
|
|
1201
489
|
"const missingProductParams: UltramodernCanonicalRoutes['/products/$slug'] = {};",
|
|
490
|
+
'void rootParams;',
|
|
1202
491
|
'void productParams;',
|
|
1203
492
|
'void optionalParams;',
|
|
493
|
+
'void presentOptionalParams;',
|
|
494
|
+
'void splatParams;',
|
|
495
|
+
'void invalidRootParams;',
|
|
496
|
+
'void invalidOptionalParams;',
|
|
1204
497
|
'void localizedParams;',
|
|
1205
498
|
'void missingProductParams;',
|
|
1206
499
|
],
|
|
1207
500
|
projectDirectory: tempDir,
|
|
1208
501
|
});
|
|
1209
502
|
});
|
|
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
503
|
});
|