@tanstack/cta-cli 0.10.0-alpha.16
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/cli.js +149 -0
- package/dist/index.js +1 -0
- package/dist/options.js +358 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/options.d.ts +7 -0
- package/dist/types/types.d.ts +16 -0
- package/dist/types/ui-environment.d.ts +2 -0
- package/dist/types.js +1 -0
- package/dist/ui-environment.js +49 -0
- package/package.json +42 -0
- package/src/cli.ts +260 -0
- package/src/index.ts +1 -0
- package/src/options.ts +452 -0
- package/src/types.ts +17 -0
- package/src/ui-environment.ts +64 -0
- package/tsconfig.json +17 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { Command, InvalidArgumentError } from 'commander'
|
|
2
|
+
import { intro, log } from '@clack/prompts'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
SUPPORTED_PACKAGE_MANAGERS,
|
|
7
|
+
addToApp,
|
|
8
|
+
createApp,
|
|
9
|
+
getAllAddOns,
|
|
10
|
+
getFrameworkById,
|
|
11
|
+
getFrameworkByName,
|
|
12
|
+
getFrameworks,
|
|
13
|
+
} from '@tanstack/cta-engine'
|
|
14
|
+
import { initAddOn } from '@tanstack/cta-custom-add-on'
|
|
15
|
+
|
|
16
|
+
import { runMCPServer } from '@tanstack/cta-mcp'
|
|
17
|
+
|
|
18
|
+
import { launchUI } from '@tanstack/cta-ui'
|
|
19
|
+
|
|
20
|
+
import { normalizeOptions, promptForOptions } from './options.js'
|
|
21
|
+
|
|
22
|
+
import { createUIEnvironment } from './ui-environment.js'
|
|
23
|
+
|
|
24
|
+
import type {
|
|
25
|
+
Mode,
|
|
26
|
+
PackageManager,
|
|
27
|
+
TemplateOptions,
|
|
28
|
+
} from '@tanstack/cta-engine'
|
|
29
|
+
|
|
30
|
+
import type { CliOptions } from './types.js'
|
|
31
|
+
|
|
32
|
+
async function listAddOns(
|
|
33
|
+
options: CliOptions,
|
|
34
|
+
{
|
|
35
|
+
forcedMode,
|
|
36
|
+
forcedAddOns = [],
|
|
37
|
+
}: {
|
|
38
|
+
forcedMode?: TemplateOptions
|
|
39
|
+
forcedAddOns?: Array<string>
|
|
40
|
+
},
|
|
41
|
+
) {
|
|
42
|
+
const addOns = await getAllAddOns(
|
|
43
|
+
getFrameworkById(options.framework || 'react-cra')!,
|
|
44
|
+
forcedMode || options.template || 'typescript',
|
|
45
|
+
)
|
|
46
|
+
for (const addOn of addOns.filter((a) => !forcedAddOns.includes(a.id))) {
|
|
47
|
+
console.log(`${chalk.bold(addOn.id)}: ${addOn.description}`)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function cli({
|
|
52
|
+
name,
|
|
53
|
+
appName,
|
|
54
|
+
forcedMode,
|
|
55
|
+
forcedAddOns,
|
|
56
|
+
}: {
|
|
57
|
+
name: string
|
|
58
|
+
appName: string
|
|
59
|
+
forcedMode?: Mode
|
|
60
|
+
forcedAddOns?: Array<string>
|
|
61
|
+
}) {
|
|
62
|
+
const environment = createUIEnvironment()
|
|
63
|
+
|
|
64
|
+
const program = new Command()
|
|
65
|
+
|
|
66
|
+
const availableFrameworks = getFrameworks().map((f) => f.name)
|
|
67
|
+
|
|
68
|
+
const toolchains = new Set<string>()
|
|
69
|
+
for (const framework of getFrameworks()) {
|
|
70
|
+
for (const addOn of framework.getAddOns()) {
|
|
71
|
+
if (addOn.type === 'toolchain') {
|
|
72
|
+
toolchains.add(addOn.id)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
program.name(name).description(`CLI to create a new ${appName} application`)
|
|
78
|
+
|
|
79
|
+
program
|
|
80
|
+
.command('add')
|
|
81
|
+
.argument('add-on', 'Name of the add-on (or add-ons separated by commas)')
|
|
82
|
+
.action(async (addOn: string) => {
|
|
83
|
+
await addToApp(
|
|
84
|
+
addOn.split(',').map((addon) => addon.trim()),
|
|
85
|
+
{
|
|
86
|
+
silent: false,
|
|
87
|
+
},
|
|
88
|
+
environment,
|
|
89
|
+
)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const addOnCommand = program.command('add-on')
|
|
93
|
+
|
|
94
|
+
addOnCommand
|
|
95
|
+
.command('update')
|
|
96
|
+
.description('Create or update an add-on from the current project')
|
|
97
|
+
.action(async () => {
|
|
98
|
+
await initAddOn('add-on', environment)
|
|
99
|
+
})
|
|
100
|
+
addOnCommand
|
|
101
|
+
.command('ui')
|
|
102
|
+
.description('Show the add-on developer UI')
|
|
103
|
+
.action(async () => {
|
|
104
|
+
launchUI()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
program
|
|
108
|
+
.command('update-starter')
|
|
109
|
+
.description('Create or update a project starter from the current project')
|
|
110
|
+
.action(async () => {
|
|
111
|
+
await initAddOn('starter', environment)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
program.argument('[project-name]', 'name of the project')
|
|
115
|
+
|
|
116
|
+
if (!forcedMode) {
|
|
117
|
+
program.option<'typescript' | 'javascript' | 'file-router'>(
|
|
118
|
+
'--template <type>',
|
|
119
|
+
'project template (typescript, javascript, file-router)',
|
|
120
|
+
(value) => {
|
|
121
|
+
if (
|
|
122
|
+
value !== 'typescript' &&
|
|
123
|
+
value !== 'javascript' &&
|
|
124
|
+
value !== 'file-router'
|
|
125
|
+
) {
|
|
126
|
+
throw new InvalidArgumentError(
|
|
127
|
+
`Invalid template: ${value}. Only the following are allowed: typescript, javascript, file-router`,
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
return value
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
program
|
|
136
|
+
.option<string>(
|
|
137
|
+
'--framework <type>',
|
|
138
|
+
`project framework (${availableFrameworks.join(', ')})`,
|
|
139
|
+
(value) => {
|
|
140
|
+
if (!availableFrameworks.includes(value)) {
|
|
141
|
+
throw new InvalidArgumentError(
|
|
142
|
+
`Invalid framework: ${value}. Only the following are allowed: ${availableFrameworks.join(', ')}`,
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
return value
|
|
146
|
+
},
|
|
147
|
+
'react',
|
|
148
|
+
)
|
|
149
|
+
.option(
|
|
150
|
+
'--starter [url]',
|
|
151
|
+
'initialize this project from a starter URL',
|
|
152
|
+
false,
|
|
153
|
+
)
|
|
154
|
+
.option<PackageManager>(
|
|
155
|
+
`--package-manager <${SUPPORTED_PACKAGE_MANAGERS.join('|')}>`,
|
|
156
|
+
`Explicitly tell the CLI to use this package manager`,
|
|
157
|
+
(value) => {
|
|
158
|
+
if (!SUPPORTED_PACKAGE_MANAGERS.includes(value as PackageManager)) {
|
|
159
|
+
throw new InvalidArgumentError(
|
|
160
|
+
`Invalid package manager: ${value}. The following are allowed: ${SUPPORTED_PACKAGE_MANAGERS.join(
|
|
161
|
+
', ',
|
|
162
|
+
)}`,
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
return value as PackageManager
|
|
166
|
+
},
|
|
167
|
+
)
|
|
168
|
+
.option<string>(
|
|
169
|
+
`--toolchain <${Array.from(toolchains).join('|')}>`,
|
|
170
|
+
`Explicitly tell the CLI to use this toolchain`,
|
|
171
|
+
(value) => {
|
|
172
|
+
if (!toolchains.has(value)) {
|
|
173
|
+
throw new InvalidArgumentError(
|
|
174
|
+
`Invalid toolchain: ${value}. The following are allowed: ${Array.from(
|
|
175
|
+
toolchains,
|
|
176
|
+
).join(', ')}`,
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
return value
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
.option('--tailwind', 'add Tailwind CSS', false)
|
|
183
|
+
.option<Array<string> | boolean>(
|
|
184
|
+
'--add-ons [...add-ons]',
|
|
185
|
+
'pick from a list of available add-ons (comma separated list)',
|
|
186
|
+
(value: string) => {
|
|
187
|
+
let addOns: Array<string> | boolean = !!value
|
|
188
|
+
if (typeof value === 'string') {
|
|
189
|
+
addOns = value.split(',').map((addon) => addon.trim())
|
|
190
|
+
}
|
|
191
|
+
return addOns
|
|
192
|
+
},
|
|
193
|
+
)
|
|
194
|
+
.option('--list-add-ons', 'list all available add-ons', false)
|
|
195
|
+
.option('--no-git', 'do not create a git repository')
|
|
196
|
+
.option(
|
|
197
|
+
'--target-dir <path>',
|
|
198
|
+
'the target directory for the application root',
|
|
199
|
+
)
|
|
200
|
+
.option('--mcp', 'run the MCP server', false)
|
|
201
|
+
.option('--mcp-sse', 'run the MCP server in SSE mode', false)
|
|
202
|
+
|
|
203
|
+
program.action(async (projectName: string, options: CliOptions) => {
|
|
204
|
+
if (options.listAddOns) {
|
|
205
|
+
await listAddOns(options, {
|
|
206
|
+
forcedMode: forcedMode as TemplateOptions,
|
|
207
|
+
forcedAddOns,
|
|
208
|
+
})
|
|
209
|
+
} else if (options.mcp || options.mcpSse) {
|
|
210
|
+
await runMCPServer(!!options.mcpSse, {
|
|
211
|
+
forcedMode: forcedMode as TemplateOptions,
|
|
212
|
+
forcedAddOns,
|
|
213
|
+
appName,
|
|
214
|
+
})
|
|
215
|
+
} else {
|
|
216
|
+
try {
|
|
217
|
+
const cliOptions = {
|
|
218
|
+
projectName,
|
|
219
|
+
...options,
|
|
220
|
+
} as CliOptions
|
|
221
|
+
|
|
222
|
+
cliOptions.framework = getFrameworkByName(
|
|
223
|
+
options.framework || 'react',
|
|
224
|
+
)!.id
|
|
225
|
+
|
|
226
|
+
if (forcedMode) {
|
|
227
|
+
cliOptions.template = forcedMode as TemplateOptions
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let finalOptions = await normalizeOptions(
|
|
231
|
+
cliOptions,
|
|
232
|
+
forcedMode,
|
|
233
|
+
forcedAddOns,
|
|
234
|
+
)
|
|
235
|
+
if (finalOptions) {
|
|
236
|
+
intro(`Creating a new ${appName} app in ${projectName}...`)
|
|
237
|
+
} else {
|
|
238
|
+
intro(`Let's configure your ${appName} application`)
|
|
239
|
+
finalOptions = await promptForOptions(cliOptions, {
|
|
240
|
+
forcedMode: forcedMode as TemplateOptions,
|
|
241
|
+
forcedAddOns,
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
await createApp(finalOptions, {
|
|
245
|
+
environment: createUIEnvironment(),
|
|
246
|
+
cwd: options.targetDir || undefined,
|
|
247
|
+
name,
|
|
248
|
+
appName,
|
|
249
|
+
})
|
|
250
|
+
} catch (error) {
|
|
251
|
+
log.error(
|
|
252
|
+
error instanceof Error ? error.message : 'An unknown error occurred',
|
|
253
|
+
)
|
|
254
|
+
process.exit(1)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
program.parse()
|
|
260
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { cli } from './cli.js'
|
package/src/options.ts
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
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
|
+
finalizeAddOns,
|
|
16
|
+
getAllAddOns,
|
|
17
|
+
getFrameworkById,
|
|
18
|
+
getPackageManager,
|
|
19
|
+
loadRemoteAddOn,
|
|
20
|
+
} from '@tanstack/cta-engine'
|
|
21
|
+
|
|
22
|
+
import type {
|
|
23
|
+
AddOn,
|
|
24
|
+
Mode,
|
|
25
|
+
Options,
|
|
26
|
+
Starter,
|
|
27
|
+
TemplateOptions,
|
|
28
|
+
Variable,
|
|
29
|
+
} from '@tanstack/cta-engine'
|
|
30
|
+
|
|
31
|
+
import type { CliOptions } from './types.js'
|
|
32
|
+
|
|
33
|
+
// If all CLI options are provided, use them directly
|
|
34
|
+
export async function normalizeOptions(
|
|
35
|
+
cliOptions: CliOptions,
|
|
36
|
+
forcedMode?: Mode,
|
|
37
|
+
forcedAddOns?: Array<string>,
|
|
38
|
+
): Promise<Options | undefined> {
|
|
39
|
+
// in some cases, if you use windows/powershell, the argument for addons
|
|
40
|
+
// if sepparated by comma is not really passed as an array, but as a string
|
|
41
|
+
// with spaces, We need to normalize this edge case.
|
|
42
|
+
if (Array.isArray(cliOptions.addOns) && cliOptions.addOns.length === 1) {
|
|
43
|
+
const parseSeparatedArgs = cliOptions.addOns[0].split(' ')
|
|
44
|
+
if (parseSeparatedArgs.length > 1) {
|
|
45
|
+
cliOptions.addOns = parseSeparatedArgs
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (cliOptions.projectName) {
|
|
50
|
+
let typescript =
|
|
51
|
+
cliOptions.template === 'typescript' ||
|
|
52
|
+
cliOptions.template === 'file-router' ||
|
|
53
|
+
cliOptions.framework === 'solid'
|
|
54
|
+
|
|
55
|
+
let tailwind = !!cliOptions.tailwind
|
|
56
|
+
if (cliOptions.framework === 'solid') {
|
|
57
|
+
tailwind = true
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let mode: typeof FILE_ROUTER | typeof CODE_ROUTER =
|
|
61
|
+
cliOptions.template === 'file-router' ? FILE_ROUTER : CODE_ROUTER
|
|
62
|
+
|
|
63
|
+
const starter = cliOptions.starter
|
|
64
|
+
? ((await loadRemoteAddOn(cliOptions.starter)) as Starter)
|
|
65
|
+
: undefined
|
|
66
|
+
|
|
67
|
+
if (starter) {
|
|
68
|
+
tailwind = starter.tailwind
|
|
69
|
+
typescript = starter.typescript
|
|
70
|
+
cliOptions.framework = starter.framework
|
|
71
|
+
mode = starter.mode
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let addOns = false
|
|
75
|
+
let chosenAddOns: Array<AddOn> = []
|
|
76
|
+
if (
|
|
77
|
+
Array.isArray(cliOptions.addOns) ||
|
|
78
|
+
starter?.dependsOn ||
|
|
79
|
+
forcedAddOns ||
|
|
80
|
+
cliOptions.toolchain
|
|
81
|
+
) {
|
|
82
|
+
addOns = true
|
|
83
|
+
let finalAddOns = Array.from(
|
|
84
|
+
new Set([...(starter?.dependsOn || []), ...(forcedAddOns || [])]),
|
|
85
|
+
)
|
|
86
|
+
if (cliOptions.addOns && Array.isArray(cliOptions.addOns)) {
|
|
87
|
+
finalAddOns = Array.from(
|
|
88
|
+
new Set([
|
|
89
|
+
...(forcedAddOns || []),
|
|
90
|
+
...finalAddOns,
|
|
91
|
+
...cliOptions.addOns,
|
|
92
|
+
]),
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
const framework = getFrameworkById(cliOptions.framework || 'react-cra')!
|
|
96
|
+
|
|
97
|
+
if (cliOptions.toolchain) {
|
|
98
|
+
finalAddOns.push(cliOptions.toolchain)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
chosenAddOns = await finalizeAddOns(
|
|
102
|
+
framework,
|
|
103
|
+
forcedMode || cliOptions.template === 'file-router'
|
|
104
|
+
? FILE_ROUTER
|
|
105
|
+
: CODE_ROUTER,
|
|
106
|
+
finalAddOns,
|
|
107
|
+
)
|
|
108
|
+
tailwind = true
|
|
109
|
+
typescript = true
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
// TODO: This is a bit to fix the default framework
|
|
114
|
+
framework: getFrameworkById(cliOptions.framework || 'react-cra')!,
|
|
115
|
+
projectName: cliOptions.projectName,
|
|
116
|
+
typescript,
|
|
117
|
+
tailwind,
|
|
118
|
+
packageManager:
|
|
119
|
+
cliOptions.packageManager ||
|
|
120
|
+
getPackageManager() ||
|
|
121
|
+
DEFAULT_PACKAGE_MANAGER,
|
|
122
|
+
mode,
|
|
123
|
+
git: !!cliOptions.git,
|
|
124
|
+
addOns,
|
|
125
|
+
chosenAddOns,
|
|
126
|
+
variableValues: {},
|
|
127
|
+
starter,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function collectVariables(
|
|
133
|
+
variables: Array<Variable>,
|
|
134
|
+
): Promise<Record<string, string | number | boolean>> {
|
|
135
|
+
const responses: Record<string, string | number | boolean> = {}
|
|
136
|
+
for (const variable of variables) {
|
|
137
|
+
if (variable.type === 'string') {
|
|
138
|
+
const response = await text({
|
|
139
|
+
message: variable.description,
|
|
140
|
+
initialValue: variable.default,
|
|
141
|
+
})
|
|
142
|
+
if (isCancel(response)) {
|
|
143
|
+
cancel('Operation cancelled.')
|
|
144
|
+
process.exit(0)
|
|
145
|
+
}
|
|
146
|
+
responses[variable.name] = response
|
|
147
|
+
} else if (variable.type === 'number') {
|
|
148
|
+
const response = await text({
|
|
149
|
+
message: variable.description,
|
|
150
|
+
initialValue: variable.default.toString(),
|
|
151
|
+
})
|
|
152
|
+
if (isCancel(response)) {
|
|
153
|
+
cancel('Operation cancelled.')
|
|
154
|
+
process.exit(0)
|
|
155
|
+
}
|
|
156
|
+
responses[variable.name] = Number(response)
|
|
157
|
+
} else {
|
|
158
|
+
const response = await confirm({
|
|
159
|
+
message: variable.description,
|
|
160
|
+
initialValue: variable.default === true,
|
|
161
|
+
})
|
|
162
|
+
if (isCancel(response)) {
|
|
163
|
+
cancel('Operation cancelled.')
|
|
164
|
+
process.exit(0)
|
|
165
|
+
}
|
|
166
|
+
responses[variable.name] = response
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return responses
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function promptForOptions(
|
|
173
|
+
cliOptions: CliOptions,
|
|
174
|
+
{
|
|
175
|
+
forcedAddOns = [],
|
|
176
|
+
forcedMode,
|
|
177
|
+
}: {
|
|
178
|
+
forcedAddOns?: Array<string>
|
|
179
|
+
forcedMode?: TemplateOptions
|
|
180
|
+
},
|
|
181
|
+
): Promise<Required<Options>> {
|
|
182
|
+
const options = {} as Required<Options>
|
|
183
|
+
|
|
184
|
+
const framework = getFrameworkById(cliOptions.framework || 'react-cra')!
|
|
185
|
+
options.framework = framework
|
|
186
|
+
// TODO: This is a bit of a hack to ensure that the framework is solid
|
|
187
|
+
if (options.framework.id === 'solid') {
|
|
188
|
+
options.typescript = true
|
|
189
|
+
options.tailwind = true
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (cliOptions.addOns) {
|
|
193
|
+
options.typescript = true
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (!cliOptions.projectName) {
|
|
197
|
+
const value = await text({
|
|
198
|
+
message: 'What would you like to name your project?',
|
|
199
|
+
defaultValue: 'my-app',
|
|
200
|
+
validate(value) {
|
|
201
|
+
if (!value) {
|
|
202
|
+
return 'Please enter a name'
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
})
|
|
206
|
+
if (isCancel(value)) {
|
|
207
|
+
cancel('Operation cancelled.')
|
|
208
|
+
process.exit(0)
|
|
209
|
+
}
|
|
210
|
+
options.projectName = value
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Router type selection
|
|
214
|
+
if (!cliOptions.template && !forcedMode) {
|
|
215
|
+
const routerType = await select({
|
|
216
|
+
message: 'Select the router type:',
|
|
217
|
+
options: [
|
|
218
|
+
{
|
|
219
|
+
value: FILE_ROUTER,
|
|
220
|
+
label: 'File Router - File-based routing structure',
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
value: CODE_ROUTER,
|
|
224
|
+
label: 'Code Router - Traditional code-based routing',
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
initialValue: FILE_ROUTER,
|
|
228
|
+
})
|
|
229
|
+
if (isCancel(routerType)) {
|
|
230
|
+
cancel('Operation cancelled.')
|
|
231
|
+
process.exit(0)
|
|
232
|
+
}
|
|
233
|
+
options.mode = routerType as typeof CODE_ROUTER | typeof FILE_ROUTER
|
|
234
|
+
} else if (forcedMode) {
|
|
235
|
+
options.mode = forcedMode === 'file-router' ? FILE_ROUTER : CODE_ROUTER
|
|
236
|
+
options.typescript = options.mode === FILE_ROUTER
|
|
237
|
+
} else {
|
|
238
|
+
options.mode =
|
|
239
|
+
cliOptions.template === 'file-router' ? FILE_ROUTER : CODE_ROUTER
|
|
240
|
+
if (options.mode === FILE_ROUTER) {
|
|
241
|
+
options.typescript = true
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// TypeScript selection (if using Code Router)
|
|
246
|
+
if (!options.typescript) {
|
|
247
|
+
if (options.mode === CODE_ROUTER) {
|
|
248
|
+
const typescriptEnable = await confirm({
|
|
249
|
+
message: 'Would you like to use TypeScript?',
|
|
250
|
+
initialValue: true,
|
|
251
|
+
})
|
|
252
|
+
if (isCancel(typescriptEnable)) {
|
|
253
|
+
cancel('Operation cancelled.')
|
|
254
|
+
process.exit(0)
|
|
255
|
+
}
|
|
256
|
+
options.typescript = typescriptEnable
|
|
257
|
+
} else {
|
|
258
|
+
options.typescript = true
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Tailwind selection
|
|
263
|
+
if (!cliOptions.tailwind && options.framework.id === 'react-cra') {
|
|
264
|
+
const tailwind = await confirm({
|
|
265
|
+
message: 'Would you like to use Tailwind CSS?',
|
|
266
|
+
initialValue: true,
|
|
267
|
+
})
|
|
268
|
+
if (isCancel(tailwind)) {
|
|
269
|
+
cancel('Operation cancelled.')
|
|
270
|
+
process.exit(0)
|
|
271
|
+
}
|
|
272
|
+
options.tailwind = tailwind
|
|
273
|
+
} else {
|
|
274
|
+
// TODO: This is a bit of a hack to ensure that the framework is solid
|
|
275
|
+
options.tailwind = options.framework.id === 'solid' || !!cliOptions.tailwind
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Package manager selection
|
|
279
|
+
if (cliOptions.packageManager === undefined) {
|
|
280
|
+
const detectedPackageManager = getPackageManager()
|
|
281
|
+
if (!detectedPackageManager) {
|
|
282
|
+
const pm = await select({
|
|
283
|
+
message: 'Select package manager:',
|
|
284
|
+
options: SUPPORTED_PACKAGE_MANAGERS.map((pm) => ({
|
|
285
|
+
value: pm,
|
|
286
|
+
label: pm,
|
|
287
|
+
})),
|
|
288
|
+
initialValue: DEFAULT_PACKAGE_MANAGER,
|
|
289
|
+
})
|
|
290
|
+
if (isCancel(pm)) {
|
|
291
|
+
cancel('Operation cancelled.')
|
|
292
|
+
process.exit(0)
|
|
293
|
+
}
|
|
294
|
+
options.packageManager = pm
|
|
295
|
+
} else {
|
|
296
|
+
options.packageManager = detectedPackageManager
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
options.packageManager = cliOptions.packageManager
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Toolchain selection
|
|
303
|
+
let toolchain: AddOn | undefined = undefined
|
|
304
|
+
if (cliOptions.toolchain === undefined) {
|
|
305
|
+
const toolchains = new Set<AddOn>()
|
|
306
|
+
for (const addOn of framework.getAddOns()) {
|
|
307
|
+
if (addOn.type === 'toolchain') {
|
|
308
|
+
toolchains.add(addOn)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const tc = await select<AddOn | undefined>({
|
|
313
|
+
message: 'Select toolchain',
|
|
314
|
+
options: [
|
|
315
|
+
{
|
|
316
|
+
value: undefined,
|
|
317
|
+
label: 'None',
|
|
318
|
+
},
|
|
319
|
+
...Array.from(toolchains).map((tc) => ({
|
|
320
|
+
value: tc,
|
|
321
|
+
label: tc.name,
|
|
322
|
+
})),
|
|
323
|
+
],
|
|
324
|
+
initialValue: undefined,
|
|
325
|
+
})
|
|
326
|
+
if (isCancel(tc)) {
|
|
327
|
+
cancel('Operation cancelled.')
|
|
328
|
+
process.exit(0)
|
|
329
|
+
}
|
|
330
|
+
toolchain = tc
|
|
331
|
+
} else {
|
|
332
|
+
for (const addOn of framework.getAddOns()) {
|
|
333
|
+
if (addOn.type === 'toolchain' && addOn.id === cliOptions.toolchain) {
|
|
334
|
+
toolchain = addOn
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
options.chosenAddOns = toolchain ? [toolchain] : []
|
|
340
|
+
if (Array.isArray(cliOptions.addOns)) {
|
|
341
|
+
options.chosenAddOns = await finalizeAddOns(
|
|
342
|
+
options.framework,
|
|
343
|
+
options.mode,
|
|
344
|
+
Array.from(
|
|
345
|
+
new Set([...cliOptions.addOns, ...forcedAddOns, toolchain?.id]),
|
|
346
|
+
).filter(Boolean) as Array<string>,
|
|
347
|
+
)
|
|
348
|
+
options.tailwind = true
|
|
349
|
+
} else if (cliOptions.addOns) {
|
|
350
|
+
// Select any add-ons
|
|
351
|
+
const allAddOns = await getAllAddOns(options.framework, options.mode)
|
|
352
|
+
const addOns = allAddOns.filter((addOn) => addOn.type === 'add-on')
|
|
353
|
+
let selectedAddOns: Array<string> = []
|
|
354
|
+
if (options.typescript && addOns.length > 0) {
|
|
355
|
+
const value = await multiselect({
|
|
356
|
+
message: 'What add-ons would you like for your project:',
|
|
357
|
+
options: addOns
|
|
358
|
+
.filter((addOn) => !forcedAddOns.includes(addOn.id))
|
|
359
|
+
.map((addOn) => ({
|
|
360
|
+
value: addOn.id,
|
|
361
|
+
label: addOn.name,
|
|
362
|
+
hint: addOn.description,
|
|
363
|
+
})),
|
|
364
|
+
required: false,
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
if (isCancel(value)) {
|
|
368
|
+
cancel('Operation cancelled.')
|
|
369
|
+
process.exit(0)
|
|
370
|
+
}
|
|
371
|
+
selectedAddOns = value
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Select any examples
|
|
375
|
+
let selectedExamples: Array<string> = []
|
|
376
|
+
const examples = allAddOns.filter((addOn) => addOn.type === 'example')
|
|
377
|
+
if (options.typescript && examples.length > 0) {
|
|
378
|
+
const value = await multiselect({
|
|
379
|
+
message: 'Would you like any examples?',
|
|
380
|
+
options: examples
|
|
381
|
+
.filter((addOn) => !forcedAddOns.includes(addOn.id))
|
|
382
|
+
.map((addOn) => ({
|
|
383
|
+
value: addOn.id,
|
|
384
|
+
label: addOn.name,
|
|
385
|
+
hint: addOn.description,
|
|
386
|
+
})),
|
|
387
|
+
required: false,
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
if (isCancel(value)) {
|
|
391
|
+
cancel('Operation cancelled.')
|
|
392
|
+
process.exit(0)
|
|
393
|
+
}
|
|
394
|
+
selectedExamples = value
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (
|
|
398
|
+
selectedAddOns.length > 0 ||
|
|
399
|
+
selectedExamples.length > 0 ||
|
|
400
|
+
forcedAddOns.length > 0 ||
|
|
401
|
+
toolchain
|
|
402
|
+
) {
|
|
403
|
+
options.chosenAddOns = await finalizeAddOns(
|
|
404
|
+
options.framework,
|
|
405
|
+
options.mode,
|
|
406
|
+
Array.from(
|
|
407
|
+
new Set([
|
|
408
|
+
...selectedAddOns,
|
|
409
|
+
...selectedExamples,
|
|
410
|
+
...forcedAddOns,
|
|
411
|
+
toolchain?.id,
|
|
412
|
+
]),
|
|
413
|
+
).filter(Boolean) as Array<string>,
|
|
414
|
+
)
|
|
415
|
+
options.tailwind = true
|
|
416
|
+
}
|
|
417
|
+
} else if (forcedAddOns.length > 0) {
|
|
418
|
+
options.chosenAddOns = await finalizeAddOns(
|
|
419
|
+
options.framework,
|
|
420
|
+
options.mode,
|
|
421
|
+
Array.from(new Set([...forcedAddOns, toolchain?.id])).filter(
|
|
422
|
+
Boolean,
|
|
423
|
+
) as Array<string>,
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Collect variables
|
|
428
|
+
const variables: Array<Variable> = []
|
|
429
|
+
for (const addOn of options.chosenAddOns) {
|
|
430
|
+
for (const variable of addOn.variables ?? []) {
|
|
431
|
+
variables.push(variable)
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
options.variableValues = await collectVariables(variables)
|
|
435
|
+
|
|
436
|
+
// Git selection
|
|
437
|
+
if (cliOptions.git === undefined) {
|
|
438
|
+
const git = await confirm({
|
|
439
|
+
message: 'Would you like to initialize a new git repository?',
|
|
440
|
+
initialValue: true,
|
|
441
|
+
})
|
|
442
|
+
if (isCancel(git)) {
|
|
443
|
+
cancel('Operation cancelled.')
|
|
444
|
+
process.exit(0)
|
|
445
|
+
}
|
|
446
|
+
options.git = git
|
|
447
|
+
} else {
|
|
448
|
+
options.git = !!cliOptions.git
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return options
|
|
452
|
+
}
|