@tanstack/router-generator 1.19.0 → 1.19.3
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/generator.cjs +148 -50
- package/dist/cjs/generator.cjs.map +1 -1
- package/dist/cjs/generator.d.cts +13 -1
- package/dist/esm/generator.d.ts +13 -1
- package/dist/esm/generator.js +148 -50
- package/dist/esm/generator.js.map +1 -1
- package/package.json +1 -1
- package/src/generator.ts +216 -61
package/src/generator.ts
CHANGED
|
@@ -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
|
|
@@ -89,6 +92,9 @@ async function getRouteNodes(config: Config) {
|
|
|
89
92
|
let isPendingComponent = routePath?.endsWith('/pendingComponent')
|
|
90
93
|
let isLoader = routePath?.endsWith('/loader')
|
|
91
94
|
|
|
95
|
+
const segments = (routePath ?? '').split('/')
|
|
96
|
+
let isLayout = segments[segments.length - 1]?.startsWith('_') || false
|
|
97
|
+
|
|
92
98
|
;(
|
|
93
99
|
[
|
|
94
100
|
[isComponent, 'component'],
|
|
@@ -126,6 +132,7 @@ async function getRouteNodes(config: Config) {
|
|
|
126
132
|
isPendingComponent,
|
|
127
133
|
isLoader,
|
|
128
134
|
isLazy,
|
|
135
|
+
isLayout,
|
|
129
136
|
})
|
|
130
137
|
}
|
|
131
138
|
}),
|
|
@@ -205,72 +212,90 @@ export async function generator(config: Config) {
|
|
|
205
212
|
let routeNodes: RouteNode[] = []
|
|
206
213
|
|
|
207
214
|
const handleNode = async (node: RouteNode) => {
|
|
208
|
-
|
|
215
|
+
let parentRoute = hasParentRoute(routeNodes, node, 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,
|
|
223
|
+
node.routePath,
|
|
224
|
+
)
|
|
225
|
+
if (possibleParentRoute) {
|
|
226
|
+
parentRoute = possibleParentRoute
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
209
230
|
if (parentRoute) node.parent = parentRoute
|
|
210
231
|
|
|
211
|
-
node.path = node
|
|
212
|
-
? node.routePath?.replace(node.parent.routePath!, '') || '/'
|
|
213
|
-
: node.routePath
|
|
232
|
+
node.path = determineNodePath(node)
|
|
214
233
|
|
|
215
234
|
const trimmedPath = trimPathLeft(node.path ?? '')
|
|
216
235
|
|
|
217
236
|
const split = trimmedPath?.split('/') ?? []
|
|
218
237
|
let first = split[0] ?? trimmedPath ?? ''
|
|
238
|
+
const lastRouteSegment = split[split.length - 1] ?? trimmedPath ?? ''
|
|
219
239
|
|
|
220
|
-
node.isNonPath =
|
|
240
|
+
node.isNonPath = lastRouteSegment.startsWith('_')
|
|
221
241
|
node.isNonLayout = first.endsWith('_')
|
|
222
|
-
node.cleanedPath = removeGroups(removeUnderscores(node.path) ?? '')
|
|
223
|
-
|
|
224
|
-
// Ensure the boilerplate for the route exists
|
|
225
|
-
const routeCode = fs.readFileSync(node.fullPath, 'utf-8')
|
|
226
242
|
|
|
227
|
-
|
|
228
|
-
node.
|
|
243
|
+
node.cleanedPath = removeGroups(
|
|
244
|
+
removeUnderscores(removeLayoutSegments(node.path)) ?? '',
|
|
229
245
|
)
|
|
230
246
|
|
|
231
|
-
|
|
247
|
+
// Ensure the boilerplate for the route exists, which can be skipped for virtual parent routes
|
|
248
|
+
if (!node.isVirtualParentRoute) {
|
|
249
|
+
const routeCode = fs.readFileSync(node.fullPath, 'utf-8')
|
|
250
|
+
|
|
251
|
+
const escapedRoutePath = removeTrailingUnderscores(
|
|
252
|
+
node.routePath?.replaceAll('$', '$$') ?? '',
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
let replaced = routeCode
|
|
232
256
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
257
|
+
if (!routeCode) {
|
|
258
|
+
if (node.isLazy) {
|
|
259
|
+
replaced = [
|
|
260
|
+
`import { createLazyFileRoute } from '@tanstack/react-router'`,
|
|
261
|
+
`export const Route = createLazyFileRoute('${escapedRoutePath}')({
|
|
238
262
|
component: () => <div>Hello ${escapedRoutePath}!</div>
|
|
239
263
|
})`,
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
264
|
+
].join('\n\n')
|
|
265
|
+
} else if (
|
|
266
|
+
node.isRoute ||
|
|
267
|
+
(!node.isComponent &&
|
|
268
|
+
!node.isErrorComponent &&
|
|
269
|
+
!node.isPendingComponent &&
|
|
270
|
+
!node.isLoader)
|
|
271
|
+
) {
|
|
272
|
+
replaced = [
|
|
273
|
+
`import { createFileRoute } from '@tanstack/react-router'`,
|
|
274
|
+
`export const Route = createFileRoute('${escapedRoutePath}')({
|
|
251
275
|
component: () => <div>Hello ${escapedRoutePath}!</div>
|
|
252
276
|
})`,
|
|
253
|
-
|
|
277
|
+
].join('\n\n')
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
replaced = routeCode
|
|
281
|
+
.replace(
|
|
282
|
+
/(FileRoute\(\s*['"])([^\s]*)(['"],?\s*\))/g,
|
|
283
|
+
(match, p1, p2, p3) => `${p1}${escapedRoutePath}${p3}`,
|
|
284
|
+
)
|
|
285
|
+
.replace(
|
|
286
|
+
/(createFileRoute\(\s*['"])([^\s]*)(['"],?\s*\))/g,
|
|
287
|
+
(match, p1, p2, p3) => `${p1}${escapedRoutePath}${p3}`,
|
|
288
|
+
)
|
|
289
|
+
.replace(
|
|
290
|
+
/(createLazyFileRoute\(\s*['"])([^\s]*)(['"],?\s*\))/g,
|
|
291
|
+
(match, p1, p2, p3) => `${p1}${escapedRoutePath}${p3}`,
|
|
292
|
+
)
|
|
254
293
|
}
|
|
255
|
-
} else {
|
|
256
|
-
replaced = routeCode
|
|
257
|
-
.replace(
|
|
258
|
-
/(FileRoute\(\s*['"])([^\s]*)(['"],?\s*\))/g,
|
|
259
|
-
(match, p1, p2, p3) => `${p1}${escapedRoutePath}${p3}`,
|
|
260
|
-
)
|
|
261
|
-
.replace(
|
|
262
|
-
/(createFileRoute\(\s*['"])([^\s]*)(['"],?\s*\))/g,
|
|
263
|
-
(match, p1, p2, p3) => `${p1}${escapedRoutePath}${p3}`,
|
|
264
|
-
)
|
|
265
|
-
.replace(
|
|
266
|
-
/(createLazyFileRoute\(\s*['"])([^\s]*)(['"],?\s*\))/g,
|
|
267
|
-
(match, p1, p2, p3) => `${p1}${escapedRoutePath}${p3}`,
|
|
268
|
-
)
|
|
269
|
-
}
|
|
270
294
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
295
|
+
if (replaced !== routeCode) {
|
|
296
|
+
logger.log(`🟡 Updating ${node.fullPath}`)
|
|
297
|
+
await fsp.writeFile(node.fullPath, replaced)
|
|
298
|
+
}
|
|
274
299
|
}
|
|
275
300
|
|
|
276
301
|
if (
|
|
@@ -312,9 +337,58 @@ export async function generator(config: Config) {
|
|
|
312
337
|
return
|
|
313
338
|
}
|
|
314
339
|
|
|
340
|
+
const cleanedPathIsEmpty = (node.cleanedPath || '').length === 0
|
|
341
|
+
|
|
342
|
+
node.isVirtualParentRequired = node.isLayout
|
|
343
|
+
? !cleanedPathIsEmpty && !node.parent
|
|
344
|
+
: false
|
|
345
|
+
|
|
346
|
+
if (!node.isVirtual && node.isVirtualParentRequired) {
|
|
347
|
+
const parentRoutePath = removeLastSegmentFromPath(node.routePath) || '/'
|
|
348
|
+
const parentVariableName = routePathToVariable(parentRoutePath)
|
|
349
|
+
|
|
350
|
+
const anchorRoute = routeNodes.find(
|
|
351
|
+
(d) => d.routePath === parentRoutePath,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
if (!anchorRoute) {
|
|
355
|
+
const parentNode = {
|
|
356
|
+
...node,
|
|
357
|
+
path: removeLastSegmentFromPath(node.path) || '/',
|
|
358
|
+
filePath: removeLastSegmentFromPath(node.filePath) || '/',
|
|
359
|
+
fullPath: removeLastSegmentFromPath(node.fullPath) || '/',
|
|
360
|
+
routePath: parentRoutePath,
|
|
361
|
+
variableName: parentVariableName,
|
|
362
|
+
isVirtual: true,
|
|
363
|
+
isLayout: false,
|
|
364
|
+
isVirtualParentRoute: true,
|
|
365
|
+
isVirtualParentRequired: false,
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
parentNode.children = parentNode.children ?? []
|
|
369
|
+
parentNode.children.push(node)
|
|
370
|
+
|
|
371
|
+
node.parent = parentNode
|
|
372
|
+
|
|
373
|
+
if (node.isLayout) {
|
|
374
|
+
// since `node.path` is used as the `id` on the route definition, we need to update it
|
|
375
|
+
node.path = determineNodePath(node)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
await handleNode(parentNode)
|
|
379
|
+
} else {
|
|
380
|
+
anchorRoute.children = anchorRoute.children ?? []
|
|
381
|
+
anchorRoute.children.push(node)
|
|
382
|
+
|
|
383
|
+
node.parent = anchorRoute
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
315
387
|
if (node.parent) {
|
|
316
|
-
|
|
317
|
-
|
|
388
|
+
if (!node.isVirtualParentRequired) {
|
|
389
|
+
node.parent.children = node.parent.children ?? []
|
|
390
|
+
node.parent.children.push(node)
|
|
391
|
+
}
|
|
318
392
|
} else {
|
|
319
393
|
routeTree.push(node)
|
|
320
394
|
}
|
|
@@ -333,6 +407,10 @@ export async function generator(config: Config) {
|
|
|
333
407
|
return
|
|
334
408
|
}
|
|
335
409
|
|
|
410
|
+
if (node.isLayout && !node.children?.length) {
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
|
|
336
414
|
const route = `${node.variableName}Route`
|
|
337
415
|
|
|
338
416
|
if (node.children?.length) {
|
|
@@ -370,7 +448,20 @@ export async function generator(config: Config) {
|
|
|
370
448
|
.filter((d) => d[1])
|
|
371
449
|
.map((d) => d[0])
|
|
372
450
|
|
|
373
|
-
const virtualRouteNodes = sortedRouteNodes
|
|
451
|
+
const virtualRouteNodes = sortedRouteNodes
|
|
452
|
+
.filter((d) => d.isVirtual)
|
|
453
|
+
.reduce((acc, route) => {
|
|
454
|
+
// ensuring we don't have any duplicated virtual routes or clashes with pre-existing routes
|
|
455
|
+
const existingPreNode = preRouteNodes.filter(
|
|
456
|
+
(d) => d.routePath === route.routePath,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
if (existingPreNode.length === 0) {
|
|
460
|
+
acc.push(route)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return acc
|
|
464
|
+
}, [] as RouteNode[])
|
|
374
465
|
|
|
375
466
|
const rootPathIdExtension =
|
|
376
467
|
config.addExtensions && rootRouteNode
|
|
@@ -425,6 +516,14 @@ export async function generator(config: Config) {
|
|
|
425
516
|
.join('\n'),
|
|
426
517
|
'// Create/Update Routes',
|
|
427
518
|
sortedRouteNodes
|
|
519
|
+
.reduce((acc, node) => {
|
|
520
|
+
// ensuring we update a unique route only once
|
|
521
|
+
if (acc.find((d) => d.routePath === node.routePath)) {
|
|
522
|
+
return acc
|
|
523
|
+
}
|
|
524
|
+
acc.push(node)
|
|
525
|
+
return acc
|
|
526
|
+
}, [] as RouteNode[])
|
|
428
527
|
.map((node) => {
|
|
429
528
|
const loaderNode = routePiecesByPath[node.routePath!]?.loader
|
|
430
529
|
const componentNode = routePiecesByPath[node.routePath!]?.component
|
|
@@ -510,9 +609,11 @@ export async function generator(config: Config) {
|
|
|
510
609
|
return `'${removeTrailingUnderscores(routeNode.routePath)}': {
|
|
511
610
|
preLoaderRoute: typeof ${routeNode.variableName}Import
|
|
512
611
|
parentRoute: typeof ${
|
|
513
|
-
routeNode.
|
|
514
|
-
? `${routeNode.parent?.variableName}
|
|
515
|
-
:
|
|
612
|
+
routeNode.isVirtualParentRequired
|
|
613
|
+
? `${routeNode.parent?.variableName}Route`
|
|
614
|
+
: routeNode.parent?.variableName
|
|
615
|
+
? `${routeNode.parent?.variableName}Import`
|
|
616
|
+
: 'rootRoute'
|
|
516
617
|
}
|
|
517
618
|
}`
|
|
518
619
|
})
|
|
@@ -637,8 +738,49 @@ function removeGroups(s: string) {
|
|
|
637
738
|
return s.replaceAll(routeGroupPatternRegex, '').replaceAll('//', '/')
|
|
638
739
|
}
|
|
639
740
|
|
|
741
|
+
/**
|
|
742
|
+
* The `node.path` is used as the `id` in the route definition.
|
|
743
|
+
* This function checks if the given node has a parent and if so, it determines the correct path for the given node.
|
|
744
|
+
* @param node - The node to determine the path for.
|
|
745
|
+
* @returns The correct path for the given node.
|
|
746
|
+
*/
|
|
747
|
+
function determineNodePath(node: RouteNode) {
|
|
748
|
+
return (node.path = node.parent
|
|
749
|
+
? node.routePath?.replace(node.parent.routePath!, '') || '/'
|
|
750
|
+
: node.routePath)
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Removes the last segment from a given path. Segments are considered to be separated by a '/'.
|
|
755
|
+
*
|
|
756
|
+
* @param {string} path - The path from which to remove the last segment. Defaults to '/'.
|
|
757
|
+
* @returns {string} The path with the last segment removed.
|
|
758
|
+
* @example
|
|
759
|
+
* removeLastSegmentFromPath('/workspace/_auth/foo') // '/workspace/_auth'
|
|
760
|
+
*/
|
|
761
|
+
export function removeLastSegmentFromPath(path: string = '/'): string {
|
|
762
|
+
const segments = path.split('/')
|
|
763
|
+
segments.pop() // Remove the last segment
|
|
764
|
+
return segments.join('/')
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Removes all segments from a given path that start with an underscore ('_').
|
|
769
|
+
*
|
|
770
|
+
* @param {string} path - The path from which to remove segments. Defaults to '/'.
|
|
771
|
+
* @returns {string} The path with all underscore-prefixed segments removed.
|
|
772
|
+
* @example
|
|
773
|
+
* removeLayoutSegments('/workspace/_auth/foo') // '/workspace/foo'
|
|
774
|
+
*/
|
|
775
|
+
function removeLayoutSegments(path: string = '/'): string {
|
|
776
|
+
const segments = path.split('/')
|
|
777
|
+
const newSegments = segments.filter((segment) => !segment.startsWith('_'))
|
|
778
|
+
return newSegments.join('/')
|
|
779
|
+
}
|
|
780
|
+
|
|
640
781
|
export function hasParentRoute(
|
|
641
782
|
routes: RouteNode[],
|
|
783
|
+
node: RouteNode,
|
|
642
784
|
routePathToCheck: string | undefined,
|
|
643
785
|
): RouteNode | null {
|
|
644
786
|
if (!routePathToCheck || routePathToCheck === '/') {
|
|
@@ -650,19 +792,32 @@ export function hasParentRoute(
|
|
|
650
792
|
(d) => d.variableName,
|
|
651
793
|
]).filter((d) => d.routePath !== `/${rootPathId}`)
|
|
652
794
|
|
|
653
|
-
|
|
654
|
-
|
|
795
|
+
if (node.isLayout) {
|
|
796
|
+
for (const route of sortedNodes) {
|
|
797
|
+
if (route.routePath === '/') continue
|
|
655
798
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
route.routePath
|
|
659
|
-
|
|
660
|
-
|
|
799
|
+
// a layout route's parent has to exactly match the layout route minus, the layout segment
|
|
800
|
+
const exactParentRoutePath = removeLastSegmentFromPath(node.routePath)
|
|
801
|
+
if (route.routePath === exactParentRoutePath) {
|
|
802
|
+
return route
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
} else {
|
|
806
|
+
for (const route of sortedNodes) {
|
|
807
|
+
if (route.routePath === '/') continue
|
|
808
|
+
|
|
809
|
+
if (
|
|
810
|
+
routePathToCheck.startsWith(`${route.routePath}/`) &&
|
|
811
|
+
route.routePath !== routePathToCheck
|
|
812
|
+
) {
|
|
813
|
+
return route
|
|
814
|
+
}
|
|
661
815
|
}
|
|
662
816
|
}
|
|
817
|
+
|
|
663
818
|
const segments = routePathToCheck.split('/')
|
|
664
819
|
segments.pop() // Remove the last segment
|
|
665
820
|
const parentRoutePath = segments.join('/')
|
|
666
821
|
|
|
667
|
-
return hasParentRoute(routes, parentRoutePath)
|
|
822
|
+
return hasParentRoute(routes, node, parentRoutePath)
|
|
668
823
|
}
|