@tanstack/router-cli 0.0.1-beta.69 → 1.0.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/LICENSE +1 -1
- package/bin/tsr.js +1 -1
- package/build/cjs/config.js +14 -10
- package/build/cjs/config.js.map +1 -1
- package/build/cjs/generate.js +1 -3
- package/build/cjs/generate.js.map +1 -1
- package/build/cjs/generator.js +203 -237
- package/build/cjs/generator.js.map +1 -1
- package/build/cjs/index.js +4 -7
- package/build/cjs/index.js.map +1 -1
- package/build/cjs/watch.js +17 -24
- package/build/cjs/watch.js.map +1 -1
- package/build/types/config.d.ts +20 -0
- package/build/types/generate.d.ts +2 -0
- package/build/types/generator.d.ts +20 -0
- package/build/types/index.d.ts +1 -1
- package/build/types/watch.d.ts +1 -0
- package/package.json +8 -2
- package/src/config.ts +13 -8
- package/src/generator.ts +290 -394
- package/src/watch.ts +19 -16
- package/build/cjs/transformCode.js +0 -584
- package/build/cjs/transformCode.js.map +0 -1
- package/build/esm/index.js +0 -903
- package/build/esm/index.js.map +0 -1
- package/src/transformCode.ts +0 -860
package/src/generator.ts
CHANGED
|
@@ -1,61 +1,105 @@
|
|
|
1
|
-
import klaw from 'klaw'
|
|
2
|
-
import through2 from 'through2'
|
|
3
1
|
import path from 'path'
|
|
4
2
|
import fs from 'fs-extra'
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
detectExports,
|
|
8
|
-
ensureBoilerplate,
|
|
9
|
-
generateRouteConfig,
|
|
10
|
-
isolatedProperties,
|
|
11
|
-
isolateOptionToExport,
|
|
12
|
-
} from './transformCode'
|
|
3
|
+
import * as prettier from 'prettier'
|
|
13
4
|
import { Config } from './config'
|
|
5
|
+
import { cleanPath, trimPathLeft } from '@tanstack/react-router'
|
|
14
6
|
|
|
15
7
|
let latestTask = 0
|
|
16
|
-
export const
|
|
17
|
-
export const
|
|
8
|
+
export const rootPathId = '__root'
|
|
9
|
+
export const fileRouteRegex = /new\s+FileRoute\(([^)]*)\)/g
|
|
18
10
|
|
|
19
11
|
export type RouteNode = {
|
|
20
|
-
|
|
21
|
-
clientFilename: string
|
|
22
|
-
fileNameNoExt: string
|
|
12
|
+
filePath: string
|
|
23
13
|
fullPath: string
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
genDir: string
|
|
31
|
-
genPathNoExt: string
|
|
32
|
-
parent?: RouteNode
|
|
33
|
-
hash?: string
|
|
34
|
-
importedFiles?: string[]
|
|
35
|
-
version?: number
|
|
36
|
-
changed?: boolean
|
|
37
|
-
new?: boolean
|
|
14
|
+
variableName: string
|
|
15
|
+
routePath?: string
|
|
16
|
+
cleanedPath?: string
|
|
17
|
+
path?: string
|
|
18
|
+
isNonPath?: boolean
|
|
19
|
+
isNonLayout?: boolean
|
|
38
20
|
isRoot?: boolean
|
|
39
21
|
children?: RouteNode[]
|
|
22
|
+
parent?: RouteNode
|
|
40
23
|
}
|
|
41
24
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
25
|
+
async function getRouteNodes(config: Config) {
|
|
26
|
+
const { routeFilePrefix, routeFileIgnorePrefix } = config
|
|
27
|
+
|
|
28
|
+
let routeNodes: RouteNode[] = []
|
|
29
|
+
|
|
30
|
+
async function recurse(dir: string) {
|
|
31
|
+
const fullDir = path.resolve(config.routesDirectory, dir)
|
|
32
|
+
let dirList = await fs.readdir(fullDir)
|
|
33
|
+
|
|
34
|
+
dirList = dirList.filter((d) => {
|
|
35
|
+
if (
|
|
36
|
+
d.startsWith('.') ||
|
|
37
|
+
(routeFileIgnorePrefix && d.startsWith(routeFileIgnorePrefix))
|
|
38
|
+
) {
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (routeFilePrefix) {
|
|
43
|
+
return d.startsWith(routeFilePrefix)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return true
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
await Promise.all(
|
|
50
|
+
dirList.map(async (fileName) => {
|
|
51
|
+
const fullPath = path.join(fullDir, fileName)
|
|
52
|
+
const relativePath = path.join(dir, fileName)
|
|
53
|
+
const stat = await fs.stat(fullPath)
|
|
54
|
+
|
|
55
|
+
if (stat.isDirectory()) {
|
|
56
|
+
await recurse(relativePath)
|
|
57
|
+
} else {
|
|
58
|
+
const filePath = path.join(dir, fileName)
|
|
59
|
+
const filePathNoExt = removeExt(filePath)
|
|
60
|
+
let routePath =
|
|
61
|
+
replaceBackslash(
|
|
62
|
+
cleanPath(`/${filePathNoExt.split('.').join('/')}`),
|
|
63
|
+
) ?? ''
|
|
64
|
+
const variableName = fileToVariable(routePath)
|
|
65
|
+
|
|
66
|
+
// Remove the index from the route path and
|
|
67
|
+
// if the route path is empty, use `/'
|
|
68
|
+
if (routePath === 'index') {
|
|
69
|
+
routePath = '/'
|
|
70
|
+
} else if (routePath.endsWith('/index')) {
|
|
71
|
+
routePath = routePath.replace(/\/index$/, '/')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
routeNodes.push({
|
|
75
|
+
filePath,
|
|
76
|
+
fullPath,
|
|
77
|
+
routePath,
|
|
78
|
+
variableName,
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
}),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return routeNodes
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await recurse('./')
|
|
88
|
+
|
|
89
|
+
return routeNodes
|
|
46
90
|
}
|
|
47
91
|
|
|
48
|
-
let
|
|
92
|
+
let first = false
|
|
93
|
+
let skipMessage = false
|
|
49
94
|
|
|
50
95
|
export async function generator(config: Config) {
|
|
51
96
|
console.log()
|
|
52
97
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (!nodeCache) {
|
|
56
|
-
first = true
|
|
98
|
+
if (!first) {
|
|
57
99
|
console.log('🔄 Generating routes...')
|
|
58
|
-
|
|
100
|
+
first = true
|
|
101
|
+
} else if (skipMessage) {
|
|
102
|
+
skipMessage = false
|
|
59
103
|
} else {
|
|
60
104
|
console.log('♻️ Regenerating routes...')
|
|
61
105
|
}
|
|
@@ -65,7 +109,7 @@ export async function generator(config: Config) {
|
|
|
65
109
|
|
|
66
110
|
const checkLatest = () => {
|
|
67
111
|
if (latestTask !== taskId) {
|
|
68
|
-
|
|
112
|
+
skipMessage = true
|
|
69
113
|
return false
|
|
70
114
|
}
|
|
71
115
|
|
|
@@ -73,398 +117,208 @@ export async function generator(config: Config) {
|
|
|
73
117
|
}
|
|
74
118
|
|
|
75
119
|
const start = Date.now()
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
120
|
+
const routePathIdPrefix = config.routeFilePrefix ?? ''
|
|
121
|
+
|
|
122
|
+
let routeNodes = await getRouteNodes(config)
|
|
123
|
+
|
|
124
|
+
routeNodes = multiSortBy(routeNodes, [
|
|
125
|
+
(d) => (d.routePath === '/' ? -1 : 1),
|
|
126
|
+
(d) => d.routePath?.split('/').length,
|
|
127
|
+
(d) => (d.routePath?.endsWith('/') ? -1 : 1),
|
|
128
|
+
(d) => d.routePath,
|
|
129
|
+
]).filter((d) => d.routePath !== `/${routePathIdPrefix + rootPathId}`)
|
|
130
|
+
|
|
131
|
+
const routeTree: RouteNode[] = []
|
|
132
|
+
|
|
133
|
+
// Loop over the flat list of routeNodes and
|
|
134
|
+
// build up a tree based on the routeNodes' routePath
|
|
135
|
+
routeNodes.forEach((node) => {
|
|
136
|
+
// routeNodes.forEach((existingNode) => {
|
|
137
|
+
// if (
|
|
138
|
+
// node.routePath?.startsWith(`${existingNode?.routePath ?? ''}/`)
|
|
139
|
+
// // node.routePath.length > existingNode.routePath!.length
|
|
140
|
+
// ) {
|
|
141
|
+
// node.parent = existingNode
|
|
142
|
+
// }
|
|
143
|
+
// })
|
|
144
|
+
const parentRoute = hasParentRoute(routeNodes, node.routePath)
|
|
145
|
+
if (parentRoute) node.parent = parentRoute
|
|
146
|
+
|
|
147
|
+
node.path = node.parent
|
|
148
|
+
? node.routePath?.replace(node.parent.routePath!, '') || '/'
|
|
149
|
+
: node.routePath
|
|
150
|
+
|
|
151
|
+
const trimmedPath = trimPathLeft(node.path ?? '')
|
|
152
|
+
|
|
153
|
+
const split = trimmedPath?.split('/') ?? []
|
|
154
|
+
let first = split[0] ?? trimmedPath ?? ''
|
|
155
|
+
|
|
156
|
+
node.isNonPath = first.startsWith('_')
|
|
157
|
+
node.isNonLayout = first.endsWith('_')
|
|
158
|
+
|
|
159
|
+
node.cleanedPath = removeUnderscores(node.path) ?? ''
|
|
160
|
+
|
|
161
|
+
if (node.parent) {
|
|
162
|
+
node.parent.children = node.parent.children ?? []
|
|
163
|
+
node.parent.children.push(node)
|
|
164
|
+
} else {
|
|
165
|
+
routeTree.push(node)
|
|
97
166
|
}
|
|
98
|
-
|
|
99
|
-
const dirListCombo = multiSortBy(
|
|
100
|
-
await Promise.all(
|
|
101
|
-
dirList.map(async (filename): Promise<RouteNode> => {
|
|
102
|
-
const fullPath = path.resolve(dir, filename)
|
|
103
|
-
const stat = await fs.lstat(fullPath)
|
|
104
|
-
const ext = path.extname(filename)
|
|
105
|
-
|
|
106
|
-
const clientFilename = filename.replace(ext, `.client${ext}`)
|
|
107
|
-
|
|
108
|
-
const pathFromRoutes = path.relative(config.routesDirectory, fullPath)
|
|
109
|
-
const genPath = path.resolve(config.routeGenDirectory, pathFromRoutes)
|
|
110
|
-
|
|
111
|
-
const genPathNoExt = removeExt(genPath)
|
|
112
|
-
const genDir = path.resolve(genPath, '..')
|
|
113
|
-
|
|
114
|
-
const fileNameNoExt = removeExt(filename)
|
|
115
|
-
|
|
116
|
-
return {
|
|
117
|
-
filename,
|
|
118
|
-
clientFilename,
|
|
119
|
-
fileNameNoExt,
|
|
120
|
-
fullPath,
|
|
121
|
-
fullDir: dir,
|
|
122
|
-
genPath,
|
|
123
|
-
genDir,
|
|
124
|
-
genPathNoExt,
|
|
125
|
-
variable: fileToVariable(removeExt(pathFromRoutes)),
|
|
126
|
-
isDirectory: stat.isDirectory(),
|
|
127
|
-
isIndex: fileNameNoExt === 'index',
|
|
128
|
-
}
|
|
129
|
-
}),
|
|
130
|
-
),
|
|
131
|
-
[
|
|
132
|
-
(d) => (d.fileNameNoExt === 'index' ? -1 : 1),
|
|
133
|
-
(d) => d.fileNameNoExt,
|
|
134
|
-
(d) => (d.isDirectory ? 1 : -1),
|
|
135
|
-
],
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
const reparented: typeof dirListCombo = []
|
|
139
|
-
|
|
140
|
-
dirListCombo.forEach(async (d, i) => {
|
|
141
|
-
if (d.isDirectory) {
|
|
142
|
-
const parent = reparented.find(
|
|
143
|
-
(dd) => !dd.isDirectory && dd.fileNameNoExt === d.filename,
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
if (parent) {
|
|
147
|
-
parent.childRoutesDir = d.fullPath
|
|
148
|
-
} else {
|
|
149
|
-
reparented.push(d)
|
|
150
|
-
}
|
|
151
|
-
} else {
|
|
152
|
-
reparented.push(d)
|
|
153
|
-
}
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
return Promise.all(
|
|
157
|
-
reparented.map(async (d) => {
|
|
158
|
-
if (d.childRoutesDir) {
|
|
159
|
-
const children = await reparent(d.childRoutesDir)
|
|
160
|
-
|
|
161
|
-
d = {
|
|
162
|
-
...d,
|
|
163
|
-
children,
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
children.forEach((child) => (child.parent = d))
|
|
167
|
-
|
|
168
|
-
return d
|
|
169
|
-
}
|
|
170
|
-
return d
|
|
171
|
-
}),
|
|
172
|
-
)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const reparented = await reparent(config.routesDirectory)
|
|
167
|
+
})
|
|
176
168
|
|
|
177
169
|
async function buildRouteConfig(
|
|
178
170
|
nodes: RouteNode[],
|
|
179
171
|
depth = 1,
|
|
180
172
|
): Promise<string> {
|
|
181
|
-
const children = nodes.map(async (
|
|
182
|
-
let node = nodeCache.find((d) => d.fullPath === n.fullPath)!
|
|
183
|
-
|
|
184
|
-
if (node) {
|
|
185
|
-
node.new = false
|
|
186
|
-
} else {
|
|
187
|
-
node = n
|
|
188
|
-
nodeCache.push(node)
|
|
189
|
-
if (!first) {
|
|
190
|
-
node.new = true
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
node.version = latestTask
|
|
195
|
-
if (node.fileNameNoExt === '__root') {
|
|
196
|
-
node.isRoot = true
|
|
197
|
-
}
|
|
198
|
-
|
|
173
|
+
const children = nodes.map(async (node) => {
|
|
199
174
|
const routeCode = await fs.readFile(node.fullPath, 'utf-8')
|
|
200
175
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
node.changed = node.hash !== hash
|
|
206
|
-
if (node.changed) {
|
|
207
|
-
nodesChanged = true
|
|
208
|
-
node.hash = hash
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
// Ensure the boilerplate for the route exists
|
|
212
|
-
const code = await ensureBoilerplate(node, routeCode)
|
|
213
|
-
|
|
214
|
-
if (code) {
|
|
215
|
-
await fs.writeFile(node.fullPath, code)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
let imports: IsolatedExport[] = []
|
|
219
|
-
|
|
220
|
-
if (!node.isRoot) {
|
|
221
|
-
// Generate the isolated files
|
|
222
|
-
const transforms = await Promise.all(
|
|
223
|
-
isolatedProperties.map(async (key): Promise<IsolatedExport> => {
|
|
224
|
-
let exported = false
|
|
225
|
-
let exports: string[] = []
|
|
226
|
-
|
|
227
|
-
const transformed = await isolateOptionToExport(
|
|
228
|
-
node,
|
|
229
|
-
routeCode,
|
|
230
|
-
{
|
|
231
|
-
isolate: key,
|
|
232
|
-
},
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
if (transformed) {
|
|
236
|
-
exports = await detectExports(transformed)
|
|
237
|
-
if (exports.includes(key)) {
|
|
238
|
-
exported = true
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return { key, exported, code: transformed }
|
|
243
|
-
}),
|
|
244
|
-
)
|
|
245
|
-
|
|
246
|
-
imports = transforms.filter(({ exported }) => exported)
|
|
247
|
-
|
|
248
|
-
node.importedFiles = await Promise.all(
|
|
249
|
-
imports.map(({ key, code }) => {
|
|
250
|
-
const importFilename = `${node.genPathNoExt}-${key}.tsx`
|
|
251
|
-
queueWriteFile(importFilename, code!)
|
|
252
|
-
return importFilename
|
|
253
|
-
}),
|
|
254
|
-
)
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const routeConfigCode = await generateRouteConfig(
|
|
258
|
-
node,
|
|
259
|
-
routeCode,
|
|
260
|
-
imports,
|
|
261
|
-
false,
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
const clientRouteConfigCode = await generateRouteConfig(
|
|
265
|
-
node,
|
|
266
|
-
routeCode,
|
|
267
|
-
imports,
|
|
268
|
-
true,
|
|
269
|
-
)
|
|
270
|
-
|
|
271
|
-
queueWriteFile(node.genPath, routeConfigCode)
|
|
272
|
-
queueWriteFile(
|
|
273
|
-
path.resolve(node.genDir, node.clientFilename),
|
|
274
|
-
clientRouteConfigCode,
|
|
275
|
-
)
|
|
276
|
-
} catch (err) {
|
|
277
|
-
node.hash = ''
|
|
278
|
-
}
|
|
176
|
+
// Ensure the boilerplate for the route exists
|
|
177
|
+
if (node.isRoot) {
|
|
178
|
+
return
|
|
279
179
|
}
|
|
280
180
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
181
|
+
// Ensure that new FileRoute(anything?) is replace with FileRoute(${node.routePath})
|
|
182
|
+
// routePath can contain $ characters, which have special meaning when used in replace
|
|
183
|
+
// so we have to escape it by turning all $ into $$. But since we do it through a replace call
|
|
184
|
+
// we have to double escape it into $$$$. For more information, see
|
|
185
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_the_replacement
|
|
186
|
+
const escapedRoutePath = node.routePath?.replaceAll('$', '$$$$') ?? ''
|
|
187
|
+
const replaced = routeCode.replace(
|
|
188
|
+
fileRouteRegex,
|
|
189
|
+
`new FileRoute('${escapedRoutePath}')`,
|
|
287
190
|
)
|
|
288
191
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
path
|
|
292
|
-
.relative(
|
|
293
|
-
config.routeGenDirectory,
|
|
294
|
-
path.resolve(node.genDir, node.clientFilename),
|
|
295
|
-
)
|
|
296
|
-
.replace(/\\/gi, '/'),
|
|
297
|
-
)}'`,
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
if (node.isRoot) {
|
|
301
|
-
return undefined
|
|
192
|
+
if (replaced !== routeCode) {
|
|
193
|
+
await fs.writeFile(node.fullPath, replaced)
|
|
302
194
|
}
|
|
303
195
|
|
|
304
|
-
const route = `${node.
|
|
196
|
+
const route = `${node.variableName}Route`
|
|
305
197
|
|
|
306
198
|
if (node.children?.length) {
|
|
307
199
|
const childConfigs = await buildRouteConfig(node.children, depth + 1)
|
|
308
|
-
return `${route}.addChildren([
|
|
309
|
-
depth * 4,
|
|
310
|
-
)}${childConfigs}\n${spaces(depth * 2)}])`
|
|
200
|
+
return `${route}.addChildren([${spaces(depth * 4)}${childConfigs}])`
|
|
311
201
|
}
|
|
312
202
|
|
|
313
203
|
return route
|
|
314
204
|
})
|
|
315
205
|
|
|
316
|
-
return (await Promise.all(children))
|
|
317
|
-
.filter(Boolean)
|
|
318
|
-
.join(`,\n${spaces(depth * 2)}`)
|
|
206
|
+
return (await Promise.all(children)).filter(Boolean).join(`,`)
|
|
319
207
|
}
|
|
320
208
|
|
|
321
|
-
const routeConfigChildrenText = await buildRouteConfig(
|
|
322
|
-
|
|
323
|
-
routeConfigImports = multiSortBy(routeConfigImports, [
|
|
324
|
-
(d) => (d.includes('__root') ? -1 : 1),
|
|
325
|
-
(d) => d.split('/').length,
|
|
326
|
-
(d) => (d.endsWith("index'") ? -1 : 1),
|
|
327
|
-
(d) => d,
|
|
328
|
-
])
|
|
329
|
-
|
|
330
|
-
routeConfigClientImports = multiSortBy(routeConfigClientImports, [
|
|
331
|
-
(d) => (d.includes('__root') ? -1 : 1),
|
|
332
|
-
(d) => d.split('/').length,
|
|
333
|
-
(d) => (d.endsWith("index.client'") ? -1 : 1),
|
|
334
|
-
(d) => d,
|
|
335
|
-
])
|
|
336
|
-
|
|
337
|
-
const routeConfig = `export const routeTree = rootRoute.addChildren([\n ${routeConfigChildrenText}\n])\nexport type __GeneratedRouteConfig = typeof routeTree`
|
|
338
|
-
const routeConfigClient = `export const routeTreeClient = rootRoute.addChildren([\n ${routeConfigChildrenText}\n]) as __GeneratedRouteConfig`
|
|
339
|
-
|
|
340
|
-
const routeConfigFileContent = [
|
|
341
|
-
routeConfigImports.join('\n'),
|
|
342
|
-
routeConfig,
|
|
343
|
-
].join('\n\n')
|
|
344
|
-
|
|
345
|
-
const routeConfigClientFileContent = [
|
|
346
|
-
`import type { __GeneratedRouteConfig } from './routeTree'`,
|
|
347
|
-
routeConfigClientImports.join('\n'),
|
|
348
|
-
routeConfigClient,
|
|
349
|
-
].join('\n\n')
|
|
350
|
-
|
|
351
|
-
if (nodesChanged) {
|
|
352
|
-
queueWriteFile(
|
|
353
|
-
path.resolve(config.routeGenDirectory, 'routeTree.ts'),
|
|
354
|
-
routeConfigFileContent,
|
|
355
|
-
)
|
|
356
|
-
queueWriteFile(
|
|
357
|
-
path.resolve(config.routeGenDirectory, 'routeTree.client.ts'),
|
|
358
|
-
routeConfigClientFileContent,
|
|
359
|
-
)
|
|
360
|
-
}
|
|
209
|
+
const routeConfigChildrenText = await buildRouteConfig(routeTree)
|
|
361
210
|
|
|
362
|
-
|
|
363
|
-
|
|
211
|
+
const routeImports = [
|
|
212
|
+
`import { Route as rootRoute } from './${sanitize(
|
|
213
|
+
path.relative(
|
|
214
|
+
path.dirname(config.generatedRouteTree),
|
|
215
|
+
path.resolve(config.routesDirectory, routePathIdPrefix + rootPathId),
|
|
216
|
+
),
|
|
217
|
+
)}'`,
|
|
218
|
+
...multiSortBy(routeNodes, [
|
|
219
|
+
(d) =>
|
|
220
|
+
d.routePath?.includes(`/${routePathIdPrefix + rootPathId}`) ? -1 : 1,
|
|
221
|
+
(d) => d.routePath?.split('/').length,
|
|
222
|
+
(d) => (d.routePath?.endsWith("index'") ? -1 : 1),
|
|
223
|
+
(d) => d,
|
|
224
|
+
]).map((node) => {
|
|
225
|
+
return `import { Route as ${node.variableName}Route } from './${sanitize(
|
|
226
|
+
removeExt(
|
|
227
|
+
path.relative(
|
|
228
|
+
path.dirname(config.generatedRouteTree),
|
|
229
|
+
path.resolve(config.routesDirectory, node.filePath),
|
|
230
|
+
),
|
|
231
|
+
),
|
|
232
|
+
)}'`
|
|
233
|
+
}),
|
|
234
|
+
].join('\n')
|
|
235
|
+
|
|
236
|
+
const routeTypes = `declare module '@tanstack/react-router' {
|
|
237
|
+
interface FileRoutesByPath {
|
|
238
|
+
${routeNodes
|
|
239
|
+
.map((routeNode) => {
|
|
240
|
+
return `'${routeNode.routePath}': {
|
|
241
|
+
parentRoute: typeof ${routeNode.parent?.variableName ?? 'root'}Route
|
|
242
|
+
}`
|
|
243
|
+
})
|
|
244
|
+
.join('\n')}
|
|
245
|
+
}
|
|
246
|
+
}`
|
|
247
|
+
|
|
248
|
+
const routeOptions = routeNodes
|
|
249
|
+
.map((routeNode) => {
|
|
250
|
+
return `Object.assign(${routeNode.variableName ?? 'root'}Route.options, {
|
|
251
|
+
${[
|
|
252
|
+
routeNode.isNonPath
|
|
253
|
+
? `id: '${routeNode.cleanedPath}'`
|
|
254
|
+
: `path: '${routeNode.cleanedPath}'`,
|
|
255
|
+
`getParentRoute: () => ${
|
|
256
|
+
routeNode.parent?.variableName ?? 'root'
|
|
257
|
+
}Route`,
|
|
258
|
+
// `\n// ${JSON.stringify(
|
|
259
|
+
// {
|
|
260
|
+
// ...routeNode,
|
|
261
|
+
// parent: undefined,
|
|
262
|
+
// children: undefined,
|
|
263
|
+
// fullPath: undefined,
|
|
264
|
+
// variableName: undefined,
|
|
265
|
+
// },
|
|
266
|
+
// null,
|
|
267
|
+
// 2,
|
|
268
|
+
// )
|
|
269
|
+
// .split('\n')
|
|
270
|
+
// .join('\n// ')}`,
|
|
271
|
+
]
|
|
272
|
+
.filter(Boolean)
|
|
273
|
+
.join(',')}
|
|
274
|
+
})`
|
|
275
|
+
})
|
|
276
|
+
.join('\n\n')
|
|
364
277
|
|
|
365
|
-
|
|
278
|
+
const routeConfig = `export const routeTree = rootRoute.addChildren([${routeConfigChildrenText}])`
|
|
366
279
|
|
|
367
|
-
await
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
current = await fs.readFile(filename, 'utf-8')
|
|
374
|
-
}
|
|
375
|
-
if (current !== content) {
|
|
376
|
-
await fs.writeFile(filename, content)
|
|
377
|
-
}
|
|
378
|
-
}),
|
|
280
|
+
const routeConfigFileContent = await prettier.format(
|
|
281
|
+
[routeImports, routeTypes, routeOptions, routeConfig].join('\n\n'),
|
|
282
|
+
{
|
|
283
|
+
semi: false,
|
|
284
|
+
parser: 'typescript',
|
|
285
|
+
},
|
|
379
286
|
)
|
|
380
287
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
288
|
+
const routeTreeContent = await fs
|
|
289
|
+
.readFile(path.resolve(config.generatedRouteTree), 'utf-8')
|
|
290
|
+
.catch((err: any) => {
|
|
291
|
+
if (err.code === 'ENOENT') {
|
|
292
|
+
return undefined
|
|
293
|
+
}
|
|
294
|
+
throw err
|
|
295
|
+
})
|
|
384
296
|
|
|
385
297
|
if (!checkLatest()) return
|
|
386
298
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
394
|
-
return true
|
|
395
|
-
})
|
|
396
|
-
|
|
397
|
-
const newNodes = nodeCache.filter((d) => d.new)
|
|
398
|
-
const updatedNodes = nodeCache.filter((d) => !d.new && d.changed)
|
|
399
|
-
|
|
400
|
-
const unusedFiles = allFiles.filter((d) => {
|
|
401
|
-
if (
|
|
402
|
-
d === path.resolve(config.routeGenDirectory, 'routeTree.ts') ||
|
|
403
|
-
d === path.resolve(config.routeGenDirectory, 'routeTree.client.ts')
|
|
404
|
-
) {
|
|
405
|
-
return false
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
let node = nodeCache.find(
|
|
409
|
-
(n) =>
|
|
410
|
-
n.genPath === d ||
|
|
411
|
-
path.resolve(n.genDir, n.clientFilename) === d ||
|
|
412
|
-
n.importedFiles?.includes(d),
|
|
299
|
+
if (routeTreeContent !== routeConfigFileContent) {
|
|
300
|
+
await fs.ensureDir(path.dirname(path.resolve(config.generatedRouteTree)))
|
|
301
|
+
if (!checkLatest()) return
|
|
302
|
+
await fs.writeFile(
|
|
303
|
+
path.resolve(config.generatedRouteTree),
|
|
304
|
+
routeConfigFileContent,
|
|
413
305
|
)
|
|
414
|
-
|
|
415
|
-
return !node
|
|
416
|
-
})
|
|
417
|
-
|
|
418
|
-
await Promise.all(
|
|
419
|
-
unusedFiles.map((d) => {
|
|
420
|
-
fs.remove(d)
|
|
421
|
-
}),
|
|
422
|
-
)
|
|
306
|
+
}
|
|
423
307
|
|
|
424
308
|
console.log(
|
|
425
|
-
`🌲 Processed ${
|
|
309
|
+
`🌲 Processed ${routeNodes.length} routes in ${Date.now() - start}ms`,
|
|
426
310
|
)
|
|
427
|
-
|
|
428
|
-
if (newNodes.length || updatedNodes.length || removedNodes.length) {
|
|
429
|
-
if (newNodes.length) {
|
|
430
|
-
console.log(`🥳 Added ${newNodes.length} new routes`)
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
if (updatedNodes.length) {
|
|
434
|
-
console.log(`✅ Updated ${updatedNodes.length} routes`)
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
if (removedNodes.length) {
|
|
438
|
-
console.log(`🗑 Removed ${removedNodes.length} unused routes`)
|
|
439
|
-
}
|
|
440
|
-
} else {
|
|
441
|
-
console.log(`🎉 No changes were found. Carry on!`)
|
|
442
|
-
}
|
|
443
311
|
}
|
|
444
312
|
|
|
445
|
-
function
|
|
446
|
-
return
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
klaw(dir)
|
|
455
|
-
.pipe(excludeDirFilter)
|
|
456
|
-
.on('data', (item) => items.push(item.path))
|
|
457
|
-
.on('error', (err) => reject(err))
|
|
458
|
-
.on('end', () => resolve(items))
|
|
459
|
-
})
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function fileToVariable(d: string) {
|
|
463
|
-
return d
|
|
464
|
-
.split('/')
|
|
465
|
-
.map((d, i) => (i > 0 ? capitalize(d) : d))
|
|
466
|
-
.join('')
|
|
467
|
-
.replace(/([^a-zA-Z0-9]|[\.])/gm, '')
|
|
313
|
+
function fileToVariable(d: string): string {
|
|
314
|
+
return (
|
|
315
|
+
removeUnderscores(d)
|
|
316
|
+
?.replace(/\$/g, '')
|
|
317
|
+
?.split(/[/-]/g)
|
|
318
|
+
.map((d, i) => (i > 0 ? capitalize(d) : d))
|
|
319
|
+
.join('')
|
|
320
|
+
.replace(/([^a-zA-Z0-9]|[\.])/gm, '') ?? ''
|
|
321
|
+
)
|
|
468
322
|
}
|
|
469
323
|
|
|
470
324
|
export function removeExt(d: string) {
|
|
@@ -511,3 +365,45 @@ function capitalize(s: string) {
|
|
|
511
365
|
if (typeof s !== 'string') return ''
|
|
512
366
|
return s.charAt(0).toUpperCase() + s.slice(1)
|
|
513
367
|
}
|
|
368
|
+
|
|
369
|
+
function sanitize(s?: string) {
|
|
370
|
+
return replaceBackslash(s?.replace(/\\index/gi, ''))
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function removeUnderscores(s?: string) {
|
|
374
|
+
return s?.replace(/(^_|_$)/, '').replace(/(\/_|_\/)/, '/')
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function replaceBackslash(s?: string) {
|
|
378
|
+
return s?.replace(/\\/gi, '/')
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function hasParentRoute(
|
|
382
|
+
routes: RouteNode[],
|
|
383
|
+
routeToCheck: string | undefined,
|
|
384
|
+
): RouteNode | null {
|
|
385
|
+
if (!routeToCheck || routeToCheck === '/') {
|
|
386
|
+
return null
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const sortedNodes = multiSortBy(routes, [
|
|
390
|
+
(d) => d.routePath!.length * -1,
|
|
391
|
+
(d) => d.variableName,
|
|
392
|
+
]).filter((d) => d.routePath !== `/${rootPathId}`)
|
|
393
|
+
|
|
394
|
+
for (const route of sortedNodes) {
|
|
395
|
+
if (route.routePath === '/') continue
|
|
396
|
+
|
|
397
|
+
if (
|
|
398
|
+
routeToCheck.startsWith(`${route.routePath}/`) &&
|
|
399
|
+
route.routePath !== routeToCheck
|
|
400
|
+
) {
|
|
401
|
+
return route
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const segments = routeToCheck.split('/')
|
|
405
|
+
segments.pop() // Remove the last segment
|
|
406
|
+
const parentRoute = segments.join('/')
|
|
407
|
+
|
|
408
|
+
return hasParentRoute(routes, parentRoute)
|
|
409
|
+
}
|