Skip to content

Commit c489673

Browse files
committed
chore: refactoring the UI out of the engine
1 parent d7eac47 commit c489673

File tree

10 files changed

+130
-62
lines changed

10 files changed

+130
-62
lines changed

packages/cta-cli/src/cli.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import { runMCPServer } from '@tanstack/cta-mcp'
1818

1919
import { normalizeOptions, promptForOptions } from './options.js'
2020

21+
import { createUIEnvironment } from './ui-environment.js'
22+
2123
import type {
2224
Framework,
2325
Mode,
@@ -58,6 +60,8 @@ export function cli({
5860
forcedMode?: Mode
5961
forcedAddOns?: Array<string>
6062
}) {
63+
const environment = createUIEnvironment()
64+
6165
const program = new Command()
6266

6367
program.name(name).description(`CLI to create a new ${appName} application`)
@@ -66,21 +70,27 @@ export function cli({
6670
.command('add')
6771
.argument('add-on', 'Name of the add-on (or add-ons separated by commas)')
6872
.action(async (addOn: string) => {
69-
await addToApp(addOn.split(',').map((addon) => addon.trim()))
73+
await addToApp(
74+
addOn.split(',').map((addon) => addon.trim()),
75+
{
76+
silent: false,
77+
},
78+
environment,
79+
)
7080
})
7181

7282
program
7383
.command('update-add-on')
7484
.description('Create or update an add-on from the current project')
7585
.action(async () => {
76-
await initAddOn('add-on')
86+
await initAddOn('add-on', environment)
7787
})
7888

7989
program
8090
.command('update-starter')
8191
.description('Create or update a project starter from the current project')
8292
.action(async () => {
83-
await initAddOn('starter')
93+
await initAddOn('starter', environment)
8494
})
8595

8696
program.argument('[project-name]', 'name of the project')
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { cancel, confirm, intro, isCancel, log, outro } from '@clack/prompts'
2+
import chalk from 'chalk'
3+
4+
import { createDefaultEnvironment } from '@tanstack/cta-engine'
5+
6+
import type { Environment } from '@tanstack/cta-engine'
7+
8+
export function createUIEnvironment(): Environment {
9+
const defaultEnvironment = createDefaultEnvironment()
10+
11+
return {
12+
...defaultEnvironment,
13+
intro: (message: string) => {
14+
intro(message)
15+
},
16+
outro: (message: string) => {
17+
outro(message)
18+
},
19+
info: (title?: string, message?: string) => {
20+
log.info(
21+
`${title ? chalk.red(title) : ''}${message ? chalk.green(message) : ''}`,
22+
)
23+
},
24+
error: (title?: string, message?: string) => {
25+
log.error(`${title ? `${title}: ` : ''}${message}`)
26+
},
27+
warn: (title?: string, message?: string) => {
28+
log.warn(`${title ? `${title}: ` : ''}${message}`)
29+
},
30+
confirm: async (message: string) => {
31+
const shouldContinue = await confirm({
32+
message,
33+
})
34+
if (isCancel(shouldContinue)) {
35+
cancel('Operation cancelled.')
36+
process.exit(0)
37+
}
38+
return shouldContinue
39+
},
40+
}
41+
}

packages/cta-engine/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@
3232
"license": "MIT",
3333
"packageManager": "[email protected]",
3434
"dependencies": {
35-
"@clack/prompts": "^0.10.0",
36-
"chalk": "^5.4.1",
3735
"ejs": "^3.1.10",
3836
"execa": "^9.5.2",
3937
"memfs": "^4.17.0",

packages/cta-engine/src/add.ts

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,7 @@
11
import { mkdir, readFile, writeFile } from 'node:fs/promises'
22
import { existsSync, statSync } from 'node:fs'
33
import { basename, dirname, resolve } from 'node:path'
4-
import chalk from 'chalk'
54
import { execa, execaSync } from 'execa'
6-
import {
7-
cancel,
8-
confirm,
9-
intro,
10-
isCancel,
11-
log,
12-
outro,
13-
spinner,
14-
} from '@clack/prompts'
155

166
import { CONFIG_FILE } from './constants.js'
177
import {
@@ -25,7 +15,7 @@ import { readConfigFile, writeConfigFile } from './config-file.js'
2515

2616
import type { PersistedOptions } from './config-file.js'
2717

28-
import type { Framework, Options } from './types.js'
18+
import type { Environment, Framework, Options } from './types.js'
2919

3020
function isDirectory(path: string) {
3121
return statSync(path).isDirectory()
@@ -69,22 +59,25 @@ export async function addToApp(
6959
}: {
7060
silent?: boolean
7161
} = {},
62+
environment: Environment,
7263
) {
7364
const persistedOptions = await readConfigFile(process.cwd())
7465
if (!persistedOptions) {
75-
console.error(`${chalk.red('There is no .cta.json file in your project.')}
76-
77-
This is probably because this was created with an older version of create-tsrouter-app.`)
66+
environment.error(
67+
'There is no .cta.json file in your project.',
68+
'This is probably because this was created with an older version of create-tsrouter-app.',
69+
)
7870
return
7971
}
8072

8173
if (!silent) {
82-
intro(`Adding ${addOns.join(', ')} to the project...`)
74+
environment.intro(`Adding ${addOns.join(', ')} to the project...`)
8375
}
8476

8577
if (await hasPendingGitChanges()) {
86-
log.error(
87-
`${chalk.red('You have pending git changes.')} Please commit or stash them before adding add-ons.`,
78+
environment.error(
79+
'You have pending git changes.',
80+
'Please commit or stash them before adding add-ons.',
8881
)
8982
return
9083
}
@@ -116,14 +109,12 @@ This is probably because this was created with an older version of create-tsrout
116109
}
117110

118111
if (overwrittenFiles.length > 0 && !silent) {
119-
log.warn(
120-
`${chalk.yellow('The following will be overwritten:')}\n${overwrittenFiles.join('\n')}`,
112+
environment.warn(
113+
'The following will be overwritten:',
114+
overwrittenFiles.join('\n'),
121115
)
122-
const shouldContinue = await confirm({
123-
message: 'Do you want to continue?',
124-
})
125-
if (isCancel(shouldContinue)) {
126-
cancel('Operation cancelled.')
116+
const shouldContinue = await environment.confirm('Do you want to continue?')
117+
if (!shouldContinue) {
127118
process.exit(0)
128119
}
129120
}
@@ -171,7 +162,7 @@ This is probably because this was created with an older version of create-tsrout
171162
const realEnvironment = createDefaultEnvironment()
172163
writeConfigFile(realEnvironment, process.cwd(), newOptions)
173164

174-
const s = silent ? null : spinner()
165+
const s = silent ? null : environment.spinner()
175166
s?.start(`Installing dependencies via ${newOptions.packageManager}...`)
176167
await realEnvironment.execute(
177168
newOptions.packageManager,
@@ -181,6 +172,6 @@ This is probably because this was created with an older version of create-tsrout
181172
s?.stop(`Installed dependencies`)
182173

183174
if (!silent) {
184-
outro('Add-ons added successfully!')
175+
environment.outro('Add-ons added successfully!')
185176
}
186177
}

packages/cta-engine/src/create-app.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { basename, dirname, resolve } from 'node:path'
2-
import { log, outro, spinner } from '@clack/prompts'
32
import { render } from 'ejs'
43
import { format } from 'prettier'
5-
import chalk from 'chalk'
64

75
import { getTemplatesRoot } from './templates.js'
86
import { CODE_ROUTER, FILE_ROUTER } from './constants.js'
@@ -119,8 +117,7 @@ function createTemplateFile(
119117
try {
120118
content = render(content, templateValues)
121119
} catch (error) {
122-
console.error(chalk.red(`EJS error in file ${file}`))
123-
console.error(error)
120+
environment.error(`EJS error in file ${file}`, error?.toString())
124121
process.exit(1)
125122
}
126123
const target = targetFileName ?? file.replace('.ejs', '')
@@ -338,7 +335,7 @@ export async function createApp(
338335

339336
if (environment.exists(targetDir)) {
340337
if (!silent) {
341-
log.error(`Directory "${options.projectName}" already exists`)
338+
environment.error(`Directory "${options.projectName}" already exists`)
342339
}
343340
return
344341
}
@@ -507,7 +504,7 @@ export async function createApp(
507504
)
508505

509506
// Copy all the asset files from the addons
510-
const s = silent ? null : spinner()
507+
const s = silent ? null : environment.spinner()
511508
for (const type of ['add-on', 'example']) {
512509
for (const phase of ['setup', 'add-on', 'example']) {
513510
for (const addOn of options.chosenAddOns.filter(
@@ -732,7 +729,10 @@ export async function createApp(
732729

733730
if (warnings.length > 0) {
734731
if (!silent) {
735-
log.warn(chalk.red(warnings.join('\n')))
732+
environment.warn(
733+
'The following will be overwritten:',
734+
warnings.join('\n'),
735+
)
736736
}
737737
}
738738

@@ -782,7 +782,7 @@ export async function createApp(
782782
if (environment.getErrors().length) {
783783
errorStatement = `
784784
785-
${chalk.red('Errors were encountered during this process:')}
785+
Errors were encountered during this process:
786786
787787
${environment.getErrors().join('\n')}`
788788
}
@@ -793,7 +793,7 @@ ${environment.getErrors().join('\n')}`
793793
startCommand = `deno ${isAddOnEnabled('start') ? 'task dev' : 'start'}`
794794
}
795795

796-
outro(`Your ${appName} app is ready in '${basename(targetDir)}'.
796+
environment.outro(`Your ${appName} app is ready in '${basename(targetDir)}'.
797797
798798
Use the following commands to start your app:
799799
% cd ${options.projectName}

packages/cta-engine/src/custom-add-on.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import { readFile, readdir } from 'node:fs/promises'
22
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
33
import { basename, dirname, resolve } from 'node:path'
4-
import chalk from 'chalk'
54

65
import { createMemoryEnvironment } from './environment.js'
76
import { createApp } from './create-app.js'
87
import { readConfigFile } from './config-file.js'
98
import { finalizeAddOns } from './add-ons.js'
109
import { readFileHelper } from './file-helper.js'
1110

12-
import type { Framework, Options } from './types.js'
11+
import type { Environment, Framework, Options } from './types.js'
1312
import type { PersistedOptions } from './config-file.js'
1413

1514
type AddOnMode = 'add-on' | 'starter'
@@ -163,32 +162,36 @@ async function compareFiles(
163162
}
164163
}
165164

166-
export async function initAddOn(mode: AddOnMode) {
165+
export async function initAddOn(mode: AddOnMode, environment: Environment) {
167166
const persistedOptions = await readConfigFile(process.cwd())
168167
if (!persistedOptions) {
169-
console.error(`${chalk.red('There is no .cta.json file in your project.')}
170-
171-
This is probably because this was created with an older version of create-tsrouter-app.`)
168+
environment.error(
169+
'There is no .cta.json file in your project.',
170+
`This is probably because this was created with an older version of create-tsrouter-app.`,
171+
)
172172
return
173173
}
174174

175175
if (mode === 'add-on') {
176176
if (persistedOptions.mode !== 'file-router') {
177-
console.error(`${chalk.red('This project is not using file-router mode.')}
178-
179-
To create an add-on, the project must be created with the file-router mode.`)
177+
environment.error(
178+
'This project is not using file-router mode.',
179+
'To create an add-on, the project must be created with the file-router mode.',
180+
)
180181
return
181182
}
182183
if (!persistedOptions.tailwind) {
183-
console.error(`${chalk.red('This project is not using Tailwind CSS.')}
184-
185-
To create an add-on, the project must be created with Tailwind CSS.`)
184+
environment.error(
185+
'This project is not using Tailwind CSS.',
186+
'To create an add-on, the project must be created with Tailwind CSS.',
187+
)
186188
return
187189
}
188190
if (!persistedOptions.typescript) {
189-
console.error(`${chalk.red('This project is not using TypeScript.')}
190-
191-
To create an add-on, the project must be created with TypeScript.`)
191+
environment.error(
192+
'This project is not using TypeScript.',
193+
'To create an add-on, the project must be created with TypeScript.',
194+
)
192195
return
193196
}
194197
}

packages/cta-engine/src/environment.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,17 @@ export function createDefaultEnvironment(): Environment {
5757
const stat = statSync(path)
5858
return stat.isDirectory()
5959
},
60+
61+
intro: () => {},
62+
outro: () => {},
63+
info: () => {},
64+
error: () => {},
65+
warn: () => {},
66+
confirm: () => Promise.resolve(true),
67+
spinner: () => ({
68+
start: () => {},
69+
stop: () => {},
70+
}),
6071
}
6172
}
6273

packages/cta-engine/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export { SUPPORTED_TOOLCHAINS, DEFAULT_TOOLCHAIN } from './toolchain.js'
2121

2222
export type {
2323
AddOn,
24+
Environment,
2425
Framework,
2526
Mode,
2627
Options,

packages/cta-engine/src/types.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ export interface Options {
2323
starter?: AddOn | undefined
2424
}
2525

26-
export type Environment = {
26+
type ProjectEnvironment = {
2727
startRun: () => void
2828
finishRun: () => void
2929
getErrors: () => Array<string>
30+
}
3031

32+
type FileEnvironment = {
3133
appendFile: (path: string, contents: string) => Promise<void>
3234
copyFile: (from: string, to: string) => Promise<void>
3335
writeFile: (path: string, contents: string) => Promise<void>
@@ -40,6 +42,23 @@ export type Environment = {
4042
isDirectory: (path: string) => boolean
4143
}
4244

45+
type UIEnvironment = {
46+
intro: (message: string) => void
47+
outro: (message: string) => void
48+
49+
info: (title?: string, message?: string) => void
50+
error: (title?: string, message?: string) => void
51+
warn: (title?: string, message?: string) => void
52+
53+
spinner: () => {
54+
start: (message: string) => void
55+
stop: (message: string) => void
56+
}
57+
confirm: (message: string) => Promise<boolean>
58+
}
59+
60+
export type Environment = ProjectEnvironment & FileEnvironment & UIEnvironment
61+
4362
type BooleanVariable = {
4463
name: string
4564
default: boolean

0 commit comments

Comments
 (0)