@tanstack/router-generator 1.133.20 → 1.133.27

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/utils.ts CHANGED
@@ -53,8 +53,12 @@ export function removeTrailingSlash(s: string) {
53
53
  }
54
54
 
55
55
  const BRACKET_CONTENT_RE = /\[(.*?)\]/g
56
+ const SPLIT_REGEX = /(?<!\[)\.(?!\])/g
56
57
 
57
- export function determineInitialRoutePath(routePath: string) {
58
+ export function determineInitialRoutePath(
59
+ routePath: string,
60
+ config?: Pick<Config, 'experimental' | 'routeToken' | 'indexToken'>,
61
+ ) {
58
62
  const DISALLOWED_ESCAPE_CHARS = new Set([
59
63
  '/',
60
64
  '\\',
@@ -70,9 +74,39 @@ export function determineInitialRoutePath(routePath: string) {
70
74
  '%',
71
75
  ])
72
76
 
73
- const parts = routePath.split(/(?<!\[)\.(?!\])/g)
77
+ const originalRoutePath =
78
+ cleanPath(
79
+ `/${(cleanPath(routePath) || '').split(SPLIT_REGEX).join('/')}`,
80
+ ) || ''
81
+
82
+ // check if the route path is a valid non-nested path,
83
+ // TODO with new major rename to reflect not experimental anymore
84
+ const isExperimentalNonNestedRoute = isValidNonNestedRoute(
85
+ originalRoutePath,
86
+ config,
87
+ )
88
+
89
+ let cleanedRoutePath = routePath
90
+
91
+ // we already identified the path as non-nested and can now remove the trailing underscores
92
+ // we need to do this now before we encounter any escaped trailing underscores
93
+ // this way we can be sure any remaining trailing underscores should remain
94
+ // TODO with new major we can remove check and always remove leading underscores
95
+ if (config?.experimental?.nonNestedRoutes) {
96
+ // we should leave trailing underscores if the route path is the root path
97
+ if (originalRoutePath !== `/${rootPathId}`) {
98
+ // remove trailing underscores on various path segments
99
+ cleanedRoutePath = removeTrailingUnderscores(
100
+ originalRoutePath,
101
+ config.routeToken,
102
+ )
103
+ }
104
+ }
105
+
106
+ const parts = cleanedRoutePath.split(SPLIT_REGEX)
74
107
 
75
108
  // Escape any characters that in square brackets
109
+ // we keep the original path untouched
76
110
  const escapedParts = parts.map((part) => {
77
111
  // Check if any disallowed characters are used in brackets
78
112
 
@@ -102,7 +136,11 @@ export function determineInitialRoutePath(routePath: string) {
102
136
 
103
137
  const final = cleanPath(`/${escapedParts.join('/')}`) || ''
104
138
 
105
- return final
139
+ return {
140
+ routePath: final,
141
+ isExperimentalNonNestedRoute,
142
+ originalRoutePath,
143
+ }
106
144
  }
107
145
 
108
146
  export function replaceBackslash(s: string) {
@@ -155,6 +193,46 @@ export function removeUnderscores(s?: string) {
155
193
  return s?.replaceAll(/(^_|_$)/gi, '').replaceAll(/(\/_|_\/)/gi, '/')
156
194
  }
157
195
 
196
+ function escapeRegExp(s: string): string {
197
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
198
+ }
199
+
200
+ export function removeLeadingUnderscores(s: string, routeToken: string) {
201
+ if (!s) return s
202
+
203
+ const hasLeadingUnderscore = routeToken[0] === '_'
204
+
205
+ const routeTokenToExclude = hasLeadingUnderscore
206
+ ? routeToken.slice(1)
207
+ : routeToken
208
+
209
+ const escapedRouteToken = escapeRegExp(routeTokenToExclude)
210
+
211
+ const leadingUnderscoreRegex = hasLeadingUnderscore
212
+ ? new RegExp(`(?<=^|\\/)_(?!${escapedRouteToken})`, 'g')
213
+ : new RegExp(`(?<=^|\\/)_`, 'g')
214
+
215
+ return s.replaceAll(leadingUnderscoreRegex, '')
216
+ }
217
+
218
+ export function removeTrailingUnderscores(s: string, routeToken: string) {
219
+ if (!s) return s
220
+
221
+ const hasTrailingUnderscore = routeToken.slice(-1) === '_'
222
+
223
+ const routeTokenToExclude = hasTrailingUnderscore
224
+ ? routeToken.slice(0, -1)
225
+ : routeToken
226
+
227
+ const escapedRouteToken = escapeRegExp(routeTokenToExclude)
228
+
229
+ const trailingUnderscoreRegex = hasTrailingUnderscore
230
+ ? new RegExp(`(?<!${escapedRouteToken})_(?=\\/|$)`, 'g')
231
+ : new RegExp(`_(?=\\/)|_$`, 'g')
232
+
233
+ return s.replaceAll(trailingUnderscoreRegex, '')
234
+ }
235
+
158
236
  export function capitalize(s: string) {
159
237
  if (typeof s !== 'string') return ''
160
238
  return s.charAt(0).toUpperCase() + s.slice(1)
@@ -287,7 +365,22 @@ export function hasParentRoute(
287
365
  routes: Array<RouteNode>,
288
366
  node: RouteNode,
289
367
  routePathToCheck: string | undefined,
368
+ originalRoutePathToCheck?: string,
290
369
  ): 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
+
291
384
  if (!routePathToCheck || routePathToCheck === '/') {
292
385
  return null
293
386
  }
@@ -297,7 +390,42 @@ export function hasParentRoute(
297
390
  (d) => d.variableName,
298
391
  ]).filter((d) => d.routePath !== `/${rootPathId}`)
299
392
 
300
- for (const route of sortedNodes) {
393
+ const filteredNodes = node._isExperimentalNonNestedRoute
394
+ ? []
395
+ : [...sortedNodes]
396
+
397
+ if (node._isExperimentalNonNestedRoute) {
398
+ const nonNestedSegments = getNonNestedSegments(
399
+ originalRoutePathToCheck ?? '',
400
+ )
401
+
402
+ for (const route of sortedNodes) {
403
+ if (route.routePath === '/') continue
404
+
405
+ if (
406
+ routePathToCheck.startsWith(`${route.routePath}/`) &&
407
+ route._isExperimentalNonNestedRoute &&
408
+ route.routePath !== routePathToCheck
409
+ ) {
410
+ return route
411
+ }
412
+
413
+ if (
414
+ nonNestedSegments.find((seg) => seg === `${route.routePath}_`) ||
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) {
301
429
  if (route.routePath === '/') continue
302
430
 
303
431
  if (
@@ -312,7 +440,7 @@ export function hasParentRoute(
312
440
  segments.pop() // Remove the last segment
313
441
  const parentRoutePath = segments.join('/')
314
442
 
315
- return hasParentRoute(routes, node, parentRoutePath)
443
+ return hasParentRoute(routes, node, parentRoutePath, originalRoutePathToCheck)
316
444
  }
317
445
 
318
446
  /**
@@ -354,9 +482,16 @@ export const inferPath = (routeNode: RouteNode): string => {
354
482
  /**
355
483
  * Infers the full path for use by TS
356
484
  */
357
- export const inferFullPath = (routeNode: RouteNode): string => {
485
+ export const inferFullPath = (
486
+ routeNode: RouteNode,
487
+ config?: Pick<Config, 'experimental' | 'routeToken'>,
488
+ ): string => {
489
+ // with new nonNestedPaths feature we can be sure any remaining trailing underscores are escaped and should remain
490
+ // TODO with new major we can remove check and only remove leading underscores
358
491
  const fullPath = removeGroups(
359
- removeUnderscores(removeLayoutSegments(routeNode.routePath)) ?? '',
492
+ (config?.experimental?.nonNestedRoutes
493
+ ? removeLayoutSegments(routeNode.routePath)
494
+ : removeUnderscores(removeLayoutSegments(routeNode.routePath))) ?? '',
360
495
  )
361
496
 
362
497
  return routeNode.cleanedPath === '/' ? fullPath : fullPath.replace(/\/$/, '')
@@ -367,9 +502,13 @@ export const inferFullPath = (routeNode: RouteNode): string => {
367
502
  */
368
503
  export const createRouteNodesByFullPath = (
369
504
  routeNodes: Array<RouteNode>,
505
+ config?: Pick<Config, 'experimental' | 'routeToken'>,
370
506
  ): Map<string, RouteNode> => {
371
507
  return new Map(
372
- routeNodes.map((routeNode) => [inferFullPath(routeNode), routeNode]),
508
+ routeNodes.map((routeNode) => [
509
+ inferFullPath(routeNode, config),
510
+ routeNode,
511
+ ]),
373
512
  )
374
513
  }
375
514
 
@@ -378,10 +517,11 @@ export const createRouteNodesByFullPath = (
378
517
  */
379
518
  export const createRouteNodesByTo = (
380
519
  routeNodes: Array<RouteNode>,
520
+ config?: Pick<Config, 'experimental' | 'routeToken'>,
381
521
  ): Map<string, RouteNode> => {
382
522
  return new Map(
383
523
  dedupeBranchesAndIndexRoutes(routeNodes).map((routeNode) => [
384
- inferTo(routeNode),
524
+ inferTo(routeNode, config),
385
525
  routeNode,
386
526
  ]),
387
527
  )
@@ -404,8 +544,11 @@ export const createRouteNodesById = (
404
544
  /**
405
545
  * Infers to path
406
546
  */
407
- export const inferTo = (routeNode: RouteNode): string => {
408
- const fullPath = inferFullPath(routeNode)
547
+ export const inferTo = (
548
+ routeNode: RouteNode,
549
+ config?: Pick<Config, 'experimental' | 'routeToken'>,
550
+ ): string => {
551
+ const fullPath = inferFullPath(routeNode, config)
409
552
 
410
553
  if (fullPath === '/') return fullPath
411
554
 
@@ -444,7 +587,7 @@ export function checkRouteFullPathUniqueness(
444
587
  config: Config,
445
588
  ) {
446
589
  const routes = _routes.map((d) => {
447
- const inferredFullPath = inferFullPath(d)
590
+ const inferredFullPath = inferFullPath(d, config)
448
591
  return { ...d, inferredFullPath }
449
592
  })
450
593
 
@@ -581,6 +724,7 @@ export function buildFileRoutesByPathInterface(opts: {
581
724
  routeNodes: Array<RouteNode>
582
725
  module: string
583
726
  interfaceName: string
727
+ config?: Pick<Config, 'experimental' | 'routeToken'>
584
728
  }): string {
585
729
  return `declare module '${opts.module}' {
586
730
  interface ${opts.interfaceName} {
@@ -594,7 +738,7 @@ export function buildFileRoutesByPathInterface(opts: {
594
738
  return `'${filePathId}': {
595
739
  id: '${filePathId}'
596
740
  path: '${inferPath(routeNode)}'
597
- fullPath: '${inferFullPath(routeNode)}'
741
+ fullPath: '${inferFullPath(routeNode, opts.config)}'
598
742
  preLoaderRoute: ${preloaderRoute}
599
743
  parentRoute: typeof ${parent}
600
744
  }`
@@ -647,3 +791,51 @@ export function getImportForRouteNode(
647
791
  ],
648
792
  } satisfies ImportDeclaration
649
793
  }
794
+
795
+ /**
796
+ * Used to validate if a route is a pathless layout route
797
+ * @param normalizedRoutePath Normalized route path, i.e `/foo/_layout/route.tsx` and `/foo._layout.route.tsx` to `/foo/_layout/route`
798
+ * @param config The `router-generator` configuration object
799
+ * @returns Boolean indicating if the route is a pathless layout route
800
+ */
801
+ export function isValidNonNestedRoute(
802
+ normalizedRoutePath: string,
803
+ config?: Pick<Config, 'experimental' | 'routeToken' | 'indexToken'>,
804
+ ): boolean {
805
+ if (!config?.experimental?.nonNestedRoutes) {
806
+ return false
807
+ }
808
+
809
+ const segments = normalizedRoutePath.split('/').filter(Boolean)
810
+
811
+ if (segments.length === 0) {
812
+ return false
813
+ }
814
+
815
+ const lastRouteSegment = segments[segments.length - 1]!
816
+
817
+ // If segment === __root, then exit as false
818
+ if (lastRouteSegment === rootPathId) {
819
+ return false
820
+ }
821
+
822
+ if (
823
+ lastRouteSegment !== config.indexToken &&
824
+ lastRouteSegment !== config.routeToken &&
825
+ lastRouteSegment.endsWith('_')
826
+ ) {
827
+ return true
828
+ }
829
+
830
+ for (const segment of segments.slice(0, -1).reverse()) {
831
+ if (segment === config.routeToken) {
832
+ return false
833
+ }
834
+
835
+ if (segment.endsWith('_')) {
836
+ return true
837
+ }
838
+ }
839
+
840
+ return false
841
+ }