@tanstack/cli 0.48.7 → 0.58.5
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.
- package/CHANGELOG.md +539 -0
- package/dist/bin.js +0 -1
- package/dist/cli.js +23 -60
- package/dist/command-line.js +35 -36
- package/dist/mcp.js +60 -0
- package/dist/options.js +8 -46
- package/dist/types/cli.d.ts +1 -5
- package/dist/types/command-line.d.ts +5 -1
- package/dist/types/mcp.d.ts +0 -1
- package/dist/types/options.d.ts +1 -2
- package/dist/types/types.d.ts +1 -3
- package/dist/types/ui-prompts.d.ts +0 -3
- package/dist/types/utils.d.ts +0 -2
- package/dist/ui-prompts.js +0 -43
- package/dist/utils.js +0 -6
- package/package.json +3 -3
- package/src/bin.ts +0 -1
- package/src/cli.ts +35 -79
- package/src/command-line.ts +51 -46
- package/src/mcp.ts +74 -1
- package/src/options.ts +6 -50
- package/src/types.ts +1 -4
- package/src/ui-prompts.ts +0 -50
- package/src/utils.ts +0 -8
- package/tests/command-line.test.ts +44 -40
- package/tests/options.test.ts +11 -99
- package/tests/ui-prompts.test.ts +0 -38
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { basename, resolve } from 'node:path'
|
|
2
2
|
import { beforeEach, describe, expect, it } from 'vitest'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
normalizeOptions,
|
|
6
|
+
validateLegacyCreateFlags,
|
|
7
|
+
} from '../src/command-line.js'
|
|
5
8
|
import {
|
|
6
9
|
sanitizePackageName,
|
|
7
10
|
getCurrentDirectoryName,
|
|
@@ -67,46 +70,25 @@ describe('normalizeOptions', () => {
|
|
|
67
70
|
expect(options?.targetDir).toBe(resolve(process.cwd()))
|
|
68
71
|
})
|
|
69
72
|
|
|
70
|
-
it('should
|
|
71
|
-
const
|
|
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({
|
|
73
|
+
it('should always enable typescript (file-router/TanStack Start requires it)', async () => {
|
|
74
|
+
const options = await normalizeOptions({
|
|
79
75
|
projectName: 'test',
|
|
80
|
-
template: 'typescript',
|
|
81
76
|
})
|
|
82
|
-
expect(
|
|
83
|
-
expect(
|
|
77
|
+
expect(options?.typescript).toBe(true)
|
|
78
|
+
expect(options?.mode).toBe('file-router')
|
|
79
|
+
})
|
|
84
80
|
|
|
85
|
-
|
|
81
|
+
it('tailwind is always enabled', async () => {
|
|
82
|
+
const options = await normalizeOptions({
|
|
86
83
|
projectName: 'test',
|
|
87
|
-
template: 'file-router',
|
|
88
84
|
})
|
|
89
|
-
expect(
|
|
90
|
-
expect(frOptions?.mode).toBe('file-router')
|
|
91
|
-
})
|
|
85
|
+
expect(options?.tailwind).toBe(true)
|
|
92
86
|
|
|
93
|
-
it('should return enable tailwind if the framework is solid', async () => {
|
|
94
87
|
const solidOptions = await normalizeOptions({
|
|
95
88
|
projectName: 'test',
|
|
96
89
|
framework: 'solid',
|
|
97
90
|
})
|
|
98
91
|
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
92
|
})
|
|
111
93
|
|
|
112
94
|
it('should handle a starter url', async () => {
|
|
@@ -137,7 +119,6 @@ describe('normalizeOptions', () => {
|
|
|
137
119
|
fetch.mockResponseOnce(
|
|
138
120
|
JSON.stringify({
|
|
139
121
|
id: 'https://github.com/cta-dev/cta-starter-solid',
|
|
140
|
-
tailwind: true,
|
|
141
122
|
typescript: false,
|
|
142
123
|
framework: 'solid',
|
|
143
124
|
mode: 'file-router',
|
|
@@ -195,7 +176,6 @@ describe('normalizeOptions', () => {
|
|
|
195
176
|
projectName: 'test',
|
|
196
177
|
framework: 'react-cra',
|
|
197
178
|
},
|
|
198
|
-
'file-router',
|
|
199
179
|
['foo'],
|
|
200
180
|
)
|
|
201
181
|
expect(options?.chosenAddOns.map((a) => a.id).includes('foo')).toBe(true)
|
|
@@ -229,16 +209,12 @@ describe('normalizeOptions', () => {
|
|
|
229
209
|
projectName: 'test',
|
|
230
210
|
addOns: ['baz'],
|
|
231
211
|
framework: 'react-cra',
|
|
232
|
-
template: 'file-router',
|
|
233
212
|
},
|
|
234
|
-
'file-router',
|
|
235
213
|
['foo'],
|
|
236
214
|
)
|
|
237
215
|
expect(options?.chosenAddOns.map((a) => a.id).includes('foo')).toBe(true)
|
|
238
216
|
expect(options?.chosenAddOns.map((a) => a.id).includes('baz')).toBe(true)
|
|
239
|
-
|
|
240
|
-
// Since mock add-ons don't have tailwind: true, tailwind should be false
|
|
241
|
-
expect(options?.tailwind).toBe(false)
|
|
217
|
+
expect(options?.tailwind).toBe(true)
|
|
242
218
|
expect(options?.typescript).toBe(true)
|
|
243
219
|
})
|
|
244
220
|
|
|
@@ -265,9 +241,7 @@ describe('normalizeOptions', () => {
|
|
|
265
241
|
toolchain: 'biome',
|
|
266
242
|
})
|
|
267
243
|
expect(options?.chosenAddOns.map((a) => a.id).includes('biome')).toBe(true)
|
|
268
|
-
|
|
269
|
-
// Since mock add-ons don't have tailwind: true, tailwind should be false
|
|
270
|
-
expect(options?.tailwind).toBe(false)
|
|
244
|
+
expect(options?.tailwind).toBe(true)
|
|
271
245
|
expect(options?.typescript).toBe(true)
|
|
272
246
|
})
|
|
273
247
|
|
|
@@ -302,3 +276,33 @@ describe('normalizeOptions', () => {
|
|
|
302
276
|
expect(options?.chosenAddOns.map((a) => a.id).includes('baz')).toBe(true)
|
|
303
277
|
})
|
|
304
278
|
})
|
|
279
|
+
|
|
280
|
+
describe('validateLegacyCreateFlags', () => {
|
|
281
|
+
it('returns no warnings or errors without legacy flags', () => {
|
|
282
|
+
const result = validateLegacyCreateFlags({})
|
|
283
|
+
expect(result.warnings).toEqual([])
|
|
284
|
+
expect(result.error).toBeUndefined()
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('warns when --router-only is used', () => {
|
|
288
|
+
const result = validateLegacyCreateFlags({ routerOnly: true })
|
|
289
|
+
expect(result.error).toBeUndefined()
|
|
290
|
+
expect(result.warnings[0]).toContain('--router-only')
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('errors for JavaScript templates', () => {
|
|
294
|
+
const result = validateLegacyCreateFlags({ template: 'javascript' })
|
|
295
|
+
expect(result.error).toContain('JavaScript/JSX templates are not supported')
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('errors for unknown template values', () => {
|
|
299
|
+
const result = validateLegacyCreateFlags({ template: 'foo' })
|
|
300
|
+
expect(result.error).toContain('Invalid --template value')
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('warns for supported deprecated template values', () => {
|
|
304
|
+
const result = validateLegacyCreateFlags({ template: 'tsx' })
|
|
305
|
+
expect(result.error).toBeUndefined()
|
|
306
|
+
expect(result.warnings[0]).toContain('--template')
|
|
307
|
+
})
|
|
308
|
+
})
|
package/tests/options.test.ts
CHANGED
|
@@ -23,28 +23,23 @@ beforeEach(() => {
|
|
|
23
23
|
{
|
|
24
24
|
id: 'react-query',
|
|
25
25
|
type: 'add-on',
|
|
26
|
-
modes: ['file-router'
|
|
26
|
+
modes: ['file-router'],
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
id: 'tanstack-chat',
|
|
30
30
|
type: 'add-on',
|
|
31
|
-
modes: ['file-router'
|
|
31
|
+
modes: ['file-router'],
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
id: 'biome',
|
|
35
35
|
type: 'toolchain',
|
|
36
|
-
modes: ['file-router'
|
|
36
|
+
modes: ['file-router'],
|
|
37
37
|
},
|
|
38
38
|
],
|
|
39
39
|
supportedModes: {
|
|
40
|
-
'code-router': {
|
|
41
|
-
displayName: 'Code Router',
|
|
42
|
-
description: 'TanStack Router using code to define the routes',
|
|
43
|
-
forceTypescript: false,
|
|
44
|
-
},
|
|
45
40
|
'file-router': {
|
|
46
41
|
displayName: 'File Router',
|
|
47
|
-
description: 'TanStack
|
|
42
|
+
description: 'TanStack Start with file-based routing',
|
|
48
43
|
forceTypescript: true,
|
|
49
44
|
},
|
|
50
45
|
},
|
|
@@ -67,11 +62,6 @@ const baseCliOptions: CliOptions = {
|
|
|
67
62
|
|
|
68
63
|
function setBasicSpies() {
|
|
69
64
|
vi.spyOn(prompts, 'getProjectName').mockImplementation(async () => 'hello')
|
|
70
|
-
vi.spyOn(prompts, 'selectRouterType').mockImplementation(
|
|
71
|
-
async () => 'file-router',
|
|
72
|
-
)
|
|
73
|
-
vi.spyOn(prompts, 'selectTypescript').mockImplementation(async () => true)
|
|
74
|
-
vi.spyOn(prompts, 'selectTailwind').mockImplementation(async () => true)
|
|
75
65
|
vi.spyOn(prompts, 'selectPackageManager').mockImplementation(
|
|
76
66
|
async () => 'npm',
|
|
77
67
|
)
|
|
@@ -101,95 +91,22 @@ describe('promptForCreateOptions', () => {
|
|
|
101
91
|
expect(options?.projectName).toBe('override')
|
|
102
92
|
})
|
|
103
93
|
|
|
104
|
-
//// Mode
|
|
94
|
+
//// Mode is always file-router (TanStack Start)
|
|
105
95
|
|
|
106
|
-
it('
|
|
96
|
+
it('mode should always be file-router', async () => {
|
|
107
97
|
setBasicSpies()
|
|
108
98
|
|
|
109
|
-
const options = await promptForCreateOptions(
|
|
110
|
-
{ ...baseCliOptions, template: 'javascript' },
|
|
111
|
-
{ forcedMode: 'file-router' },
|
|
112
|
-
)
|
|
99
|
+
const options = await promptForCreateOptions(baseCliOptions, {})
|
|
113
100
|
|
|
114
101
|
expect(options?.mode).toBe('file-router')
|
|
115
102
|
expect(options?.typescript).toBe(true)
|
|
116
103
|
})
|
|
117
104
|
|
|
118
|
-
|
|
119
|
-
setBasicSpies()
|
|
120
|
-
|
|
121
|
-
vi.spyOn(prompts, 'selectRouterType').mockImplementation(
|
|
122
|
-
async () => 'code-router',
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
const options = await promptForCreateOptions(
|
|
126
|
-
{ ...baseCliOptions, template: 'javascript' },
|
|
127
|
-
{},
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
expect(options?.mode).toBe('code-router')
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
it('takes template from cli options - file-router', async () => {
|
|
134
|
-
setBasicSpies()
|
|
135
|
-
|
|
136
|
-
vi.spyOn(prompts, 'selectRouterType').mockImplementation(
|
|
137
|
-
async () => 'code-router',
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
const options = await promptForCreateOptions(
|
|
141
|
-
{ ...baseCliOptions, template: 'file-router' },
|
|
142
|
-
{},
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
expect(options?.mode).toBe('file-router')
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
it('prompt for router type when unspecified', async () => {
|
|
149
|
-
setBasicSpies()
|
|
150
|
-
|
|
151
|
-
vi.spyOn(prompts, 'selectRouterType').mockImplementation(
|
|
152
|
-
async () => 'code-router',
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
const options = await promptForCreateOptions(
|
|
156
|
-
{ ...baseCliOptions, tailwind: false, framework: undefined },
|
|
157
|
-
{},
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
expect(options?.mode).toBe('code-router')
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
//// Tailwind
|
|
164
|
-
|
|
165
|
-
it('prompt for tailwind when unspecified in react-cra', async () => {
|
|
166
|
-
setBasicSpies()
|
|
167
|
-
vi.spyOn(prompts, 'selectTailwind').mockImplementation(async () => false)
|
|
168
|
-
const options = await promptForCreateOptions(
|
|
169
|
-
{ ...baseCliOptions, tailwind: undefined },
|
|
170
|
-
{},
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
expect(options?.tailwind).toBe(false)
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
it('prompt for tailwind when unspecified in react-cra - true', async () => {
|
|
177
|
-
setBasicSpies()
|
|
178
|
-
vi.spyOn(prompts, 'selectTailwind').mockImplementation(async () => true)
|
|
179
|
-
const options = await promptForCreateOptions(
|
|
180
|
-
{ ...baseCliOptions, tailwind: undefined },
|
|
181
|
-
{},
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
expect(options?.tailwind).toBe(true)
|
|
185
|
-
})
|
|
105
|
+
//// Tailwind is always enabled
|
|
186
106
|
|
|
187
|
-
it('
|
|
107
|
+
it('tailwind is always enabled', async () => {
|
|
188
108
|
setBasicSpies()
|
|
189
|
-
const options = await promptForCreateOptions(
|
|
190
|
-
{ ...baseCliOptions, tailwind: undefined, framework: 'solid' },
|
|
191
|
-
{},
|
|
192
|
-
)
|
|
109
|
+
const options = await promptForCreateOptions(baseCliOptions, {})
|
|
193
110
|
|
|
194
111
|
expect(options?.tailwind).toBe(true)
|
|
195
112
|
})
|
|
@@ -207,7 +124,7 @@ describe('promptForCreateOptions', () => {
|
|
|
207
124
|
expect(options?.packageManager).toBe('bun')
|
|
208
125
|
})
|
|
209
126
|
|
|
210
|
-
it('
|
|
127
|
+
it('detects package manager from environment', async () => {
|
|
211
128
|
setBasicSpies()
|
|
212
129
|
|
|
213
130
|
process.env.npm_config_userconfig = 'blarg'
|
|
@@ -257,7 +174,6 @@ describe('promptForCreateOptions', () => {
|
|
|
257
174
|
expect(options?.chosenAddOns.map((a) => a.id).sort()).toEqual([
|
|
258
175
|
'react-query',
|
|
259
176
|
])
|
|
260
|
-
// Tailwind should be prompted (and mock returns true) since no add-on explicitly requires it
|
|
261
177
|
expect(options?.tailwind).toBe(true)
|
|
262
178
|
expect(options?.typescript).toBe(true)
|
|
263
179
|
})
|
|
@@ -274,9 +190,6 @@ describe('promptForCreateOptions', () => {
|
|
|
274
190
|
'biome',
|
|
275
191
|
'react-query',
|
|
276
192
|
])
|
|
277
|
-
// In non-interactive mode with add-ons, tailwind defaults to false unless explicitly required
|
|
278
|
-
// But since we're in interactive mode (addOns is an array but we still prompt), tailwind is prompted
|
|
279
|
-
// The mock returns true, so tailwind should be true
|
|
280
193
|
expect(options?.tailwind).toBe(true)
|
|
281
194
|
expect(options?.typescript).toBe(true)
|
|
282
195
|
})
|
|
@@ -297,7 +210,6 @@ describe('promptForCreateOptions', () => {
|
|
|
297
210
|
'biome',
|
|
298
211
|
'react-query',
|
|
299
212
|
])
|
|
300
|
-
// Tailwind should be prompted (and mock returns true) since no add-on explicitly requires it
|
|
301
213
|
expect(options?.tailwind).toBe(true)
|
|
302
214
|
expect(options?.typescript).toBe(true)
|
|
303
215
|
})
|
package/tests/ui-prompts.test.ts
CHANGED
|
@@ -7,8 +7,6 @@ import {
|
|
|
7
7
|
selectAddOns,
|
|
8
8
|
selectGit,
|
|
9
9
|
selectPackageManager,
|
|
10
|
-
selectRouterType,
|
|
11
|
-
selectTailwind,
|
|
12
10
|
selectToolchain,
|
|
13
11
|
} from '../src/ui-prompts'
|
|
14
12
|
|
|
@@ -37,42 +35,6 @@ describe('getProjectName', () => {
|
|
|
37
35
|
})
|
|
38
36
|
})
|
|
39
37
|
|
|
40
|
-
describe('selectRouterType', () => {
|
|
41
|
-
it('should select the file router', async () => {
|
|
42
|
-
vi.spyOn(clack, 'select').mockImplementation(async () => 'file-router')
|
|
43
|
-
vi.spyOn(clack, 'isCancel').mockImplementation(() => false)
|
|
44
|
-
|
|
45
|
-
const routerType = await selectRouterType()
|
|
46
|
-
expect(routerType).toBe('file-router')
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('should exit on cancel', async () => {
|
|
50
|
-
vi.spyOn(clack, 'select').mockImplementation(async () => 'Cancelled')
|
|
51
|
-
vi.spyOn(clack, 'isCancel').mockImplementation(() => true)
|
|
52
|
-
|
|
53
|
-
await expect(() => selectRouterType()).rejects.toThrowError(/exit/)
|
|
54
|
-
})
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
describe('selectTailwind', () => {
|
|
58
|
-
it('should select tailwind', async () => {
|
|
59
|
-
vi.spyOn(clack, 'confirm').mockImplementation(async () => true)
|
|
60
|
-
vi.spyOn(clack, 'isCancel').mockImplementation(() => false)
|
|
61
|
-
|
|
62
|
-
const tailwind = await selectTailwind()
|
|
63
|
-
expect(tailwind).toBe(true)
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('should exit on cancel', async () => {
|
|
67
|
-
vi.spyOn(clack, 'confirm').mockImplementation(async () =>
|
|
68
|
-
Symbol.for('cancel'),
|
|
69
|
-
)
|
|
70
|
-
vi.spyOn(clack, 'isCancel').mockImplementation(() => true)
|
|
71
|
-
|
|
72
|
-
await expect(selectTailwind()).rejects.toThrowError(/exit/)
|
|
73
|
-
})
|
|
74
|
-
})
|
|
75
|
-
|
|
76
38
|
describe('selectPackageManager', () => {
|
|
77
39
|
it('should select pnpm', async () => {
|
|
78
40
|
vi.spyOn(clack, 'select').mockImplementation(async () => 'pnpm')
|