@tanstack/router-generator 1.167.0 → 1.167.2

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/src/generator.ts CHANGED
@@ -17,6 +17,8 @@ import {
17
17
  buildRouteTreeConfig,
18
18
  checkFileExists,
19
19
  checkRouteFullPathUniqueness,
20
+ countRoutePathSegments,
21
+ countSlashSeparatedParts,
20
22
  createRouteNodesByFullPath,
21
23
  createRouteNodesById,
22
24
  createRouteNodesByTo,
@@ -33,9 +35,8 @@ import {
33
35
  removeExt,
34
36
  removeGroups,
35
37
  removeLastSegmentFromPath,
36
- removeLayoutSegmentsWithEscape,
38
+ removeLayoutSegmentsAndUnderscoresWithEscape,
37
39
  removeTrailingSlash,
38
- removeUnderscoresWithEscape,
39
40
  replaceBackslash,
40
41
  trimPathLeft,
41
42
  } from './utils'
@@ -51,6 +52,7 @@ import type {
51
52
  HandleNodeAccumulator,
52
53
  ImportDeclaration,
53
54
  RouteNode,
55
+ RoutePathSegmentMetadata,
54
56
  } from './types'
55
57
  import type { Config } from './config'
56
58
  import type { Logger } from './logger'
@@ -70,6 +72,32 @@ interface fs {
70
72
  chown: (filePath: string, uid: number, gid: number) => Promise<void>
71
73
  }
72
74
 
75
+ function getRoutePathSegmentMetadataForPath(
76
+ node: RouteNode,
77
+ routePath: string,
78
+ parentRoutePath?: string,
79
+ ): Array<RoutePathSegmentMetadata | undefined> | undefined {
80
+ if (!node._routePathSegmentMetadata) return undefined
81
+
82
+ const segments = routePath.split('/')
83
+ const result = new Array<RoutePathSegmentMetadata | undefined>(
84
+ segments.length,
85
+ )
86
+ const parentSegmentCount = countRoutePathSegments(parentRoutePath)
87
+ let hasMetadata = false
88
+ let segmentCount = 0
89
+
90
+ for (let i = 0; i < segments.length; i++) {
91
+ if (!segments[i]) continue
92
+ const sourceIndex = parentSegmentCount + segmentCount + 1
93
+ result[i] = node._routePathSegmentMetadata[sourceIndex]
94
+ hasMetadata ||= !!result[i]
95
+ segmentCount++
96
+ }
97
+
98
+ return hasMetadata ? result : undefined
99
+ }
100
+
73
101
  const DefaultFileSystem: fs = {
74
102
  stat: async (filePath) => {
75
103
  const res = await fsp.stat(filePath, { bigint: true })
@@ -392,7 +420,10 @@ export class Generator {
392
420
 
393
421
  const preRouteNodes = multiSortBy(beforeRouteNodes, [
394
422
  (d) => (d.routePath === '/' ? -1 : 1),
395
- (d) => d.routePath?.split('/').length,
423
+ (d) =>
424
+ d.routePath === undefined
425
+ ? undefined
426
+ : countSlashSeparatedParts(d.routePath),
396
427
  (d) => (d.filePath.match(this.indexTokenFilenameRegex) ? 1 : -1),
397
428
  (d) => (d.filePath.match(Generator.componentPieceRegex) ? 1 : -1),
398
429
  (d) => (d.filePath.match(this.routeTokenFilenameRegex) ? -1 : 1),
@@ -587,7 +618,10 @@ export class Generator {
587
618
 
588
619
  const sortedRouteNodes = multiSortBy(acc.routeNodes, [
589
620
  (d) => (d.routePath?.includes(`/${rootPathId}`) ? -1 : 1),
590
- (d) => d.routePath?.split('/').length,
621
+ (d) =>
622
+ d.routePath === undefined
623
+ ? undefined
624
+ : countSlashSeparatedParts(d.routePath),
591
625
  (d) => {
592
626
  const segments = d.routePath?.split('/').filter(Boolean) ?? []
593
627
  const last = segments[segments.length - 1] ?? ''
@@ -1430,29 +1464,39 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved
1430
1464
  node.path = determineNodePath(node)
1431
1465
 
1432
1466
  const trimmedPath = trimPathLeft(node.path ?? '')
1433
- const trimmedOriginalPath = trimPathLeft(
1434
- node.originalRoutePath?.replace(
1435
- node.parent?.originalRoutePath ?? '',
1436
- '',
1437
- ) ?? '',
1467
+ const originalPath =
1468
+ node.originalRoutePath && node.parent?.originalRoutePath
1469
+ ? node.originalRoutePath.replace(node.parent.originalRoutePath, '') ||
1470
+ '/'
1471
+ : node.originalRoutePath
1472
+ const routePathSegmentMetadata = getRoutePathSegmentMetadataForPath(
1473
+ node,
1474
+ node.path ?? '/',
1475
+ node.parent?.routePath,
1438
1476
  )
1477
+ const trimmedOriginalPath = trimPathLeft(originalPath ?? '')
1439
1478
 
1440
1479
  const split = trimmedPath.split('/')
1480
+ const pathSplit = (node.path ?? '').split('/')
1441
1481
  const originalSplit = trimmedOriginalPath.split('/')
1442
1482
  const lastRouteSegment = split[split.length - 1] ?? trimmedPath
1443
1483
  const lastOriginalSegment =
1444
1484
  originalSplit[originalSplit.length - 1] ?? trimmedOriginalPath
1485
+ const lastRouteSegmentMetadata =
1486
+ routePathSegmentMetadata?.[pathSplit.length - 1]
1445
1487
 
1446
1488
  // A segment is non-path if it starts with underscore AND the underscore is not escaped
1447
1489
  node.isNonPath =
1448
- isSegmentPathless(lastRouteSegment, lastOriginalSegment) ||
1490
+ (!lastRouteSegmentMetadata?.literalLeadingUnderscore &&
1491
+ isSegmentPathless(lastRouteSegment, lastOriginalSegment)) ||
1449
1492
  split.every((part) => this.routeGroupPatternRegex.test(part))
1450
1493
 
1451
- // Use escape-aware functions to compute cleanedPath
1494
+ // Use a single pass so layout removal does not desync escaped underscore checks.
1452
1495
  node.cleanedPath = removeGroups(
1453
- removeUnderscoresWithEscape(
1454
- removeLayoutSegmentsWithEscape(node.path, node.originalRoutePath),
1455
- node.originalRoutePath,
1496
+ removeLayoutSegmentsAndUnderscoresWithEscape(
1497
+ node.path,
1498
+ originalPath,
1499
+ routePathSegmentMetadata,
1456
1500
  ),
1457
1501
  )
1458
1502
 
@@ -1530,13 +1574,17 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved
1530
1574
  candidate.originalRoutePath ?? '',
1531
1575
  '',
1532
1576
  ) || '/'
1577
+ const routePathSegmentMetadataRelativeToParent =
1578
+ getRoutePathSegmentMetadataForPath(
1579
+ node,
1580
+ pathRelativeToParent,
1581
+ candidate.routePath,
1582
+ )
1533
1583
  node.cleanedPath = removeGroups(
1534
- removeUnderscoresWithEscape(
1535
- removeLayoutSegmentsWithEscape(
1536
- pathRelativeToParent,
1537
- originalPathRelativeToParent,
1538
- ),
1584
+ removeLayoutSegmentsAndUnderscoresWithEscape(
1585
+ pathRelativeToParent,
1539
1586
  originalPathRelativeToParent,
1587
+ routePathSegmentMetadataRelativeToParent,
1540
1588
  ),
1541
1589
  )
1542
1590
  break
package/src/types.ts CHANGED
@@ -1,3 +1,8 @@
1
+ export type RoutePathSegmentMetadata = {
2
+ literalLeadingUnderscore?: boolean
3
+ literalTrailingUnderscore?: boolean
4
+ }
5
+
1
6
  export type RouteNode = {
2
7
  filePath: string
3
8
  fullPath: string
@@ -20,6 +25,8 @@ export type RouteNode = {
20
25
  * (e.g., when the parent is a virtual file-less route that gets filtered out).
21
26
  */
22
27
  _virtualParentRoutePath?: string
28
+ /** Internal routePath segment metadata for escaped or explicit literal syntax. */
29
+ _routePathSegmentMetadata?: Array<RoutePathSegmentMetadata | undefined>
23
30
  }
24
31
 
25
32
  export interface GetRouteNodesResult {
package/src/utils.ts CHANGED
@@ -4,7 +4,11 @@ import path from 'node:path'
4
4
  import * as prettier from 'prettier'
5
5
  import { rootPathId } from './filesystem/physical/rootPathId'
6
6
  import type { Config, TokenMatcher } from './config'
7
- import type { ImportDeclaration, RouteNode } from './types'
7
+ import type {
8
+ ImportDeclaration,
9
+ RouteNode,
10
+ RoutePathSegmentMetadata,
11
+ } from './types'
8
12
 
9
13
  /**
10
14
  * Prefix map for O(1) parent route lookups.
@@ -244,6 +248,169 @@ export function hasEscapedTrailingUnderscore(originalSegment: string): boolean {
244
248
  )
245
249
  }
246
250
 
251
+ export function countRoutePathSegments(routePath?: string): number {
252
+ const path = routePath ?? ''
253
+ let count = 0
254
+ let inSegment = false
255
+
256
+ for (let i = 0; i < path.length; i++) {
257
+ if (path[i] === '/') {
258
+ inSegment = false
259
+ continue
260
+ }
261
+
262
+ if (!inSegment) {
263
+ count++
264
+ inSegment = true
265
+ }
266
+ }
267
+
268
+ return count
269
+ }
270
+
271
+ export function countSlashSeparatedParts(path: string): number {
272
+ let count = 1
273
+
274
+ for (let i = 0; i < path.length; i++) {
275
+ if (path[i] === '/') count++
276
+ }
277
+
278
+ return count
279
+ }
280
+
281
+ export function hasRoutePathSegmentMetadata(
282
+ metadata: RoutePathSegmentMetadata | undefined,
283
+ ): metadata is RoutePathSegmentMetadata {
284
+ return !!(
285
+ metadata?.literalLeadingUnderscore || metadata?.literalTrailingUnderscore
286
+ )
287
+ }
288
+
289
+ function mergeRoutePathSegmentMetadata(
290
+ current: RoutePathSegmentMetadata | undefined,
291
+ incoming: RoutePathSegmentMetadata | undefined,
292
+ ): RoutePathSegmentMetadata | undefined {
293
+ const hasCurrent = hasRoutePathSegmentMetadata(current)
294
+ const hasIncoming = hasRoutePathSegmentMetadata(incoming)
295
+
296
+ if (!hasCurrent) return hasIncoming ? incoming : undefined
297
+ if (!hasIncoming) return current
298
+
299
+ return {
300
+ literalLeadingUnderscore:
301
+ current.literalLeadingUnderscore || incoming.literalLeadingUnderscore,
302
+ literalTrailingUnderscore:
303
+ current.literalTrailingUnderscore || incoming.literalTrailingUnderscore,
304
+ }
305
+ }
306
+
307
+ export function createRoutePathSegmentMetadata(
308
+ routePath: string = '/',
309
+ originalPath?: string,
310
+ ): Array<RoutePathSegmentMetadata | undefined> | undefined {
311
+ if (!originalPath) return undefined
312
+
313
+ const routeSegments = routePath.split('/')
314
+ const originalSegments = originalPath.split('/')
315
+ const metadata = new Array<RoutePathSegmentMetadata | undefined>(
316
+ routeSegments.length,
317
+ )
318
+ let hasMetadata = false
319
+
320
+ for (let i = 0; i < routeSegments.length; i++) {
321
+ const segment = routeSegments[i]!
322
+ const originalSegment = originalSegments[i] || ''
323
+ const literalLeadingUnderscore =
324
+ segment.startsWith('_') && hasEscapedLeadingUnderscore(originalSegment)
325
+ const literalTrailingUnderscore =
326
+ segment.endsWith('_') && hasEscapedTrailingUnderscore(originalSegment)
327
+
328
+ if (!literalLeadingUnderscore && !literalTrailingUnderscore) continue
329
+
330
+ hasMetadata = true
331
+ metadata[i] = {
332
+ literalLeadingUnderscore: literalLeadingUnderscore || undefined,
333
+ literalTrailingUnderscore: literalTrailingUnderscore || undefined,
334
+ }
335
+ }
336
+
337
+ return hasMetadata ? metadata : undefined
338
+ }
339
+
340
+ export function createLiteralRoutePathSegmentMetadata(
341
+ routePath: string,
342
+ parent?: RouteNode,
343
+ literalNewSegments = false,
344
+ ): Array<RoutePathSegmentMetadata | undefined> | undefined {
345
+ const routeSegments = routePath.split('/')
346
+ const metadata = new Array<RoutePathSegmentMetadata | undefined>(
347
+ routeSegments.length,
348
+ )
349
+ const parentDepth = countRoutePathSegments(parent?.routePath)
350
+ let hasMetadata = false
351
+ let depth = 0
352
+
353
+ for (let i = 0; i < routeSegments.length; i++) {
354
+ const segment = routeSegments[i]
355
+ metadata[i] = parent?._routePathSegmentMetadata?.[i]
356
+ hasMetadata ||= hasRoutePathSegmentMetadata(metadata[i])
357
+
358
+ if (!segment) continue
359
+
360
+ if (literalNewSegments && depth >= parentDepth) {
361
+ const literalLeadingUnderscore = segment.startsWith('_')
362
+ const literalTrailingUnderscore = segment.endsWith('_')
363
+
364
+ if (literalLeadingUnderscore || literalTrailingUnderscore) {
365
+ metadata[i] = mergeRoutePathSegmentMetadata(metadata[i], {
366
+ literalLeadingUnderscore: literalLeadingUnderscore || undefined,
367
+ literalTrailingUnderscore: literalTrailingUnderscore || undefined,
368
+ })
369
+ hasMetadata = true
370
+ }
371
+ }
372
+
373
+ depth++
374
+ }
375
+
376
+ return hasMetadata ? metadata : undefined
377
+ }
378
+
379
+ export function joinRoutePathSegmentMetadata(
380
+ routePath: string,
381
+ prefixPath: string,
382
+ prefixMetadata: Array<RoutePathSegmentMetadata | undefined> | undefined,
383
+ childMetadata: Array<RoutePathSegmentMetadata | undefined> | undefined,
384
+ ): Array<RoutePathSegmentMetadata | undefined> | undefined {
385
+ const metadata = new Array<RoutePathSegmentMetadata | undefined>(
386
+ countSlashSeparatedParts(routePath),
387
+ )
388
+ let hasMetadata = false
389
+
390
+ if (prefixMetadata) {
391
+ for (let i = 0; i < prefixMetadata.length && i < metadata.length; i++) {
392
+ metadata[i] = prefixMetadata[i]
393
+ hasMetadata ||= hasRoutePathSegmentMetadata(metadata[i])
394
+ }
395
+ }
396
+
397
+ const offset = countRoutePathSegments(prefixPath)
398
+ if (childMetadata) {
399
+ for (let i = 1; i < childMetadata.length; i++) {
400
+ const targetIndex = offset + i
401
+ if (targetIndex >= metadata.length) break
402
+
403
+ metadata[targetIndex] = mergeRoutePathSegmentMetadata(
404
+ metadata[targetIndex],
405
+ childMetadata[i],
406
+ )
407
+ hasMetadata ||= hasRoutePathSegmentMetadata(metadata[targetIndex])
408
+ }
409
+ }
410
+
411
+ return hasMetadata ? metadata : undefined
412
+ }
413
+
247
414
  const backslashRegex = /\\/g
248
415
 
249
416
  export function replaceBackslash(s: string) {
@@ -314,6 +481,23 @@ export function removeUnderscores(s?: string) {
314
481
  .replace(underscoreSlashRegex, '/')
315
482
  }
316
483
 
484
+ function removeUnderscoresFromSegment(
485
+ segment: string,
486
+ metadata?: RoutePathSegmentMetadata,
487
+ ): string {
488
+ let result = segment
489
+
490
+ if (result.startsWith('_') && !metadata?.literalLeadingUnderscore) {
491
+ result = result.slice(1)
492
+ }
493
+
494
+ if (result.endsWith('_') && !metadata?.literalTrailingUnderscore) {
495
+ result = result.slice(0, -1)
496
+ }
497
+
498
+ return result
499
+ }
500
+
317
501
  /**
318
502
  * Removes underscores from a path, but preserves underscores that were escaped
319
503
  * in the original path (indicated by [_] syntax).
@@ -330,30 +514,50 @@ export function removeUnderscoresWithEscape(
330
514
  if (!originalPath) return removeUnderscores(routePath) ?? ''
331
515
 
332
516
  const routeSegments = routePath.split('/')
333
- const originalSegments = originalPath.split('/')
517
+ const metadata = createRoutePathSegmentMetadata(routePath, originalPath)
518
+ const newSegments = new Array<string>(routeSegments.length)
334
519
 
335
- const newSegments = routeSegments.map((segment, i) => {
336
- const originalSegment = originalSegments[i] || ''
520
+ for (let i = 0; i < routeSegments.length; i++) {
521
+ newSegments[i] = removeUnderscoresFromSegment(
522
+ routeSegments[i]!,
523
+ metadata?.[i],
524
+ )
525
+ }
337
526
 
338
- // Check if leading underscore is escaped
339
- const leadingEscaped = hasEscapedLeadingUnderscore(originalSegment)
340
- // Check if trailing underscore is escaped
341
- const trailingEscaped = hasEscapedTrailingUnderscore(originalSegment)
527
+ return newSegments.join('/')
528
+ }
342
529
 
343
- let result = segment
530
+ export function removeLayoutSegmentsAndUnderscoresWithEscape(
531
+ routePath: string = '/',
532
+ originalPath?: string,
533
+ routePathSegmentMetadata?: Array<RoutePathSegmentMetadata | undefined>,
534
+ ): string {
535
+ if (!originalPath) {
536
+ return removeUnderscores(removeLayoutSegments(routePath)) ?? ''
537
+ }
344
538
 
345
- // Remove leading underscore only if not escaped
346
- if (result.startsWith('_') && !leadingEscaped) {
347
- result = result.slice(1)
348
- }
539
+ const metadata =
540
+ routePathSegmentMetadata ??
541
+ createRoutePathSegmentMetadata(routePath, originalPath)
542
+
543
+ const routeSegments = routePath.split('/')
544
+ const originalSegments = originalPath.split('/')
545
+ const newSegments: Array<string> = []
349
546
 
350
- // Remove trailing underscore only if not escaped
351
- if (result.endsWith('_') && !trailingEscaped) {
352
- result = result.slice(0, -1)
547
+ for (let i = 0; i < routeSegments.length; i++) {
548
+ const segment = routeSegments[i]!
549
+ const originalSegment = originalSegments[i] || ''
550
+ const segmentMetadata = metadata?.[i]
551
+
552
+ if (
553
+ !segmentMetadata?.literalLeadingUnderscore &&
554
+ isSegmentPathless(segment, originalSegment)
555
+ ) {
556
+ continue
353
557
  }
354
558
 
355
- return result
356
- })
559
+ newSegments.push(removeUnderscoresFromSegment(segment, segmentMetadata))
560
+ }
357
561
 
358
562
  return newSegments.join('/')
359
563
  }
@@ -686,12 +890,10 @@ const inferPath = (routeNode: RouteNode): string => {
686
890
  */
687
891
  export const inferFullPath = (routeNode: RouteNode): string => {
688
892
  const fullPath = removeGroups(
689
- removeUnderscoresWithEscape(
690
- removeLayoutSegmentsWithEscape(
691
- routeNode.routePath,
692
- routeNode.originalRoutePath,
693
- ),
893
+ removeLayoutSegmentsAndUnderscoresWithEscape(
894
+ routeNode.routePath,
694
895
  routeNode.originalRoutePath,
896
+ routeNode._routePathSegmentMetadata,
695
897
  ),
696
898
  )
697
899