@tanstack/router-generator 1.120.4-alpha.19 → 1.120.4

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.
Files changed (51) hide show
  1. package/dist/cjs/config.cjs +14 -14
  2. package/dist/cjs/config.cjs.map +1 -1
  3. package/dist/cjs/config.d.cts +31 -68
  4. package/dist/cjs/filesystem/physical/getRouteNodes.cjs +5 -1
  5. package/dist/cjs/filesystem/physical/getRouteNodes.cjs.map +1 -1
  6. package/dist/cjs/filesystem/physical/getRouteNodes.d.cts +2 -2
  7. package/dist/cjs/filesystem/virtual/getRouteNodes.cjs.map +1 -1
  8. package/dist/cjs/filesystem/virtual/getRouteNodes.d.cts +2 -2
  9. package/dist/cjs/generator.cjs +172 -160
  10. package/dist/cjs/generator.cjs.map +1 -1
  11. package/dist/cjs/generator.d.cts +59 -0
  12. package/dist/cjs/index.cjs +2 -23
  13. package/dist/cjs/index.cjs.map +1 -1
  14. package/dist/cjs/index.d.cts +4 -8
  15. package/dist/cjs/template.cjs +12 -4
  16. package/dist/cjs/template.cjs.map +1 -1
  17. package/dist/cjs/template.d.cts +1 -0
  18. package/dist/cjs/types.d.cts +1 -1
  19. package/dist/cjs/utils.cjs +2 -61
  20. package/dist/cjs/utils.cjs.map +1 -1
  21. package/dist/cjs/utils.d.cts +2 -4
  22. package/dist/esm/config.d.ts +31 -68
  23. package/dist/esm/config.js +14 -14
  24. package/dist/esm/config.js.map +1 -1
  25. package/dist/esm/filesystem/physical/getRouteNodes.d.ts +2 -2
  26. package/dist/esm/filesystem/physical/getRouteNodes.js +6 -2
  27. package/dist/esm/filesystem/physical/getRouteNodes.js.map +1 -1
  28. package/dist/esm/filesystem/virtual/getRouteNodes.d.ts +2 -2
  29. package/dist/esm/filesystem/virtual/getRouteNodes.js.map +1 -1
  30. package/dist/esm/generator.d.ts +59 -0
  31. package/dist/esm/generator.js +175 -163
  32. package/dist/esm/generator.js.map +1 -1
  33. package/dist/esm/index.d.ts +4 -8
  34. package/dist/esm/index.js +4 -25
  35. package/dist/esm/index.js.map +1 -1
  36. package/dist/esm/template.d.ts +1 -0
  37. package/dist/esm/template.js +12 -4
  38. package/dist/esm/template.js.map +1 -1
  39. package/dist/esm/types.d.ts +1 -1
  40. package/dist/esm/utils.d.ts +2 -4
  41. package/dist/esm/utils.js +2 -61
  42. package/dist/esm/utils.js.map +1 -1
  43. package/package.json +3 -3
  44. package/src/config.ts +11 -14
  45. package/src/filesystem/physical/getRouteNodes.ts +14 -13
  46. package/src/filesystem/virtual/getRouteNodes.ts +3 -18
  47. package/src/generator.ts +221 -242
  48. package/src/index.ts +7 -32
  49. package/src/template.ts +15 -4
  50. package/src/types.ts +1 -0
  51. package/src/utils.ts +4 -85
package/src/generator.ts CHANGED
@@ -2,10 +2,12 @@ import path from 'node:path'
2
2
  import * as fs from 'node:fs'
3
3
  import * as fsp from 'node:fs/promises'
