@tanstack/router-generator 1.139.14 → 1.140.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,7 +17,7 @@ import type {
17
17
  import type { FsRouteType, GetRouteNodesResult, RouteNode } from '../../types'
18
18
  import type { Config } from '../../config'
19
19
 
20
- const disallowedRouteGroupConfiguration = /\(([^)]+)\).(ts|js|tsx|jsx)/
20
+ const disallowedRouteGroupConfiguration = /\(([^)]+)\).(ts|js|tsx|jsx|vue)/
21
21
 
22
22
  const virtualConfigFileRegExp = /__virtual\.[mc]?[jt]s$/
23
23
  export function isVirtualConfigFile(fileName: string): boolean {
@@ -129,7 +129,7 @@ export async function getRouteNodes(
129
129
 
130
130
  if (dirent.isDirectory()) {
131
131
  await recurse(relativePath)
132
- } else if (fullPath.match(/\.(tsx|ts|jsx|js)$/)) {
132
+ } else if (fullPath.match(/\.(tsx|ts|jsx|js|vue)$/)) {
133
133
  const filePath = replaceBackslash(path.join(dir, dirent.name))
134
134
  const filePathNoExt = removeExt(filePath)
135
135
  const {
@@ -170,31 +170,37 @@ export async function getRouteNodes(
170
170
  routeType = 'pathless_layout'
171
171
  }
172
172
 
173
- ;(
174
- [
175
- ['component', 'component'],
176
- ['errorComponent', 'errorComponent'],
177
- ['pendingComponent', 'pendingComponent'],
178
- ['loader', 'loader'],
179
- ] satisfies Array<[FsRouteType, string]>
180
- ).forEach(([matcher, type]) => {
181
- if (routeType === matcher) {
182
- logger.warn(
183
- `WARNING: The \`.${type}.tsx\` suffix used for the ${filePath} file is deprecated. Use the new \`.lazy.tsx\` suffix instead.`,
184
- )
185
- }
186
- })
173
+ // Only show deprecation warning for .tsx/.ts files, not .vue files
174
+ // Vue files using .component.vue is the Vue-native way
175
+ const isVueFile = filePath.endsWith('.vue')
176
+ if (!isVueFile) {
177
+ ;(
178
+ [
179
+ ['component', 'component'],
180
+ ['errorComponent', 'errorComponent'],
181
+ ['notFoundComponent', 'notFoundComponent'],
182
+ ['pendingComponent', 'pendingComponent'],
183
+ ['loader', 'loader'],
184
+ ] satisfies Array<[FsRouteType, string]>
185
+ ).forEach(([matcher, type]) => {
186
+ if (routeType === matcher) {
187
+ logger.warn(
188
+ `WARNING: The \`.${type}.tsx\` suffix used for the ${filePath} file is deprecated. Use the new \`.lazy.tsx\` suffix instead.`,
189
+ )
190
+ }
191
+ })
192
+ }
187
193
 
188
194
  routePath = routePath.replace(
189
195
  new RegExp(
190
- `/(component|errorComponent|pendingComponent|loader|${config.routeToken}|lazy)$`,
196
+ `/(component|errorComponent|notFoundComponent|pendingComponent|loader|${config.routeToken}|lazy)$`,
191
197
  ),
192
198
  '',
193
199
  )
194
200
 
195
201
  originalRoutePath = originalRoutePath.replace(
196
202
  new RegExp(
197
- `/(component|errorComponent|pendingComponent|loader|${config.routeToken}|lazy)$`,
203
+ `/(component|errorComponent|notFoundComponent|pendingComponent|loader|${config.routeToken}|lazy)$`,
198
204
  ),
199
205
  '',
200
206
  )
@@ -234,7 +240,20 @@ export async function getRouteNodes(
234
240
 
235
241
  await recurse('./')
236
242
 
237
- const rootRouteNode = routeNodes.find((d) => d.routePath === `/${rootPathId}`)
243
+ // Find the root route node - prefer the actual route file over component/loader files
244
+ const rootRouteNode =
245
+ routeNodes.find(
246
+ (d) =>
247
+ d.routePath === `/${rootPathId}` &&
248
+ ![
249
+ 'component',
250
+ 'errorComponent',
251
+ 'notFoundComponent',
252
+ 'pendingComponent',
253
+ 'loader',
254
+ 'lazy',
255
+ ].includes(d._fsRouteType),
256
+ ) ?? routeNodes.find((d) => d.routePath === `/${rootPathId}`)
238
257
  if (rootRouteNode) {
239
258
  rootRouteNode._fsRouteType = '__root'
240
259
  rootRouteNode.variableName = 'root'
@@ -270,6 +289,7 @@ export function getRouteMeta(
270
289
  | 'component'
271
290
  | 'pendingComponent'
272
291
  | 'errorComponent'
292
+ | 'notFoundComponent'
273
293
  >
274
294
  variableName: string
275
295
  } {
@@ -293,6 +313,9 @@ export function getRouteMeta(
293
313
  } else if (routePath.endsWith('/errorComponent')) {
294
314
  // error component routes, i.e. `/foo.errorComponent.tsx`
295
315
  fsRouteType = 'errorComponent'
316
+ } else if (routePath.endsWith('/notFoundComponent')) {
317
+ // not found component routes, i.e. `/foo.notFoundComponent.tsx`
318
+ fsRouteType = 'notFoundComponent'
296
319
  }
297
320
 
298
321
  const variableName = routePathToVariable(routePath)
package/src/generator.ts CHANGED
@@ -353,7 +353,7 @@ export class Generator {
353
353
  : -1,
354
354
  (d) =>
355
355
  d.filePath.match(
356
- /[./](component|errorComponent|pendingComponent|loader|lazy)[.]/,
356
+ /[./](component|errorComponent|notFoundComponent|pendingComponent|loader|lazy)[.]/,
357
357
  )
358
358
  ? 1
359
359
  : -1,
@@ -363,7 +363,20 @@ export class Generator {
363
363
  : 1,
364
364
  (d) => (d.routePath?.endsWith('/') ? -1 : 1),
365
365
  (d) => d.routePath,
366
- ]).filter((d) => ![`/${rootPathId}`].includes(d.routePath || ''))
366
+ ]).filter((d) => {
367
+ // Exclude the root route itself, but keep component/loader pieces for the root
368
+ if (d.routePath === `/${rootPathId}`) {
369
+ return [
370
+ 'component',
371
+ 'errorComponent',
372
+ 'notFoundComponent',
373
+ 'pendingComponent',
374
+ 'loader',
375
+ 'lazy',
376
+ ].includes(d._fsRouteType)
377
+ }
378
+ return true
379
+ })
367
380
 
368
381
  const routeFileAllResult = await Promise.allSettled(
369
382
  preRouteNodes
@@ -562,6 +575,31 @@ export class Generator {
562
575
  source: this.targetTemplate.fullPkg,
563
576
  })
564
577
  }
578
+ // Add lazyRouteComponent import if there are component pieces
579
+ const hasComponentPieces = sortedRouteNodes.some(
580
+ (node) =>
581
+ acc.routePiecesByPath[node.routePath!]?.component ||
582
+ acc.routePiecesByPath[node.routePath!]?.errorComponent ||
583
+ acc.routePiecesByPath[node.routePath!]?.notFoundComponent ||
584
+ acc.routePiecesByPath[node.routePath!]?.pendingComponent,
585
+ )
586
+ // Add lazyFn import if there are loader pieces
587
+ const hasLoaderPieces = sortedRouteNodes.some(
588
+ (node) => acc.routePiecesByPath[node.routePath!]?.loader,
589
+ )
590
+ if (hasComponentPieces || hasLoaderPieces) {
591
+ const runtimeImport: ImportDeclaration = {
592
+ specifiers: [],
593
+ source: this.targetTemplate.fullPkg,
594
+ }
595
+ if (hasComponentPieces) {
596
+ runtimeImport.specifiers.push({ imported: 'lazyRouteComponent' })
597
+ }
598
+ if (hasLoaderPieces) {
599
+ runtimeImport.specifiers.push({ imported: 'lazyFn' })
600
+ }
601
+ imports.push(runtimeImport)
602
+ }
565
603
  if (config.verboseFileRoutes === false) {
566
604
  const typeImport: ImportDeclaration = {
567
605
  specifiers: [],
@@ -602,6 +640,8 @@ export class Generator {
602
640
  const componentNode = acc.routePiecesByPath[node.routePath!]?.component
603
641
  const errorComponentNode =
604
642
  acc.routePiecesByPath[node.routePath!]?.errorComponent
643
+ const notFoundComponentNode =
644
+ acc.routePiecesByPath[node.routePath!]?.notFoundComponent
605
645
  const pendingComponentNode =
606
646
  acc.routePiecesByPath[node.routePath!]?.pendingComponent
607
647
  const lazyComponentNode = acc.routePiecesByPath[node.routePath!]?.lazy
@@ -628,50 +668,147 @@ export class Generator {
628
668
  ),
629
669
  )}'), 'loader') })`
630
670
  : '',
631
- componentNode || errorComponentNode || pendingComponentNode
671
+ componentNode ||
672
+ errorComponentNode ||
673
+ notFoundComponentNode ||
674
+ pendingComponentNode
632
675
  ? `.update({
633
676
  ${(
634
677
  [
635
678
  ['component', componentNode],
636
679
  ['errorComponent', errorComponentNode],
680
+ ['notFoundComponent', notFoundComponentNode],
637
681
  ['pendingComponent', pendingComponentNode],
638
682
  ] as const
639
683
  )
640
684
  .filter((d) => d[1])
641
685
  .map((d) => {
686
+ // For .vue files, use 'default' as the export name since Vue SFCs export default
687
+ const isVueFile = d[1]!.filePath.endsWith('.vue')
688
+ const exportName = isVueFile ? 'default' : d[0]
689
+ // Keep .vue extension for Vue files since Vite requires it
690
+ const importPath = replaceBackslash(
691
+ isVueFile
692
+ ? path.relative(
693
+ path.dirname(config.generatedRouteTree),
694
+ path.resolve(
695
+ config.routesDirectory,
696
+ d[1]!.filePath,
697
+ ),
698
+ )
699
+ : removeExt(
700
+ path.relative(
701
+ path.dirname(config.generatedRouteTree),
702
+ path.resolve(
703
+ config.routesDirectory,
704
+ d[1]!.filePath,
705
+ ),
706
+ ),
707
+ config.addExtensions,
708
+ ),
709
+ )
642
710
  return `${
643
711
  d[0]
644
- }: lazyRouteComponent(() => import('./${replaceBackslash(
645
- removeExt(
646
- path.relative(
647
- path.dirname(config.generatedRouteTree),
648
- path.resolve(config.routesDirectory, d[1]!.filePath),
649
- ),
650
- config.addExtensions,
651
- ),
652
- )}'), '${d[0]}')`
712
+ }: lazyRouteComponent(() => import('./${importPath}'), '${exportName}')`
653
713
  })
654
714
  .join('\n,')}
655
715
  })`
656
716
  : '',
657
717
  lazyComponentNode
658
- ? `.lazy(() => import('./${replaceBackslash(
659
- removeExt(
660
- path.relative(
661
- path.dirname(config.generatedRouteTree),
662
- path.resolve(
663
- config.routesDirectory,
664
- lazyComponentNode.filePath,
665
- ),
666
- ),
667
- config.addExtensions,
668
- ),
669
- )}').then((d) => d.Route))`
718
+ ? (() => {
719
+ // For .vue files, use 'default' export since Vue SFCs export default
720
+ const isVueFile = lazyComponentNode.filePath.endsWith('.vue')
721
+ const exportAccessor = isVueFile ? 'd.default' : 'd.Route'
722
+ // Keep .vue extension for Vue files since Vite requires it
723
+ const importPath = replaceBackslash(
724
+ isVueFile
725
+ ? path.relative(
726
+ path.dirname(config.generatedRouteTree),
727
+ path.resolve(
728
+ config.routesDirectory,
729
+ lazyComponentNode.filePath,
730
+ ),
731
+ )
732
+ : removeExt(
733
+ path.relative(
734
+ path.dirname(config.generatedRouteTree),
735
+ path.resolve(
736
+ config.routesDirectory,
737
+ lazyComponentNode.filePath,
738
+ ),
739
+ ),
740
+ config.addExtensions,
741
+ ),
742
+ )
743
+ return `.lazy(() => import('./${importPath}').then((d) => ${exportAccessor}))`
744
+ })()
670
745
  : '',
671
746
  ].join(''),
672
747
  ].join('\n\n')
673
748
  })
