@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/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
- console.warn(
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
- console.log('')
161
+ const logger = logging({ disabled: config.disableLogging })
162
+ logger.log('')
154
163
 
155
164
  if (!first) {
156
- console.log('♻️ Generating routes...')
165
+ logger.log('♻️ Generating routes...')
157
166
  first = true
158
167
  } else if (skipMessage) {
159
168
  skipMessage = false
160
169
  } else {
161
- console.log('♻️ Regenerating routes...')
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
- const parentRoute = hasParentRoute(routeNodes, node.routePath)
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 = first.startsWith('_')
241
+ node.isNonPath = lastRouteSegment.startsWith('_')
219
242
  node.isNonLayout = first.endsWith('_')
220
- node.cleanedPath = removeGroups(removeUnderscores(node.path) ?? '')
221
243
 
222
- // Ensure the boilerplate for the route exists
223
- const routeCode = fs.readFileSync(node.fullPath, 'utf-8')
224
-
225
- const escapedRoutePath = removeTrailingUnderscores(
226
- node.routePath?.replaceAll('$', '$$') ?? '',
244
+ node.cleanedPath = removeGroups(
245
+ removeUnderscores(removeLayoutSegments(node.path)) ?? '',
227
246
  )
228
247
 
229
- let replaced = routeCode
230
-
231
- if (!routeCode) {
232
- if (node.isLazy) {
233
- replaced = [
234
- `import { createLazyFileRoute } from '@tanstack/react-router'`,
235
- `export const Route = createLazyFileRoute('${escapedRoutePath}')({
236
- component: () => <div>Hello ${escapedRoutePath}!</div>
237
- })`,
238
- ].join('\n\n')
239
- } else if (
240
- node.isRoute ||
241
- (!node.isComponent &&
242
- !node.isErrorComponent &&
243
- !node.isPendingComponent &&
244
- !node.isLoader)
245
- ) {
246
- replaced = [
247
- `import { createFileRoute } from '@tanstack/react-router'`,
248
- `export const Route = createFileRoute('${escapedRoutePath}')({
249
- component: () => <div>Hello ${escapedRoutePath}!</div>
250
- })`,
251
- ].join('\n\n')
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
- if (replaced !== routeCode) {
270
- console.log(`🟡 Updating ${node.fullPath}`)
271
- await fsp.writeFile(node.fullPath, replaced)
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
- node.parent.children = node.parent.children ?? []
315
- node.parent.children.push(node)
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.parent?.variableName
512
- ? `${routeNode.parent?.variableName}Import`
513
- : 'rootRoute'
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
- console.log(
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
+ }