@tanstack/start-plugin-core 1.20.3-alpha.1

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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +12 -0
  3. package/dist/cjs/compilers.cjs +402 -0
  4. package/dist/cjs/compilers.cjs.map +1 -0
  5. package/dist/cjs/compilers.d.cts +21 -0
  6. package/dist/cjs/extractHtmlScripts.cjs +35 -0
  7. package/dist/cjs/extractHtmlScripts.cjs.map +1 -0
  8. package/dist/cjs/extractHtmlScripts.d.cts +4 -0
  9. package/dist/cjs/index.cjs +15 -0
  10. package/dist/cjs/index.cjs.map +1 -0
  11. package/dist/cjs/index.d.cts +7 -0
  12. package/dist/cjs/nitro/build-nitro.cjs +18 -0
  13. package/dist/cjs/nitro/build-nitro.cjs.map +1 -0
  14. package/dist/cjs/nitro/build-nitro.d.cts +2 -0
  15. package/dist/cjs/nitro/build-sitemap.cjs +54 -0
  16. package/dist/cjs/nitro/build-sitemap.cjs.map +1 -0
  17. package/dist/cjs/nitro/build-sitemap.d.cts +9 -0
  18. package/dist/cjs/nitro/dev-server-plugin.cjs +128 -0
  19. package/dist/cjs/nitro/dev-server-plugin.cjs.map +1 -0
  20. package/dist/cjs/nitro/dev-server-plugin.d.cts +5 -0
  21. package/dist/cjs/nitro/nitro-plugin.cjs +128 -0
  22. package/dist/cjs/nitro/nitro-plugin.cjs.map +1 -0
  23. package/dist/cjs/nitro/nitro-plugin.d.cts +3 -0
  24. package/dist/cjs/plugin.cjs +117 -0
  25. package/dist/cjs/plugin.cjs.map +1 -0
  26. package/dist/cjs/plugin.d.cts +2713 -0
  27. package/dist/cjs/prerender.cjs +171 -0
  28. package/dist/cjs/prerender.cjs.map +1 -0
  29. package/dist/cjs/prerender.d.cts +8 -0
  30. package/dist/cjs/queue.cjs +131 -0
  31. package/dist/cjs/queue.cjs.map +1 -0
  32. package/dist/cjs/queue.d.cts +32 -0
  33. package/dist/cjs/routesManifestPlugin.cjs +165 -0
  34. package/dist/cjs/routesManifestPlugin.cjs.map +1 -0
  35. package/dist/cjs/routesManifestPlugin.d.cts +3 -0
  36. package/dist/cjs/schema.cjs +136 -0
  37. package/dist/cjs/schema.cjs.map +1 -0
  38. package/dist/cjs/schema.d.cts +8128 -0
  39. package/dist/cjs/start-compiler-plugin.cjs +72 -0
  40. package/dist/cjs/start-compiler-plugin.cjs.map +1 -0
  41. package/dist/cjs/start-compiler-plugin.d.cts +13 -0
  42. package/dist/cjs/start-server-routes-plugin/config.d.cts +49 -0
  43. package/dist/cjs/start-server-routes-plugin/plugin.cjs +608 -0
  44. package/dist/cjs/start-server-routes-plugin/plugin.cjs.map +1 -0
  45. package/dist/cjs/start-server-routes-plugin/plugin.d.cts +3 -0
  46. package/dist/cjs/start-server-routes-plugin/template.cjs +111 -0
  47. package/dist/cjs/start-server-routes-plugin/template.cjs.map +1 -0
  48. package/dist/cjs/start-server-routes-plugin/template.d.cts +34 -0
  49. package/dist/esm/compilers.d.ts +21 -0
  50. package/dist/esm/compilers.js +384 -0
  51. package/dist/esm/compilers.js.map +1 -0
  52. package/dist/esm/extractHtmlScripts.d.ts +4 -0
  53. package/dist/esm/extractHtmlScripts.js +18 -0
  54. package/dist/esm/extractHtmlScripts.js.map +1 -0
  55. package/dist/esm/index.d.ts +7 -0
  56. package/dist/esm/index.js +15 -0
  57. package/dist/esm/index.js.map +1 -0
  58. package/dist/esm/nitro/build-nitro.d.ts +2 -0
  59. package/dist/esm/nitro/build-nitro.js +18 -0
  60. package/dist/esm/nitro/build-nitro.js.map +1 -0
  61. package/dist/esm/nitro/build-sitemap.d.ts +9 -0
  62. package/dist/esm/nitro/build-sitemap.js +54 -0
  63. package/dist/esm/nitro/build-sitemap.js.map +1 -0
  64. package/dist/esm/nitro/dev-server-plugin.d.ts +5 -0
  65. package/dist/esm/nitro/dev-server-plugin.js +128 -0
  66. package/dist/esm/nitro/dev-server-plugin.js.map +1 -0
  67. package/dist/esm/nitro/nitro-plugin.d.ts +3 -0
  68. package/dist/esm/nitro/nitro-plugin.js +128 -0
  69. package/dist/esm/nitro/nitro-plugin.js.map +1 -0
  70. package/dist/esm/plugin.d.ts +2713 -0
  71. package/dist/esm/plugin.js +117 -0
  72. package/dist/esm/plugin.js.map +1 -0
  73. package/dist/esm/prerender.d.ts +8 -0
  74. package/dist/esm/prerender.js +171 -0
  75. package/dist/esm/prerender.js.map +1 -0
  76. package/dist/esm/queue.d.ts +32 -0
  77. package/dist/esm/queue.js +131 -0
  78. package/dist/esm/queue.js.map +1 -0
  79. package/dist/esm/routesManifestPlugin.d.ts +3 -0
  80. package/dist/esm/routesManifestPlugin.js +165 -0
  81. package/dist/esm/routesManifestPlugin.js.map +1 -0
  82. package/dist/esm/schema.d.ts +8128 -0
  83. package/dist/esm/schema.js +136 -0
  84. package/dist/esm/schema.js.map +1 -0
  85. package/dist/esm/start-compiler-plugin.d.ts +13 -0
  86. package/dist/esm/start-compiler-plugin.js +72 -0
  87. package/dist/esm/start-compiler-plugin.js.map +1 -0
  88. package/dist/esm/start-server-routes-plugin/config.d.ts +49 -0
  89. package/dist/esm/start-server-routes-plugin/plugin.d.ts +3 -0
  90. package/dist/esm/start-server-routes-plugin/plugin.js +608 -0
  91. package/dist/esm/start-server-routes-plugin/plugin.js.map +1 -0
  92. package/dist/esm/start-server-routes-plugin/template.d.ts +34 -0
  93. package/dist/esm/start-server-routes-plugin/template.js +111 -0
  94. package/dist/esm/start-server-routes-plugin/template.js.map +1 -0
  95. package/package.json +72 -0
  96. package/src/compilers.ts +759 -0
  97. package/src/extractHtmlScripts.ts +19 -0
  98. package/src/index.ts +15 -0
  99. package/src/nitro/build-nitro.ts +27 -0
  100. package/src/nitro/build-sitemap.ts +79 -0
  101. package/src/nitro/dev-server-plugin.ts +159 -0
  102. package/src/nitro/nitro-plugin.ts +161 -0
  103. package/src/plugin.ts +145 -0
  104. package/src/prerender.ts +245 -0
  105. package/src/queue.ts +153 -0
  106. package/src/routesManifestPlugin.ts +216 -0
  107. package/src/schema.ts +193 -0
  108. package/src/start-compiler-plugin.ts +111 -0
  109. package/src/start-server-routes-plugin/config.ts +8 -0
  110. package/src/start-server-routes-plugin/plugin.ts +890 -0
  111. package/src/start-server-routes-plugin/template.ts +164 -0
