@tanstack/router-generator 1.52.0 → 1.55.0

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.
Files changed (53) hide show
  1. package/dist/cjs/config.cjs +21 -19
  2. package/dist/cjs/config.cjs.map +1 -1
  3. package/dist/cjs/config.d.cts +3 -0
  4. package/dist/cjs/filesystem/physical/getRouteNodes.cjs +125 -0
  5. package/dist/cjs/filesystem/physical/getRouteNodes.cjs.map +1 -0
  6. package/dist/cjs/filesystem/physical/getRouteNodes.d.cts +3 -0
  7. package/dist/cjs/filesystem/physical/rootPathId.cjs +5 -0
  8. package/dist/cjs/filesystem/physical/rootPathId.cjs.map +1 -0
  9. package/dist/cjs/filesystem/physical/rootPathId.d.cts +1 -0
  10. package/dist/cjs/filesystem/virtual/config.cjs +37 -0
  11. package/dist/cjs/filesystem/virtual/config.cjs.map +1 -0
  12. package/dist/cjs/filesystem/virtual/config.d.cts +3 -0
  13. package/dist/cjs/filesystem/virtual/getRouteNodes.cjs +119 -0
  14. package/dist/cjs/filesystem/virtual/getRouteNodes.cjs.map +1 -0
  15. package/dist/cjs/filesystem/virtual/getRouteNodes.d.cts +5 -0
  16. package/dist/cjs/generator.cjs +46 -191
  17. package/dist/cjs/generator.cjs.map +1 -1
  18. package/dist/cjs/generator.d.cts +1 -27
  19. package/dist/cjs/types.d.cts +27 -0
  20. package/dist/cjs/utils.cjs +54 -0
  21. package/dist/cjs/utils.cjs.map +1 -1
  22. package/dist/cjs/utils.d.cts +9 -0
  23. package/dist/esm/config.d.ts +3 -0
  24. package/dist/esm/config.js +2 -0
  25. package/dist/esm/config.js.map +1 -1
  26. package/dist/esm/filesystem/physical/getRouteNodes.d.ts +3 -0
  27. package/dist/esm/filesystem/physical/getRouteNodes.js +108 -0
  28. package/dist/esm/filesystem/physical/getRouteNodes.js.map +1 -0
  29. package/dist/esm/filesystem/physical/rootPathId.d.ts +1 -0
  30. package/dist/esm/filesystem/physical/rootPathId.js +5 -0
  31. package/dist/esm/filesystem/physical/rootPathId.js.map +1 -0
  32. package/dist/esm/filesystem/virtual/config.d.ts +3 -0
  33. package/dist/esm/filesystem/virtual/config.js +37 -0
  34. package/dist/esm/filesystem/virtual/config.js.map +1 -0
  35. package/dist/esm/filesystem/virtual/getRouteNodes.d.ts +5 -0
  36. package/dist/esm/filesystem/virtual/getRouteNodes.js +119 -0
  37. package/dist/esm/filesystem/virtual/getRouteNodes.js.map +1 -0
  38. package/dist/esm/generator.d.ts +1 -27
  39. package/dist/esm/generator.js +29 -174
  40. package/dist/esm/generator.js.map +1 -1
  41. package/dist/esm/types.d.ts +27 -0
  42. package/dist/esm/utils.d.ts +9 -0
  43. package/dist/esm/utils.js +54 -0
  44. package/dist/esm/utils.js.map +1 -1
  45. package/package.json +3 -2
  46. package/src/config.ts +2 -0
  47. package/src/filesystem/physical/getRouteNodes.ts +151 -0
  48. package/src/filesystem/physical/rootPathId.ts +1 -0
  49. package/src/filesystem/virtual/config.ts +45 -0
  50. package/src/filesystem/virtual/getRouteNodes.ts +141 -0
  51. package/src/generator.ts +46 -269
  52. package/src/types.ts +28 -0
  53. package/src/utils.ts +73 -0
package/src/generator.ts CHANGED
@@ -2,175 +2,26 @@ import path from 'node:path'
2
2
  import * as fs from 'node:fs'
3
3
  import * as fsp from 'node:fs/promises'
4
4
  import * as prettier from 'prettier'