674
749
 
750
+ // Generate update for root route if it has component pieces
751
+ const rootRoutePath = `/${rootPathId}`
752
+ const rootComponentNode = acc.routePiecesByPath[rootRoutePath]?.component
753
+ const rootErrorComponentNode =
754
+ acc.routePiecesByPath[rootRoutePath]?.errorComponent
755
+ const rootNotFoundComponentNode =
756
+ acc.routePiecesByPath[rootRoutePath]?.notFoundComponent
757
+ const rootPendingComponentNode =
758
+ acc.routePiecesByPath[rootRoutePath]?.pendingComponent
759
+
760
+ let rootRouteUpdate = ''
761
+ if (
762
+ rootComponentNode ||
763
+ rootErrorComponentNode ||
764
+ rootNotFoundComponentNode ||
765
+ rootPendingComponentNode
766
+ ) {
767
+ rootRouteUpdate = `const rootRouteWithChildren = rootRouteImport${
768
+ rootComponentNode ||
769
+ rootErrorComponentNode ||
770
+ rootNotFoundComponentNode ||
771
+ rootPendingComponentNode
772
+ ? `.update({
773
+ ${(
774
+ [
775
+ ['component', rootComponentNode],
776
+ ['errorComponent', rootErrorComponentNode],
777
+ ['notFoundComponent', rootNotFoundComponentNode],
778
+ ['pendingComponent', rootPendingComponentNode],
779
+ ] as const
780
+ )
781
+ .filter((d) => d[1])
782
+ .map((d) => {
783
+ // For .vue files, use 'default' as the export name since Vue SFCs export default
784
+ const isVueFile = d[1]!.filePath.endsWith('.vue')
785
+ const exportName = isVueFile ? 'default' : d[0]
786
+ // Keep .vue extension for Vue files since Vite requires it
787
+ const importPath = replaceBackslash(
788
+ isVueFile
789
+ ? path.relative(
790
+ path.dirname(config.generatedRouteTree),
791
+ path.resolve(config.routesDirectory, d[1]!.filePath),
792
+ )
793
+ : removeExt(
794
+ path.relative(
795
+ path.dirname(config.generatedRouteTree),
796
+ path.resolve(
797
+ config.routesDirectory,
798
+ d[1]!.filePath,
799
+ ),
800
+ ),
801
+ config.addExtensions,
802
+ ),
803
+ )
804
+ return `${d[0]}: lazyRouteComponent(() => import('./${importPath}'), '${exportName}')`
805
+ })
806
+ .join('\n,')}
807
+ })`
808
+ : ''
809
+ }._addFileChildren(rootRouteChildren)${config.disableTypes ? '' : `._addFileTypes<FileRouteTypes>()`}`
810
+ }
811
+
675
812
  let fileRoutesByPathInterface = ''
