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