@@ -0,0 +1,890 @@
1
+ import path, { isAbsolute, join, normalize } from 'node:path'
2
+ import fs from 'node:fs'
3
+ import fsp from 'node:fs/promises'
4
+ import {
5
+ format,
6
+ logging,
7
+ multiSortBy,
8
+ physicalGetRouteNodes,
9
+ removeExt,
10
+ removeUnderscores,
11
+ replaceBackslash,
12
+ resetRegex,
13
+ rootPathId,
14
+ routePathToVariable,
15
+ trimPathLeft,
16
+ virtualGetRouteNodes,
17
+ writeIfDifferent,
18
+ } from '@tanstack/router-generator'
19
+ import { rootRouteId } from '@tanstack/router-core'
20
+ import { fillTemplate, getTargetTemplate } from './template'
21
+ import type { GetRouteNodesResult, RouteNode } from '@tanstack/router-generator'
22
+ import type { Config } from './config'
23
+ import type { Plugin } from 'vite'
24
+
25
+ let lock = false
26
+ const checkLock = () => lock
27
+ const setLock = (bool: boolean) => {
28
+ lock = bool
29
+ }
30
+
31
+ export function TanStackStartServerRoutesVite(config: Config): Plugin {
32
+ let ROOT: string = process.cwd()
33
+
34
+ const getRoutesDirectoryPath = () => {
35
+ return isAbsolute(config.routesDirectory)
36
+ ? config.routesDirectory
37
+ : join(ROOT, config.routesDirectory)
38
+ }
39
+
40
+ const generate = async () => {
41
+ if (checkLock()) {
42
+ return
43
+ }
44
+
45
+ setLock(true)
46
+
47
+ try {
48
+ await generator(config, ROOT)
49
+ } catch (err) {
50
+ console.error(err)
51
+ console.info()
52
+ } finally {
53
+ setLock(false)
54
+ }
55
+ }
56
+
57
+ const handleFile = async (file: string) => {
58
+ const filePath = normalize(file)
59
+
60
+ const routesDirectoryPath = getRoutesDirectoryPath()
61
+ if (filePath.startsWith(routesDirectoryPath)) {
62
+ await generate()
63
+ }
64
+ }
65
+
66
+ return {
67
+ name: 'tanstack-start-server-routes-plugin',
68
+ configureServer(server) {
69
+ server.watcher.on('all', (event, path) => {
70
+ handleFile(path)
71
+ })
72
+ },
73
+ configResolved(config) {
74
+ ROOT = config.root
75
+ },
76
+ async buildStart() {
77
+ await generate()
78
+ // if (this.environment.name === 'server') {
79
+ // }
80
+ },
81
+ sharedDuringBuild: true,
82
+ resolveId(id) {
83
+ if (id === 'tanstack:server-routes') {
84
+ const generatedRouteTreePath = getGeneratedRouteTreePath(ROOT)
85
+ return generatedRouteTreePath
86
+ }
87
+ return null
88
+ },
89
+ }
90
+ }
91
+
92
+ // Maybe import this from `@tanstack/router-core` in the future???
93
+ let latestTask = 0
94
+ const routeGroupPatternRegex = /\(.+\)/g
95
+ const possiblyNestedRouteGroupPatternRegex = /\([^/]+\)\/?/g
96
+
97
+ let isFirst = false
98
+ let skipMessage = false
99
+
100
+ function getGeneratedRouteTreePath(root: string) {
101
+ return path.resolve(root, '.tanstack-start/server-routes/routeTree.gen.ts')
102
+ }
103
+
104
+ async function generator(config: Config, root: string) {
105
+ const generatedServerRouteTreePath = getGeneratedRouteTreePath(root)
106
+ const ROUTE_TEMPLATE = getTargetTemplate(config.target)
107
+ const logger = logging({ disabled: config.disableLogging })
108
+
109
+ if (!isFirst) {
110
+ // logger.log('♻️ Generating server routes...')
111
+ isFirst = true
112
+ } else if (skipMessage) {
113
+ skipMessage = false
114
+ } else {
115
+ // logger.log('♻️ Regenerating server routes...')
116
+ }
117
+
118
+ const taskId = latestTask + 1
119
+ latestTask = taskId
120
+
121
+ const checkLatest = () => {
122
+ if (latestTask !== taskId) {
123
+ skipMessage = true
124
+ return false
125
+ }
126
+
127
+ return true
128
+ }
129
+
130
+ const start = Date.now()
131
+
132
+ let getRouteNodesResult: GetRouteNodesResult
133
+
134
+ if (config.virtualRouteConfig) {
135
+ getRouteNodesResult = await virtualGetRouteNodes(config, root)
136
+ } else {
137
+ getRouteNodesResult = await physicalGetRouteNodes(config, root)
138
+ }
139
+
140
+ const { rootRouteNode, routeNodes: beforeRouteNodes } = getRouteNodesResult
141
+ if (rootRouteNode === undefined) {
142
+ let errorMessage = `rootRouteNode must not be undefined. Make sure you've added your root route into the route-tree.`
143
+ if (!config.virtualRouteConfig) {
144
+ errorMessage += `\nMake sure that you add a "${rootPathId}.tsx" file to your routes directory.\nAdd the file in: "${config.routesDirectory}/${rootPathId}.tsx"`
145
+ }
146
+ throw new Error(errorMessage)
147
+ }
148
+
149
+ const preRouteNodes = multiSortBy(beforeRouteNodes, [
150
+ (d) => (d.routePath === '/' ? -1 : 1),
151
+ (d) => d.routePath?.split('/').length,
152
+ (d) =>
153
+ d.filePath.match(new RegExp(`[./]${config.indexToken}[.]`)) ? 1 : -1,
154
+ (d) =>
155
+ d.filePath.match(
156
+ /[./](component|errorComponent|pendingComponent|loader|lazy)[.]/,
157
+ )
158
+ ? 1
159
+ : -1,
160
+ (d) =>
161
+ d.filePath.match(new RegExp(`[./]${config.routeToken}[.]`)) ? -1 : 1,
162
+ (d) => (d.routePath?.endsWith('/') ? -1 : 1),
163
+ (d) => d.routePath,
164
+ ]).filter((d) => ![`/${rootPathId}`].includes(d.routePath || ''))
165
+
166
+ const routeTree: Array<RouteNode> = []
167
+
168
+ // Loop over the flat list of routeNodes and
169
+ // build up a tree based on the routeNodes' routePath
170
+ const routeNodes: Array<RouteNode> = []
171
+
172
+ // the handleRootNode function is not being collapsed into the handleNode function
173
+ // because it requires only a subset of the logic that the handleNode function requires
174
+ // and it's easier to read and maintain this way
175
+ const handleRootNode = async (node?: RouteNode) => {
176
+ if (!node) {
177
+ // currently this is not being handled, but it could be in the future
178
+ // for example to handle a virtual root route
179
+ return
180
+ }
181
+
182
+ // from here on, we are only handling the root node that's present in the file system
183
+ const routeCode = fs.readFileSync(node.fullPath, 'utf-8')
184
+
185
+ if (!routeCode) {
186
+ const _rootTemplate = ROUTE_TEMPLATE.rootRoute
187
+ const replaced = await fillTemplate(config, _rootTemplate.template(), {
188
+ tsrImports: _rootTemplate.imports.tsrImports(),
189
+ tsrPath: rootPathId,
190
+ tsrExportStart: _rootTemplate.imports.tsrExportStart(),
191
+ tsrExportEnd: _rootTemplate.imports.tsrExportEnd(),
192
+ })
193
+
194
+ await writeIfDifferent(
195
+ node.fullPath,
196
+ '', // Empty string because the file doesn't exist yet
197
+ replaced,
198
+ {
199
+ beforeWrite: () => {
200
+ // logger.log(`🟡 Creating ${node.fullPath}`)
201
+ },
202
+ },
203
+ )
204
+ }
205
+ }
206
+
207
+ await handleRootNode(rootRouteNode)
208
+
209
+ const handleNode = async (node: RouteNode) => {
210
+ // Do not remove this as we need to set the lastIndex to 0 as it
211
+ // is necessary to reset the regex's index when using the global flag
212
+ // otherwise it might not match the next time it's used
213
+ resetRegex(routeGroupPatternRegex)
214
+
215
+ let parentRoute = hasParentRoute(routeNodes, node, node.routePath)
216
+
217
+ // if the parent route is a virtual parent route, we need to find the real parent route
218
+ if (parentRoute?.isVirtualParentRoute && parentRoute.children?.length) {
219
+ // only if this sub-parent route returns a valid parent route, we use it, if not leave it as it
220
+ const possibleParentRoute = hasParentRoute(
221
+ parentRoute.children,
222
+ node,
223
+ node.routePath,
224
+ )
225
+ if (possibleParentRoute) {
226
+ parentRoute = possibleParentRoute
227
+ }
228
+ }
229
+
230
+ if (parentRoute) node.parent = parentRoute
231
+
232
+ node.path = determineNodePath(node)
233
+
234
+ const trimmedPath = trimPathLeft(node.path ?? '')
235
+
236
+ const split = trimmedPath.split('/')
237
+ const lastRouteSegment = split[split.length - 1] ?? trimmedPath
238
+
239
+ node.isNonPath =
240
+ lastRouteSegment.startsWith('_') ||
241
+ routeGroupPatternRegex.test(lastRouteSegment)
242
+
243
+ node.cleanedPath = removeGroups(
244
+ removeUnderscores(removeLayoutSegments(node.path)) ?? '',
245
+ )
246
+
247
+ const routeCode = fs.readFileSync(node.fullPath, 'utf-8')
248
+
249
+ // Ensure the boilerplate for the route exists, which can be skipped for virtual parent routes and virtual routes
250
+ if (!node.isVirtualParentRoute && !node.isVirtual) {
251
+ // const escapedRoutePath = node.routePath?.replaceAll('$', '$$') ?? ''
252
+ // let replaced = routeCode
253
+ // await writeIfDifferent(node.fullPath, routeCode, replaced, {
254
+ // beforeWrite: () => {
255
+ // // logger.log(`🟡 Updating ${node.fullPath}`)
256
+ // },
257
+ // })
258
+ }
259
+
260
+ const cleanedPathIsEmpty = (node.cleanedPath || '').length === 0
261
+ const nonPathRoute =
262
+ node._fsRouteType === 'pathless_layout' && node.isNonPath
263
+
264
+ node.isVirtualParentRequired =
265
+ node._fsRouteType === 'pathless_layout' || nonPathRoute
266
+ ? !cleanedPathIsEmpty
267
+ : false
268
+
269
+ if (!node.isVirtual && node.isVirtualParentRequired) {
270
+ const parentRoutePath = removeLastSegmentFromPath(node.routePath) || '/'
271
+ const parentVariableName = routePathToVariable(parentRoutePath)
272
+
273
+ const anchorRoute = routeNodes.find(
274
+ (d) => d.routePath === parentRoutePath,
275
+ )
276
+
277
+ if (!anchorRoute) {
278
+ const parentNode: RouteNode = {
279
+ ...node,
280
+ path: removeLastSegmentFromPath(node.path) || '/',
281
+ filePath: removeLastSegmentFromPath(node.filePath) || '/',
282
+ fullPath: removeLastSegmentFromPath(node.fullPath) || '/',
283
+ routePath: parentRoutePath,
284
+ variableName: parentVariableName,
285
+ isVirtual: true,
286
+ _fsRouteType: 'layout', // layout since this route will wrap other routes
287
+ isVirtualParentRoute: true,
288
+ isVirtualParentRequired: false,
289
+ }
290
+
291
+ parentNode.children = parentNode.children ?? []
292
+ parentNode.children.push(node)
293
+
294
+ node.parent = parentNode
295
+
296
+ if (node._fsRouteType === 'pathless_layout') {
297
+ // since `node.path` is used as the `id` on the route definition, we need to update it
298
+ node.path = determineNodePath(node)
299
+ }
300
+
301
+ await handleNode(parentNode)
302
+ } else {
303
+ anchorRoute.children = anchorRoute.children ?? []
304
+ anchorRoute.children.push(node)
305
+
306
+ node.parent = anchorRoute
307
+ }
308
+ }
309
+
310
+ if (
311
+ !routeCode
312
+ .split('\n')
313
+ .some((line) => line.trim().startsWith('export const ServerRoute'))
314
+ ) {
315
+ return
316
+ }
317
+
318
+ if (node.parent) {
319
+ if (!node.isVirtualParentRequired) {
320
+ node.parent.children = node.parent.children ?? []
321
+ node.parent.children.push(node)
322
+ }
323
+ } else {
324
+ routeTree.push(node)
325
+ }
326
+
327
+ routeNodes.push(node)
328
+ }
329
+
330
+ for (const node of preRouteNodes) {
331
+ await handleNode(node)
332
+ }
333
+
334
+ // This is run against the `routeNodes` array since it
335
+ // has the accumulated (intended) Server Route nodes
336
+ // Since TSR allows multiple way of defining a route,
337
+ // we need to ensure that a user hasn't defined the
338
+ // same route in multiple ways (i.e. `flat`, `nested`, `virtual`)
339
+ checkRouteFullPathUniqueness(routeNodes, config)
340
+
341
+ function buildRouteTreeConfig(nodes: Array<RouteNode>, depth = 1): string {
342
+ const children = nodes.map((node) => {
343
+ if (node._fsRouteType === '__root') {
344
+ return
345
+ }
346
+
347
+ if (node._fsRouteType === 'pathless_layout' && !node.children?.length) {
348
+ return
349
+ }
350
+
351
+ const route = `${node.variableName}Route`
352
+
353
+ if (node.children?.length) {
354
+ const childConfigs = buildRouteTreeConfig(node.children, depth + 1)
355
+
356
+ const childrenDeclaration = `interface ${route}Children {
357
+ ${node.children.map((child) => `${child.variableName}Route: typeof ${getResolvedRouteNodeVariableName(child)}`).join(',')}
358
+ }`
359
+
360
+ const children = `const ${route}Children: ${route}Children = {
361
+ ${node.children.map((child) => `${child.variableName}Route: ${getResolvedRouteNodeVariableName(child)}`).join(',')}
362
+ }`
363
+
364
+ const routeWithChildren = `const ${route}WithChildren = ${route}._addFileChildren(${route}Children)`
365
+
366
+ return [
367
+ childConfigs,
368
+ childrenDeclaration,
369
+ children,
370
+ routeWithChildren,
371
+ ].join('\n\n')
372
+ }
373
+
374
+ return undefined
375
+ })
376
+
377
+ return children.filter(Boolean).join('\n\n')
378
+ }
379
+
380
+ const routeConfigChildrenText = buildRouteTreeConfig(routeTree)
381
+
382
+ const sortedRouteNodes = multiSortBy(routeNodes, [
383
+ (d) => (d.routePath?.includes(`/${rootPathId}`) ? -1 : 1),
384
+ (d) => d.routePath?.split('/').length,
385
+ (d) => (d.routePath?.endsWith(config.indexToken) ? -1 : 1),
386
+ (d) => d,
387
+ ])
388
+
389
+ const imports = Object.entries({
390
+ createFileRoute: sortedRouteNodes.some((d) => d.isVirtual),
391
+ })
392
+ .filter((d) => d[1])
393
+ .map((d) => d[0])
394
+
395
+ const virtualRouteNodes = sortedRouteNodes.filter((d) => d.isVirtual)
396
+
397
+ function getImportPath(node: RouteNode) {
398
+ return replaceBackslash(
399
+ removeExt(
400
+ path.relative(
401
+ path.dirname(generatedServerRouteTreePath),
402
+ path.resolve(config.routesDirectory, node.filePath),
403
+ ),
404
+ ),
405
+ )
406
+ }
407
+
408
+ const rootRouteExists = fs.existsSync(rootRouteNode.fullPath)
409
+ const rootRouteCode = rootRouteExists
410
+ ? fs.readFileSync(rootRouteNode.fullPath, 'utf-8')
411
+ : ''
412
+ const hasServerRootRoute =
413
+ rootRouteExists && rootRouteCode.includes('export const ServerRoute')
414
+
415
+ const routeImports = [
416
+ ...config.routeTreeFileHeader,
417
+ `// This file was automatically generated by TanStack Router.
418
+ // You should NOT make any changes in this file as it will be overwritten.
419
+ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.`,
420
+ imports.length
421
+ ? `import { ${imports.join(', ')} } from '${ROUTE_TEMPLATE.fullPkg}'\n`
422
+ : '',
423
+ '// Import Routes',
424
+ [
425
+ `import type { FileRoutesByPath, CreateServerFileRoute } from '${ROUTE_TEMPLATE.fullPkg}'`,
426
+ `import { createServerRoute, createServerFileRoute } from '${ROUTE_TEMPLATE.fullPkg}'`,
427
+ hasServerRootRoute
428
+ ? `import { ServerRoute as rootRouteImport } from './${getImportPath(rootRouteNode)}'`
429
+ : '',
430
+ ...sortedRouteNodes
431
+ .filter((d) => !d.isVirtual)
432
+ .map((node) => {
433
+ return `import { ServerRoute as ${
434
+ node.variableName
435
+ }RouteImport } from './${getImportPath(node)}'`
436
+ }),
437
+ ].join('\n'),
438
+ virtualRouteNodes.length ? '// Create Virtual Routes' : '',
439
+ virtualRouteNodes
440
+ .map((node) => {
441
+ return `const ${
442
+ node.variableName
443
+ }RouteImport = createFileRoute('${node.routePath}')()`
444
+ })
445
+ .join('\n'),
446
+ '// Create/Update Routes',
447
+ !hasServerRootRoute
448
+ ? `
449
+ const rootRoute = createServerRoute()
450
+ `
451
+ : '',
452
+ sortedRouteNodes
453
+ .map((node) => {
454
+ return [
455
+ [
456
+ `const ${node.variableName}Route = ${node.variableName}RouteImport.update({
457
+ ${[
458
+ `id: '${node.path}'`,
459
+ !node.isNonPath ? `path: '${node.cleanedPath}'` : undefined,
460
+ `getParentRoute: () => ${node.parent?.variableName ?? 'root'}Route`,
461
+ ]
462
+ .filter(Boolean)
463
+ .join(',')}
464
+ } as any)`,
465
+ ].join(''),
466
+ ].join('\n\n')
467
+ })
468
+ .join('\n\n'),
469
+ '',
470
+
471
+ '// Populate the FileRoutesByPath interface',
472
+ `declare module '${ROUTE_TEMPLATE.fullPkg}' {
473
+ interface FileRoutesByPath {
474
+ ${routeNodes
475
+ .map((routeNode) => {
476
+ const filePathId = routeNode.routePath
477
+
478
+ return `'${filePathId}': {
479
+ id: '${filePathId}'
480
+ path: '${inferPath(routeNode)}'
481
+ fullPath: '${inferFullPath(routeNode)}'
482
+ preLoaderRoute: typeof ${routeNode.variableName}RouteImport
483
+ parentRoute: typeof ${
484
+ routeNode.isVirtualParentRequired
485
+ ? `${routeNode.parent?.variableName}Route`
486
+ : routeNode.parent?.variableName
487
+ ? `${routeNode.parent.variableName}RouteImport`
488
+ : 'rootRoute'
489
+ }
490
+ }`
491
+ })
492
+ .join('\n')}
493
+ }
494
+ }`,
495
+ `// Add type-safety to the createFileRoute function across the route tree`,
496
+ routeNodes
497
+ .map((routeNode) => {
498
+ return `declare module './${getImportPath(routeNode)}' {
499
+ const createServerFileRoute: CreateServerFileRoute<
500
+ FileRoutesByPath['${routeNode.routePath}']['parentRoute'],
501
+ FileRoutesByPath['${routeNode.routePath}']['id'],
502
+ FileRoutesByPath['${routeNode.routePath}']['path'],
503
+ FileRoutesByPath['${routeNode.routePath}']['fullPath'],
504
+ ${routeNode.children?.length ? `${routeNode.variableName}RouteChildren` : 'unknown'}
505
+ >
506
+ }`
507
+ })
508
+ .join('\n'),
509
+ '// Create and export the route tree',
510
+ routeConfigChildrenText,
511
+ `export interface FileRoutesByFullPath {
512
+ ${[...createRouteNodesByFullPath(routeNodes).entries()].map(
513
+ ([fullPath, routeNode]) => {
514
+ return `'${fullPath}': typeof ${getResolvedRouteNodeVariableName(routeNode)}`
515
+ },
516
+ )}
517
+ }`,
518
+ `export interface FileRoutesByTo {
519
+ ${[...createRouteNodesByTo(routeNodes).entries()].map(([to, routeNode]) => {
520
+ return `'${to}': typeof ${getResolvedRouteNodeVariableName(routeNode)}`
521
+ })}
522
+ }`,
523
+ `export interface FileRoutesById {
524
+ '${rootRouteId}': typeof rootRoute,
525
+ ${[...createRouteNodesById(routeNodes).entries()].map(([id, routeNode]) => {
526
+ return `'${id}': typeof ${getResolvedRouteNodeVariableName(routeNode)}`
527
+ })}
528
+ }`,
529
+ `export interface FileRouteTypes {
530
+ fileRoutesByFullPath: FileRoutesByFullPath
531
+ fullPaths: ${routeNodes.length > 0 ? [...createRouteNodesByFullPath(routeNodes).keys()].map((fullPath) => `'${fullPath}'`).join('|') : 'never'}
532
+ fileRoutesByTo: FileRoutesByTo
533
+ to: ${routeNodes.length > 0 ? [...createRouteNodesByTo(routeNodes).keys()].map((to) => `'${to}'`).join('|') : 'never'}
534
+ id: ${[`'${rootRouteId}'`, ...[...createRouteNodesById(routeNodes).keys()].map((id) => `'${id}'`)].join('|')}
535
+ fileRoutesById: FileRoutesById
536
+ }`,
537
+ `export interface RootRouteChildren {
538
+ ${routeTree.map((child) => `${child.variableName}Route: typeof ${getResolvedRouteNodeVariableName(child)}`).join(',')}
539
+ }`,
540
+ `const rootRouteChildren: RootRouteChildren = {
541
+ ${routeTree.map((child) => `${child.variableName}Route: ${getResolvedRouteNodeVariableName(child)}`).join(',')}
542
+ }`,
543
+ `export const routeTree = rootRoute._addFileChildren(rootRouteChildren)._addFileTypes<FileRouteTypes>()`,
544
+ ]
545
+ .filter(Boolean)
546
+ .join('\n\n')
547
+
548
+ const createRouteManifest = () => {
549
+ const routesManifest = {
550
+ [rootRouteId]: {
551
+ filePath: rootRouteNode.filePath,
552
+ children: routeTree.map((d) => d.routePath),
553
+ },
554
+ ...Object.fromEntries(
555
+ routeNodes.map((d) => {
556
+ const filePathId = d.routePath
557
+
558
+ return [
559
+ filePathId,
560
+ {
561
+ filePath: d.filePath,
562
+ parent: d.parent?.routePath ? d.parent.routePath : undefined,
563
+ children: d.children?.map((childRoute) => childRoute.routePath),
564
+ },
565
+ ]
566
+ }),
567
+ ),
568
+ }
569
+
570
+ return JSON.stringify(
571
+ {
572
+ routes: routesManifest,
573
+ },
574
+ null,
575
+ 2,
576
+ )
577
+ }
578
+
579
+ const includeManifest = ['react', 'solid']
580
+ const routeConfigFileContent = !includeManifest.includes(config.target)
581
+ ? routeImports
582
+ : [
583
+ routeImports,
584
+ '\n',
585
+ '/* ROUTE_MANIFEST_START',
586
+ createRouteManifest(),
587
+ 'ROUTE_MANIFEST_END */',
588
+ ].join('\n')
589
+
590
+ if (!checkLatest()) return
591
+
592
+ const existingRouteTreeContent = await fsp
593
+ .readFile(path.resolve(generatedServerRouteTreePath), 'utf-8')
594
+ .catch((err) => {
595
+ if (err.code === 'ENOENT') {
596
+ return ''
597
+ }
598
+
599
+ throw err
600
+ })
601
+
602
+ if (!checkLatest()) return
603
+
604
+ // Ensure the directory exists
605
+ await fsp.mkdir(path.dirname(path.resolve(generatedServerRouteTreePath)), {
606
+ recursive: true,
607
+ })
608
+
609
+ if (!checkLatest()) return
610
+
611
+ // Write the route tree file, if it has changed
612
+ const routeTreeWriteResult = await writeIfDifferent(
613
+ path.resolve(generatedServerRouteTreePath),
614
+ await format(existingRouteTreeContent, config),
615
+ await format(routeConfigFileContent, config),
616
+ {
617
+ beforeWrite: () => {
618
+ // logger.log(`🟡 Updating ${generatedRouteTreePath}`)
619
+ },
620
+ },
621
+ )
622
+
623
+ // Write declaration file
624
+ const startDeclarationFilePath = path.join(
625
+ path.resolve(root, config.srcDirectory),
626
+ 'tanstack-start.d.ts',
627
+ )
628
+ const serverRoutesRelativePath = removeExt(
629
+ path.relative(
630
+ path.dirname(startDeclarationFilePath),
631
+ generatedServerRouteTreePath,
632
+ ),
633
+ )
634
+ const startDeclarationFileContent = buildStartDeclarationFile({
635
+ serverRoutesRelativePath,
636
+ })
637
+ if (!fs.existsSync(startDeclarationFilePath)) {
638
+ await writeIfDifferent(
639
+ startDeclarationFilePath,
640
+ '',
641
+ startDeclarationFileContent,
642
+ {
643
+ beforeWrite: () => {
644
+ logger.log(`🟡 Creating tanstack-start.d.ts`)
645
+ },
646
+ },
647
+ )
648
+ } else {
649
+ const existingDeclarationFileContent = await fsp
650
+ .readFile(startDeclarationFilePath, 'utf-8')
651
+ .catch((err) => {
652
+ if (err.code === 'ENOENT') {
653
+ return ''
654
+ }
655
+ throw err
656
+ })
657
+ await writeIfDifferent(
658
+ startDeclarationFilePath,
659
+ existingDeclarationFileContent,
660
+ startDeclarationFileContent,
661
+ {
662
+ beforeWrite: () => {
663
+ logger.log(`🟡 Updating tanstack-start.d.ts`)
664
+ },
665
+ },
666
+ )
667
+ }
668
+
669
+ if (routeTreeWriteResult && !checkLatest()) {
670
+ return
671
+ }
672
+
673
+ // logger.log(
674
+ // `✅ Processed ${routeNodes.length === 1 ? 'server route' : 'server routes'} in ${
675
+ // Date.now() - start
676
+ // }ms`,
677
+ // )
678
+ }
679
+
680
+ function buildStartDeclarationFile({
681
+ serverRoutesRelativePath,
682
+ }: {
683
+ serverRoutesRelativePath: string
684
+ }) {
685
+ return [`import '${serverRoutesRelativePath}'`].join('\n') + '\n'
686
+ }
687
+
688
+ function removeGroups(s: string) {
689
+ return s.replace(possiblyNestedRouteGroupPatternRegex, '')
690
+ }
691
+
692
+ /**
693
+ * The `node.path` is used as the `id` in the route definition.
694
+ * This function checks if the given node has a parent and if so, it determines the correct path for the given node.
695
+ * @param node - The node to determine the path for.
696
+ * @returns The correct path for the given node.
697
+ */
698
+ function determineNodePath(node: RouteNode) {
699
+ return (node.path = node.parent
700
+ ? node.routePath?.replace(node.parent.routePath ?? '', '') || '/'
701
+ : node.routePath)
702
+ }
703
+
704
+ /**
705
+ * Removes the last segment from a given path. Segments are considered to be separated by a '/'.
706
+ *
707
+ * @param {string} routePath - The path from which to remove the last segment. Defaults to '/'.
708
+ * @returns {string} The path with the last segment removed.
709
+ * @example
710
+ * removeLastSegmentFromPath('/workspace/_auth/foo') // '/workspace/_auth'
711
+ */
712
+ function removeLastSegmentFromPath(routePath: string = '/'): string {
713
+ const segments = routePath.split('/')
714
+ segments.pop() // Remove the last segment
715
+ return segments.join('/')
716
+ }
717
+
718
+ /**
719
+ * Removes all segments from a given path that start with an underscore ('_').
720
+ *
721
+ * @param {string} routePath - The path from which to remove segments. Defaults to '/'.
722
+ * @returns {string} The path with all underscore-prefixed segments removed.
723
+ * @example
724
+ * removeLayoutSegments('/workspace/_auth/foo') // '/workspace/foo'
725
+ */
726
+ function removeLayoutSegments(routePath: string = '/'): string {
727
+ const segments = routePath.split('/')
728
+ const newSegments = segments.filter((segment) => !segment.startsWith('_'))
729
+ return newSegments.join('/')
730
+ }
731
+
732
+ function hasParentRoute(
733
+ routes: Array<RouteNode>,
734
+ node: RouteNode,
735
+ routePathToCheck: string | undefined,
736
+ ): RouteNode | null {
737
+ if (!routePathToCheck || routePathToCheck === '/') {
738
+ return null
739
+ }
740
+
741
+ const sortedNodes = multiSortBy(routes, [
742
+ (d) => d.routePath!.length * -1,
743
+ (d) => d.variableName,
744
+ ]).filter((d) => d.routePath !== `/${rootPathId}`)
745
+
746
+ for (const route of sortedNodes) {
747
+ if (route.routePath === '/') continue
748
+
749
+ if (
750
+ routePathToCheck.startsWith(`${route.routePath}/`) &&
751
+ route.routePath !== routePathToCheck
752
+ ) {
753
+ return route
754
+ }
755
+ }
756
+
757
+ const segments = routePathToCheck.split('/')
758
+ segments.pop() // Remove the last segment
759
+ const parentRoutePath = segments.join('/')
760
+
761
+ return hasParentRoute(routes, node, parentRoutePath)
762
+ }
763
+
764
+ /**
765
+ * Gets the final variable name for a route
766
+ */
767
+ const getResolvedRouteNodeVariableName = (routeNode: RouteNode): string => {
768
+ return routeNode.children?.length
769
+ ? `${routeNode.variableName}RouteWithChildren`
770
+ : `${routeNode.variableName}Route`
771
+ }
772
+
773
+ /**
774
+ * Creates a map from fullPath to routeNode
775
+ */
776
+ const createRouteNodesByFullPath = (
777
+ routeNodes: Array<RouteNode>,
778
+ ): Map<string, RouteNode> => {
779
+ return new Map(
780
+ routeNodes.map((routeNode) => [inferFullPath(routeNode), routeNode]),
781
+ )
782
+ }
783
+
784
+ /**
785
+ * Create a map from 'to' to a routeNode
786
+ */
787
+ const createRouteNodesByTo = (
788
+ routeNodes: Array<RouteNode>,
789
+ ): Map<string, RouteNode> => {
790
+ return new Map(
791
+ dedupeBranchesAndIndexRoutes(routeNodes).map((routeNode) => [
792
+ inferTo(routeNode),
793
+ routeNode,
794
+ ]),
795
+ )
796
+ }
797
+
798
+ /**
799
+ * Create a map from 'id' to a routeNode
800
+ */
801
+ const createRouteNodesById = (
802
+ routeNodes: Array<RouteNode>,
803
+ ): Map<string, RouteNode> => {
804
+ return new Map(
805
+ routeNodes.map((routeNode) => {
806
+ const id = routeNode.routePath ?? ''
807
+ return [id, routeNode]
808
+ }),
809
+ )
810
+ }
811
+
812
+ /**
813
+ * Infers the full path for use by TS
814
+ */
815
+ const inferFullPath = (routeNode: RouteNode): string => {
816
+ const fullPath = removeGroups(
817
+ removeUnderscores(removeLayoutSegments(routeNode.routePath)) ?? '',
818
+ )
819
+
820
+ return routeNode.cleanedPath === '/' ? fullPath : fullPath.replace(/\/$/, '')
821
+ }
822
+
823
+ /**
824
+ * Infers the path for use by TS
825
+ */
826
+ const inferPath = (routeNode: RouteNode): string => {
827
+ return routeNode.cleanedPath === '/'
828
+ ? routeNode.cleanedPath
829
+ : (routeNode.cleanedPath?.replace(/\/$/, '') ?? '')
830
+ }
831
+
832
+ /**
833
+ * Infers to path
834
+ */
835
+ const inferTo = (routeNode: RouteNode): string => {
836
+ const fullPath = inferFullPath(routeNode)
837
+
838
+ if (fullPath === '/') return fullPath
839
+
840
+ return fullPath.replace(/\/$/, '')
841
+ }
842
+
843
+ /**
844
+ * Dedupes branches and index routes
845
+ */
846
+ const dedupeBranchesAndIndexRoutes = (
847
+ routes: Array<RouteNode>,
848
+ ): Array<RouteNode> => {
849
+ return routes.filter((route) => {
850
+ if (route.children?.find((child) => child.cleanedPath === '/')) return false
851
+ return true
852
+ })
853
+ }
854
+
855
+ function checkUnique<TElement>(routes: Array<TElement>, key: keyof TElement) {
856
+ // Check no two routes have the same `key`
857
+ // if they do, throw an error with the conflicting filePaths
858
+ const keys = routes.map((d) => d[key])
859
+ const uniqueKeys = new Set(keys)
860
+ if (keys.length !== uniqueKeys.size) {
861
+ const duplicateKeys = keys.filter((d, i) => keys.indexOf(d) !== i)
862
+ const conflictingFiles = routes.filter((d) =>
863
+ duplicateKeys.includes(d[key]),
864
+ )
865
+ return conflictingFiles
866
+ }
867
+ return undefined
868
+ }
869
+
870
+ function checkRouteFullPathUniqueness(
871
+ _routes: Array<RouteNode>,
872
+ config: Config,
873
+ ) {
874
+ const routes = _routes.map((d) => {
875
+ const inferredFullPath = inferFullPath(d)
876
+ return { ...d, inferredFullPath }
877
+ })
878
+
879
+ const conflictingFiles = checkUnique(routes, 'inferredFullPath')
880
+
881
+ if (conflictingFiles !== undefined) {
882
+ const errorMessage = `Conflicting configuration paths were found for the following route${conflictingFiles.length > 1 ? 's' : ''}: ${conflictingFiles
883
+ .map((p) => `"${p.inferredFullPath}"`)
884
+ .join(', ')}.
885
+ Please ensure each Server Route has a unique full path.
886
+ Conflicting files: \n ${conflictingFiles.map((d) => path.resolve(config.routesDirectory, d.filePath)).join('\n ')}\n`
887
+ console.error(errorMessage)
888
+ process.exit(1)
889
+ }
890
+ }