676
813
  let fileRoutesByFullPath = ''
677
814
 
@@ -741,7 +878,12 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved
741
878
  )
742
879
  .join(',')}
743
880
  }`,
744
- `export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)${config.disableTypes ? '' : `._addFileTypes<FileRouteTypes>()`}`,
881
+ rootRouteUpdate
882
+ ? rootRouteUpdate.replace(
883
+ 'const rootRouteWithChildren = ',
884
+ 'export const routeTree = ',
885
+ )
886
+ : `export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)${config.disableTypes ? '' : `._addFileTypes<FileRouteTypes>()`}`,
745
887
  ].join('\n')
746
888
 
747
889
  checkRouteFullPathUniqueness(
@@ -894,6 +1036,7 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved
894
1036
  'component',
895
1037
  'pendingComponent',
896
1038
  'errorComponent',
1039
+ 'notFoundComponent',
897
1040
  'loader',
898
1041
  ] satisfies Array<FsRouteType>
899
1042
  ).every((d) => d !== node._fsRouteType)
@@ -915,32 +1058,39 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved
915
1058
  return null
916
1059
  }
917
1060
  }
918
- // transform the file
919
- const transformResult = await transform({
920
- source: updatedCacheEntry.fileContent,
921
- ctx: {
922
- target: this.config.target,
923
- routeId: escapedRoutePath,
924
- lazy: node._fsRouteType === 'lazy',
925
- verboseFileRoutes: !(this.config.verboseFileRoutes === false),
926
- },
927
- node,
928
- })
929
1061
 
930
- if (transformResult.result === 'no-route-export') {
931
- this.logger.warn(
932
- `Route file "${node.fullPath}" does not contain any route piece. This is likely a mistake.`,
933
- )
934
- return null
935
- }
936
- if (transformResult.result === 'error') {
937
- throw new Error(
938
- `Error transforming route file ${node.fullPath}: ${transformResult.error}`,
939
- )
940
- }
941
- if (transformResult.result === 'modified') {
942
- updatedCacheEntry.fileContent = transformResult.output
943
- shouldWriteRouteFile = true
1062
+ // Check if this is a Vue component file
1063
+ // Vue SFC files (.vue) don't need transformation as they can't have a Route export
1064
+ const isVueFile = node.filePath.endsWith('.vue')
1065
+
1066
+ if (!isVueFile) {
1067
+ // transform the file
1068
+ const transformResult = await transform({
1069
+ source: updatedCacheEntry.fileContent,
1070
+ ctx: {
1071
+ target: this.config.target,
1072
+ routeId: escapedRoutePath,
1073
+ lazy: node._fsRouteType === 'lazy',
1074
+ verboseFileRoutes: !(this.config.verboseFileRoutes === false),
1075
+ },
1076
+ node,
1077
+ })
1078
+
1079
+ if (transformResult.result === 'no-route-export') {
1080
+ this.logger.warn(
1081
+ `Route file "${node.fullPath}" does not contain any route piece. This is likely a mistake.`,
1082
+ )
1083
+ return null
1084
+ }
1085
+ if (transformResult.result === 'error') {
1086
+ throw new Error(
1087
+ `Error transforming route file ${node.fullPath}: ${transformResult.error}`,
1088
+ )
1089
+ }
1090
+ if (transformResult.result === 'modified') {
1091
+ updatedCacheEntry.fileContent = transformResult.output
1092
+ shouldWriteRouteFile = true
1093
+ }
944
1094
  }
945
1095
 
946
1096
  for (const plugin of this.plugins) {
@@ -1262,6 +1412,7 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved
1262
1412
  'component',
1263
1413
  'pendingComponent',
1264
1414
  'errorComponent',
1415
+ 'notFoundComponent',
1265
1416
  ] satisfies Array<FsRouteType>
1266
1417
  ).some((d) => d === node._fsRouteType)
1267
1418
  ) {
@@ -1275,16 +1426,19 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved
1275
1426
  ? 'loader'
1276
1427
  : node._fsRouteType === 'errorComponent'
1277
1428
  ? 'errorComponent'
1278
- : node._fsRouteType === 'pendingComponent'
1279
- ? 'pendingComponent'
1280
- : 'component'
1429
+ : node._fsRouteType === 'notFoundComponent'
1430
+ ? 'notFoundComponent'
1431
+ : node._fsRouteType === 'pendingComponent'
1432
+ ? 'pendingComponent'
1433
+ : 'component'
1281
1434
  ] = node
1282
1435
 
1283
1436
  const anchorRoute = acc.routeNodes.find(
1284
1437
  (d) => d.routePath === node.routePath,
1285
1438
  )
1286
1439
 
1287
- if (!anchorRoute) {
1440
+ // Don't create virtual routes for root route component pieces - the root route is handled separately
1441
+ if (!anchorRoute && node.routePath !== `/${rootPathId}`) {
1288
1442
  this.handleNode(
1289
1443
  {
1290
1444
  ...node,
package/src/template.ts CHANGED
@@ -167,6 +167,71 @@ export function getTargetTemplate(config: Config): TargetTemplate {
167
167
  ? 'export const Route = createLazyFileRoute('
168
168
  : `export const Route = createLazyFileRoute('${routePath}')(`,
169
169
 
170
+ tsrExportEnd: () => ');',
171
+ },
172
+ },
173
+ }
174
+ case 'vue':
175
+ return {
176
+ fullPkg: '@tanstack/vue-router',
177
+ subPkg: 'vue-router',
178
+ rootRoute: {
179
+ template: () =>
180
+ [
181
+ 'import { h } from "vue"\n',
182
+ '%%tsrImports%%',
183
+ '\n\n',
184
+ '%%tsrExportStart%%{\n component: RootComponent\n }%%tsrExportEnd%%\n\n',
185
+ 'function RootComponent() { return h("div", {}, ["Hello \\"%%tsrPath%%\\"!", h(Outlet)]) };\n',
186
+ ].join(''),
187
+ imports: {
188
+ tsrImports: () =>
189
+ "import { Outlet, createRootRoute } from '@tanstack/vue-router';",
190
+ tsrExportStart: () => 'export const Route = createRootRoute(',
191
+ tsrExportEnd: () => ');',
192
+ },
193
+ },
194
+ route: {
195
+ template: () =>
196
+ [
197
+ 'import { h } from "vue"\n',
198
+ '%%tsrImports%%',
199
+ '\n\n',
200
+ '%%tsrExportStart%%{\n component: RouteComponent\n }%%tsrExportEnd%%\n\n',
201
+ 'function RouteComponent() { return h("div", {}, "Hello \\"%%tsrPath%%\\"!") };\n',
202
+ ].join(''),
203
+ imports: {
204
+ tsrImports: () =>
205
+ config.verboseFileRoutes === false
206
+ ? ''
207
+ : "import { createFileRoute } from '@tanstack/vue-router';",
208
+ tsrExportStart: (routePath) =>
209
+ config.verboseFileRoutes === false
210
+ ? 'export const Route = createFileRoute('
211
+ : `export const Route = createFileRoute('${routePath}')(`,
212
+ tsrExportEnd: () => ');',
213
+ },
214
+ },
215
+ lazyRoute: {
216
+ template: () =>
217
+ [
218
+ 'import { h } from "vue"\n',
219
+ '%%tsrImports%%',
220
+ '\n\n',
221
+ '%%tsrExportStart%%{\n component: RouteComponent\n }%%tsrExportEnd%%\n\n',
222
+ 'function RouteComponent() { return h("div", {}, "Hello \\"%%tsrPath%%\\"!") };\n',
223
+ ].join(''),
224
+ imports: {
225
+ tsrImports: () =>
226
+ config.verboseFileRoutes === false
227
+ ? ''
228
+ : "import { createLazyFileRoute } from '@tanstack/vue-router';",
229
+
230
+ tsrExportStart: (routePath) =>
231
+ config.verboseFileRoutes === false
232
+ ? 'export const Route = createLazyFileRoute('
233
+ : `export const Route = createLazyFileRoute('${routePath}')(`,
234
+
170
235
  tsrExportEnd: () => ');',
171
236
  },
172
237
  },
package/src/types.ts CHANGED
@@ -33,10 +33,12 @@ export type FsRouteType =
33
33
  | 'component' // @deprecated
34
34
  | 'pendingComponent' // @deprecated
35
35
  | 'errorComponent' // @deprecated
36
+ | 'notFoundComponent' // @deprecated
36
37
 
37
38
  export type RouteSubNode = {
38
39
  component?: RouteNode
39
40
  errorComponent?: RouteNode
41
+ notFoundComponent?: RouteNode
40
42
  pendingComponent?: RouteNode
41
43
  loader?: RouteNode
42
44
  lazy?: RouteNode