@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/dist/cjs/config.cjs +3 -0
- package/dist/cjs/config.cjs.map +1 -1
- package/dist/cjs/config.d.cts +6 -0
- package/dist/cjs/generator.cjs +130 -37
- package/dist/cjs/generator.cjs.map +1 -1
- package/dist/cjs/generator.d.cts +14 -0
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +4 -2
- package/dist/esm/config.d.ts +6 -0
- package/dist/esm/config.js +3 -0
- package/dist/esm/config.js.map +1 -1
- package/dist/esm/generator.d.ts +14 -0
- package/dist/esm/generator.js +131 -38
- package/dist/esm/generator.js.map +1 -1
- package/dist/esm/index.d.ts +4 -2
- package/dist/esm/index.js +3 -2
- package/package.json +1 -1
- package/src/config.ts +3 -0
- package/src/generator.ts +172 -37
- package/src/index.ts +5 -2
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
676
|
-
{
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
691
|
-
|
|
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 {
|
|
2
|
-
export {
|
|
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'
|