@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.
- package/LICENSE +21 -0
- package/dist/bin.js +7 -0
- package/dist/cli.js +481 -0
- package/dist/command-line.js +174 -0
- package/dist/dev-watch.js +290 -0
- package/dist/file-syncer.js +148 -0
- package/dist/index.js +1 -0
- package/dist/mcp/api.js +31 -0
- package/dist/mcp/tools.js +250 -0
- package/dist/mcp/types.js +37 -0
- package/dist/mcp.js +121 -0
- package/dist/options.js +162 -0
- package/dist/types/bin.d.ts +2 -0
- package/dist/types/cli.d.ts +16 -0
- package/dist/types/command-line.d.ts +10 -0
- package/dist/types/dev-watch.d.ts +27 -0
- package/dist/types/file-syncer.d.ts +18 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/mcp/api.d.ts +4 -0
- package/dist/types/mcp/tools.d.ts +2 -0
- package/dist/types/mcp/types.d.ts +217 -0
- package/dist/types/mcp.d.ts +6 -0
- package/dist/types/options.d.ts +8 -0
- package/dist/types/types.d.ts +25 -0
- package/dist/types/ui-environment.d.ts +2 -0
- package/dist/types/ui-prompts.d.ts +12 -0
- package/dist/types/utils.d.ts +8 -0
- package/dist/types.js +1 -0
- package/dist/ui-environment.js +52 -0
- package/dist/ui-prompts.js +244 -0
- package/dist/utils.js +30 -0
- package/package.json +46 -47
- package/src/bin.ts +6 -93
- package/src/cli.ts +692 -0
- package/src/command-line.ts +236 -0
- package/src/dev-watch.ts +430 -0
- package/src/file-syncer.ts +205 -0
- package/src/index.ts +1 -85
- package/src/mcp.ts +190 -0
- package/src/options.ts +260 -0
- package/src/types.ts +27 -0
- package/src/ui-environment.ts +74 -0
- package/src/ui-prompts.ts +322 -0
- package/src/utils.ts +38 -0
- package/tests/command-line.test.ts +304 -0
- package/tests/index.test.ts +9 -0
- package/tests/mcp.test.ts +225 -0
- package/tests/options.test.ts +304 -0
- package/tests/setupVitest.ts +6 -0
- package/tests/ui-environment.test.ts +97 -0
- package/tests/ui-prompts.test.ts +238 -0
- package/tsconfig.json +17 -0
- package/vitest.config.js +7 -0
- package/dist/bin.cjs +0 -769
- package/dist/bin.d.cts +0 -1
- package/dist/bin.d.mts +0 -1
- package/dist/bin.mjs +0 -768
- package/dist/fetch-CbFFGJEw.cjs +0 -3
- package/dist/fetch-DG5dLrsb.cjs +0 -522
- package/dist/fetch-DhlVXS6S.mjs +0 -390
- package/dist/fetch-I_OVg8JX.mjs +0 -3
- package/dist/index.cjs +0 -37
- package/dist/index.d.cts +0 -1172
- package/dist/index.d.mts +0 -1172
- package/dist/index.mjs +0 -4
- package/dist/template-Szi7-AZJ.mjs +0 -2202
- package/dist/template-lWrIZhCQ.cjs +0 -2314
- package/src/api/fetch.test.ts +0 -114
- package/src/api/fetch.ts +0 -278
- package/src/cache/index.ts +0 -89
- package/src/commands/create.ts +0 -470
- package/src/commands/mcp.test.ts +0 -152
- package/src/commands/mcp.ts +0 -211
- package/src/engine/compile-with-addons.test.ts +0 -302
- package/src/engine/compile.test.ts +0 -404
- package/src/engine/compile.ts +0 -569
- package/src/engine/config-file.test.ts +0 -118
- package/src/engine/config-file.ts +0 -61
- package/src/engine/custom-addons/integration.ts +0 -323
- package/src/engine/custom-addons/shared.test.ts +0 -98
- package/src/engine/custom-addons/shared.ts +0 -281
- package/src/engine/custom-addons/template.test.ts +0 -288
- package/src/engine/custom-addons/template.ts +0 -124
- package/src/engine/template.test.ts +0 -256
- package/src/engine/template.ts +0 -269
- package/src/engine/types.ts +0 -336
- package/src/parse-gitignore.d.ts +0 -5
- package/src/templates/base.ts +0 -883
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { beforeEach, describe, it, expect, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { promptForCreateOptions } from '../src/options'
|
|
4
|
+
import {
|
|
5
|
+
__testClearFrameworks,
|
|
6
|
+
__testRegisterFramework,
|
|
7
|
+
} from '@tanstack/create'
|
|
8
|
+
|
|
9
|
+
import * as prompts from '../src/ui-prompts'
|
|
10
|
+
|
|
11
|
+
import type { Framework } from '@tanstack/create'
|
|
12
|
+
|
|
13
|
+
import type { CliOptions } from '../src/types'
|
|
14
|
+
|
|
15
|
+
vi.mock('../src/ui-prompts')
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
__testClearFrameworks()
|
|
19
|
+
__testRegisterFramework({
|
|
20
|
+
id: 'react-cra',
|
|
21
|
+
name: 'react',
|
|
22
|
+
getAddOns: () => [
|
|
23
|
+
{
|
|
24
|
+
id: 'react-query',
|
|
25
|
+
type: 'add-on',
|
|
26
|
+
modes: ['file-router', 'code-router'],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'tanstack-chat',
|
|
30
|
+
type: 'add-on',
|
|
31
|
+
modes: ['file-router', 'code-router'],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'biome',
|
|
35
|
+
type: 'toolchain',
|
|
36
|
+
modes: ['file-router', 'code-router'],
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
supportedModes: {
|
|
40
|
+
'code-router': {
|
|
41
|
+
displayName: 'Code Router',
|
|
42
|
+
description: 'TanStack Router using code to define the routes',
|
|
43
|
+
forceTypescript: false,
|
|
44
|
+
},
|
|
45
|
+
'file-router': {
|
|
46
|
+
displayName: 'File Router',
|
|
47
|
+
description: 'TanStack Router using files to define the routes',
|
|
48
|
+
forceTypescript: true,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
} as unknown as Framework)
|
|
52
|
+
|
|
53
|
+
__testRegisterFramework({
|
|
54
|
+
id: 'solid',
|
|
55
|
+
name: 'solid',
|
|
56
|
+
getAddOns: () => [],
|
|
57
|
+
} as unknown as Framework)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const baseCliOptions: CliOptions = {
|
|
61
|
+
framework: 'react-cra',
|
|
62
|
+
addOns: [],
|
|
63
|
+
toolchain: undefined,
|
|
64
|
+
projectName: undefined,
|
|
65
|
+
git: undefined,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function setBasicSpies() {
|
|
69
|
+
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
|
+
vi.spyOn(prompts, 'selectPackageManager').mockImplementation(
|
|
76
|
+
async () => 'npm',
|
|
77
|
+
)
|
|
78
|
+
vi.spyOn(prompts, 'selectToolchain').mockImplementation(async () => undefined)
|
|
79
|
+
vi.spyOn(prompts, 'selectAddOns').mockImplementation(async () => [])
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe('promptForCreateOptions', () => {
|
|
83
|
+
//// Project name
|
|
84
|
+
|
|
85
|
+
it('prompt for a project name', async () => {
|
|
86
|
+
setBasicSpies()
|
|
87
|
+
|
|
88
|
+
const options = await promptForCreateOptions(baseCliOptions, {})
|
|
89
|
+
|
|
90
|
+
expect(options?.projectName).toBe('hello')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('accept incoming project name', async () => {
|
|
94
|
+
setBasicSpies()
|
|
95
|
+
|
|
96
|
+
const options = await promptForCreateOptions(
|
|
97
|
+
{ ...baseCliOptions, projectName: 'override' },
|
|
98
|
+
{},
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
expect(options?.projectName).toBe('override')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
//// Mode (router type)
|
|
105
|
+
|
|
106
|
+
it('forceMode should override template', async () => {
|
|
107
|
+
setBasicSpies()
|
|
108
|
+
|
|
109
|
+
const options = await promptForCreateOptions(
|
|
110
|
+
{ ...baseCliOptions, template: 'javascript' },
|
|
111
|
+
{ forcedMode: 'file-router' },
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
expect(options?.mode).toBe('file-router')
|
|
115
|
+
expect(options?.typescript).toBe(true)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('takes template from cli options - code-router', async () => {
|
|
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
|
+
})
|
|
186
|
+
|
|
187
|
+
it('set tailwind when solid', async () => {
|
|
188
|
+
setBasicSpies()
|
|
189
|
+
const options = await promptForCreateOptions(
|
|
190
|
+
{ ...baseCliOptions, tailwind: undefined, framework: 'solid' },
|
|
191
|
+
{},
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
expect(options?.tailwind).toBe(true)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
//// Package manager
|
|
198
|
+
|
|
199
|
+
it('uses the package manager from the cli options', async () => {
|
|
200
|
+
setBasicSpies()
|
|
201
|
+
|
|
202
|
+
const options = await promptForCreateOptions(
|
|
203
|
+
{ ...baseCliOptions, packageManager: 'bun' },
|
|
204
|
+
{},
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
expect(options?.packageManager).toBe('bun')
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('uses the package manager from the cli options', async () => {
|
|
211
|
+
setBasicSpies()
|
|
212
|
+
|
|
213
|
+
process.env.npm_config_userconfig = 'blarg'
|
|
214
|
+
|
|
215
|
+
const options = await promptForCreateOptions(
|
|
216
|
+
{ ...baseCliOptions, packageManager: undefined },
|
|
217
|
+
{},
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
expect(options?.packageManager).toBe('pnpm')
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
//// Add-ons
|
|
224
|
+
it('should be clean when no add-ons are selected', async () => {
|
|
225
|
+
setBasicSpies()
|
|
226
|
+
|
|
227
|
+
const options = await promptForCreateOptions({ ...baseCliOptions }, {})
|
|
228
|
+
|
|
229
|
+
expect(options?.chosenAddOns).toEqual([])
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('should select biome when toolchain is specified', async () => {
|
|
233
|
+
setBasicSpies()
|
|
234
|
+
|
|
235
|
+
vi.spyOn(prompts, 'selectToolchain').mockImplementation(async () => 'biome')
|
|
236
|
+
|
|
237
|
+
const options = await promptForCreateOptions(
|
|
238
|
+
{ ...baseCliOptions, toolchain: 'biome' },
|
|
239
|
+
{},
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
expect(options?.chosenAddOns.map((a) => a.id).sort()).toEqual(['biome'])
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('should handle forced add-ons', async () => {
|
|
246
|
+
setBasicSpies()
|
|
247
|
+
|
|
248
|
+
vi.spyOn(prompts, 'selectToolchain').mockImplementation(
|
|
249
|
+
async () => undefined,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
const options = await promptForCreateOptions(
|
|
253
|
+
{ ...baseCliOptions },
|
|
254
|
+
{ forcedAddOns: ['react-query'] },
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
expect(options?.chosenAddOns.map((a) => a.id).sort()).toEqual([
|
|
258
|
+
'react-query',
|
|
259
|
+
])
|
|
260
|
+
// Tailwind should be prompted (and mock returns true) since no add-on explicitly requires it
|
|
261
|
+
expect(options?.tailwind).toBe(true)
|
|
262
|
+
expect(options?.typescript).toBe(true)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('should handle add-ons from the CLI', async () => {
|
|
266
|
+
setBasicSpies()
|
|
267
|
+
|
|
268
|
+
const options = await promptForCreateOptions(
|
|
269
|
+
{ ...baseCliOptions, addOns: ['biome', 'react-query'] },
|
|
270
|
+
{},
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
expect(options?.chosenAddOns.map((a) => a.id).sort()).toEqual([
|
|
274
|
+
'biome',
|
|
275
|
+
'react-query',
|
|
276
|
+
])
|
|
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
|
+
expect(options?.tailwind).toBe(true)
|
|
281
|
+
expect(options?.typescript).toBe(true)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('should handle user-selected add-ons', async () => {
|
|
285
|
+
setBasicSpies()
|
|
286
|
+
|
|
287
|
+
vi.spyOn(prompts, 'selectAddOns').mockImplementation(async () =>
|
|
288
|
+
Promise.resolve(['biome', 'react-query']),
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
const options = await promptForCreateOptions(
|
|
292
|
+
{ ...baseCliOptions, addOns: undefined },
|
|
293
|
+
{},
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
expect(options?.chosenAddOns.map((a) => a.id).sort()).toEqual([
|
|
297
|
+
'biome',
|
|
298
|
+
'react-query',
|
|
299
|
+
])
|
|
300
|
+
// Tailwind should be prompted (and mock returns true) since no add-on explicitly requires it
|
|
301
|
+
expect(options?.tailwind).toBe(true)
|
|
302
|
+
expect(options?.typescript).toBe(true)
|
|
303
|
+
})
|
|
304
|
+
})
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import * as clack from '@clack/prompts'
|
|
4
|
+
|
|
5
|
+
import { createUIEnvironment } from '../src/ui-environment.js'
|
|
6
|
+
|
|
7
|
+
vi.mock('@clack/prompts')
|
|
8
|
+
|
|
9
|
+
// @ts-expect-error
|
|
10
|
+
vi.spyOn(process, 'exit').mockImplementation(() => {})
|
|
11
|
+
|
|
12
|
+
describe('createUIEnvironment', () => {
|
|
13
|
+
it('should create a silent UI environment', () => {
|
|
14
|
+
const environment = createUIEnvironment('test', true)
|
|
15
|
+
expect(environment.appName).toBe('test')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('should handle intro', () => {
|
|
19
|
+
const environment = createUIEnvironment('test', false)
|
|
20
|
+
const spy = vi
|
|
21
|
+
.spyOn(clack, 'intro')
|
|
22
|
+
.mockImplementation(async () => undefined)
|
|
23
|
+
environment.intro('test')
|
|
24
|
+
expect(spy).toHaveBeenCalledWith('test')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should handle outro', () => {
|
|
28
|
+
const environment = createUIEnvironment('test', false)
|
|
29
|
+
const spy = vi
|
|
30
|
+
.spyOn(clack, 'outro')
|
|
31
|
+
.mockImplementation(async () => undefined)
|
|
32
|
+
environment.outro('test')
|
|
33
|
+
expect(spy).toHaveBeenCalledWith('test')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should handle info', () => {
|
|
37
|
+
const environment = createUIEnvironment('test', false)
|
|
38
|
+
const spy = vi
|
|
39
|
+
.spyOn(clack.log, 'info')
|
|
40
|
+
.mockImplementation(async () => undefined)
|
|
41
|
+
environment.info('test')
|
|
42
|
+
expect(spy).toHaveBeenCalled()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should handle error', () => {
|
|
46
|
+
const environment = createUIEnvironment('test', false)
|
|
47
|
+
const spy = vi
|
|
48
|
+
.spyOn(clack.log, 'error')
|
|
49
|
+
.mockImplementation(async () => undefined)
|
|
50
|
+
environment.error('test')
|
|
51
|
+
expect(spy).toHaveBeenCalled()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should handle warn', () => {
|
|
55
|
+
const environment = createUIEnvironment('test', false)
|
|
56
|
+
const spy = vi
|
|
57
|
+
.spyOn(clack.log, 'warn')
|
|
58
|
+
.mockImplementation(async () => undefined)
|
|
59
|
+
environment.warn('test')
|
|
60
|
+
expect(spy).toHaveBeenCalled()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should handle confirm', async () => {
|
|
64
|
+
const environment = createUIEnvironment('test', false)
|
|
65
|
+
const spy = vi.spyOn(clack, 'confirm').mockImplementation(async () => true)
|
|
66
|
+
const isCancelSpy = vi
|
|
67
|
+
.spyOn(clack, 'isCancel')
|
|
68
|
+
.mockImplementation(() => false)
|
|
69
|
+
const result = await environment.confirm('test')
|
|
70
|
+
expect(spy).toHaveBeenCalled()
|
|
71
|
+
expect(isCancelSpy).toHaveBeenCalled()
|
|
72
|
+
expect(result).toBe(true)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should handle confirm', async () => {
|
|
76
|
+
const environment = createUIEnvironment('test', false)
|
|
77
|
+
const spy = vi.spyOn(clack, 'confirm').mockImplementation(async () => true)
|
|
78
|
+
const isCancelSpy = vi
|
|
79
|
+
.spyOn(clack, 'isCancel')
|
|
80
|
+
.mockImplementation(() => true)
|
|
81
|
+
await environment.confirm('test')
|
|
82
|
+
expect(spy).toHaveBeenCalled()
|
|
83
|
+
expect(isCancelSpy).toHaveBeenCalled()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should handle spinner', async () => {
|
|
87
|
+
const environment = createUIEnvironment('test', false)
|
|
88
|
+
// @ts-expect-error
|
|
89
|
+
const spy = vi.spyOn(clack, 'spinner').mockImplementation(async () => ({
|
|
90
|
+
start: (_msg?: string) => {},
|
|
91
|
+
stop: (_msg?: string) => {},
|
|
92
|
+
message: (_msg?: string) => {},
|
|
93
|
+
}))
|
|
94
|
+
const result = await environment.spinner()
|
|
95
|
+
expect(spy).toHaveBeenCalled()
|
|
96
|
+
})
|
|
97
|
+
})
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import * as clack from '@clack/prompts'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
getProjectName,
|
|
7
|
+
selectAddOns,
|
|
8
|
+
selectGit,
|
|
9
|
+
selectPackageManager,
|
|
10
|
+
selectRouterType,
|
|
11
|
+
selectTailwind,
|
|
12
|
+
selectToolchain,
|
|
13
|
+
} from '../src/ui-prompts'
|
|
14
|
+
|
|
15
|
+
import type { AddOn, Framework } from '@tanstack/create'
|
|
16
|
+
|
|
17
|
+
vi.mock('@clack/prompts')
|
|
18
|
+
|
|
19
|
+
vi.spyOn(process, 'exit').mockImplementation((number) => {
|
|
20
|
+
throw new Error(`process.exit: ${number}`)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('getProjectName', () => {
|
|
24
|
+
it('should return the project name', async () => {
|
|
25
|
+
vi.spyOn(clack, 'text').mockImplementation(async () => 'my-app')
|
|
26
|
+
vi.spyOn(clack, 'isCancel').mockImplementation(() => false)
|
|
27
|
+
|
|
28
|
+
const projectName = await getProjectName()
|
|
29
|
+
expect(projectName).toBe('my-app')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should exit on cancel', async () => {
|
|
33
|
+
vi.spyOn(clack, 'text').mockImplementation(async () => 'Cancelled')
|
|
34
|
+
vi.spyOn(clack, 'isCancel').mockImplementation(() => true)
|
|
35
|
+
|
|
36
|
+
await expect(getProjectName()).rejects.toThrowError(/exit/)
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
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
|
+
describe('selectPackageManager', () => {
|
|
77
|
+
it('should select pnpm', async () => {
|
|
78
|
+
vi.spyOn(clack, 'select').mockImplementation(async () => 'pnpm')
|
|
79
|
+
vi.spyOn(clack, 'isCancel').mockImplementation(() => false)
|
|
80
|
+
|
|
81
|
+
const packageManager = await selectPackageManager()
|
|
82
|
+
expect(packageManager).toBe('pnpm')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('should exit on cancel', async () => {
|
|
86
|
+
vi.spyOn(clack, 'select').mockImplementation(async () =>
|
|
87
|
+
Symbol.for('cancel'),
|
|
88
|
+
)
|
|
89
|
+
vi.spyOn(clack, 'isCancel').mockImplementation(() => true)
|
|
90
|
+
|
|
91
|
+
await expect(selectPackageManager()).rejects.toThrowError(/exit/)
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('selectAddOns', () => {
|
|
96
|
+
it('should show keyboard shortcuts help and select add-ons', async () => {
|
|
97
|
+
const noteSpy = vi.spyOn(clack, 'note').mockImplementation(() => {})
|
|
98
|
+
vi.spyOn(clack, 'multiselect').mockImplementation(async () => ['add-on-1'])
|
|
99
|
+
vi.spyOn(clack, 'isCancel').mockImplementation(() => false)
|
|
100
|
+
|
|
101
|
+
const packageManager = await selectAddOns(
|
|
102
|
+
{
|
|
103
|
+
getAddOns: () =>
|
|
104
|
+
[
|
|
105
|
+
{
|
|
106
|
+
id: 'add-on-1',
|
|
107
|
+
name: 'Add-on 1',
|
|
108
|
+
description: 'Add-on 1 description',
|
|
109
|
+
type: 'add-on',
|
|
110
|
+
modes: ['file-router'],
|
|
111
|
+
},
|
|
112
|
+
] as Array<AddOn>,
|
|
113
|
+
} as Framework,
|
|
114
|
+
'file-router',
|
|
115
|
+
'add-on',
|
|
116
|
+
'Select add-ons',
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
expect(packageManager).toEqual(['add-on-1'])
|
|
120
|
+
expect(noteSpy).toHaveBeenCalledWith(
|
|
121
|
+
'Use ↑/↓ to navigate • Space to select/deselect • Enter to confirm',
|
|
122
|
+
'Keyboard Shortcuts',
|
|
123
|
+
)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should exit on cancel', async () => {
|
|
127
|
+
vi.spyOn(clack, 'select').mockImplementation(async () =>
|
|
128
|
+
Symbol.for('cancel'),
|
|
129
|
+
)
|
|
130
|
+
vi.spyOn(clack, 'isCancel').mockImplementation(() => true)
|
|
131
|
+
|
|
132
|
+
await expect(
|
|
133
|
+
selectAddOns(
|
|
134
|
+
{
|
|
135
|
+
getAddOns: () =>
|
|
136
|
+
[
|
|
137
|
+
{
|
|
138
|
+
id: 'add-on-1',
|
|
139
|
+
name: 'Add-on 1',
|
|
140
|
+
description: 'Add-on 1 description',
|
|
141
|
+
type: 'add-on',
|
|
142
|
+
modes: ['file-router'],
|
|
143
|
+
},
|
|
144
|
+
] as Array<AddOn>,
|
|
145
|
+
} as Framework,
|
|
146
|
+
'file-router',
|
|
147
|
+
'add-on',
|
|
148
|
+
'Select add-ons',
|
|
149
|
+
),
|
|
150
|
+
).rejects.toThrowError(/exit/)
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('selectGit', () => {
|
|
155
|
+
it('should select git', async () => {
|
|
156
|
+
vi.spyOn(clack, 'confirm').mockImplementation(async () => true)
|
|
157
|
+
vi.spyOn(clack, 'isCancel').mockImplementation(() => false)
|
|
158
|
+
|
|
159
|
+
const git = await selectGit()
|
|
160
|
+
expect(git).toBe(true)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('should exit on cancel', async () => {
|
|
164
|
+
vi.spyOn(clack, 'confirm').mockImplementation(async () =>
|
|
165
|
+
Symbol.for('cancel'),
|
|
166
|
+
)
|
|
167
|
+
vi.spyOn(clack, 'isCancel').mockImplementation(() => true)
|
|
168
|
+
|
|
169
|
+
await expect(selectGit()).rejects.toThrowError(/exit/)
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
describe('selectToolchain', () => {
|
|
174
|
+
it('should select a toolchain', async () => {
|
|
175
|
+
vi.spyOn(clack, 'select').mockImplementation(async () => 'biome')
|
|
176
|
+
vi.spyOn(clack, 'isCancel').mockImplementation(() => false)
|
|
177
|
+
|
|
178
|
+
const packageManager = await selectToolchain({
|
|
179
|
+
getAddOns: () =>
|
|
180
|
+
[
|
|
181
|
+
{
|
|
182
|
+
id: 'biome',
|
|
183
|
+
name: 'Biome',
|
|
184
|
+
description: 'Biome description',
|
|
185
|
+
type: 'toolchain',
|
|
186
|
+
modes: ['file-router'],
|
|
187
|
+
},
|
|
188
|
+
] as Array<AddOn>,
|
|
189
|
+
} as Framework)
|
|
190
|
+
expect(packageManager).toEqual('biome')
|
|
191
|
+
})
|
|
192
|
+
it('should select a toolchain', async () => {
|
|
193
|
+
const selectSpy = vi
|
|
194
|
+
.spyOn(clack, 'select')
|
|
195
|
+
.mockImplementation(async () => 'biome')
|
|
196
|
+
vi.spyOn(clack, 'isCancel').mockImplementation(() => false)
|
|
197
|
+
|
|
198
|
+
const packageManager = await selectToolchain(
|
|
199
|
+
{
|
|
200
|
+
getAddOns: () =>
|
|
201
|
+
[
|
|
202
|
+
{
|
|
203
|
+
id: 'biome',
|
|
204
|
+
name: 'Biome',
|
|
205
|
+
description: 'Biome description',
|
|
206
|
+
type: 'toolchain',
|
|
207
|
+
modes: ['file-router'],
|
|
208
|
+
},
|
|
209
|
+
] as Array<AddOn>,
|
|
210
|
+
} as Framework,
|
|
211
|
+
'biome',
|
|
212
|
+
)
|
|
213
|
+
expect(packageManager).toEqual('biome')
|
|
214
|
+
expect(selectSpy).not.toHaveBeenCalled()
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('should exit on cancel', async () => {
|
|
218
|
+
vi.spyOn(clack, 'select').mockImplementation(async () =>
|
|
219
|
+
Symbol.for('cancel'),
|
|
220
|
+
)
|
|
221
|
+
vi.spyOn(clack, 'isCancel').mockImplementation(() => true)
|
|
222
|
+
|
|
223
|
+
await expect(
|
|
224
|
+
selectToolchain({
|
|
225
|
+
getAddOns: () =>
|
|
226
|
+
[
|
|
227
|
+
{
|
|
228
|
+
id: 'biome',
|
|
229
|
+
name: 'Biome',
|
|
230
|
+
description: 'Biome description',
|
|
231
|
+
type: 'toolchain',
|
|
232
|
+
modes: ['file-router'],
|
|
233
|
+
},
|
|
234
|
+
] as Array<AddOn>,
|
|
235
|
+
} as Framework),
|
|
236
|
+
).rejects.toThrowError(/exit/)
|
|
237
|
+
})
|
|
238
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ES2020",
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"rootDir": "./src",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"moduleResolution": "node",
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationDir": "./dist/types"
|
|
14
|
+
},
|
|
15
|
+
"include": ["./src/**/*.ts"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|