@tanstack/router-generator 1.121.0-alpha.5 → 1.121.1
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 +23 -5
- package/dist/cjs/config.cjs.map +1 -1
- package/dist/cjs/config.d.cts +7 -3
- package/dist/cjs/filesystem/physical/getRouteNodes.cjs +5 -3
- package/dist/cjs/filesystem/physical/getRouteNodes.cjs.map +1 -1
- package/dist/cjs/filesystem/virtual/getRouteNodes.cjs +1 -1
- package/dist/cjs/filesystem/virtual/getRouteNodes.cjs.map +1 -1
- package/dist/cjs/generator.cjs +836 -668
- package/dist/cjs/generator.cjs.map +1 -1
- package/dist/cjs/generator.d.cts +71 -1
- package/dist/cjs/index.cjs +5 -2
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +7 -3
- package/dist/cjs/logger.cjs +37 -0
- package/dist/cjs/logger.cjs.map +1 -0
- package/dist/cjs/logger.d.cts +10 -0
- package/dist/cjs/plugin/default-generator-plugin.cjs +88 -0
- package/dist/cjs/plugin/default-generator-plugin.cjs.map +1 -0
- package/dist/cjs/plugin/default-generator-plugin.d.cts +2 -0
- package/dist/cjs/plugin/types.d.cts +46 -0
- package/dist/cjs/template.cjs +10 -10
- package/dist/cjs/template.cjs.map +1 -1
- package/dist/cjs/template.d.cts +2 -2
- package/dist/cjs/transform/default-transform-plugin.cjs +95 -0
- package/dist/cjs/transform/default-transform-plugin.cjs.map +1 -0
- package/dist/cjs/transform/default-transform-plugin.d.cts +2 -0
- package/dist/cjs/transform/transform.cjs +351 -0
- package/dist/cjs/transform/transform.cjs.map +1 -0
- package/dist/cjs/transform/transform.d.cts +4 -0
- package/dist/cjs/transform/types.d.cts +43 -0
- package/dist/cjs/transform/utils.cjs +36 -0
- package/dist/cjs/transform/utils.cjs.map +1 -0
- package/dist/cjs/transform/utils.d.cts +2 -0
- package/dist/cjs/types.d.cts +22 -0
- package/dist/cjs/utils.cjs +262 -40
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +82 -9
- package/dist/esm/config.d.ts +7 -3
- package/dist/esm/config.js +21 -3
- package/dist/esm/config.js.map +1 -1
- package/dist/esm/filesystem/physical/getRouteNodes.js +3 -1
- package/dist/esm/filesystem/physical/getRouteNodes.js.map +1 -1
- package/dist/esm/filesystem/virtual/getRouteNodes.js +1 -1
- package/dist/esm/filesystem/virtual/getRouteNodes.js.map +1 -1
- package/dist/esm/generator.d.ts +71 -1
- package/dist/esm/generator.js +827 -658
- package/dist/esm/generator.js.map +1 -1
- package/dist/esm/index.d.ts +7 -3
- package/dist/esm/index.js +7 -4
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/logger.d.ts +10 -0
- package/dist/esm/logger.js +37 -0
- package/dist/esm/logger.js.map +1 -0
- package/dist/esm/plugin/default-generator-plugin.d.ts +2 -0
- package/dist/esm/plugin/default-generator-plugin.js +88 -0
- package/dist/esm/plugin/default-generator-plugin.js.map +1 -0
- package/dist/esm/plugin/types.d.ts +46 -0
- package/dist/esm/template.d.ts +2 -2
- package/dist/esm/template.js +10 -10
- package/dist/esm/template.js.map +1 -1
- package/dist/esm/transform/default-transform-plugin.d.ts +2 -0
- package/dist/esm/transform/default-transform-plugin.js +95 -0
- package/dist/esm/transform/default-transform-plugin.js.map +1 -0
- package/dist/esm/transform/transform.d.ts +4 -0
- package/dist/esm/transform/transform.js +351 -0
- package/dist/esm/transform/transform.js.map +1 -0
- package/dist/esm/transform/types.d.ts +43 -0
- package/dist/esm/transform/utils.d.ts +2 -0
- package/dist/esm/transform/utils.js +36 -0
- package/dist/esm/transform/utils.js.map +1 -0
- package/dist/esm/types.d.ts +22 -0
- package/dist/esm/utils.d.ts +82 -9
- package/dist/esm/utils.js +262 -40
- package/dist/esm/utils.js.map +1 -1
- package/package.json +9 -10
- package/src/config.ts +23 -2
- package/src/filesystem/physical/getRouteNodes.ts +2 -1
- package/src/filesystem/virtual/getRouteNodes.ts +1 -1
- package/src/generator.ts +1123 -945
- package/src/index.ts +25 -3
- package/src/logger.ts +43 -0
- package/src/plugin/default-generator-plugin.ts +96 -0
- package/src/plugin/types.ts +51 -0
- package/src/template.ts +33 -12
- package/src/transform/default-transform-plugin.ts +103 -0
- package/src/transform/transform.ts +430 -0
- package/src/transform/types.ts +50 -0
- package/src/transform/utils.ts +42 -0
- package/src/types.ts +25 -0
- package/src/utils.ts +385 -36
package/src/generator.ts
CHANGED
|
@@ -1,370 +1,1192 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
-
import * as fs from 'node:fs'
|
|
3
2
|
import * as fsp from 'node:fs/promises'
|
|
3
|
+
import { mkdtempSync } from 'node:fs'
|
|
4
|
+
import crypto from 'node:crypto'
|
|
5
|
+
import { deepEqual, rootRouteId } from '@tanstack/router-core'
|
|
6
|
+
import { logging } from './logger'
|
|
7
|
+
import { getRouteNodes as physicalGetRouteNodes } from './filesystem/physical/getRouteNodes'
|
|
8
|
+
import { getRouteNodes as virtualGetRouteNodes } from './filesystem/virtual/getRouteNodes'
|
|
9
|
+
import { rootPathId } from './filesystem/physical/rootPathId'
|
|
4
10
|
import {
|
|
11
|
+
buildFileRoutesByPathInterface,
|
|
12
|
+
buildImportString,
|
|
13
|
+
buildRouteTreeConfig,
|
|
14
|
+
checkFileExists,
|
|
15
|
+
createRouteNodesByFullPath,
|
|
16
|
+
createRouteNodesById,
|
|
17
|
+
createRouteNodesByTo,
|
|
18
|
+
determineNodePath,
|
|
19
|
+
findParent,
|
|
5
20
|
format,
|
|
6
|
-
|
|
21
|
+
getResolvedRouteNodeVariableName,
|
|
22
|
+
hasParentRoute,
|
|
23
|
+
isRouteNodeValidForAugmentation,
|
|
24
|
+
lowerCaseFirstChar,
|
|
25
|
+
mergeImportDeclarations,
|
|
7
26
|
multiSortBy,
|
|
8
27
|
removeExt,
|
|
28
|
+
removeGroups,
|
|
29
|
+
removeLastSegmentFromPath,
|
|
30
|
+
removeLayoutSegments,
|
|
9
31
|
removeUnderscores,
|
|
10
32
|
replaceBackslash,
|
|
11
33
|
resetRegex,
|
|
12
34
|
routePathToVariable,
|
|
13
35
|
trimPathLeft,
|
|
14
|
-
writeIfDifferent,
|
|
15
36
|
} 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
37
|
import { fillTemplate, getTargetTemplate } from './template'
|
|
20
|
-
import
|
|
38
|
+
import { transform } from './transform/transform'
|
|
39
|
+
import { defaultGeneratorPlugin } from './plugin/default-generator-plugin'
|
|
40
|
+
import type {
|
|
41
|
+
GeneratorPlugin,
|
|
42
|
+
GeneratorPluginWithTransform,
|
|
43
|
+
} from './plugin/types'
|
|
44
|
+
import type { TargetTemplate } from './template'
|
|
45
|
+
import type {
|
|
46
|
+
FsRouteType,
|
|
47
|
+
GetRouteNodesResult,
|
|
48
|
+
HandleNodeAccumulator,
|
|
49
|
+
ImportDeclaration,
|
|
50
|
+
RouteNode,
|
|
51
|
+
} from './types'
|
|
21
52
|
import type { Config } from './config'
|
|
53
|
+
import type { Logger } from './logger'
|
|
54
|
+
import type { TransformPlugin } from './transform/types'
|
|
55
|
+
|
|
56
|
+
interface fs {
|
|
57
|
+
stat: (filePath: string) => Promise<{ mtimeMs: bigint }>
|
|
58
|
+
mkdtempSync: (prefix: string) => string
|
|
59
|
+
rename: (oldPath: string, newPath: string) => Promise<void>
|
|
60
|
+
writeFile: (filePath: string, content: string) => Promise<void>
|
|
61
|
+
readFile: (
|
|
62
|
+
filePath: string,
|
|
63
|
+
) => Promise<
|
|
64
|
+
{ stat: { mtimeMs: bigint }; fileContent: string } | 'file-not-existing'
|
|
65
|
+
>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const DefaultFileSystem: fs = {
|
|
69
|
+
stat: (filePath) => fsp.stat(filePath, { bigint: true }),
|
|
70
|
+
mkdtempSync: mkdtempSync,
|
|
71
|
+
rename: (oldPath, newPath) => fsp.rename(oldPath, newPath),
|
|
72
|
+
writeFile: (filePath, content) => fsp.writeFile(filePath, content),
|
|
73
|
+
readFile: async (filePath: string) => {
|
|
74
|
+
try {
|
|
75
|
+
const fileHandle = await fsp.open(filePath, 'r')
|
|
76
|
+
const stat = await fileHandle.stat({ bigint: true })
|
|
77
|
+
const fileContent = (await fileHandle.readFile()).toString()
|
|
78
|
+
await fileHandle.close()
|
|
79
|
+
return { stat, fileContent }
|
|
80
|
+
} catch (e: any) {
|
|
81
|
+
if ('code' in e) {
|
|
82
|
+
if (e.code === 'ENOENT') {
|
|
83
|
+
return 'file-not-existing'
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
throw e
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface Rerun {
|
|
92
|
+
rerun: true
|
|
93
|
+
msg?: string
|
|
94
|
+
event: GeneratorEvent
|
|
95
|
+
}
|
|
96
|
+
function rerun(opts: { msg?: string; event?: GeneratorEvent }): Rerun {
|
|
97
|
+
const { event, ...rest } = opts
|
|
98
|
+
return { rerun: true, event: event ?? { type: 'rerun' }, ...rest }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isRerun(result: unknown): result is Rerun {
|
|
102
|
+
return (
|
|
103
|
+
typeof result === 'object' &&
|
|
104
|
+
result !== null &&
|
|
105
|
+
'rerun' in result &&
|
|
106
|
+
result.rerun === true
|
|
107
|
+
)
|
|
108
|
+
}
|
|
22
109
|
|
|
23
|
-
|
|
24
|
-
|
|
110
|
+
export type FileEventType = 'create' | 'update' | 'delete'
|
|
111
|
+
export type FileEvent = {
|
|
112
|
+
type: FileEventType
|
|
113
|
+
path: string
|
|
114
|
+
}
|
|
115
|
+
export type GeneratorEvent = FileEvent | { type: 'rerun' }
|
|
25
116
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
117
|
+
type FileCacheChange<TCacheEntry extends GeneratorCacheEntry> =
|
|
118
|
+
| {
|
|
119
|
+
result: false
|
|
120
|
+
cacheEntry: TCacheEntry
|
|
121
|
+
}
|
|
122
|
+
| { result: true; mtimeMs: bigint; cacheEntry: TCacheEntry }
|
|
123
|
+
| {
|
|
124
|
+
result: 'file-not-in-cache'
|
|
125
|
+
}
|
|
126
|
+
| {
|
|
127
|
+
result: 'cannot-stat-file'
|
|
128
|
+
}
|
|
29
129
|
|
|
30
|
-
|
|
31
|
-
|
|
130
|
+
interface GeneratorCacheEntry {
|
|
131
|
+
mtimeMs: bigint
|
|
132
|
+
fileContent: string
|
|
133
|
+
}
|
|
32
134
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
errorComponent?: RouteNode
|
|
36
|
-
pendingComponent?: RouteNode
|
|
37
|
-
loader?: RouteNode
|
|
38
|
-
lazy?: RouteNode
|
|
135
|
+
interface RouteNodeCacheEntry extends GeneratorCacheEntry {
|
|
136
|
+
exports: Array<string>
|
|
39
137
|
}
|
|
40
138
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
139
|
+
type GeneratorRouteNodeCache = Map</** filePath **/ string, RouteNodeCacheEntry>
|
|
140
|
+
|
|
141
|
+
export class Generator {
|
|
142
|
+
/**
|
|
143
|
+
* why do we have two caches for the route files?
|
|
144
|
+
* During processing, we READ from the cache and WRITE to the shadow cache.
|
|
145
|
+
*
|
|
146
|
+
* After a route file is processed, we write to the shadow cache.
|
|
147
|
+
* If during processing we bail out and re-run, we don't lose this modification
|
|
148
|
+
* but still can track whether the file contributed changes and thus the route tree file needs to be regenerated.
|
|
149
|
+
* After all files are processed, we swap the shadow cache with the main cache and initialize a new shadow cache.
|
|
150
|
+
* That way we also ensure deleted/renamed files don't stay in the cache forever.
|
|
151
|
+
*/
|
|
152
|
+
private routeNodeCache: GeneratorRouteNodeCache = new Map()
|
|
153
|
+
private routeNodeShadowCache: GeneratorRouteNodeCache = new Map()
|
|
154
|
+
|
|
155
|
+
private routeTreeFileCache: GeneratorCacheEntry | undefined
|
|
156
|
+
|
|
157
|
+
public config: Config
|
|
158
|
+
public targetTemplate: TargetTemplate
|
|
159
|
+
|
|
160
|
+
private root: string
|
|
161
|
+
private routesDirectoryPath: string
|
|
162
|
+
private tmpDir: string
|
|
163
|
+
private fs: fs
|
|
164
|
+
private logger: Logger
|
|
165
|
+
private generatedRouteTreePath: string
|
|
166
|
+
private runPromise: Promise<void> | undefined
|
|
167
|
+
private fileEventQueue: Array<GeneratorEvent> = []
|
|
168
|
+
private plugins: Array<GeneratorPlugin> = [defaultGeneratorPlugin()]
|
|
169
|
+
private pluginsWithTransform: Array<GeneratorPluginWithTransform> = []
|
|
170
|
+
// this is just a cache for the transform plugins since we need them for each route file that is to be processed
|
|
171
|
+
private transformPlugins: Array<TransformPlugin> = []
|
|
172
|
+
private routeGroupPatternRegex = /\(.+\)/g
|
|
173
|
+
|
|
174
|
+
constructor(opts: { config: Config; root: string; fs?: fs }) {
|
|
175
|
+
this.config = opts.config
|
|
176
|
+
this.logger = logging({ disabled: this.config.disableLogging })
|
|
177
|
+
this.root = opts.root
|
|
178
|
+
this.fs = opts.fs || DefaultFileSystem
|
|
179
|
+
this.tmpDir = this.fs.mkdtempSync(
|
|
180
|
+
path.join(this.config.tmpDir, 'router-generator-'),
|
|
181
|
+
)
|
|
182
|
+
this.generatedRouteTreePath = path.resolve(this.config.generatedRouteTree)
|
|
183
|
+
this.targetTemplate = getTargetTemplate(this.config)
|
|
184
|
+
|
|
185
|
+
this.routesDirectoryPath = this.getRoutesDirectoryPath()
|
|
186
|
+
this.plugins.push(...(opts.config.plugins || []))
|
|
187
|
+
this.plugins.forEach((plugin) => {
|
|
188
|
+
if ('transformPlugin' in plugin) {
|
|
189
|
+
if (this.pluginsWithTransform.find((p) => p.name === plugin.name)) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
`Plugin with name "${plugin.name}" is already registered for export ${plugin.transformPlugin.exportName}!`,
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
this.pluginsWithTransform.push(plugin)
|
|
195
|
+
this.transformPlugins.push(plugin.transformPlugin)
|
|
196
|
+
}
|
|
197
|
+
})
|
|
53
198
|
}
|
|
54
199
|
|
|
55
|
-
|
|
56
|
-
|
|
200
|
+
private getRoutesDirectoryPath() {
|
|
201
|
+
return path.isAbsolute(this.config.routesDirectory)
|
|
202
|
+
? this.config.routesDirectory
|
|
203
|
+
: path.resolve(this.root, this.config.routesDirectory)
|
|
204
|
+
}
|
|
57
205
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
206
|
+
public async run(event?: GeneratorEvent): Promise<void> {
|
|
207
|
+
// we are only interested in FileEvents that affect either the generated route tree or files inside the routes folder
|
|
208
|
+
if (event && event.type !== 'rerun') {
|
|
209
|
+
if (
|
|
210
|
+
!(
|
|
211
|
+
event.path === this.generatedRouteTreePath ||
|
|
212
|
+
event.path.startsWith(this.routesDirectoryPath)
|
|
213
|
+
)
|
|
214
|
+
) {
|
|
215
|
+
return
|
|
216
|
+
}
|
|
62
217
|
}
|
|
218
|
+
this.fileEventQueue.push(event ?? { type: 'rerun' })
|
|
219
|
+
// only allow a single run at a time
|
|
220
|
+
if (this.runPromise) {
|
|
221
|
+
return this.runPromise
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this.runPromise = (async () => {
|
|
225
|
+
do {
|
|
226
|
+
// synchronously copy and clear the queue since we are going to iterate asynchronously over it
|
|
227
|
+
// and while we do so, a new event could be put into the queue
|
|
228
|
+
const tempQueue = this.fileEventQueue
|
|
229
|
+
this.fileEventQueue = []
|
|
230
|
+
// if we only have 'update' events in the queue
|
|
231
|
+
// and we already have the affected files' latest state in our cache, we can exit early
|
|
232
|
+
const remainingEvents = (
|
|
233
|
+
await Promise.all(
|
|
234
|
+
tempQueue.map(async (e) => {
|
|
235
|
+
if (e.type === 'update') {
|
|
236
|
+
let cacheEntry
|
|
237
|
+
if (e.path === this.generatedRouteTreePath) {
|
|
238
|
+
cacheEntry = this.routeTreeFileCache
|
|
239
|
+
} else {
|
|
240
|
+
// we only check the routeNodeCache here
|
|
241
|
+
// if the file's state is only up-to-date in the shadow cache we need to re-run
|
|
242
|
+
cacheEntry = this.routeNodeCache.get(e.path)
|
|
243
|
+
}
|
|
244
|
+
const change = await this.didFileChangeComparedToCache(
|
|
245
|
+
{ path: e.path },
|
|
246
|
+
cacheEntry,
|
|
247
|
+
)
|
|
248
|
+
if (change.result === false) {
|
|
249
|
+
return null
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return e
|
|
253
|
+
}),
|
|
254
|
+
)
|
|
255
|
+
).filter((e) => e !== null)
|
|
256
|
+
|
|
257
|
+
if (remainingEvents.length === 0) {
|
|
258
|
+
break
|
|
259
|
+
}
|
|
63
260
|
|
|
64
|
-
|
|
261
|
+
try {
|
|
262
|
+
const start = performance.now()
|
|
263
|
+
await this.generatorInternal()
|
|
264
|
+
const end = performance.now()
|
|
265
|
+
this.logger.info(
|
|
266
|
+
`Generated route tree in ${Math.round(end - start)}ms`,
|
|
267
|
+
)
|
|
268
|
+
} catch (err) {
|
|
269
|
+
const errArray = !Array.isArray(err) ? [err] : err
|
|
270
|
+
|
|
271
|
+
const recoverableErrors = errArray.filter((e) => isRerun(e))
|
|
272
|
+
if (recoverableErrors.length === errArray.length) {
|
|
273
|
+
this.fileEventQueue.push(...recoverableErrors.map((e) => e.event))
|
|
274
|
+
recoverableErrors.forEach((e) => {
|
|
275
|
+
if (e.msg) {
|
|
276
|
+
this.logger.info(e.msg)
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
} else {
|
|
280
|
+
const unrecoverableErrors = errArray.filter((e) => !isRerun(e))
|
|
281
|
+
this.runPromise = undefined
|
|
282
|
+
throw new Error(
|
|
283
|
+
unrecoverableErrors.map((e) => (e as Error).message).join(),
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} while (this.fileEventQueue.length)
|
|
288
|
+
this.runPromise = undefined
|
|
289
|
+
})()
|
|
290
|
+
return this.runPromise
|
|
65
291
|
}
|
|
66
292
|
|
|
67
|
-
|
|
293
|
+
private async generatorInternal() {
|
|
294
|
+
let writeRouteTreeFile: boolean | 'force' = false
|
|
68
295
|
|
|
69
|
-
|
|
296
|
+
let getRouteNodesResult: GetRouteNodesResult
|
|
70
297
|
|
|
71
|
-
|
|
298
|
+
if (this.config.virtualRouteConfig) {
|
|
299
|
+
getRouteNodesResult = await virtualGetRouteNodes(this.config, this.root)
|
|
300
|
+
} else {
|
|
301
|
+
getRouteNodesResult = await physicalGetRouteNodes(this.config, this.root)
|
|
302
|
+
}
|
|
72
303
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
304
|
+
const { rootRouteNode, routeNodes: beforeRouteNodes } = getRouteNodesResult
|
|
305
|
+
if (rootRouteNode === undefined) {
|
|
306
|
+
let errorMessage = `rootRouteNode must not be undefined. Make sure you've added your root route into the route-tree.`
|
|
307
|
+
if (!this.config.virtualRouteConfig) {
|
|
308
|
+
errorMessage += `\nMake sure that you add a "${rootPathId}.${this.config.disableTypes ? 'js' : 'tsx'}" file to your routes directory.\nAdd the file in: "${this.config.routesDirectory}/${rootPathId}.${this.config.disableTypes ? 'js' : 'tsx'}"`
|
|
309
|
+
}
|
|
310
|
+
throw new Error(errorMessage)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
writeRouteTreeFile = await this.handleRootNode(rootRouteNode)
|
|
314
|
+
|
|
315
|
+
const preRouteNodes = multiSortBy(beforeRouteNodes, [
|
|
316
|
+
(d) => (d.routePath === '/' ? -1 : 1),
|
|
317
|
+
(d) => d.routePath?.split('/').length,
|
|
318
|
+
(d) =>
|
|
319
|
+
d.filePath.match(new RegExp(`[./]${this.config.indexToken}[.]`))
|
|
320
|
+
? 1
|
|
321
|
+
: -1,
|
|
322
|
+
(d) =>
|
|
323
|
+
d.filePath.match(
|
|
324
|
+
/[./](component|errorComponent|pendingComponent|loader|lazy)[.]/,
|
|
325
|
+
)
|
|
326
|
+
? 1
|
|
327
|
+
: -1,
|
|
328
|
+
(d) =>
|
|
329
|
+
d.filePath.match(new RegExp(`[./]${this.config.routeToken}[.]`))
|
|
330
|
+
? -1
|
|
331
|
+
: 1,
|
|
332
|
+
(d) => (d.routePath?.endsWith('/') ? -1 : 1),
|
|
333
|
+
(d) => d.routePath,
|
|
334
|
+
]).filter((d) => ![`/${rootPathId}`].includes(d.routePath || ''))
|
|
335
|
+
|
|
336
|
+
const routeFileAllResult = await Promise.allSettled(
|
|
337
|
+
preRouteNodes
|
|
338
|
+
// only process routes that are backed by an actual file
|
|
339
|
+
.filter((n) => !n.isVirtualParentRoute && !n.isVirtual)
|
|
340
|
+
.map((n) => this.processRouteNodeFile(n)),
|
|
341
|
+
)
|
|
78
342
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
|
|
343
|
+
const rejections = routeFileAllResult.filter(
|
|
344
|
+
(result) => result.status === 'rejected',
|
|
345
|
+
)
|
|
346
|
+
if (rejections.length > 0) {
|
|
347
|
+
throw rejections.map((e) => e.reason)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const routeFileResult = routeFileAllResult.flatMap((result) => {
|
|
351
|
+
if (result.status === 'fulfilled' && result.value !== null) {
|
|
352
|
+
return result.value
|
|
353
|
+
}
|
|
354
|
+
return []
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
routeFileResult.forEach((result) => {
|
|
358
|
+
if (!result.node.exports?.length) {
|
|
359
|
+
this.logger.warn(
|
|
360
|
+
`Route file "${result.cacheEntry.fileContent}" does not export any route piece. This is likely a mistake.`,
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
})
|
|
364
|
+
if (routeFileResult.find((r) => r.shouldWriteTree)) {
|
|
365
|
+
writeRouteTreeFile = true
|
|
84
366
|
}
|
|
85
|
-
throw new Error(errorMessage)
|
|
86
|
-
}
|
|
87
367
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
368
|
+
// this is the first time the generator runs, so read in the route tree file if it exists yet
|
|
369
|
+
if (!this.routeTreeFileCache) {
|
|
370
|
+
const routeTreeFile = await this.fs.readFile(this.generatedRouteTreePath)
|
|
371
|
+
if (routeTreeFile !== 'file-not-existing') {
|
|
372
|
+
this.routeTreeFileCache = {
|
|
373
|
+
fileContent: routeTreeFile.fileContent,
|
|
374
|
+
mtimeMs: routeTreeFile.stat.mtimeMs,
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
writeRouteTreeFile = true
|
|
378
|
+
} else {
|
|
379
|
+
const routeTreeFileChange = await this.didFileChangeComparedToCache(
|
|
380
|
+
{ path: this.generatedRouteTreePath },
|
|
381
|
+
this.routeTreeFileCache,
|
|
96
382
|
)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// the handleRootNode function is not being collapsed into the handleNode function
|
|
113
|
-
// because it requires only a subset of the logic that the handleNode function requires
|
|
114
|
-
// and it's easier to read and maintain this way
|
|
115
|
-
const handleRootNode = async (node?: RouteNode) => {
|
|
116
|
-
if (!node) {
|
|
117
|
-
// currently this is not being handled, but it could be in the future
|
|
118
|
-
// for example to handle a virtual root route
|
|
119
|
-
return
|
|
383
|
+
if (routeTreeFileChange.result !== false) {
|
|
384
|
+
writeRouteTreeFile = 'force'
|
|
385
|
+
if (routeTreeFileChange.result === true) {
|
|
386
|
+
const routeTreeFile = await this.fs.readFile(
|
|
387
|
+
this.generatedRouteTreePath,
|
|
388
|
+
)
|
|
389
|
+
if (routeTreeFile !== 'file-not-existing') {
|
|
390
|
+
this.routeTreeFileCache = {
|
|
391
|
+
fileContent: routeTreeFile.fileContent,
|
|
392
|
+
mtimeMs: routeTreeFile.stat.mtimeMs,
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
120
397
|
}
|
|
121
398
|
|
|
122
|
-
|
|
123
|
-
|
|
399
|
+
if (!writeRouteTreeFile) {
|
|
400
|
+
// only needs to be done if no other changes have been detected yet
|
|
401
|
+
// compare shadowCache and cache to identify deleted routes
|
|
402
|
+
for (const fullPath of this.routeNodeCache.keys()) {
|
|
403
|
+
if (!this.routeNodeShadowCache.has(fullPath)) {
|
|
404
|
+
writeRouteTreeFile = true
|
|
405
|
+
break
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
124
409
|
|
|
125
|
-
if (!
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
tsrImports: _rootTemplate.imports.tsrImports(),
|
|
129
|
-
tsrPath: rootPathId,
|
|
130
|
-
tsrExportStart: _rootTemplate.imports.tsrExportStart(),
|
|
131
|
-
tsrExportEnd: _rootTemplate.imports.tsrExportEnd(),
|
|
132
|
-
})
|
|
410
|
+
if (!writeRouteTreeFile) {
|
|
411
|
+
return
|
|
412
|
+
}
|
|
133
413
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
414
|
+
let routeTreeContent = this.buildRouteTreeFileContent(
|
|
415
|
+
rootRouteNode,
|
|
416
|
+
preRouteNodes,
|
|
417
|
+
routeFileResult,
|
|
418
|
+
)
|
|
419
|
+
routeTreeContent = this.config.enableRouteTreeFormatting
|
|
420
|
+
? await format(routeTreeContent, this.config)
|
|
421
|
+
: routeTreeContent
|
|
422
|
+
|
|
423
|
+
let newMtimeMs: bigint | undefined
|
|
424
|
+
if (this.routeTreeFileCache) {
|
|
425
|
+
if (
|
|
426
|
+
writeRouteTreeFile !== 'force' &&
|
|
427
|
+
this.routeTreeFileCache.fileContent === routeTreeContent
|
|
428
|
+
) {
|
|
429
|
+
// existing route tree file is already up-to-date, don't write it
|
|
430
|
+
// we should only get here in the initial run when the route cache is not filled yet
|
|
431
|
+
} else {
|
|
432
|
+
const newRouteTreeFileStat = await this.safeFileWrite({
|
|
433
|
+
filePath: this.generatedRouteTreePath,
|
|
434
|
+
newContent: routeTreeContent,
|
|
435
|
+
strategy: {
|
|
436
|
+
type: 'mtime',
|
|
437
|
+
expectedMtimeMs: this.routeTreeFileCache.mtimeMs,
|
|
141
438
|
},
|
|
439
|
+
})
|
|
440
|
+
newMtimeMs = newRouteTreeFileStat.mtimeMs
|
|
441
|
+
}
|
|
442
|
+
} else {
|
|
443
|
+
const newRouteTreeFileStat = await this.safeFileWrite({
|
|
444
|
+
filePath: this.generatedRouteTreePath,
|
|
445
|
+
newContent: routeTreeContent,
|
|
446
|
+
strategy: {
|
|
447
|
+
type: 'new-file',
|
|
142
448
|
},
|
|
143
|
-
)
|
|
449
|
+
})
|
|
450
|
+
newMtimeMs = newRouteTreeFileStat.mtimeMs
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (newMtimeMs !== undefined) {
|
|
454
|
+
this.routeTreeFileCache = {
|
|
455
|
+
fileContent: routeTreeContent,
|
|
456
|
+
mtimeMs: newMtimeMs,
|
|
457
|
+
}
|
|
144
458
|
}
|
|
459
|
+
|
|
460
|
+
// now that we have finished this run, we can finally swap the caches
|
|
461
|
+
this.routeNodeCache = this.routeNodeShadowCache
|
|
462
|
+
this.routeNodeShadowCache = new Map()
|
|
145
463
|
}
|
|
146
464
|
|
|
147
|
-
|
|
465
|
+
private buildRouteTreeFileContent(
|
|
466
|
+
rootRouteNode: RouteNode,
|
|
467
|
+
preRouteNodes: Array<RouteNode>,
|
|
468
|
+
routeFileResult: Array<{
|
|
469
|
+
cacheEntry: RouteNodeCacheEntry
|
|
470
|
+
node: RouteNode
|
|
471
|
+
}>,
|
|
472
|
+
) {
|
|
473
|
+
const getImportForRouteNode = (node: RouteNode, exportName: string) => {
|
|
474
|
+
if (node.exports?.includes(exportName)) {
|
|
475
|
+
return {
|
|
476
|
+
source: `./${this.getImportPath(node)}`,
|
|
477
|
+
specifiers: [
|
|
478
|
+
{
|
|
479
|
+
imported: exportName,
|
|
480
|
+
local: `${node.variableName}${exportName}Import`,
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
} satisfies ImportDeclaration
|
|
484
|
+
}
|
|
485
|
+
return undefined
|
|
486
|
+
}
|
|
148
487
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
488
|
+
const buildRouteTreeForExport = (plugin: GeneratorPluginWithTransform) => {
|
|
489
|
+
const exportName = plugin.transformPlugin.exportName
|
|
490
|
+
const acc: HandleNodeAccumulator = {
|
|
491
|
+
routeTree: [],
|
|
492
|
+
routeNodes: [],
|
|
493
|
+
routePiecesByPath: {},
|
|
494
|
+
}
|
|
495
|
+
for (const node of preRouteNodes) {
|
|
496
|
+
if (node.exports?.includes(plugin.transformPlugin.exportName)) {
|
|
497
|
+
this.handleNode(node, acc)
|
|
498
|
+
}
|
|
499
|
+
}
|
|
154
500
|
|
|
155
|
-
|
|
501
|
+
const sortedRouteNodes = multiSortBy(acc.routeNodes, [
|
|
502
|
+
(d) => (d.routePath?.includes(`/${rootPathId}`) ? -1 : 1),
|
|
503
|
+
(d) => d.routePath?.split('/').length,
|
|
504
|
+
(d) => (d.routePath?.endsWith(this.config.indexToken) ? -1 : 1),
|
|
505
|
+
(d) => d,
|
|
506
|
+
])
|
|
507
|
+
|
|
508
|
+
const pluginConfig = plugin.config({
|
|
509
|
+
generator: this,
|
|
510
|
+
rootRouteNode,
|
|
511
|
+
sortedRouteNodes,
|
|
512
|
+
})
|
|
156
513
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
514
|
+
const routeImports = sortedRouteNodes
|
|
515
|
+
.filter((d) => !d.isVirtual)
|
|
516
|
+
.flatMap((node) => getImportForRouteNode(node, exportName) ?? [])
|
|
517
|
+
|
|
518
|
+
const hasMatchingRouteFiles =
|
|
519
|
+
acc.routeNodes.length > 0 || rootRouteNode.exports?.includes(exportName)
|
|
520
|
+
|
|
521
|
+
const virtualRouteNodes = sortedRouteNodes
|
|
522
|
+
.filter((d) => d.isVirtual)
|
|
523
|
+
.map((node) => {
|
|
524
|
+
return `const ${
|
|
525
|
+
node.variableName
|
|
526
|
+
}${exportName}Import = ${plugin.createVirtualRouteCode({ node })}`
|
|
527
|
+
})
|
|
528
|
+
if (
|
|
529
|
+
!rootRouteNode.exports?.includes(exportName) &&
|
|
530
|
+
pluginConfig.virtualRootRoute
|
|
531
|
+
) {
|
|
532
|
+
virtualRouteNodes.unshift(
|
|
533
|
+
`const ${rootRouteNode.variableName}${exportName}Import = ${plugin.createRootRouteCode()}`,
|
|
534
|
+
)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const imports = plugin.imports({
|
|
538
|
+
sortedRouteNodes,
|
|
539
|
+
acc,
|
|
540
|
+
generator: this,
|
|
541
|
+
rootRouteNode,
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
const routeTreeConfig = buildRouteTreeConfig(
|
|
545
|
+
acc.routeTree,
|
|
546
|
+
exportName,
|
|
547
|
+
this.config.disableTypes,
|
|
164
548
|
)
|
|
165
|
-
|
|
166
|
-
|
|
549
|
+
|
|
550
|
+
const createUpdateRoutes = sortedRouteNodes.map((node) => {
|
|
551
|
+
const loaderNode = acc.routePiecesByPath[node.routePath!]?.loader
|
|
552
|
+
const componentNode = acc.routePiecesByPath[node.routePath!]?.component
|
|
553
|
+
const errorComponentNode =
|
|
554
|
+
acc.routePiecesByPath[node.routePath!]?.errorComponent
|
|
555
|
+
const pendingComponentNode =
|
|
556
|
+
acc.routePiecesByPath[node.routePath!]?.pendingComponent
|
|
557
|
+
const lazyComponentNode = acc.routePiecesByPath[node.routePath!]?.lazy
|
|
558
|
+
|
|
559
|
+
return [
|
|
560
|
+
[
|
|
561
|
+
`const ${node.variableName}${exportName} = ${node.variableName}${exportName}Import.update({
|
|
562
|
+
${[
|
|
563
|
+
`id: '${node.path}'`,
|
|
564
|
+
!node.isNonPath ? `path: '${node.cleanedPath}'` : undefined,
|
|
565
|
+
`getParentRoute: () => ${findParent(node, exportName)}`,
|
|
566
|
+
]
|
|
567
|
+
.filter(Boolean)
|
|
568
|
+
.join(',')}
|
|
569
|
+
}${this.config.disableTypes ? '' : 'as any'})`,
|
|
570
|
+
loaderNode
|
|
571
|
+
? `.updateLoader({ loader: lazyFn(() => import('./${replaceBackslash(
|
|
572
|
+
removeExt(
|
|
573
|
+
path.relative(
|
|
574
|
+
path.dirname(this.config.generatedRouteTree),
|
|
575
|
+
path.resolve(
|
|
576
|
+
this.config.routesDirectory,
|
|
577
|
+
loaderNode.filePath,
|
|
578
|
+
),
|
|
579
|
+
),
|
|
580
|
+
this.config.addExtensions,
|
|
581
|
+
),
|
|
582
|
+
)}'), 'loader') })`
|
|
583
|
+
: '',
|
|
584
|
+
componentNode || errorComponentNode || pendingComponentNode
|
|
585
|
+
? `.update({
|
|
586
|
+
${(
|
|
587
|
+
[
|
|
588
|
+
['component', componentNode],
|
|
589
|
+
['errorComponent', errorComponentNode],
|
|
590
|
+
['pendingComponent', pendingComponentNode],
|
|
591
|
+
] as const
|
|
592
|
+
)
|
|
593
|
+
.filter((d) => d[1])
|
|
594
|
+
.map((d) => {
|
|
595
|
+
return `${
|
|
596
|
+
d[0]
|
|
597
|
+
}: lazyRouteComponent(() => import('./${replaceBackslash(
|
|
598
|
+
removeExt(
|
|
599
|
+
path.relative(
|
|
600
|
+
path.dirname(this.config.generatedRouteTree),
|
|
601
|
+
path.resolve(
|
|
602
|
+
this.config.routesDirectory,
|
|
603
|
+
d[1]!.filePath,
|
|
604
|
+
),
|
|
605
|
+
),
|
|
606
|
+
this.config.addExtensions,
|
|
607
|
+
),
|
|
608
|
+
)}'), '${d[0]}')`
|
|
609
|
+
})
|
|
610
|
+
.join('\n,')}
|
|
611
|
+
})`
|
|
612
|
+
: '',
|
|
613
|
+
lazyComponentNode
|
|
614
|
+
? `.lazy(() => import('./${replaceBackslash(
|
|
615
|
+
removeExt(
|
|
616
|
+
path.relative(
|
|
617
|
+
path.dirname(this.config.generatedRouteTree),
|
|
618
|
+
path.resolve(
|
|
619
|
+
this.config.routesDirectory,
|
|
620
|
+
lazyComponentNode.filePath,
|
|
621
|
+
),
|
|
622
|
+
),
|
|
623
|
+
this.config.addExtensions,
|
|
624
|
+
),
|
|
625
|
+
)}').then((d) => d.${exportName}))`
|
|
626
|
+
: '',
|
|
627
|
+
].join(''),
|
|
628
|
+
].join('\n\n')
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
let fileRoutesByPathInterfacePerPlugin = ''
|
|
632
|
+
let fileRoutesByFullPathPerPlugin = ''
|
|
633
|
+
|
|
634
|
+
if (!this.config.disableTypes && hasMatchingRouteFiles) {
|
|
635
|
+
fileRoutesByFullPathPerPlugin = [
|
|
636
|
+
`export interface File${exportName}sByFullPath {
|
|
637
|
+
${[...createRouteNodesByFullPath(acc.routeNodes).entries()].map(
|
|
638
|
+
([fullPath, routeNode]) => {
|
|
639
|
+
return `'${fullPath}': typeof ${getResolvedRouteNodeVariableName(routeNode, exportName)}`
|
|
640
|
+
},
|
|
641
|
+
)}
|
|
642
|
+
}`,
|
|
643
|
+
`export interface File${exportName}sByTo {
|
|
644
|
+
${[...createRouteNodesByTo(acc.routeNodes).entries()].map(([to, routeNode]) => {
|
|
645
|
+
return `'${to}': typeof ${getResolvedRouteNodeVariableName(routeNode, exportName)}`
|
|
646
|
+
})}
|
|
647
|
+
}`,
|
|
648
|
+
`export interface File${exportName}sById {
|
|
649
|
+
'${rootRouteId}': typeof root${exportName}Import,
|
|
650
|
+
${[...createRouteNodesById(acc.routeNodes).entries()].map(([id, routeNode]) => {
|
|
651
|
+
return `'${id}': typeof ${getResolvedRouteNodeVariableName(routeNode, exportName)}`
|
|
652
|
+
})}
|
|
653
|
+
}`,
|
|
654
|
+
`export interface File${exportName}Types {
|
|
655
|
+
file${exportName}sByFullPath: File${exportName}sByFullPath
|
|
656
|
+
fullPaths: ${acc.routeNodes.length > 0 ? [...createRouteNodesByFullPath(acc.routeNodes).keys()].map((fullPath) => `'${fullPath}'`).join('|') : 'never'}
|
|
657
|
+
file${exportName}sByTo: File${exportName}sByTo
|
|
658
|
+
to: ${acc.routeNodes.length > 0 ? [...createRouteNodesByTo(acc.routeNodes).keys()].map((to) => `'${to}'`).join('|') : 'never'}
|
|
659
|
+
id: ${[`'${rootRouteId}'`, ...[...createRouteNodesById(acc.routeNodes).keys()].map((id) => `'${id}'`)].join('|')}
|
|
660
|
+
file${exportName}sById: File${exportName}sById
|
|
661
|
+
}`,
|
|
662
|
+
`export interface Root${exportName}Children {
|
|
663
|
+
${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${getResolvedRouteNodeVariableName(child, exportName)}`).join(',')}
|
|
664
|
+
}`,
|
|
665
|
+
].join('\n')
|
|
666
|
+
|
|
667
|
+
fileRoutesByPathInterfacePerPlugin = buildFileRoutesByPathInterface({
|
|
668
|
+
...plugin.moduleAugmentation({ generator: this }),
|
|
669
|
+
routeNodes:
|
|
670
|
+
this.config.verboseFileRoutes !== false
|
|
671
|
+
? sortedRouteNodes
|
|
672
|
+
: [
|
|
673
|
+
...routeFileResult.map(({ node }) => node),
|
|
674
|
+
...sortedRouteNodes.filter((d) => d.isVirtual),
|
|
675
|
+
],
|
|
676
|
+
exportName,
|
|
677
|
+
})
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
let routeTree = ''
|
|
681
|
+
if (hasMatchingRouteFiles) {
|
|
682
|
+
routeTree = [
|
|
683
|
+
`const root${exportName}Children${this.config.disableTypes ? '' : `: Root${exportName}Children`} = {
|
|
684
|
+
${acc.routeTree
|
|
685
|
+
.map(
|
|
686
|
+
(child) =>
|
|
687
|
+
`${child.variableName}${exportName}: ${getResolvedRouteNodeVariableName(child, exportName)}`,
|
|
688
|
+
)
|
|
689
|
+
.join(',')}
|
|
690
|
+
}`,
|
|
691
|
+
`export const ${lowerCaseFirstChar(exportName)}Tree = root${exportName}Import._addFileChildren(root${exportName}Children)${this.config.disableTypes ? '' : `._addFileTypes<File${exportName}Types>()`}`,
|
|
692
|
+
].join('\n')
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return {
|
|
696
|
+
routeImports,
|
|
697
|
+
sortedRouteNodes,
|
|
698
|
+
acc,
|
|
699
|
+
virtualRouteNodes,
|
|
700
|
+
routeTreeConfig,
|
|
701
|
+
routeTree,
|
|
702
|
+
imports,
|
|
703
|
+
createUpdateRoutes,
|
|
704
|
+
fileRoutesByFullPathPerPlugin,
|
|
705
|
+
fileRoutesByPathInterfacePerPlugin,
|
|
167
706
|
}
|
|
168
707
|
}
|
|
169
708
|
|
|
170
|
-
|
|
709
|
+
const routeTrees = this.pluginsWithTransform.map((plugin) => ({
|
|
710
|
+
exportName: plugin.transformPlugin.exportName,
|
|
711
|
+
...buildRouteTreeForExport(plugin),
|
|
712
|
+
}))
|
|
171
713
|
|
|
172
|
-
|
|
714
|
+
this.plugins.map((plugin) => {
|
|
715
|
+
return plugin.onRouteTreesChanged?.({
|
|
716
|
+
routeTrees,
|
|
717
|
+
rootRouteNode,
|
|
718
|
+
generator: this,
|
|
719
|
+
})
|
|
720
|
+
})
|
|
173
721
|
|
|
174
|
-
|
|
722
|
+
let mergedImports = mergeImportDeclarations(
|
|
723
|
+
routeTrees.flatMap((d) => d.imports),
|
|
724
|
+
)
|
|
725
|
+
if (this.config.disableTypes) {
|
|
726
|
+
mergedImports = mergedImports.filter((d) => d.importKind !== 'type')
|
|
727
|
+
}
|
|
175
728
|
|
|
176
|
-
const
|
|
177
|
-
|
|
729
|
+
const importStatements = mergedImports.map(buildImportString)
|
|
730
|
+
|
|
731
|
+
let moduleAugmentation = ''
|
|
732
|
+
if (this.config.verboseFileRoutes === false && !this.config.disableTypes) {
|
|
733
|
+
moduleAugmentation = routeFileResult
|
|
734
|
+
.map(({ node }) => {
|
|
735
|
+
const getModuleDeclaration = (routeNode?: RouteNode) => {
|
|
736
|
+
if (!isRouteNodeValidForAugmentation(routeNode)) {
|
|
737
|
+
return ''
|
|
738
|
+
}
|
|
739
|
+
const moduleAugmentation = this.pluginsWithTransform
|
|
740
|
+
.map((plugin) => {
|
|
741
|
+
return plugin.routeModuleAugmentation({
|
|
742
|
+
routeNode,
|
|
743
|
+
})
|
|
744
|
+
})
|
|
745
|
+
.filter(Boolean)
|
|
746
|
+
.join('\n')
|
|
178
747
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
748
|
+
return `declare module './${this.getImportPath(routeNode)}' {
|
|
749
|
+
${moduleAugmentation}
|
|
750
|
+
}`
|
|
751
|
+
}
|
|
752
|
+
return getModuleDeclaration(node)
|
|
753
|
+
})
|
|
754
|
+
.join('\n')
|
|
755
|
+
}
|
|
182
756
|
|
|
183
|
-
|
|
184
|
-
|
|
757
|
+
const routeImports = routeTrees.flatMap((t) => t.routeImports)
|
|
758
|
+
const rootRouteImports = this.pluginsWithTransform.flatMap(
|
|
759
|
+
(p) =>
|
|
760
|
+
getImportForRouteNode(rootRouteNode, p.transformPlugin.exportName) ??
|
|
761
|
+
[],
|
|
185
762
|
)
|
|
763
|
+
if (rootRouteImports.length > 0) {
|
|
764
|
+
routeImports.unshift(...rootRouteImports)
|
|
765
|
+
}
|
|
766
|
+
const routeTreeContent = [
|
|
767
|
+
...this.config.routeTreeFileHeader,
|
|
768
|
+
`// This file was automatically generated by TanStack Router.
|
|
769
|
+
// You should NOT make any changes in this file as it will be overwritten.
|
|
770
|
+
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.`,
|
|
771
|
+
[...importStatements].join('\n'),
|
|
772
|
+
mergeImportDeclarations(routeImports).map(buildImportString).join('\n'),
|
|
773
|
+
routeTrees.flatMap((t) => t.virtualRouteNodes).join('\n'),
|
|
774
|
+
routeTrees.flatMap((t) => t.createUpdateRoutes).join('\n'),
|
|
775
|
+
|
|
776
|
+
routeTrees.map((t) => t.fileRoutesByFullPathPerPlugin).join('\n'),
|
|
777
|
+
routeTrees.map((t) => t.fileRoutesByPathInterfacePerPlugin).join('\n'),
|
|
778
|
+
moduleAugmentation,
|
|
779
|
+
routeTrees.flatMap((t) => t.routeTreeConfig).join('\n'),
|
|
780
|
+
routeTrees.map((t) => t.routeTree).join('\n'),
|
|
781
|
+
...this.config.routeTreeFileFooter,
|
|
782
|
+
]
|
|
783
|
+
.filter(Boolean)
|
|
784
|
+
.join('\n\n')
|
|
785
|
+
return routeTreeContent
|
|
786
|
+
}
|
|
186
787
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
788
|
+
private getImportPath(node: RouteNode) {
|
|
789
|
+
return replaceBackslash(
|
|
790
|
+
removeExt(
|
|
791
|
+
path.relative(
|
|
792
|
+
path.dirname(this.config.generatedRouteTree),
|
|
793
|
+
path.resolve(this.config.routesDirectory, node.filePath),
|
|
794
|
+
),
|
|
795
|
+
this.config.addExtensions,
|
|
796
|
+
),
|
|
797
|
+
)
|
|
798
|
+
}
|
|
190
799
|
|
|
191
|
-
|
|
800
|
+
private async processRouteNodeFile(node: RouteNode): Promise<{
|
|
801
|
+
shouldWriteTree: boolean
|
|
802
|
+
cacheEntry: RouteNodeCacheEntry
|
|
803
|
+
node: RouteNode
|
|
804
|
+
} | null> {
|
|
805
|
+
const result = await this.isRouteFileCacheFresh(node)
|
|
192
806
|
|
|
193
|
-
|
|
807
|
+
if (result.status === 'fresh') {
|
|
808
|
+
node.exports = result.cacheEntry.exports
|
|
809
|
+
return {
|
|
810
|
+
node,
|
|
811
|
+
shouldWriteTree: result.exportsChanged,
|
|
812
|
+
cacheEntry: result.cacheEntry,
|
|
813
|
+
}
|
|
814
|
+
}
|
|
194
815
|
|
|
195
|
-
|
|
196
|
-
|
|
816
|
+
const existingRouteFile = await this.fs.readFile(node.fullPath)
|
|
817
|
+
if (existingRouteFile === 'file-not-existing') {
|
|
818
|
+
throw new Error(`⚠️ File ${node.fullPath} does not exist`)
|
|
819
|
+
}
|
|
197
820
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
replaced = await fillTemplate(
|
|
204
|
-
config,
|
|
205
|
-
(config.customScaffolding?.lazyRouteTemplate ||
|
|
206
|
-
config.customScaffolding?.routeTemplate) ??
|
|
207
|
-
tLazyRouteTemplate.template(),
|
|
208
|
-
{
|
|
209
|
-
tsrImports: tLazyRouteTemplate.imports.tsrImports(),
|
|
210
|
-
tsrPath: escapedRoutePath.replaceAll(/\{(.+)\}/gm, '$1'),
|
|
211
|
-
tsrExportStart:
|
|
212
|
-
tLazyRouteTemplate.imports.tsrExportStart(escapedRoutePath),
|
|
213
|
-
tsrExportEnd: tLazyRouteTemplate.imports.tsrExportEnd(),
|
|
214
|
-
},
|
|
215
|
-
)
|
|
216
|
-
} else if (
|
|
217
|
-
// Creating a new normal route file
|
|
218
|
-
(['layout', 'static'] satisfies Array<FsRouteType>).some(
|
|
219
|
-
(d) => d === node._fsRouteType,
|
|
220
|
-
) ||
|
|
221
|
-
(
|
|
222
|
-
[
|
|
223
|
-
'component',
|
|
224
|
-
'pendingComponent',
|
|
225
|
-
'errorComponent',
|
|
226
|
-
'loader',
|
|
227
|
-
] satisfies Array<FsRouteType>
|
|
228
|
-
).every((d) => d !== node._fsRouteType)
|
|
229
|
-
) {
|
|
230
|
-
replaced = await fillTemplate(
|
|
231
|
-
config,
|
|
232
|
-
config.customScaffolding?.routeTemplate ??
|
|
233
|
-
tRouteTemplate.template(),
|
|
234
|
-
{
|
|
235
|
-
tsrImports: tRouteTemplate.imports.tsrImports(),
|
|
236
|
-
tsrPath: escapedRoutePath.replaceAll(/\{(.+)\}/gm, '$1'),
|
|
237
|
-
tsrExportStart:
|
|
238
|
-
tRouteTemplate.imports.tsrExportStart(escapedRoutePath),
|
|
239
|
-
tsrExportEnd: tRouteTemplate.imports.tsrExportEnd(),
|
|
240
|
-
},
|
|
241
|
-
)
|
|
242
|
-
}
|
|
243
|
-
} else if (config.verboseFileRoutes === false) {
|
|
244
|
-
// Check if the route file has a Route export
|
|
245
|
-
if (
|
|
246
|
-
!routeCode
|
|
247
|
-
.split('\n')
|
|
248
|
-
.some((line) => line.trim().startsWith('export const Route'))
|
|
249
|
-
) {
|
|
250
|
-
return
|
|
251
|
-
}
|
|
821
|
+
const updatedCacheEntry: RouteNodeCacheEntry = {
|
|
822
|
+
fileContent: existingRouteFile.fileContent,
|
|
823
|
+
mtimeMs: existingRouteFile.stat.mtimeMs,
|
|
824
|
+
exports: [],
|
|
825
|
+
}
|
|
252
826
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
827
|
+
const escapedRoutePath = node.routePath?.replaceAll('$', '$$') ?? ''
|
|
828
|
+
|
|
829
|
+
let shouldWriteRouteFile = false
|
|
830
|
+
// now we need to either scaffold the file or transform it
|
|
831
|
+
if (!existingRouteFile.fileContent) {
|
|
832
|
+
shouldWriteRouteFile = true
|
|
833
|
+
// Creating a new lazy route file
|
|
834
|
+
if (node._fsRouteType === 'lazy') {
|
|
835
|
+
const tLazyRouteTemplate = this.targetTemplate.lazyRoute
|
|
836
|
+
// Check by default check if the user has a specific lazy route template
|
|
837
|
+
// If not, check if the user has a route template and use that instead
|
|
838
|
+
updatedCacheEntry.fileContent = await fillTemplate(
|
|
839
|
+
this.config,
|
|
840
|
+
(this.config.customScaffolding?.lazyRouteTemplate ||
|
|
841
|
+
this.config.customScaffolding?.routeTemplate) ??
|
|
842
|
+
tLazyRouteTemplate.template(),
|
|
843
|
+
{
|
|
844
|
+
tsrImports: tLazyRouteTemplate.imports.tsrImports(),
|
|
845
|
+
tsrPath: escapedRoutePath.replaceAll(/\{(.+)\}/gm, '$1'),
|
|
846
|
+
tsrExportStart:
|
|
847
|
+
tLazyRouteTemplate.imports.tsrExportStart(escapedRoutePath),
|
|
848
|
+
tsrExportEnd: tLazyRouteTemplate.imports.tsrExportEnd(),
|
|
849
|
+
},
|
|
850
|
+
)
|
|
851
|
+
updatedCacheEntry.exports = ['Route']
|
|
852
|
+
} else if (
|
|
853
|
+
// Creating a new normal route file
|
|
854
|
+
(['layout', 'static'] satisfies Array<FsRouteType>).some(
|
|
855
|
+
(d) => d === node._fsRouteType,
|
|
856
|
+
) ||
|
|
857
|
+
(
|
|
858
|
+
[
|
|
859
|
+
'component',
|
|
860
|
+
'pendingComponent',
|
|
861
|
+
'errorComponent',
|
|
862
|
+
'loader',
|
|
863
|
+
] satisfies Array<FsRouteType>
|
|
864
|
+
).every((d) => d !== node._fsRouteType)
|
|
865
|
+
) {
|
|
866
|
+
const tRouteTemplate = this.targetTemplate.route
|
|
867
|
+
updatedCacheEntry.fileContent = await fillTemplate(
|
|
868
|
+
this.config,
|
|
869
|
+
this.config.customScaffolding?.routeTemplate ??
|
|
870
|
+
tRouteTemplate.template(),
|
|
871
|
+
{
|
|
872
|
+
tsrImports: tRouteTemplate.imports.tsrImports(),
|
|
873
|
+
tsrPath: escapedRoutePath.replaceAll(/\{(.+)\}/gm, '$1'),
|
|
874
|
+
tsrExportStart:
|
|
875
|
+
tRouteTemplate.imports.tsrExportStart(escapedRoutePath),
|
|
876
|
+
tsrExportEnd: tRouteTemplate.imports.tsrExportEnd(),
|
|
877
|
+
},
|
|
878
|
+
)
|
|
879
|
+
updatedCacheEntry.exports = ['Route']
|
|
880
|
+
} else {
|
|
881
|
+
return null
|
|
882
|
+
}
|
|
883
|
+
} else {
|
|
884
|
+
// transform the file
|
|
885
|
+
const transformResult = await transform({
|
|
886
|
+
source: updatedCacheEntry.fileContent,
|
|
887
|
+
ctx: {
|
|
888
|
+
target: this.config.target,
|
|
889
|
+
routeId: escapedRoutePath,
|
|
890
|
+
lazy: node._fsRouteType === 'lazy',
|
|
891
|
+
verboseFileRoutes: !(this.config.verboseFileRoutes === false),
|
|
892
|
+
},
|
|
893
|
+
plugins: this.transformPlugins,
|
|
894
|
+
})
|
|
273
895
|
|
|
274
|
-
|
|
275
|
-
|
|
896
|
+
if (transformResult.result === 'error') {
|
|
897
|
+
throw new Error(
|
|
898
|
+
`Error transforming route file ${node.fullPath}: ${transformResult.error}`,
|
|
899
|
+
)
|
|
900
|
+
}
|
|
901
|
+
updatedCacheEntry.exports = transformResult.exports
|
|
902
|
+
if (transformResult.result === 'modified') {
|
|
903
|
+
updatedCacheEntry.fileContent = transformResult.output
|
|
904
|
+
shouldWriteRouteFile = true
|
|
905
|
+
}
|
|
906
|
+
}
|
|
276
907
|
|
|
277
|
-
|
|
278
|
-
|
|
908
|
+
// file was changed
|
|
909
|
+
if (shouldWriteRouteFile) {
|
|
910
|
+
const stats = await this.safeFileWrite({
|
|
911
|
+
filePath: node.fullPath,
|
|
912
|
+
newContent: updatedCacheEntry.fileContent,
|
|
913
|
+
strategy: {
|
|
914
|
+
type: 'mtime',
|
|
915
|
+
expectedMtimeMs: updatedCacheEntry.mtimeMs,
|
|
916
|
+
},
|
|
917
|
+
})
|
|
918
|
+
updatedCacheEntry.mtimeMs = stats.mtimeMs
|
|
919
|
+
}
|
|
279
920
|
|
|
280
|
-
|
|
921
|
+
this.routeNodeShadowCache.set(node.fullPath, updatedCacheEntry)
|
|
922
|
+
node.exports = updatedCacheEntry.exports
|
|
923
|
+
const shouldWriteTree = !deepEqual(
|
|
924
|
+
result.cacheEntry?.exports,
|
|
925
|
+
updatedCacheEntry.exports,
|
|
926
|
+
)
|
|
927
|
+
return {
|
|
928
|
+
node,
|
|
929
|
+
shouldWriteTree,
|
|
930
|
+
cacheEntry: updatedCacheEntry,
|
|
931
|
+
}
|
|
932
|
+
}
|
|
281
933
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
934
|
+
private async didRouteFileChangeComparedToCache(
|
|
935
|
+
file: {
|
|
936
|
+
path: string
|
|
937
|
+
mtimeMs?: bigint
|
|
938
|
+
},
|
|
939
|
+
cache: 'routeNodeCache' | 'routeNodeShadowCache',
|
|
940
|
+
): Promise<FileCacheChange<RouteNodeCacheEntry>> {
|
|
941
|
+
const cacheEntry = this[cache].get(file.path)
|
|
942
|
+
return this.didFileChangeComparedToCache(file, cacheEntry)
|
|
943
|
+
}
|
|
285
944
|
|
|
286
|
-
|
|
287
|
-
|
|
945
|
+
private async didFileChangeComparedToCache<
|
|
946
|
+
TCacheEntry extends GeneratorCacheEntry,
|
|
947
|
+
>(
|
|
948
|
+
file: {
|
|
949
|
+
path: string
|
|
950
|
+
mtimeMs?: bigint
|
|
951
|
+
},
|
|
952
|
+
cacheEntry: TCacheEntry | undefined,
|
|
953
|
+
): Promise<FileCacheChange<TCacheEntry>> {
|
|
954
|
+
// for now we rely on the modification time of the file
|
|
955
|
+
// to determine if the file has changed
|
|
956
|
+
// we could also compare the file content but this would be slower as we would have to read the file
|
|
957
|
+
|
|
958
|
+
if (!cacheEntry) {
|
|
959
|
+
return { result: 'file-not-in-cache' }
|
|
960
|
+
}
|
|
961
|
+
let mtimeMs = file.mtimeMs
|
|
962
|
+
|
|
963
|
+
if (mtimeMs === undefined) {
|
|
964
|
+
try {
|
|
965
|
+
const currentStat = await this.fs.stat(file.path)
|
|
966
|
+
mtimeMs = currentStat.mtimeMs
|
|
967
|
+
} catch {
|
|
968
|
+
return { result: 'cannot-stat-file' }
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return { result: mtimeMs !== cacheEntry.mtimeMs, mtimeMs, cacheEntry }
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
private async safeFileWrite(opts: {
|
|
975
|
+
filePath: string
|
|
976
|
+
newContent: string
|
|
977
|
+
strategy:
|
|
978
|
+
| {
|
|
979
|
+
type: 'mtime'
|
|
980
|
+
expectedMtimeMs: bigint
|
|
981
|
+
}
|
|
982
|
+
| {
|
|
983
|
+
type: 'new-file'
|
|
984
|
+
}
|
|
985
|
+
}) {
|
|
986
|
+
const tmpPath = this.getTempFileName(opts.filePath)
|
|
987
|
+
await this.fs.writeFile(tmpPath, opts.newContent)
|
|
988
|
+
|
|
989
|
+
if (opts.strategy.type === 'mtime') {
|
|
990
|
+
const beforeStat = await this.fs.stat(opts.filePath)
|
|
991
|
+
if (beforeStat.mtimeMs !== opts.strategy.expectedMtimeMs) {
|
|
992
|
+
throw rerun({
|
|
993
|
+
msg: `File ${opts.filePath} was modified by another process during processing.`,
|
|
994
|
+
event: { type: 'update', path: opts.filePath },
|
|
995
|
+
})
|
|
996
|
+
}
|
|
997
|
+
} else {
|
|
998
|
+
if (await checkFileExists(opts.filePath)) {
|
|
999
|
+
throw rerun({
|
|
1000
|
+
msg: `File ${opts.filePath} already exists. Cannot overwrite.`,
|
|
1001
|
+
event: { type: 'update', path: opts.filePath },
|
|
1002
|
+
})
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
288
1005
|
|
|
289
|
-
|
|
290
|
-
const before = beforeCreateFileRoute()
|
|
291
|
-
const after = afterCreateFileRoute()
|
|
1006
|
+
const stat = await this.fs.stat(tmpPath)
|
|
292
1007
|
|
|
293
|
-
|
|
1008
|
+
await this.fs.rename(tmpPath, opts.filePath)
|
|
294
1009
|
|
|
295
|
-
|
|
1010
|
+
return stat
|
|
1011
|
+
}
|
|
296
1012
|
|
|
297
|
-
|
|
298
|
-
|
|
1013
|
+
private getTempFileName(filePath: string) {
|
|
1014
|
+
const absPath = path.resolve(filePath)
|
|
1015
|
+
const hash = crypto.createHash('md5').update(absPath).digest('hex')
|
|
1016
|
+
return path.join(this.tmpDir, hash)
|
|
1017
|
+
}
|
|
299
1018
|
|
|
300
|
-
|
|
1019
|
+
private async isRouteFileCacheFresh(node: RouteNode): Promise<
|
|
1020
|
+
| {
|
|
1021
|
+
status: 'fresh'
|
|
1022
|
+
cacheEntry: RouteNodeCacheEntry
|
|
1023
|
+
exportsChanged: boolean
|
|
1024
|
+
}
|
|
1025
|
+
| { status: 'stale'; cacheEntry?: RouteNodeCacheEntry }
|
|
1026
|
+
> {
|
|
1027
|
+
const fileChangedCache = await this.didRouteFileChangeComparedToCache(
|
|
1028
|
+
{ path: node.fullPath },
|
|
1029
|
+
'routeNodeCache',
|
|
1030
|
+
)
|
|
1031
|
+
if (fileChangedCache.result === false) {
|
|
1032
|
+
this.routeNodeShadowCache.set(node.fullPath, fileChangedCache.cacheEntry)
|
|
1033
|
+
return {
|
|
1034
|
+
status: 'fresh',
|
|
1035
|
+
exportsChanged: false,
|
|
1036
|
+
cacheEntry: fileChangedCache.cacheEntry,
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
if (fileChangedCache.result === 'cannot-stat-file') {
|
|
1040
|
+
throw new Error(`⚠️ expected route file to exist at ${node.fullPath}`)
|
|
1041
|
+
}
|
|
1042
|
+
const mtimeMs =
|
|
1043
|
+
fileChangedCache.result === true ? fileChangedCache.mtimeMs : undefined
|
|
301
1044
|
|
|
302
|
-
|
|
1045
|
+
const shadowCacheFileChange = await this.didRouteFileChangeComparedToCache(
|
|
1046
|
+
{ path: node.fullPath, mtimeMs },
|
|
1047
|
+
'routeNodeShadowCache',
|
|
1048
|
+
)
|
|
303
1049
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
// Check if the route file has a Route export
|
|
1050
|
+
if (shadowCacheFileChange.result === 'cannot-stat-file') {
|
|
1051
|
+
throw new Error(`⚠️ expected route file to exist at ${node.fullPath}`)
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
if (shadowCacheFileChange.result === false) {
|
|
1055
|
+
// shadow cache has latest file state already
|
|
1056
|
+
// compare shadowCache against cache to determine whether exports changed
|
|
1057
|
+
// if they didn't, cache is fresh
|
|
1058
|
+
if (fileChangedCache.result === true) {
|
|
314
1059
|
if (
|
|
315
|
-
|
|
316
|
-
.
|
|
317
|
-
.
|
|
1060
|
+
deepEqual(
|
|
1061
|
+
fileChangedCache.cacheEntry.exports,
|
|
1062
|
+
shadowCacheFileChange.cacheEntry.exports,
|
|
1063
|
+
)
|
|
318
1064
|
) {
|
|
319
|
-
return
|
|
1065
|
+
return {
|
|
1066
|
+
status: 'fresh',
|
|
1067
|
+
exportsChanged: false,
|
|
1068
|
+
cacheEntry: shadowCacheFileChange.cacheEntry,
|
|
1069
|
+
}
|
|
320
1070
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
.replace(
|
|
326
|
-
/(FileRoute\(\s*['"])([^\s]*)(['"],?\s*\))/g,
|
|
327
|
-
(_, p1, __, p3) => `${p1}${escapedRoutePath}${p3}`,
|
|
328
|
-
)
|
|
329
|
-
// fix missing ids
|
|
330
|
-
.replace(
|
|
331
|
-
/((FileRoute)(\s*)(\({))/g,
|
|
332
|
-
(_, __, p2, p3, p4) => `${p2}('${escapedRoutePath}')${p3}${p4}`,
|
|
333
|
-
)
|
|
334
|
-
.replace(
|
|
335
|
-
new RegExp(
|
|
336
|
-
`(import\\s*\\{.*)(create(Lazy)?FileRoute)(.*\\}\\s*from\\s*['"]@tanstack\\/${ROUTE_TEMPLATE.subPkg}['"])`,
|
|
337
|
-
'gs',
|
|
338
|
-
),
|
|
339
|
-
(_, p1, __, ___, p4) =>
|
|
340
|
-
`${p1}${node._fsRouteType === 'lazy' ? 'createLazyFileRoute' : 'createFileRoute'}${p4}`,
|
|
341
|
-
)
|
|
342
|
-
.replace(
|
|
343
|
-
/create(Lazy)?FileRoute(\(\s*['"])([^\s]*)(['"],?\s*\))/g,
|
|
344
|
-
(_, __, p2, ___, p4) =>
|
|
345
|
-
`${node._fsRouteType === 'lazy' ? 'createLazyFileRoute' : 'createFileRoute'}${p2}${escapedRoutePath}${p4}`,
|
|
346
|
-
)
|
|
347
|
-
|
|
348
|
-
// check whether the import statement is already present
|
|
349
|
-
const regex = new RegExp(
|
|
350
|
-
`(import\\s*\\{.*)(create(Lazy)?FileRoute)(.*\\}\\s*from\\s*['"]@tanstack\\/${ROUTE_TEMPLATE.subPkg}['"])`,
|
|
351
|
-
'gm',
|
|
352
|
-
)
|
|
353
|
-
if (!replaced.match(regex)) {
|
|
354
|
-
replaced = [
|
|
355
|
-
`import { ${node._fsRouteType === 'lazy' ? 'createLazyFileRoute' : 'createFileRoute'} } from '@tanstack/${ROUTE_TEMPLATE.subPkg}'`,
|
|
356
|
-
...replaced.split('\n'),
|
|
357
|
-
].join('\n')
|
|
1071
|
+
return {
|
|
1072
|
+
status: 'fresh',
|
|
1073
|
+
exportsChanged: true,
|
|
1074
|
+
cacheEntry: shadowCacheFileChange.cacheEntry,
|
|
358
1075
|
}
|
|
359
1076
|
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (fileChangedCache.result === 'file-not-in-cache') {
|
|
1080
|
+
return {
|
|
1081
|
+
status: 'stale',
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
return { status: 'stale', cacheEntry: fileChangedCache.cacheEntry }
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
private async handleRootNode(node: RouteNode) {
|
|
1088
|
+
const result = await this.isRouteFileCacheFresh(node)
|
|
1089
|
+
|
|
1090
|
+
if (result.status === 'fresh') {
|
|
1091
|
+
node.exports = result.cacheEntry.exports
|
|
1092
|
+
this.routeNodeShadowCache.set(node.fullPath, result.cacheEntry)
|
|
1093
|
+
return result.exportsChanged
|
|
1094
|
+
}
|
|
1095
|
+
const rootNodeFile = await this.fs.readFile(node.fullPath)
|
|
1096
|
+
if (rootNodeFile === 'file-not-existing') {
|
|
1097
|
+
throw new Error(`⚠️ expected root route to exist at ${node.fullPath}`)
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const updatedCacheEntry: RouteNodeCacheEntry = {
|
|
1101
|
+
fileContent: rootNodeFile.fileContent,
|
|
1102
|
+
mtimeMs: rootNodeFile.stat.mtimeMs,
|
|
1103
|
+
exports: [],
|
|
1104
|
+
}
|
|
360
1105
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
1106
|
+
// scaffold the root route
|
|
1107
|
+
if (!rootNodeFile.fileContent) {
|
|
1108
|
+
const rootTemplate = this.targetTemplate.rootRoute
|
|
1109
|
+
const rootRouteContent = await fillTemplate(
|
|
1110
|
+
this.config,
|
|
1111
|
+
rootTemplate.template(),
|
|
1112
|
+
{
|
|
1113
|
+
tsrImports: rootTemplate.imports.tsrImports(),
|
|
1114
|
+
tsrPath: rootPathId,
|
|
1115
|
+
tsrExportStart: rootTemplate.imports.tsrExportStart(),
|
|
1116
|
+
tsrExportEnd: rootTemplate.imports.tsrExportEnd(),
|
|
1117
|
+
},
|
|
1118
|
+
)
|
|
1119
|
+
|
|
1120
|
+
this.logger.log(`🟡 Creating ${node.fullPath}`)
|
|
1121
|
+
const stats = await this.safeFileWrite({
|
|
1122
|
+
filePath: node.fullPath,
|
|
1123
|
+
newContent: rootRouteContent,
|
|
1124
|
+
strategy: {
|
|
1125
|
+
type: 'mtime',
|
|
1126
|
+
expectedMtimeMs: rootNodeFile.stat.mtimeMs,
|
|
364
1127
|
},
|
|
365
1128
|
})
|
|
1129
|
+
updatedCacheEntry.fileContent = rootRouteContent
|
|
1130
|
+
updatedCacheEntry.mtimeMs = stats.mtimeMs
|
|
366
1131
|
}
|
|
367
1132
|
|
|
1133
|
+
const rootRouteExports: Array<string> = []
|
|
1134
|
+
for (const plugin of this.pluginsWithTransform) {
|
|
1135
|
+
const exportName = plugin.transformPlugin.exportName
|
|
1136
|
+
if (rootNodeFile.fileContent.includes(`export const ${exportName}`)) {
|
|
1137
|
+
rootRouteExports.push(exportName)
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
updatedCacheEntry.exports = rootRouteExports
|
|
1142
|
+
node.exports = rootRouteExports
|
|
1143
|
+
this.routeNodeShadowCache.set(node.fullPath, updatedCacheEntry)
|
|
1144
|
+
|
|
1145
|
+
const shouldWriteTree = !deepEqual(
|
|
1146
|
+
result.cacheEntry?.exports,
|
|
1147
|
+
rootRouteExports,
|
|
1148
|
+
)
|
|
1149
|
+
return shouldWriteTree
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
private handleNode(node: RouteNode, acc: HandleNodeAccumulator) {
|
|
1153
|
+
// Do not remove this as we need to set the lastIndex to 0 as it
|
|
1154
|
+
// is necessary to reset the regex's index when using the global flag
|
|
1155
|
+
// otherwise it might not match the next time it's used
|
|
1156
|
+
resetRegex(this.routeGroupPatternRegex)
|
|
1157
|
+
|
|
1158
|
+
let parentRoute = hasParentRoute(acc.routeNodes, node, node.routePath)
|
|
1159
|
+
|
|
1160
|
+
// if the parent route is a virtual parent route, we need to find the real parent route
|
|
1161
|
+
if (parentRoute?.isVirtualParentRoute && parentRoute.children?.length) {
|
|
1162
|
+
// only if this sub-parent route returns a valid parent route, we use it, if not leave it as it
|
|
1163
|
+
const possibleParentRoute = hasParentRoute(
|
|
1164
|
+
parentRoute.children,
|
|
1165
|
+
node,
|
|
1166
|
+
node.routePath,
|
|
1167
|
+
)
|
|
1168
|
+
if (possibleParentRoute) {
|
|
1169
|
+
parentRoute = possibleParentRoute
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
if (parentRoute) node.parent = parentRoute
|
|
1174
|
+
|
|
1175
|
+
node.path = determineNodePath(node)
|
|
1176
|
+
|
|
1177
|
+
const trimmedPath = trimPathLeft(node.path ?? '')
|
|
1178
|
+
|
|
1179
|
+
const split = trimmedPath.split('/')
|
|
1180
|
+
const lastRouteSegment = split[split.length - 1] ?? trimmedPath
|
|
1181
|
+
|
|
1182
|
+
node.isNonPath =
|
|
1183
|
+
lastRouteSegment.startsWith('_') ||
|
|
1184
|
+
this.routeGroupPatternRegex.test(lastRouteSegment)
|
|
1185
|
+
|
|
1186
|
+
node.cleanedPath = removeGroups(
|
|
1187
|
+
removeUnderscores(removeLayoutSegments(node.path)) ?? '',
|
|
1188
|
+
)
|
|
1189
|
+
|
|
368
1190
|
if (
|
|
369
1191
|
!node.isVirtual &&
|
|
370
1192
|
(
|
|
@@ -377,10 +1199,10 @@ export async function generator(config: Config, root: string) {
|
|
|
377
1199
|
] satisfies Array<FsRouteType>
|
|
378
1200
|
).some((d) => d === node._fsRouteType)
|
|
379
1201
|
) {
|
|
380
|
-
routePiecesByPath[node.routePath!] =
|
|
381
|
-
routePiecesByPath[node.routePath!] || {}
|
|
1202
|
+
acc.routePiecesByPath[node.routePath!] =
|
|
1203
|
+
acc.routePiecesByPath[node.routePath!] || {}
|
|
382
1204
|
|
|
383
|
-
routePiecesByPath[node.routePath!]![
|
|
1205
|
+
acc.routePiecesByPath[node.routePath!]![
|
|
384
1206
|
node._fsRouteType === 'lazy'
|
|
385
1207
|
? 'lazy'
|
|
386
1208
|
: node._fsRouteType === 'loader'
|
|
@@ -392,14 +1214,19 @@ export async function generator(config: Config, root: string) {
|
|
|
392
1214
|
: 'component'
|
|
393
1215
|
] = node
|
|
394
1216
|
|
|
395
|
-
const anchorRoute = routeNodes.find(
|
|
1217
|
+
const anchorRoute = acc.routeNodes.find(
|
|
1218
|
+
(d) => d.routePath === node.routePath,
|
|
1219
|
+
)
|
|
396
1220
|
|
|
397
1221
|
if (!anchorRoute) {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
1222
|
+
this.handleNode(
|
|
1223
|
+
{
|
|
1224
|
+
...node,
|
|
1225
|
+
isVirtual: true,
|
|
1226
|
+
_fsRouteType: 'static',
|
|
1227
|
+
},
|
|
1228
|
+
acc,
|
|
1229
|
+
)
|
|
403
1230
|
}
|
|
404
1231
|
return
|
|
405
1232
|
}
|
|
@@ -417,7 +1244,7 @@ export async function generator(config: Config, root: string) {
|
|
|
417
1244
|
const parentRoutePath = removeLastSegmentFromPath(node.routePath) || '/'
|
|
418
1245
|
const parentVariableName = routePathToVariable(parentRoutePath)
|
|
419
1246
|
|
|
420
|
-
const anchorRoute = routeNodes.find(
|
|
1247
|
+
const anchorRoute = acc.routeNodes.find(
|
|
421
1248
|
(d) => d.routePath === parentRoutePath,
|
|
422
1249
|
)
|
|
423
1250
|
|
|
@@ -445,7 +1272,7 @@ export async function generator(config: Config, root: string) {
|
|
|
445
1272
|
node.path = determineNodePath(node)
|
|
446
1273
|
}
|
|
447
1274
|
|
|
448
|
-
|
|
1275
|
+
this.handleNode(parentNode, acc)
|
|
449
1276
|
} else {
|
|
450
1277
|
anchorRoute.children = anchorRoute.children ?? []
|
|
451
1278
|
anchorRoute.children.push(node)
|
|
@@ -460,658 +1287,9 @@ export async function generator(config: Config, root: string) {
|
|
|
460
1287
|
node.parent.children.push(node)
|
|
461
1288
|
}
|
|
462
1289
|
} else {
|
|
463
|
-
routeTree.push(node)
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
routeNodes.push(node)
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
for (const node of preRouteNodes) {
|
|
470
|
-
await handleNode(node)
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// This is run against the `preRouteNodes` array since it
|
|
474
|
-
// has the flattened Route nodes and not the full tree
|
|
475
|
-
// Since TSR allows multiple way of defining a route,
|
|
476
|
-
// we need to ensure that a user hasn't defined the
|
|
477
|
-
// same route in multiple ways (i.e. `flat`, `nested`, `virtual`)
|
|
478
|
-
checkRouteFullPathUniqueness(
|
|
479
|
-
preRouteNodes.filter(
|
|
480
|
-
(d) => d.children === undefined && 'lazy' !== d._fsRouteType,
|
|
481
|
-
),
|
|
482
|
-
config,
|
|
483
|
-
)
|
|
484
|
-
|
|
485
|
-
function buildRouteTreeConfig(nodes: Array<RouteNode>, depth = 1): string {
|
|
486
|
-
const children = nodes.map((node) => {
|
|
487
|
-
if (node._fsRouteType === '__root') {
|
|
488
|
-
return
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
if (node._fsRouteType === 'pathless_layout' && !node.children?.length) {
|
|
492
|
-
return
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
const route = `${node.variableName}Route`
|
|
496
|
-
|
|
497
|
-
if (node.children?.length) {
|
|
498
|
-
const childConfigs = buildRouteTreeConfig(node.children, depth + 1)
|
|
499
|
-
|
|
500
|
-
const childrenDeclaration = TYPES_DISABLED
|
|
501
|
-
? ''
|
|
502
|
-
: `interface ${route}Children {
|
|
503
|
-
${node.children.map((child) => `${child.variableName}Route: typeof ${getResolvedRouteNodeVariableName(child)}`).join(',')}
|
|
504
|
-
}`
|
|
505
|
-
|
|
506
|
-
const children = `const ${route}Children${TYPES_DISABLED ? '' : `: ${route}Children`} = {
|
|
507
|
-
${node.children.map((child) => `${child.variableName}Route: ${getResolvedRouteNodeVariableName(child)}`).join(',')}
|
|
508
|
-
}`
|
|
509
|
-
|
|
510
|
-
const routeWithChildren = `const ${route}WithChildren = ${route}._addFileChildren(${route}Children)`
|
|
511
|
-
|
|
512
|
-
return [
|
|
513
|
-
childConfigs,
|
|
514
|
-
childrenDeclaration,
|
|
515
|
-
children,
|
|
516
|
-
routeWithChildren,
|
|
517
|
-
].join('\n\n')
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
return undefined
|
|
521
|
-
})
|
|
522
|
-
|
|
523
|
-
return children.filter(Boolean).join('\n\n')
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
const routeConfigChildrenText = buildRouteTreeConfig(routeTree)
|
|
527
|
-
|
|
528
|
-
const sortedRouteNodes = multiSortBy(routeNodes, [
|
|
529
|
-
(d) => (d.routePath?.includes(`/${rootPathId}`) ? -1 : 1),
|
|
530
|
-
(d) => d.routePath?.split('/').length,
|
|
531
|
-
(d) => (d.routePath?.endsWith(config.indexToken) ? -1 : 1),
|
|
532
|
-
(d) => d,
|
|
533
|
-
])
|
|
534
|
-
|
|
535
|
-
const typeImports = Object.entries({
|
|
536
|
-
// Used for augmentation of regular routes
|
|
537
|
-
CreateFileRoute:
|
|
538
|
-
config.verboseFileRoutes === false &&
|
|
539
|
-
sortedRouteNodes.some(
|
|
540
|
-
(d) => isRouteNodeValidForAugmentation(d) && d._fsRouteType !== 'lazy',
|
|
541
|
-
),
|
|
542
|
-
// Used for augmentation of lazy (`.lazy`) routes
|
|
543
|
-
CreateLazyFileRoute:
|
|
544
|
-
config.verboseFileRoutes === false &&
|
|
545
|
-
sortedRouteNodes.some(
|
|
546
|
-
(node) =>
|
|
547
|
-
routePiecesByPath[node.routePath!]?.lazy &&
|
|
548
|
-
isRouteNodeValidForAugmentation(node),
|
|
549
|
-
),
|
|
550
|
-
// Used in the process of augmenting the routes
|
|
551
|
-
FileRoutesByPath:
|
|
552
|
-
config.verboseFileRoutes === false &&
|
|
553
|
-
sortedRouteNodes.some((d) => isRouteNodeValidForAugmentation(d)),
|
|
554
|
-
})
|
|
555
|
-
.filter((d) => d[1])
|
|
556
|
-
.map((d) => d[0])
|
|
557
|
-
.sort((a, b) => a.localeCompare(b))
|
|
558
|
-
|
|
559
|
-
const imports = Object.entries({
|
|
560
|
-
createFileRoute: sortedRouteNodes.some((d) => d.isVirtual),
|
|
561
|
-
lazyFn: sortedRouteNodes.some(
|
|
562
|
-
(node) => routePiecesByPath[node.routePath!]?.loader,
|
|
563
|
-
),
|
|
564
|
-
lazyRouteComponent: sortedRouteNodes.some(
|
|
565
|
-
(node) =>
|
|
566
|
-
routePiecesByPath[node.routePath!]?.component ||
|
|
567
|
-
routePiecesByPath[node.routePath!]?.errorComponent ||
|
|
568
|
-
routePiecesByPath[node.routePath!]?.pendingComponent,
|
|
569
|
-
),
|
|
570
|
-
})
|
|
571
|
-
.filter((d) => d[1])
|
|
572
|
-
.map((d) => d[0])
|
|
573
|
-
|
|
574
|
-
const virtualRouteNodes = sortedRouteNodes.filter((d) => d.isVirtual)
|
|
575
|
-
|
|
576
|
-
function getImportPath(node: RouteNode) {
|
|
577
|
-
return replaceBackslash(
|
|
578
|
-
removeExt(
|
|
579
|
-
path.relative(
|
|
580
|
-
path.dirname(config.generatedRouteTree),
|
|
581
|
-
path.resolve(config.routesDirectory, node.filePath),
|
|
582
|
-
),
|
|
583
|
-
config.addExtensions,
|
|
584
|
-
),
|
|
585
|
-
)
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
const routeImports = [
|
|
589
|
-
...config.routeTreeFileHeader,
|
|
590
|
-
`// This file was automatically generated by TanStack Router.
|
|
591
|
-
// You should NOT make any changes in this file as it will be overwritten.
|
|
592
|
-
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.`,
|
|
593
|
-
[
|
|
594
|
-
imports.length
|
|
595
|
-
? `import { ${imports.join(', ')} } from '${ROUTE_TEMPLATE.fullPkg}'`
|
|
596
|
-
: '',
|
|
597
|
-
!TYPES_DISABLED && typeImports.length
|
|
598
|
-
? `import type { ${typeImports.join(', ')} } from '${ROUTE_TEMPLATE.fullPkg}'`
|
|
599
|
-
: '',
|
|
600
|
-
]
|
|
601
|
-
.filter(Boolean)
|
|
602
|
-
.join('\n'),
|
|
603
|
-
'// Import Routes',
|
|
604
|
-
[
|
|
605
|
-
`import { Route as rootRoute } from './${getImportPath(rootRouteNode)}'`,
|
|
606
|
-
...sortedRouteNodes
|
|
607
|
-
.filter((d) => !d.isVirtual)
|
|
608
|
-
.map((node) => {
|
|
609
|
-
return `import { Route as ${
|
|
610
|
-
node.variableName
|
|
611
|
-
}RouteImport } from './${getImportPath(node)}'`
|
|
612
|
-
}),
|
|
613
|
-
].join('\n'),
|
|
614
|
-
virtualRouteNodes.length ? '// Create Virtual Routes' : '',
|
|
615
|
-
virtualRouteNodes
|
|
616
|
-
.map((node) => {
|
|
617
|
-
return `const ${
|
|
618
|
-
node.variableName
|
|
619
|
-
}RouteImport = createFileRoute('${node.routePath}')()`
|
|
620
|
-
})
|
|
621
|
-
.join('\n'),
|
|
622
|
-
'// Create/Update Routes',
|
|
623
|
-
sortedRouteNodes
|
|
624
|
-
.map((node) => {
|
|
625
|
-
const loaderNode = routePiecesByPath[node.routePath!]?.loader
|
|
626
|
-
const componentNode = routePiecesByPath[node.routePath!]?.component
|
|
627
|
-
const errorComponentNode =
|
|
628
|
-
routePiecesByPath[node.routePath!]?.errorComponent
|
|
629
|
-
const pendingComponentNode =
|
|
630
|
-
routePiecesByPath[node.routePath!]?.pendingComponent
|
|
631
|
-
const lazyComponentNode = routePiecesByPath[node.routePath!]?.lazy
|
|
632
|
-
|
|
633
|
-
return [
|
|
634
|
-
[
|
|
635
|
-
`const ${node.variableName}Route = ${node.variableName}RouteImport.update({
|
|
636
|
-
${[
|
|
637
|
-
`id: '${node.path}'`,
|
|
638
|
-
!node.isNonPath ? `path: '${node.cleanedPath}'` : undefined,
|
|
639
|
-
`getParentRoute: () => ${node.parent?.variableName ?? 'root'}Route`,
|
|
640
|
-
]
|
|
641
|
-
.filter(Boolean)
|
|
642
|
-
.join(',')}
|
|
643
|
-
}${TYPES_DISABLED ? '' : 'as any'})`,
|
|
644
|
-
loaderNode
|
|
645
|
-
? `.updateLoader({ loader: lazyFn(() => import('./${replaceBackslash(
|
|
646
|
-
removeExt(
|
|
647
|
-
path.relative(
|
|
648
|
-
path.dirname(config.generatedRouteTree),
|
|
649
|
-
path.resolve(config.routesDirectory, loaderNode.filePath),
|
|
650
|
-
),
|
|
651
|
-
config.addExtensions,
|
|
652
|
-
),
|
|
653
|
-
)}'), 'loader') })`
|
|
654
|
-
: '',
|
|
655
|
-
componentNode || errorComponentNode || pendingComponentNode
|
|
656
|
-
? `.update({
|
|
657
|
-
${(
|
|
658
|
-
[
|
|
659
|
-
['component', componentNode],
|
|
660
|
-
['errorComponent', errorComponentNode],
|
|
661
|
-
['pendingComponent', pendingComponentNode],
|
|
662
|
-
] as const
|
|
663
|
-
)
|
|
664
|
-
.filter((d) => d[1])
|
|
665
|
-
.map((d) => {
|
|
666
|
-
return `${
|
|
667
|
-
d[0]
|
|
668
|
-
}: lazyRouteComponent(() => import('./${replaceBackslash(
|
|
669
|
-
removeExt(
|
|
670
|
-
path.relative(
|
|
671
|
-
path.dirname(config.generatedRouteTree),
|
|
672
|
-
path.resolve(config.routesDirectory, d[1]!.filePath),
|
|
673
|
-
),
|
|
674
|
-
config.addExtensions,
|
|
675
|
-
),
|
|
676
|
-
)}'), '${d[0]}')`
|
|
677
|
-
})
|
|
678
|
-
.join('\n,')}
|
|
679
|
-
})`
|
|
680
|
-
: '',
|
|
681
|
-
lazyComponentNode
|
|
682
|
-
? `.lazy(() => import('./${replaceBackslash(
|
|
683
|
-
removeExt(
|
|
684
|
-
path.relative(
|
|
685
|
-
path.dirname(config.generatedRouteTree),
|
|
686
|
-
path.resolve(
|
|
687
|
-
config.routesDirectory,
|
|
688
|
-
lazyComponentNode.filePath,
|
|
689
|
-
),
|
|
690
|
-
),
|
|
691
|
-
config.addExtensions,
|
|
692
|
-
),
|
|
693
|
-
)}').then((d) => d.Route))`
|
|
694
|
-
: '',
|
|
695
|
-
].join(''),
|
|
696
|
-
].join('\n\n')
|
|
697
|
-
})
|
|
698
|
-
.join('\n\n'),
|
|
699
|
-
...(TYPES_DISABLED
|
|
700
|
-
? []
|
|
701
|
-
: [
|
|
702
|
-
'// Populate the FileRoutesByPath interface',
|
|
703
|
-
`declare module '${ROUTE_TEMPLATE.fullPkg}' {
|
|
704
|
-
interface FileRoutesByPath {
|
|
705
|
-
${routeNodes
|
|
706
|
-
.map((routeNode) => {
|
|
707
|
-
const filePathId = routeNode.routePath
|
|
708
|
-
|
|
709
|
-
return `'${filePathId}': {
|
|
710
|
-
id: '${filePathId}'
|
|
711
|
-
path: '${inferPath(routeNode)}'
|
|
712
|
-
fullPath: '${inferFullPath(routeNode)}'
|
|
713
|
-
preLoaderRoute: typeof ${routeNode.variableName}RouteImport
|
|
714
|
-
parentRoute: typeof ${
|
|
715
|
-
routeNode.isVirtualParentRequired
|
|
716
|
-
? `${routeNode.parent?.variableName}Route`
|
|
717
|
-
: routeNode.parent?.variableName
|
|
718
|
-
? `${routeNode.parent.variableName}RouteImport`
|
|
719
|
-
: 'rootRoute'
|
|
720
|
-
}
|
|
721
|
-
}`
|
|
722
|
-
})
|
|
723
|
-
.join('\n')}
|
|
724
|
-
}
|
|
725
|
-
}`,
|
|
726
|
-
]),
|
|
727
|
-
...(TYPES_DISABLED
|
|
728
|
-
? []
|
|
729
|
-
: config.verboseFileRoutes !== false
|
|
730
|
-
? []
|
|
731
|
-
: [
|
|
732
|
-
`// Add type-safety to the createFileRoute function across the route tree`,
|
|
733
|
-
routeNodes
|
|
734
|
-
.map((routeNode) => {
|
|
735
|
-
function getModuleDeclaration(routeNode?: RouteNode) {
|
|
736
|
-
if (!isRouteNodeValidForAugmentation(routeNode)) {
|
|
737
|
-
return ''
|
|
738
|
-
}
|
|
739
|
-
return `declare module './${getImportPath(routeNode)}' {
|
|
740
|
-
const ${routeNode._fsRouteType === 'lazy' ? 'createLazyFileRoute' : 'createFileRoute'}: ${
|
|
741
|
-
routeNode._fsRouteType === 'lazy'
|
|
742
|
-
? `CreateLazyFileRoute<FileRoutesByPath['${routeNode.routePath}']['preLoaderRoute']>}`
|
|
743
|
-
: `CreateFileRoute<
|
|
744
|
-
'${routeNode.routePath}',
|
|
745
|
-
FileRoutesByPath['${routeNode.routePath}']['parentRoute'],
|
|
746
|
-
FileRoutesByPath['${routeNode.routePath}']['id'],
|
|
747
|
-
FileRoutesByPath['${routeNode.routePath}']['path'],
|
|
748
|
-
FileRoutesByPath['${routeNode.routePath}']['fullPath']
|
|
749
|
-
>
|
|
750
|
-
}`
|
|
751
|
-
}`
|
|
752
|
-
}
|
|
753
|
-
return (
|
|
754
|
-
getModuleDeclaration(routeNode) +
|
|
755
|
-
getModuleDeclaration(
|
|
756
|
-
routePiecesByPath[routeNode.routePath!]?.lazy,
|
|
757
|
-
)
|
|
758
|
-
)
|
|
759
|
-
})
|
|
760
|
-
.join('\n'),
|
|
761
|
-
]),
|
|
762
|
-
'// Create and export the route tree',
|
|
763
|
-
routeConfigChildrenText,
|
|
764
|
-
...(TYPES_DISABLED
|
|
765
|
-
? []
|
|
766
|
-
: [
|
|
767
|
-
`export interface FileRoutesByFullPath {
|
|
768
|
-
${[...createRouteNodesByFullPath(routeNodes).entries()].map(
|
|
769
|
-
([fullPath, routeNode]) => {
|
|
770
|
-
return `'${fullPath}': typeof ${getResolvedRouteNodeVariableName(routeNode)}`
|
|
771
|
-
},
|
|
772
|
-
)}
|
|
773
|
-
}`,
|
|
774
|
-
`export interface FileRoutesByTo {
|
|
775
|
-
${[...createRouteNodesByTo(routeNodes).entries()].map(([to, routeNode]) => {
|
|
776
|
-
return `'${to}': typeof ${getResolvedRouteNodeVariableName(routeNode)}`
|
|
777
|
-
})}
|
|
778
|
-
}`,
|
|
779
|
-
`export interface FileRoutesById {
|
|
780
|
-
'${rootRouteId}': typeof rootRoute,
|
|
781
|
-
${[...createRouteNodesById(routeNodes).entries()].map(([id, routeNode]) => {
|
|
782
|
-
return `'${id}': typeof ${getResolvedRouteNodeVariableName(routeNode)}`
|
|
783
|
-
})}
|
|
784
|
-
}`,
|
|
785
|
-
`export interface FileRouteTypes {
|
|
786
|
-
fileRoutesByFullPath: FileRoutesByFullPath
|
|
787
|
-
fullPaths: ${routeNodes.length > 0 ? [...createRouteNodesByFullPath(routeNodes).keys()].map((fullPath) => `'${fullPath}'`).join('|') : 'never'}
|
|
788
|
-
fileRoutesByTo: FileRoutesByTo
|
|
789
|
-
to: ${routeNodes.length > 0 ? [...createRouteNodesByTo(routeNodes).keys()].map((to) => `'${to}'`).join('|') : 'never'}
|
|
790
|
-
id: ${[`'${rootRouteId}'`, ...[...createRouteNodesById(routeNodes).keys()].map((id) => `'${id}'`)].join('|')}
|
|
791
|
-
fileRoutesById: FileRoutesById
|
|
792
|
-
}`,
|
|
793
|
-
`export interface RootRouteChildren {
|
|
794
|
-
${routeTree.map((child) => `${child.variableName}Route: typeof ${getResolvedRouteNodeVariableName(child)}`).join(',')}
|
|
795
|
-
}`,
|
|
796
|
-
]),
|
|
797
|
-
`const rootRouteChildren${TYPES_DISABLED ? '' : ': RootRouteChildren'} = {
|
|
798
|
-
${routeTree.map((child) => `${child.variableName}Route: ${getResolvedRouteNodeVariableName(child)}`).join(',')}
|
|
799
|
-
}`,
|
|
800
|
-
`export const routeTree = rootRoute._addFileChildren(rootRouteChildren)${TYPES_DISABLED ? '' : '._addFileTypes<FileRouteTypes>()'}`,
|
|
801
|
-
...config.routeTreeFileFooter,
|
|
802
|
-
]
|
|
803
|
-
.filter(Boolean)
|
|
804
|
-
.join('\n\n')
|
|
805
|
-
|
|
806
|
-
const createRouteManifest = () => {
|
|
807
|
-
const routesManifest = {
|
|
808
|
-
[rootRouteId]: {
|
|
809
|
-
filePath: rootRouteNode.filePath,
|
|
810
|
-
children: routeTree.map((d) => d.routePath),
|
|
811
|
-
},
|
|
812
|
-
...Object.fromEntries(
|
|
813
|
-
routeNodes.map((d) => {
|
|
814
|
-
const filePathId = d.routePath
|
|
815
|
-
|
|
816
|
-
return [
|
|
817
|
-
filePathId,
|
|
818
|
-
{
|
|
819
|
-
filePath: d.filePath,
|
|
820
|
-
parent: d.parent?.routePath ? d.parent.routePath : undefined,
|
|
821
|
-
children: d.children?.map((childRoute) => childRoute.routePath),
|
|
822
|
-
},
|
|
823
|
-
]
|
|
824
|
-
}),
|
|
825
|
-
),
|
|
1290
|
+
acc.routeTree.push(node)
|
|
826
1291
|
}
|
|
827
1292
|
|
|
828
|
-
|
|
829
|
-
{
|
|
830
|
-
routes: routesManifest,
|
|
831
|
-
},
|
|
832
|
-
null,
|
|
833
|
-
2,
|
|
834
|
-
)
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
const includeManifest = ['react', 'solid']
|
|
838
|
-
const routeConfigFileContent =
|
|
839
|
-
config.disableManifestGeneration || !includeManifest.includes(config.target)
|
|
840
|
-
? routeImports
|
|
841
|
-
: [
|
|
842
|
-
routeImports,
|
|
843
|
-
'\n',
|
|
844
|
-
'/* ROUTE_MANIFEST_START',
|
|
845
|
-
createRouteManifest(),
|
|
846
|
-
'ROUTE_MANIFEST_END */',
|
|
847
|
-
].join('\n')
|
|
848
|
-
|
|
849
|
-
if (!checkLatest()) return
|
|
850
|
-
|
|
851
|
-
const existingRouteTreeContent = await fsp
|
|
852
|
-
.readFile(path.resolve(config.generatedRouteTree), 'utf-8')
|
|
853
|
-
.catch((err) => {
|
|
854
|
-
if (err.code === 'ENOENT') {
|
|
855
|
-
return ''
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
throw err
|
|
859
|
-
})
|
|
860
|
-
|
|
861
|
-
if (!checkLatest()) return
|
|
862
|
-
|
|
863
|
-
// Ensure the directory exists
|
|
864
|
-
await fsp.mkdir(path.dirname(path.resolve(config.generatedRouteTree)), {
|
|
865
|
-
recursive: true,
|
|
866
|
-
})
|
|
867
|
-
|
|
868
|
-
if (!checkLatest()) return
|
|
869
|
-
|
|
870
|
-
// Write the route tree file, if it has changed
|
|
871
|
-
const routeTreeWriteResult = await writeIfDifferent(
|
|
872
|
-
path.resolve(config.generatedRouteTree),
|
|
873
|
-
config.enableRouteTreeFormatting
|
|
874
|
-
? await format(existingRouteTreeContent, config)
|
|
875
|
-
: existingRouteTreeContent,
|
|
876
|
-
config.enableRouteTreeFormatting
|
|
877
|
-
? await format(routeConfigFileContent, config)
|
|
878
|
-
: routeConfigFileContent,
|
|
879
|
-
{
|
|
880
|
-
beforeWrite: () => {
|
|
881
|
-
logger.log(`🟡 Updating ${config.generatedRouteTree}`)
|
|
882
|
-
},
|
|
883
|
-
},
|
|
884
|
-
)
|
|
885
|
-
if (routeTreeWriteResult && !checkLatest()) {
|
|
886
|
-
return
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
logger.log(
|
|
890
|
-
`✅ Processed ${routeNodes.length === 1 ? 'route' : 'routes'} in ${
|
|
891
|
-
Date.now() - start
|
|
892
|
-
}ms`,
|
|
893
|
-
)
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
// function removeTrailingUnderscores(s?: string) {
|
|
897
|
-
// return s?.replaceAll(/(_$)/gi, '').replaceAll(/(_\/)/gi, '/')
|
|
898
|
-
// }
|
|
899
|
-
|
|
900
|
-
function removeGroups(s: string) {
|
|
901
|
-
return s.replace(possiblyNestedRouteGroupPatternRegex, '')
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
/**
|
|
905
|
-
* Checks if a given RouteNode is valid for augmenting it with typing based on conditions.
|
|
906
|
-
* Also asserts that the RouteNode is defined.
|
|
907
|
-
*
|
|
908
|
-
* @param routeNode - The RouteNode to check.
|
|
909
|
-
* @returns A boolean indicating whether the RouteNode is defined.
|
|
910
|
-
*/
|
|
911
|
-
function isRouteNodeValidForAugmentation(
|
|
912
|
-
routeNode?: RouteNode,
|
|
913
|
-
): routeNode is RouteNode {
|
|
914
|
-
if (!routeNode || routeNode.isVirtual) {
|
|
915
|
-
return false
|
|
916
|
-
}
|
|
917
|
-
return true
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
/**
|
|
921
|
-
* The `node.path` is used as the `id` in the route definition.
|
|
922
|
-
* This function checks if the given node has a parent and if so, it determines the correct path for the given node.
|
|
923
|
-
* @param node - The node to determine the path for.
|
|
924
|
-
* @returns The correct path for the given node.
|
|
925
|
-
*/
|
|
926
|
-
function determineNodePath(node: RouteNode) {
|
|
927
|
-
return (node.path = node.parent
|
|
928
|
-
? node.routePath?.replace(node.parent.routePath ?? '', '') || '/'
|
|
929
|
-
: node.routePath)
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
/**
|
|
933
|
-
* Removes the last segment from a given path. Segments are considered to be separated by a '/'.
|
|
934
|
-
*
|
|
935
|
-
* @param {string} routePath - The path from which to remove the last segment. Defaults to '/'.
|
|
936
|
-
* @returns {string} The path with the last segment removed.
|
|
937
|
-
* @example
|
|
938
|
-
* removeLastSegmentFromPath('/workspace/_auth/foo') // '/workspace/_auth'
|
|
939
|
-
*/
|
|
940
|
-
function removeLastSegmentFromPath(routePath: string = '/'): string {
|
|
941
|
-
const segments = routePath.split('/')
|
|
942
|
-
segments.pop() // Remove the last segment
|
|
943
|
-
return segments.join('/')
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
/**
|
|
947
|
-
* Removes all segments from a given path that start with an underscore ('_').
|
|
948
|
-
*
|
|
949
|
-
* @param {string} routePath - The path from which to remove segments. Defaults to '/'.
|
|
950
|
-
* @returns {string} The path with all underscore-prefixed segments removed.
|
|
951
|
-
* @example
|
|
952
|
-
* removeLayoutSegments('/workspace/_auth/foo') // '/workspace/foo'
|
|
953
|
-
*/
|
|
954
|
-
function removeLayoutSegments(routePath: string = '/'): string {
|
|
955
|
-
const segments = routePath.split('/')
|
|
956
|
-
const newSegments = segments.filter((segment) => !segment.startsWith('_'))
|
|
957
|
-
return newSegments.join('/')
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
function hasParentRoute(
|
|
961
|
-
routes: Array<RouteNode>,
|
|
962
|
-
node: RouteNode,
|
|
963
|
-
routePathToCheck: string | undefined,
|
|
964
|
-
): RouteNode | null {
|
|
965
|
-
if (!routePathToCheck || routePathToCheck === '/') {
|
|
966
|
-
return null
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
const sortedNodes = multiSortBy(routes, [
|
|
970
|
-
(d) => d.routePath!.length * -1,
|
|
971
|
-
(d) => d.variableName,
|
|
972
|
-
]).filter((d) => d.routePath !== `/${rootPathId}`)
|
|
973
|
-
|
|
974
|
-
for (const route of sortedNodes) {
|
|
975
|
-
if (route.routePath === '/') continue
|
|
976
|
-
|
|
977
|
-
if (
|
|
978
|
-
routePathToCheck.startsWith(`${route.routePath}/`) &&
|
|
979
|
-
route.routePath !== routePathToCheck
|
|
980
|
-
) {
|
|
981
|
-
return route
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
const segments = routePathToCheck.split('/')
|
|
986
|
-
segments.pop() // Remove the last segment
|
|
987
|
-
const parentRoutePath = segments.join('/')
|
|
988
|
-
|
|
989
|
-
return hasParentRoute(routes, node, parentRoutePath)
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
/**
|
|
993
|
-
* Gets the final variable name for a route
|
|
994
|
-
*/
|
|
995
|
-
const getResolvedRouteNodeVariableName = (routeNode: RouteNode): string => {
|
|
996
|
-
return routeNode.children?.length
|
|
997
|
-
? `${routeNode.variableName}RouteWithChildren`
|
|
998
|
-
: `${routeNode.variableName}Route`
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
/**
|
|
1002
|
-
* Creates a map from fullPath to routeNode
|
|
1003
|
-
*/
|
|
1004
|
-
const createRouteNodesByFullPath = (
|
|
1005
|
-
routeNodes: Array<RouteNode>,
|
|
1006
|
-
): Map<string, RouteNode> => {
|
|
1007
|
-
return new Map(
|
|
1008
|
-
routeNodes.map((routeNode) => [inferFullPath(routeNode), routeNode]),
|
|
1009
|
-
)
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
/**
|
|
1013
|
-
* Create a map from 'to' to a routeNode
|
|
1014
|
-
*/
|
|
1015
|
-
const createRouteNodesByTo = (
|
|
1016
|
-
routeNodes: Array<RouteNode>,
|
|
1017
|
-
): Map<string, RouteNode> => {
|
|
1018
|
-
return new Map(
|
|
1019
|
-
dedupeBranchesAndIndexRoutes(routeNodes).map((routeNode) => [
|
|
1020
|
-
inferTo(routeNode),
|
|
1021
|
-
routeNode,
|
|
1022
|
-
]),
|
|
1023
|
-
)
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
/**
|
|
1027
|
-
* Create a map from 'id' to a routeNode
|
|
1028
|
-
*/
|
|
1029
|
-
const createRouteNodesById = (
|
|
1030
|
-
routeNodes: Array<RouteNode>,
|
|
1031
|
-
): Map<string, RouteNode> => {
|
|
1032
|
-
return new Map(
|
|
1033
|
-
routeNodes.map((routeNode) => {
|
|
1034
|
-
const id = routeNode.routePath ?? ''
|
|
1035
|
-
return [id, routeNode]
|
|
1036
|
-
}),
|
|
1037
|
-
)
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
/**
|
|
1041
|
-
* Infers the full path for use by TS
|
|
1042
|
-
*/
|
|
1043
|
-
const inferFullPath = (routeNode: RouteNode): string => {
|
|
1044
|
-
const fullPath = removeGroups(
|
|
1045
|
-
removeUnderscores(removeLayoutSegments(routeNode.routePath)) ?? '',
|
|
1046
|
-
)
|
|
1047
|
-
|
|
1048
|
-
return routeNode.cleanedPath === '/' ? fullPath : fullPath.replace(/\/$/, '')
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
/**
|
|
1052
|
-
* Infers the path for use by TS
|
|
1053
|
-
*/
|
|
1054
|
-
const inferPath = (routeNode: RouteNode): string => {
|
|
1055
|
-
return routeNode.cleanedPath === '/'
|
|
1056
|
-
? routeNode.cleanedPath
|
|
1057
|
-
: (routeNode.cleanedPath?.replace(/\/$/, '') ?? '')
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
/**
|
|
1061
|
-
* Infers to path
|
|
1062
|
-
*/
|
|
1063
|
-
const inferTo = (routeNode: RouteNode): string => {
|
|
1064
|
-
const fullPath = inferFullPath(routeNode)
|
|
1065
|
-
|
|
1066
|
-
if (fullPath === '/') return fullPath
|
|
1067
|
-
|
|
1068
|
-
return fullPath.replace(/\/$/, '')
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
/**
|
|
1072
|
-
* Dedupes branches and index routes
|
|
1073
|
-
*/
|
|
1074
|
-
const dedupeBranchesAndIndexRoutes = (
|
|
1075
|
-
routes: Array<RouteNode>,
|
|
1076
|
-
): Array<RouteNode> => {
|
|
1077
|
-
return routes.filter((route) => {
|
|
1078
|
-
if (route.children?.find((child) => child.cleanedPath === '/')) return false
|
|
1079
|
-
return true
|
|
1080
|
-
})
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
function checkUnique<TElement>(routes: Array<TElement>, key: keyof TElement) {
|
|
1084
|
-
// Check no two routes have the same `key`
|
|
1085
|
-
// if they do, throw an error with the conflicting filePaths
|
|
1086
|
-
const keys = routes.map((d) => d[key])
|
|
1087
|
-
const uniqueKeys = new Set(keys)
|
|
1088
|
-
if (keys.length !== uniqueKeys.size) {
|
|
1089
|
-
const duplicateKeys = keys.filter((d, i) => keys.indexOf(d) !== i)
|
|
1090
|
-
const conflictingFiles = routes.filter((d) =>
|
|
1091
|
-
duplicateKeys.includes(d[key]),
|
|
1092
|
-
)
|
|
1093
|
-
return conflictingFiles
|
|
1094
|
-
}
|
|
1095
|
-
return undefined
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
function checkRouteFullPathUniqueness(
|
|
1099
|
-
_routes: Array<RouteNode>,
|
|
1100
|
-
config: Config,
|
|
1101
|
-
) {
|
|
1102
|
-
const routes = _routes.map((d) => {
|
|
1103
|
-
const inferredFullPath = inferFullPath(d)
|
|
1104
|
-
return { ...d, inferredFullPath }
|
|
1105
|
-
})
|
|
1106
|
-
|
|
1107
|
-
const conflictingFiles = checkUnique(routes, 'inferredFullPath')
|
|
1108
|
-
|
|
1109
|
-
if (conflictingFiles !== undefined) {
|
|
1110
|
-
const errorMessage = `Conflicting configuration paths were found for the following route${conflictingFiles.length > 1 ? 's' : ''}: ${conflictingFiles
|
|
1111
|
-
.map((p) => `"${p.inferredFullPath}"`)
|
|
1112
|
-
.join(', ')}.
|
|
1113
|
-
Please ensure each Route has a unique full path.
|
|
1114
|
-
Conflicting files: \n ${conflictingFiles.map((d) => path.resolve(config.routesDirectory, d.filePath)).join('\n ')}\n`
|
|
1115
|
-
throw new Error(errorMessage)
|
|
1293
|
+
acc.routeNodes.push(node)
|
|
1116
1294
|
}
|
|
1117
1295
|
}
|