Skip to content

Commit

Permalink
feat: improved terminal output
Browse files Browse the repository at this point in the history
  • Loading branch information
mdonnalley committed Oct 19, 2023
1 parent 9f3c4bc commit aaab8ec
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 10 deletions.
9 changes: 7 additions & 2 deletions src/commands/plugins/install.ts
Original file line number Diff line number Diff line change
@@ -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']
Expand Down Expand Up @@ -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<void> {
const {argv, flags} = await this.parse(PluginsInstall)
this.flags = flags
Expand Down Expand Up @@ -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}`))
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/commands/plugins/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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()
}
}
3 changes: 3 additions & 0 deletions src/commands/plugins/uninstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('@')) {
Expand Down Expand Up @@ -74,6 +75,8 @@ export default class PluginsUninstall extends Command {
}

ux.action.stop()

WarningsCache.getInstance().flush()
}
}
}
9 changes: 8 additions & 1 deletion src/commands/plugins/update.ts
Original file line number Diff line number Diff line change
@@ -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.'
Expand All @@ -15,6 +16,12 @@ export default class PluginsUpdate extends Command {
async run(): Promise<void> {
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()
}
}
4 changes: 2 additions & 2 deletions src/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,11 @@ export default class Plugins {
* @returns Promise<void>
*/
async refresh(options: {all: boolean; prod: boolean}, ...roots: string[]): Promise<void> {
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,
})
Expand Down Expand Up @@ -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 || {}
Expand Down Expand Up @@ -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() {
Expand Down
27 changes: 27 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -94,3 +95,29 @@ export async function findNpm(): Promise<string> {
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)
}
}
}
33 changes: 28 additions & 5 deletions src/yarn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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<void> {
async exec(args: string[] = [], opts: YarnExecOptions): Promise<void> {
const {cwd, silent, verbose} = opts
if (args[0] !== 'run') {
// https://classic.yarnpkg.com/lang/en/docs/cli/#toc-concurrency-and-mutex
Expand Down Expand Up @@ -80,16 +89,30 @@ export default class Yarn {
}
}

fork(modulePath: string, args: string[] = [], options: Record<string, unknown> = {}): Promise<void> {
fork(modulePath: string, args: string[] = [], options: YarnExecOptions): Promise<void> {
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)
Expand Down

0 comments on commit aaab8ec

Please sign in to comment.