@tanstack/cli 0.0.7 → 0.48.2

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 (83) hide show
  1. package/dist/bin.js +7 -0
  2. package/dist/cli.js +481 -0
  3. package/dist/command-line.js +174 -0
  4. package/dist/dev-watch.js +290 -0
  5. package/dist/file-syncer.js +148 -0
  6. package/dist/index.js +1 -0
  7. package/dist/mcp/api.js +31 -0
  8. package/dist/mcp/tools.js +250 -0
  9. package/dist/mcp/types.js +37 -0
  10. package/dist/mcp.js +121 -0
  11. package/dist/options.js +162 -0
  12. package/dist/types/bin.d.ts +2 -0
  13. package/dist/types/cli.d.ts +16 -0
  14. package/dist/types/command-line.d.ts +10 -0
  15. package/dist/types/dev-watch.d.ts +27 -0
  16. package/dist/types/file-syncer.d.ts +18 -0
  17. package/dist/types/index.d.ts +1 -0
  18. package/dist/types/mcp/api.d.ts +4 -0
  19. package/dist/types/mcp/tools.d.ts +2 -0
  20. package/dist/types/mcp/types.d.ts +217 -0
  21. package/dist/types/mcp.d.ts +6 -0
  22. package/dist/types/options.d.ts +8 -0
  23. package/dist/types/types.d.ts +25 -0
  24. package/dist/types/ui-environment.d.ts +2 -0
  25. package/dist/types/ui-prompts.d.ts +12 -0
  26. package/dist/types/utils.d.ts +8 -0
  27. package/dist/types.js +1 -0
  28. package/dist/ui-environment.js +52 -0
  29. package/dist/ui-prompts.js +244 -0
  30. package/dist/utils.js +30 -0
  31. package/package.json +46 -46
  32. package/src/bin.ts +6 -93
  33. package/src/cli.ts +692 -0
  34. package/src/command-line.ts +236 -0
  35. package/src/dev-watch.ts +430 -0
  36. package/src/file-syncer.ts +205 -0
  37. package/src/index.ts +1 -85
  38. package/src/mcp.ts +190 -0
  39. package/src/options.ts +260 -0
  40. package/src/types.ts +27 -0
  41. package/src/ui-environment.ts +74 -0
  42. package/src/ui-prompts.ts +322 -0
  43. package/src/utils.ts +38 -0
  44. package/tests/command-line.test.ts +304 -0
  45. package/tests/index.test.ts +9 -0
  46. package/tests/mcp.test.ts +225 -0
  47. package/tests/options.test.ts +304 -0
  48. package/tests/setupVitest.ts +6 -0
  49. package/tests/ui-environment.test.ts +97 -0
  50. package/tests/ui-prompts.test.ts +238 -0
  51. package/tsconfig.json +17 -0
  52. package/vitest.config.js +7 -0
  53. package/dist/bin.cjs +0 -761
  54. package/dist/bin.d.cts +0 -1
  55. package/dist/bin.d.mts +0 -1
  56. package/dist/bin.mjs +0 -760
  57. package/dist/index.cjs +0 -36
  58. package/dist/index.d.cts +0 -1172
  59. package/dist/index.d.mts +0 -1172
  60. package/dist/index.mjs +0 -3
  61. package/dist/template-CkAkdP8n.mjs +0 -2545
  62. package/dist/template-Cup47s9h.cjs +0 -2783
  63. package/src/api/fetch.test.ts +0 -114
  64. package/src/api/fetch.ts +0 -249
  65. package/src/cache/index.ts +0 -89
  66. package/src/commands/create.ts +0 -463
  67. package/src/commands/mcp.test.ts +0 -152
  68. package/src/commands/mcp.ts +0 -203
  69. package/src/engine/compile-with-addons.test.ts +0 -302
  70. package/src/engine/compile.test.ts +0 -404
  71. package/src/engine/compile.ts +0 -551
  72. package/src/engine/config-file.test.ts +0 -118
  73. package/src/engine/config-file.ts +0 -61
  74. package/src/engine/custom-addons/integration.ts +0 -323
  75. package/src/engine/custom-addons/shared.test.ts +0 -98
  76. package/src/engine/custom-addons/shared.ts +0 -281
  77. package/src/engine/custom-addons/template.test.ts +0 -288
  78. package/src/engine/custom-addons/template.ts +0 -124
  79. package/src/engine/template.test.ts +0 -256
  80. package/src/engine/template.ts +0 -269
  81. package/src/engine/types.ts +0 -336
  82. package/src/parse-gitignore.d.ts +0 -5
  83. package/src/templates/base.ts +0 -891
@@ -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,6 @@
1
+ import createFetchMock from 'vitest-fetch-mock'
2
+ import { vi } from 'vitest'
3
+
4
+ const fetchMocker = createFetchMock(vi)
5
+
6
+ fetchMocker.enableMocks()
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ setupFiles: ['./tests/setupVitest.js'],
6
+ },
7
+ })