@tanstack/start-plugin-core 1.121.0-alpha.24 → 1.121.0-alpha.26

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