@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.
- package/dist/cjs/config.cjs +21 -19
- package/dist/cjs/config.cjs.map +1 -1
- package/dist/cjs/config.d.cts +3 -0
- package/dist/cjs/filesystem/physical/getRouteNodes.cjs +125 -0
- package/dist/cjs/filesystem/physical/getRouteNodes.cjs.map +1 -0
- package/dist/cjs/filesystem/physical/getRouteNodes.d.cts +3 -0
- package/dist/cjs/filesystem/physical/rootPathId.cjs +5 -0
- package/dist/cjs/filesystem/physical/rootPathId.cjs.map +1 -0
- package/dist/cjs/filesystem/physical/rootPathId.d.cts +1 -0
- package/dist/cjs/filesystem/virtual/config.cjs +37 -0
- package/dist/cjs/filesystem/virtual/config.cjs.map +1 -0
- package/dist/cjs/filesystem/virtual/config.d.cts +3 -0
- package/dist/cjs/filesystem/virtual/getRouteNodes.cjs +119 -0
- package/dist/cjs/filesystem/virtual/getRouteNodes.cjs.map +1 -0
- package/dist/cjs/filesystem/virtual/getRouteNodes.d.cts +5 -0
- package/dist/cjs/generator.cjs +46 -191
- package/dist/cjs/generator.cjs.map +1 -1
- package/dist/cjs/generator.d.cts +1 -27
- package/dist/cjs/types.d.cts +27 -0
- package/dist/cjs/utils.cjs +54 -0
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +9 -0
- package/dist/esm/config.d.ts +3 -0
- package/dist/esm/config.js +2 -0
- package/dist/esm/config.js.map +1 -1
- package/dist/esm/filesystem/physical/getRouteNodes.d.ts +3 -0
- package/dist/esm/filesystem/physical/getRouteNodes.js +108 -0
- package/dist/esm/filesystem/physical/getRouteNodes.js.map +1 -0
- package/dist/esm/filesystem/physical/rootPathId.d.ts +1 -0
- package/dist/esm/filesystem/physical/rootPathId.js +5 -0
- package/dist/esm/filesystem/physical/rootPathId.js.map +1 -0
- package/dist/esm/filesystem/virtual/config.d.ts +3 -0
- package/dist/esm/filesystem/virtual/config.js +37 -0
- package/dist/esm/filesystem/virtual/config.js.map +1 -0
- package/dist/esm/filesystem/virtual/getRouteNodes.d.ts +5 -0
- package/dist/esm/filesystem/virtual/getRouteNodes.js +119 -0
- package/dist/esm/filesystem/virtual/getRouteNodes.js.map +1 -0
- package/dist/esm/generator.d.ts +1 -27
- package/dist/esm/generator.js +29 -174
- package/dist/esm/generator.js.map +1 -1
- package/dist/esm/types.d.ts +27 -0
- package/dist/esm/utils.d.ts +9 -0
- package/dist/esm/utils.js +54 -0
- package/dist/esm/utils.js.map +1 -1
- package/package.json +3 -2
- package/src/config.ts +2 -0
- package/src/filesystem/physical/getRouteNodes.ts +151 -0
- package/src/filesystem/physical/rootPathId.ts +1 -0
- package/src/filesystem/virtual/config.ts +45 -0
- package/src/filesystem/virtual/getRouteNodes.ts +141 -0
- package/src/generator.ts +46 -269
- package/src/types.ts +28 -0
- 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.
|
|
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
|
+
}
|