@bleedingdev/modern-js-plugin-tanstack 3.2.0-ultramodern.12 → 3.2.0-ultramodern.120
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 +47 -9
- package/dist/cjs/cli/routeSplitting.js +87 -0
- package/dist/cjs/cli/tanstackTypes.js +230 -63
- package/dist/cjs/cli.js +12 -8
- package/dist/cjs/runtime/DefaultNotFound.js +9 -5
- package/dist/cjs/runtime/basepathRewrite.js +12 -8
- package/dist/cjs/runtime/dataMutation.js +9 -5
- package/dist/cjs/runtime/hooks.js +9 -5
- package/dist/cjs/runtime/hydrationBoundary.js +48 -0
- package/dist/cjs/runtime/index.js +330 -74
- package/dist/cjs/runtime/lifecycle.js +15 -11
- package/dist/cjs/runtime/outlet.js +58 -0
- package/dist/cjs/runtime/plugin.js +203 -98
- package/dist/cjs/runtime/plugin.node.js +38 -16
- package/dist/cjs/runtime/plugin.worker.js +53 -0
- package/dist/cjs/runtime/prefetchLink.js +10 -6
- package/dist/cjs/runtime/routeTree.js +81 -17
- package/dist/cjs/runtime/rsc/ClientSlot.js +9 -5
- package/dist/cjs/runtime/rsc/CompositeComponent.js +9 -5
- package/dist/cjs/runtime/rsc/ReplayableStream.js +14 -9
- package/dist/cjs/runtime/rsc/RscNodeRenderer.js +9 -5
- package/dist/cjs/runtime/rsc/SlotContext.js +9 -5
- package/dist/cjs/runtime/rsc/client.js +9 -5
- package/dist/cjs/runtime/rsc/createRscProxy.js +9 -5
- package/dist/cjs/runtime/rsc/index.js +9 -5
- package/dist/cjs/runtime/rsc/payloadRouter.js +9 -5
- package/dist/cjs/runtime/rsc/server.js +9 -5
- package/dist/cjs/runtime/rsc/slotUsageSanitizer.js +9 -5
- package/dist/cjs/runtime/rsc/symbols.js +20 -15
- package/dist/cjs/runtime/types.js +31 -1
- package/dist/cjs/runtime/utils.js +9 -5
- package/dist/cjs/runtime.js +9 -5
- package/dist/esm/cli/index.mjs +28 -6
- package/dist/esm/cli/routeSplitting.mjs +43 -0
- package/dist/esm/cli/tanstackTypes.mjs +219 -59
- package/dist/esm/runtime/hydrationBoundary.mjs +10 -0
- package/dist/esm/runtime/index.mjs +3 -2
- package/dist/esm/runtime/outlet.mjs +17 -0
- package/dist/esm/runtime/plugin.mjs +197 -96
- package/dist/esm/runtime/plugin.node.mjs +30 -12
- package/dist/esm/runtime/plugin.worker.mjs +1 -0
- package/dist/esm/runtime/prefetchLink.mjs +1 -1
- package/dist/esm/runtime/routeTree.mjs +73 -13
- package/dist/esm/runtime/types.mjs +7 -0
- package/dist/esm-node/cli/index.mjs +28 -6
- package/dist/esm-node/cli/routeSplitting.mjs +44 -0
- package/dist/esm-node/cli/tanstackTypes.mjs +219 -59
- package/dist/esm-node/runtime/hydrationBoundary.mjs +11 -0
- package/dist/esm-node/runtime/index.mjs +3 -2
- package/dist/esm-node/runtime/outlet.mjs +18 -0
- package/dist/esm-node/runtime/plugin.mjs +197 -96
- package/dist/esm-node/runtime/plugin.node.mjs +30 -12
- package/dist/esm-node/runtime/plugin.worker.mjs +2 -0
- package/dist/esm-node/runtime/prefetchLink.mjs +1 -1
- package/dist/esm-node/runtime/routeTree.mjs +73 -13
- package/dist/esm-node/runtime/types.mjs +7 -0
- package/dist/types/cli/index.d.ts +7 -1
- package/dist/types/cli/routeSplitting.d.ts +29 -0
- package/dist/types/cli/tanstackTypes.d.ts +9 -0
- package/dist/types/runtime/hooks.d.ts +9 -24
- package/dist/types/runtime/hydrationBoundary.d.ts +2 -0
- package/dist/types/runtime/index.d.ts +5 -2
- package/dist/types/runtime/outlet.d.ts +2 -0
- package/dist/types/runtime/plugin.d.ts +1 -1
- package/dist/types/runtime/plugin.node.d.ts +1 -1
- package/dist/types/runtime/plugin.worker.d.ts +1 -0
- package/dist/types/runtime/types.d.ts +7 -0
- package/package.json +20 -20
- package/src/cli/index.ts +59 -2
- package/src/cli/routeSplitting.ts +81 -0
- package/src/cli/tanstackTypes.ts +347 -67
- package/src/runtime/hydrationBoundary.tsx +12 -0
- package/src/runtime/index.tsx +107 -2
- package/src/runtime/outlet.tsx +48 -0
- package/src/runtime/plugin.node.tsx +58 -8
- package/src/runtime/plugin.tsx +372 -157
- package/src/runtime/plugin.worker.tsx +4 -0
- package/src/runtime/prefetchLink.tsx +1 -1
- package/src/runtime/routeTree.ts +194 -23
- package/src/runtime/ssr-shim.d.ts +1 -3
- package/src/runtime/types.ts +13 -0
- package/tests/router/cli.test.ts +315 -0
- package/tests/router/fastDefaults.test.ts +25 -0
- package/tests/router/hydrationBoundary.test.tsx +23 -0
- package/tests/router/prefetchLink.test.tsx +43 -7
- package/tests/router/routeTree.test.ts +416 -1
- package/tests/router/tanstackTypes.test.ts +415 -1
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
1
2
|
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
3
|
import { tmpdir } from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import {
|
|
7
|
+
collectCanonicalRoutesForEntry,
|
|
8
|
+
generateTanstackRouterTypesSourceForEntry,
|
|
9
|
+
} from '../../src/cli/tanstackTypes';
|
|
10
|
+
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
5
12
|
|
|
6
13
|
describe('tanstack router type generation', () => {
|
|
7
14
|
let tempDir: string | undefined;
|
|
@@ -55,8 +62,415 @@ describe('tanstack router type generation', () => {
|
|
|
55
62
|
);
|
|
56
63
|
expect(routerGenTs).toContain('modernRouteLoader: loader_0');
|
|
57
64
|
expect(routerGenTs).toContain('modernRouteAction: action_0');
|
|
65
|
+
expect(routerGenTs).toContain('modernRouteId?: string;');
|
|
66
|
+
expect(routerGenTs).not.toContain(
|
|
67
|
+
'return Object.keys(staticData).length > 0 ? staticData : undefined;',
|
|
68
|
+
);
|
|
58
69
|
expect(routerGenTs).toContain(
|
|
59
70
|
"} from '@modern-js/plugin-tanstack/runtime';",
|
|
60
71
|
);
|
|
72
|
+
expect(routerGenTs).toContain('modernTanstackRouterFastDefaults,');
|
|
73
|
+
expect(routerGenTs).toContain('...modernTanstackRouterFastDefaults,');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('typechecks generated TanStack search contracts', async () => {
|
|
77
|
+
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
|
|
78
|
+
const srcDirectory = path.join(tempDir, 'src');
|
|
79
|
+
const routeDir = path.join(srcDirectory, 'routes');
|
|
80
|
+
const generatedDir = path.join(srcDirectory, 'modern-tanstack', 'index');
|
|
81
|
+
await mkdir(routeDir, { recursive: true });
|
|
82
|
+
await mkdir(generatedDir, { recursive: true });
|
|
83
|
+
await writeFile(
|
|
84
|
+
path.join(routeDir, 'search.contract.ts'),
|
|
85
|
+
[
|
|
86
|
+
'export const validateSearch = (search: { q?: string }) => ({ q: search.q ?? "" });',
|
|
87
|
+
'export const loaderDeps = ({ search }: { search: { q: string } }) => ({ q: search.q });',
|
|
88
|
+
].join('\n'),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
|
|
92
|
+
appContext: {
|
|
93
|
+
srcDirectory,
|
|
94
|
+
internalSrcAlias: '@/_',
|
|
95
|
+
} as any,
|
|
96
|
+
entryName: 'index',
|
|
97
|
+
routes: [
|
|
98
|
+
{
|
|
99
|
+
type: 'nested',
|
|
100
|
+
id: 'layout',
|
|
101
|
+
isRoot: true,
|
|
102
|
+
validateSearch: '@/_/routes/search.contract',
|
|
103
|
+
loaderDeps: '@/_/routes/search.contract',
|
|
104
|
+
children: [
|
|
105
|
+
{
|
|
106
|
+
type: 'nested',
|
|
107
|
+
id: 'search/page',
|
|
108
|
+
path: 'search',
|
|
109
|
+
validateSearch: '@/_/routes/search.contract',
|
|
110
|
+
loaderDeps: '@/_/routes/search.contract',
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
] as any,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await writeFile(path.join(generatedDir, 'router.gen.ts'), routerGenTs);
|
|
118
|
+
await writeFile(
|
|
119
|
+
path.join(srcDirectory, 'runtime-shim.d.ts'),
|
|
120
|
+
[
|
|
121
|
+
"declare module '@modern-js/plugin-tanstack/runtime' {",
|
|
122
|
+
' type RouteOptions = {',
|
|
123
|
+
' getParentRoute?: () => unknown;',
|
|
124
|
+
' id?: string;',
|
|
125
|
+
' loader?: unknown;',
|
|
126
|
+
' loaderDeps?: unknown;',
|
|
127
|
+
' path?: string;',
|
|
128
|
+
' staticData?: unknown;',
|
|
129
|
+
' validateSearch?: unknown;',
|
|
130
|
+
' };',
|
|
131
|
+
' type Route<TOptions extends RouteOptions> = {',
|
|
132
|
+
' options: TOptions;',
|
|
133
|
+
' addChildren<TChildren extends readonly unknown[]>(children: TChildren): Route<TOptions> & { children: TChildren };',
|
|
134
|
+
' };',
|
|
135
|
+
' export function createMemoryHistory(options: unknown): unknown;',
|
|
136
|
+
' export const modernTanstackRouterFastDefaults: Record<string, unknown>;',
|
|
137
|
+
' export function createRootRouteWithContext<TContext>(): <TOptions extends RouteOptions>(options: TOptions) => Route<TOptions>;',
|
|
138
|
+
' export function createRoute<TOptions extends RouteOptions>(options: TOptions): Route<TOptions>;',
|
|
139
|
+
' export function createRouter<TOptions extends Record<string, unknown>>(options: TOptions): TOptions;',
|
|
140
|
+
' export function notFound(): never;',
|
|
141
|
+
' export function redirect(options: unknown): never;',
|
|
142
|
+
'}',
|
|
143
|
+
].join('\n'),
|
|
144
|
+
);
|
|
145
|
+
await writeFile(
|
|
146
|
+
path.join(srcDirectory, 'assert-search-contracts.ts'),
|
|
147
|
+
[
|
|
148
|
+
"import { rootRoute, routeTree } from './modern-tanstack/index/router.gen';",
|
|
149
|
+
"import { loaderDeps, validateSearch } from './routes/search.contract';",
|
|
150
|
+
'',
|
|
151
|
+
'const rootValidateSearch: typeof validateSearch = rootRoute.options.validateSearch;',
|
|
152
|
+
'const rootLoaderDeps: typeof loaderDeps = rootRoute.options.loaderDeps;',
|
|
153
|
+
'const childValidateSearch: typeof validateSearch = routeTree.children[0].options.validateSearch;',
|
|
154
|
+
'const childLoaderDeps: typeof loaderDeps = routeTree.children[0].options.loaderDeps;',
|
|
155
|
+
'',
|
|
156
|
+
"rootValidateSearch({ q: 'root' });",
|
|
157
|
+
"rootLoaderDeps({ search: { q: 'root' } });",
|
|
158
|
+
"childValidateSearch({ q: 'child' });",
|
|
159
|
+
"childLoaderDeps({ search: { q: 'child' } });",
|
|
160
|
+
].join('\n'),
|
|
161
|
+
);
|
|
162
|
+
await writeFile(
|
|
163
|
+
path.join(tempDir, 'tsconfig.json'),
|
|
164
|
+
JSON.stringify(
|
|
165
|
+
{
|
|
166
|
+
compilerOptions: {
|
|
167
|
+
lib: ['ESNext', 'DOM'],
|
|
168
|
+
module: 'Preserve',
|
|
169
|
+
moduleResolution: 'Bundler',
|
|
170
|
+
noEmit: true,
|
|
171
|
+
skipLibCheck: true,
|
|
172
|
+
strict: true,
|
|
173
|
+
target: 'ESNext',
|
|
174
|
+
types: [],
|
|
175
|
+
},
|
|
176
|
+
include: ['src/**/*.ts', 'src/**/*.d.ts'],
|
|
177
|
+
},
|
|
178
|
+
null,
|
|
179
|
+
2,
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
await expect(
|
|
184
|
+
execFileAsync('tsgo', ['--noEmit', '-p', 'tsconfig.json'], {
|
|
185
|
+
cwd: tempDir,
|
|
186
|
+
}),
|
|
187
|
+
).resolves.toBeDefined();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('preserves typed child trees for localized nested route aliases', async () => {
|
|
191
|
+
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
|
|
192
|
+
const srcDirectory = path.join(tempDir, 'src');
|
|
193
|
+
|
|
194
|
+
const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
|
|
195
|
+
appContext: {
|
|
196
|
+
srcDirectory,
|
|
197
|
+
internalSrcAlias: '@/_',
|
|
198
|
+
} as any,
|
|
199
|
+
entryName: 'index',
|
|
200
|
+
routes: [
|
|
201
|
+
{
|
|
202
|
+
type: 'nested',
|
|
203
|
+
id: 'layout',
|
|
204
|
+
isRoot: true,
|
|
205
|
+
children: [
|
|
206
|
+
{
|
|
207
|
+
type: 'nested',
|
|
208
|
+
id: '(lang)/layout',
|
|
209
|
+
path: ':lang',
|
|
210
|
+
children: [
|
|
211
|
+
{
|
|
212
|
+
type: 'nested',
|
|
213
|
+
id: '(lang)/products/(slug)/page',
|
|
214
|
+
path: 'products/:slug',
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
type: 'nested',
|
|
218
|
+
id: '(lang)/products/(slug)/page__localised_produkty_slug',
|
|
219
|
+
path: 'produkty/:slug',
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
type: 'nested',
|
|
223
|
+
id: '(lang)/optional/(slug$)/page__localised_volitelne_slug',
|
|
224
|
+
path: 'volitelne/:slug?',
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
] as any,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(routerGenTs).toContain(
|
|
234
|
+
'const route__lang__layout__base = createRoute({',
|
|
235
|
+
);
|
|
236
|
+
expect(routerGenTs).toContain(
|
|
237
|
+
'getParentRoute: () => route__lang__layout__base,',
|
|
238
|
+
);
|
|
239
|
+
expect(routerGenTs).toContain('path: "produkty/$slug",');
|
|
240
|
+
expect(routerGenTs).toContain('path: "volitelne/{-$slug}",');
|
|
241
|
+
expect(routerGenTs).toContain(
|
|
242
|
+
'const route__lang__layout = route__lang__layout__base.addChildren([route__lang__products__slug__page, route__lang__products__slug__page__localised_produkty_slug, route__lang__optional__slug$__page__localised_volitelne_slug]);',
|
|
243
|
+
);
|
|
244
|
+
expect(routerGenTs).toContain(
|
|
245
|
+
'export const routeTree = rootRoute.addChildren([route__lang__layout]);',
|
|
246
|
+
);
|
|
247
|
+
expect(routerGenTs).not.toContain('route__lang__layout.addChildren([');
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('collectCanonicalRoutesForEntry', () => {
|
|
252
|
+
test('returns null for a route tree with no locale param and no canonical metadata', () => {
|
|
253
|
+
const result = collectCanonicalRoutesForEntry([
|
|
254
|
+
{
|
|
255
|
+
type: 'nested',
|
|
256
|
+
id: 'layout',
|
|
257
|
+
isRoot: true,
|
|
258
|
+
children: [
|
|
259
|
+
{
|
|
260
|
+
type: 'nested',
|
|
261
|
+
id: 'about/page',
|
|
262
|
+
path: 'about',
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
type: 'nested',
|
|
266
|
+
id: 'contact/page',
|
|
267
|
+
path: 'contact',
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
},
|
|
271
|
+
] as any);
|
|
272
|
+
|
|
273
|
+
expect(result).toBeNull();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('strips leading :lang param and maps index under :lang to "/"', () => {
|
|
277
|
+
const result = collectCanonicalRoutesForEntry([
|
|
278
|
+
{
|
|
279
|
+
type: 'nested',
|
|
280
|
+
id: 'layout',
|
|
281
|
+
isRoot: true,
|
|
282
|
+
children: [
|
|
283
|
+
{
|
|
284
|
+
type: 'nested',
|
|
285
|
+
id: '(lang)/layout',
|
|
286
|
+
path: ':lang',
|
|
287
|
+
children: [
|
|
288
|
+
{
|
|
289
|
+
type: 'nested',
|
|
290
|
+
id: '(lang)/page',
|
|
291
|
+
index: true,
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
type: 'nested',
|
|
295
|
+
id: '(lang)/about/page',
|
|
296
|
+
path: 'about',
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
},
|
|
302
|
+
] as any);
|
|
303
|
+
|
|
304
|
+
expect(result).not.toBeNull();
|
|
305
|
+
expect(result!['/']).toBe('Record<string, never>');
|
|
306
|
+
expect(result!['/about']).toBe('Record<string, never>');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('converts :slug to $slug with required params type', () => {
|
|
310
|
+
const result = collectCanonicalRoutesForEntry([
|
|
311
|
+
{
|
|
312
|
+
type: 'nested',
|
|
313
|
+
id: 'layout',
|
|
314
|
+
isRoot: true,
|
|
315
|
+
children: [
|
|
316
|
+
{
|
|
317
|
+
type: 'nested',
|
|
318
|
+
id: '(lang)/layout',
|
|
319
|
+
path: ':lang',
|
|
320
|
+
children: [
|
|
321
|
+
{
|
|
322
|
+
type: 'nested',
|
|
323
|
+
id: '(lang)/products/(slug)/page',
|
|
324
|
+
path: 'products/:slug',
|
|
325
|
+
},
|
|
326
|
+
],
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
},
|
|
330
|
+
] as any);
|
|
331
|
+
|
|
332
|
+
expect(result).not.toBeNull();
|
|
333
|
+
expect(result!['/products/$slug']).toBe('{ "slug": string }');
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('converts :slug? to optional {-$slug} with optional params type', () => {
|
|
337
|
+
const result = collectCanonicalRoutesForEntry([
|
|
338
|
+
{
|
|
339
|
+
type: 'nested',
|
|
340
|
+
id: 'layout',
|
|
341
|
+
isRoot: true,
|
|
342
|
+
children: [
|
|
343
|
+
{
|
|
344
|
+
type: 'nested',
|
|
345
|
+
id: '(lang)/layout',
|
|
346
|
+
path: ':lang',
|
|
347
|
+
children: [
|
|
348
|
+
{
|
|
349
|
+
type: 'nested',
|
|
350
|
+
id: '(lang)/optional/(slug$)/page',
|
|
351
|
+
path: 'optional/:slug?',
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
},
|
|
357
|
+
] as any);
|
|
358
|
+
|
|
359
|
+
expect(result).not.toBeNull();
|
|
360
|
+
expect(result!['/optional/{-$slug}']).toBe('{ "slug"?: string }');
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test('converts * splat to $ with optional _splat param', () => {
|
|
364
|
+
const result = collectCanonicalRoutesForEntry([
|
|
365
|
+
{
|
|
366
|
+
type: 'nested',
|
|
367
|
+
id: 'layout',
|
|
368
|
+
isRoot: true,
|
|
369
|
+
children: [
|
|
370
|
+
{
|
|
371
|
+
type: 'nested',
|
|
372
|
+
id: '(lang)/layout',
|
|
373
|
+
path: ':lang',
|
|
374
|
+
children: [
|
|
375
|
+
{
|
|
376
|
+
type: 'nested',
|
|
377
|
+
id: '(lang)/files/page',
|
|
378
|
+
path: 'files/*',
|
|
379
|
+
},
|
|
380
|
+
],
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
},
|
|
384
|
+
] as any);
|
|
385
|
+
|
|
386
|
+
expect(result).not.toBeNull();
|
|
387
|
+
expect(result!['/files/$']).toBe("{ '_splat'?: string }");
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test('collapses localized variants with shared modernCanonicalPath to one canonical key', () => {
|
|
391
|
+
// This reuses the same fixture shape as the 'preserves typed child trees'
|
|
392
|
+
// test but adds modernCanonicalPath fields as plugin-i18n now emits.
|
|
393
|
+
const result = collectCanonicalRoutesForEntry([
|
|
394
|
+
{
|
|
395
|
+
type: 'nested',
|
|
396
|
+
id: 'layout',
|
|
397
|
+
isRoot: true,
|
|
398
|
+
children: [
|
|
399
|
+
{
|
|
400
|
+
type: 'nested',
|
|
401
|
+
id: '(lang)/layout',
|
|
402
|
+
path: ':lang',
|
|
403
|
+
children: [
|
|
404
|
+
{
|
|
405
|
+
type: 'nested',
|
|
406
|
+
id: '(lang)/products/(slug)/page',
|
|
407
|
+
path: 'products/:slug',
|
|
408
|
+
modernCanonicalPath: '/products/:slug',
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
type: 'nested',
|
|
412
|
+
id: '(lang)/products/(slug)/page__localised_produkty_slug',
|
|
413
|
+
path: 'produkty/:slug',
|
|
414
|
+
modernCanonicalPath: '/products/:slug',
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
type: 'nested',
|
|
418
|
+
id: '(lang)/optional/(slug$)/page__localised_volitelne_slug',
|
|
419
|
+
path: 'volitelne/:slug?',
|
|
420
|
+
modernCanonicalPath: '/optional/:slug?',
|
|
421
|
+
},
|
|
422
|
+
],
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
},
|
|
426
|
+
] as any);
|
|
427
|
+
|
|
428
|
+
expect(result).not.toBeNull();
|
|
429
|
+
// Two physical variants share the same canonical path — only one entry.
|
|
430
|
+
const keys = Object.keys(result!);
|
|
431
|
+
// /products/$slug and /optional/{-$slug} — exactly 2 keys with params
|
|
432
|
+
expect(keys.filter(k => k.startsWith('/products'))).toHaveLength(1);
|
|
433
|
+
expect(result!['/products/$slug']).toBe('{ "slug": string }');
|
|
434
|
+
expect(result!['/optional/{-$slug}']).toBe('{ "slug"?: string }');
|
|
435
|
+
// The Czech localized path must not appear as a separate key.
|
|
436
|
+
expect('/produkty/$slug' in result!).toBe(false);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test('output is sorted alphabetically by canonical key', () => {
|
|
440
|
+
const result = collectCanonicalRoutesForEntry([
|
|
441
|
+
{
|
|
442
|
+
type: 'nested',
|
|
443
|
+
id: 'layout',
|
|
444
|
+
isRoot: true,
|
|
445
|
+
children: [
|
|
446
|
+
{
|
|
447
|
+
type: 'nested',
|
|
448
|
+
id: '(lang)/layout',
|
|
449
|
+
path: ':lang',
|
|
450
|
+
children: [
|
|
451
|
+
{
|
|
452
|
+
type: 'nested',
|
|
453
|
+
id: '(lang)/products/(slug)/page',
|
|
454
|
+
path: 'products/:slug',
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
type: 'nested',
|
|
458
|
+
id: '(lang)/about/page',
|
|
459
|
+
path: 'about',
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
type: 'nested',
|
|
463
|
+
id: '(lang)/page',
|
|
464
|
+
index: true,
|
|
465
|
+
},
|
|
466
|
+
],
|
|
467
|
+
},
|
|
468
|
+
],
|
|
469
|
+
},
|
|
470
|
+
] as any);
|
|
471
|
+
|
|
472
|
+
expect(result).not.toBeNull();
|
|
473
|
+
const keys = Object.keys(result!);
|
|
474
|
+
expect(keys).toEqual([...keys].sort((a, b) => a.localeCompare(b)));
|
|
61
475
|
});
|
|
62
476
|
});
|