5
- import { cleanPath, logging, trimPathLeft } from './utils'
5
+ import {
6
+ determineInitialRoutePath,
7
+ logging,
8
+ multiSortBy,
9
+ removeExt,
10
+ removeTrailingSlash,
11
+ removeUnderscores,
12
+ replaceBackslash,
13
+ routePathToVariable,
14
+ trimPathLeft,
15
+ } from './utils'
16
+ import { getRouteNodes as physicalGetRouteNodes } from './filesystem/physical/getRouteNodes'
17
+ import { getRouteNodes as virtualGetRouteNodes } from './filesystem/virtual/getRouteNodes'
18
+ import { rootPathId } from './filesystem/physical/rootPathId'
19
+ import type { GetRouteNodesResult, RouteNode } from './types'
6
20
  import type { Config } from './config'
7
21
 
8
22
  let latestTask = 0
9
- export const rootPathId = '__root'
10
23
  const routeGroupPatternRegex = /\(.+\)/g
11
24
  const possiblyNestedRouteGroupPatternRegex = /\([^/]+\)\/?/g
12
- const disallowedRouteGroupConfiguration = /\(([^)]+)\).(ts|js|tsx|jsx)/
13
-
14
- export type RouteNode = {
15
- filePath: string
16
- fullPath: string
17
- variableName: string
18
- routePath?: string
19
- cleanedPath?: string
20
- path?: string
21
- isNonPath?: boolean
22
- isNonLayout?: boolean
23
- isLayout?: boolean
24
- isVirtualParentRequired?: boolean
25
- isVirtualParentRoute?: boolean
26
- isRoute?: boolean
27
- isAPIRoute?: boolean
28
- isLoader?: boolean
29
- isComponent?: boolean
30
- isErrorComponent?: boolean
31
- isPendingComponent?: boolean
32
- isVirtual?: boolean
33
- isLazy?: boolean
34
- isRoot?: boolean
35
- children?: Array<RouteNode>
36
- parent?: RouteNode
37
- }
38
-
39
- async function getRouteNodes(config: Config) {
40
- const { routeFilePrefix, routeFileIgnorePrefix, routeFileIgnorePattern } =
41
- config
42
- const logger = logging({ disabled: config.disableLogging })
43
- const routeFileIgnoreRegExp = new RegExp(routeFileIgnorePattern ?? '', 'g')
44
-
45
- const routeNodes: Array<RouteNode> = []
46
-
47
- async function recurse(dir: string) {
48
- const fullDir = path.resolve(config.routesDirectory, dir)
49
- let dirList = await fsp.readdir(fullDir, { withFileTypes: true })
50
-
51
- dirList = dirList.filter((d) => {
52
- if (
53
- d.name.startsWith('.') ||
54
- (routeFileIgnorePrefix && d.name.startsWith(routeFileIgnorePrefix))
55
- ) {
56
- return false
57
- }
58
-
59
- if (routeFilePrefix) {
60
- return d.name.startsWith(routeFilePrefix)
61
- }
62
-
63
- if (routeFileIgnorePattern) {
64
- return !d.name.match(routeFileIgnoreRegExp)
65
- }
66
-
67
- return true
68
- })
69
-
70
- await Promise.all(
71
- dirList.map(async (dirent) => {
72
- const fullPath = path.join(fullDir, dirent.name)
73
- const relativePath = path.join(dir, dirent.name)
74
-
75
- if (dirent.isDirectory()) {
76
- await recurse(relativePath)
77
- } else if (fullPath.match(/\.(tsx|ts|jsx|js)$/)) {
78
- const filePath = replaceBackslash(path.join(dir, dirent.name))
79
- const filePathNoExt = removeExt(filePath)
80
- let routePath = determineInitialRoutePath(filePathNoExt)
81
-
82
- if (routeFilePrefix) {
83
- routePath = routePath.replaceAll(routeFilePrefix, '')
84
- }
85
-
86
- if (disallowedRouteGroupConfiguration.test(dirent.name)) {
87
- const errorMessage = `A route configuration for a route group was found at \`${filePath}\`. This is not supported. Did you mean to use a layout/pathless route instead?`
88
- logger.error(`ERROR: ${errorMessage}`)
89
- throw new Error(errorMessage)
90
- }
91
-
92
- const variableName = routePathToVariable(routePath)
93
-
94
- // Remove the index from the route path and
95
- // if the route path is empty, use `/'
96
-
97
- const isLazy = routePath.endsWith('/lazy')
98
-
99
- if (isLazy) {
100
- routePath = routePath.replace(/\/lazy$/, '')
101
- }
102
-
103
- const isRoute = routePath.endsWith(`/${config.routeToken}`)
104
- const isComponent = routePath.endsWith('/component')
105
- const isErrorComponent = routePath.endsWith('/errorComponent')
106
- const isPendingComponent = routePath.endsWith('/pendingComponent')
107
- const isLoader = routePath.endsWith('/loader')
108
- const isAPIRoute = routePath.startsWith(
109
- `${removeTrailingSlash(config.apiBase)}/`,
110
- )
111
-
112
- const segments = routePath.split('/')
113
- const lastRouteSegment = segments[segments.length - 1]
114
- const isLayout =
115
- (lastRouteSegment !== config.indexToken &&
116
- lastRouteSegment !== config.routeToken &&
117
- lastRouteSegment?.startsWith('_')) ||
118
- false
119
-
120
- ;(
121
- [
122
- [isComponent, 'component'],
123
- [isErrorComponent, 'errorComponent'],
124
- [isPendingComponent, 'pendingComponent'],
125
- [isLoader, 'loader'],
126
- ] as const
127
- ).forEach(([isType, type]) => {
128
- if (isType) {
129
- logger.warn(
130
- `WARNING: The \`.${type}.tsx\` suffix used for the ${filePath} file is deprecated. Use the new \`.lazy.tsx\` suffix instead.`,
131
- )
132
- }
133
- })
134
-
135
- routePath = routePath.replace(
136
- new RegExp(
137
- `/(component|errorComponent|pendingComponent|loader|${config.routeToken}|lazy)$`,
138
- ),
139
- '',
140
- )
141
-
142
- if (routePath === config.indexToken) {
143
- routePath = '/'
144
- }
145
-
146
- routePath =
147
- routePath.replace(new RegExp(`/${config.indexToken}$`), '/') || '/'
148
-
149
- routeNodes.push({
150
- filePath,
151
- fullPath,
152
- routePath,
153
- variableName,
154
- isRoute,
155
- isComponent,
156
- isErrorComponent,
157
- isPendingComponent,
158
- isLoader,
159
- isLazy,
160
- isLayout,
161
- isAPIRoute,
162
- })
163
- }
164
- }),
165
- )
166
-
167
- return routeNodes
168
- }
169
-
170
- await recurse('./')
171
-
172
- return routeNodes
173
- }
174
25
 
