@tanstack/cli 0.0.8 → 0.48.3

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 (88) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bin.js +7 -0
  3. package/dist/cli.js +481 -0
  4. package/dist/command-line.js +174 -0
  5. package/dist/dev-watch.js +290 -0
  6. package/dist/file-syncer.js +148 -0
  7. package/dist/index.js +1 -0
  8. package/dist/mcp/api.js +31 -0
  9. package/dist/mcp/tools.js +250 -0
  10. package/dist/mcp/types.js +37 -0
  11. package/dist/mcp.js +121 -0
  12. package/dist/options.js +162 -0
  13. package/dist/types/bin.d.ts +2 -0
  14. package/dist/types/cli.d.ts +16 -0
  15. package/dist/types/command-line.d.ts +10 -0
  16. package/dist/types/dev-watch.d.ts +27 -0
  17. package/dist/types/file-syncer.d.ts +18 -0
  18. package/dist/types/index.d.ts +1 -0
  19. package/dist/types/mcp/api.d.ts +4 -0
  20. package/dist/types/mcp/tools.d.ts +2 -0
  21. package/dist/types/mcp/types.d.ts +217 -0
  22. package/dist/types/mcp.d.ts +6 -0
  23. package/dist/types/options.d.ts +8 -0
  24. package/dist/types/types.d.ts +25 -0
  25. package/dist/types/ui-environment.d.ts +2 -0
  26. package/dist/types/ui-prompts.d.ts +12 -0
  27. package/dist/types/utils.d.ts +8 -0
  28. package/dist/types.js +1 -0
  29. package/dist/ui-environment.js +52 -0
  30. package/dist/ui-prompts.js +244 -0
  31. package/dist/utils.js +30 -0
  32. package/package.json +46 -47
  33. package/src/bin.ts +6 -93
  34. package/src/cli.ts +692 -0
  35. package/src/command-line.ts +236 -0
  36. package/src/dev-watch.ts +430 -0
  37. package/src/file-syncer.ts +205 -0
  38. package/src/index.ts +1 -85
  39. package/src/mcp.ts +190 -0
  40. package/src/options.ts +260 -0
  41. package/src/types.ts +27 -0
  42. package/src/ui-environment.ts +74 -0
  43. package/src/ui-prompts.ts +322 -0
  44. package/src/utils.ts +38 -0
  45. package/tests/command-line.test.ts +304 -0
  46. package/tests/index.test.ts +9 -0
  47. package/tests/mcp.test.ts +225 -0
  48. package/tests/options.test.ts +304 -0
  49. package/tests/setupVitest.ts +6 -0
  50. package/tests/ui-environment.test.ts +97 -0
  51. package/tests/ui-prompts.test.ts +238 -0
  52. package/tsconfig.json +17 -0
  53. package/vitest.config.js +7 -0
  54. package/dist/bin.cjs +0 -769
  55. package/dist/bin.d.cts +0 -1
  56. package/dist/bin.d.mts +0 -1
  57. package/dist/bin.mjs +0 -768
  58. package/dist/fetch-CbFFGJEw.cjs +0 -3
  59. package/dist/fetch-DG5dLrsb.cjs +0 -522
  60. package/dist/fetch-DhlVXS6S.mjs +0 -390
  61. package/dist/fetch-I_OVg8JX.mjs +0 -3
  62. package/dist/index.cjs +0 -37
  63. package/dist/index.d.cts +0 -1172
  64. package/dist/index.d.mts +0 -1172
  65. package/dist/index.mjs +0 -4
  66. package/dist/template-Szi7-AZJ.mjs +0 -2202
  67. package/dist/template-lWrIZhCQ.cjs +0 -2314
  68. package/src/api/fetch.test.ts +0 -114
  69. package/src/api/fetch.ts +0 -278
  70. package/src/cache/index.ts +0 -89
  71. package/src/commands/create.ts +0 -470
  72. package/src/commands/mcp.test.ts +0 -152
  73. package/src/commands/mcp.ts +0 -211
  74. package/src/engine/compile-with-addons.test.ts +0 -302
  75. package/src/engine/compile.test.ts +0 -404
  76. package/src/engine/compile.ts +0 -569
  77. package/src/engine/config-file.test.ts +0 -118
  78. package/src/engine/config-file.ts +0 -61
  79. package/src/engine/custom-addons/integration.ts +0 -323
  80. package/src/engine/custom-addons/shared.test.ts +0 -98
  81. package/src/engine/custom-addons/shared.ts +0 -281
  82. package/src/engine/custom-addons/template.test.ts +0 -288
  83. package/src/engine/custom-addons/template.ts +0 -124
  84. package/src/engine/template.test.ts +0 -256
  85. package/src/engine/template.ts +0 -269
  86. package/src/engine/types.ts +0 -336
  87. package/src/parse-gitignore.d.ts +0 -5
  88. package/src/templates/base.ts +0 -883
