-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
perf: switch to clack for CLI prompts #14589
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,14 @@ | ||
import fsMod, { existsSync, promises as fs } from 'node:fs'; | ||
import path from 'node:path'; | ||
import { fileURLToPath, pathToFileURL } from 'node:url'; | ||
import boxen from 'boxen'; | ||
import * as clack from '@clack/prompts'; | ||
import { diffWords } from 'diff'; | ||
import { bold, cyan, dim, green, magenta, red, yellow } from 'kleur/colors'; | ||
import { type ASTNode, builders, generateCode, loadFile, type ProxifiedModule } from 'magicast'; | ||
import { getDefaultExportOptions } from 'magicast/helpers'; | ||
import { detect, resolveCommand } from 'package-manager-detector'; | ||
import prompts from 'prompts'; | ||
import maxSatisfying from 'semver/ranges/max-satisfying.js'; | ||
import type yargsParser from 'yargs-parser'; | ||
import yoctoSpinner from 'yocto-spinner'; | ||
import { | ||
loadTSConfig, | ||
resolveConfig, | ||
|
@@ -363,19 +361,19 @@ export async function add(names: string[], { flags }: AddOptions) { | |
), | ||
); | ||
if (integrations.find((integration) => integration.integrationName === 'tailwind')) { | ||
const code = boxen(getDiffContent('---\n---', "---\nimport '../styles/global.css'\n---")!, { | ||
margin: 0.5, | ||
padding: 0.5, | ||
borderStyle: 'round', | ||
title: 'src/layouts/Layout.astro', | ||
}); | ||
logger.warn( | ||
'SKIP_FORMAT', | ||
msg.actionRequired( | ||
'You must import your Tailwind stylesheet, e.g. in a shared layout:\n', | ||
), | ||
); | ||
logger.info('SKIP_FORMAT', code + '\n'); | ||
clack.box( | ||
getDiffContent('---\n---', "---\nimport '../styles/global.css'\n---")!, | ||
'src/layouts/Layout.astro', | ||
{ | ||
rounded: true, | ||
}, | ||
); | ||
} | ||
} | ||
} | ||
|
@@ -561,18 +559,15 @@ async function updateAstroConfig({ | |
return UpdateResult.none; | ||
} | ||
|
||
const message = `\n${boxen(diff, { | ||
margin: 0.5, | ||
padding: 0.5, | ||
borderStyle: 'round', | ||
title: configURL.pathname.split('/').pop(), | ||
})}\n`; | ||
|
||
logger.info( | ||
'SKIP_FORMAT', | ||
`\n ${magenta('Astro will make the following changes to your config file:')}\n${message}`, | ||
`\n ${magenta('Astro will make the following changes to your config file:')}`, | ||
); | ||
|
||
clack.box(diff, configURL.pathname.split('/').pop(), { | ||
rounded: true, | ||
}); | ||
|
||
if (logAdapterInstructions) { | ||
logger.info( | ||
'SKIP_FORMAT', | ||
|
@@ -676,20 +671,19 @@ async function tryToInstallIntegrations({ | |
); | ||
|
||
const coloredOutput = `${bold(installCommand.command)} ${installCommand.args.join(' ')} ${cyan(installSpecifiers.join(' '))}`; | ||
const message = `\n${boxen(coloredOutput, { | ||
margin: 0.5, | ||
padding: 0.5, | ||
borderStyle: 'round', | ||
})}\n`; | ||
logger.info( | ||
'SKIP_FORMAT', | ||
`\n ${magenta('Astro will run the following command:')}\n ${dim( | ||
'If you skip this step, you can always run it yourself later', | ||
)}\n${message}`, | ||
)}`, | ||
); | ||
clack.box(coloredOutput, undefined, { | ||
rounded: true, | ||
}); | ||
|
||
if (await askToContinue({ flags })) { | ||
const spinner = yoctoSpinner({ text: 'Installing dependencies...' }).start(); | ||
const spinner = clack.spinner(); | ||
spinner.start('Installing dependencies...'); | ||
try { | ||
await exec(installCommand.command, [...installCommand.args, ...installSpecifiers], { | ||
nodeOptions: { | ||
|
@@ -698,10 +692,10 @@ async function tryToInstallIntegrations({ | |
env: { NODE_ENV: undefined }, | ||
}, | ||
}); | ||
spinner.success(); | ||
spinner.stop(); | ||
return UpdateResult.updated; | ||
} catch (err: any) { | ||
spinner.error(); | ||
spinner.stop(undefined, 2); | ||
logger.debug('add', 'Error installing dependencies', err); | ||
// NOTE: `err.stdout` can be an empty string, so log the full error instead for a more helpful log | ||
console.error('\n', err.stdout || err.message, '\n'); | ||
|
@@ -716,7 +710,8 @@ async function validateIntegrations( | |
integrations: string[], | ||
flags: yargsParser.Arguments, | ||
): Promise<IntegrationInfo[]> { | ||
const spinner = yoctoSpinner({ text: 'Resolving packages...' }).start(); | ||
const spinner = clack.spinner(); | ||
spinner.start('Resolving packages...'); | ||
try { | ||
const integrationEntries = await Promise.all( | ||
integrations.map(async (integration): Promise<IntegrationInfo> => { | ||
|
@@ -734,17 +729,17 @@ async function validateIntegrations( | |
const firstPartyPkgCheck = await fetchPackageJson('@astrojs', name, tag); | ||
if (firstPartyPkgCheck instanceof Error) { | ||
if (firstPartyPkgCheck.message) { | ||
spinner.warning(yellow(firstPartyPkgCheck.message)); | ||
spinner.message(yellow(firstPartyPkgCheck.message)); | ||
} | ||
spinner.warning(yellow(`${bold(integration)} is not an official Astro package.`)); | ||
spinner.message(yellow(`${bold(integration)} is not an official Astro package.`)); | ||
if (!(await askToContinue({ flags }))) { | ||
throw new Error( | ||
`No problem! Find our official integrations at ${cyan( | ||
'https://astro.build/integrations', | ||
)}`, | ||
); | ||
} | ||
spinner.start('Resolving with third party packages...'); | ||
spinner.message('Resolving with third party packages...'); | ||
pkgType = 'third-party'; | ||
} else { | ||
pkgType = 'first-party'; | ||
|
@@ -755,7 +750,7 @@ async function validateIntegrations( | |
const thirdPartyPkgCheck = await fetchPackageJson(scope, name, tag); | ||
if (thirdPartyPkgCheck instanceof Error) { | ||
if (thirdPartyPkgCheck.message) { | ||
spinner.warning(yellow(thirdPartyPkgCheck.message)); | ||
spinner.message(yellow(thirdPartyPkgCheck.message)); | ||
} | ||
throw new Error(`Unable to fetch ${bold(integration)}. Does the package exist?`); | ||
} else { | ||
|
@@ -813,11 +808,11 @@ async function validateIntegrations( | |
}; | ||
}), | ||
); | ||
spinner.success(); | ||
spinner.stop(); | ||
return integrationEntries; | ||
} catch (e) { | ||
if (e instanceof Error) { | ||
spinner.error(e.message); | ||
spinner.stop(e.message, 2); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Linking bombshell-dev/clack#405 here as a reminder to revisit if that change to the spinner API goes ahead. |
||
process.exit(1); | ||
} else { | ||
throw e; | ||
|
@@ -872,18 +867,15 @@ async function updateTSConfig( | |
return UpdateResult.none; | ||
} | ||
|
||
const message = `\n${boxen(diff, { | ||
margin: 0.5, | ||
padding: 0.5, | ||
borderStyle: 'round', | ||
title: configFileName, | ||
})}\n`; | ||
|
||
logger.info( | ||
'SKIP_FORMAT', | ||
`\n ${magenta(`Astro will make the following changes to your ${configFileName}:`)}\n${message}`, | ||
`\n ${magenta(`Astro will make the following changes to your ${configFileName}:`)}`, | ||
); | ||
|
||
clack.box(diff, configFileName, { | ||
rounded: true, | ||
}); | ||
|
||
// Every major framework, apart from Vue and Svelte requires different `jsxImportSource`, as such it's impossible to config | ||
// all of them in the same `tsconfig.json`. However, Vue only need `"jsx": "preserve"` for template intellisense which | ||
// can be compatible with some frameworks (ex: Solid) | ||
|
@@ -935,14 +927,12 @@ function parseIntegrationName(spec: string) { | |
async function askToContinue({ flags }: { flags: Flags }): Promise<boolean> { | ||
if (flags.yes || flags.y) return true; | ||
|
||
const response = await prompts({ | ||
type: 'confirm', | ||
name: 'askToContinue', | ||
const response = await clack.confirm({ | ||
message: 'Continue?', | ||
initial: true, | ||
initialValue: true, | ||
}); | ||
|
||
return Boolean(response.askToContinue); | ||
return response === true; | ||
} | ||
|
||
function getDiffContent(input: string, output: string): string | null { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
import { spawn, spawnSync } from 'node:child_process'; | ||
import { arch, platform } from 'node:os'; | ||
import * as clack from '@clack/prompts'; | ||
import * as colors from 'kleur/colors'; | ||
import prompts from 'prompts'; | ||
import { resolveConfig } from '../../core/config/index.js'; | ||
import { ASTRO_VERSION } from '../../core/constants.js'; | ||
import { apply as applyPolyfill } from '../../core/polyfill.js'; | ||
|
@@ -124,14 +124,12 @@ async function copyToClipboard(text: string, force?: boolean) { | |
} | ||
|
||
if (!force) { | ||
const { shouldCopy } = await prompts({ | ||
type: 'confirm', | ||
name: 'shouldCopy', | ||
const shouldCopy = await clack.confirm({ | ||
message: 'Copy to clipboard?', | ||
initial: true, | ||
initialValue: true, | ||
}); | ||
|
||
if (!shouldCopy) return; | ||
if (shouldCopy !== true) return; | ||
} | ||
Comment on lines
126
to
133
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
||
try { | ||
|
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -1,10 +1,8 @@ | ||||||||||
import { createRequire } from 'node:module'; | ||||||||||
import boxen from 'boxen'; | ||||||||||
import * as clack from '@clack/prompts'; | ||||||||||
import ci from 'ci-info'; | ||||||||||
import { bold, cyan, dim, magenta } from 'kleur/colors'; | ||||||||||
import { detect, resolveCommand } from 'package-manager-detector'; | ||||||||||
import prompts from 'prompts'; | ||||||||||
import yoctoSpinner from 'yocto-spinner'; | ||||||||||
import type { Logger } from '../core/logger/core.js'; | ||||||||||
import { exec } from './exec.js'; | ||||||||||
|
||||||||||
|
@@ -75,34 +73,30 @@ async function installPackage( | |||||||||
packageNames = packageNames.map((name) => `npm:${name}`); | ||||||||||
} | ||||||||||
const coloredOutput = `${bold(installCommand.command)} ${installCommand.args.join(' ')} ${cyan(packageNames.join(' '))}`; | ||||||||||
const message = `\n${boxen(coloredOutput, { | ||||||||||
margin: 0.5, | ||||||||||
padding: 0.5, | ||||||||||
borderStyle: 'round', | ||||||||||
})}\n`; | ||||||||||
logger.info( | ||||||||||
'SKIP_FORMAT', | ||||||||||
`\n ${magenta('Astro will run the following command:')}\n ${dim( | ||||||||||
'If you skip this step, you can always run it yourself later', | ||||||||||
)}\n${message}`, | ||||||||||
)}`, | ||||||||||
); | ||||||||||
clack.box(coloredOutput, undefined, { | ||||||||||
rounded: true, | ||||||||||
}); | ||||||||||
|
||||||||||
let response; | ||||||||||
if (options.skipAsk) { | ||||||||||
response = true; | ||||||||||
} else { | ||||||||||
response = ( | ||||||||||
await prompts({ | ||||||||||
type: 'confirm', | ||||||||||
name: 'askToContinue', | ||||||||||
response = | ||||||||||
(await clack.confirm({ | ||||||||||
message: 'Continue?', | ||||||||||
initial: true, | ||||||||||
}) | ||||||||||
).askToContinue; | ||||||||||
initialValue: true, | ||||||||||
})) === true; | ||||||||||
} | ||||||||||
Comment on lines
-78
to
95
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Documenting the visual changes here:
Cancel state:
I quite like how the bold text in the old version helps the “Continue?” question stand out, so could be something for us to create a little set of styled utilities for? For example, something like this would help us use that style consistently: const confirm = ({ message, ...opts }) =>
clack.confirm({ message: bold(message), ...opts }); As noted in my main comment, you can see the inconsistency of switching between the Clack style with the left border and the regular logging here. |
||||||||||
|
||||||||||
if (Boolean(response)) { | ||||||||||
const spinner = yoctoSpinner({ text: 'Installing dependencies...' }).start(); | ||||||||||
const spinner = clack.spinner(); | ||||||||||
spinner.start('Installing dependencies...'); | ||||||||||
try { | ||||||||||
await exec(installCommand.command, [...installCommand.args, ...packageNames], { | ||||||||||
nodeOptions: { | ||||||||||
|
@@ -111,12 +105,12 @@ async function installPackage( | |||||||||
env: { NODE_ENV: undefined }, | ||||||||||
}, | ||||||||||
}); | ||||||||||
spinner.success(); | ||||||||||
spinner.stop(); | ||||||||||
|
||||||||||
return true; | ||||||||||
} catch (err) { | ||||||||||
logger.debug('add', 'Error installing dependencies', err); | ||||||||||
spinner.error(); | ||||||||||
spinner.stop(undefined, 2); | ||||||||||
|
||||||||||
return false; | ||||||||||
} | ||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quick comparison of the two states here:
The “Resolving packages” message is preserved currently as a reference to the completed task, whereas it looks like Clack’s
stop()
clears that out. I think it would be nice to preserve a message to refer back to.Maybe as simple as?