From ba37ede0fd48c03b7f5acf768c5ee018e339a4aa Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Fri, 17 May 2024 10:59:13 -0600 Subject: [PATCH] fix: jsdocs and types --- README.md | 5 +++ package.json | 6 +-- src/index.ts | 79 +++++++++++++++++++++++++------------ test/capture-output.test.ts | 16 +++++++- test/run-command.test.ts | 13 ++++++ yarn.lock | 2 +- 6 files changed, 89 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index d5f1e25..90cd3e2 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,11 @@ See the [V4 Migration Guide](./MIGRATION.md) if you are migrating from v3 or old `captureOutput` allows you to get the stdout, stderr, return value, and error of the callback you provide it. This makes it possible to assert that certain strings were printed to stdout and stderr or that the callback failed with the expected error or succeeded with the expected result. +**Options** + +- `print` - Print everything that goes to stdout and stderr. +- `stripAnsi` - Strip ansi codes from everything that goes to stdout and stderr. Defaults to true. + See the [tests](./test/capture-output.test.ts) for example usage. ### `runCommand` diff --git a/package.json b/package.json index 11e7903..52f61a0 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,11 @@ "author": "Salesforce", "bugs": "https://github.com/oclif/test/issues", "dependencies": { - "ansis": "^3.2.0", - "debug": "^4.3.4" + "debug": "^4.3.4", + "strip-ansi": "^7.1.0" }, "peerDependencies": { - "@oclif/core": "^4.0.0-beta.7" + "@oclif/core": ">= 3.0.0" }, "devDependencies": { "@commitlint/config-conventional": "^18.6.3", diff --git a/src/index.ts b/src/index.ts index 8061d17..d0ff4e4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,4 @@ import {Config, Errors, Interfaces, run} from '@oclif/core' -import ansis from 'ansis' import makeDebug from 'debug' import {dirname} from 'node:path' @@ -10,6 +9,13 @@ type CaptureOptions = { stripAnsi?: boolean } +type CaptureResult = { + error?: Error & Partial + result?: T + stderr: string + stdout: string +} + type MockedStdout = typeof process.stdout.write type MockedStderr = typeof process.stderr.write @@ -39,17 +45,20 @@ function makeLoadOptions(loadOpts?: Interfaces.LoadOptions): Interfaces.LoadOpti return loadOpts ?? {root: findRoot()} } -export async function captureOutput( - fn: () => Promise, - opts?: CaptureOptions, -): Promise<{ - error?: Error & Partial - result?: T - stderr: string - stdout: string -}> { +/** + * Capture the stderr and stdout output of a function + * @param fn async function to run + * @param opts options + * - print: Whether to print the output to the console + * - stripAnsi: Whether to strip ANSI codes from the output + * @returns {Promise>} Captured output + * - error: Error object if the function throws an error + * - result: Result of the function if it returns a value and succeeds + * - stderr: Captured stderr output + * - stdout: Captured stdout output + */ +export async function captureOutput(fn: () => Promise, opts?: CaptureOptions): Promise> { const print = opts?.print ?? false - const stripAnsi = opts?.stripAnsi ?? true const originals = { NODE_ENV: process.env.NODE_ENV, @@ -62,7 +71,8 @@ export async function captureOutput( stdout: [], } - const toString = (str: Uint8Array | string): string => (stripAnsi ? ansis.strip(str.toString()) : str.toString()) + const {default: stripAnsi} = opts?.stripAnsi ?? true ? await import('strip-ansi') : {default: (str: string) => str} + const toString = (str: Uint8Array | string): string => stripAnsi(str.toString()) const getStderr = (): string => output.stderr.map((b) => toString(b)).join('') const getStdout = (): string => output.stdout.map((b) => toString(b)).join('') @@ -108,16 +118,24 @@ export async function captureOutput( } } +/** + * Capture the stderr and stdout output of a command in your CLI + * @param args Command arguments, e.g. `['my:command', '--flag']` or `'my:command --flag'` + * @param loadOpts options for loading oclif `Config` + * @param captureOpts options for capturing the output + * - print: Whether to print the output to the console + * - stripAnsi: Whether to strip ANSI codes from the output + * @returns {Promise>} Captured output + * - error: Error object if the command throws an error + * - result: Result of the command if it returns a value and succeeds + * - stderr: Captured stderr output + * - stdout: Captured stdout output + */ export async function runCommand( args: string | string[], loadOpts?: Interfaces.LoadOptions, captureOpts?: CaptureOptions, -): Promise<{ - error?: Error & Partial - result?: T - stderr: string - stdout: string -}> { +): Promise> { const loadOptions = makeLoadOptions(loadOpts) const argsArray = (Array.isArray(args) ? args : [args]).join(' ').split(' ') @@ -130,17 +148,26 @@ export async function runCommand( return captureOutput(async () => run(finalArgs, loadOptions), captureOpts) } +/** + * Capture the stderr and stdout output of a hook in your CLI + * @param hook Hook name + * @param options options to pass to the hook + * @param loadOpts options for loading oclif `Config` + * @param captureOpts options for capturing the output + * - print: Whether to print the output to the console + * - stripAnsi: Whether to strip ANSI codes from the output + * @returns {Promise>} Captured output + * - error: Error object if the hook throws an error + * - result: Result of the hook if it returns a value and succeeds + * - stderr: Captured stderr output + * - stdout: Captured stdout output + */ export async function runHook( hook: string, options: Record, loadOpts?: Interfaces.LoadOptions, - recordOpts?: CaptureOptions, -): Promise<{ - error?: Error & Partial - result?: T - stderr: string - stdout: string -}> { + captureOpts?: CaptureOptions, +): Promise> { const loadOptions = makeLoadOptions(loadOpts) debug('loadOpts: %O', loadOptions) @@ -148,5 +175,5 @@ export async function runHook( return captureOutput(async () => { const config = await Config.load(loadOptions) return config.runHook(hook, options) - }, recordOpts) + }, captureOpts) } diff --git a/test/capture-output.test.ts b/test/capture-output.test.ts index b684f9d..9e70501 100644 --- a/test/capture-output.test.ts +++ b/test/capture-output.test.ts @@ -3,6 +3,8 @@ import {expect} from 'chai' import {captureOutput} from '../src' +const bold = (s: string) => `\u001B[1m${s}\u001B[22m` + class MyCommand extends Command { static flags = { channel: Flags.option({ @@ -20,11 +22,11 @@ class MyCommand extends Command { if (flags.throw) throw new Errors.CLIError('error', {exit: flags.throw}) if (flags.channel.includes('stdout')) { - this.log('hello world!') + this.log(bold('hello world!')) } if (flags.channel.includes('stderr')) { - this.logToStderr('hello world!') + this.logToStderr(bold('hello world!')) } return {success: true} @@ -66,4 +68,14 @@ describe('captureOutput', () => { const {error} = await captureOutput(async () => MyCommand.run(['-c=stdout', '--throw=101'])) expect(error?.oclif?.exit).to.equal(101) }) + + it('should strip ansi codes by default', async () => { + const {stdout} = await captureOutput(async () => MyCommand.run(['-c=stdout'])) + expect(stdout).to.equal('hello world!\n') + }) + + it('should not strip ansi codes if stripAnsi is false', async () => { + const {stdout} = await captureOutput(async () => MyCommand.run(['-c=stdout']), {stripAnsi: false}) + expect(stdout).to.equal('\u001B[1mhello world!\u001B[22m\n') + }) }) diff --git a/test/run-command.test.ts b/test/run-command.test.ts index 6d09fb0..5040ee7 100644 --- a/test/run-command.test.ts +++ b/test/run-command.test.ts @@ -1,3 +1,4 @@ +import {Config} from '@oclif/core' import {expect} from 'chai' import {join} from 'node:path' @@ -37,6 +38,18 @@ describe('runCommand', () => { expect(error?.message).to.equal('EEXIT: 101') expect(error?.oclif?.exit).to.equal(101) }) + + it('should take existing Config instance', async () => { + const config = await Config.load(root) + const {result, stdout} = await runCommand<{name: string}>(['foo:bar'], config) + expect(stdout).to.equal('hello world!\n') + expect(result?.name).to.equal('world') + }) + + it('should find root dynamically if not provided', async () => { + const {stdout} = await runCommand(['--help']) + expect(stdout).to.include('$ @oclif/test [COMMAND]') + }) }) describe('single command cli', () => { diff --git a/yarn.lock b/yarn.lock index a402f25..1f9a3d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -690,7 +690,7 @@ ansi-styles@^6.0.0, ansi-styles@^6.2.1: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== -ansis@^3.0.1, ansis@^3.2.0: +ansis@^3.0.1: version "3.2.0" resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.2.0.tgz#0e050c5be94784f32ffdac4b84fccba064aeae4b" integrity sha512-Yk3BkHH9U7oPyCN3gL5Tc7CpahG/+UFv/6UG03C311Vy9lzRmA5uoxDTpU9CO3rGHL6KzJz/pdDeXZCZ5Mu/Sg==