175
26
  let isFirst = false
176
27
  let skipMessage = false
@@ -216,12 +67,18 @@ export async function generator(config: Config) {
216
67
  parser: 'typescript',
217
68
  }
218
69
 
219
- const routePathIdPrefix = config.routeFilePrefix ?? ''
220
- const beforeRouteNodes = await getRouteNodes(config)
221
- const rootRouteNode = beforeRouteNodes.find(
222
- (d) => d.routePath === `/${rootPathId}`,
223
- )
70
+ let getRouteNodesResult: GetRouteNodesResult
224
71
 
72
+ if (config.virtualRouteConfig) {
73
+ getRouteNodesResult = await virtualGetRouteNodes(config)
74
+ } else {
75
+ getRouteNodesResult = await physicalGetRouteNodes(config)
76
+ }
77
+
78
+ const { rootRouteNode, routeNodes: beforeRouteNodes } = getRouteNodesResult
79
+ if (rootRouteNode === undefined) {
80
+ throw new Error(`rootRouteNode must not be undefined`)
81
+ }
225
82
  const preRouteNodes = multiSortBy(beforeRouteNodes, [
226
83
  (d) => (d.routePath === '/' ? -1 : 1),
227
84
  (d) => d.routePath?.split('/').length,
@@ -307,13 +164,11 @@ export const Route = createRootRoute({
307
164
  const trimmedPath = trimPathLeft(node.path ?? '')
308
165
 
309
166
  const split = trimmedPath.split('/')
310
- const first = split[0] ?? trimmedPath
311
167
  const lastRouteSegment = split[split.length - 1] ?? trimmedPath
312
168
 
313
169
  node.isNonPath =
314
170
  lastRouteSegment.startsWith('_') ||
315
171
  routeGroupPatternRegex.test(lastRouteSegment)
316
- node.isNonLayout = first.endsWith('_')
317
172
 
318
173
  node.cleanedPath = removeGroups(
319
174
  removeUnderscores(removeLayoutSegments(node.path)) ?? '',
@@ -571,11 +426,18 @@ export const Route = createAPIFileRoute('${escapedRoutePath}')({
571
426
  .map((d) => d[0])
572
427
 
573
428
  const virtualRouteNodes = sortedRouteNodes.filter((d) => d.isVirtual)
574
- const rootPathIdExtension =
575
- config.addExtensions && rootRouteNode
576
- ? path.extname(rootRouteNode.filePath)
577
- : ''
578
429
 
430
+ function getImportPath(node: RouteNode) {
431
+ return replaceBackslash(
432
+ removeExt(
433
+ path.relative(
434
+ path.dirname(config.generatedRouteTree),
435
+ path.resolve(config.routesDirectory, node.filePath),
436
+ ),
437
+ config.addExtensions,
438
+ ),
439
+ )
440
+ }
579
441
  const routeImports = [
580
442
  ...config.routeTreeFileHeader,
581
443
  '// This file is auto-generated by TanStack Router',
@@ -584,29 +446,13 @@ export const Route = createAPIFileRoute('${escapedRoutePath}')({
584
446
  : '',
585
447
  '// Import Routes',
586
448
  [
587
- `import { Route as rootRoute } from './${replaceBackslash(
588
- path.relative(
589
- path.dirname(config.generatedRouteTree),
590
- path.resolve(
591
- config.routesDirectory,
592
- `${routePathIdPrefix}${rootPathId}${rootPathIdExtension}`,
593
- ),
594
- ),
595
- )}'`,
449
+ `import { Route as rootRoute } from './${getImportPath(rootRouteNode)}'`,
596
450
  ...sortedRouteNodes
597
451
  .filter((d) => !d.isVirtual)
598
452
  .map((node) => {
599
453
  return `import { Route as ${
600
454
  node.variableName
601
- }Import } from './${replaceBackslash(
602
- removeExt(
603
- path.relative(
604
- path.dirname(config.generatedRouteTree),
605
- path.resolve(config.routesDirectory, node.filePath),
606
- ),
607
- config.addExtensions,
608
- ),
609
- )}'`
455
+ }Import } from './${getImportPath(node)}'`
610
456
  }),
611
457
  ].join('\n'),
612
458
  virtualRouteNodes.length ? '// Create Virtual Routes' : '',
@@ -704,7 +550,7 @@ export const Route = createAPIFileRoute('${escapedRoutePath}')({
704
550
  ${routeNodes
705
551
  .map((routeNode) => {
706
552
  const [filePathId, routeId] = getFilePathIdAndRouteIdFromPath(
707
- routeNode.routePath!,
553
+ routeNode.routePath,
708
554
  )
709
555
 
710
556
  return `'${filePathId}': {
@@ -735,14 +581,14 @@ export const Route = createAPIFileRoute('${escapedRoutePath}')({
735
581
  const createRouteManifest = () => {
736
582
  const routesManifest = {
737
583
  __root__: {
738
- filePath: rootRouteNode?.filePath,
584
+ filePath: rootRouteNode.filePath,
739
585
  children: routeTree.map(
740
- (d) => getFilePathIdAndRouteIdFromPath(d.routePath!)[1],
586
+ (d) => getFilePathIdAndRouteIdFromPath(d.routePath)[1],
741
587
  ),
742
588
  },
743
589
  ...Object.fromEntries(
744
590
  routeNodes.map((d) => {
745
- const [_, routeId] = getFilePathIdAndRouteIdFromPath(d.routePath!)
591
+ const [_, routeId] = getFilePathIdAndRouteIdFromPath(d.routePath)
746
592
 
747
593
  return [
748
594
  routeId,
@@ -753,7 +599,7 @@ export const Route = createAPIFileRoute('${escapedRoutePath}')({
753
599
  : undefined,
754
600
  children: d.children?.map(
755
601
  (childRoute) =>
756
- getFilePathIdAndRouteIdFromPath(childRoute.routePath!)[1],
602
+ getFilePathIdAndRouteIdFromPath(childRoute.routePath)[1],
757
603
  ),
758
604
  },
759
605
  ]
@@ -820,89 +666,20 @@ export const Route = createAPIFileRoute('${escapedRoutePath}')({
820
666
  )
821
667
  }
822
668
 
823
- function routePathToVariable(routePath: string): string {
824
- return (
825
- removeUnderscores(routePath)
826
- ?.replace(/\/\$\//g, '/splat/')
827
- .replace(/\$$/g, 'splat')
828
- .replace(/\$/g, '')
829
- .split(/[/-]/g)
830
- .map((d, i) => (i > 0 ? capitalize(d) : d))
831
- .join('')
832
- .replace(/([^a-zA-Z0-9]|[.])/gm, '')
833
- .replace(/^(\d)/g, 'R$1') ?? ''
834
- )
835
- }
836
-
837
- export function removeExt(d: string, keepExtension: boolean = false) {
838
- return keepExtension ? d : d.substring(0, d.lastIndexOf('.')) || d
839
- }
840
-
841
669
  function spaces(d: number): string {
842
670
  return Array.from({ length: d })
843
671
  .map(() => ' ')
844
672
  .join('')
845
673
  }
846
674
 
847
- export function multiSortBy<T>(
848
- arr: Array<T>,
849
- accessors: Array<(item: T) => any> = [(d) => d],
850
- ): Array<T> {
851
- return arr
852
- .map((d, i) => [d, i] as const)
853
- .sort(([a, ai], [b, bi]) => {
854
- for (const accessor of accessors) {
855
- const ao = accessor(a)
856
- const bo = accessor(b)
857
-
858
- if (typeof ao === 'undefined') {
859
- if (typeof bo === 'undefined') {
860
- continue
861
- }
862
- return 1
863
- }
864
-
865
- if (ao === bo) {
866
- continue
867
- }
868
-
869
- return ao > bo ? 1 : -1
870
- }
871
-
872
- return ai - bi
873
- })
874
- .map(([d]) => d)
875
- }
876
-
877
- function capitalize(s: string) {
878
- if (typeof s !== 'string') return ''
879
- return s.charAt(0).toUpperCase() + s.slice(1)
880
- }
881
-
882
- function removeUnderscores(s?: string) {
883
- return s?.replaceAll(/(^_|_$)/gi, '').replaceAll(/(\/_|_\/)/gi, '/')
884
- }
885
-
886
675
  function removeTrailingUnderscores(s?: string) {
887
676
  return s?.replaceAll(/(_$)/gi, '').replaceAll(/(_\/)/gi, '/')
888
677
  }
889
678
 
890
- function replaceBackslash(s: string) {
891
- return s.replaceAll(/\\/gi, '/')
892
- }
893
-
894
679
  function removeGroups(s: string) {
895
680
  return s.replace(possiblyNestedRouteGroupPatternRegex, '')
896
681
  }
897
682
 
898
- function removeTrailingSlash(s: string) {
899
- return s.replace(/\/$/, '')
900
- }
901
-
902
- function determineInitialRoutePath(routePath: string) {
903
- return cleanPath(`/${routePath.split('.').join('/')}`) || ''
904
- }
905
-
906
683
  /**
907
684
  * The `node.path` is used as the `id` in the route definition.
908
685
  * This function checks if the given node has a parent and if so, it determines the correct path for the given node.
@@ -911,7 +688,7 @@ function determineInitialRoutePath(routePath: string) {
911
688
  */
912
689
  function determineNodePath(node: RouteNode) {
913
690
  return (node.path = node.parent
914
- ? node.routePath?.replace(node.parent.routePath!, '') || '/'
691
+ ? node.routePath?.replace(node.parent.routePath ?? '', '') || '/'
915
692
  : node.routePath)
916
693
  }
917
694
 
@@ -995,7 +772,7 @@ export const inferPath = (routeNode: RouteNode): string => {
995
772
  : (routeNode.cleanedPath?.replace(/\/$/, '') ?? '')
996
773
  }
997
774
 
998
- function getFilePathIdAndRouteIdFromPath(pathname: string) {
775
+ function getFilePathIdAndRouteIdFromPath(pathname?: string) {
999
776
  const filePathId = removeTrailingUnderscores(pathname)
1000
777
  const id = removeGroups(filePathId ?? '')
1001
778
 
package/src/types.ts ADDED
@@ -0,0 +1,28 @@
1
+ export type RouteNode = {
2
+ filePath: string
3
+ fullPath: string
4
+ variableName: string
5
+ routePath?: string
6
+ cleanedPath?: string
7
+ path?: string
8
+ isNonPath?: boolean
9
+ isLayout?: boolean
10
+ isVirtualParentRequired?: boolean
11
+ isVirtualParentRoute?: boolean
12
+ isRoute?: boolean
13
+ isAPIRoute?: boolean
14
+ isLoader?: boolean
15
+ isComponent?: boolean
16
+ isErrorComponent?: boolean
17
+ isPendingComponent?: boolean
18
+ isVirtual?: boolean
19
+ isLazy?: boolean
20
+ isRoot?: boolean
21
+ children?: Array<RouteNode>
22
+ parent?: RouteNode
23
+ }
24
+
25
+ export interface GetRouteNodesResult {
26
+ rootRouteNode?: RouteNode
27
+ routeNodes: Array<RouteNode>
28
+ }
package/src/utils.ts CHANGED
@@ -1,3 +1,33 @@
1
+ export function multiSortBy<T>(
2
+ arr: Array<T>,
3
+ accessors: Array<(item: T) => any> = [(d) => d],
4
+ ): Array<T> {
5
+ return arr
6
+ .map((d, i) => [d, i] as const)
7
+ .sort(([a, ai], [b, bi]) => {
8
+ for (const accessor of accessors) {
9
+ const ao = accessor(a)
10
+ const bo = accessor(b)
11
+
12
+ if (typeof ao === 'undefined') {
13
+ if (typeof bo === 'undefined') {
14
+ continue
15
+ }
16
+ return 1
17
+ }
18
+
19
+ if (ao === bo) {
20
+ continue
21
+ }
22
+
23
+ return ao > bo ? 1 : -1
24
+ }
25
+
26
+ return ai - bi
27
+ })
28
+ .map(([d]) => d)
29
+ }
30
+
1
31
  export function cleanPath(path: string) {
2
32
  // remove double slashes
3
33
  return path.replace(/\/{2,}/g, '/')
@@ -26,3 +56,46 @@ export function logging(config: { disabled: boolean }) {
26
56
  },
27
57
  }
28
58
  }
59
+
60
+ export function removeLeadingSlash(path: string): string {
61
+ return path.replace(/^\//, '')
62
+ }
63
+
64
+ export function removeTrailingSlash(s: string) {
65
+ return s.replace(/\/$/, '')
66
+ }
67
+
68
+ export function determineInitialRoutePath(routePath: string) {
69
+ return cleanPath(`/${routePath.split('.').join('/')}`) || ''
70
+ }
71
+
72
+ export function replaceBackslash(s: string) {
73
+ return s.replaceAll(/\\/gi, '/')
74
+ }
75
+
76
+ export function routePathToVariable(routePath: string): string {
77
+ return (
78
+ removeUnderscores(routePath)
79
+ ?.replace(/\/\$\//g, '/splat/')
80
+ .replace(/\$$/g, 'splat')
81
+ .replace(/\$/g, '')
82
+ .split(/[/-]/g)
83
+ .map((d, i) => (i > 0 ? capitalize(d) : d))
84
+ .join('')
85
+ .replace(/([^a-zA-Z0-9]|[.])/gm, '')
86
+ .replace(/^(\d)/g, 'R$1') ?? ''
87
+ )
88
+ }
89
+
90
+ export function removeUnderscores(s?: string) {
91
+ return s?.replaceAll(/(^_|_$)/gi, '').replaceAll(/(\/_|_\/)/gi, '/')
92
+ }
93
+
94
+ export function capitalize(s: string) {
95
+ if (typeof s !== 'string') return ''
96
+ return s.charAt(0).toUpperCase() + s.slice(1)
97
+ }
98
+
99
+ export function removeExt(d: string, keepExtension: boolean = false) {
100
+ return keepExtension ? d : d.substring(0, d.lastIndexOf('.')) || d
101
+ }