@tanstack/router-generator 1.18.1 → 1.19.2
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 +2 -1
- package/dist/cjs/config.cjs.map +1 -1
- package/dist/cjs/config.d.cts +3 -0
- package/dist/cjs/generator.cjs +112 -49
- package/dist/cjs/generator.cjs.map +1 -1
- package/dist/cjs/generator.d.cts +12 -0
- package/dist/cjs/utils.cjs +25 -0
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +9 -0
- package/dist/esm/config.d.ts +3 -0
- package/dist/esm/config.js +2 -1
- package/dist/esm/config.js.map +1 -1
- package/dist/esm/generator.d.ts +12 -0
- package/dist/esm/generator.js +113 -50
- package/dist/esm/generator.js.map +1 -1
- package/dist/esm/utils.d.ts +9 -0
- package/dist/esm/utils.js +25 -0
- package/dist/esm/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +1 -0
- package/src/generator.ts +162 -60
- package/src/utils.ts +20 -0
package/src/generator.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as fs from 'fs'
|
|
|
3
3
|
import * as fsp from 'fs/promises'
|
|
4
4
|
import * as prettier from 'prettier'
|
|
5
5
|
import { Config } from './config'
|
|
6
|
-
import { cleanPath, trimPathLeft } from './utils'
|
|
6
|
+
import { cleanPath, logging, trimPathLeft } from './utils'
|
|
7
7
|
|
|
8
8
|
let latestTask = 0
|
|
9
9
|
export const rootPathId = '__root'
|
|
@@ -18,6 +18,9 @@ export type RouteNode = {
|
|
|
18
18
|
path?: string
|
|
19
19
|
isNonPath?: boolean
|
|
20
20
|
isNonLayout?: boolean
|
|
21
|
+
isLayout?: boolean
|
|
22
|
+
isVirtualParentRequired?: boolean
|
|
23
|
+
isVirtualParentRoute?: boolean
|
|
21
24
|
isRoute?: boolean
|
|
22
25
|
isLoader?: boolean
|
|
23
26
|
isComponent?: boolean
|
|
@@ -32,6 +35,7 @@ export type RouteNode = {
|
|
|
32
35
|
|
|
33
36
|
async function getRouteNodes(config: Config) {
|
|
34
37
|
const { routeFilePrefix, routeFileIgnorePrefix } = config
|
|
38
|
+
const logger = logging({ disabled: config.disableLogging })
|
|
35
39
|
|
|
36
40
|
let routeNodes: RouteNode[] = []
|
|
37
41
|
|
|
@@ -88,6 +92,9 @@ async function getRouteNodes(config: Config) {
|
|
|
88
92
|
let isPendingComponent = routePath?.endsWith('/pendingComponent')
|
|
89
93
|
let isLoader = routePath?.endsWith('/loader')
|
|
90
94
|
|
|
95
|
+
const segments = (routePath ?? '').split('/')
|
|
96
|
+
let isLayout = segments[segments.length - 1]?.startsWith('_') || false
|
|
97
|
+
|
|
91
98
|
;(
|
|
92
99
|
[
|
|
93
100
|
[isComponent, 'component'],
|
|
@@ -97,7 +104,7 @@ async function getRouteNodes(config: Config) {
|
|
|
97
104
|
] as const
|
|
98
105
|
).forEach(([isType, type]) => {
|
|
99
106
|
if (isType) {
|
|
100
|
-
|
|
107
|
+
logger.warn(
|
|
101
108
|
`WARNING: The \`.${type}.tsx\` suffix used for the ${filePath} file is deprecated. Use the new \`.lazy.tsx\` suffix instead.`,
|
|
102
109
|
)
|
|
103
110
|
}
|
|
@@ -125,6 +132,7 @@ async function getRouteNodes(config: Config) {
|
|
|
125
132
|
isPendingComponent,
|
|
126
133
|
isLoader,
|
|
127
134
|
isLazy,
|
|
135
|
+
isLayout,
|
|
128
136
|
})
|
|
129
137
|
}
|
|
130
138
|
}),
|
|
@@ -150,15 +158,16 @@ type RouteSubNode = {
|
|
|
150
158
|
}
|
|
151
159
|
|
|
152
160
|
export async function generator(config: Config) {
|
|
153
|
-
|
|
161
|
+
const logger = logging({ disabled: config.disableLogging })
|
|
162
|
+
logger.log('')
|
|
154
163
|
|
|
155
164
|
if (!first) {
|
|
156
|
-
|
|
165
|
+
logger.log('♻️ Generating routes...')
|
|
157
166
|
first = true
|
|
158
167
|
} else if (skipMessage) {
|
|
159
168
|
skipMessage = false
|
|
160
169
|
} else {
|
|
161
|
-
|
|
170
|
+
logger.log('♻️ Regenerating routes...')
|
|
162
171
|
}
|
|
163
172
|
|
|
164
173
|
const taskId = latestTask + 1
|
|
@@ -203,7 +212,20 @@ export async function generator(config: Config) {
|
|
|
203
212
|
let routeNodes: RouteNode[] = []
|
|
204
213
|
|
|
205
214
|
const handleNode = async (node: RouteNode) => {
|
|
206
|
-
|
|
215
|
+
let parentRoute = hasParentRoute(routeNodes, node.routePath)
|
|
216
|
+
|
|
217
|
+
// if the parent route is a virtual parent route, we need to find the real parent route
|
|
218
|
+
if (parentRoute?.isVirtualParentRoute && parentRoute.children?.length) {
|
|
219
|
+
// only if this sub-parent route returns a valid parent route, we use it, if not leave it as it
|
|
220
|
+
const possibleParentRoute = hasParentRoute(
|
|
221
|
+
parentRoute.children,
|
|
222
|
+
node.routePath,
|
|
223
|
+
)
|
|
224
|
+
if (possibleParentRoute) {
|
|
225
|
+
parentRoute = possibleParentRoute
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
207
229
|
if (parentRoute) node.parent = parentRoute
|
|
208
230
|
|
|
209
231
|
node.path = node.parent
|
|
@@ -214,61 +236,67 @@ export async function generator(config: Config) {
|
|
|
214
236
|
|
|
215
237
|
const split = trimmedPath?.split('/') ?? []
|
|
216
238
|
let first = split[0] ?? trimmedPath ?? ''
|
|
239
|
+
const lastRouteSegment = split[split.length - 1] ?? trimmedPath ?? ''
|
|
217
240
|
|
|
218
|
-
node.isNonPath =
|
|
241
|
+
node.isNonPath = lastRouteSegment.startsWith('_')
|
|
219
242
|
node.isNonLayout = first.endsWith('_')
|
|
220
|
-
node.cleanedPath = removeGroups(removeUnderscores(node.path) ?? '')
|
|
221
243
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const escapedRoutePath = removeTrailingUnderscores(
|
|
226
|
-
node.routePath?.replaceAll('$', '$$') ?? '',
|
|
244
|
+
node.cleanedPath = removeGroups(
|
|
245
|
+
removeUnderscores(removeLayoutSegments(node.path)) ?? '',
|
|
227
246
|
)
|
|
228
247
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
node.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
248
|
+
// Ensure the boilerplate for the route exists, which can be skipped for virtual parent routes
|
|
249
|
+
if (!node.isVirtualParentRoute) {
|
|
250
|
+
const routeCode = fs.readFileSync(node.fullPath, 'utf-8')
|
|
251
|
+
|
|
252
|
+
const escapedRoutePath = removeTrailingUnderscores(
|
|
253
|
+
node.routePath?.replaceAll('$', '$$') ?? '',
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
let replaced = routeCode
|
|
257
|
+
|
|
258
|
+
if (!routeCode) {
|
|
259
|
+
if (node.isLazy) {
|
|
260
|
+
replaced = [
|
|
261
|
+
`import { createLazyFileRoute } from '@tanstack/react-router'`,
|
|
262
|
+
`export const Route = createLazyFileRoute('${escapedRoutePath}')({
|
|
263
|
+
component: () => <div>Hello ${escapedRoutePath}!</div>
|
|
264
|
+
})`,
|
|
265
|
+
].join('\n\n')
|
|
266
|
+
} else if (
|
|
267
|
+
node.isRoute ||
|
|
268
|
+
(!node.isComponent &&
|
|
269
|
+
!node.isErrorComponent &&
|
|
270
|
+
!node.isPendingComponent &&
|
|
271
|
+
!node.isLoader)
|
|
272
|
+
) {
|
|
273
|
+
replaced = [
|
|
274
|
+
`import { createFileRoute } from '@tanstack/react-router'`,
|
|
275
|
+
`export const Route = createFileRoute('${escapedRoutePath}')({
|
|
276
|
+
component: () => <div>Hello ${escapedRoutePath}!</div>
|
|
277
|
+
})`,
|
|
278
|
+
].join('\n\n')
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
replaced = routeCode
|
|
282
|
+
.replace(
|
|
283
|
+
/(FileRoute\(\s*['"])([^\s]*)(['"],?\s*\))/g,
|
|
284
|
+
(match, p1, p2, p3) => `${p1}${escapedRoutePath}${p3}`,
|
|
285
|
+
)
|
|
286
|
+
.replace(
|
|
287
|
+
/(createFileRoute\(\s*['"])([^\s]*)(['"],?\s*\))/g,
|
|
288
|
+
(match, p1, p2, p3) => `${p1}${escapedRoutePath}${p3}`,
|
|
289
|
+
)
|
|
290
|
+
.replace(
|
|
291
|
+
/(createLazyFileRoute\(\s*['"])([^\s]*)(['"],?\s*\))/g,
|
|
292
|
+
(match, p1, p2, p3) => `${p1}${escapedRoutePath}${p3}`,
|
|
293
|
+
)
|
|
252
294
|
}
|
|
253
|
-
} else {
|
|
254
|
-
replaced = routeCode
|
|
255
|
-
.replace(
|
|
256
|
-
/(FileRoute\(\s*['"])([^\s]*)(['"],?\s*\))/g,
|
|
257
|
-
(match, p1, p2, p3) => `${p1}${escapedRoutePath}${p3}`,
|
|
258
|
-
)
|
|
259
|
-
.replace(
|
|
260
|
-
/(createFileRoute\(\s*['"])([^\s]*)(['"],?\s*\))/g,
|
|
261
|
-
(match, p1, p2, p3) => `${p1}${escapedRoutePath}${p3}`,
|
|
262
|
-
)
|
|
263
|
-
.replace(
|
|
264
|
-
/(createLazyFileRoute\(\s*['"])([^\s]*)(['"],?\s*\))/g,
|
|
265
|
-
(match, p1, p2, p3) => `${p1}${escapedRoutePath}${p3}`,
|
|
266
|
-
)
|
|
267
|
-
}
|
|
268
295
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
296
|
+
if (replaced !== routeCode) {
|
|
297
|
+
logger.log(`🟡 Updating ${node.fullPath}`)
|
|
298
|
+
await fsp.writeFile(node.fullPath, replaced)
|
|
299
|
+
}
|
|
272
300
|
}
|
|
273
301
|
|
|
274
302
|
if (
|
|
@@ -310,9 +338,53 @@ export async function generator(config: Config) {
|
|
|
310
338
|
return
|
|
311
339
|
}
|
|
312
340
|
|
|
341
|
+
node.isVirtualParentRequired =
|
|
342
|
+
removeGroups(node.path ?? '')
|
|
343
|
+
.split('')
|
|
344
|
+
.filter((char) => char === '/').length >= 2 && // check that the parent route wouldn't be the root route
|
|
345
|
+
!node.parent &&
|
|
346
|
+
node.isLayout
|
|
347
|
+
|
|
348
|
+
if (!node.isVirtual && node.isVirtualParentRequired) {
|
|
349
|
+
const parentRoutePath = removeLastSegmentFromPath(node.routePath) || '/'
|
|
350
|
+
|
|
351
|
+
const anchorRoute = routeNodes.find(
|
|
352
|
+
(d) => d.routePath === parentRoutePath,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
if (!anchorRoute) {
|
|
356
|
+
const parentNode = {
|
|
357
|
+
...node,
|
|
358
|
+
path: removeLastSegmentFromPath(node.path) || '/',
|
|
359
|
+
filePath: removeLastSegmentFromPath(node.filePath) || '/',
|
|
360
|
+
fullPath: removeLastSegmentFromPath(node.fullPath) || '/',
|
|
361
|
+
routePath: parentRoutePath,
|
|
362
|
+
variableName: routePathToVariable(parentRoutePath),
|
|
363
|
+
isVirtual: true,
|
|
364
|
+
isLayout: false,
|
|
365
|
+
isVirtualParentRoute: true,
|
|
366
|
+
isVirtualParentRequired: false,
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
parentNode.children = parentNode.children ?? []
|
|
370
|
+
parentNode.children.push(node)
|
|
371
|
+
|
|
372
|
+
node.parent = parentNode
|
|
373
|
+
|
|
374
|
+
await handleNode(parentNode)
|
|
375
|
+
} else {
|
|
376
|
+
anchorRoute.children = anchorRoute.children ?? []
|
|
377
|
+
anchorRoute.children.push(node)
|
|
378
|
+
|
|
379
|
+
node.parent = anchorRoute
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
313
383
|
if (node.parent) {
|
|
314
|
-
|
|
315
|
-
|
|
384
|
+
if (!node.isVirtualParentRequired) {
|
|
385
|
+
node.parent.children = node.parent.children ?? []
|
|
386
|
+
node.parent.children.push(node)
|
|
387
|
+
}
|
|
316
388
|
} else {
|
|
317
389
|
routeTree.push(node)
|
|
318
390
|
}
|
|
@@ -508,9 +580,11 @@ export async function generator(config: Config) {
|
|
|
508
580
|
return `'${removeTrailingUnderscores(routeNode.routePath)}': {
|
|
509
581
|
preLoaderRoute: typeof ${routeNode.variableName}Import
|
|
510
582
|
parentRoute: typeof ${
|
|
511
|
-
routeNode.
|
|
512
|
-
? `${routeNode.parent?.variableName}
|
|
513
|
-
:
|
|
583
|
+
routeNode.isVirtualParentRequired
|
|
584
|
+
? `${routeNode.parent?.variableName}Route`
|
|
585
|
+
: routeNode.parent?.variableName
|
|
586
|
+
? `${routeNode.parent?.variableName}Import`
|
|
587
|
+
: 'rootRoute'
|
|
514
588
|
}
|
|
515
589
|
}`
|
|
516
590
|
})
|
|
@@ -553,7 +627,7 @@ export async function generator(config: Config) {
|
|
|
553
627
|
)
|
|
554
628
|
}
|
|
555
629
|
|
|
556
|
-
|
|
630
|
+
logger.log(
|
|
557
631
|
`✅ Processed ${routeNodes.length === 1 ? 'route' : 'routes'} in ${
|
|
558
632
|
Date.now() - start
|
|
559
633
|
}ms`,
|
|
@@ -635,6 +709,34 @@ function removeGroups(s: string) {
|
|
|
635
709
|
return s.replaceAll(routeGroupPatternRegex, '').replaceAll('//', '/')
|
|
636
710
|
}
|
|
637
711
|
|
|
712
|
+
/**
|
|
713
|
+
* Removes the last segment from a given path. Segments are considered to be separated by a '/'.
|
|
714
|
+
*
|
|
715
|
+
* @param {string} path - The path from which to remove the last segment. Defaults to '/'.
|
|
716
|
+
* @returns {string} The path with the last segment removed.
|
|
717
|
+
* @example
|
|
718
|
+
* removeLastSegmentFromPath('/workspace/_auth/foo') // '/workspace/_auth'
|
|
719
|
+
*/
|
|
720
|
+
export function removeLastSegmentFromPath(path: string = '/'): string {
|
|
721
|
+
const segments = path.split('/')
|
|
722
|
+
segments.pop() // Remove the last segment
|
|
723
|
+
return segments.join('/')
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Removes all segments from a given path that start with an underscore ('_').
|
|
728
|
+
*
|
|
729
|
+
* @param {string} path - The path from which to remove segments. Defaults to '/'.
|
|
730
|
+
* @returns {string} The path with all underscore-prefixed segments removed.
|
|
731
|
+
* @example
|
|
732
|
+
* removeLayoutSegments('/workspace/_auth/foo') // '/workspace/foo'
|
|
733
|
+
*/
|
|
734
|
+
function removeLayoutSegments(path: string = '/'): string {
|
|
735
|
+
const segments = path.split('/')
|
|
736
|
+
const newSegments = segments.filter((segment) => !segment.startsWith('_'))
|
|
737
|
+
return newSegments.join('/')
|
|
738
|
+
}
|
|
739
|
+
|
|
638
740
|
export function hasParentRoute(
|
|
639
741
|
routes: RouteNode[],
|
|
640
742
|
routePathToCheck: string | undefined,
|
package/src/utils.ts
CHANGED
|
@@ -6,3 +6,23 @@ export function cleanPath(path: string) {
|
|
|
6
6
|
export function trimPathLeft(path: string) {
|
|
7
7
|
return path === '/' ? path : path.replace(/^\/{1,}/, '')
|
|
8
8
|
}
|
|
9
|
+
|
|
10
|
+
export function logging(config: { disabled: boolean }) {
|
|
11
|
+
return {
|
|
12
|
+
log: (...args: any[]) => {
|
|
13
|
+
if (!config.disabled) console.log(...args)
|
|
14
|
+
},
|
|
15
|
+
debug: (...args: any[]) => {
|
|
16
|
+
if (!config.disabled) console.debug(...args)
|
|
17
|
+
},
|
|
18
|
+
info: (...args: any[]) => {
|
|
19
|
+
if (!config.disabled) console.info(...args)
|
|
20
|
+
},
|
|
21
|
+
warn: (...args: any[]) => {
|
|
22
|
+
if (!config.disabled) console.warn(...args)
|
|
23
|
+
},
|
|
24
|
+
error: (...args: any[]) => {
|
|
25
|
+
if (!config.disabled) console.error(...args)
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
}
|