@tanstack/router-generator 1.141.5 → 1.141.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/generator.cjs +87 -52
- package/dist/cjs/generator.cjs.map +1 -1
- package/dist/cjs/generator.d.cts +3 -0
- package/dist/cjs/utils.cjs +187 -79
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +37 -1
- package/dist/esm/generator.d.ts +3 -0
- package/dist/esm/generator.js +88 -53
- package/dist/esm/generator.js.map +1 -1
- package/dist/esm/utils.d.ts +37 -1
- package/dist/esm/utils.js +187 -79
- package/dist/esm/utils.js.map +1 -1
- package/package.json +5 -5
- package/src/generator.ts +102 -89
- package/src/utils.ts +266 -137
package/src/utils.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/prefer-for-of */
|
|
1
2
|
import * as fsp from 'node:fs/promises'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
import * as prettier from 'prettier'
|
|
@@ -5,34 +6,170 @@ import { rootPathId } from './filesystem/physical/rootPathId'
|
|
|
5
6
|
import type { Config } from './config'
|
|
6
7
|
import type { ImportDeclaration, RouteNode } from './types'
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Prefix map for O(1) parent route lookups.
|
|
11
|
+
* Maps each route path prefix to the route node that owns that prefix.
|
|
12
|
+
* Enables finding longest matching parent without linear search.
|
|
13
|
+
*/
|
|
14
|
+
export class RoutePrefixMap {
|
|
15
|
+
private prefixToRoute: Map<string, RouteNode> = new Map()
|
|
16
|
+
private layoutRoutes: Array<RouteNode> = []
|
|
17
|
+
private nonNestedRoutes: Array<RouteNode> = []
|
|
18
|
+
|
|
19
|
+
constructor(routes: Array<RouteNode>) {
|
|
20
|
+
for (const route of routes) {
|
|
21
|
+
if (!route.routePath || route.routePath === `/${rootPathId}`) continue
|
|
22
|
+
|
|
23
|
+
// Index by exact path for direct lookups
|
|
24
|
+
this.prefixToRoute.set(route.routePath, route)
|
|
25
|
+
|
|
26
|
+
// Track layout routes separately for non-nested route handling
|
|
27
|
+
if (
|
|
28
|
+
route._fsRouteType === 'pathless_layout' ||
|
|
29
|
+
route._fsRouteType === 'layout' ||
|
|
30
|
+
route._fsRouteType === '__root'
|
|
31
|
+
) {
|
|
32
|
+
this.layoutRoutes.push(route)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Track non-nested routes separately
|
|
36
|
+
if (route._isExperimentalNonNestedRoute) {
|
|
37
|
+
this.nonNestedRoutes.push(route)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Sort by path length descending for longest-match-first
|
|
42
|
+
this.layoutRoutes.sort(
|
|
43
|
+
(a, b) => (b.routePath?.length ?? 0) - (a.routePath?.length ?? 0),
|
|
44
|
+
)
|
|
45
|
+
this.nonNestedRoutes.sort(
|
|
46
|
+
(a, b) => (b.routePath?.length ?? 0) - (a.routePath?.length ?? 0),
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Find the longest matching parent route for a given path.
|
|
52
|
+
* O(k) where k is the number of path segments, not O(n) routes.
|
|
53
|
+
*/
|
|
54
|
+
findParent(routePath: string): RouteNode | null {
|
|
55
|
+
if (!routePath || routePath === '/') return null
|
|
56
|
+
|
|
57
|
+
// Walk up the path segments
|
|
58
|
+
let searchPath = routePath
|
|
59
|
+
while (searchPath.length > 0) {
|
|
60
|
+
const lastSlash = searchPath.lastIndexOf('/')
|
|
61
|
+
if (lastSlash <= 0) break
|
|
62
|
+
|
|
63
|
+
searchPath = searchPath.substring(0, lastSlash)
|
|
64
|
+
const parent = this.prefixToRoute.get(searchPath)
|
|
65
|
+
if (parent && parent.routePath !== routePath) {
|
|
66
|
+
return parent
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Find parent for non-nested routes (needs layout route matching).
|
|
74
|
+
*/
|
|
75
|
+
findParentForNonNested(
|
|
76
|
+
routePath: string,
|
|
77
|
+
originalRoutePath: string | undefined,
|
|
78
|
+
nonNestedSegments: Array<string>,
|
|
79
|
+
): RouteNode | null {
|
|
80
|
+
// First check for other non-nested routes that are prefixes
|
|
81
|
+
// Use pre-sorted array for longest-match-first
|
|
82
|
+
for (const route of this.nonNestedRoutes) {
|
|
83
|
+
if (
|
|
84
|
+
route.routePath !== routePath &&
|
|
85
|
+
originalRoutePath?.startsWith(`${route.originalRoutePath}/`)
|
|
86
|
+
) {
|
|
87
|
+
return route
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Then check layout routes
|
|
92
|
+
for (const route of this.layoutRoutes) {
|
|
93
|
+
if (route.routePath === '/') continue
|
|
94
|
+
|
|
95
|
+
// Skip if this route's original path + underscore matches a non-nested segment
|
|
96
|
+
if (
|
|
97
|
+
nonNestedSegments.some((seg) => seg === `${route.originalRoutePath}_`)
|
|
98
|
+
) {
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check if this layout route is a prefix of the path we're looking for
|
|
103
|
+
if (
|
|
104
|
+
routePath.startsWith(`${route.routePath}/`) &&
|
|
105
|
+
route.routePath !== routePath
|
|
106
|
+
) {
|
|
107
|
+
return route
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if a route exists at the given path.
|
|
116
|
+
*/
|
|
117
|
+
has(routePath: string): boolean {
|
|
118
|
+
return this.prefixToRoute.has(routePath)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get a route by exact path.
|
|
123
|
+
*/
|
|
124
|
+
get(routePath: string): RouteNode | undefined {
|
|
125
|
+
return this.prefixToRoute.get(routePath)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
8
129
|
export function multiSortBy<T>(
|
|
9
130
|
arr: Array<T>,
|
|
10
131
|
accessors: Array<(item: T) => any> = [(d) => d],
|
|
11
132
|
): Array<T> {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
133
|
+
const len = arr.length
|
|
134
|
+
// Pre-compute all accessor values to avoid repeated function calls during sort
|
|
135
|
+
const indexed: Array<{ item: T; index: number; keys: Array<any> }> =
|
|
136
|
+
new Array(len)
|
|
137
|
+
for (let i = 0; i < len; i++) {
|
|
138
|
+
const item = arr[i]!
|
|
139
|
+
const keys = new Array(accessors.length)
|
|
140
|
+
for (let j = 0; j < accessors.length; j++) {
|
|
141
|
+
keys[j] = accessors[j]!(item)
|
|
142
|
+
}
|
|
143
|
+
indexed[i] = { item, index: i, keys }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
indexed.sort((a, b) => {
|
|
147
|
+
for (let j = 0; j < accessors.length; j++) {
|
|
148
|
+
const ao = a.keys[j]
|
|
149
|
+
const bo = b.keys[j]
|
|
25
150
|
|
|
26
|
-
|
|
151
|
+
if (typeof ao === 'undefined') {
|
|
152
|
+
if (typeof bo === 'undefined') {
|
|
27
153
|
continue
|
|
28
154
|
}
|
|
155
|
+
return 1
|
|
156
|
+
}
|
|
29
157
|
|
|
30
|
-
|
|
158
|
+
if (ao === bo) {
|
|
159
|
+
continue
|
|
31
160
|
}
|
|
32
161
|
|
|
33
|
-
return
|
|
34
|
-
}
|
|
35
|
-
|
|
162
|
+
return ao > bo ? 1 : -1
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return a.index - b.index
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const result: Array<T> = new Array(len)
|
|
169
|
+
for (let i = 0; i < len; i++) {
|
|
170
|
+
result[i] = indexed[i]!.item
|
|
171
|
+
}
|
|
172
|
+
return result
|
|
36
173
|
}
|
|
37
174
|
|
|
38
175
|
export function cleanPath(path: string) {
|
|
@@ -143,54 +280,74 @@ export function determineInitialRoutePath(
|
|
|
143
280
|
}
|
|
144
281
|
}
|
|
145
282
|
|
|
283
|
+
const backslashRegex = /\\/g
|
|
284
|
+
|
|
146
285
|
export function replaceBackslash(s: string) {
|
|
147
|
-
return s.
|
|
286
|
+
return s.replace(backslashRegex, '/')
|
|
148
287
|
}
|
|
149
288
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
289
|
+
const alphanumericRegex = /[a-zA-Z0-9_]/
|
|
290
|
+
const splatSlashRegex = /\/\$\//g
|
|
291
|
+
const trailingSplatRegex = /\$$/g
|
|
292
|
+
const bracketSplatRegex = /\$\{\$\}/g
|
|
293
|
+
const dollarSignRegex = /\$/g
|
|
294
|
+
const splitPathRegex = /[/-]/g
|
|
295
|
+
const leadingDigitRegex = /^(\d)/g
|
|
296
|
+
|
|
297
|
+
const toVariableSafeChar = (char: string): string => {
|
|
298
|
+
if (alphanumericRegex.test(char)) {
|
|
299
|
+
return char // Keep alphanumeric characters and underscores as is
|
|
300
|
+
}
|
|
155
301
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
302
|
+
// Replace special characters with meaningful text equivalents
|
|
303
|
+
switch (char) {
|
|
304
|
+
case '.':
|
|
305
|
+
return 'Dot'
|
|
306
|
+
case '-':
|
|
307
|
+
return 'Dash'
|
|
308
|
+
case '@':
|
|
309
|
+
return 'At'
|
|
310
|
+
case '(':
|
|
311
|
+
return '' // Removed since route groups use parentheses
|
|
312
|
+
case ')':
|
|
313
|
+
return '' // Removed since route groups use parentheses
|
|
314
|
+
case ' ':
|
|
315
|
+
return '' // Remove spaces
|
|
316
|
+
default:
|
|
317
|
+
return `Char${char.charCodeAt(0)}` // For any other characters
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function routePathToVariable(routePath: string): string {
|
|
322
|
+
const cleaned = removeUnderscores(routePath)
|
|
323
|
+
if (!cleaned) return ''
|
|
324
|
+
|
|
325
|
+
const parts = cleaned
|
|
326
|
+
.replace(splatSlashRegex, '/splat/')
|
|
327
|
+
.replace(trailingSplatRegex, 'splat')
|
|
328
|
+
.replace(bracketSplatRegex, 'splat')
|
|
329
|
+
.replace(dollarSignRegex, '')
|
|
330
|
+
.split(splitPathRegex)
|
|
331
|
+
|
|
332
|
+
let result = ''
|
|
333
|
+
for (let i = 0; i < parts.length; i++) {
|
|
334
|
+
const part = parts[i]!
|
|
335
|
+
const segment = i > 0 ? capitalize(part) : part
|
|
336
|
+
for (let j = 0; j < segment.length; j++) {
|
|
337
|
+
result += toVariableSafeChar(segment[j]!)
|
|
172
338
|
}
|
|
173
339
|
}
|
|
174
340
|
|
|
175
|
-
return (
|
|
176
|
-
removeUnderscores(routePath)
|
|
177
|
-
?.replace(/\/\$\//g, '/splat/')
|
|
178
|
-
.replace(/\$$/g, 'splat')
|
|
179
|
-
.replace(/\$\{\$\}/g, 'splat')
|
|
180
|
-
.replace(/\$/g, '')
|
|
181
|
-
.split(/[/-]/g)
|
|
182
|
-
.map((d, i) => (i > 0 ? capitalize(d) : d))
|
|
183
|
-
.join('')
|
|
184
|
-
.split('')
|
|
185
|
-
.map(toVariableSafeChar)
|
|
186
|
-
.join('')
|
|
187
|
-
// .replace(/([^a-zA-Z0-9]|[.])/gm, '')
|
|
188
|
-
.replace(/^(\d)/g, 'R$1') ?? ''
|
|
189
|
-
)
|
|
341
|
+
return result.replace(leadingDigitRegex, 'R$1')
|
|
190
342
|
}
|
|
191
343
|
|
|
344
|
+
const underscoreStartEndRegex = /(^_|_$)/gi
|
|
345
|
+
const underscoreSlashRegex = /(\/_|_\/)/gi
|
|
346
|
+
|
|
192
347
|
export function removeUnderscores(s?: string) {
|
|
193
|
-
return s
|
|
348
|
+
return s
|
|
349
|
+
?.replace(underscoreStartEndRegex, '')
|
|
350
|
+
.replace(underscoreSlashRegex, '/')
|
|
194
351
|
}
|
|
195
352
|
|
|
196
353
|
function escapeRegExp(s: string): string {
|
|
@@ -361,86 +518,53 @@ export function removeLastSegmentFromPath(routePath: string = '/'): string {
|
|
|
361
518
|
return segments.join('/')
|
|
362
519
|
}
|
|
363
520
|
|
|
521
|
+
const nonNestedSegmentRegex = /_(?=\/|$)/g
|
|
522
|
+
const openBracketRegex = /\[/g
|
|
523
|
+
const closeBracketRegex = /\]/g
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Extracts non-nested segments from a route path.
|
|
527
|
+
* Used for determining parent routes in non-nested route scenarios.
|
|
528
|
+
*/
|
|
529
|
+
export function getNonNestedSegments(routePath: string): Array<string> {
|
|
530
|
+
nonNestedSegmentRegex.lastIndex = 0
|
|
531
|
+
const result: Array<string> = []
|
|
532
|
+
for (const match of routePath.matchAll(nonNestedSegmentRegex)) {
|
|
533
|
+
const beforeStr = routePath.substring(0, match.index)
|
|
534
|
+
openBracketRegex.lastIndex = 0
|
|
535
|
+
closeBracketRegex.lastIndex = 0
|
|
536
|
+
const openBrackets = beforeStr.match(openBracketRegex)?.length ?? 0
|
|
537
|
+
const closeBrackets = beforeStr.match(closeBracketRegex)?.length ?? 0
|
|
538
|
+
if (openBrackets === closeBrackets) {
|
|
539
|
+
result.push(routePath.substring(0, match.index + 1))
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return result.reverse()
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Find parent route using RoutePrefixMap for O(k) lookups instead of O(n).
|
|
547
|
+
*/
|
|
364
548
|
export function hasParentRoute(
|
|
365
|
-
|
|
549
|
+
prefixMap: RoutePrefixMap,
|
|
366
550
|
node: RouteNode,
|
|
367
551
|
routePathToCheck: string | undefined,
|
|
368
552
|
originalRoutePathToCheck: string | undefined,
|
|
369
553
|
): RouteNode | null {
|
|
370
|
-
const getNonNestedSegments = (routePath: string) => {
|
|
371
|
-
const regex = /_(?=\/|$)/g
|
|
372
|
-
|
|
373
|
-
return [...routePath.matchAll(regex)]
|
|
374
|
-
.filter((match) => {
|
|
375
|
-
const beforeStr = routePath.substring(0, match.index)
|
|
376
|
-
const openBrackets = (beforeStr.match(/\[/g) || []).length
|
|
377
|
-
const closeBrackets = (beforeStr.match(/\]/g) || []).length
|
|
378
|
-
return openBrackets === closeBrackets
|
|
379
|
-
})
|
|
380
|
-
.map((match) => routePath.substring(0, match.index + 1))
|
|
381
|
-
.reverse()
|
|
382
|
-
}
|
|
383
|
-
|
|
384
554
|
if (!routePathToCheck || routePathToCheck === '/') {
|
|
385
555
|
return null
|
|
386
556
|
}
|
|
387
557
|
|
|
388
|
-
const sortedNodes = multiSortBy(routes, [
|
|
389
|
-
(d) => d.routePath!.length * -1,
|
|
390
|
-
(d) => d.variableName,
|
|
391
|
-
]).filter((d) => d.routePath !== `/${rootPathId}`)
|
|
392
|
-
|
|
393
|
-
const filteredNodes = node._isExperimentalNonNestedRoute
|
|
394
|
-
? []
|
|
395
|
-
: [...sortedNodes]
|
|
396
|
-
|
|
397
558
|
if (node._isExperimentalNonNestedRoute && originalRoutePathToCheck) {
|
|
398
559
|
const nonNestedSegments = getNonNestedSegments(originalRoutePathToCheck)
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
route._isExperimentalNonNestedRoute &&
|
|
405
|
-
route.routePath !== routePathToCheck &&
|
|
406
|
-
originalRoutePathToCheck.startsWith(`${route.originalRoutePath}/`)
|
|
407
|
-
) {
|
|
408
|
-
return route
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
if (
|
|
412
|
-
nonNestedSegments.find(
|
|
413
|
-
(seg) => seg === `${route.originalRoutePath}_`,
|
|
414
|
-
) ||
|
|
415
|
-
!(
|
|
416
|
-
route._fsRouteType === 'pathless_layout' ||
|
|
417
|
-
route._fsRouteType === 'layout' ||
|
|
418
|
-
route._fsRouteType === '__root'
|
|
419
|
-
)
|
|
420
|
-
) {
|
|
421
|
-
continue
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
filteredNodes.push(route)
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
for (const route of filteredNodes) {
|
|
429
|
-
if (route.routePath === '/') continue
|
|
430
|
-
|
|
431
|
-
if (
|
|
432
|
-
routePathToCheck.startsWith(`${route.routePath}/`) &&
|
|
433
|
-
route.routePath !== routePathToCheck
|
|
434
|
-
) {
|
|
435
|
-
return route
|
|
436
|
-
}
|
|
560
|
+
return prefixMap.findParentForNonNested(
|
|
561
|
+
routePathToCheck,
|
|
562
|
+
originalRoutePathToCheck,
|
|
563
|
+
nonNestedSegments,
|
|
564
|
+
)
|
|
437
565
|
}
|
|
438
566
|
|
|
439
|
-
|
|
440
|
-
segments.pop() // Remove the last segment
|
|
441
|
-
const parentRoutePath = segments.join('/')
|
|
442
|
-
|
|
443
|
-
return hasParentRoute(routes, node, parentRoutePath, originalRoutePathToCheck)
|
|
567
|
+
return prefixMap.findParent(routePathToCheck)
|
|
444
568
|
}
|
|
445
569
|
|
|
446
570
|
/**
|
|
@@ -682,28 +806,33 @@ export function lowerCaseFirstChar(value: string) {
|
|
|
682
806
|
export function mergeImportDeclarations(
|
|
683
807
|
imports: Array<ImportDeclaration>,
|
|
684
808
|
): Array<ImportDeclaration> {
|
|
685
|
-
const merged
|
|
809
|
+
const merged = new Map<string, ImportDeclaration>()
|
|
686
810
|
|
|
687
811
|
for (const imp of imports) {
|
|
688
|
-
const key = `${imp.source}-${imp.importKind}`
|
|
689
|
-
|
|
690
|
-
|
|
812
|
+
const key = `${imp.source}-${imp.importKind ?? ''}`
|
|
813
|
+
let existing = merged.get(key)
|
|
814
|
+
if (!existing) {
|
|
815
|
+
existing = { ...imp, specifiers: [] }
|
|
816
|
+
merged.set(key, existing)
|
|
691
817
|
}
|
|
818
|
+
|
|
819
|
+
const existingSpecs = existing.specifiers
|
|
692
820
|
for (const specifier of imp.specifiers) {
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
821
|
+
let found = false
|
|
822
|
+
for (let i = 0; i < existingSpecs.length; i++) {
|
|
823
|
+
const e = existingSpecs[i]!
|
|
824
|
+
if (e.imported === specifier.imported && e.local === specifier.local) {
|
|
825
|
+
found = true
|
|
826
|
+
break
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (!found) {
|
|
830
|
+
existingSpecs.push(specifier)
|
|
702
831
|
}
|
|
703
832
|
}
|
|
704
833
|
}
|
|
705
834
|
|
|
706
|
-
return
|
|
835
|
+
return [...merged.values()]
|
|
707
836
|
}
|
|
708
837
|
|
|
709
838
|
export const findParent = (node: RouteNode | undefined): string => {
|