@tanstack/router-generator 1.121.0-alpha.26 → 1.121.0-alpha.28

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 (75) hide show
  1. package/dist/cjs/config.cjs +5 -5
  2. package/dist/cjs/config.cjs.map +1 -1
  3. package/dist/cjs/config.d.cts +3 -3
  4. package/dist/cjs/filesystem/physical/getRouteNodes.cjs +18 -4
  5. package/dist/cjs/filesystem/physical/getRouteNodes.cjs.map +1 -1
  6. package/dist/cjs/filesystem/physical/getRouteNodes.d.cts +1 -0
  7. package/dist/cjs/filesystem/virtual/config.cjs.map +1 -1
  8. package/dist/cjs/filesystem/virtual/getRouteNodes.cjs +18 -12
  9. package/dist/cjs/filesystem/virtual/getRouteNodes.cjs.map +1 -1
  10. package/dist/cjs/filesystem/virtual/getRouteNodes.d.cts +4 -1
  11. package/dist/cjs/filesystem/virtual/loadConfigFile.cjs.map +1 -1
  12. package/dist/cjs/generator.cjs +147 -98
  13. package/dist/cjs/generator.cjs.map +1 -1
  14. package/dist/cjs/generator.d.cts +11 -9
  15. package/dist/cjs/index.d.cts +1 -1
  16. package/dist/cjs/logger.cjs.map +1 -1
  17. package/dist/cjs/plugin/default-generator-plugin.cjs +16 -10
  18. package/dist/cjs/plugin/default-generator-plugin.cjs.map +1 -1
  19. package/dist/cjs/template.cjs.map +1 -1
  20. package/dist/cjs/transform/default-transform-plugin.cjs +6 -4
  21. package/dist/cjs/transform/default-transform-plugin.cjs.map +1 -1
  22. package/dist/cjs/transform/transform.cjs +48 -32
  23. package/dist/cjs/transform/transform.cjs.map +1 -1
  24. package/dist/cjs/transform/transform.d.cts +1 -1
  25. package/dist/cjs/transform/types.d.cts +1 -1
  26. package/dist/cjs/transform/utils.cjs.map +1 -1
  27. package/dist/cjs/transform/utils.d.cts +1 -1
  28. package/dist/cjs/types.d.cts +5 -0
  29. package/dist/cjs/utils.cjs +43 -24
  30. package/dist/cjs/utils.cjs.map +1 -1
  31. package/dist/cjs/utils.d.cts +6 -0
  32. package/dist/esm/config.d.ts +3 -3
  33. package/dist/esm/config.js +6 -6
  34. package/dist/esm/config.js.map +1 -1
  35. package/dist/esm/filesystem/physical/getRouteNodes.d.ts +1 -0
  36. package/dist/esm/filesystem/physical/getRouteNodes.js +19 -5
  37. package/dist/esm/filesystem/physical/getRouteNodes.js.map +1 -1
  38. package/dist/esm/filesystem/virtual/config.js.map +1 -1
  39. package/dist/esm/filesystem/virtual/getRouteNodes.d.ts +4 -1
  40. package/dist/esm/filesystem/virtual/getRouteNodes.js +19 -13
  41. package/dist/esm/filesystem/virtual/getRouteNodes.js.map +1 -1
  42. package/dist/esm/filesystem/virtual/loadConfigFile.js.map +1 -1
  43. package/dist/esm/generator.d.ts +11 -9
  44. package/dist/esm/generator.js +150 -101
  45. package/dist/esm/generator.js.map +1 -1
  46. package/dist/esm/index.d.ts +1 -1
  47. package/dist/esm/logger.js.map +1 -1
  48. package/dist/esm/plugin/default-generator-plugin.js +16 -10
  49. package/dist/esm/plugin/default-generator-plugin.js.map +1 -1
  50. package/dist/esm/template.js.map +1 -1
  51. package/dist/esm/transform/default-transform-plugin.js +6 -4
  52. package/dist/esm/transform/default-transform-plugin.js.map +1 -1
  53. package/dist/esm/transform/transform.d.ts +1 -1
  54. package/dist/esm/transform/transform.js +48 -32
  55. package/dist/esm/transform/transform.js.map +1 -1
  56. package/dist/esm/transform/types.d.ts +1 -1
  57. package/dist/esm/transform/utils.d.ts +1 -1
  58. package/dist/esm/transform/utils.js.map +1 -1
  59. package/dist/esm/types.d.ts +5 -0
  60. package/dist/esm/utils.d.ts +6 -0
  61. package/dist/esm/utils.js +43 -24
  62. package/dist/esm/utils.js.map +1 -1
  63. package/package.json +5 -5
  64. package/src/config.ts +6 -5
  65. package/src/filesystem/physical/getRouteNodes.ts +31 -11
  66. package/src/filesystem/virtual/getRouteNodes.ts +32 -23
  67. package/src/generator.ts +196 -72
  68. package/src/index.ts +2 -0
  69. package/src/plugin/default-generator-plugin.ts +16 -3
  70. package/src/transform/default-transform-plugin.ts +5 -2
  71. package/src/transform/transform.ts +68 -43
  72. package/src/transform/types.ts +1 -1
  73. package/src/transform/utils.ts +1 -1
  74. package/src/types.ts +7 -0
  75. package/src/utils.ts +79 -31
