@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/src/options.ts CHANGED
@@ -1,174 +1,26 @@
1
- import {
2
- cancel,
3
- confirm,
4
- isCancel,
5
- multiselect,
6
- select,
7
- text,
8
- } from '@clack/prompts'
9
-
10
1
  import {
11
2
  CODE_ROUTER,
12
- DEFAULT_PACKAGE_MANAGER,
13
3
  FILE_ROUTER,
14
- SUPPORTED_PACKAGE_MANAGERS,
15
4
  finalizeAddOns,
16
- getAllAddOns,
17
5
  getFrameworkById,
18
6
  getPackageManager,
19
- loadRemoteAddOn,
20
7
  } from '@tanstack/cta-engine'
21
8
 
22
- import type {
23
- AddOn,
24
- Mode,
25
- Options,
26
- Starter,
27
- TemplateOptions,
28
- Variable,
29
- } from '@tanstack/cta-engine'
9
+ import {
10
+ getProjectName,
11
+ selectAddOns,
12
+ selectGit,
13
+ selectPackageManager,
14
+ selectRouterType,
15
+ selectTailwind,
16
+ selectToolchain,
17
+ selectTypescript,
18
+ } from './ui-prompts.js'
19
+
20
+ import type { Mode, Options } from '@tanstack/cta-engine'
30
21
 
31
22
  import type { CliOptions } from './types.js'
32
23
 
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
24
  export async function promptForOptions(
173
25
  cliOptions: CliOptions,
174
26
  {
@@ -176,279 +28,101 @@ export async function promptForOptions(
176
28
  forcedMode,
177
29
  }: {
178
30
  forcedAddOns?: Array<string>
179
- forcedMode?: TemplateOptions
31
+ forcedMode?: Mode
180
32
  },
181
33
  ): Promise<Required<Options> | undefined> {
182
34
  const options = {} as Required<Options>
183
35
 
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
- }
36
+ options.framework = getFrameworkById(cliOptions.framework || 'react-cra')!
195
37
 
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
- } else {
212
- options.projectName = cliOptions.projectName
213
- }
38
+ options.projectName = cliOptions.projectName || (await getProjectName())
214
39
 
215
40
  // Router type selection
