@tanstack/router-generator 1.48.3 → 1.51.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/src/generator.ts CHANGED
@@ -24,6 +24,7 @@ export type RouteNode = {
24
24
  isVirtualParentRequired?: boolean
25
25
  isVirtualParentRoute?: boolean
26
26
  isRoute?: boolean
27
+ isAPIRoute?: boolean
27
28
  isLoader?: boolean
28
29
  isComponent?: boolean
29
30
  isErrorComponent?: boolean
@@ -76,8 +77,7 @@ async function getRouteNodes(config: Config) {
76
77
  } else if (fullPath.match(/\.(tsx|ts|jsx|js)$/)) {
77
78
  const filePath = replaceBackslash(path.join(dir, dirent.name))
78
79
  const filePathNoExt = removeExt(filePath)
79
- let routePath =
80
- cleanPath(`/${filePathNoExt.split('.').join('/')}`) || ''
80
+ let routePath = determineInitialRoutePath(filePathNoExt)
81
81
 
82
82
  if (routeFilePrefix) {
83
83
  routePath = routePath.replaceAll(routeFilePrefix, '')
@@ -105,6 +105,9 @@ async function getRouteNodes(config: Config) {
105
105
  const isErrorComponent = routePath.endsWith('/errorComponent')
106
106
  const isPendingComponent = routePath.endsWith('/pendingComponent')
107
107
  const isLoader = routePath.endsWith('/loader')
108
+ const isAPIRoute = routePath.startsWith(
109
+ `${removeTrailingSlash(config.apiBase)}/`,
110
+ )
108
111
 
109
112
  const segments = routePath.split('/')
110
113
  const isLayout =
@@ -148,6 +151,7 @@ async function getRouteNodes(config: Config) {
148
151
  isLoader,
149
152
  isLazy,
150
153
  isLayout,
154
+ isAPIRoute,
151
155
  })
152
156
  }
153
157
  }),
