From aaab8eca7d758b952210d4ecd21ffc7590a74b3b Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Thu, 19 Oct 2023 11:02:24 -0600 Subject: [PATCH] feat: improved terminal output --- src/commands/plugins/install.ts | 9 +++++++-- src/commands/plugins/link.ts | 3 +++ src/commands/plugins/uninstall.ts | 3 +++ src/commands/plugins/update.ts | 9 ++++++++- src/plugins.ts | 4 ++-- src/util.ts | 27 +++++++++++++++++++++++++ src/yarn.ts | 33 ++++++++++++++++++++++++++----- 7 files changed, 78 insertions(+), 10 deletions(-) diff --git a/src/commands/plugins/install.ts b/src/commands/plugins/install.ts index edb9e4b0..cee9e10a 100644 --- a/src/commands/plugins/install.ts +++ b/src/commands/plugins/install.ts @@ -1,8 +1,10 @@ +/* eslint-disable no-await-in-loop */ import {Args, Command, Errors, Flags, Interfaces, ux} from '@oclif/core' import chalk from 'chalk' import validate from 'validate-npm-package-name' import Plugins from '../../plugins.js' +import {WarningsCache} from '../../util.js' export default class PluginsInstall extends Command { static aliases = ['plugins:add'] @@ -127,9 +129,7 @@ e.g. If you have a core plugin that has a 'hello' command, installing a user-ins const {name, tag} = await getNameAndTag(input) return {name, tag, type: 'npm'} } - /* eslint-enable no-await-in-loop */ - /* eslint-disable no-await-in-loop */ async run(): Promise { const {argv, flags} = await this.parse(PluginsInstall) this.flags = flags @@ -157,10 +157,15 @@ e.g. If you have a core plugin that has a 'hello' command, installing a user-ins } } catch (error) { ux.action.stop(chalk.bold.red('failed')) + WarningsCache.getInstance().flush() throw error } ux.action.stop(`installed v${plugin.version}`) + + WarningsCache.getInstance().flush() + + ux.log(chalk.green(`Successfully installed ${plugin.name} v${plugin.version}`)) } } } diff --git a/src/commands/plugins/link.ts b/src/commands/plugins/link.ts index 48ff34e5..08d35a5c 100644 --- a/src/commands/plugins/link.ts +++ b/src/commands/plugins/link.ts @@ -2,6 +2,7 @@ import {Args, Command, Flags, ux} from '@oclif/core' import chalk from 'chalk' import Plugins from '../../plugins.js' +import {WarningsCache} from '../../util.js' export default class PluginsLink extends Command { static args = { @@ -36,5 +37,7 @@ e.g. If you have a user-installed or core plugin that has a 'hello' command, ins ux.action.start(`Linking plugin ${chalk.cyan(args.path)}`) await this.plugins.link(args.path, {install: flags.install}) ux.action.stop() + + WarningsCache.getInstance().flush() } } diff --git a/src/commands/plugins/uninstall.ts b/src/commands/plugins/uninstall.ts index 31a1ceba..09c24116 100644 --- a/src/commands/plugins/uninstall.ts +++ b/src/commands/plugins/uninstall.ts @@ -3,6 +3,7 @@ import {Args, Command, Flags, ux} from '@oclif/core' import chalk from 'chalk' import Plugins from '../../plugins.js' +import {WarningsCache} from '../../util.js' function removeTags(plugin: string): string { if (plugin.includes('@')) { @@ -74,6 +75,8 @@ export default class PluginsUninstall extends Command { } ux.action.stop() + + WarningsCache.getInstance().flush() } } } diff --git a/src/commands/plugins/update.ts b/src/commands/plugins/update.ts index f54b56aa..caffa1ab 100644 --- a/src/commands/plugins/update.ts +++ b/src/commands/plugins/update.ts @@ -1,6 +1,7 @@ -import {Command, Flags} from '@oclif/core' +import {Command, Flags, ux} from '@oclif/core' import Plugins from '../../plugins.js' +import {WarningsCache} from '../../util.js' export default class PluginsUpdate extends Command { static description = 'Update installed plugins.' @@ -15,6 +16,12 @@ export default class PluginsUpdate extends Command { async run(): Promise { const {flags} = await this.parse(PluginsUpdate) this.plugins.verbose = flags.verbose + ux.action.start(`${this.config.name}: Updating plugins`) + await this.plugins.update() + + ux.action.stop() + + WarningsCache.getInstance().flush() } } diff --git a/src/plugins.ts b/src/plugins.ts index e3e5bb98..78babab8 100644 --- a/src/plugins.ts +++ b/src/plugins.ts @@ -209,9 +209,11 @@ export default class Plugins { * @returns Promise */ async refresh(options: {all: boolean; prod: boolean}, ...roots: string[]): Promise { + ux.action.status = 'Refreshing user plugins...' const doRefresh = async (root: string) => { await this.yarn.exec(options.prod ? ['--prod'] : [], { cwd: root, + noSpinner: true, silent: this.silent, verbose: this.verbose, }) @@ -283,7 +285,6 @@ export default class Plugins { // eslint-disable-next-line unicorn/no-await-expression-member let plugins = (await this.list()).filter((p): p is Interfaces.PJSON.PluginTypes.User => p.type === 'user') if (plugins.length === 0) return - ux.action.start(`${this.config.name}: Updating plugins`) // migrate deprecated plugins const aliases = this.config.pjson.oclif.aliases || {} @@ -334,7 +335,6 @@ export default class Plugins { await this.refresh({all: true, prod: true}) await this.add(...modifiedPlugins) - ux.action.stop() } private async createPJSON() { diff --git a/src/util.ts b/src/util.ts index 9c6a9046..0a923f78 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,3 +1,4 @@ +import {ux} from '@oclif/core' import * as fs from 'node:fs' import * as fsPromises from 'node:fs/promises' import {createRequire} from 'node:module' @@ -94,3 +95,29 @@ export async function findNpm(): Promise { const npmPath = npmPjsonPath.slice(0, Math.max(0, npmPjsonPath.lastIndexOf(path.sep))) return path.join(npmPath, npmPjson.bin.npm) } + +export class WarningsCache { + private static cache: string[] = [] + private static instance: WarningsCache + public static getInstance(): WarningsCache { + if (!WarningsCache.instance) { + WarningsCache.instance = new WarningsCache() + } + + return WarningsCache.instance + } + + public add(...warnings: string[]): void { + for (const warning of warnings) { + if (!WarningsCache.cache.includes(warning)) { + WarningsCache.cache.push(warning) + } + } + } + + public flush(): void { + for (const warning of WarningsCache.cache) { + ux.warn(warning) + } + } +} diff --git a/src/yarn.ts b/src/yarn.ts index 47e3a9bd..9de76c33 100644 --- a/src/yarn.ts +++ b/src/yarn.ts @@ -5,10 +5,19 @@ import {createRequire} from 'node:module' import * as path from 'node:path' import NpmRunPath from 'npm-run-path' +import {WarningsCache} from './util.js' + const debug = makeDebug('cli:yarn') const require = createRequire(import.meta.url) +type YarnExecOptions = { + cwd: string + noSpinner?: boolean + silent: boolean + verbose: boolean +} + export default class Yarn { config: Interfaces.Config @@ -20,7 +29,7 @@ export default class Yarn { return require.resolve('yarn/bin/yarn.js') } - async exec(args: string[] = [], opts: {cwd: string; silent: boolean; verbose: boolean}): Promise { + async exec(args: string[] = [], opts: YarnExecOptions): Promise { const {cwd, silent, verbose} = opts if (args[0] !== 'run') { // https://classic.yarnpkg.com/lang/en/docs/cli/#toc-concurrency-and-mutex @@ -80,16 +89,30 @@ export default class Yarn { } } - fork(modulePath: string, args: string[] = [], options: Record = {}): Promise { + fork(modulePath: string, args: string[] = [], options: YarnExecOptions): Promise { + const cache = WarningsCache.getInstance() + return new Promise((resolve, reject) => { const forked = fork(modulePath, args, options) - forked.stderr?.on('data', (d) => { - if (!options.silent) process.stderr.write(d) + forked.stderr?.on('data', (d: Buffer) => { + if (!options.silent) + cache.add( + ...d + .toString() + .split('\n') + .map((i) => + i + .trim() + .replace(/^warning/, '') + .trim(), + ) + .filter(Boolean), + ) }) forked.stdout?.setEncoding('utf8') forked.stdout?.on('data', (d) => { if (options.verbose) process.stdout.write(d) - else ux.action.status = d.replace(/\n$/, '').split('\n').pop() + else if (!options.noSpinner) ux.action.status = d.replace(/\n$/, '').split('\n').pop() }) forked.on('error', reject)