@tanstack/router-generator 1.132.0-alpha.9 → 1.132.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.
Files changed (58) hide show
  1. package/dist/cjs/config.cjs +6 -2
  2. package/dist/cjs/config.cjs.map +1 -1
  3. package/dist/cjs/config.d.cts +12 -9
  4. package/dist/cjs/generator.cjs +264 -316
  5. package/dist/cjs/generator.cjs.map +1 -1
  6. package/dist/cjs/generator.d.cts +20 -7
  7. package/dist/cjs/index.cjs +0 -1
  8. package/dist/cjs/index.cjs.map +1 -1
  9. package/dist/cjs/index.d.cts +4 -4
  10. package/dist/cjs/plugin/types.d.cts +10 -38
  11. package/dist/cjs/transform/transform.cjs +108 -40
  12. package/dist/cjs/transform/transform.cjs.map +1 -1
  13. package/dist/cjs/transform/transform.d.cts +1 -1
  14. package/dist/cjs/transform/types.d.cts +4 -18
  15. package/dist/cjs/types.d.cts +1 -1
  16. package/dist/cjs/utils.cjs +55 -39
  17. package/dist/cjs/utils.cjs.map +1 -1
  18. package/dist/cjs/utils.d.cts +5 -5
  19. package/dist/esm/config.d.ts +12 -9
  20. package/dist/esm/config.js +6 -2
  21. package/dist/esm/config.js.map +1 -1
  22. package/dist/esm/generator.d.ts +20 -7
  23. package/dist/esm/generator.js +266 -318
  24. package/dist/esm/generator.js.map +1 -1
  25. package/dist/esm/index.d.ts +4 -4
  26. package/dist/esm/index.js +1 -2
  27. package/dist/esm/plugin/types.d.ts +10 -38
  28. package/dist/esm/transform/transform.d.ts +1 -1
  29. package/dist/esm/transform/transform.js +106 -38
  30. package/dist/esm/transform/transform.js.map +1 -1
  31. package/dist/esm/transform/types.d.ts +4 -18
  32. package/dist/esm/types.d.ts +1 -1
  33. package/dist/esm/utils.d.ts +5 -5
  34. package/dist/esm/utils.js +55 -39
  35. package/dist/esm/utils.js.map +1 -1
  36. package/package.json +5 -5
  37. package/src/config.ts +7 -1
  38. package/src/generator.ts +306 -366
  39. package/src/index.ts +2 -7
  40. package/src/plugin/types.ts +11 -44
  41. package/src/transform/transform.ts +118 -53
  42. package/src/transform/types.ts +5 -18
  43. package/src/types.ts +1 -1
  44. package/src/utils.ts +85 -70
  45. package/dist/cjs/plugin/default-generator-plugin.cjs +0 -94
  46. package/dist/cjs/plugin/default-generator-plugin.cjs.map +0 -1
  47. package/dist/cjs/plugin/default-generator-plugin.d.cts +0 -2
  48. package/dist/cjs/transform/default-transform-plugin.cjs +0 -97
  49. package/dist/cjs/transform/default-transform-plugin.cjs.map +0 -1
  50. package/dist/cjs/transform/default-transform-plugin.d.cts +0 -2
  51. package/dist/esm/plugin/default-generator-plugin.d.ts +0 -2
  52. package/dist/esm/plugin/default-generator-plugin.js +0 -94
  53. package/dist/esm/plugin/default-generator-plugin.js.map +0 -1
  54. package/dist/esm/transform/default-transform-plugin.d.ts +0 -2
  55. package/dist/esm/transform/default-transform-plugin.js +0 -97
  56. package/dist/esm/transform/default-transform-plugin.js.map +0 -1
  57. package/src/plugin/default-generator-plugin.ts +0 -109
  58. package/src/transform/default-transform-plugin.ts +0 -106
package/src/generator.ts CHANGED
@@ -2,7 +2,7 @@ import path from 'node:path'
2
2
  import * as fsp from 'node:fs/promises'
3
3
  import { existsSync, mkdirSync } from 'node:fs'
4
4
  import crypto from 'node:crypto'
5
- import { deepEqual, rootRouteId } from '@tanstack/router-core'
5
+ import { rootRouteId } from '@tanstack/router-core'
6
6
  import { logging } from './logger'