4
4
  import {
5
+ determineInitialRoutePath,
5
6
  format,
6
7
  logging,
7
8
  multiSortBy,
8
9
  removeExt,
10
+ removeTrailingSlash,
9
11
  removeUnderscores,
10
12
  replaceBackslash,
11
13
  resetRegex,
@@ -16,12 +18,18 @@ import {
16
18
  import { getRouteNodes as physicalGetRouteNodes } from './filesystem/physical/getRouteNodes'
17
19
  import { getRouteNodes as virtualGetRouteNodes } from './filesystem/virtual/getRouteNodes'
18
20
  import { rootPathId } from './filesystem/physical/rootPathId'
19
- import { fillTemplate, getTargetTemplate } from './template'
21
+ import {
22
+ defaultAPIRouteTemplate,
23
+ fillTemplate,
24
+ getTargetTemplate,
25
+ } from './template'
20
26
  import type { FsRouteType, GetRouteNodesResult, RouteNode } from './types'
21
27
  import type { Config } from './config'
22
28
 
23
- // Maybe import this from `@tanstack/router-core` in the future???
24
- const rootRouteId = '__root__'
29
+ export const CONSTANTS = {
30
+ // When changing this, you'll want to update the import in `start-api-routes/src/index.ts#defaultAPIFileRouteHandler`
31
+ APIRouteExportVariable: 'APIRoute',
32
+ }
25
33
 
26
34
  let latestTask = 0
27
35
  const routeGroupPatternRegex = /\(.+\)/g
@@ -68,6 +76,10 @@ export async function generator(config: Config, root: string) {
68
76
 
69
77
  const TYPES_DISABLED = config.disableTypes
70
78
 
79
+ // Controls whether API Routes are generated for TanStack Start
80
+ const ENABLED_API_ROUTES_GENERATION =
81
+ config.__enableAPIRoutesGeneration ?? false
82
+
71
83
  let getRouteNodesResult: GetRouteNodesResult
72
84
 
73
85
  if (config.virtualRouteConfig) {
@@ -105,6 +117,30 @@ export async function generator(config: Config, root: string) {
105
117
  const routeTree: Array<RouteNode> = []
106
118
  const routePiecesByPath: Record<string, RouteSubNode> = {}
107
119
 
120
+ // Filtered API Route nodes
121
+ const onlyAPIRouteNodes = preRouteNodes.filter((d) => {
122
+ if (!ENABLED_API_ROUTES_GENERATION) {
123
+ return false
124
+ }
125
+
126
+ if (d._fsRouteType !== 'api') {
127
+ return false
128
+ }
129
+
130
+ return true
131
+ })
132
+
133
+ // Filtered Generator Route nodes
134
+ const onlyGeneratorRouteNodes = preRouteNodes.filter((d) => {
135
+ if (ENABLED_API_ROUTES_GENERATION) {
136
+ if (d._fsRouteType === 'api') {
137
+ return false
138
+ }
139
+ }
140
+
141
+ return true
142
+ })
143
+
108
144
  // Loop over the flat list of routeNodes and
109
145
  // build up a tree based on the routeNodes' routePath
110
146
  const routeNodes: Array<RouteNode> = []
@@ -207,7 +243,7 @@ export async function generator(config: Config, root: string) {
207
243
  tLazyRouteTemplate.template(),
208
244
  {
209
245
  tsrImports: tLazyRouteTemplate.imports.tsrImports(),
210
- tsrPath: escapedRoutePath.replaceAll(/\{(.+)\}/gm, '$1'),
246
+ tsrPath: escapedRoutePath,
211
247
  tsrExportStart:
212
248
  tLazyRouteTemplate.imports.tsrExportStart(escapedRoutePath),
213
249
  tsrExportEnd: tLazyRouteTemplate.imports.tsrExportEnd(),
@@ -233,104 +269,20 @@ export async function generator(config: Config, root: string) {
233
269
  tRouteTemplate.template(),
234
270
  {
235
271
  tsrImports: tRouteTemplate.imports.tsrImports(),
236
- tsrPath: escapedRoutePath.replaceAll(/\{(.+)\}/gm, '$1'),
272
+ tsrPath: escapedRoutePath,
237
273
  tsrExportStart:
238
274
  tRouteTemplate.imports.tsrExportStart(escapedRoutePath),
239
275
  tsrExportEnd: tRouteTemplate.imports.tsrExportEnd(),
240
276
  },
241
277
  )
242
278
  }
243
- } else if (config.verboseFileRoutes === false) {
244
- // Check if the route file has a Route export
245
- if (
246
- !routeCode
247
- .split('\n')
248
- .some((line) => line.trim().startsWith('export const Route'))
249
- ) {
250
- return
251
- }
252
-
253
- // Update the existing route file
254
- replaced = routeCode
255
- .replace(
256
- /(FileRoute\(\s*['"])([^\s]*)(['"],?\s*\))/g,
257
- (_, p1, __, p3) => `${p1}${escapedRoutePath}${p3}`,
258
- )
259
- .replace(
260
- new RegExp(
261
- `(import\\s*\\{)(.*)(create(Lazy)?FileRoute)(.*)(\\}\\s*from\\s*['"]@tanstack\\/${ROUTE_TEMPLATE.subPkg}['"])`,
262
- 'gs',
263
- ),
264
- (_, p1, p2, ___, ____, p5, p6) => {
265
- const beforeCreateFileRoute = () => {
266
- if (!p2) return ''
267
-
268
- let trimmed = p2.trim()
269
-
270
- if (trimmed.endsWith(',')) {
271
- trimmed = trimmed.slice(0, -1)
272
- }
273
-
274
- return trimmed
275
- }
276
-
277
- const afterCreateFileRoute = () => {
278
- if (!p5) return ''
279
-
280
- let trimmed = p5.trim()
281
-
282
- if (trimmed.startsWith(',')) {
283
- trimmed = trimmed.slice(1)
284
- }
285
-
286
- return trimmed
287
- }
288
-
289
- const newImport = () => {
290
- const before = beforeCreateFileRoute()
291
- const after = afterCreateFileRoute()
292
-
293
- if (!before) return after
294
-
295
- if (!after) return before
296
-
297
- return `${before},${after}`
298
- }
299
-
300
- const middle = newImport()
301
-
302
- if (middle === '') return ''
303
-
304
- return `${p1} ${newImport()} ${p6}`
305
- },
306
- )
307
- .replace(
308
- /create(Lazy)?FileRoute(\(\s*['"])([^\s]*)(['"],?\s*\))/g,
309
- (_, __, p2, ___, p4) =>
310
- `${node._fsRouteType === 'lazy' ? 'createLazyFileRoute' : 'createFileRoute'}`,
311
- )
312
279
  } else {
313
- // Check if the route file has a Route export
314
- if (
315
- !routeCode
316
- .split('\n')
317
- .some((line) => line.trim().startsWith('export const Route'))
318
- ) {
319
- return
320
- }
321
-
322
280
  // Update the existing route file
323
281
  replaced = routeCode
324
- // fix wrong ids
325
282
  .replace(
326
283
  /(FileRoute\(\s*['"])([^\s]*)(['"],?\s*\))/g,
327
284
  (_, p1, __, p3) => `${p1}${escapedRoutePath}${p3}`,
328
285
  )
329
- // fix missing ids
330
- .replace(
331
- /((FileRoute)(\s*)(\({))/g,
332
- (_, __, p2, p3, p4) => `${p2}('${escapedRoutePath}')${p3}${p4}`,
333
- )
334
286
  .replace(
335
287
  new RegExp(
336
288
  `(import\\s*\\{.*)(create(Lazy)?FileRoute)(.*\\}\\s*from\\s*['"]@tanstack\\/${ROUTE_TEMPLATE.subPkg}['"])`,
@@ -344,18 +296,6 @@ export async function generator(config: Config, root: string) {
344
296
  (_, __, p2, ___, p4) =>
345
297
  `${node._fsRouteType === 'lazy' ? 'createLazyFileRoute' : 'createFileRoute'}${p2}${escapedRoutePath}${p4}`,
346
298
  )
347
-
348
- // check whether the import statement is already present
349
- const regex = new RegExp(
350
- `(import\\s*\\{.*)(create(Lazy)?FileRoute)(.*\\}\\s*from\\s*['"]@tanstack\\/${ROUTE_TEMPLATE.subPkg}['"])`,
351
- 'gm',
352
- )
353
- if (!replaced.match(regex)) {
354
- replaced = [
355
- `import { ${node._fsRouteType === 'lazy' ? 'createLazyFileRoute' : 'createFileRoute'} } from '@tanstack/${ROUTE_TEMPLATE.subPkg}'`,
356
- ...replaced.split('\n'),
357
- ].join('\n')
358
- }
359
299
  }
360
300
 
361
301
  await writeIfDifferent(node.fullPath, routeCode, replaced, {
@@ -466,22 +406,77 @@ export async function generator(config: Config, root: string) {
466
406
  routeNodes.push(node)
467
407
  }
468
408
 
469
- for (const node of preRouteNodes) {
409
+ for (const node of onlyGeneratorRouteNodes) {
470
410
  await handleNode(node)
471
411
  }
472
-
473
- // This is run against the `preRouteNodes` array since it
474
- // has the flattened Route nodes and not the full tree
475
- // Since TSR allows multiple way of defining a route,
476
- // we need to ensure that a user hasn't defined the
477
- // same route in multiple ways (i.e. `flat`, `nested`, `virtual`)
478
412
  checkRouteFullPathUniqueness(
479
413
  preRouteNodes.filter(
480
- (d) => d.children === undefined && 'lazy' !== d._fsRouteType,
414
+ (d) =>
415
+ d.children === undefined &&
416
+ (['api', 'lazy'] satisfies Array<FsRouteType>).every(
417
+ (type) => type !== d._fsRouteType,
418
+ ),
481
419
  ),
482
420
  config,
483
421
  )
484
422
 
423
+ const startAPIRouteNodes: Array<RouteNode> = checkStartAPIRoutes(
424
+ onlyAPIRouteNodes,
425
+ config,
426
+ )
427
+
428
+ const handleAPINode = async (node: RouteNode) => {
429
+ const routeCode = fs.readFileSync(node.fullPath, 'utf-8')
430
+
431
+ const escapedRoutePath = node.routePath?.replaceAll('$', '$$') ?? ''
432
+
433
+ if (!routeCode) {
434
+ const replaced = await fillTemplate(
435
+ config,
436
+ config.customScaffolding?.apiTemplate ?? defaultAPIRouteTemplate,
437
+ {
438
+ tsrImports:
439
+ "import { createAPIFileRoute } from '@tanstack/react-start/api';",
440
+ tsrPath: escapedRoutePath,
441
+ tsrExportStart: `export const ${CONSTANTS.APIRouteExportVariable} = createAPIFileRoute('${escapedRoutePath}')(`,
442
+ tsrExportEnd: ');',
443
+ },
444
+ )
445
+
446
+ await writeIfDifferent(
447
+ node.fullPath,
448
+ '', // Empty string because the file doesn't exist yet
449
+ replaced,
450
+ {
451
+ beforeWrite: () => {
452
+ logger.log(`🟡 Creating ${node.fullPath}`)
453
+ },
454
+ },
455
+ )
456
+ } else {
457
+ await writeIfDifferent(
458
+ node.fullPath,
459
+ routeCode,
460
+ routeCode.replace(
461
+ /(createAPIFileRoute\(\s*['"])([^\s]*)(['"],?\s*\))/g,
462
+ (_, p1, __, p3) => `${p1}${escapedRoutePath}${p3}`,
463
+ ),
464
+ {
465
+ beforeWrite: () => {
466
+ logger.log(`🟡 Updating ${node.fullPath}`)
467
+ },
468
+ },
469
+ )
470
+ }
471
+ }
472
+
473
+ // Handle the API routes for TanStack Start
474
+ if (ENABLED_API_ROUTES_GENERATION) {
475
+ for (const node of startAPIRouteNodes) {
476
+ await handleAPINode(node)
477
+ }
478
+ }
479
+
485
480
  function buildRouteTreeConfig(nodes: Array<RouteNode>, depth = 1): string {
486
481
  const children = nodes.map((node) => {
487
482
  if (node._fsRouteType === '__root') {
@@ -532,30 +527,6 @@ export async function generator(config: Config, root: string) {
532
527
  (d) => d,
533
528
  ])
534
529
 
535
- const typeImports = Object.entries({
536
- // Used for augmentation of regular routes
537
- CreateFileRoute:
538
- config.verboseFileRoutes === false &&
539
- sortedRouteNodes.some(
540
- (d) => isRouteNodeValidForAugmentation(d) && d._fsRouteType !== 'lazy',
541
- ),
542
- // Used for augmentation of lazy (`.lazy`) routes
543
- CreateLazyFileRoute:
544
- config.verboseFileRoutes === false &&
545
- sortedRouteNodes.some(
546
- (node) =>
547
- routePiecesByPath[node.routePath!]?.lazy &&
548
- isRouteNodeValidForAugmentation(node),
549
- ),
550
- // Used in the process of augmenting the routes
551
- FileRoutesByPath:
552
- config.verboseFileRoutes === false &&
553
- sortedRouteNodes.some((d) => isRouteNodeValidForAugmentation(d)),
554
- })
555
- .filter((d) => d[1])
556
- .map((d) => d[0])
557
- .sort((a, b) => a.localeCompare(b))
558
-
559
530
  const imports = Object.entries({
560
531
  createFileRoute: sortedRouteNodes.some((d) => d.isVirtual),
561
532
  lazyFn: sortedRouteNodes.some(
@@ -584,22 +555,14 @@ export async function generator(config: Config, root: string) {
584
555
  ),
585
556
  )
586
557
  }
587
-
588
558
  const routeImports = [
589
559
  ...config.routeTreeFileHeader,
590
560
  `// This file was automatically generated by TanStack Router.
591
561
  // You should NOT make any changes in this file as it will be overwritten.
592
562
  // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.`,
593
- [
594
- imports.length
595
- ? `import { ${imports.join(', ')} } from '${ROUTE_TEMPLATE.fullPkg}'`
596
- : '',
597
- !TYPES_DISABLED && typeImports.length
598
- ? `import type { ${typeImports.join(', ')} } from '${ROUTE_TEMPLATE.fullPkg}'`
599
- : '',
600
- ]
601
- .filter(Boolean)
602
- .join('\n'),
563
+ imports.length
564
+ ? `import { ${imports.join(', ')} } from '${ROUTE_TEMPLATE.fullPkg}'\n`
565
+ : '',
603
566
  '// Import Routes',
604
567
  [
605
568
  `import { Route as rootRoute } from './${getImportPath(rootRouteNode)}'`,
@@ -608,7 +571,7 @@ export async function generator(config: Config, root: string) {
608
571
  .map((node) => {
609
572
  return `import { Route as ${
610
573
  node.variableName
611
- }RouteImport } from './${getImportPath(node)}'`
574
+ }Import } from './${getImportPath(node)}'`
612
575
  }),
613
576
  ].join('\n'),
614
577
  virtualRouteNodes.length ? '// Create Virtual Routes' : '',
@@ -616,7 +579,7 @@ export async function generator(config: Config, root: string) {
616
579
  .map((node) => {
617
580
  return `const ${
618
581
  node.variableName
619
- }RouteImport = createFileRoute('${node.routePath}')()`
582
+ }Import = createFileRoute('${node.routePath}')()`
620
583
  })
621
584
  .join('\n'),
622
585
  '// Create/Update Routes',
@@ -631,8 +594,7 @@ export async function generator(config: Config, root: string) {
631
594
  const lazyComponentNode = routePiecesByPath[node.routePath!]?.lazy
632
595
 
633
596
  return [
634
- [
635
- `const ${node.variableName}Route = ${node.variableName}RouteImport.update({
597
+ `const ${node.variableName}Route = ${node.variableName}Import.update({
636
598
  ${[
637
599
  `id: '${node.path}'`,
638
600
  !node.isNonPath ? `path: '${node.cleanedPath}'` : undefined,
@@ -641,19 +603,19 @@ export async function generator(config: Config, root: string) {
641
603
  .filter(Boolean)
642
604
  .join(',')}
643
605
  }${TYPES_DISABLED ? '' : 'as any'})`,
644
- loaderNode
645
- ? `.updateLoader({ loader: lazyFn(() => import('./${replaceBackslash(
646
- removeExt(
647
- path.relative(
648
- path.dirname(config.generatedRouteTree),
649
- path.resolve(config.routesDirectory, loaderNode.filePath),
650
- ),
651
- config.addExtensions,
606
+ loaderNode
607
+ ? `.updateLoader({ loader: lazyFn(() => import('./${replaceBackslash(
608
+ removeExt(
609
+ path.relative(
610
+ path.dirname(config.generatedRouteTree),
611
+ path.resolve(config.routesDirectory, loaderNode.filePath),
652
612
  ),
653
- )}'), 'loader') })`
654
- : '',
655
- componentNode || errorComponentNode || pendingComponentNode
656
- ? `.update({
613
+ config.addExtensions,
614
+ ),
615
+ )}'), 'loader') })`
616
+ : '',
617
+ componentNode || errorComponentNode || pendingComponentNode
618
+ ? `.update({
657
619
  ${(
658
620
  [
659
621
  ['component', componentNode],
@@ -677,23 +639,22 @@ export async function generator(config: Config, root: string) {
677
639
  })
678
640
  .join('\n,')}
679
641
  })`
680
- : '',
681
- lazyComponentNode
682
- ? `.lazy(() => import('./${replaceBackslash(
683
- removeExt(
684
- path.relative(
685
- path.dirname(config.generatedRouteTree),
686
- path.resolve(
687
- config.routesDirectory,
688
- lazyComponentNode.filePath,
689
- ),
642
+ : '',
643
+ lazyComponentNode
644
+ ? `.lazy(() => import('./${replaceBackslash(
645
+ removeExt(
646
+ path.relative(
647
+ path.dirname(config.generatedRouteTree),
648
+ path.resolve(
649
+ config.routesDirectory,
650
+ lazyComponentNode.filePath,
690
651
  ),
691
- config.addExtensions,
692
652
  ),
693
- )}').then((d) => d.Route))`
694
- : '',
695
- ].join(''),
696
- ].join('\n\n')
653
+ config.addExtensions,
654
+ ),
655
+ )}').then((d) => d.Route))`
656
+ : '',
657
+ ].join('')
697
658
  })
698
659
  .join('\n\n'),
699
660
  ...(TYPES_DISABLED
@@ -710,12 +671,12 @@ export async function generator(config: Config, root: string) {
710
671
  id: '${filePathId}'
711
672
  path: '${inferPath(routeNode)}'
712
673
  fullPath: '${inferFullPath(routeNode)}'
713
- preLoaderRoute: typeof ${routeNode.variableName}RouteImport
674
+ preLoaderRoute: typeof ${routeNode.variableName}Import
714
675
  parentRoute: typeof ${
715
676
  routeNode.isVirtualParentRequired
716
677
  ? `${routeNode.parent?.variableName}Route`
717
678
  : routeNode.parent?.variableName
718
- ? `${routeNode.parent.variableName}RouteImport`
679
+ ? `${routeNode.parent.variableName}Import`
719
680
  : 'rootRoute'
720
681
  }
721
682
  }`
@@ -724,41 +685,6 @@ export async function generator(config: Config, root: string) {
724
685
  }
725
686
  }`,
726
687
  ]),
727
- ...(TYPES_DISABLED
728
- ? []
729
- : config.verboseFileRoutes !== false
730
- ? []
731
- : [
732
- `// Add type-safety to the createFileRoute function across the route tree`,
733
- routeNodes
734
- .map((routeNode) => {
735
- function getModuleDeclaration(routeNode?: RouteNode) {
736
- if (!isRouteNodeValidForAugmentation(routeNode)) {
737
- return ''
738
- }
739
- return `declare module './${getImportPath(routeNode)}' {
740
- const ${routeNode._fsRouteType === 'lazy' ? 'createLazyFileRoute' : 'createFileRoute'}: ${
741
- routeNode._fsRouteType === 'lazy'
742
- ? `CreateLazyFileRoute<FileRoutesByPath['${routeNode.routePath}']['preLoaderRoute']>}`
743
- : `CreateFileRoute<
744
- '${routeNode.routePath}',
745
- FileRoutesByPath['${routeNode.routePath}']['parentRoute'],
746
- FileRoutesByPath['${routeNode.routePath}']['id'],
747
- FileRoutesByPath['${routeNode.routePath}']['path'],
748
- FileRoutesByPath['${routeNode.routePath}']['fullPath']
749
- >
750
- }`
751
- }`
752
- }
753
- return (
754
- getModuleDeclaration(routeNode) +
755
- getModuleDeclaration(
756
- routePiecesByPath[routeNode.routePath!]?.lazy,
757
- )
758
- )
759
- })
760
- .join('\n'),
761
- ]),
762
688
  '// Create and export the route tree',
763
689
  routeConfigChildrenText,
764
690
  ...(TYPES_DISABLED
@@ -777,7 +703,7 @@ export async function generator(config: Config, root: string) {
777
703
  })}
778
704
  }`,
779
705
  `export interface FileRoutesById {
780
- '${rootRouteId}': typeof rootRoute,
706
+ '__root__': typeof rootRoute,
781
707
  ${[...createRouteNodesById(routeNodes).entries()].map(([id, routeNode]) => {
782
708
  return `'${id}': typeof ${getResolvedRouteNodeVariableName(routeNode)}`
783
709
  })}
@@ -787,7 +713,7 @@ export async function generator(config: Config, root: string) {
787
713
  fullPaths: ${routeNodes.length > 0 ? [...createRouteNodesByFullPath(routeNodes).keys()].map((fullPath) => `'${fullPath}'`).join('|') : 'never'}
788
714
  fileRoutesByTo: FileRoutesByTo
789
715
  to: ${routeNodes.length > 0 ? [...createRouteNodesByTo(routeNodes).keys()].map((to) => `'${to}'`).join('|') : 'never'}
790
- id: ${[`'${rootRouteId}'`, ...[...createRouteNodesById(routeNodes).keys()].map((id) => `'${id}'`)].join('|')}
716
+ id: ${[`'__root__'`, ...[...createRouteNodesById(routeNodes).keys()].map((id) => `'${id}'`)].join('|')}
791
717
  fileRoutesById: FileRoutesById
792
718
  }`,
793
719
  `export interface RootRouteChildren {
@@ -805,7 +731,7 @@ export async function generator(config: Config, root: string) {
805
731
 
806
732
  const createRouteManifest = () => {
807
733
  const routesManifest = {
808
- [rootRouteId]: {
734
+ __root__: {
809
735
  filePath: rootRouteNode.filePath,
810
736
  children: routeTree.map((d) => d.routePath),
811
737
  },
@@ -901,22 +827,6 @@ function removeGroups(s: string) {
901
827
  return s.replace(possiblyNestedRouteGroupPatternRegex, '')
902
828
  }
903
829
 
904
- /**
905
- * Checks if a given RouteNode is valid for augmenting it with typing based on conditions.
906
- * Also asserts that the RouteNode is defined.
907
- *
908
- * @param routeNode - The RouteNode to check.
909
- * @returns A boolean indicating whether the RouteNode is defined.
910
- */
911
- function isRouteNodeValidForAugmentation(
912
- routeNode?: RouteNode,
913
- ): routeNode is RouteNode {
914
- if (!routeNode || routeNode.isVirtual) {
915
- return false
916
- }
917
- return true
918
- }
919
-
920
830
  /**
921
831
  * The `node.path` is used as the `id` in the route definition.
922
832
  * This function checks if the given node has a parent and if so, it determines the correct path for the given node.
@@ -937,7 +847,7 @@ function determineNodePath(node: RouteNode) {
937
847
  * @example
938
848
  * removeLastSegmentFromPath('/workspace/_auth/foo') // '/workspace/_auth'
939
849
  */
940
- function removeLastSegmentFromPath(routePath: string = '/'): string {
850
+ export function removeLastSegmentFromPath(routePath: string = '/'): string {
941
851
  const segments = routePath.split('/')
942
852
  segments.pop() // Remove the last segment
943
853
  return segments.join('/')
@@ -957,7 +867,7 @@ function removeLayoutSegments(routePath: string = '/'): string {
957
867
  return newSegments.join('/')
958
868
  }
959
869
 
960
- function hasParentRoute(
870
+ export function hasParentRoute(
961
871
  routes: Array<RouteNode>,
962
872
  node: RouteNode,
963
873
  routePathToCheck: string | undefined,
@@ -992,7 +902,9 @@ function hasParentRoute(
992
902
  /**
993
903
  * Gets the final variable name for a route
994
904
  */
995
- const getResolvedRouteNodeVariableName = (routeNode: RouteNode): string => {
905
+ export const getResolvedRouteNodeVariableName = (
906
+ routeNode: RouteNode,
907
+ ): string => {
996
908
  return routeNode.children?.length
997
909
  ? `${routeNode.variableName}RouteWithChildren`
998
910
  : `${routeNode.variableName}Route`
@@ -1001,7 +913,7 @@ const getResolvedRouteNodeVariableName = (routeNode: RouteNode): string => {
1001
913
  /**
1002
914
  * Creates a map from fullPath to routeNode
1003
915
  */
1004
- const createRouteNodesByFullPath = (
916
+ export const createRouteNodesByFullPath = (
1005
917
  routeNodes: Array<RouteNode>,
1006
918
  ): Map<string, RouteNode> => {
1007
919
  return new Map(
@@ -1012,7 +924,7 @@ const createRouteNodesByFullPath = (
1012
924
  /**
1013
925
  * Create a map from 'to' to a routeNode
1014
926
  */
1015
- const createRouteNodesByTo = (
927
+ export const createRouteNodesByTo = (
1016
928
  routeNodes: Array<RouteNode>,
1017
929
  ): Map<string, RouteNode> => {
1018
930
  return new Map(
@@ -1026,7 +938,7 @@ const createRouteNodesByTo = (
1026
938
  /**
1027
939
  * Create a map from 'id' to a routeNode
1028
940
  */
1029
- const createRouteNodesById = (
941
+ export const createRouteNodesById = (
1030
942
  routeNodes: Array<RouteNode>,
1031
943
  ): Map<string, RouteNode> => {
1032
944
  return new Map(
@@ -1040,7 +952,7 @@ const createRouteNodesById = (
1040
952
  /**
1041
953
  * Infers the full path for use by TS
1042
954
  */
1043
- const inferFullPath = (routeNode: RouteNode): string => {
955
+ export const inferFullPath = (routeNode: RouteNode): string => {
1044
956
  const fullPath = removeGroups(
1045
957
  removeUnderscores(removeLayoutSegments(routeNode.routePath)) ?? '',
1046
958
  )
@@ -1051,7 +963,7 @@ const inferFullPath = (routeNode: RouteNode): string => {
1051
963
  /**
1052
964
  * Infers the path for use by TS
1053
965
  */
1054
- const inferPath = (routeNode: RouteNode): string => {
966
+ export const inferPath = (routeNode: RouteNode): string => {
1055
967
  return routeNode.cleanedPath === '/'
1056
968
  ? routeNode.cleanedPath
1057
969
  : (routeNode.cleanedPath?.replace(/\/$/, '') ?? '')
@@ -1060,7 +972,7 @@ const inferPath = (routeNode: RouteNode): string => {
1060
972
  /**
1061
973
  * Infers to path
1062
974
  */
1063
- const inferTo = (routeNode: RouteNode): string => {
975
+ export const inferTo = (routeNode: RouteNode): string => {
1064
976
  const fullPath = inferFullPath(routeNode)
1065
977
 
1066
978
  if (fullPath === '/') return fullPath
@@ -1071,7 +983,7 @@ const inferTo = (routeNode: RouteNode): string => {
1071
983
  /**
1072
984
  * Dedupes branches and index routes
1073
985
  */
1074
- const dedupeBranchesAndIndexRoutes = (
986
+ export const dedupeBranchesAndIndexRoutes = (
1075
987
  routes: Array<RouteNode>,
1076
988
  ): Array<RouteNode> => {
1077
989
  return routes.filter((route) => {
@@ -1110,8 +1022,75 @@ function checkRouteFullPathUniqueness(
1110
1022
  const errorMessage = `Conflicting configuration paths were found for the following route${conflictingFiles.length > 1 ? 's' : ''}: ${conflictingFiles
1111
1023
  .map((p) => `"${p.inferredFullPath}"`)
1112
1024
  .join(', ')}.
1113
- Please ensure each Route has a unique full path.
1025
+ Please ensure each route has a unique full path.
1026
+ Conflicting files: \n ${conflictingFiles.map((d) => path.resolve(config.routesDirectory, d.filePath)).join('\n ')}\n`
1027
+ throw new Error(errorMessage)
1028
+ }
1029
+ }
1030
+
1031
+ function checkStartAPIRoutes(_routes: Array<RouteNode>, config: Config) {
1032
+ if (_routes.length === 0) {
1033
+ return []
1034
+ }
1035
+
1036
+ // Make sure these are valid URLs
1037
+ // Route Groups and Layout Routes aren't being removed since
1038
+ // you may want to have an API route that starts with an underscore
1039
+ // or be wrapped in parentheses
1040
+ const routes = _routes.map((d) => {
1041
+ const routePath = removeTrailingSlash(d.routePath ?? '')
1042
+ return { ...d, routePath }
1043
+ })
1044
+
1045
+ const conflictingFiles = checkUnique(routes, 'routePath')
1046
+
1047
+ if (conflictingFiles !== undefined) {
1048
+ const errorMessage = `Conflicting configuration paths were found for the following API route${conflictingFiles.length > 1 ? 's' : ''}: ${conflictingFiles
1049
+ .map((p) => `"${p}"`)
1050
+ .join(', ')}.
1051
+ Please ensure each API route has a unique route path.
1114
1052
  Conflicting files: \n ${conflictingFiles.map((d) => path.resolve(config.routesDirectory, d.filePath)).join('\n ')}\n`
1115
1053
  throw new Error(errorMessage)
1116
1054
  }
1055
+
1056
+ return routes
1057
+ }
1058
+
1059
+ export type StartAPIRoutePathSegment = {
1060
+ value: string
1061
+ type: 'path' | 'param' | 'splat'
1062
+ }
1063
+
1064
+ /**
1065
+ * This function takes in a path in the format accepted by TanStack Router
1066
+ * and returns an array of path segments that can be used to generate
1067
+ * the pathname of the TanStack Start API route.
1068
+ *
1069
+ * @param src
1070
+ * @returns
1071
+ */
1072
+ export function startAPIRouteSegmentsFromTSRFilePath(
1073
+ src: string,
1074
+ config: Config,
1075
+ ): Array<StartAPIRoutePathSegment> {
1076
+ const routePath = determineInitialRoutePath(src)
1077
+
1078
+ const parts = routePath
1079
+ .replaceAll('.', '/')
1080
+ .split('/')
1081
+ .filter((p) => !!p && p !== config.indexToken)
1082
+ const segments: Array<StartAPIRoutePathSegment> = parts.map((part) => {
1083
+ if (part.startsWith('$')) {
1084
+ if (part === '$') {
1085
+ return { value: part, type: 'splat' }
1086
+ }
1087
+
1088
+ part.replaceAll('$', '')
1089
+ return { value: part, type: 'param' }
1090
+ }
1091
+
1092
+ return { value: part, type: 'path' }
1093
+ })
1094
+
1095
+ return segments
1117
1096
  }