@tanstack/cta-cli 0.10.0-alpha.19 → 0.10.0-alpha.21
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/dist/cli.js +82 -33
- package/dist/command-line.js +75 -0
- package/dist/mcp.js +170 -0
- package/dist/options.js +40 -331
- package/dist/types/cli.d.ts +3 -1
- package/dist/types/command-line.d.ts +3 -0
- package/dist/types/mcp.d.ts +7 -0
- package/dist/types/options.d.ts +2 -3
- package/dist/types/types.d.ts +3 -1
- package/dist/types/ui-environment.d.ts +1 -1
- package/dist/types/ui-prompts.d.ts +10 -0
- package/dist/types/utils.d.ts +3 -0
- package/dist/ui-environment.js +45 -42
- package/dist/ui-prompts.js +140 -0
- package/dist/utils.js +7 -0
- package/package.json +10 -6
- package/src/cli.ts +102 -46
- package/src/command-line.ts +111 -0
- package/src/mcp.ts +247 -0
- package/src/options.ts +69 -395
- package/src/types.ts +4 -1
- package/src/ui-environment.ts +54 -44
- package/src/ui-prompts.ts +187 -0
- package/src/utils.ts +11 -0
- package/tests/command-line.test.ts +205 -0
- package/tests/index.test.ts +9 -0
- package/tests/options.test.ts +287 -0
- package/tests/setupVitest.ts +6 -0
- package/tests/ui-environment.test.ts +97 -0
- package/tests/ui-prompts.test.ts +233 -0
- package/vitest.config.js +7 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cancel,
|
|
3
|
+
confirm,
|
|
4
|
+
isCancel,
|
|
5
|
+
multiselect,
|
|
6
|
+
select,
|
|
7
|
+
text,
|
|
8
|
+
} from '@clack/prompts'
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
CODE_ROUTER,
|
|
12
|
+
DEFAULT_PACKAGE_MANAGER,
|
|
13
|
+
FILE_ROUTER,
|
|
14
|
+
SUPPORTED_PACKAGE_MANAGERS,
|
|
15
|
+
getAllAddOns,
|
|
16
|
+
} from '@tanstack/cta-engine'
|
|
17
|
+
|
|
18
|
+
import type { AddOn, Mode, PackageManager } from '@tanstack/cta-engine'
|
|
19
|
+
|
|
20
|
+
import type { Framework } from '@tanstack/cta-engine/dist/types/types.js'
|
|
21
|
+
|
|
22
|
+
export async function getProjectName(): Promise<string> {
|
|
23
|
+
const value = await text({
|
|
24
|
+
message: 'What would you like to name your project?',
|
|
25
|
+
defaultValue: 'my-app',
|
|
26
|
+
validate(value) {
|
|
27
|
+
if (!value) {
|
|
28
|
+
return 'Please enter a name'
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
if (isCancel(value)) {
|
|
34
|
+
cancel('Operation cancelled.')
|
|
35
|
+
process.exit(0)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return value
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function selectRouterType(): Promise<Mode> {
|
|
42
|
+
const routerType = await select({
|
|
43
|
+
message: 'Select the router type:',
|
|
44
|
+
options: [
|
|
45
|
+
{
|
|
46
|
+
value: FILE_ROUTER,
|
|
47
|
+
label: 'File Router - File-based routing structure',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
value: CODE_ROUTER,
|
|
51
|
+
label: 'Code Router - Traditional code-based routing',
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
initialValue: FILE_ROUTER,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
if (isCancel(routerType)) {
|
|
58
|
+
cancel('Operation cancelled.')
|
|
59
|
+
process.exit(0)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return routerType as Mode
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function selectTypescript(): Promise<boolean> {
|
|
66
|
+
const typescriptEnable = await confirm({
|
|
67
|
+
message: 'Would you like to use TypeScript?',
|
|
68
|
+
initialValue: true,
|
|
69
|
+
})
|
|
70
|
+
if (isCancel(typescriptEnable)) {
|
|
71
|
+
cancel('Operation cancelled.')
|
|
72
|
+
process.exit(0)
|
|
73
|
+
}
|
|
74
|
+
return typescriptEnable
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function selectTailwind(): Promise<boolean> {
|
|
78
|
+
const tailwind = await confirm({
|
|
79
|
+
message: 'Would you like to use Tailwind CSS?',
|
|
80
|
+
initialValue: true,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
if (isCancel(tailwind)) {
|
|
84
|
+
cancel('Operation cancelled.')
|
|
85
|
+
process.exit(0)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return tailwind
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function selectPackageManager(): Promise<PackageManager> {
|
|
92
|
+
const packageManager = await select({
|
|
93
|
+
message: 'Select package manager:',
|
|
94
|
+
options: SUPPORTED_PACKAGE_MANAGERS.map((pm) => ({
|
|
95
|
+
value: pm,
|
|
96
|
+
label: pm,
|
|
97
|
+
})),
|
|
98
|
+
initialValue: DEFAULT_PACKAGE_MANAGER,
|
|
99
|
+
})
|
|
100
|
+
if (isCancel(packageManager)) {
|
|
101
|
+
cancel('Operation cancelled.')
|
|
102
|
+
process.exit(0)
|
|
103
|
+
}
|
|
104
|
+
return packageManager
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function selectAddOns(
|
|
108
|
+
framework: Framework,
|
|
109
|
+
mode: Mode,
|
|
110
|
+
type: string,
|
|
111
|
+
message: string,
|
|
112
|
+
forcedAddOns: Array<string> = [],
|
|
113
|
+
): Promise<Array<string>> {
|
|
114
|
+
const allAddOns = await getAllAddOns(framework, mode)
|
|
115
|
+
const addOns = allAddOns.filter((addOn) => addOn.type === type)
|
|
116
|
+
if (addOns.length === 0) {
|
|
117
|
+
return []
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const value = await multiselect({
|
|
121
|
+
message,
|
|
122
|
+
options: addOns
|
|
123
|
+
.filter((addOn) => !forcedAddOns.includes(addOn.id))
|
|
124
|
+
.map((addOn) => ({
|
|
125
|
+
value: addOn.id,
|
|
126
|
+
label: addOn.name,
|
|
127
|
+
hint: addOn.description,
|
|
128
|
+
})),
|
|
129
|
+
required: false,
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
if (isCancel(value)) {
|
|
133
|
+
cancel('Operation cancelled.')
|
|
134
|
+
process.exit(0)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return value
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function selectGit(): Promise<boolean> {
|
|
141
|
+
const git = await confirm({
|
|
142
|
+
message: 'Would you like to initialize a new git repository?',
|
|
143
|
+
initialValue: true,
|
|
144
|
+
})
|
|
145
|
+
if (isCancel(git)) {
|
|
146
|
+
cancel('Operation cancelled.')
|
|
147
|
+
process.exit(0)
|
|
148
|
+
}
|
|
149
|
+
return git
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function selectToolchain(
|
|
153
|
+
framework: Framework,
|
|
154
|
+
toolchain?: string,
|
|
155
|
+
): Promise<string | undefined> {
|
|
156
|
+
const toolchains = new Set<AddOn>()
|
|
157
|
+
for (const addOn of framework.getAddOns()) {
|
|
158
|
+
if (addOn.type === 'toolchain') {
|
|
159
|
+
toolchains.add(addOn)
|
|
160
|
+
if (toolchain && addOn.id === toolchain) {
|
|
161
|
+
return toolchain
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const tc = await select({
|
|
167
|
+
message: 'Select toolchain',
|
|
168
|
+
options: [
|
|
169
|
+
{
|
|
170
|
+
value: undefined,
|
|
171
|
+
label: 'None',
|
|
172
|
+
},
|
|
173
|
+
...Array.from(toolchains).map((tc) => ({
|
|
174
|
+
value: tc.id,
|
|
175
|
+
label: tc.name,
|
|
176
|
+
})),
|
|
177
|
+
],
|
|
178
|
+
initialValue: undefined,
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
if (isCancel(tc)) {
|
|
182
|
+
cancel('Operation cancelled.')
|
|
183
|
+
process.exit(0)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return tc
|
|
187
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { CODE_ROUTER, FILE_ROUTER } from '@tanstack/cta-engine'
|
|
2
|
+
import type { Mode } from '@tanstack/cta-engine'
|
|
3
|
+
|
|
4
|
+
import type { TemplateOptions } from './types.js'
|
|
5
|
+
|
|
6
|
+
export function convertTemplateToMode(template: TemplateOptions): Mode {
|
|
7
|
+
if (template === 'typescript' || template === 'javascript') {
|
|
8
|
+
return CODE_ROUTER
|
|
9
|
+
}
|
|
10
|
+
return FILE_ROUTER
|
|
11
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { normalizeOptions } from '../src/command-line.js'
|
|
4
|
+
import {
|
|
5
|
+
__testRegisterFramework,
|
|
6
|
+
__testClearFrameworks,
|
|
7
|
+
} from '@tanstack/cta-engine/dist/frameworks.js'
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
__testClearFrameworks()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
describe('normalizeOptions', () => {
|
|
14
|
+
it('should return undefined if project name is not provided', async () => {
|
|
15
|
+
const options = await normalizeOptions({})
|
|
16
|
+
expect(options).toBeUndefined()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should return enable typescript based on the framework', async () => {
|
|
20
|
+
const jsOptions = await normalizeOptions({
|
|
21
|
+
projectName: 'test',
|
|
22
|
+
template: 'javascript',
|
|
23
|
+
})
|
|
24
|
+
expect(jsOptions?.typescript).toBe(false)
|
|
25
|
+
expect(jsOptions?.mode).toBe('code-router')
|
|
26
|
+
|
|
27
|
+
const tsOptions = await normalizeOptions({
|
|
28
|
+
projectName: 'test',
|
|
29
|
+
template: 'typescript',
|
|
30
|
+
})
|
|
31
|
+
expect(tsOptions?.typescript).toBe(true)
|
|
32
|
+
expect(tsOptions?.mode).toBe('code-router')
|
|
33
|
+
|
|
34
|
+
const frOptions = await normalizeOptions({
|
|
35
|
+
projectName: 'test',
|
|
36
|
+
template: 'file-router',
|
|
37
|
+
})
|
|
38
|
+
expect(frOptions?.typescript).toBe(true)
|
|
39
|
+
expect(frOptions?.mode).toBe('file-router')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should return enable tailwind if the framework is solid', async () => {
|
|
43
|
+
const solidOptions = await normalizeOptions({
|
|
44
|
+
projectName: 'test',
|
|
45
|
+
framework: 'solid',
|
|
46
|
+
})
|
|
47
|
+
expect(solidOptions?.tailwind).toBe(true)
|
|
48
|
+
|
|
49
|
+
const twOptions = await normalizeOptions({
|
|
50
|
+
projectName: 'test',
|
|
51
|
+
tailwind: true,
|
|
52
|
+
})
|
|
53
|
+
expect(twOptions?.tailwind).toBe(true)
|
|
54
|
+
|
|
55
|
+
const noOptions = await normalizeOptions({
|
|
56
|
+
projectName: 'test',
|
|
57
|
+
})
|
|
58
|
+
expect(noOptions?.tailwind).toBe(false)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should handle a starter url', async () => {
|
|
62
|
+
__testRegisterFramework({
|
|
63
|
+
id: 'solid',
|
|
64
|
+
name: 'Solid',
|
|
65
|
+
getAddOns: () => [],
|
|
66
|
+
})
|
|
67
|
+
fetch.mockResponseOnce(
|
|
68
|
+
JSON.stringify({
|
|
69
|
+
id: 'https://github.com/cta-dev/cta-starter-solid',
|
|
70
|
+
tailwind: true,
|
|
71
|
+
typescript: false,
|
|
72
|
+
framework: 'solid',
|
|
73
|
+
mode: 'file-router',
|
|
74
|
+
type: 'starter',
|
|
75
|
+
description: 'A starter for Solid',
|
|
76
|
+
name: 'My Solid Starter',
|
|
77
|
+
dependsOn: [],
|
|
78
|
+
files: {},
|
|
79
|
+
deletedFiles: [],
|
|
80
|
+
}),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const options = await normalizeOptions({
|
|
84
|
+
projectName: 'test',
|
|
85
|
+
starter: 'https://github.com/cta-dev/cta-starter-solid',
|
|
86
|
+
})
|
|
87
|
+
expect(options?.mode).toBe('file-router')
|
|
88
|
+
expect(options?.tailwind).toBe(true)
|
|
89
|
+
expect(options?.typescript).toBe(false)
|
|
90
|
+
expect(options?.framework?.id).toBe('solid')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should default to react-cra if no framework is provided', async () => {
|
|
94
|
+
__testRegisterFramework({
|
|
95
|
+
id: 'react-cra',
|
|
96
|
+
name: 'react',
|
|
97
|
+
})
|
|
98
|
+
const options = await normalizeOptions({
|
|
99
|
+
projectName: 'test',
|
|
100
|
+
})
|
|
101
|
+
expect(options?.framework?.id).toBe('react-cra')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should handle forced addons', async () => {
|
|
105
|
+
__testRegisterFramework({
|
|
106
|
+
id: 'react-cra',
|
|
107
|
+
name: 'react',
|
|
108
|
+
getAddOns: () => [
|
|
109
|
+
{
|
|
110
|
+
id: 'foo',
|
|
111
|
+
name: 'foobar',
|
|
112
|
+
modes: ['file-router'],
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
})
|
|
116
|
+
const options = await normalizeOptions(
|
|
117
|
+
{
|
|
118
|
+
projectName: 'test',
|
|
119
|
+
framework: 'react-cra',
|
|
120
|
+
},
|
|
121
|
+
'file-router',
|
|
122
|
+
['foo'],
|
|
123
|
+
)
|
|
124
|
+
expect(options?.chosenAddOns.map((a) => a.id).includes('foo')).toBe(true)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('should handle additional addons from the CLI', async () => {
|
|
128
|
+
__testRegisterFramework({
|
|
129
|
+
id: 'react-cra',
|
|
130
|
+
name: 'react',
|
|
131
|
+
getAddOns: () => [
|
|
132
|
+
{
|
|
133
|
+
id: 'foo',
|
|
134
|
+
name: 'foobar',
|
|
135
|
+
modes: ['file-router'],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: 'baz',
|
|
139
|
+
name: 'baz',
|
|
140
|
+
modes: ['file-router'],
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
})
|
|
144
|
+
const options = await normalizeOptions(
|
|
145
|
+
{
|
|
146
|
+
projectName: 'test',
|
|
147
|
+
addOns: ['baz'],
|
|
148
|
+
framework: 'react-cra',
|
|
149
|
+
template: 'file-router',
|
|
150
|
+
},
|
|
151
|
+
'file-router',
|
|
152
|
+
['foo'],
|
|
153
|
+
)
|
|
154
|
+
expect(options?.chosenAddOns.map((a) => a.id).includes('foo')).toBe(true)
|
|
155
|
+
expect(options?.chosenAddOns.map((a) => a.id).includes('baz')).toBe(true)
|
|
156
|
+
expect(options?.tailwind).toBe(true)
|
|
157
|
+
expect(options?.typescript).toBe(true)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('should handle toolchain as an addon', async () => {
|
|
161
|
+
__testRegisterFramework({
|
|
162
|
+
id: 'react-cra',
|
|
163
|
+
name: 'react',
|
|
164
|
+
getAddOns: () => [
|
|
165
|
+
{
|
|
166
|
+
id: 'biome',
|
|
167
|
+
name: 'Biome',
|
|
168
|
+
modes: ['file-router', 'code-router'],
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
})
|
|
172
|
+
const options = await normalizeOptions({
|
|
173
|
+
projectName: 'test',
|
|
174
|
+
toolchain: 'biome',
|
|
175
|
+
})
|
|
176
|
+
expect(options?.chosenAddOns.map((a) => a.id).includes('biome')).toBe(true)
|
|
177
|
+
expect(options?.tailwind).toBe(true)
|
|
178
|
+
expect(options?.typescript).toBe(true)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('should handle the funky Windows edge case with CLI parsing', async () => {
|
|
182
|
+
__testRegisterFramework({
|
|
183
|
+
id: 'react-cra',
|
|
184
|
+
name: 'react',
|
|
185
|
+
getAddOns: () => [
|
|
186
|
+
{
|
|
187
|
+
id: 'foo',
|
|
188
|
+
name: 'foobar',
|
|
189
|
+
modes: ['file-router', 'code-router'],
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
id: 'baz',
|
|
193
|
+
name: 'baz',
|
|
194
|
+
modes: ['file-router', 'code-router'],
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
})
|
|
198
|
+
const options = await normalizeOptions({
|
|
199
|
+
projectName: 'test',
|
|
200
|
+
addOns: ['baz foo'],
|
|
201
|
+
})
|
|
202
|
+
expect(options?.chosenAddOns.map((a) => a.id).includes('foo')).toBe(true)
|
|
203
|
+
expect(options?.chosenAddOns.map((a) => a.id).includes('baz')).toBe(true)
|
|
204
|
+
})
|
|
205
|
+
})
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { beforeEach, describe, it, expect, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { promptForOptions } from '../src/options'
|
|
4
|
+
import {
|
|
5
|
+
__testClearFrameworks,
|
|
6
|
+
__testRegisterFramework,
|
|
7
|
+
} from '@tanstack/cta-engine/dist/frameworks'
|
|
8
|
+
|
|
9
|
+
import * as prompts from '../src/ui-prompts'
|
|
10
|
+
|
|
11
|
+
import type { Framework } from '@tanstack/cta-engine'
|
|
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
|
+
} as unknown as Framework)
|
|
40
|
+
|
|
41
|
+
__testRegisterFramework({
|
|
42
|
+
id: 'solid',
|
|
43
|
+
name: 'solid',
|
|
44
|
+
getAddOns: () => [],
|
|
45
|
+
} as unknown as Framework)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const baseCliOptions: CliOptions = {
|
|
49
|
+
framework: 'react-cra',
|
|
50
|
+
addOns: [],
|
|
51
|
+
toolchain: undefined,
|
|
52
|
+
projectName: undefined,
|
|
53
|
+
git: undefined,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function setBasicSpies() {
|
|
57
|
+
vi.spyOn(prompts, 'getProjectName').mockImplementation(async () => 'hello')
|
|
58
|
+
vi.spyOn(prompts, 'selectRouterType').mockImplementation(
|
|
59
|
+
async () => 'file-router',
|
|
60
|
+
)
|
|
61
|
+
vi.spyOn(prompts, 'selectTypescript').mockImplementation(async () => true)
|
|
62
|
+
vi.spyOn(prompts, 'selectTailwind').mockImplementation(async () => true)
|
|
63
|
+
vi.spyOn(prompts, 'selectPackageManager').mockImplementation(
|
|
64
|
+
async () => 'npm',
|
|
65
|
+
)
|
|
66
|
+
vi.spyOn(prompts, 'selectToolchain').mockImplementation(async () => undefined)
|
|
67
|
+
vi.spyOn(prompts, 'selectAddOns').mockImplementation(async () => [])
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe('promptForOptions', () => {
|
|
71
|
+
//// Project name
|
|
72
|
+
|
|
73
|
+
it('prompt for a project name', async () => {
|
|
74
|
+
setBasicSpies()
|
|
75
|
+
|
|
76
|
+
const options = await promptForOptions(baseCliOptions, {})
|
|
77
|
+
|
|
78
|
+
expect(options?.projectName).toBe('hello')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('accept incoming project name', async () => {
|
|
82
|
+
setBasicSpies()
|
|
83
|
+
|
|
84
|
+
const options = await promptForOptions(
|
|
85
|
+
{ ...baseCliOptions, projectName: 'override' },
|
|
86
|
+
{},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
expect(options?.projectName).toBe('override')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
//// Mode (router type)
|
|
93
|
+
|
|
94
|
+
it('forceMode should override template', async () => {
|
|
95
|
+
setBasicSpies()
|
|
96
|
+
|
|
97
|
+
const options = await promptForOptions(
|
|
98
|
+
{ ...baseCliOptions, template: 'javascript' },
|
|
99
|
+
{ forcedMode: 'file-router' },
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
expect(options?.mode).toBe('file-router')
|
|
103
|
+
expect(options?.typescript).toBe(true)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('takes template from cli options - code-router', async () => {
|
|
107
|
+
setBasicSpies()
|
|
108
|
+
|
|
109
|
+
vi.spyOn(prompts, 'selectRouterType').mockImplementation(
|
|
110
|
+
async () => 'code-router',
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
const options = await promptForOptions(
|
|
114
|
+
{ ...baseCliOptions, template: 'javascript' },
|
|
115
|
+
{},
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
expect(options?.mode).toBe('code-router')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('takes template from cli options - file-router', async () => {
|
|
122
|
+
setBasicSpies()
|
|
123
|
+
|
|
124
|
+
vi.spyOn(prompts, 'selectRouterType').mockImplementation(
|
|
125
|
+
async () => 'code-router',
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
const options = await promptForOptions(
|
|
129
|
+
{ ...baseCliOptions, template: 'file-router' },
|
|
130
|
+
{},
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
expect(options?.mode).toBe('file-router')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('prompt for router type when unspecified', async () => {
|
|
137
|
+
setBasicSpies()
|
|
138
|
+
|
|
139
|
+
vi.spyOn(prompts, 'selectRouterType').mockImplementation(
|
|
140
|
+
async () => 'code-router',
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
const options = await promptForOptions(
|
|
144
|
+
{ ...baseCliOptions, tailwind: false, framework: undefined },
|
|
145
|
+
{},
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
expect(options?.mode).toBe('code-router')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
//// Tailwind
|
|
152
|
+
|
|
153
|
+
it('prompt for tailwind when unspecified in react-cra', async () => {
|
|
154
|
+
setBasicSpies()
|
|
155
|
+
vi.spyOn(prompts, 'selectTailwind').mockImplementation(async () => false)
|
|
156
|
+
const options = await promptForOptions(
|
|
157
|
+
{ ...baseCliOptions, tailwind: undefined },
|
|
158
|
+
{},
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
expect(options?.tailwind).toBe(false)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('prompt for tailwind when unspecified in react-cra - true', async () => {
|
|
165
|
+
setBasicSpies()
|
|
166
|
+
vi.spyOn(prompts, 'selectTailwind').mockImplementation(async () => true)
|
|
167
|
+
const options = await promptForOptions(
|
|
168
|
+
{ ...baseCliOptions, tailwind: undefined },
|
|
169
|
+
{},
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
expect(options?.tailwind).toBe(true)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('set tailwind when solid', async () => {
|
|
176
|
+
setBasicSpies()
|
|
177
|
+
const options = await promptForOptions(
|
|
178
|
+
{ ...baseCliOptions, tailwind: undefined, framework: 'solid' },
|
|
179
|
+
{},
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
expect(options?.tailwind).toBe(true)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
//// Package manager
|
|
186
|
+
|
|
187
|
+
it('uses the package manager from the cli options', async () => {
|
|
188
|
+
setBasicSpies()
|
|
189
|
+
|
|
190
|
+
const options = await promptForOptions(
|
|
191
|
+
{ ...baseCliOptions, packageManager: 'bun' },
|
|
192
|
+
{},
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
expect(options?.packageManager).toBe('bun')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('uses the package manager from the cli options', async () => {
|
|
199
|
+
setBasicSpies()
|
|
200
|
+
|
|
201
|
+
process.env.npm_config_userconfig = 'blarg'
|
|
202
|
+
|
|
203
|
+
const options = await promptForOptions(
|
|
204
|
+
{ ...baseCliOptions, packageManager: undefined },
|
|
205
|
+
{},
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
expect(options?.packageManager).toBe('pnpm')
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
//// Add-ons
|
|
212
|
+
it('should be clean when no add-ons are selected', async () => {
|
|
213
|
+
setBasicSpies()
|
|
214
|
+
|
|
215
|
+
const options = await promptForOptions({ ...baseCliOptions }, {})
|
|
216
|
+
|
|
217
|
+
expect(options?.chosenAddOns).toEqual([])
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('should select biome when toolchain is specified', async () => {
|
|
221
|
+
setBasicSpies()
|
|
222
|
+
|
|
223
|
+
vi.spyOn(prompts, 'selectToolchain').mockImplementation(async () => 'biome')
|
|
224
|
+
|
|
225
|
+
const options = await promptForOptions(
|
|
226
|
+
{ ...baseCliOptions, toolchain: 'biome' },
|
|
227
|
+
{},
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
expect(options?.chosenAddOns.map((a) => a.id).sort()).toEqual(['biome'])
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('should handle forced add-ons', async () => {
|
|
234
|
+
setBasicSpies()
|
|
235
|
+
|
|
236
|
+
vi.spyOn(prompts, 'selectToolchain').mockImplementation(
|
|
237
|
+
async () => undefined,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
const options = await promptForOptions(
|
|
241
|
+
{ ...baseCliOptions },
|
|
242
|
+
{ forcedAddOns: ['react-query'] },
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
expect(options?.chosenAddOns.map((a) => a.id).sort()).toEqual([
|
|
246
|
+
'react-query',
|
|
247
|
+
])
|
|
248
|
+
expect(options?.tailwind).toBe(true)
|
|
249
|
+
expect(options?.typescript).toBe(true)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('should handle add-ons from the CLI', async () => {
|
|
253
|
+
setBasicSpies()
|
|
254
|
+
|
|
255
|
+
const options = await promptForOptions(
|
|
256
|
+
{ ...baseCliOptions, addOns: ['biome', 'react-query'] },
|
|
257
|
+
{},
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
expect(options?.chosenAddOns.map((a) => a.id).sort()).toEqual([
|
|
261
|
+
'biome',
|
|
262
|
+
'react-query',
|
|
263
|
+
])
|
|
264
|
+
expect(options?.tailwind).toBe(true)
|
|
265
|
+
expect(options?.typescript).toBe(true)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('should handle user-selected add-ons', async () => {
|
|
269
|
+
setBasicSpies()
|
|
270
|
+
|
|
271
|
+
vi.spyOn(prompts, 'selectAddOns').mockImplementation(async () =>
|
|
272
|
+
Promise.resolve(['biome', 'react-query']),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
const options = await promptForOptions(
|
|
276
|
+
{ ...baseCliOptions, addOns: undefined },
|
|
277
|
+
{},
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
expect(options?.chosenAddOns.map((a) => a.id).sort()).toEqual([
|
|
281
|
+
'biome',
|
|
282
|
+
'react-query',
|
|
283
|
+
])
|
|
284
|
+
expect(options?.tailwind).toBe(true)
|
|
285
|
+
expect(options?.typescript).toBe(true)
|
|
286
|
+
})
|
|
287
|
+
})
|