@@ -0,0 +1,304 @@
1
+ import { basename, resolve } from 'node:path'
2
+ import { beforeEach, describe, expect, it } from 'vitest'
3
+
4
+ import { normalizeOptions } from '../src/command-line.js'
5
+ import {
6
+ sanitizePackageName,
7
+ getCurrentDirectoryName,
8
+ } from '../src/utils.js'
9
+ import {
10
+ __testRegisterFramework,
11
+ __testClearFrameworks,
12
+ } from '@tanstack/create'
13
+
14
+ beforeEach(() => {
15
+ __testClearFrameworks()
16
+ })
17
+
18
+ describe('sanitizePackageName', () => {
19
+ it('should convert to lowercase', () => {
20
+ expect(sanitizePackageName('MyProject')).toBe('myproject')
21
+ })
22
+
23
+ it('should replace spaces with hyphens', () => {
24
+ expect(sanitizePackageName('my project')).toBe('my-project')
25
+ })
26
+
27
+ it('should replace underscores with hyphens', () => {
28
+ expect(sanitizePackageName('my_project')).toBe('my-project')
29
+ })
30
+
31
+ it('should remove invalid characters', () => {
32
+ expect(sanitizePackageName('my@project!')).toBe('myproject')
33
+ })
34
+
35
+ it('should ensure it starts with a letter', () => {
36
+ expect(sanitizePackageName('123project')).toBe('project')
37
+ expect(sanitizePackageName('_myproject')).toBe('myproject')
38
+ })
39
+
40
+ it('should collapse multiple hyphens', () => {
41
+ expect(sanitizePackageName('my--project')).toBe('my-project')
42
+ })
43
+
44
+ it('should remove trailing hyphen', () => {
45
+ expect(sanitizePackageName('myproject-')).toBe('myproject')
46
+ })
47
+ })
48
+
49
+ describe('getCurrentDirectoryName', () => {
50
+ it('should return the basename of the current working directory', () => {
51
+ expect(getCurrentDirectoryName()).toBe(basename(process.cwd()))
52
+ })
53
+ })
54
+
55
+ describe('normalizeOptions', () => {
56
+ it('should return undefined if project name is not provided', async () => {
57
+ const options = await normalizeOptions({})
58
+ expect(options).toBeUndefined()
59
+ })
60
+
61
+ it('should handle "." as project name by using sanitized current directory name', async () => {
62
+ const options = await normalizeOptions({
63
+ projectName: '.',
64
+ })
65
+ const expectedName = sanitizePackageName(getCurrentDirectoryName())
66
+ expect(options?.projectName).toBe(expectedName)
67
+ expect(options?.targetDir).toBe(resolve(process.cwd()))
68
+ })
69
+
70
+ it('should return enable typescript based on the framework', async () => {
71
+ const jsOptions = await normalizeOptions({
72
+ projectName: 'test',
73
+ template: 'javascript',
74
+ })
75
+ expect(jsOptions?.typescript).toBe(false)
76
+ expect(jsOptions?.mode).toBe('code-router')
77
+
78
+ const tsOptions = await normalizeOptions({
79
+ projectName: 'test',
80
+ template: 'typescript',
81
+ })
82
+ expect(tsOptions?.typescript).toBe(true)
83
+ expect(tsOptions?.mode).toBe('code-router')
84
+
85
+ const frOptions = await normalizeOptions({
86
+ projectName: 'test',
87
+ template: 'file-router',
88
+ })
89
+ expect(frOptions?.typescript).toBe(true)
90
+ expect(frOptions?.mode).toBe('file-router')
91
+ })
92
+
93
+ it('should return enable tailwind if the framework is solid', async () => {
94
+ const solidOptions = await normalizeOptions({
95
+ projectName: 'test',
96
+ framework: 'solid',
97
+ })
98
+ expect(solidOptions?.tailwind).toBe(true)
99
+
100
+ const twOptions = await normalizeOptions({
101
+ projectName: 'test',
102
+ tailwind: true,
103
+ })
104
+ expect(twOptions?.tailwind).toBe(true)
105
+
106
+ const noOptions = await normalizeOptions({
107
+ projectName: 'test',
108
+ })
109
+ expect(noOptions?.tailwind).toBe(false)
110
+ })
111
+
112
+ it('should handle a starter url', async () => {
113
+ __testRegisterFramework({
114
+ id: 'solid',
115
+ name: 'Solid',
116
+ getAddOns: () => [
117
+ {
118
+ id: 'nitro',
119
+ name: 'nitro',
120
+ modes: ['file-router'],
121
+ default: true,
122
+ },
123
+ ],
124
+ supportedModes: {
125
+ 'code-router': {
126
+ displayName: 'Code Router',
127
+ description: 'TanStack Router using code to define the routes',
128
+ forceTypescript: false,
129
+ },
130
+ 'file-router': {
131
+ displayName: 'File Router',
132
+ description: 'TanStack Router using files to define the routes',
133
+ forceTypescript: true,
134
+ },
135
+ },
136
+ })
137
+ fetch.mockResponseOnce(
138
+ JSON.stringify({
139
+ id: 'https://github.com/cta-dev/cta-starter-solid',
140
+ tailwind: true,
141
+ typescript: false,
142
+ framework: 'solid',
143
+ mode: 'file-router',
144
+ type: 'starter',
145
+ description: 'A starter for Solid',
146
+ name: 'My Solid Starter',
147
+ dependsOn: [],
148
+ files: {},
149
+ deletedFiles: [],
150
+ }),
151
+ )
152
+
153
+ const options = await normalizeOptions({
154
+ projectName: 'test',
155
+ starter: 'https://github.com/cta-dev/cta-starter-solid',
156
+ deployment: 'nitro',
157
+ })
158
+ expect(options?.mode).toBe('file-router')
159
+ expect(options?.tailwind).toBe(true)
160
+ expect(options?.typescript).toBe(true)
161
+ expect(options?.framework?.id).toBe('solid')
162
+ })
163
+
164
+ it('should default to react-cra if no framework is provided', async () => {
165
+ __testRegisterFramework({
166
+ id: 'react-cra',
167
+ name: 'react',
168
+ })
169
+ const options = await normalizeOptions({
170
+ projectName: 'test',
171
+ })
172
+ expect(options?.framework?.id).toBe('react-cra')
173
+ })
174
+
175
+ it('should handle forced addons', async () => {
176
+ __testRegisterFramework({
177
+ id: 'react-cra',
178
+ name: 'react',
179
+ getAddOns: () => [
180
+ {
181
+ id: 'foo',
182
+ name: 'foobar',
183
+ modes: ['file-router'],
184
+ },
185
+ {
186
+ id: 'nitro',
187
+ name: 'nitro',
188
+ modes: ['file-router'],
189
+ default: true,
190
+ },
191
+ ],
192
+ })
193
+ const options = await normalizeOptions(
194
+ {
195
+ projectName: 'test',
196
+ framework: 'react-cra',
197
+ },
198
+ 'file-router',
199
+ ['foo'],
200
+ )
201
+ expect(options?.chosenAddOns.map((a) => a.id).includes('foo')).toBe(true)
202
+ })
203
+
204
+ it('should handle additional addons from the CLI', async () => {
205
+ __testRegisterFramework({
206
+ id: 'react-cra',
207
+ name: 'react',
208
+ getAddOns: () => [
209
+ {
210
+ id: 'foo',
211
+ name: 'foobar',
212
+ modes: ['file-router'],
213
+ },
214
+ {
215
+ id: 'baz',
216
+ name: 'baz',
217
+ modes: ['file-router'],
218
+ },
219
+ {
220
+ id: 'nitro',
221
+ name: 'nitro',
222
+ modes: ['file-router'],
223
+ default: true,
224
+ },
225
+ ],
226
+ })
227
+ const options = await normalizeOptions(
228
+ {
229
+ projectName: 'test',
230
+ addOns: ['baz'],
231
+ framework: 'react-cra',
232
+ template: 'file-router',
233
+ },
234
+ 'file-router',
235
+ ['foo'],
236
+ )
237
+ expect(options?.chosenAddOns.map((a) => a.id).includes('foo')).toBe(true)
238
+ expect(options?.chosenAddOns.map((a) => a.id).includes('baz')).toBe(true)
239
+ // Tailwind is not automatically set to true unless an add-on explicitly requires it
240
+ // Since mock add-ons don't have tailwind: true, tailwind should be false
241
+ expect(options?.tailwind).toBe(false)
242
+ expect(options?.typescript).toBe(true)
243
+ })
244
+
245
+ it('should handle toolchain as an addon', async () => {
246
+ __testRegisterFramework({
247
+ id: 'react-cra',
248
+ name: 'react',
249
+ getAddOns: () => [
250
+ {
251
+ id: 'biome',
252
+ name: 'Biome',
253
+ modes: ['file-router', 'code-router'],
254
+ },
255
+ {
256
+ id: 'nitro',
257
+ name: 'nitro',
258
+ modes: ['file-router', 'code-router'],
259
+ default: true,
260
+ },
261
+ ],
262
+ })
263
+ const options = await normalizeOptions({
264
+ projectName: 'test',
265
+ toolchain: 'biome',
266
+ })
267
+ expect(options?.chosenAddOns.map((a) => a.id).includes('biome')).toBe(true)
268
+ // Tailwind is not automatically set to true unless an add-on explicitly requires it
269
+ // Since mock add-ons don't have tailwind: true, tailwind should be false
270
+ expect(options?.tailwind).toBe(false)
271
+ expect(options?.typescript).toBe(true)
272
+ })
273
+
274
+ it('should handle the funky Windows edge case with CLI parsing', async () => {
275
+ __testRegisterFramework({
276
+ id: 'react-cra',
277
+ name: 'react',
278
+ getAddOns: () => [
279
+ {
280
+ id: 'foo',
281
+ name: 'foobar',
282
+ modes: ['file-router', 'code-router'],
283
+ },
284
+ {
285
+ id: 'baz',
286
+ name: 'baz',
287
+ modes: ['file-router', 'code-router'],
288
+ },
289
+ {
290
+ id: 'nitro',
291
+ name: 'nitro',
292
+ modes: ['file-router', 'code-router'],
293
+ default: true,
294
+ },
295
+ ],
296
+ })
297
+ const options = await normalizeOptions({
298
+ projectName: 'test',
299
+ addOns: ['baz foo'],
300
+ })
301
+ expect(options?.chosenAddOns.map((a) => a.id).includes('foo')).toBe(true)
302
+ expect(options?.chosenAddOns.map((a) => a.id).includes('baz')).toBe(true)
303
+ })
304
+ })
@@ -0,0 +1,9 @@
1
+ import { describe, it, expect } from 'vitest'
2
+
3
+ import { cli } from '../src/index.js'
4
+
5
+ describe('cli', () => {
6
+ it('should call the cli with the correct arguments', async () => {
7
+ expect(cli).toBeDefined()
8
+ })
9
+ })
@@ -0,0 +1,225 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
3
+ import { registerDocTools } from '../src/mcp/tools.js'
4
+ import * as api from '../src/mcp/api.js'
5
+
6
+ vi.mock('../src/mcp/api.js')
7
+
8
+ const mockLibrariesResponse = {
9
+ libraries: [
10
+ {
11
+ id: 'query',
12
+ name: 'TanStack Query',
13
+ tagline: 'Powerful asynchronous state management',
14
+ description: 'Data fetching library',
15
+ frameworks: ['react', 'vue', 'solid'],
16
+ latestVersion: 'v5',
17
+ latestBranch: 'main',
18
+ availableVersions: ['v5', 'v4'],
19
+ docsUrl: 'https://tanstack.com/query',
20
+ githubUrl: 'https://github.com/TanStack/query',
21
+ repo: 'TanStack/query',
22
+ docsRoot: 'docs',
23
+ },
24
+ {
25
+ id: 'router',
26
+ name: 'TanStack Router',
27
+ tagline: 'Type-safe routing',
28
+ description: 'Router library',
29
+ frameworks: ['react'],
30
+ latestVersion: 'v1',
31
+ latestBranch: 'main',
32
+ availableVersions: ['v1'],
33
+ docsUrl: 'https://tanstack.com/router',
34
+ githubUrl: 'https://github.com/TanStack/router',
35
+ repo: 'TanStack/router',
36
+ docsRoot: 'docs',
37
+ },
38
+ ],
39
+ groups: {
40
+ state: ['query'],
41
+ headlessUI: [],
42
+ performance: [],
43
+ tooling: ['router'],
44
+ },
45
+ groupNames: {
46
+ state: 'State Management',
47
+ headlessUI: 'Headless UI',
48
+ performance: 'Performance',
49
+ tooling: 'Tooling',
50
+ },
51
+ }
52
+
53
+ const mockPartnersResponse = {
54
+ partners: [
55
+ {
56
+ id: 'neon',
57
+ name: 'Neon',
58
+ description: 'Serverless Postgres',
59
+ category: 'database',
60
+ categoryLabel: 'Database',
61
+ libraries: ['start', 'router'],
62
+ url: 'https://neon.tech',
63
+ },
64
+ ],
65
+ categories: ['database', 'auth'],
66
+ categoryLabels: {
67
+ database: 'Database',
68
+ auth: 'Authentication',
69
+ },
70
+ }
71
+
72
+ describe('MCP Tools', () => {
73
+ let server: McpServer
74
+ let registeredTools: Map<string, { handler: Function; schema: unknown }>
75
+
76
+ beforeEach(() => {
77
+ vi.resetAllMocks()
78
+
79
+ // Create a mock server that captures tool registrations
80
+ registeredTools = new Map()
81
+ server = {
82
+ tool: vi.fn((name, description, schema, handler) => {
83
+ registeredTools.set(name, { handler, schema })
84
+ }),
85
+ } as unknown as McpServer
86
+
87
+ vi.mocked(api.fetchLibraries).mockResolvedValue(mockLibrariesResponse)
88
+ vi.mocked(api.fetchPartners).mockResolvedValue(mockPartnersResponse)
89
+ vi.mocked(api.fetchDocContent).mockResolvedValue('# Test Doc\n\nContent here')
90
+
91
+ registerDocTools(server)
92
+ })
93
+
94
+ describe('tanstack_list_libraries', () => {
95
+ it('should register the tool', () => {
96
+ expect(registeredTools.has('tanstack_list_libraries')).toBe(true)
97
+ })
98
+
99
+ it('should list all libraries when no group specified', async () => {
100
+ const tool = registeredTools.get('tanstack_list_libraries')!
101
+ const result = await tool.handler({})
102
+
103
+ expect(result.content[0].type).toBe('text')
104
+ const data = JSON.parse(result.content[0].text)
105
+ expect(data.count).toBe(2)
106
+ expect(data.libraries).toHaveLength(2)
107
+ })
108
+
109
+ it('should filter libraries by group', async () => {
110
+ const tool = registeredTools.get('tanstack_list_libraries')!
111
+ const result = await tool.handler({ group: 'state' })
112
+
113
+ const data = JSON.parse(result.content[0].text)
114
+ expect(data.count).toBe(1)
115
+ expect(data.libraries[0].id).toBe('query')
116
+ expect(data.group).toBe('State Management')
117
+ })
118
+
119
+ it('should handle API errors', async () => {
120
+ vi.mocked(api.fetchLibraries).mockRejectedValue(new Error('Network error'))
121
+
122
+ const tool = registeredTools.get('tanstack_list_libraries')!
123
+ const result = await tool.handler({})
124
+
125
+ expect(result.isError).toBe(true)
126
+ expect(result.content[0].text).toContain('Error')
127
+ })
128
+ })
129
+
130
+ describe('tanstack_doc', () => {
131
+ it('should register the tool', () => {
132
+ expect(registeredTools.has('tanstack_doc')).toBe(true)
133
+ })
134
+
135
+ it('should fetch doc content', async () => {
136
+ const tool = registeredTools.get('tanstack_doc')!
137
+ const result = await tool.handler({
138
+ library: 'query',
139
+ path: 'framework/react/overview',
140
+ })
141
+
142
+ expect(api.fetchDocContent).toHaveBeenCalledWith(
143
+ 'TanStack/query',
144
+ 'main',
145
+ 'docs/framework/react/overview.md',
146
+ )
147
+
148
+ const data = JSON.parse(result.content[0].text)
149
+ expect(data.content).toContain('# Test Doc')
150
+ })
151
+
152
+ it('should error for unknown library', async () => {
153
+ const tool = registeredTools.get('tanstack_doc')!
154
+ const result = await tool.handler({
155
+ library: 'unknown',
156
+ path: 'overview',
157
+ })
158
+
159
+ expect(result.isError).toBe(true)
160
+ expect(result.content[0].text).toContain('not found')
161
+ })
162
+
163
+ it('should error for unknown version', async () => {
164
+ const tool = registeredTools.get('tanstack_doc')!
165
+ const result = await tool.handler({
166
+ library: 'query',
167
+ path: 'overview',
168
+ version: 'v999',
169
+ })
170
+
171
+ expect(result.isError).toBe(true)
172
+ expect(result.content[0].text).toContain('Version')
173
+ })
174
+
175
+ it('should handle 404 doc', async () => {
176
+ vi.mocked(api.fetchDocContent).mockResolvedValue(null)
177
+
178
+ const tool = registeredTools.get('tanstack_doc')!
179
+ const result = await tool.handler({
180
+ library: 'query',
181
+ path: 'nonexistent',
182
+ })
183
+
184
+ expect(result.isError).toBe(true)
185
+ expect(result.content[0].text).toContain('not found')
186
+ })
187
+ })
188
+
189
+ describe('tanstack_ecosystem', () => {
190
+ it('should register the tool', () => {
191
+ expect(registeredTools.has('tanstack_ecosystem')).toBe(true)
192
+ })
193
+
194
+ it('should list ecosystem partners', async () => {
195
+ const tool = registeredTools.get('tanstack_ecosystem')!
196
+ const result = await tool.handler({})
197
+
198
+ const data = JSON.parse(result.content[0].text)
199
+ expect(data.partners).toHaveLength(1)
200
+ expect(data.partners[0].id).toBe('neon')
201
+ })
202
+
203
+ it('should filter by category', async () => {
204
+ const tool = registeredTools.get('tanstack_ecosystem')!
205
+ const result = await tool.handler({ category: 'database' })
206
+
207
+ const data = JSON.parse(result.content[0].text)
208
+ expect(data.partners).toHaveLength(1)
209
+ })
210
+
211
+ it('should filter by library', async () => {
212
+ const tool = registeredTools.get('tanstack_ecosystem')!
213
+ const result = await tool.handler({ library: 'start' })
214
+
215
+ const data = JSON.parse(result.content[0].text)
216
+ expect(data.partners).toHaveLength(1)
217
+ })
218
+ })
219
+
220
+ describe('tanstack_search_docs', () => {
221
+ it('should register the tool', () => {
222
+ expect(registeredTools.has('tanstack_search_docs')).toBe(true)
223
+ })
224
+ })
225
+ })