7
7
  import {
8
8
  isVirtualConfigFile,
@@ -15,16 +15,18 @@ import {
15
15
  buildImportString,
16
16
  buildRouteTreeConfig,
17
17
  checkFileExists,
18
+ checkRouteFullPathUniqueness,
18
19
  createRouteNodesByFullPath,
19
20
  createRouteNodesById,
20
21
  createRouteNodesByTo,
21
22
  determineNodePath,
22
23
  findParent,
23
24
  format,
25
+ getImportForRouteNode,
26
+ getImportPath,
24
27
  getResolvedRouteNodeVariableName,
25
28
  hasParentRoute,
26
29
  isRouteNodeValidForAugmentation,
27
- lowerCaseFirstChar,
28
30
  mergeImportDeclarations,
29
31
  multiSortBy,
30
32
  removeExt,
@@ -39,11 +41,7 @@ import {
39
41
  } from './utils'
40
42
  import { fillTemplate, getTargetTemplate } from './template'
41
43
  import { transform } from './transform/transform'
42
- import { defaultGeneratorPlugin } from './plugin/default-generator-plugin'
43
- import type {
44
- GeneratorPlugin,
45
- GeneratorPluginWithTransform,
46
- } from './plugin/types'
44
+ import type { GeneratorPlugin } from './plugin/types'
47
45
  import type { TargetTemplate } from './template'
48
46
  import type {
49
47
  FsRouteType,
@@ -55,7 +53,6 @@ import type {
55
53
  } from './types'
56
54
  import type { Config } from './config'
57
55
  import type { Logger } from './logger'
58
- import type { TransformPlugin } from './transform/types'
59
56
 
60
57
  interface fs {
61
58
  stat: (
@@ -149,12 +146,18 @@ interface GeneratorCacheEntry {
149
146
  }
150
147
 
151
148
  interface RouteNodeCacheEntry extends GeneratorCacheEntry {
152
- exports: Array<string>
153
149
  routeId: string
150
+ node: RouteNode
154
151
  }
155
152
 
156
153
  type GeneratorRouteNodeCache = Map</** filePath **/ string, RouteNodeCacheEntry>
157
154
 
155
+ interface CrawlingResult {
156
+ rootRouteNode: RouteNode
157
+ routeFileResult: Array<RouteNode>
158
+ acc: HandleNodeAccumulator
159
+ }
160
+
158
161
  export class Generator {
159
162
  /**
160
163
  * why do we have two caches for the route files?
@@ -171,6 +174,7 @@ export class Generator {
171
174
 
172
175
  private routeTreeFileCache: GeneratorCacheEntry | undefined
173
176
 
177
+ private crawlingResult: CrawlingResult | undefined
174
178
  public config: Config
175
179
  public targetTemplate: TargetTemplate
176
180
 
@@ -182,11 +186,8 @@ export class Generator {
182
186
  private generatedRouteTreePath: string
183
187
  private runPromise: Promise<void> | undefined
184
188
  private fileEventQueue: Array<GeneratorEvent> = []
185
- private plugins: Array<GeneratorPlugin> = [defaultGeneratorPlugin()]
186
- private pluginsWithTransform: Array<GeneratorPluginWithTransform> = []
187
- // this is just a cache for the transform plugins since we need them for each route file that is to be processed
188
- private transformPlugins: Array<TransformPlugin> = []
189
- private routeGroupPatternRegex = /\(.+\)/g
189
+ private plugins: Array<GeneratorPlugin> = []
190
+ private static routeGroupPatternRegex = /\(.+\)/g
190
191
  private physicalDirectories: Array<string> = []
191
192
 
192
193
  constructor(opts: { config: Config; root: string; fs?: fs }) {
@@ -199,17 +200,10 @@ export class Generator {
199
200
 
200
201
  this.routesDirectoryPath = this.getRoutesDirectoryPath()
201
202
  this.plugins.push(...(opts.config.plugins || []))
202
- this.plugins.forEach((plugin) => {
203
- if ('transformPlugin' in plugin) {
204
- if (this.pluginsWithTransform.find((p) => p.name === plugin.name)) {
205
- throw new Error(
206
- `Plugin with name "${plugin.name}" is already registered for export ${plugin.transformPlugin.exportName}!`,
207
- )
208
- }
209
- this.pluginsWithTransform.push(plugin)
210
- this.transformPlugins.push(plugin.transformPlugin)
211
- }
212
- })
203
+
204
+ for (const plugin of this.plugins) {
205
+ plugin.init?.({ generator: this })
206
+ }
213
207
  }
214
208
 
215
209
  private getGeneratedRouteTreePath() {
@@ -295,12 +289,7 @@ export class Generator {
295
289
  }
296
290
 
297
291
  try {
298
- const start = performance.now()
299
292
  await this.generatorInternal()
300
- const end = performance.now()
301
- this.logger.info(
302
- `Generated route tree in ${Math.round(end - start)}ms`,
303
- )
304
293
  } catch (err) {
305
294
  const errArray = !Array.isArray(err) ? [err] : err
306
295
 
@@ -351,7 +340,7 @@ export class Generator {
351
340
  }
352
341
  this.physicalDirectories = physicalDirectories
353
342
 
354
- writeRouteTreeFile = await this.handleRootNode(rootRouteNode)
343
+ await this.handleRootNode(rootRouteNode)
355
344
 
356
345
  const preRouteNodes = multiSortBy(beforeRouteNodes, [
357
346
  (d) => (d.routePath === '/' ? -1 : 1),
@@ -390,22 +379,29 @@ export class Generator {
390
379
 
391
380
  const routeFileResult = routeFileAllResult.flatMap((result) => {
392
381
  if (result.status === 'fulfilled' && result.value !== null) {
393
- return result.value
382
+ if (result.value.shouldWriteTree) {
383
+ writeRouteTreeFile = true
384
+ }
385
+ return result.value.node
394
386
  }
395
387
  return []
396
388
  })
397
389
 
398
- routeFileResult.forEach((result) => {
399
- if (!result.node.exports?.length) {
400
- this.logger.warn(
401
- `Route file "${result.cacheEntry.fileContent}" does not export any route piece. This is likely a mistake.`,
402
- )
403
- }
404
- })
405
- if (routeFileResult.find((r) => r.shouldWriteTree)) {
406
- writeRouteTreeFile = true
390
+ // reset children in case we re-use a node from the cache
391
+ routeFileResult.forEach((r) => (r.children = undefined))
392
+
393
+ const acc: HandleNodeAccumulator = {
394
+ routeTree: [],
395
+ routeNodes: [],
396
+ routePiecesByPath: {},
407
397
  }
408
398
 
399
+ for (const node of routeFileResult) {
400
+ Generator.handleNode(node, acc)
401
+ }
402
+
403
+ this.crawlingResult = { rootRouteNode, routeFileResult, acc }
404
+
409
405
  // this is the first time the generator runs, so read in the route tree file if it exists yet
410
406
  if (!this.routeTreeFileCache) {
411
407
  const routeTreeFile = await this.fs.readFile(this.generatedRouteTreePath)
@@ -440,10 +436,14 @@ export class Generator {
440
436
  if (!writeRouteTreeFile) {
441
437
  // only needs to be done if no other changes have been detected yet
442
438
  // compare shadowCache and cache to identify deleted routes
443
- for (const fullPath of this.routeNodeCache.keys()) {
444
- if (!this.routeNodeShadowCache.has(fullPath)) {
445
- writeRouteTreeFile = true
446
- break
439
+ if (this.routeNodeCache.size !== this.routeNodeShadowCache.size) {
440
+ writeRouteTreeFile = true
441
+ } else {
442
+ for (const fullPath of this.routeNodeCache.keys()) {
443
+ if (!this.routeNodeShadowCache.has(fullPath)) {
444
+ writeRouteTreeFile = true
445
+ break
446
+ }
447
447
  }
448
448
  }
449
449
  }
@@ -453,11 +453,13 @@ export class Generator {
453
453
  return
454
454
  }
455
455
 
456
- let routeTreeContent = this.buildRouteTreeFileContent(
456
+ const buildResult = this.buildRouteTree({
457
457
  rootRouteNode,
458
- preRouteNodes,
458
+ acc,
459
459
  routeFileResult,
460
- )
460
+ })
461
+ let routeTreeContent = buildResult.routeTreeContent
462
+
461
463
  routeTreeContent = this.config.enableRouteTreeFormatting
462
464
  ? await format(routeTreeContent, this.config)
463
465
  : routeTreeContent
@@ -499,6 +501,14 @@ export class Generator {
499
501
  }
500
502
  }
501
503
 
504
+ this.plugins.map((plugin) => {
505
+ return plugin.onRouteTreeChanged?.({
506
+ routeTree: buildResult.routeTree,
507
+ routeNodes: buildResult.routeNodes,
508
+ acc,
509
+ rootRouteNode,
510
+ })
511
+ })
502
512
  this.swapCaches()
503
513
  }
504
514
 
@@ -507,127 +517,117 @@ export class Generator {
507
517
  this.routeNodeShadowCache = new Map()
508
518
  }
509
519
 
510
- private buildRouteTreeFileContent(
511
- rootRouteNode: RouteNode,
512
- preRouteNodes: Array<RouteNode>,
513
- routeFileResult: Array<{
514
- cacheEntry: RouteNodeCacheEntry
515
- node: RouteNode
516
- }>,
517
- ) {
518
- const getImportForRouteNode = (node: RouteNode, exportName: string) => {
519
- if (node.exports?.includes(exportName)) {
520
- return {
521
- source: `./${this.getImportPath(node)}`,
522
- specifiers: [
523
- {
524
- imported: exportName,
525
- local: `${node.variableName}${exportName}Import`,
526
- },
527
- ],
528
- } satisfies ImportDeclaration
529
- }
530
- return undefined
531
- }
520
+ public buildRouteTree(opts: {
521
+ rootRouteNode: RouteNode
522
+ acc: HandleNodeAccumulator
523
+ routeFileResult: Array<RouteNode>
524
+ config?: Partial<Config>
525
+ }) {
526
+ const config = { ...this.config, ...(opts.config || {}) }
532
527
 
533
- const buildRouteTreeForExport = (plugin: GeneratorPluginWithTransform) => {
534
- const exportName = plugin.transformPlugin.exportName
535
- const acc: HandleNodeAccumulator = {
536
- routeTree: [],
537
- routeNodes: [],
538
- routePiecesByPath: {},
539
- }
540
- for (const node of preRouteNodes) {
541
- if (node.exports?.includes(plugin.transformPlugin.exportName)) {
542
- this.handleNode(node, acc)
543
- }
544
- }
528
+ const { rootRouteNode, acc } = opts
545
529
 
546
- const sortedRouteNodes = multiSortBy(acc.routeNodes, [
547
- (d) => (d.routePath?.includes(`/${rootPathId}`) ? -1 : 1),
548
- (d) => d.routePath?.split('/').length,
549
- (d) => (d.routePath?.endsWith(this.config.indexToken) ? -1 : 1),
550
- (d) => d,
551
- ])
530
+ const sortedRouteNodes = multiSortBy(acc.routeNodes, [
531
+ (d) => (d.routePath?.includes(`/${rootPathId}`) ? -1 : 1),
532
+ (d) => d.routePath?.split('/').length,
533
+ (d) => (d.routePath?.endsWith(config.indexToken) ? -1 : 1),
534
+ (d) => d,
535
+ ])
536
+
537
+ const routeImports = sortedRouteNodes
538
+ .filter((d) => !d.isVirtual)
539
+ .flatMap((node) =>
540
+ getImportForRouteNode(
541
+ node,
542
+ config,
543
+ this.generatedRouteTreePath,
544
+ this.root,
545
+ ),
546
+ )
552
547
 
553
- const pluginConfig = plugin.config({
554
- generator: this,
555
- rootRouteNode,
556
- sortedRouteNodes,
548
+ const virtualRouteNodes = sortedRouteNodes
549
+ .filter((d) => d.isVirtual)
550
+ .map((node) => {
551
+ return `const ${
552
+ node.variableName
553
+ }RouteImport = createFileRoute('${node.routePath}')()`
557
554
  })
558
555
 
559
- const routeImports = sortedRouteNodes
560
- .filter((d) => !d.isVirtual)
561
- .flatMap((node) => getImportForRouteNode(node, exportName) ?? [])
562
-
563
- const hasMatchingRouteFiles =
564
- acc.routeNodes.length > 0 || rootRouteNode.exports?.includes(exportName)
565
-
566
- const virtualRouteNodes = sortedRouteNodes
567
- .filter((d) => d.isVirtual)
568
- .map((node) => {
569
- return `const ${
570
- node.variableName
571
- }${exportName}Import = ${plugin.createVirtualRouteCode({ node })}`
572
- })
556
+ const imports: Array<ImportDeclaration> = []
557
+ if (acc.routeNodes.some((n) => n.isVirtual)) {
558
+ imports.push({
559
+ specifiers: [{ imported: 'createFileRoute' }],
560
+ source: this.targetTemplate.fullPkg,
561
+ })
562
+ }
563
+ if (config.verboseFileRoutes === false) {
564
+ const typeImport: ImportDeclaration = {
565
+ specifiers: [],
566
+ source: this.targetTemplate.fullPkg,
567
+ importKind: 'type',
568
+ }
573
569
  if (
574
- !rootRouteNode.exports?.includes(exportName) &&
575
- pluginConfig.virtualRootRoute
570
+ sortedRouteNodes.some(
571
+ (d) =>
572
+ isRouteNodeValidForAugmentation(d) && d._fsRouteType !== 'lazy',
573
+ )
576
574
  ) {
577
- virtualRouteNodes.unshift(
578
- `const ${rootRouteNode.variableName}${exportName}Import = ${plugin.createRootRouteCode()}`,
575
+ typeImport.specifiers.push({ imported: 'CreateFileRoute' })
576
+ }
577
+ if (
578
+ sortedRouteNodes.some(
579
+ (node) =>
580
+ acc.routePiecesByPath[node.routePath!]?.lazy &&
581
+ isRouteNodeValidForAugmentation(node),
579
582
  )
583
+ ) {
584
+ typeImport.specifiers.push({ imported: 'CreateLazyFileRoute' })
580
585
  }
581
586
 
582
- const imports = plugin.imports({
583
- sortedRouteNodes,
584
- acc,
585
- generator: this,
586
- rootRouteNode,
587
- })
587
+ if (typeImport.specifiers.length > 0) {
588
+ typeImport.specifiers.push({ imported: 'FileRoutesByPath' })
589
+ imports.push(typeImport)
590
+ }
591
+ }
588
592
 
589
- const routeTreeConfig = buildRouteTreeConfig(
590
- acc.routeTree,
591
- exportName,
592
- this.config.disableTypes,
593
- )
593
+ const routeTreeConfig = buildRouteTreeConfig(
594
+ acc.routeTree,
595
+ config.disableTypes,
596
+ )
594
597
 
595
- const createUpdateRoutes = sortedRouteNodes.map((node) => {
596
- const loaderNode = acc.routePiecesByPath[node.routePath!]?.loader
597
- const componentNode = acc.routePiecesByPath[node.routePath!]?.component
598
- const errorComponentNode =
599
- acc.routePiecesByPath[node.routePath!]?.errorComponent
600
- const pendingComponentNode =
601
- acc.routePiecesByPath[node.routePath!]?.pendingComponent
602
- const lazyComponentNode = acc.routePiecesByPath[node.routePath!]?.lazy
598
+ const createUpdateRoutes = sortedRouteNodes.map((node) => {
599
+ const loaderNode = acc.routePiecesByPath[node.routePath!]?.loader
600
+ const componentNode = acc.routePiecesByPath[node.routePath!]?.component
601
+ const errorComponentNode =
602
+ acc.routePiecesByPath[node.routePath!]?.errorComponent
603
+ const pendingComponentNode =
604
+ acc.routePiecesByPath[node.routePath!]?.pendingComponent
605
+ const lazyComponentNode = acc.routePiecesByPath[node.routePath!]?.lazy
603
606
 
604
- return [
605
- [
606
- `const ${node.variableName}${exportName} = ${node.variableName}${exportName}Import.update({
607
+ return [
608
+ [
609
+ `const ${node.variableName}Route = ${node.variableName}RouteImport.update({
607
610
  ${[
608
611
  `id: '${node.path}'`,
609
612
  !node.isNonPath ? `path: '${node.cleanedPath}'` : undefined,
610
- `getParentRoute: () => ${findParent(node, exportName)}`,
613
+ `getParentRoute: () => ${findParent(node)}`,
611
614
  ]
612
615
  .filter(Boolean)
613
616
  .join(',')}
614
- }${this.config.disableTypes ? '' : 'as any'})`,
615
- loaderNode
616
- ? `.updateLoader({ loader: lazyFn(() => import('./${replaceBackslash(
617
- removeExt(
618
- path.relative(
619
- path.dirname(this.config.generatedRouteTree),
620
- path.resolve(
621
- this.config.routesDirectory,
622
- loaderNode.filePath,
623
- ),
624
- ),
625
- this.config.addExtensions,
617
+ }${config.disableTypes ? '' : 'as any'})`,
618
+ loaderNode
619
+ ? `.updateLoader({ loader: lazyFn(() => import('./${replaceBackslash(
620
+ removeExt(
621
+ path.relative(
622
+ path.dirname(config.generatedRouteTree),
623
+ path.resolve(config.routesDirectory, loaderNode.filePath),
626
624
  ),
627
- )}'), 'loader') })`
628
- : '',
629
- componentNode || errorComponentNode || pendingComponentNode
630
- ? `.update({
625
+ config.addExtensions,
626
+ ),
627
+ )}'), 'loader') })`
628
+ : '',
629
+ componentNode || errorComponentNode || pendingComponentNode
630
+ ? `.update({
631
631
  ${(
632
632
  [
633
633
  ['component', componentNode],
@@ -642,171 +642,141 @@ export class Generator {
642
642
  }: lazyRouteComponent(() => import('./${replaceBackslash(
643
643
  removeExt(
644
644
  path.relative(
645
- path.dirname(this.config.generatedRouteTree),
646
- path.resolve(
647
- this.config.routesDirectory,
648
- d[1]!.filePath,
649
- ),
645
+ path.dirname(config.generatedRouteTree),
646
+ path.resolve(config.routesDirectory, d[1]!.filePath),
650
647
  ),
651
- this.config.addExtensions,
648
+ config.addExtensions,
652
649
  ),
653
650
  )}'), '${d[0]}')`
654
651
  })
655
652
  .join('\n,')}
656
653
  })`
657
- : '',
658
- lazyComponentNode
659
- ? `.lazy(() => import('./${replaceBackslash(
660
- removeExt(
661
- path.relative(
662
- path.dirname(this.config.generatedRouteTree),
663
- path.resolve(
664
- this.config.routesDirectory,
665
- lazyComponentNode.filePath,
666
- ),
654
+ : '',
655
+ lazyComponentNode
656
+ ? `.lazy(() => import('./${replaceBackslash(
657
+ removeExt(
658
+ path.relative(
659
+ path.dirname(config.generatedRouteTree),
660
+ path.resolve(
661
+ config.routesDirectory,
662
+ lazyComponentNode.filePath,
667
663
  ),
668
- this.config.addExtensions,
669
664
  ),
670
- )}').then((d) => d.${exportName}))`
671
- : '',
672
- ].join(''),
673
- ].join('\n\n')
674
- })
665
+ config.addExtensions,
666
+ ),
667
+ )}').then((d) => d.Route))`
668
+ : '',
669
+ ].join(''),
670
+ ].join('\n\n')
671
+ })
675
672
 
676
- let fileRoutesByPathInterfacePerPlugin = ''
677
- let fileRoutesByFullPathPerPlugin = ''
673
+ let fileRoutesByPathInterface = ''
674
+ let fileRoutesByFullPath = ''
678
675
 
679
- if (!this.config.disableTypes && hasMatchingRouteFiles) {
680
- fileRoutesByFullPathPerPlugin = [
681
- `export interface File${exportName}sByFullPath {
676
+ if (!config.disableTypes) {
677
+ fileRoutesByFullPath = [
678
+ `export interface FileRoutesByFullPath {
682
679
  ${[...createRouteNodesByFullPath(acc.routeNodes).entries()]
683
680
  .filter(([fullPath]) => fullPath)
684
681
  .map(([fullPath, routeNode]) => {
685
- return `'${fullPath}': typeof ${getResolvedRouteNodeVariableName(routeNode, exportName)}`
682
+ return `'${fullPath}': typeof ${getResolvedRouteNodeVariableName(routeNode)}`
686
683
  })}
687
684
  }`,
688
- `export interface File${exportName}sByTo {
685
+ `export interface FileRoutesByTo {
689
686
  ${[...createRouteNodesByTo(acc.routeNodes).entries()]
690
687
  .filter(([to]) => to)
691
688
  .map(([to, routeNode]) => {
692
- return `'${to}': typeof ${getResolvedRouteNodeVariableName(routeNode, exportName)}`
689
+ return `'${to}': typeof ${getResolvedRouteNodeVariableName(routeNode)}`
693
690
  })}
694
691
  }`,
695
- `export interface File${exportName}sById {
696
- '${rootRouteId}': typeof root${exportName}Import,
692
+ `export interface FileRoutesById {
693
+ '${rootRouteId}': typeof rootRouteImport,
697
694
  ${[...createRouteNodesById(acc.routeNodes).entries()].map(([id, routeNode]) => {
698
- return `'${id}': typeof ${getResolvedRouteNodeVariableName(routeNode, exportName)}`
695
+ return `'${id}': typeof ${getResolvedRouteNodeVariableName(routeNode)}`
699
696
  })}
700
697
  }`,
701
- `export interface File${exportName}Types {
702
- file${exportName}sByFullPath: File${exportName}sByFullPath
698
+ `export interface FileRouteTypes {
699
+ fileRoutesByFullPath: FileRoutesByFullPath
703
700
  fullPaths: ${
704
- acc.routeNodes.length > 0
705
- ? [...createRouteNodesByFullPath(acc.routeNodes).keys()]
706
- .filter((fullPath) => fullPath)
707
- .map((fullPath) => `'${fullPath}'`)
708
- .join('|')
709
- : 'never'
710
- }
711
- file${exportName}sByTo: File${exportName}sByTo
701
+ acc.routeNodes.length > 0
702
+ ? [...createRouteNodesByFullPath(acc.routeNodes).keys()]
703
+ .filter((fullPath) => fullPath)
704
+ .map((fullPath) => `'${fullPath}'`)
705
+ .join('|')
706
+ : 'never'
707
+ }
708
+ fileRoutesByTo: FileRoutesByTo
712
709
  to: ${
713
- acc.routeNodes.length > 0
714
- ? [...createRouteNodesByTo(acc.routeNodes).keys()]
715
- .filter((to) => to)
716
- .map((to) => `'${to}'`)
717
- .join('|')
718
- : 'never'
719
- }
710
+ acc.routeNodes.length > 0
711
+ ? [...createRouteNodesByTo(acc.routeNodes).keys()]
712
+ .filter((to) => to)
713
+ .map((to) => `'${to}'`)
714
+ .join('|')
715
+ : 'never'
716
+ }
720
717
  id: ${[`'${rootRouteId}'`, ...[...createRouteNodesById(acc.routeNodes).keys()].map((id) => `'${id}'`)].join('|')}
721
- file${exportName}sById: File${exportName}sById
718
+ fileRoutesById: FileRoutesById
722
719
  }`,
723
- `export interface Root${exportName}Children {
724
- ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${getResolvedRouteNodeVariableName(child, exportName)}`).join(',')}
720
+ `export interface RootRouteChildren {
721
+ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolvedRouteNodeVariableName(child)}`).join(',')}
725
722
  }`,
726
- ].join('\n')
727
-
728
- fileRoutesByPathInterfacePerPlugin = buildFileRoutesByPathInterface({
729
- ...plugin.moduleAugmentation({ generator: this }),
730
- routeNodes:
731
- this.config.verboseFileRoutes !== false
732
- ? sortedRouteNodes
733
- : [
734
- ...routeFileResult.map(({ node }) => node),
735
- ...sortedRouteNodes.filter((d) => d.isVirtual),
736
- ],
737
- exportName,
738
- })
739
- }
723
+ ].join('\n')
724
+
725
+ fileRoutesByPathInterface = buildFileRoutesByPathInterface({
726
+ module: this.targetTemplate.fullPkg,
727
+ interfaceName: 'FileRoutesByPath',
728
+ routeNodes: sortedRouteNodes,
729
+ })
730
+ }
740
731
 
741
- let routeTree = ''
742
- if (hasMatchingRouteFiles) {
743
- routeTree = [
744
- `const root${exportName}Children${this.config.disableTypes ? '' : `: Root${exportName}Children`} = {
732
+ const routeTree = [
733
+ `const rootRouteChildren${config.disableTypes ? '' : `: RootRouteChildren`} = {
745
734
  ${acc.routeTree
746
735
  .map(
747
736
  (child) =>
748
- `${child.variableName}${exportName}: ${getResolvedRouteNodeVariableName(child, exportName)}`,
737
+ `${child.variableName}Route: ${getResolvedRouteNodeVariableName(child)}`,
749
738
  )
750
739
  .join(',')}
751
740
  }`,
752
- `export const ${lowerCaseFirstChar(exportName)}Tree = root${exportName}Import._addFileChildren(root${exportName}Children)${this.config.disableTypes ? '' : `._addFileTypes<File${exportName}Types>()`}`,
753
- ].join('\n')
754
- }
755
-
756
- return {
757
- routeImports,
758
- sortedRouteNodes,
759
- acc,
760
- virtualRouteNodes,
761
- routeTreeConfig,
762
- routeTree,
763
- imports,
764
- createUpdateRoutes,
765
- fileRoutesByFullPathPerPlugin,
766
- fileRoutesByPathInterfacePerPlugin,
767
- }
768
- }
769
-
770
- const routeTrees = this.pluginsWithTransform.map((plugin) => ({
771
- exportName: plugin.transformPlugin.exportName,
772
- ...buildRouteTreeForExport(plugin),
773
- }))
774
-
775
- this.plugins.map((plugin) => {
776
- return plugin.onRouteTreesChanged?.({
777
- routeTrees,
778
- rootRouteNode,
779
- generator: this,
780
- })
781
- })
741
+ `export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)${config.disableTypes ? '' : `._addFileTypes<FileRouteTypes>()`}`,
742
+ ].join('\n')
782
743
 
783
- let mergedImports = mergeImportDeclarations(
784
- routeTrees.flatMap((d) => d.imports),
744
+ checkRouteFullPathUniqueness(
745
+ sortedRouteNodes.filter(
746
+ (d) => d.children === undefined && 'lazy' !== d._fsRouteType,
747
+ ),
748
+ config,
785
749
  )
786
- if (this.config.disableTypes) {
750
+
751
+ let mergedImports = mergeImportDeclarations(imports)
752
+ if (config.disableTypes) {
787
753
  mergedImports = mergedImports.filter((d) => d.importKind !== 'type')
788
754
  }
789
755
 
790
756
  const importStatements = mergedImports.map(buildImportString)
791
757
 
792
758
  let moduleAugmentation = ''
793
- if (this.config.verboseFileRoutes === false && !this.config.disableTypes) {
794
- moduleAugmentation = routeFileResult
795
- .map(({ node }) => {
759
+ if (config.verboseFileRoutes === false && !config.disableTypes) {
760
+ moduleAugmentation = opts.routeFileResult
761
+ .map((node) => {
796
762
  const getModuleDeclaration = (routeNode?: RouteNode) => {
797
763
  if (!isRouteNodeValidForAugmentation(routeNode)) {
798
764
  return ''
799
765
  }
800
- const moduleAugmentation = this.pluginsWithTransform
801
- .map((plugin) => {
802
- return plugin.routeModuleAugmentation({
803
- routeNode,
804
- })
805
- })
806
- .filter(Boolean)
807
- .join('\n')
766
+ let moduleAugmentation = ''
767
+ if (routeNode._fsRouteType === 'lazy') {
768
+ moduleAugmentation = `const createLazyFileRoute: CreateLazyFileRoute<FileRoutesByPath['${routeNode.routePath}']['preLoaderRoute']>`
769
+ } else {
770
+ moduleAugmentation = `const createFileRoute: CreateFileRoute<'${routeNode.routePath}',
771
+ FileRoutesByPath['${routeNode.routePath}']['parentRoute'],
772
+ FileRoutesByPath['${routeNode.routePath}']['id'],
773
+ FileRoutesByPath['${routeNode.routePath}']['path'],
774
+ FileRoutesByPath['${routeNode.routePath}']['fullPath']
775
+ >
776
+ `
777
+ }
808
778
 
809
- return `declare module './${this.getImportPath(routeNode)}' {
779
+ return `declare module './${getImportPath(routeNode, config, this.generatedRouteTreePath)}' {
810
780
  ${moduleAugmentation}
811
781
  }`
812
782
  }
@@ -815,47 +785,45 @@ ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${get
815
785
  .join('\n')
816
786
  }
817
787
 
818
- const routeImports = routeTrees.flatMap((t) => t.routeImports)
819
- const rootRouteImports = this.pluginsWithTransform.flatMap(
820
- (p) =>
821
- getImportForRouteNode(rootRouteNode, p.transformPlugin.exportName) ??
822
- [],
788
+ const rootRouteImport = getImportForRouteNode(
789
+ rootRouteNode,
790
+ config,
791
+ this.generatedRouteTreePath,
792
+ this.root,
823
793
  )
824
- if (rootRouteImports.length > 0) {
825
- routeImports.unshift(...rootRouteImports)
794
+ routeImports.unshift(rootRouteImport)
795
+
796
+ let footer: Array<string> = []
797
+ if (config.routeTreeFileFooter) {
798
+ if (Array.isArray(config.routeTreeFileFooter)) {
799
+ footer = config.routeTreeFileFooter
800
+ } else {
801
+ footer = config.routeTreeFileFooter()
802
+ }
826
803
  }
827
804
  const routeTreeContent = [
828
- ...this.config.routeTreeFileHeader,
805
+ ...config.routeTreeFileHeader,
829
806
  `// This file was automatically generated by TanStack Router.
830
807
  // You should NOT make any changes in this file as it will be overwritten.
831
808
  // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.`,
832
809
  [...importStatements].join('\n'),
833
810
  mergeImportDeclarations(routeImports).map(buildImportString).join('\n'),
834
- routeTrees.flatMap((t) => t.virtualRouteNodes).join('\n'),
835
- routeTrees.flatMap((t) => t.createUpdateRoutes).join('\n'),
836
-
837
- routeTrees.map((t) => t.fileRoutesByFullPathPerPlugin).join('\n'),
838
- routeTrees.map((t) => t.fileRoutesByPathInterfacePerPlugin).join('\n'),
811
+ virtualRouteNodes.join('\n'),
812
+ createUpdateRoutes.join('\n'),
813
+ fileRoutesByFullPath,
814
+ fileRoutesByPathInterface,
839
815
  moduleAugmentation,
840
- routeTrees.flatMap((t) => t.routeTreeConfig).join('\n'),
841
- routeTrees.map((t) => t.routeTree).join('\n'),
842
- ...this.config.routeTreeFileFooter,
816
+ routeTreeConfig.join('\n'),
817
+ routeTree,
818
+ ...footer,
843
819
  ]
844
820
  .filter(Boolean)
845
821
  .join('\n\n')
846
- return routeTreeContent
847
- }
848
-
849
- private getImportPath(node: RouteNode) {
850
- return replaceBackslash(
851
- removeExt(
852
- path.relative(
853
- path.dirname(this.config.generatedRouteTree),
854
- path.resolve(this.config.routesDirectory, node.filePath),
855
- ),
856
- this.config.addExtensions,
857
- ),
858
- )
822
+ return {
823
+ routeTreeContent,
824
+ routeTree: acc.routeTree,
825
+ routeNodes: acc.routeNodes,
826
+ }
859
827
  }
860
828
 
861
829
  private async processRouteNodeFile(node: RouteNode): Promise<{
@@ -866,14 +834,15 @@ ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${get
866
834
  const result = await this.isRouteFileCacheFresh(node)
867
835
 
868
836
  if (result.status === 'fresh') {
869
- node.exports = result.cacheEntry.exports
870
837
  return {
871
- node,
872
- shouldWriteTree: result.exportsChanged,
838
+ node: result.cacheEntry.node,
839
+ shouldWriteTree: false,
873
840
  cacheEntry: result.cacheEntry,
874
841
  }
875
842
  }
876
843
 
844
+ const previousCacheEntry = result.cacheEntry
845
+
877
846
  const existingRouteFile = await this.fs.readFile(node.fullPath)
878
847
  if (existingRouteFile === 'file-not-existing') {
879
848
  throw new Error(`⚠️ File ${node.fullPath} does not exist`)
@@ -882,16 +851,18 @@ ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${get
882
851
  const updatedCacheEntry: RouteNodeCacheEntry = {
883
852
  fileContent: existingRouteFile.fileContent,
884
853
  mtimeMs: existingRouteFile.stat.mtimeMs,
885
- exports: [],
886
854
  routeId: node.routePath ?? '$$TSR_NO_ROUTE_PATH_ASSIGNED$$',
855
+ node,
887
856
  }
888
857
 
889
858
  const escapedRoutePath = node.routePath?.replaceAll('$', '$$') ?? ''
890
859
 
891
860
  let shouldWriteRouteFile = false
861
+ let shouldWriteTree = false
892
862
  // now we need to either scaffold the file or transform it
893
863
  if (!existingRouteFile.fileContent) {
894
864
  shouldWriteRouteFile = true
865
+ shouldWriteTree = true
895
866
  // Creating a new lazy route file
896
867
  if (node._fsRouteType === 'lazy') {
897
868
  const tLazyRouteTemplate = this.targetTemplate.lazyRoute
@@ -910,7 +881,6 @@ ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${get
910
881
  tsrExportEnd: tLazyRouteTemplate.imports.tsrExportEnd(),
911
882
  },
912
883
  )
913
- updatedCacheEntry.exports = ['Route']
914
884
  } else if (
915
885
  // Creating a new normal route file
916
886
  (['layout', 'static'] satisfies Array<FsRouteType>).some(
@@ -938,33 +908,40 @@ ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${get
938
908
  tsrExportEnd: tRouteTemplate.imports.tsrExportEnd(),
939
909
  },
940
910
  )
941
- updatedCacheEntry.exports = ['Route']
942
911
  } else {
943
912
  return null
944
913
  }
945
- } else {
946
- // transform the file
947
- const transformResult = await transform({
948
- source: updatedCacheEntry.fileContent,
949
- ctx: {
950
- target: this.config.target,
951
- routeId: escapedRoutePath,
952
- lazy: node._fsRouteType === 'lazy',
953
- verboseFileRoutes: !(this.config.verboseFileRoutes === false),
954
- },
955
- plugins: this.transformPlugins,
956
- })
914
+ }
915
+ // transform the file
916
+ const transformResult = await transform({
917
+ source: updatedCacheEntry.fileContent,
918
+ ctx: {
919
+ target: this.config.target,
920
+ routeId: escapedRoutePath,
921
+ lazy: node._fsRouteType === 'lazy',
922
+ verboseFileRoutes: !(this.config.verboseFileRoutes === false),
923
+ },
924
+ node,
925
+ })
957
926
 
958
- if (transformResult.result === 'error') {
959
- throw new Error(
960
- `Error transforming route file ${node.fullPath}: ${transformResult.error}`,
961
- )
962
- }
963
- updatedCacheEntry.exports = transformResult.exports
964
- if (transformResult.result === 'modified') {
965
- updatedCacheEntry.fileContent = transformResult.output
966
- shouldWriteRouteFile = true
967
- }
927
+ if (transformResult.result === 'no-route-export') {
928
+ this.logger.warn(
929
+ `Route file "${node.fullPath}" does not contain any route piece. This is likely a mistake.`,
930
+ )
931
+ return null
932
+ }
933
+ if (transformResult.result === 'error') {
934
+ throw new Error(
935
+ `Error transforming route file ${node.fullPath}: ${transformResult.error}`,
936
+ )
937
+ }
938
+ if (transformResult.result === 'modified') {
939
+ updatedCacheEntry.fileContent = transformResult.output
940
+ shouldWriteRouteFile = true
941
+ }
942
+
943
+ for (const plugin of this.plugins) {
944
+ plugin.afterTransform?.({ node, prevNode: previousCacheEntry?.node })
968
945
  }
969
946
 
970
947
  // file was changed
@@ -981,11 +958,6 @@ ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${get
981
958
  }
982
959
 
983
960
  this.routeNodeShadowCache.set(node.fullPath, updatedCacheEntry)
984
- node.exports = updatedCacheEntry.exports
985
- const shouldWriteTree = !deepEqual(
986
- result.cacheEntry?.exports,
987
- updatedCacheEntry.exports,
988
- )
989
961
  return {
990
962
  node,
991
963
  shouldWriteTree,
@@ -1113,7 +1085,6 @@ ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${get
1113
1085
  | {
1114
1086
  status: 'fresh'
1115
1087
  cacheEntry: RouteNodeCacheEntry
1116
- exportsChanged: boolean
1117
1088
  }
1118
1089
  | { status: 'stale'; cacheEntry?: RouteNodeCacheEntry }
1119
1090
  > {
@@ -1125,7 +1096,6 @@ ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${get
1125
1096
  this.routeNodeShadowCache.set(node.fullPath, fileChangedCache.cacheEntry)
1126
1097
  return {
1127
1098
  status: 'fresh',
1128
- exportsChanged: false,
1129
1099
  cacheEntry: fileChangedCache.cacheEntry,
1130
1100
  }
1131
1101
  }
@@ -1146,24 +1116,9 @@ ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${get
1146
1116
 
1147
1117
  if (shadowCacheFileChange.result === false) {
1148
1118
  // shadow cache has latest file state already
1149
- // compare shadowCache against cache to determine whether exports changed
1150
- // if they didn't, cache is fresh
1151
1119
  if (fileChangedCache.result === true) {
1152
- if (
1153
- deepEqual(
1154
- fileChangedCache.cacheEntry.exports,
1155
- shadowCacheFileChange.cacheEntry.exports,
1156
- )
1157
- ) {
1158
- return {
1159
- status: 'fresh',
1160
- exportsChanged: false,
1161
- cacheEntry: shadowCacheFileChange.cacheEntry,
1162
- }
1163
- }
1164
1120
  return {
1165
1121
  status: 'fresh',
1166
- exportsChanged: true,
1167
1122
  cacheEntry: shadowCacheFileChange.cacheEntry,
1168
1123
  }
1169
1124
  }
@@ -1181,9 +1136,7 @@ ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${get
1181
1136
  const result = await this.isRouteFileCacheFresh(node)
1182
1137
 
1183
1138
  if (result.status === 'fresh') {
1184
- node.exports = result.cacheEntry.exports
1185
1139
  this.routeNodeShadowCache.set(node.fullPath, result.cacheEntry)
1186
- return result.exportsChanged
1187
1140
  }
1188
1141
  const rootNodeFile = await this.fs.readFile(node.fullPath)
1189
1142
  if (rootNodeFile === 'file-not-existing') {
@@ -1193,8 +1146,8 @@ ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${get
1193
1146
  const updatedCacheEntry: RouteNodeCacheEntry = {
1194
1147
  fileContent: rootNodeFile.fileContent,
1195
1148
  mtimeMs: rootNodeFile.stat.mtimeMs,
1196
- exports: [],
1197
1149
  routeId: node.routePath ?? '$$TSR_NO_ROOT_ROUTE_PATH_ASSIGNED$$',
1150
+ node,
1198
1151
  }
1199
1152
 
1200
1153
  // scaffold the root route
@@ -1224,28 +1177,15 @@ ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${get
1224
1177
  updatedCacheEntry.mtimeMs = stats.mtimeMs
1225
1178
  }
1226
1179
 
1227
- const rootRouteExports: Array<string> = []
1228
- for (const plugin of this.pluginsWithTransform) {
1229
- const exportName = plugin.transformPlugin.exportName
1230
- // TODO we need to parse instead of just string match
1231
- // otherwise a commented out export will still be detected
1232
- if (rootNodeFile.fileContent.includes(`export const ${exportName}`)) {
1233
- rootRouteExports.push(exportName)
1234
- }
1235
- }
1236
-
1237
- updatedCacheEntry.exports = rootRouteExports
1238
- node.exports = rootRouteExports
1239
1180
  this.routeNodeShadowCache.set(node.fullPath, updatedCacheEntry)
1181
+ }
1240
1182
 
1241
- const shouldWriteTree = !deepEqual(
1242
- result.cacheEntry?.exports,
1243
- rootRouteExports,
1244
- )
1245
- return shouldWriteTree
1183
+ public async getCrawlingResult(): Promise<CrawlingResult | undefined> {
1184
+ await this.runPromise
1185
+ return this.crawlingResult
1246
1186
  }
1247
1187
 
1248
- private handleNode(node: RouteNode, acc: HandleNodeAccumulator) {
1188
+ private static handleNode(node: RouteNode, acc: HandleNodeAccumulator) {
1249
1189
  // Do not remove this as we need to set the lastIndex to 0 as it
1250
1190
  // is necessary to reset the regex's index when using the global flag
1251
1191
  // otherwise it might not match the next time it's used