216
- if (!cliOptions.template && !forcedMode) {
217
- const routerType = await select({
218
- message: 'Select the router type:',
219
- options: [
220
- {
221
- value: FILE_ROUTER,
222
- label: 'File Router - File-based routing structure',
223
- },
224
- {
225
- value: CODE_ROUTER,
226
- label: 'Code Router - Traditional code-based routing',
227
- },
228
- ],
229
- initialValue: FILE_ROUTER,
230
- })
231
- if (isCancel(routerType)) {
232
- cancel('Operation cancelled.')
233
- process.exit(0)
234
- }
235
- options.mode = routerType as typeof CODE_ROUTER | typeof FILE_ROUTER
236
- } else if (forcedMode) {
237
- options.mode = forcedMode === 'file-router' ? FILE_ROUTER : CODE_ROUTER
238
- options.typescript = options.mode === FILE_ROUTER
239
- } else {
41
+ if (forcedMode) {
42
+ options.mode = forcedMode
43
+ } else if (cliOptions.template) {
240
44
  options.mode =
241
45
  cliOptions.template === 'file-router' ? FILE_ROUTER : CODE_ROUTER
242
- if (options.mode === FILE_ROUTER) {
243
- options.typescript = true
244
- }
46
+ } else {
47
+ options.mode = await selectRouterType()
245
48
  }
246
49
 
247
50
  // TypeScript selection (if using Code Router)
248
- if (!options.typescript) {
249
- if (options.mode === CODE_ROUTER) {
250
- const typescriptEnable = await confirm({
251
- message: 'Would you like to use TypeScript?',
252
- initialValue: true,
253
- })
254
- if (isCancel(typescriptEnable)) {
255
- cancel('Operation cancelled.')
256
- process.exit(0)
257
- }
258
- options.typescript = typescriptEnable
259
- } else {
260
- options.typescript = true
261
- }
51
+ options.typescript =
52
+ options.mode === FILE_ROUTER || options.framework.id === 'solid'
53
+ if (!options.typescript && options.mode === CODE_ROUTER) {
54
+ options.typescript = await selectTypescript()
262
55
  }
263
56
 
264
57
  // Tailwind selection
265
58
  if (!cliOptions.tailwind && options.framework.id === 'react-cra') {
266
- const tailwind = await confirm({
267
- message: 'Would you like to use Tailwind CSS?',
268
- initialValue: true,
269
- })
270
- if (isCancel(tailwind)) {
271
- cancel('Operation cancelled.')
272
- process.exit(0)
273
- }
274
- options.tailwind = tailwind
59
+ options.tailwind = await selectTailwind()
275
60
  } else {
276
- // TODO: This is a bit of a hack to ensure that the framework is solid
277
- options.tailwind = options.framework.id === 'solid' || !!cliOptions.tailwind
61
+ options.tailwind = true
278
62
  }
279
63
 
280
64
  // Package manager selection
281
- if (cliOptions.packageManager === undefined) {
282
- const detectedPackageManager = getPackageManager()
283
- if (!detectedPackageManager) {
284
- const pm = await select({
285
- message: 'Select package manager:',
286
- options: SUPPORTED_PACKAGE_MANAGERS.map((pm) => ({
287
- value: pm,
288
- label: pm,
289
- })),
290
- initialValue: DEFAULT_PACKAGE_MANAGER,
291
- })
292
- if (isCancel(pm)) {
293
- cancel('Operation cancelled.')
294
- process.exit(0)
295
- }
296
- options.packageManager = pm
297
- } else {
298
- options.packageManager = detectedPackageManager
299
- }
300
- } else {
65
+ if (cliOptions.packageManager) {
301
66
  options.packageManager = cliOptions.packageManager
67
+ } else {
68
+ const detectedPackageManager = await getPackageManager()
69
+ options.packageManager =
70
+ detectedPackageManager || (await selectPackageManager())
302
71
  }
303
72
 
304
73
  // Toolchain selection
305
- let toolchain: AddOn | undefined = undefined
306
- if (cliOptions.toolchain === undefined) {
307
- const toolchains = new Set<AddOn>()
308
- for (const addOn of framework.getAddOns()) {
309
- if (addOn.type === 'toolchain') {
310
- toolchains.add(addOn)
311
- }
312
- }
74
+ const toolchain = await selectToolchain(
75
+ options.framework,
76
+ cliOptions.toolchain,
77
+ )
313
78
 
314
- const tc = await select<AddOn | undefined>({
315
- message: 'Select toolchain',
316
- options: [
317
- {
318
- value: undefined,
319
- label: 'None',
320
- },
321
- ...Array.from(toolchains).map((tc) => ({
322
- value: tc,
323
- label: tc.name,
324
- })),
325
- ],
326
- initialValue: undefined,
327
- })
328
- if (isCancel(tc)) {
329
- cancel('Operation cancelled.')
330
- process.exit(0)
331
- }
332
- toolchain = tc
333
- } else {
334
- for (const addOn of framework.getAddOns()) {
335
- if (addOn.type === 'toolchain' && addOn.id === cliOptions.toolchain) {
336
- toolchain = addOn
337
- }
338
- }
79
+ // Add-ons selection
80
+ const addOns: Set<string> = new Set()
81
+
82
+ if (toolchain) {
83
+ addOns.add(toolchain)
84
+ }
85
+
86
+ for (const addOn of forcedAddOns) {
87
+ addOns.add(addOn)
339
88
  }
340
89
 
341
- options.chosenAddOns = toolchain ? [toolchain] : []
342
90
  if (Array.isArray(cliOptions.addOns)) {
343
- options.chosenAddOns = await finalizeAddOns(
91
+ for (const addOn of cliOptions.addOns) {
92
+ addOns.add(addOn)
93
+ }
94
+ } else {
95
+ for (const addOn of await selectAddOns(
344
96
  options.framework,
345
97
  options.mode,
346
- Array.from(
347
- new Set([...cliOptions.addOns, ...forcedAddOns, toolchain?.id]),
348
- ).filter(Boolean) as Array<string>,
349
- )
350
- options.tailwind = true
351
- } else if (cliOptions.addOns) {
352
- // Select any add-ons
353
- const allAddOns = await getAllAddOns(options.framework, options.mode)
354
- const addOns = allAddOns.filter((addOn) => addOn.type === 'add-on')
355
- let selectedAddOns: Array<string> = []
356
- if (options.typescript && addOns.length > 0) {
357
- const value = await multiselect({
358
- message: 'What add-ons would you like for your project:',
359
- options: addOns
360
- .filter((addOn) => !forcedAddOns.includes(addOn.id))
361
- .map((addOn) => ({
362
- value: addOn.id,
363
- label: addOn.name,
364
- hint: addOn.description,
365
- })),
366
- required: false,
367
- })
368
-
369
- if (isCancel(value)) {
370
- cancel('Operation cancelled.')
371
- process.exit(0)
372
- }
373
- selectedAddOns = value
98
+ 'add-on',
99
+ 'What add-ons would you like for your project?',
100
+ forcedAddOns,
101
+ )) {
102
+ addOns.add(addOn)
374
103
  }
375
104
 
376
- // Select any examples
377
- let selectedExamples: Array<string> = []
378
- const examples = allAddOns.filter((addOn) => addOn.type === 'example')
379
- if (options.typescript && examples.length > 0) {
380
- const value = await multiselect({
381
- message: 'Would you like any examples?',
382
- options: examples
383
- .filter((addOn) => !forcedAddOns.includes(addOn.id))
384
- .map((addOn) => ({
385
- value: addOn.id,
386
- label: addOn.name,
387
- hint: addOn.description,
388
- })),
389
- required: false,
390
- })
391
-
392
- if (isCancel(value)) {
393
- cancel('Operation cancelled.')
394
- process.exit(0)
395
- }
396
- selectedExamples = value
397
- }
398
-
399
- if (
400
- selectedAddOns.length > 0 ||
401
- selectedExamples.length > 0 ||
402
- forcedAddOns.length > 0 ||
403
- toolchain
404
- ) {
405
- options.chosenAddOns = await finalizeAddOns(
406
- options.framework,
407
- options.mode,
408
- Array.from(
409
- new Set([
410
- ...selectedAddOns,
411
- ...selectedExamples,
412
- ...forcedAddOns,
413
- toolchain?.id,
414
- ]),
415
- ).filter(Boolean) as Array<string>,
416
- )
417
- options.tailwind = true
418
- }
419
- } else if (forcedAddOns.length > 0) {
420
- options.chosenAddOns = await finalizeAddOns(
105
+ for (const addOn of await selectAddOns(
421
106
  options.framework,
422
107
  options.mode,
423
- Array.from(new Set([...forcedAddOns, toolchain?.id])).filter(
424
- Boolean,
425
- ) as Array<string>,
426
- )
427
- }
428
-
429
- // Collect variables
430
- const variables: Array<Variable> = []
431
- for (const addOn of options.chosenAddOns) {
432
- for (const variable of addOn.variables ?? []) {
433
- variables.push(variable)
108
+ 'example',
109
+ 'Would you like any examples?',
110
+ forcedAddOns,
111
+ )) {
112
+ addOns.add(addOn)
434
113
  }
435
114
  }
436
- options.variableValues = await collectVariables(variables)
437
115
 
438
- // Git selection
439
- if (cliOptions.git === undefined) {
440
- const git = await confirm({
441
- message: 'Would you like to initialize a new git repository?',
442
- initialValue: true,
443
- })
444
- if (isCancel(git)) {
445
- cancel('Operation cancelled.')
446
- process.exit(0)
447
- }
448
- options.git = git
449
- } else {
450
- options.git = !!cliOptions.git
116
+ options.chosenAddOns = Array.from(
117
+ await finalizeAddOns(options.framework, options.mode, Array.from(addOns)),
118
+ )
119
+
120
+ if (options.chosenAddOns.length) {
121
+ options.tailwind = true
122
+ options.typescript = true
451
123
  }
452
124
 
125
+ options.git = cliOptions.git || (await selectGit())
126
+
453
127
  return options
454
128
  }
package/src/types.ts CHANGED
@@ -1,4 +1,6 @@
1
- import type { PackageManager, TemplateOptions } from '@tanstack/cta-engine'
1
+ import type { PackageManager } from '@tanstack/cta-engine'
2
+
3
+ export type TemplateOptions = 'typescript' | 'javascript' | 'file-router'
2
4
 
3
5
  export interface CliOptions {
4
6
  template?: TemplateOptions
@@ -15,4 +17,5 @@ export interface CliOptions {
15
17
  starter?: string
16
18
  targetDir?: string
17
19
  interactive?: boolean
20
+ ui?: boolean
18
21
  }
@@ -13,52 +13,62 @@ import { createDefaultEnvironment } from '@tanstack/cta-engine'
13
13
 
14
14
  import type { Environment } from '@tanstack/cta-engine'
15
15
 
16
- export function createUIEnvironment(): Environment {
16
+ export function createUIEnvironment(
17
+ appName: string,
18
+ silent: boolean,
19
+ ): Environment {
17
20
  const defaultEnvironment = createDefaultEnvironment()
18
21
 
19
- return {
22
+ let newEnvironment = {
20
23
  ...defaultEnvironment,
21
- intro: (message: string) => {
22
- intro(message)
23
- },
24
- outro: (message: string) => {
25
- outro(message)
26
- },
27
- info: (title?: string, message?: string) => {
28
- console.log('info', title, message)
29
- log.info(
30
- `${title ? chalk.red(title) : ''}${message ? chalk.green(message) : ''}`,
31
- )
32
- },
33
- error: (title?: string, message?: string) => {
34
- console.log('error', title, message)
35
- log.error(`${title ? `${title}: ` : ''}${message}`)
36
- },
37
- warn: (title?: string, message?: string) => {
38
- console.log('warn', title, message)
39
- log.warn(`${title ? `${title}: ` : ''}${message}`)
40
- },
41
- confirm: async (message: string) => {
42
- console.log('confirm', message)
43
- const shouldContinue = await confirm({
44
- message,
45
- })
46
- if (isCancel(shouldContinue)) {
47
- cancel('Operation cancelled.')
48
- process.exit(0)
49
- }
50
- return shouldContinue
51
- },
52
- spinner: () => {
53
- const s = spinner()
54
- return {
55
- start: (message: string) => {
56
- s.start(message)
57
- },
58
- stop: (message: string) => {
59
- s.stop(message)
60
- },
61
- }
62
- },
24
+ appName,
63
25
  }
26
+
27
+ if (!silent) {
28
+ newEnvironment = {
29
+ ...newEnvironment,
30
+ intro: (message: string) => {
31
+ intro(message)
32
+ },
33
+ outro: (message: string) => {
34
+ outro(message)
35
+ },
36
+ info: (title?: string, message?: string) => {
37
+ log.info(
38
+ `${title ? chalk.red(title) : ''}${message ? '\n' + chalk.green(message) : ''}`,
39
+ )
40
+ },
41
+ error: (title?: string, message?: string) => {
42
+ log.error(
43
+ `${title ? `${title}: ` : ''}${message ? '\n' + message : ''}`,
44
+ )
45
+ },
46
+ warn: (title?: string, message?: string) => {
47
+ log.warn(`${title ? `${title}: ` : ''}${message ? '\n' + message : ''}`)
48
+ },
49
+ confirm: async (message: string) => {
50
+ const shouldContinue = await confirm({
51
+ message,
52
+ })
53
+ if (isCancel(shouldContinue)) {
54
+ cancel('Operation cancelled.')
55
+ process.exit(0)
56
+ }
57
+ return shouldContinue
58
+ },
59
+ spinner: () => {
60
+ const s = spinner()
61
+ return {
62
+ start: (message: string) => {
63
+ s.start(message)
64
+ },
65
+ stop: (message: string) => {
66
+ s.stop(message)
67
+ },
68
+ }
69
+ },
70
+ }
71
+ }
72
+
73
+ return newEnvironment
64
74
  }