@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/router-generator",
3
- "version": "1.52.0",
3
+ "version": "1.55.0",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -50,7 +50,8 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "prettier": "^3.3.3",
53
- "zod": "^3.23.8"
53
+ "zod": "^3.23.8",
54
+ "@tanstack/virtual-file-routes": "^1.52.5"
54
55
  },
55
56
  "scripts": {}
56
57
  }
package/src/config.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import path from 'node:path'
2
2
  import { existsSync, readFileSync } from 'node:fs'
3
3
  import { z } from 'zod'
4
+ import { virtualRootRouteSchema } from './filesystem/virtual/config'
4
5
 
5
6
  export const configSchema = z.object({
7
+ virtualRouteConfig: virtualRootRouteSchema.optional(),
6
8
  routeFilePrefix: z.string().optional(),
7
9
  routeFileIgnorePrefix: z.string().optional().default('-'),
8
10
  routeFileIgnorePattern: z.string().optional(),
@@ -0,0 +1,151 @@
1
+ import path from 'node:path'
2
+ import * as fsp from 'node:fs/promises'
3
+ import {
4
+ determineInitialRoutePath,
5
+ logging,
6
+ removeExt,
7
+ removeTrailingSlash,
8
+ replaceBackslash,
9
+ routePathToVariable,
10
+ } from '../../utils'
11
+ import { rootPathId } from './rootPathId'
12
+ import type { GetRouteNodesResult, RouteNode } from '../../types'
13
+ import type { Config } from '../../config'
14
+
15
+ const disallowedRouteGroupConfiguration = /\(([^)]+)\).(ts|js|tsx|jsx)/
16
+
17
+ export async function getRouteNodes(
18
+ config: Config,
19
+ ): Promise<GetRouteNodesResult> {
20
+ const { routeFilePrefix, routeFileIgnorePrefix, routeFileIgnorePattern } =
21
+ config
22
+ const logger = logging({ disabled: config.disableLogging })
23
+ const routeFileIgnoreRegExp = new RegExp(routeFileIgnorePattern ?? '', 'g')
24
+
25
+ const routeNodes: Array<RouteNode> = []
26
+
27
+ async function recurse(dir: string) {
28
+ const fullDir = path.resolve(config.routesDirectory, dir)
29
+ let dirList = await fsp.readdir(fullDir, { withFileTypes: true })
30
+
31
+ dirList = dirList.filter((d) => {
32
+ if (
33
+ d.name.startsWith('.') ||
34
+ (routeFileIgnorePrefix && d.name.startsWith(routeFileIgnorePrefix))
35
+ ) {
36
+ return false
37
+ }
38
+
39
+ if (routeFilePrefix) {
40
+ return d.name.startsWith(routeFilePrefix)
41
+ }
42
+
43
+ if (routeFileIgnorePattern) {
44
+ return !d.name.match(routeFileIgnoreRegExp)
45
+ }
46
+
47
+ return true
48
+ })
49
+
50
+ await Promise.all(
51
+ dirList.map(async (dirent) => {
52
+ const fullPath = path.join(fullDir, dirent.name)
53
+ const relativePath = path.join(dir, dirent.name)
54
+
55
+ if (dirent.isDirectory()) {
56
+ await recurse(relativePath)
57
+ } else if (fullPath.match(/\.(tsx|ts|jsx|js)$/)) {
58
+ const filePath = replaceBackslash(path.join(dir, dirent.name))
59
+ const filePathNoExt = removeExt(filePath)
60
+ let routePath = determineInitialRoutePath(filePathNoExt)
61
+
62
+ if (routeFilePrefix) {
63
+ routePath = routePath.replaceAll(routeFilePrefix, '')
64
+ }
65
+
66
+ if (disallowedRouteGroupConfiguration.test(dirent.name)) {
67
+ 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?`
68
+ logger.error(`ERROR: ${errorMessage}`)
69
+ throw new Error(errorMessage)
70
+ }
71
+
72
+ const variableName = routePathToVariable(routePath)
73
+
74
+ const isLazy = routePath.endsWith('/lazy')
75
+
76
+ if (isLazy) {
77
+ routePath = routePath.replace(/\/lazy$/, '')
78
+ }
79
+
80
+ const isRoute = routePath.endsWith(`/${config.routeToken}`)
81
+ const isComponent = routePath.endsWith('/component')
82
+ const isErrorComponent = routePath.endsWith('/errorComponent')
83
+ const isPendingComponent = routePath.endsWith('/pendingComponent')
84
+ const isLoader = routePath.endsWith('/loader')
85
+ const isAPIRoute = routePath.startsWith(
86
+ `${removeTrailingSlash(config.apiBase)}/`,
87
+ )
88
+
89
+ const segments = routePath.split('/')
90
+ const lastRouteSegment = segments[segments.length - 1]
91
+ const isLayout =
92
+ (lastRouteSegment !== config.indexToken &&
93
+ lastRouteSegment !== config.routeToken &&
94
+ lastRouteSegment?.startsWith('_')) ||
95
+ false
96
+
97
+ ;(
98
+ [
99
+ [isComponent, 'component'],
100
+ [isErrorComponent, 'errorComponent'],
101
+ [isPendingComponent, 'pendingComponent'],
102
+ [isLoader, 'loader'],
103
+ ] as const
104
+ ).forEach(([isType, type]) => {
105
+ if (isType) {
106
+ logger.warn(
107
+ `WARNING: The \`.${type}.tsx\` suffix used for the ${filePath} file is deprecated. Use the new \`.lazy.tsx\` suffix instead.`,
108
+ )
109
+ }
110
+ })
111
+
112
+ routePath = routePath.replace(
113
+ new RegExp(
114
+ `/(component|errorComponent|pendingComponent|loader|${config.routeToken}|lazy)$`,
115
+ ),
116
+ '',
117
+ )
118
+
119
+ if (routePath === config.indexToken) {
120
+ routePath = '/'
121
+ }
122
+
123
+ routePath =
124
+ routePath.replace(new RegExp(`/${config.indexToken}$`), '/') || '/'
125
+
126
+ routeNodes.push({
127
+ filePath,
128
+ fullPath,
129
+ routePath,
130
+ variableName,
131
+ isRoute,
132
+ isComponent,
133
+ isErrorComponent,
134
+ isPendingComponent,
135
+ isLoader,
136
+ isLazy,
137
+ isLayout,
138
+ isAPIRoute,
139
+ })
140
+ }
141
+ }),
142
+ )
143
+
144
+ return routeNodes
145
+ }
146
+
147
+ await recurse('./')
148
+
149
+ const rootRouteNode = routeNodes.find((d) => d.routePath === `/${rootPathId}`)
150
+ return { rootRouteNode, routeNodes }
151
+ }
@@ -0,0 +1 @@
1
+ export const rootPathId = '__root'
@@ -0,0 +1,45 @@
1
+ import { z } from 'zod'
2
+ import type {
3
+ LayoutRoute,
4
+ PhysicalSubtree,
5
+ Route,
6
+ VirtualRootRoute,
7
+ } from '@tanstack/virtual-file-routes'
8
+
9
+ const indexRouteSchema = z.object({
10
+ type: z.literal('index'),
11
+ file: z.string(),
12
+ })
13
+
14
+ const layoutRouteSchema: z.ZodType<LayoutRoute> = z.object({
15
+ type: z.literal('layout'),
16
+ id: z.string(),
17
+ file: z.string(),
18
+ children: z.array(z.lazy(() => virtualRouteNodeSchema)).optional(),
19
+ })
20
+
21
+ const routeSchema: z.ZodType<Route> = z.object({
22
+ type: z.literal('route'),
23
+ file: z.string(),
24
+ path: z.string(),
25
+ children: z.array(z.lazy(() => virtualRouteNodeSchema)).optional(),
26
+ })
27
+
28
+ const physicalSubTreeSchema: z.ZodType<PhysicalSubtree> = z.object({
29
+ type: z.literal('physical'),
30
+ directory: z.string(),
31
+ pathPrefix: z.string(),
32
+ })
33
+
34
+ const virtualRouteNodeSchema = z.union([
35
+ indexRouteSchema,
36
+ layoutRouteSchema,
37
+ routeSchema,
38
+ physicalSubTreeSchema,
39
+ ])
40
+
41
+ export const virtualRootRouteSchema: z.ZodType<VirtualRootRoute> = z.object({
42
+ type: z.literal('root'),
43
+ file: z.string(),
44
+ children: z.array(virtualRouteNodeSchema).optional(),
45
+ })
@@ -0,0 +1,141 @@
1
+ import { join, resolve } from 'node:path'
2
+ import {
3
+ removeExt,
4
+ removeLeadingSlash,
5
+ removeTrailingSlash,
6
+ routePathToVariable,
7
+ } from '../../utils'
8
+ import { getRouteNodes as getRouteNodesPhysical } from '../physical/getRouteNodes'
9
+ import type { VirtualRouteNode } from '@tanstack/virtual-file-routes'
10
+ import type { GetRouteNodesResult, RouteNode } from '../../types'
11
+ import type { Config } from '../../config'
12
+
13
+ function ensureLeadingUnderScore(id: string) {
14
+ if (id.startsWith('_')) {
15
+ return id
16
+ }
17
+ return `_${id}`
18
+ }
19
+
20
+ function flattenTree(node: RouteNode): Array<RouteNode> {
21
+ const result = [node]
22
+
23
+ if (node.children) {
24
+ for (const child of node.children) {
25
+ result.push(...flattenTree(child))
26
+ }
27
+ }
28
+ delete node.children
29
+
30
+ return result
31
+ }
32
+
33
+ export async function getRouteNodes(
34
+ tsrConfig: Config,
35
+ ): Promise<GetRouteNodesResult> {
36
+ const fullDir = resolve(tsrConfig.routesDirectory)
37
+ if (tsrConfig.virtualRouteConfig === undefined) {
38
+ throw new Error(`virtualRouteConfig is undefined`)
39
+ }
40
+ const children = await getRouteNodesRecursive(
41
+ tsrConfig,
42
+ fullDir,
43
+ tsrConfig.virtualRouteConfig.children,
44
+ )
45
+ const allNodes = flattenTree({
46
+ children,
47
+ filePath: tsrConfig.virtualRouteConfig.file,
48
+ fullPath: join(fullDir, tsrConfig.virtualRouteConfig.file),
49
+ variableName: 'rootRoute',
50
+ routePath: '/',
51
+ isRoot: true,
52
+ })
53
+
54
+ const rootRouteNode = allNodes[0]
55
+ const routeNodes = allNodes.slice(1)
56
+
57
+ return { rootRouteNode, routeNodes }
58
+ }
59
+
60
+ export async function getRouteNodesRecursive(
61
+ tsrConfig: Config,
62
+ fullDir: string,
63
+ nodes?: Array<VirtualRouteNode>,
64
+ parent?: RouteNode,
65
+ ): Promise<Array<RouteNode>> {
66
+ if (nodes === undefined) {
67
+ return []
68
+ }
69
+ const children = await Promise.all(
70
+ nodes.map(async (node) => {
71
+ if (node.type === 'physical') {
72
+ const { routeNodes } = await getRouteNodesPhysical({
73
+ ...tsrConfig,
74
+ routesDirectory: resolve(fullDir, node.directory),
75
+ })
76
+ routeNodes.forEach((subtreeNode) => {
77
+ subtreeNode.variableName = routePathToVariable(
78
+ `${node.pathPrefix}/${removeExt(subtreeNode.filePath)}`,
79
+ )
80
+ subtreeNode.routePath = `${parent?.routePath ?? ''}${node.pathPrefix}${subtreeNode.routePath}`
81
+ subtreeNode.filePath = `${node.directory}/${subtreeNode.filePath}`
82
+ })
83
+ return routeNodes
84
+ }
85
+
86
+ const filePath = node.file
87
+ const variableName = routePathToVariable(removeExt(filePath))
88
+ const fullPath = join(fullDir, filePath)
89
+ const parentRoutePath = removeTrailingSlash(parent?.routePath ?? '/')
90
+ const isLayout = node.type === 'layout'
91
+ switch (node.type) {
92
+ case 'index': {
93
+ const routePath = `${parentRoutePath}/`
94
+ return {
95
+ filePath,
96
+ fullPath,
97
+ variableName,
98
+ routePath,
99
+ isLayout,
100
+ } satisfies RouteNode
101
+ }
102
+
103
+ case 'route':
104
+ case 'layout': {
105
+ let lastSegment: string
106
+ if (node.type === 'layout') {
107
+ if (node.id !== undefined) {
108
+ node.id = ensureLeadingUnderScore(node.id)
109
+ } else {
110
+ node.id = '_layout'
111
+ }
112
+ lastSegment = node.id
113
+ } else {
114
+ lastSegment = node.path
115
+ }
116
+ const routePath = `${parentRoutePath}/${removeLeadingSlash(lastSegment)}`
117
+
118
+ const routeNode: RouteNode = {
119
+ fullPath,
120
+ isLayout,
121
+ filePath,
122
+ variableName,
123
+ routePath,
124
+ }
125
+
126
+ if (node.children !== undefined) {
127
+ const children = await getRouteNodesRecursive(
128
+ tsrConfig,
129
+ fullDir,
130
+ node.children,
131
+ routeNode,
132
+ )
133
+ routeNode.children = children
134
+ }
135
+ return routeNode
136
+ }
137
+ }
138
+ }),
139
+ )
140
+ return children.flat()
141
+ }