@@ -458,11 +462,59 @@ export const Route = createRootRoute({
458
462
  routeNodes.push(node)
459
463
  }
460
464
 
461
- for (const node of preRouteNodes) {
465
+ for (const node of preRouteNodes.filter((d) => !d.isAPIRoute)) {
462
466
  await handleNode(node)
463
467
  }
464
468
 
465
- function buildRouteConfig(nodes: Array<RouteNode>, depth = 1): string {
469
+ const startAPIRouteNodes: Array<RouteNode> = checkStartAPIRoutes(
470
+ preRouteNodes.filter((d) => d.isAPIRoute),
471
+ )
472
+
473
+ const handleAPINode = async (node: RouteNode) => {
474
+ const routeCode = fs.readFileSync(node.fullPath, 'utf-8')
475
+
476
+ const escapedRoutePath = removeTrailingUnderscores(
477
+ node.routePath?.replaceAll('$', '$$') ?? '',
478
+ )
479
+
480
+ if (!routeCode) {
481
+ const replaced = `import { json } from '@tanstack/start'
482
+ import { createAPIFileRoute } from '@tanstack/start/api'
483
+
484
+ export const Route = createAPIFileRoute('${escapedRoutePath}')({
485
+ GET: ({ request, params }) => {
486
+ return json({ message: 'Hello ${escapedRoutePath}' })
487
+ },
488
+ })
489
+
490
+ `
491
+
492
+ logger.log(`🟡 Creating ${node.fullPath}`)
493
+ fs.writeFileSync(
494
+ node.fullPath,
495
+ await prettier.format(replaced, prettierOptions),
496
+ )
497
+ } else {
498
+ const copied = routeCode.replace(
499
+ /(createAPIFileRoute\(\s*['"])([^\s]*)(['"],?\s*\))/g,
500
+ (_, p1, __, p3) => `${p1}${escapedRoutePath}${p3}`,
501
+ )
502
+
503
+ if (copied !== routeCode) {
504
+ logger.log(`🟡 Updating ${node.fullPath}`)
505
+ await fsp.writeFile(
506
+ node.fullPath,
507
+ await prettier.format(copied, prettierOptions),
508
+ )
509
+ }
510
+ }
511
+ }
512
+
513
+ for (const node of startAPIRouteNodes) {
514
+ await handleAPINode(node)
515
+ }
516
+
517
+ function buildRouteTreeConfig(nodes: Array<RouteNode>, depth = 1): string {
466
518
  const children = nodes.map((node) => {
467
519
  if (node.isRoot) {
468
520
  return
@@ -475,7 +527,7 @@ export const Route = createRootRoute({
475
527
  const route = `${node.variableName}Route`
476
528
 
477
529
  if (node.children?.length) {
478
- const childConfigs = buildRouteConfig(node.children, depth + 1)
530
+ const childConfigs = buildRouteTreeConfig(node.children, depth + 1)
479
531
  return `${route}: ${route}.addChildren({${spaces(depth * 4)}${childConfigs}})`
480
532
  }
481
533
 
@@ -485,7 +537,7 @@ export const Route = createRootRoute({
485
537
  return children.filter(Boolean).join(`,`)
486
538
  }
487
539
 
488
- const routeConfigChildrenText = buildRouteConfig(routeTree)
540
+ const routeConfigChildrenText = buildRouteTreeConfig(routeTree)
489
541
 
490
542
  const sortedRouteNodes = multiSortBy(routeNodes, [
491
543
  (d) => (d.routePath?.includes(`/${rootPathId}`) ? -1 : 1),
@@ -671,42 +723,43 @@ export const Route = createRootRoute({
671
723
  .filter(Boolean)
672
724
  .join('\n\n')
673
725
 
674
- const createRouteManifest = () =>
675
- JSON.stringify(
676
- {
677
- routes: {
678
- __root__: {
679
- filePath: rootRouteNode?.filePath,
680
- children: routeTree.map(
681
- (d) => getFilePathIdAndRouteIdFromPath(d.routePath!)[1],
682
- ),
683
- },
684
- ...Object.fromEntries(
685
- routeNodes.map((d) => {
686
- const [filePathId, routeId] = getFilePathIdAndRouteIdFromPath(
687
- d.routePath!,
688
- )
726
+ const createRouteManifest = () => {
727
+ const routesManifest = {
728
+ __root__: {
729
+ filePath: rootRouteNode?.filePath,
730
+ children: routeTree.map(
731
+ (d) => getFilePathIdAndRouteIdFromPath(d.routePath!)[1],
732
+ ),
733
+ },
734
+ ...Object.fromEntries(
735
+ routeNodes.map((d) => {
736
+ const [_, routeId] = getFilePathIdAndRouteIdFromPath(d.routePath!)
737
+
738
+ return [
739
+ routeId,
740
+ {
741
+ filePath: d.filePath,
742
+ parent: d.parent?.routePath
743
+ ? getFilePathIdAndRouteIdFromPath(d.parent.routePath)[1]
744
+ : undefined,
745
+ children: d.children?.map(
746
+ (childRoute) =>
747
+ getFilePathIdAndRouteIdFromPath(childRoute.routePath!)[1],
748
+ ),
749
+ },
750
+ ]
751
+ }),
752
+ ),
753
+ }
689
754
 
690
- return [
691
- routeId,
692
- {
693
- filePath: d.filePath,
694
- parent: d.parent?.routePath
695
- ? getFilePathIdAndRouteIdFromPath(d.parent.routePath)[1]
696
- : undefined,
697
- children: d.children?.map(
698
- (childRoute) =>
699
- getFilePathIdAndRouteIdFromPath(childRoute.routePath!)[1],
700
- ),
701
- },
702
- ]
703
- }),
704
- ),
705
- },
755
+ return JSON.stringify(
756
+ {
757
+ routes: routesManifest,
706
758
  },
707
759
  null,
708
760
  2,
709
761
  )
762
+ }
710
763
 
711
764
  const routeConfigFileContent = await prettier.format(
712
765
  config.disableManifestGeneration
@@ -833,6 +886,14 @@ function removeGroups(s: string) {
833
886
  return s.replace(possiblyNestedRouteGroupPatternRegex, '')
834
887
  }
835
888
 
889
+ function removeTrailingSlash(s: string) {
890
+ return s.replace(/\/$/, '')
891
+ }
892
+
893
+ function determineInitialRoutePath(routePath: string) {
894
+ return cleanPath(`/${routePath.split('.').join('/')}`) || ''
895
+ }
896
+
836
897
  /**
837
898
  * The `node.path` is used as the `id` in the route definition.
838
899
  * This function checks if the given node has a parent and if so, it determines the correct path for the given node.
@@ -931,3 +992,77 @@ function getFilePathIdAndRouteIdFromPath(pathname: string) {
931
992
 
932
993
  return [filePathId, id] as const
933
994
  }
995
+
996
+ function checkStartAPIRoutes(_routes: Array<RouteNode>) {
997
+ if (_routes.length === 0) {
998
+ return []
999
+ }
1000
+
1001
+ // Make sure these are valid URLs
1002
+ // Route Groups and Layout Routes aren't being removed since
1003
+ // you may want to have an API route that starts with an underscore
1004
+ // or be wrapped in parentheses
1005
+ const routes = _routes.map((d) => {
1006
+ const routePath = removeTrailingSlash(d.routePath ?? '')
1007
+ return { ...d, routePath }
1008
+ })
1009
+
1010
+ // Check no two API routes have the same routePath
1011
+ // if they do, throw an error with the conflicting filePaths
1012
+ const routePaths = routes.map((d) => d.routePath)
1013
+ const uniqueRoutePaths = new Set(routePaths)
1014
+ if (routePaths.length !== uniqueRoutePaths.size) {
1015
+ const duplicateRoutePaths = routePaths.filter(
1016
+ (d, i) => routePaths.indexOf(d) !== i,
1017
+ )
1018
+ const conflictingFiles = routes
1019
+ .filter((d) => duplicateRoutePaths.includes(d.routePath))
1020
+ .map((d) => `${d.fullPath}`)
1021
+ const errorMessage = `Conflicting configuration paths was for found for the following API route${duplicateRoutePaths.length > 1 ? 's' : ''}: ${duplicateRoutePaths
1022
+ .map((p) => `"${p}"`)
1023
+ .join(', ')}.
1024
+ Please ensure each API route has a unique route path.
1025
+ Conflicting files: \n ${conflictingFiles.join('\n ')}\n`
1026
+ throw new Error(errorMessage)
1027
+ }
1028
+
1029
+ return routes
1030
+ }
1031
+
1032
+ export type StartAPIRoutePathSegment = {
1033
+ value: string
1034
+ type: 'path' | 'param' | 'splat'
1035
+ }
1036
+
1037
+ /**
1038
+ * This function takes in a path in the format accepted by TanStack Router
1039
+ * and returns an array of path segments that can be used to generate
1040
+ * the pathname of the TanStack Start API route.
1041
+ *
1042
+ * @param src
1043
+ * @returns
1044
+ */
1045
+ export function startAPIRouteSegmentsFromTSRFilePath(
1046
+ src: string,
1047
+ ): Array<StartAPIRoutePathSegment> {
1048
+ const routePath = determineInitialRoutePath(src)
1049
+
1050
+ const parts = routePath
1051
+ .replaceAll('.', '/')
1052
+ .split('/')
1053
+ .filter((p) => !!p && p !== 'index')
1054
+ const segments: Array<StartAPIRoutePathSegment> = parts.map((part) => {
1055
+ if (part.startsWith('$')) {
1056
+ if (part === '$') {
1057
+ return { value: part, type: 'splat' }
1058
+ }
1059
+
1060
+ part.replaceAll('$', '')
1061
+ return { value: part, type: 'param' }
1062
+ }
1063
+
1064
+ return { value: part, type: 'path' }
1065
+ })
1066
+
1067
+ return segments
1068
+ }
package/src/index.ts CHANGED
@@ -1,2 +1,5 @@
1
- export { type Config, configSchema, getConfig } from './config'
2
- export { generator } from './generator'
1
+ export { configSchema, getConfig } from './config'
2
+ export type { Config } from './config'
3
+
4
+ export { generator, startAPIRouteSegmentsFromTSRFilePath } from './generator'
5
+ export type { StartAPIRoutePathSegment } from './generator'