package/src/generator.ts CHANGED
@@ -1,13 +1,17 @@
1
1
  import path from 'node:path'
2
2
  import * as fsp from 'node:fs/promises'
3
- import { mkdtempSync } from 'node:fs'
3
+ import { mkdirSync } from 'node:fs'
4
4
  import crypto from 'node:crypto'
5
5
  import { deepEqual, rootRouteId } from '@tanstack/router-core'
6
6
  import { logging } from './logger'
7
- import { getRouteNodes as physicalGetRouteNodes } from './filesystem/physical/getRouteNodes'
7
+ import {
8
+ isVirtualConfigFile,
9
+ getRouteNodes as physicalGetRouteNodes,
10
+ } from './filesystem/physical/getRouteNodes'
8
11
  import { getRouteNodes as virtualGetRouteNodes } from './filesystem/virtual/getRouteNodes'
9
12
  import { rootPathId } from './filesystem/physical/rootPathId'
10
13
  import {
14
+ buildFileRoutesByPathInterface,
11
15
  buildImportString,
12
16
  buildRouteTreeConfig,
13
17
  checkFileExists,
@@ -19,8 +23,6 @@ import {
19
23
  format,
20
24
  getResolvedRouteNodeVariableName,
21
25
  hasParentRoute,
22
- inferFullPath,
23
- inferPath,
24
26
  isRouteNodeValidForAugmentation,
25
27
  lowerCaseFirstChar,
26
28
  mergeImportDeclarations,
@@ -46,6 +48,7 @@ import type { TargetTemplate } from './template'
46
48
  import type {
47
49
  FsRouteType,
48
50
  GetRouteNodesResult,
51
+ GetRoutesByFileMapResult,
49
52
  HandleNodeAccumulator,
50
53
  ImportDeclaration,
51
54
  RouteNode,
@@ -55,8 +58,9 @@ import type { Logger } from './logger'
55
58
  import type { TransformPlugin } from './transform/types'
56
59
 
57
60
  interface fs {
58
- stat: (filePath: string) => Promise<{ mtimeMs: bigint }>
59
- mkdtempSync: (prefix: string) => string
61
+ stat: (
62
+ filePath: string,
63
+ ) => Promise<{ mtimeMs: bigint; mode: number; uid: number; gid: number }>
60
64
  rename: (oldPath: string, newPath: string) => Promise<void>
61
65
  writeFile: (filePath: string, content: string) => Promise<void>
62
66
  readFile: (
@@ -64,11 +68,20 @@ interface fs {
64
68
  ) => Promise<
65
69
  { stat: { mtimeMs: bigint }; fileContent: string } | 'file-not-existing'
66
70
  >
71
+ chmod: (filePath: string, mode: number) => Promise<void>
72
+ chown: (filePath: string, uid: number, gid: number) => Promise<void>
67
73
  }
68
74
 
69
75
  const DefaultFileSystem: fs = {
70
- stat: (filePath) => fsp.stat(filePath, { bigint: true }),
71
- mkdtempSync: mkdtempSync,
76
+ stat: async (filePath) => {
77
+ const res = await fsp.stat(filePath, { bigint: true })
78
+ return {
79
+ mtimeMs: res.mtimeMs,
80
+ mode: Number(res.mode),
81
+ uid: Number(res.uid),
82
+ gid: Number(res.gid),
83
+ }
84
+ },
72
85
  rename: (oldPath, newPath) => fsp.rename(oldPath, newPath),
73
86
  writeFile: (filePath, content) => fsp.writeFile(filePath, content),
74
87
  readFile: async (filePath: string) => {
@@ -87,6 +100,8 @@ const DefaultFileSystem: fs = {
87
100
  throw e
88
101
  }
89
102
  },
103
+ chmod: (filePath, mode) => fsp.chmod(filePath, mode),
104
+ chown: (filePath, uid, gid) => fsp.chown(filePath, uid, gid),
90
105
  }
91
106
 
92
107
  interface Rerun {
@@ -135,6 +150,7 @@ interface GeneratorCacheEntry {
135
150
 
136
151
  interface RouteNodeCacheEntry extends GeneratorCacheEntry {
137
152
  exports: Array<string>
153
+ routeId: string
138
154
  }
139
155
 
140
156
  type GeneratorRouteNodeCache = Map</** filePath **/ string, RouteNodeCacheEntry>
@@ -160,7 +176,7 @@ export class Generator {
160
176
 
161
177
  private root: string
162
178
  private routesDirectoryPath: string
163
- private tmpDir: string
179
+ private sessionId?: string
164
180
  private fs: fs
165
181
  private logger: Logger
166
182
  private generatedRouteTreePath: string
@@ -171,15 +187,13 @@ export class Generator {
171
187
  // this is just a cache for the transform plugins since we need them for each route file that is to be processed
172
188
  private transformPlugins: Array<TransformPlugin> = []
173
189
  private routeGroupPatternRegex = /\(.+\)/g
190
+ private physicalDirectories: Array<string> = []
174
191
 
175
192
  constructor(opts: { config: Config; root: string; fs?: fs }) {
176
193
  this.config = opts.config
177
194
  this.logger = logging({ disabled: this.config.disableLogging })
178
195
  this.root = opts.root
179
196
  this.fs = opts.fs || DefaultFileSystem
180
- this.tmpDir = this.fs.mkdtempSync(
181
- path.join(this.config.tmpDir, 'tanstack-router-'),
182
- )
183
197
  this.generatedRouteTreePath = path.resolve(this.config.generatedRouteTree)
184
198
  this.targetTemplate = getTargetTemplate(this.config)
185
199
 
@@ -204,17 +218,22 @@ export class Generator {
204
218
  : path.resolve(this.root, this.config.routesDirectory)
205
219
  }
206
220
 
221
+ public getRoutesByFileMap(): GetRoutesByFileMapResult {
222
+ return new Map(
223
+ [...this.routeNodeCache.entries()].map(([filePath, cacheEntry]) => [
224
+ filePath,
225
+ { routePath: cacheEntry.routeId },
226
+ ]),
227
+ )
228
+ }
229
+
207
230
  public async run(event?: GeneratorEvent): Promise<void> {
208
- // we are only interested in FileEvents that affect either the generated route tree or files inside the routes folder
209
- if (event && event.type !== 'rerun') {
210
- if (
211
- !(
212
- event.path === this.generatedRouteTreePath ||
213
- event.path.startsWith(this.routesDirectoryPath)
214
- )
215
- ) {
216
- return
217
- }
231
+ if (
232
+ event &&
233
+ event.type !== 'rerun' &&
234
+ !this.isFileRelevantForRouteTreeGeneration(event.path)
235
+ ) {
236
+ return
218
237
  }
219
238
  this.fileEventQueue.push(event ?? { type: 'rerun' })
220
239
  // only allow a single run at a time
@@ -234,7 +253,7 @@ export class Generator {
234
253
  await Promise.all(
235
254
  tempQueue.map(async (e) => {
236
255
  if (e.type === 'update') {
237
- let cacheEntry
256
+ let cacheEntry: GeneratorCacheEntry | undefined
238
257
  if (e.path === this.generatedRouteTreePath) {
239
258
  cacheEntry = this.routeTreeFileCache
240
259
  } else {
@@ -292,7 +311,7 @@ export class Generator {
292
311
  }
293
312
 
294
313
  private async generatorInternal() {
295
- let writeRouteTreeFile = false as boolean
314
+ let writeRouteTreeFile: boolean | 'force' = false
296
315
 
297
316
  let getRouteNodesResult: GetRouteNodesResult
298
317
 
@@ -302,7 +321,11 @@ export class Generator {
302
321
  getRouteNodesResult = await physicalGetRouteNodes(this.config, this.root)
303
322
  }
304
323
 
305
- const { rootRouteNode, routeNodes: beforeRouteNodes } = getRouteNodesResult
324
+ const {
325
+ rootRouteNode,
326
+ routeNodes: beforeRouteNodes,
327
+ physicalDirectories,
328
+ } = getRouteNodesResult
306
329
  if (rootRouteNode === undefined) {
307
330
  let errorMessage = `rootRouteNode must not be undefined. Make sure you've added your root route into the route-tree.`
308
331
  if (!this.config.virtualRouteConfig) {
@@ -310,6 +333,7 @@ export class Generator {
310
333
  }
311
334
  throw new Error(errorMessage)
312
335
  }
336
+ this.physicalDirectories = physicalDirectories
313
337
 
314
338
  writeRouteTreeFile = await this.handleRootNode(rootRouteNode)
315
339
 
@@ -376,9 +400,40 @@ export class Generator {
376
400
  }
377
401
  }
378
402
  writeRouteTreeFile = true
403
+ } else {
404
+ const routeTreeFileChange = await this.didFileChangeComparedToCache(
405
+ { path: this.generatedRouteTreePath },
406
+ this.routeTreeFileCache,
407
+ )
408
+ if (routeTreeFileChange.result !== false) {
409
+ writeRouteTreeFile = 'force'
410
+ if (routeTreeFileChange.result === true) {
411
+ const routeTreeFile = await this.fs.readFile(
412
+ this.generatedRouteTreePath,
413
+ )
414
+ if (routeTreeFile !== 'file-not-existing') {
415
+ this.routeTreeFileCache = {
416
+ fileContent: routeTreeFile.fileContent,
417
+ mtimeMs: routeTreeFile.stat.mtimeMs,
418
+ }
419
+ }
420
+ }
421
+ }
422
+ }
423
+
424
+ if (!writeRouteTreeFile) {
425
+ // only needs to be done if no other changes have been detected yet
426
+ // compare shadowCache and cache to identify deleted routes
427
+ for (const fullPath of this.routeNodeCache.keys()) {
428
+ if (!this.routeNodeShadowCache.has(fullPath)) {
429
+ writeRouteTreeFile = true
430
+ break
431
+ }
432
+ }
379
433
  }
380
434
 
381
435
  if (!writeRouteTreeFile) {
436
+ this.swapCaches()
382
437
  return
383
438
  }
384
439
 
@@ -393,7 +448,10 @@ export class Generator {
393
448
 
394
449
  let newMtimeMs: bigint | undefined
395
450
  if (this.routeTreeFileCache) {
396
- if (this.routeTreeFileCache.fileContent === routeTreeContent) {
451
+ if (
452
+ writeRouteTreeFile !== 'force' &&
453
+ this.routeTreeFileCache.fileContent === routeTreeContent
454
+ ) {
397
455
  // existing route tree file is already up-to-date, don't write it
398
456
  // we should only get here in the initial run when the route cache is not filled yet
399
457
  } else {
@@ -425,7 +483,10 @@ export class Generator {
425
483
  }
426
484
  }
427
485
 
428
- // now that we have finished this run, we can finally swap the caches
486
+ this.swapCaches()
487
+ }
488
+
489
+ private swapCaches() {
429
490
  this.routeNodeCache = this.routeNodeShadowCache
430
491
  this.routeNodeShadowCache = new Map()
431
492
  }
@@ -602,16 +663,18 @@ export class Generator {
602
663
  if (!this.config.disableTypes && hasMatchingRouteFiles) {
603
664
  fileRoutesByFullPathPerPlugin = [
604
665
  `export interface File${exportName}sByFullPath {
605
- ${[...createRouteNodesByFullPath(acc.routeNodes).entries()].map(
606
- ([fullPath, routeNode]) => {
666
+ ${[...createRouteNodesByFullPath(acc.routeNodes).entries()]
667
+ .filter(([fullPath]) => fullPath)
668
+ .map(([fullPath, routeNode]) => {
607
669
  return `'${fullPath}': typeof ${getResolvedRouteNodeVariableName(routeNode, exportName)}`
608
- },
609
- )}
670
+ })}
610
671
  }`,
611
672
  `export interface File${exportName}sByTo {
612
- ${[...createRouteNodesByTo(acc.routeNodes).entries()].map(([to, routeNode]) => {
613
- return `'${to}': typeof ${getResolvedRouteNodeVariableName(routeNode, exportName)}`
614
- })}
673
+ ${[...createRouteNodesByTo(acc.routeNodes).entries()]
674
+ .filter(([to]) => to)
675
+ .map(([to, routeNode]) => {
676
+ return `'${to}': typeof ${getResolvedRouteNodeVariableName(routeNode, exportName)}`
677
+ })}
615
678
  }`,
616
679
  `export interface File${exportName}sById {
617
680
  '${rootRouteId}': typeof root${exportName}Import,
@@ -621,9 +684,23 @@ ${[...createRouteNodesById(acc.routeNodes).entries()].map(([id, routeNode]) => {
621
684
  }`,
622
685
  `export interface File${exportName}Types {
623
686
  file${exportName}sByFullPath: File${exportName}sByFullPath
624
- fullPaths: ${acc.routeNodes.length > 0 ? [...createRouteNodesByFullPath(acc.routeNodes).keys()].map((fullPath) => `'${fullPath}'`).join('|') : 'never'}
687
+ fullPaths: ${
688
+ acc.routeNodes.length > 0
689
+ ? [...createRouteNodesByFullPath(acc.routeNodes).keys()]
690
+ .filter((fullPath) => fullPath)
691
+ .map((fullPath) => `'${fullPath}'`)
692
+ .join('|')
693
+ : 'never'
694
+ }
625
695
  file${exportName}sByTo: File${exportName}sByTo
626
- to: ${acc.routeNodes.length > 0 ? [...createRouteNodesByTo(acc.routeNodes).keys()].map((to) => `'${to}'`).join('|') : 'never'}
696
+ to: ${
697
+ acc.routeNodes.length > 0
698
+ ? [...createRouteNodesByTo(acc.routeNodes).keys()]
699
+ .filter((to) => to)
700
+ .map((to) => `'${to}'`)
701
+ .join('|')
702
+ : 'never'
703
+ }
627
704
  id: ${[`'${rootRouteId}'`, ...[...createRouteNodesById(acc.routeNodes).keys()].map((id) => `'${id}'`)].join('|')}
628
705
  file${exportName}sById: File${exportName}sById
629
706
  }`,
@@ -634,7 +711,13 @@ ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${get
634
711
 
635
712
  fileRoutesByPathInterfacePerPlugin = buildFileRoutesByPathInterface({
636
713
  ...plugin.moduleAugmentation({ generator: this }),
637
- routeNodes: preRouteNodes,
714
+ routeNodes:
715
+ this.config.verboseFileRoutes !== false
716
+ ? sortedRouteNodes
717
+ : [
718
+ ...routeFileResult.map(({ node }) => node),
719
+ ...sortedRouteNodes.filter((d) => d.isVirtual),
720
+ ],
638
721
  exportName,
639
722
  })
640
723
  }
@@ -784,6 +867,7 @@ ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${get
784
867
  fileContent: existingRouteFile.fileContent,
785
868
  mtimeMs: existingRouteFile.stat.mtimeMs,
786
869
  exports: [],
870
+ routeId: node.routePath ?? '$$TSR_NO_ROUTE_PATH_ASSIGNED$$',
787
871
  }
788
872
 
789
873
  const escapedRoutePath = node.routePath?.replaceAll('$', '$$') ?? ''
@@ -804,7 +888,7 @@ ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${get
804
888
  tLazyRouteTemplate.template(),
805
889
  {
806
890
  tsrImports: tLazyRouteTemplate.imports.tsrImports(),
807
- tsrPath: escapedRoutePath.replaceAll(/\{(.+)\}/gm, '$1'),
891
+ tsrPath: escapedRoutePath.replaceAll(/\{(.+?)\}/gm, '$1'),
808
892
  tsrExportStart:
809
893
  tLazyRouteTemplate.imports.tsrExportStart(escapedRoutePath),
810
894
  tsrExportEnd: tLazyRouteTemplate.imports.tsrExportEnd(),
@@ -832,7 +916,7 @@ ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${get
832
916
  tRouteTemplate.template(),
833
917
  {
834
918
  tsrImports: tRouteTemplate.imports.tsrImports(),
835
- tsrPath: escapedRoutePath.replaceAll(/\{(.+)\}/gm, '$1'),
919
+ tsrPath: escapedRoutePath.replaceAll(/\{(.+?)\}/gm, '$1'),
836
920
  tsrExportStart:
837
921
  tRouteTemplate.imports.tsrExportStart(escapedRoutePath),
838
922
  tsrExportEnd: tRouteTemplate.imports.tsrExportEnd(),
@@ -956,6 +1040,31 @@ ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${get
956
1040
  event: { type: 'update', path: opts.filePath },
957
1041
  })
958
1042
  }
1043
+ const newFileState = await this.fs.stat(tmpPath)
1044
+ if (newFileState.mode !== beforeStat.mode) {
1045
+ await this.fs.chmod(tmpPath, beforeStat.mode)
1046
+ }
1047
+ if (
1048
+ newFileState.uid !== beforeStat.uid ||
1049
+ newFileState.gid !== beforeStat.gid
1050
+ ) {
1051
+ try {
1052
+ await this.fs.chown(tmpPath, beforeStat.uid, beforeStat.gid)
1053
+ } catch (err) {
1054
+ if (
1055
+ typeof err === 'object' &&
1056
+ err !== null &&
1057
+ 'code' in err &&
1058
+ (err as any).code === 'EPERM'
1059
+ ) {
1060
+ console.warn(
1061
+ `[safeFileWrite] chown failed: ${(err as any).message}`,
1062
+ )
1063
+ } else {
1064
+ throw err
1065
+ }
1066
+ }
1067
+ }
959
1068
  } else {
960
1069
  if (await checkFileExists(opts.filePath)) {
961
1070
  throw rerun({
@@ -975,7 +1084,13 @@ ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${get
975
1084
  private getTempFileName(filePath: string) {
976
1085
  const absPath = path.resolve(filePath)
977
1086
  const hash = crypto.createHash('md5').update(absPath).digest('hex')
978
- return path.join(this.tmpDir, hash)
1087
+ // lazy initialize sessionId to only create tmpDir when it is first needed
1088
+ if (!this.sessionId) {
1089
+ // ensure the directory exists
1090
+ mkdirSync(this.config.tmpDir, { recursive: true })
1091
+ this.sessionId = crypto.randomBytes(4).toString('hex')
1092
+ }
1093
+ return path.join(this.config.tmpDir, `${this.sessionId}-${hash}`)
979
1094
  }
980
1095
 
981
1096
  private async isRouteFileCacheFresh(node: RouteNode): Promise<
@@ -1050,7 +1165,9 @@ ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${get
1050
1165
  const result = await this.isRouteFileCacheFresh(node)
1051
1166
 
1052
1167
  if (result.status === 'fresh') {
1053
- return false
1168
+ node.exports = result.cacheEntry.exports
1169
+ this.routeNodeShadowCache.set(node.fullPath, result.cacheEntry)
1170
+ return result.exportsChanged
1054
1171
  }
1055
1172
  const rootNodeFile = await this.fs.readFile(node.fullPath)
1056
1173
  if (rootNodeFile === 'file-not-existing') {
@@ -1061,6 +1178,7 @@ ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${get
1061
1178
  fileContent: rootNodeFile.fileContent,
1062
1179
  mtimeMs: rootNodeFile.stat.mtimeMs,
1063
1180
  exports: [],
1181
+ routeId: node.routePath ?? '$$TSR_NO_ROOT_ROUTE_PATH_ASSIGNED$$',
1064
1182
  }
1065
1183
 
1066
1184
  // scaffold the root route
@@ -1093,6 +1211,8 @@ ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${get
1093
1211
  const rootRouteExports: Array<string> = []
1094
1212
  for (const plugin of this.pluginsWithTransform) {
1095
1213
  const exportName = plugin.transformPlugin.exportName
1214
+ // TODO we need to parse instead of just string match
1215
+ // otherwise a commented out export will still be detected
1096
1216
  if (rootNodeFile.fileContent.includes(`export const ${exportName}`)) {
1097
1217
  rootRouteExports.push(exportName)
1098
1218
  }
@@ -1141,7 +1261,7 @@ ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${get
1141
1261
 
1142
1262
  node.isNonPath =
1143
1263
  lastRouteSegment.startsWith('_') ||
1144
- this.routeGroupPatternRegex.test(lastRouteSegment)
1264
+ split.every((part) => this.routeGroupPatternRegex.test(part))
1145
1265
 
1146
1266
  node.cleanedPath = removeGroups(
1147
1267
  removeUnderscores(removeLayoutSegments(node.path)) ?? '',
@@ -1252,38 +1372,42 @@ ${acc.routeTree.map((child) => `${child.variableName}${exportName}: typeof ${get
1252
1372
 
1253
1373
  acc.routeNodes.push(node)
1254
1374
  }
1255
- }
1256
1375
 
1257
- export function buildFileRoutesByPathInterface(opts: {
1258
- routeNodes: Array<RouteNode>
1259
- module: string
1260
- interfaceName: string
1261
- exportName: string
1262
- }): string {
1263
- return `declare module '${opts.module}' {
1264
- interface ${opts.interfaceName} {
1265
- ${opts.routeNodes
1266
- .map((routeNode) => {
1267
- const filePathId = routeNode.routePath
1268
- let preloaderRoute = ''
1269
-
1270
- if (routeNode.exports?.includes(opts.exportName)) {
1271
- preloaderRoute = `typeof ${routeNode.variableName}${opts.exportName}Import`
1272
- } else {
1273
- preloaderRoute = 'unknown'
1274
- }
1376
+ // only process files that are relevant for the route tree generation
1377
+ private isFileRelevantForRouteTreeGeneration(filePath: string): boolean {
1378
+ // the generated route tree file
1379
+ if (filePath === this.generatedRouteTreePath) {
1380
+ return true
1381
+ }
1275
1382
 
1276
- const parent = findParent(routeNode, opts.exportName)
1383
+ // files inside the routes folder
1384
+ if (filePath.startsWith(this.routesDirectoryPath)) {
1385
+ return true
1386
+ }
1277
1387
 
1278
- return `'${filePathId}': {
1279
- id: '${filePathId}'
1280
- path: '${inferPath(routeNode)}'
1281
- fullPath: '${inferFullPath(routeNode)}'
1282
- preLoaderRoute: ${preloaderRoute}
1283
- parentRoute: typeof ${parent}
1284
- }`
1285
- })
1286
- .join('\n')}
1388
+ // the virtual route config file passed into `virtualRouteConfig`
1389
+ if (
1390
+ typeof this.config.virtualRouteConfig === 'string' &&
1391
+ filePath === this.config.virtualRouteConfig
1392
+ ) {
1393
+ return true
1394
+ }
1395
+
1396
+ // this covers all files that are mounted via `virtualRouteConfig` or any `__virtual.ts` files
1397
+ if (this.routeNodeCache.has(filePath)) {
1398
+ return true
1399
+ }
1400
+
1401
+ // virtual config files such as`__virtual.ts`
1402
+ if (isVirtualConfigFile(path.basename(filePath))) {
1403
+ return true
1404
+ }
1405
+
1406
+ // route files inside directories mounted via `physical()` inside a virtual route config
1407
+ if (this.physicalDirectories.some((dir) => filePath.startsWith(dir))) {
1408
+ return true
1409
+ }
1410
+
1411
+ return false
1287
1412
  }
1288
- }`
1289
1413
  }
package/src/index.ts CHANGED
@@ -37,6 +37,8 @@ export {
37
37
  export type {
38
38
  RouteNode,
39
39
  GetRouteNodesResult,
40
+ GetRoutesByFileMapResult,
41
+ GetRoutesByFileMapResultValue,
40
42
  ImportDeclaration,
41
43
  ImportSpecifier,
42
44
  } from './types'
@@ -6,6 +6,7 @@ import {
6
6
  import type { ImportDeclaration } from '../types'
7
7
  import type { GeneratorPluginWithTransform } from './types'
8
8
 
9
+ const EXPORT_NAME = 'Route'
9
10
  export function defaultGeneratorPlugin(): GeneratorPluginWithTransform {
10
11
  return {
11
12
  name: 'default',
@@ -47,6 +48,16 @@ export function defaultGeneratorPlugin(): GeneratorPluginWithTransform {
47
48
  imports.push(typeImport)
48
49
  }
49
50
  }
51
+ const hasMatchingRouteFiles = opts.acc.routeNodes.length > 0
52
+ if (hasMatchingRouteFiles) {
53
+ // needs a virtual root route
54
+ if (!opts.rootRouteNode.exports?.includes(EXPORT_NAME)) {
55
+ imports.push({
56
+ specifiers: [{ imported: 'createRootRoute' }],
57
+ source: opts.generator.targetTemplate.fullPkg,
58
+ })
59
+ }
60
+ }
50
61
  return imports
51
62
  },
52
63
  moduleAugmentation: ({ generator }) => ({
@@ -54,7 +65,9 @@ export function defaultGeneratorPlugin(): GeneratorPluginWithTransform {
54
65
  interfaceName: 'FileRoutesByPath',
55
66
  }),
56
67
  onRouteTreesChanged: ({ routeTrees, generator }) => {
57
- const routeTree = routeTrees.find((tree) => tree.exportName === 'Route')
68
+ const routeTree = routeTrees.find(
69
+ (tree) => tree.exportName === EXPORT_NAME,
70
+ )
58
71
  if (!routeTree) {
59
72
  throw new Error(
60
73
  'No route tree found with export name "Route". Please ensure your routes are correctly defined.',
@@ -65,7 +78,7 @@ export function defaultGeneratorPlugin(): GeneratorPluginWithTransform {
65
78
  (d) =>
66
79
  d.children === undefined &&
67
80
  'lazy' !== d._fsRouteType &&
68
- d.exports?.includes('Route'),
81
+ d.exports?.includes(EXPORT_NAME),
69
82
  ),
70
83
  generator.config,
71
84
  )
@@ -83,7 +96,7 @@ export function defaultGeneratorPlugin(): GeneratorPluginWithTransform {
83
96
  `
84
97
  }
85
98
  },
86
- createRootRouteCode: () => `createRooRoute()`,
99
+ createRootRouteCode: () => `createRootRoute()`,
87
100
  createVirtualRouteCode: ({ node }) =>
88
101
  `createFileRoute('${node.routePath}')()`,
89
102
  config: ({ sortedRouteNodes }) => {
@@ -4,9 +4,10 @@ import type { TransformImportsConfig, TransformPlugin } from './types'
4
4
 
5
5
  const b = types.builders
6
6
 
7
+ const EXPORT_NAME = 'Route'
7
8
  export const defaultTransformPlugin: TransformPlugin = {
8
9
  name: 'default-transform',
9
- exportName: 'Route',
10
+ exportName: EXPORT_NAME,
10
11
  imports: (ctx) => {
11
12
  const imports: TransformImportsConfig = {}
12
13
  const targetModule = `@tanstack/${ctx.target}-router`
@@ -87,7 +88,9 @@ export const defaultTransformPlugin: TransformPlugin = {
87
88
  }
88
89
  }
89
90
  if (identifier === undefined) {
90
- throw new Error(`expected identifier to be present`)
91
+ throw new Error(
92
+ `expected identifier to be present in ${ctx.routeId} for export ${EXPORT_NAME}`,
93
+ )
91
94
  }
92
95
  if (identifier.name === 'createFileRoute' && ctx.lazy) {
93
96
  identifier.name = 'createLazyFileRoute'