diff --git a/.commitlintrc.json b/.commitlintrc.json new file mode 100644 index 000000000..c30e5a970 --- /dev/null +++ b/.commitlintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["@commitlint/config-conventional"] +} diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index beffa3084..000000000 --- a/.editorconfig +++ /dev/null @@ -1,11 +0,0 @@ -root = true - -[*] -indent_style = space -indent_size = 2 -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.md] -trim_trailing_whitespace = false diff --git a/.eslintrc.json b/.eslintrc.json index 2887d4e87..5971db05c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,8 @@ { "extends": [ "oclif", - "oclif-typescript" + "oclif-typescript", + "prettier" ], "rules": { "sort-imports": "error", diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml index 796eafae2..b61a8843d 100644 --- a/.github/workflows/automerge.yml +++ b/.github/workflows/automerge.yml @@ -2,9 +2,10 @@ name: automerge on: workflow_dispatch: schedule: - - cron: '17 2,5,8,11 * * *' + - cron: "17 2,5,8,11 * * *" jobs: automerge: - uses: oclif/github-workflows/.github/workflows/automerge.yml@main - secrets: inherit + uses: salesforcecli/github-workflows/.github/workflows/automerge.yml@main + secrets: + SVC_CLI_BOT_GITHUB_TOKEN: ${{ secrets.SVC_CLI_BOT_GITHUB_TOKEN }} diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..4abc587ad --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +yarn lint-staged --concurrent false diff --git a/.lintstagedrc.json b/.lintstagedrc.json new file mode 100644 index 000000000..c31470780 --- /dev/null +++ b/.lintstagedrc.json @@ -0,0 +1,5 @@ +{ + "*.json": ["prettier --write"], + "*.md": ["prettier --write"], + "+(src|test)/**/*.+(ts|js)": ["eslint --fix", "prettier --write"] +} diff --git a/.nycrc.json b/.nycrc.json new file mode 100644 index 000000000..d6c706920 --- /dev/null +++ b/.nycrc.json @@ -0,0 +1,11 @@ +{ + "check-coverage": true, + "lines": 80, + "statements": 70, + "functions": 70, + "branches": 60, + "reporter": ["lcov", "text"], + "extension": [".ts"], + "include": ["**/*.ts"], + "exclude": ["**/*.d.ts", "test/**"] +} diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 000000000..631433570 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1 @@ +"@oclif/prettier-config" diff --git a/commitlint.config.js b/commitlint.config.js deleted file mode 100644 index 28fe5c5bf..000000000 --- a/commitlint.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = {extends: ['@commitlint/config-conventional']} diff --git a/guides/PRE_CORE_MIGRATION.md b/guides/PRE_CORE_MIGRATION.md index 16a6fdc20..649af2cda 100644 --- a/guides/PRE_CORE_MIGRATION.md +++ b/guides/PRE_CORE_MIGRATION.md @@ -5,6 +5,7 @@ Migrating to `@oclif/core` from the deprecated oclif libraries (`@oclif/config`, - [Migrating to @oclif/core from deprecated libraries](#migrating-to-oclifcore-from-deprecated-libraries) - [Update Imports](#update-imports) + - [Update Command Args](#update-command-args) - [Update your bin scripts](#update-your-bin-scripts) - [Add `main` to your package.json](#add-main-to-your-packagejson) - [Restore `-h`, `-v`, and `version`](#restore--h--v-and-version) @@ -30,6 +31,50 @@ With this import: import {Command, Flags, Topic, Help} from '@oclif/core'; ``` +## Update Command Args + +We updated the `Command.args` to more closely resemble flags + +**Before** + +```typescript +import { Command } from '@oclif/core' + +export default MyCommand extends Command { + static args = [{name: arg1, description: 'an argument', required: true}] + + public async run(): Promise { + const {args} = await this.parse(MyCommand) // args is useless {[name: string]: any} + } +} +``` + +**After** + +```typescript +import { Command, Args } from '@oclif/core' + +export default MyCommand extends Command { + static args = { + arg1: Args.string({description: 'an argument', required: true}) + } + + public async run(): Promise { + const {args} = await this.parse(MyCommand) // args is { arg1: string } + } +} +``` + +These are the available Args: +- string +- integer +- boolean +- url +- file +- directory +- custom + + ## Update your bin scripts `@oclif/core` now supports separate bin scripts for production and development. diff --git a/guides/V3_MIGRATION.md b/guides/V3_MIGRATION.md index 9b83abc6f..c6ca67632 100644 --- a/guides/V3_MIGRATION.md +++ b/guides/V3_MIGRATION.md @@ -1,26 +1,28 @@ -Migrating to @oclif/core@V3 -============== +# Migrating to @oclif/core@V3 - [Migrating to @oclif/core@V3](#migrating-to-oclifcorev3) - [BREAKING CHANGES โ—](#breaking-changes-) - [Dropping node 14 and node 16 support](#dropping-node-14-and-node-16-support) - [Bin Scripts for ESM/CJS Interoperability](#bin-scripts-for-esmcjs-interoperability) + - [Dropped `ts-node` as a dependency](#dropped-ts-node-as-a-dependency) - [`Config.plugins`](#configplugins) - [Readonly properties on `Config`](#readonly-properties-on-config) - [Private methods on `Plugin`](#private-methods-on-plugin) - [`global['cli-ux']` -\> `global.ux`](#globalcli-ux---globalux) - [`handle`](#handle) - [`noCacheDefault` flag property replaces `isWritingManifest`](#nocachedefault-flag-property-replaces-iswritingmanifest) + - [Removed Unnecessary Exports](#removed-unnecessary-exports) - [Features ๐ŸŽ‰](#features-) - - [Cache Flexible taxonomy Command Permutations](#cache-flexible-taxonomy-command-permutations) + - [Performance Improvements](#performance-improvements) - [charAliases Flag Property](#charaliases-flag-property) - [Flags.option](#flagsoption) - + - [Set spinner styles](#set-spinner-styles) ## BREAKING CHANGES โ— ### Dropping node 14 and node 16 support - The end-of-life date for Node.js 14 was [April 30, 2023](https://nodejs.org/en/about/releases/). + +The end-of-life date for Node.js 14 was [April 30, 2023](https://nodejs.org/en/about/releases/). The end-of-life date for Node.js 16 was [September 11, 2023](https://nodejs.org/en/about/releases/). This date is earlier than previously published. Node.jsโ€™s [blog](https://nodejs.org/en/blog/announcements/nodejs16-eol/) explains why they chose this earlier end-of-life date. @@ -36,44 +38,23 @@ In order to support ESM and CommonJS plugin interoperability you will need to up If you'd like to migrate your plugin to ESM, please read our guide [here](https://oclif.io/docs/esm) +### Dropped `ts-node` as a dependency + +We removed `ts-node` as a dependency to reduce the package size. By doing this, it means that linked plugin **must** have `ts-node` as a `devDependency` in order for auto-transpilation to work. + ### `Config.plugins` + `Config.plugins` is now a `Map` where the keys are the plugin names and the values are the loaded `Plugin` instances. Previously it was an array of loaded `Plugin` instances. By using a `Map` we can now do more efficient lookups during command execution. `Config.getPluginsList` was added in case you still would like a flat array of `Plugin` instances. ### Readonly properties on `Config` -Various properties on `Config` are now `readonly` - - `name` - - `version` - - `channel` - - `pjson` - - `root` - - `arch` - - `bin` - - `cacheDir` - - `configDir` - - `dataDir` - - `dirname` - - `errLog` - - `home` - - `platform` - - `shell` - - `userAgent` - - `windows` - - `debug` - - `npmRegistry` - - `userPJSON` - - `plugins` - - `binPath` - - `binAliases` - - `nsisCustomization` - - `valid` - - `flexibleTaxonomy` - - `commands` + +Various properties on `Config` are now `readonly` - `name` - `version` - `channel` - `pjson` - `root` - `arch` - `bin` - `cacheDir` - `configDir` - `dataDir` - `dirname` - `errLog` - `home` - `platform` - `shell` - `userAgent` - `windows` - `debug` - `npmRegistry` - `userPJSON` - `plugins` - `binPath` - `binAliases` - `nsisCustomization` - `valid` - `flexibleTaxonomy` - `commands` ### Private methods on `Plugin` -The `_manifest` and `warn` methods on `Plugin` are now `private` +The `_manifest` and `warn` methods on `Plugin` are now `private` ### `global['cli-ux']` -> `global.ux` @@ -91,12 +72,12 @@ Version 2 allowed you to optionally return non-sensitive input if the `default` export const mySensitiveFlag = Flags.string({ default: async (context, isWritingManifest) => { if (isWritingManifest) { - return undefined; + return undefined } return 'sensitive info' }, -}); +}) ``` Version 3 removes the `isWritingManifest` parameter in favor of a flag and arg property, `noCacheDefault`. Setting it to true will automatically keep it from being cached in the manifest. @@ -107,15 +88,24 @@ export const mySensitiveFlag = Flags.string({ default: async (context) => { return 'sensitive info' }, -}); +}) ``` +### Removed Unnecessary Exports + +The following exports have been removed: + +- `toCached` +- `tsPath` ## Features ๐ŸŽ‰ -### Cache Flexible taxonomy Command Permutations +### Performance Improvements -The command permutations for flexible taxonomy are now cached in the oclif.manifest.json allowing for quicker startup times. +- Cache command permutations for flexible taxonomy in the `oclif.manifest.json` +- Cache additional command properties (`isESM`, `relativePath`) in the `oclif.manifest.json` +- Improved accuracy in the `DEBUG=perf` output. +- Remove `ts-node` from `dependencies` to reduce the package size. ### charAliases Flag Property @@ -149,3 +139,15 @@ export default class MyCommand extends Command { } } ``` + +### Set spinner styles + +You can now configure the style of the spinner when using `ux.action.start`. See [spinners](https://github.com/oclif/core/blob/main/src/cli-ux/action/spinners.ts) for all the different options. + +```typescript +ux.action.start('starting spinner', 'spinning', {style: 'arc'}) +await ux.wait(2500) +ux.action.status = 'still going' +await ux.wait(2500) +ux.action.stop() +``` diff --git a/package.json b/package.json index 1c62c19fd..bee342bf0 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,10 @@ { "name": "@oclif/core", "description": "base library for oclif CLIs", - "version": "3.0.0-beta.17", + "version": "3.0.0-beta.22", "author": "Salesforce", "bugs": "https://github.com/oclif/core/issues", "dependencies": { - "@types/cli-progress": "^3.11.0", "ansi-escapes": "^4.3.2", "ansi-styles": "^4.3.0", "cardinal": "^2.1.1", @@ -33,22 +32,23 @@ "wrap-ansi": "^7.0.0" }, "devDependencies": { - "@commitlint/config-conventional": "^12.1.4", + "@commitlint/config-conventional": "^17.7.0", "@oclif/plugin-help": "^5.2.8", "@oclif/plugin-plugins": "^3.3.0", - "@oclif/test": "^2.4.7", + "@oclif/prettier-config": "^0.2.1", + "@oclif/test": "^3.0.1", "@types/ansi-styles": "^3.2.1", "@types/benchmark": "^2.1.2", - "@types/chai": "^4.3.4", "@types/chai-as-promised": "^7.1.5", + "@types/chai": "^4.3.4", "@types/clean-stack": "^2.1.1", + "@types/cli-progress": "^3.11.0", "@types/ejs": "^3.1.2", "@types/indent-string": "^4.0.1", "@types/js-yaml": "^3.12.7", - "@types/mocha": "^8.2.3", - "@types/nock": "^11.1.0", - "@types/node": "^18", + "@types/mocha": "^10.0.2", "@types/node-notifier": "^8.0.2", + "@types/node": "^18", "@types/slice-ansi": "^4.0.0", "@types/strip-ansi": "^5.2.1", "@types/supports-color": "^8.1.1", @@ -57,16 +57,20 @@ "benchmark": "^2.1.4", "chai": "^4.3.7", "chai-as-promised": "^7.1.1", - "commitlint": "^12.1.4", + "commitlint": "^17.7.2", "cross-env": "^7.0.3", "eslint": "^8.50.0", "eslint-config-oclif": "^5.0.0", "eslint-config-oclif-typescript": "^2.0.1", - "fancy-test": "^3.0.0-beta.2", + "eslint-config-prettier": "^9.0.0", + "fancy-test": "^3.0.1", "globby": "^11.1.0", - "husky": "6", + "husky": "^8", + "lint-staged": "^14.0.1", + "madge": "^6.1.0", "mocha": "^10.2.0", - "nock": "^13.3.0", + "nyc": "^15.1.0", + "prettier": "^3.0.3", "shx": "^0.3.4", "sinon": "^11.1.2", "ts-node": "^10.9.1", @@ -104,14 +108,16 @@ "build": "shx rm -rf lib && tsc", "commitlint": "commitlint", "compile": "tsc", + "format": "prettier --write \"+(src|test)/**/*.+(ts|js|json)\"", "lint": "eslint . --ext .ts", - "posttest": "yarn lint", + "posttest": "yarn lint && yarn test:circular-deps", "prepack": "yarn run build", - "pretest": "yarn build --noEmit && tsc -p test --noEmit --skipLibCheck", + "pretest": "yarn build && tsc -p test --noEmit --skipLibCheck", + "test:circular-deps": "madge lib/ -c", "test:e2e": "mocha --forbid-only \"test/**/*.e2e.ts\" --parallel --timeout 1200000", "test:esm-cjs": "cross-env DEBUG=e2e:* ts-node test/integration/esm-cjs.ts", "test:perf": "ts-node test/perf/parser.perf.ts", - "test": "mocha --forbid-only \"test/**/*.test.ts\"" + "test": "nyc mocha --forbid-only \"test/**/*.test.ts\"" }, "types": "lib/index.d.ts" } diff --git a/src/args.ts b/src/args.ts index 861bf8954..09a9fca1d 100644 --- a/src/args.ts +++ b/src/args.ts @@ -1,8 +1,8 @@ - import {Arg, ArgDefinition} from './interfaces/parser' -import {dirExists, fileExists, isNotFalsy} from './util' +import {dirExists, fileExists} from './util/fs' import {Command} from './command' import {URL} from 'node:url' +import {isNotFalsy} from './util/util' /** * Create a custom arg. @@ -33,13 +33,12 @@ export function custom>(defaults: Partial({ - parse: async b => Boolean(b) && isNotFalsy(b), + parse: async (b) => Boolean(b) && isNotFalsy(b), }) -export const integer = custom({ +export const integer = custom({ async parse(input, _, opts) { - if (!/^-?\d+$/.test(input)) - throw new Error(`Expected an integer but received: ${input}`) + if (!/^-?\d+$/.test(input)) throw new Error(`Expected an integer but received: ${input}`) const num = Number.parseInt(input, 10) if (opts.min !== undefined && num < opts.min) throw new Error(`Expected an integer greater than or equal to ${opts.min} but received: ${input}`) diff --git a/src/cli-ux/action/base.ts b/src/cli-ux/action/base.ts index a29bfc809..e06b9f803 100644 --- a/src/cli-ux/action/base.ts +++ b/src/cli-ux/action/base.ts @@ -1,12 +1,12 @@ import {stderr, stdout} from '../stream' import {Options} from './types' -import {castArray} from '../../util' +import {castArray} from '../../util/util' import {inspect} from 'node:util' export interface ITask { - action: string; - status: string | undefined; - active: boolean; + action: string + status: string | undefined + active: boolean } export type ActionType = 'spinner' | 'simple' | 'debug' @@ -45,8 +45,8 @@ export class ActionBase { this._stdout(false) } - private get globals(): { action: { task?: ITask }; output: string | undefined } { - (global as any).ux = (global as any).ux || {} + private get globals(): {action: {task?: ITask}; output: string | undefined} { + ;(global as any).ux = (global as any).ux || {} const globals = (global as any).ux globals.action = globals.action || {} return globals @@ -201,19 +201,19 @@ export class ActionBase { // write to the real stdout/stderr protected _write(std: 'stdout' | 'stderr', s: string | string[]): void { switch (std) { - case 'stdout': { - this.stdmockOrigs.stdout.apply(stdout, castArray(s) as [string]) - break - } + case 'stdout': { + this.stdmockOrigs.stdout.apply(stdout, castArray(s) as [string]) + break + } - case 'stderr': { - this.stdmockOrigs.stderr.apply(stderr, castArray(s) as [string]) - break - } + case 'stderr': { + this.stdmockOrigs.stderr.apply(stderr, castArray(s) as [string]) + break + } - default: { - throw new Error(`invalid std: ${std}`) - } + default: { + throw new Error(`invalid std: ${std}`) + } } } } diff --git a/src/cli-ux/action/spinner.ts b/src/cli-ux/action/spinner.ts index d462514b3..b85caf7f3 100644 --- a/src/cli-ux/action/spinner.ts +++ b/src/cli-ux/action/spinner.ts @@ -7,6 +7,8 @@ import {errtermwidth} from '../../screen' import spinners from './spinners' import stripAnsi from 'strip-ansi' +const ansiEscapes = require('ansi-escapes') + function color(s: string): string { if (!supportsColor) return s const has256 = supportsColor.stdout ? supportsColor.stdout.has256 : (process.env.TERM || '').includes('256') @@ -34,10 +36,10 @@ export default class SpinnerAction extends ActionBase { this._reset() if (this.spinner) clearInterval(this.spinner) this._render() - this.spinner = setInterval(icon => - this._render.bind(this)(icon), - process.platform === 'win32' ? 500 : 100, - 'spinner', + this.spinner = setInterval( + (icon) => this._render.bind(this)(icon), + process.platform === 'win32' ? 500 : 100, + 'spinner', ) const interval = this.spinner interval.unref() @@ -64,7 +66,7 @@ export default class SpinnerAction extends ActionBase { } private getFrames(opts?: Options) { - if (opts?.style) return spinners[process.platform === 'win32' ? 'line' : opts.style].frames + if (opts?.style) return spinners[opts.style].frames return spinners[process.platform === 'win32' ? 'line' : 'dots2'].frames } @@ -82,15 +84,12 @@ export default class SpinnerAction extends ActionBase { private _reset() { if (!this.output) return - const ansiEscapes = require('ansi-escapes') const lines = this._lines(this.output) this._write(this.std, ansiEscapes.cursorLeft + ansiEscapes.cursorUp(lines) + ansiEscapes.eraseDown) this.output = undefined } private _lines(s: string): number { - return (stripAnsi(s).split('\n') as any[]) - .map(l => Math.ceil(l.length / errtermwidth)) - .reduce((c, i) => c + i, 0) + return (stripAnsi(s).split('\n') as any[]).map((l) => Math.ceil(l.length / errtermwidth)).reduce((c, i) => c + i, 0) } } diff --git a/src/cli-ux/action/spinners.ts b/src/cli-ux/action/spinners.ts index d38cc75f7..e7ba12bad 100644 --- a/src/cli-ux/action/spinners.ts +++ b/src/cli-ux/action/spinners.ts @@ -157,7 +157,7 @@ export default { }, flip: { interval: 70, - frames: ['_', '_', '_', '-', '`', '`', '\'', 'ยด', '-', '_', '_', '_'], + frames: ['_', '_', '_', '-', '`', '`', "'", 'ยด', '-', '_', '_', '_'], }, hamburger: { interval: 100, diff --git a/src/cli-ux/action/types.ts b/src/cli-ux/action/types.ts index 0e5eb9179..7fe562ed1 100644 --- a/src/cli-ux/action/types.ts +++ b/src/cli-ux/action/types.ts @@ -1,6 +1,6 @@ import spinners from './spinners' export type Options = { - stdout?: boolean; - style?: keyof typeof spinners; + stdout?: boolean + style?: keyof typeof spinners } diff --git a/src/cli-ux/config.ts b/src/cli-ux/config.ts index 5718f9775..c6d8780c0 100644 --- a/src/cli-ux/config.ts +++ b/src/cli-ux/config.ts @@ -1,26 +1,26 @@ import {ActionBase} from './action/base' import {PJSON} from '../interfaces/pjson' -import {requireJson} from '../util' +import {requireJson} from '../util/fs' import simple from './action/simple' import spinner from './action/spinner' export type Levels = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' export interface ConfigMessage { - type: 'config'; - prop: string; - value: any; + type: 'config' + prop: string + value: any } const g: any = global const globals = g.ux || (g.ux = {}) -const actionType = ( - Boolean(process.stderr.isTTY) - && !process.env.CI - && !['dumb', 'emacs-color'].includes(process.env.TERM!) - && 'spinner' -) || 'simple' +const actionType = + (Boolean(process.stderr.isTTY) && + !process.env.CI && + !['dumb', 'emacs-color'].includes(process.env.TERM!) && + 'spinner') || + 'simple' const Action = actionType === 'spinner' ? spinner : simple diff --git a/src/cli-ux/exit.ts b/src/cli-ux/exit.ts index a7ce32f27..8d154caef 100644 --- a/src/cli-ux/exit.ts +++ b/src/cli-ux/exit.ts @@ -1,6 +1,6 @@ export class ExitError extends Error { public ux: { - exit: number; + exit: number } public code: 'EEXIT' diff --git a/src/cli-ux/flush.ts b/src/cli-ux/flush.ts index c6a0c152d..63713eea1 100644 --- a/src/cli-ux/flush.ts +++ b/src/cli-ux/flush.ts @@ -1,18 +1,19 @@ -import {Errors, stdout} from '..' +import {error} from '../errors' +import {stdout} from './stream' function timeout(p: Promise, ms: number) { function wait(ms: number, unref = false) { - return new Promise(resolve => { + return new Promise((resolve) => { const t: any = setTimeout(() => resolve(null), ms) if (unref) t.unref() }) } - return Promise.race([p, wait(ms, true).then(() => Errors.error('timed out'))]) + return Promise.race([p, wait(ms, true).then(() => error('timed out'))]) } async function _flush() { - const p = new Promise(resolve => { + const p = new Promise((resolve) => { stdout.once('drain', () => resolve(null)) }) const flushed = stdout.write('') diff --git a/src/cli-ux/global.d.ts b/src/cli-ux/global.d.ts index 08e1e9e13..503b2265d 100644 --- a/src/cli-ux/global.d.ts +++ b/src/cli-ux/global.d.ts @@ -1,5 +1,5 @@ declare namespace NodeJS { interface Global { - ux: any; + ux: any } } diff --git a/src/cli-ux/index.ts b/src/cli-ux/index.ts index 1b7811f7a..94d9e5446 100644 --- a/src/cli-ux/index.ts +++ b/src/cli-ux/index.ts @@ -1,4 +1,3 @@ - import * as Errors from '../errors' import * as styled from './styled' import * as uxPrompt from './prompt' @@ -42,8 +41,16 @@ export class ux { this.info(chalk.dim('=== ') + chalk.bold(header) + '\n') } - public static get styledJSON(): typeof styled.styledJSON { - return styled.styledJSON + public static styledJSON(obj: unknown): void { + const json = JSON.stringify(obj, null, 2) + if (!chalk.level) { + info(json) + return + } + + const cardinal = require('cardinal') + const theme = require('cardinal/themes/jq') + this.info(cardinal.highlight(json, {json: true, theme})) } public static get table(): typeof styled.Table.table { @@ -169,7 +176,7 @@ const uxProcessExitHandler = async () => { // to avoid MaxListenersExceededWarning // only attach named listener once -const uxListener = process.listeners('exit').find(fn => fn.name === uxProcessExitHandler.name) +const uxListener = process.listeners('exit').find((fn) => fn.name === uxProcessExitHandler.name) if (!uxListener) { process.once('exit', uxProcessExitHandler) } diff --git a/src/cli-ux/list.ts b/src/cli-ux/list.ts index 46773cb7d..e1f985933 100644 --- a/src/cli-ux/list.ts +++ b/src/cli-ux/list.ts @@ -1,4 +1,4 @@ -import {maxBy} from '../util' +import {maxBy} from '../util/util' import {stdtermwidth} from '../screen' const wordwrap = require('wordwrap') @@ -15,8 +15,8 @@ export function renderList(items: IListItem[]): string { return '' } - const maxLength = maxBy(items, item => item[0].length)?.[0].length ?? 0 - const lines = items.map(i => { + const maxLength = maxBy(items, (item) => item[0].length)?.[0].length ?? 0 + const lines = items.map((i) => { let left = i[0] let right = i[1] if (!right) { diff --git a/src/cli-ux/prompt.ts b/src/cli-ux/prompt.ts index 9af137f84..a218e1231 100644 --- a/src/cli-ux/prompt.ts +++ b/src/cli-ux/prompt.ts @@ -4,24 +4,24 @@ import {config} from './config' import {stderr} from './stream' export interface IPromptOptions { - prompt?: string; - type?: 'normal' | 'mask' | 'hide' | 'single'; - timeout?: number; + prompt?: string + type?: 'normal' | 'mask' | 'hide' | 'single' + timeout?: number /** * Requires user input if true, otherwise allows empty input */ - required?: boolean; - default?: string; + required?: boolean + default?: string } interface IPromptConfig { - name: string; - prompt: string; - type: 'normal' | 'mask' | 'hide' | 'single'; - isTTY: boolean; - required: boolean; - default?: string; - timeout?: number; + name: string + prompt: string + type: 'normal' | 'mask' | 'hide' | 'single' + isTTY: boolean + required: boolean + default?: string + timeout?: number } function normal(options: IPromptConfig, retries = 100): Promise { @@ -39,14 +39,14 @@ function normal(options: IPromptConfig, retries = 100): Promise { process.stdin.setEncoding('utf8') stderr.write(options.prompt) process.stdin.resume() - process.stdin.once('data', b => { + process.stdin.once('data', (b) => { if (timer) clearTimeout(timer) process.stdin.pause() const data: string = (typeof b === 'string' ? b : b.toString()).trim() if (!options.default && options.required && data === '') { resolve(normal(options, retries - 1)) } else { - resolve(data || options.default as string) + resolve(data || (options.default as string)) } }) }) @@ -76,8 +76,15 @@ async function single(options: IPromptConfig): Promise { function replacePrompt(prompt: string) { const ansiEscapes = require('ansi-escapes') - stderr.write(ansiEscapes.cursorHide + ansiEscapes.cursorUp(1) + ansiEscapes.cursorLeft + prompt - + ansiEscapes.cursorDown(1) + ansiEscapes.cursorLeft + ansiEscapes.cursorShow) + stderr.write( + ansiEscapes.cursorHide + + ansiEscapes.cursorUp(1) + + ansiEscapes.cursorLeft + + prompt + + ansiEscapes.cursorDown(1) + + ansiEscapes.cursorLeft + + ansiEscapes.cursorShow, + ) } async function _prompt(name: string, inputOptions: Partial = {}): Promise { @@ -94,36 +101,36 @@ async function _prompt(name: string, inputOptions: Partial = {}) const passwordPrompt = require('password-prompt') switch (options.type) { - case 'normal': { - return normal(options) - } + case 'normal': { + return normal(options) + } - case 'single': { - return single(options) - } + case 'single': { + return single(options) + } - case 'mask': { - return passwordPrompt(options.prompt, { - method: options.type, - required: options.required, - default: options.default, - }).then((value: string) => { - replacePrompt(getPrompt(name, 'hide', inputOptions.default)) - return value - }) - } + case 'mask': { + return passwordPrompt(options.prompt, { + method: options.type, + required: options.required, + default: options.default, + }).then((value: string) => { + replacePrompt(getPrompt(name, 'hide', inputOptions.default)) + return value + }) + } - case 'hide': { - return passwordPrompt(options.prompt, { - method: options.type, - required: options.required, - default: options.default, - }) - } + case 'hide': { + return passwordPrompt(options.prompt, { + method: options.type, + required: options.required, + default: options.default, + }) + } - default: { - throw new Error(`unexpected type ${options.type}`) - } + default: { + throw new Error(`unexpected type ${options.type}`) + } } } diff --git a/src/cli-ux/styled/index.ts b/src/cli-ux/styled/index.ts index 93ce7faa2..dd83bbe9e 100644 --- a/src/cli-ux/styled/index.ts +++ b/src/cli-ux/styled/index.ts @@ -1,6 +1,4 @@ - export * as Table from './table' export {default as progress} from './progress' -export {default as styledJSON} from './json' export {default as styledObject} from './object' export {default as tree} from './tree' diff --git a/src/cli-ux/styled/json.ts b/src/cli-ux/styled/json.ts deleted file mode 100644 index c7db532d0..000000000 --- a/src/cli-ux/styled/json.ts +++ /dev/null @@ -1,15 +0,0 @@ -import chalk from 'chalk' - -import {ux} from '../../index' - -export default function styledJSON(obj: unknown): void { - const json = JSON.stringify(obj, null, 2) - if (!chalk.level) { - ux.info(json) - return - } - - const cardinal = require('cardinal') - const theme = require('cardinal/themes/jq') - ux.info(cardinal.highlight(json, {json: true, theme})) -} diff --git a/src/cli-ux/styled/object.ts b/src/cli-ux/styled/object.ts index 96345c016..272f66bcc 100644 --- a/src/cli-ux/styled/object.ts +++ b/src/cli-ux/styled/object.ts @@ -3,20 +3,21 @@ import {inspect} from 'node:util' export default function styledObject(obj: any, keys?: string[]): string { const output: string[] = [] - const keyLengths = Object.keys(obj).map(key => key.toString().length) + const keyLengths = Object.keys(obj).map((key) => key.toString().length) const maxKeyLength = Math.max(...keyLengths) + 2 function pp(obj: any) { if (typeof obj === 'string' || typeof obj === 'number') return obj if (typeof obj === 'object') { return Object.keys(obj) - .map(k => k + ': ' + inspect(obj[k])) - .join(', ') + .map((k) => k + ': ' + inspect(obj[k])) + .join(', ') } return inspect(obj) } - const logKeyValue = (key: string, value: any): string => `${chalk.blue(key)}:` + ' '.repeat(maxKeyLength - key.length - 1) + pp(value) + const logKeyValue = (key: string, value: any): string => + `${chalk.blue(key)}:` + ' '.repeat(maxKeyLength - key.length - 1) + pp(value) for (const key of keys || Object.keys(obj).sort()) { const value = obj[key] diff --git a/src/cli-ux/styled/table.ts b/src/cli-ux/styled/table.ts index dc4f7f7d6..94c58b488 100644 --- a/src/cli-ux/styled/table.ts +++ b/src/cli-ux/styled/table.ts @@ -1,6 +1,6 @@ import * as F from '../../flags' import * as Interfaces from '../../interfaces' -import {capitalize, sumBy} from '../../util' +import {capitalize, sumBy} from '../../util/util' import chalk from 'chalk' import {inspect} from 'node:util' import {orderBy} from 'natural-orderby' @@ -11,11 +11,15 @@ import {stdtermwidth} from '../../screen' import sw from 'string-width' class Table> { - options: table.Options & { printLine(s: any): any } + options: table.Options & {printLine(s: any): any} - columns: (table.Column & { key: string; width?: number; maxWidth?: number })[] + columns: (table.Column & {key: string; width?: number; maxWidth?: number})[] - constructor(private data: T[], columns: table.Columns, options: table.Options = {}) { + constructor( + private data: T[], + columns: table.Columns, + options: table.Options = {}, + ) { // assign columns this.columns = Object.keys(columns).map((key: string) => { const col = columns[key] @@ -52,7 +56,7 @@ class Table> { display() { // build table rows from input array data - let rows = this.data.map(d => { + let rows = this.data.map((d) => { const row: any = {} for (const col of this.columns) { let val = col.get(d) @@ -81,9 +85,9 @@ class Table> { // sort rows if (this.options.sort) { const sorters = this.options.sort!.split(',') - const sortHeaders = sorters.map(k => k[0] === '-' ? k.slice(1) : k) - const sortKeys = this.filterColumnsFromHeaders(sortHeaders).map(c => ((v: any) => v[c.key])) - const sortKeysOrder = sorters.map(k => k[0] === '-' ? 'desc' : 'asc') + const sortHeaders = sorters.map((k) => (k[0] === '-' ? k.slice(1) : k)) + const sortKeys = this.filterColumnsFromHeaders(sortHeaders).map((c) => (v: any) => v[c.key]) + const sortKeysOrder = sorters.map((k) => (k[0] === '-' ? 'desc' : 'asc')) rows = orderBy(rows, sortKeys, sortKeysOrder) } @@ -93,43 +97,47 @@ class Table> { this.columns = this.filterColumnsFromHeaders(filters) } else if (!this.options.extended) { // show extented columns/properties - this.columns = this.columns.filter(c => !c.extended) + this.columns = this.columns.filter((c) => !c.extended) } this.data = rows switch (this.options.output) { - case 'csv': { - this.outputCSV() - break - } + case 'csv': { + this.outputCSV() + break + } - case 'json': { - this.outputJSON() - break - } + case 'json': { + this.outputJSON() + break + } - case 'yaml': { - this.outputYAML() - break - } + case 'yaml': { + this.outputYAML() + break + } - default: { - this.outputTable() - } + default: { + this.outputTable() + } } } - private findColumnFromHeader(header: string): (table.Column & { key: string; width?: number; maxWidth?: number }) | undefined { - return this.columns.find(c => c.header.toLowerCase() === header.toLowerCase()) + private findColumnFromHeader( + header: string, + ): (table.Column & {key: string; width?: number; maxWidth?: number}) | undefined { + return this.columns.find((c) => c.header.toLowerCase() === header.toLowerCase()) } - private filterColumnsFromHeaders(filters: string[]): (table.Column & { key: string; width?: number; maxWidth?: number })[] { + private filterColumnsFromHeaders( + filters: string[], + ): (table.Column & {key: string; width?: number; maxWidth?: number})[] { // unique - filters = [...(new Set(filters))] + filters = [...new Set(filters)] const cols: (table.Column & {key: string; width?: number; maxWidth?: number})[] = [] for (const f of filters) { - const c = this.columns.find(c => c.header.toLowerCase() === f.toLowerCase()) + const c = this.columns.find((c) => c.header.toLowerCase() === f.toLowerCase()) if (c) cols.push(c) } @@ -137,17 +145,16 @@ class Table> { } private getCSVRow(d: any): string[] { - const values = this.columns.map(col => d[col.key] || '') - const lineToBeEscaped = values.find((e: string) => e.includes('"') || e.includes('\n') || e.includes('\r\n') || e.includes('\r') || e.includes(',')) - return values.map(e => lineToBeEscaped ? `"${e.replace('"', '""')}"` : e) + const values = this.columns.map((col) => d[col.key] || '') + const lineToBeEscaped = values.find( + (e: string) => e.includes('"') || e.includes('\n') || e.includes('\r\n') || e.includes('\r') || e.includes(','), + ) + return values.map((e) => (lineToBeEscaped ? `"${e.replace('"', '""')}"` : e)) } private resolveColumnsToObjectArray() { const {data, columns} = this - return data.map((d: any) => - - Object.fromEntries(columns.map(col => [col.key, d[col.key] ?? ''])), - ) + return data.map((d: any) => Object.fromEntries(columns.map((col) => [col.key, d[col.key] ?? '']))) } private outputJSON() { @@ -162,7 +169,7 @@ class Table> { const {data, columns, options} = this if (!options['no-header']) { - options.printLine(columns.map(c => c.header).join(',')) + options.printLine(columns.map((c) => c.header).join(',')) } for (const d of data) { @@ -176,7 +183,7 @@ class Table> { // column truncation // // find max width for each column - const columns = this.columns.map(c => { + const columns = this.columns.map((c) => { const maxWidth = Math.max(sw('.'.padEnd(c.minWidth! - 1)), sw(c.header), getWidestColumnWith(data, c.key)) + 1 return { ...c, @@ -193,7 +200,7 @@ class Table> { if (options['no-truncate'] || (!stdout.isTTY && !process.env.CLI_UX_SKIP_TTY_CHECK)) return // don't shorten if there is enough screen width - const dataMaxWidth = sumBy(columns, c => c.width!) + const dataMaxWidth = sumBy(columns, (c) => c.width!) const overWidth = dataMaxWidth - maxWidth if (overWidth <= 0) return @@ -205,15 +212,17 @@ class Table> { // if sum(minWidth's) is greater than term width // nothing can be done so // display all as minWidth - const dataMinWidth = sumBy(columns, c => c.minWidth!) + const dataMinWidth = sumBy(columns, (c) => c.minWidth!) if (dataMinWidth >= maxWidth) return // some wiggle room left, add it back to "needy" columns let wiggleRoom = maxWidth - dataMinWidth - const needyCols = columns.map(c => ({key: c.key, needs: c.maxWidth! - c.width!})).sort((a, b) => a.needs - b.needs) + const needyCols = columns + .map((c) => ({key: c.key, needs: c.maxWidth! - c.width!})) + .sort((a, b) => a.needs - b.needs) for (const {key, needs} of needyCols) { if (!needs) continue - const col = columns.find(c => key === c.key) + const col = columns.find((c) => key === c.key) if (!col) continue if (wiggleRoom > needs) { col.width = col.width! + needs @@ -231,7 +240,12 @@ class Table> { if (options.title) { options.printLine(options.title) // print title divider - options.printLine(''.padEnd(columns.reduce((sum, col) => sum + col.width!, 1), '=')) + options.printLine( + ''.padEnd( + columns.reduce((sum, col) => sum + col.width!, 1), + '=', + ), + ) options.rowStart = '| ' } @@ -280,9 +294,9 @@ class Table> { let d = (row as any)[col.key] d = d.split('\n')[i] || '' const visualWidth = sw(d) - const colorWidth = (d.length - visualWidth) + const colorWidth = d.length - visualWidth let cell = d.padEnd(width + colorWidth) - if ((cell.length - colorWidth) > width || visualWidth === width) { + if (cell.length - colorWidth > width || visualWidth === width) { // truncate the cell, preserving ANSI escape sequences, and keeping // into account the width of fullwidth unicode characters cell = sliceAnsi(cell, 0, width - 2) + 'โ€ฆ ' @@ -300,23 +314,27 @@ class Table> { } } -export function table>(data: T[], columns: table.Columns, options: table.Options = {}): void { +export function table>( + data: T[], + columns: table.Columns, + options: table.Options = {}, +): void { new Table(data, columns, options).display() } export namespace table { export const Flags: { - columns: Interfaces.OptionFlag; - sort: Interfaces.OptionFlag; - filter: Interfaces.OptionFlag; - csv: Interfaces.BooleanFlag; - output: Interfaces.OptionFlag; - extended: Interfaces.BooleanFlag; - 'no-truncate': Interfaces.BooleanFlag; - 'no-header': Interfaces.BooleanFlag; + columns: Interfaces.OptionFlag + sort: Interfaces.OptionFlag + filter: Interfaces.OptionFlag + csv: Interfaces.BooleanFlag + output: Interfaces.OptionFlag + extended: Interfaces.BooleanFlag + 'no-truncate': Interfaces.BooleanFlag + 'no-header': Interfaces.BooleanFlag } = { columns: F.string({exclusive: ['extended'], description: 'only show provided columns (comma-separated)'}), - sort: F.string({description: 'property to sort by (prepend \'-\' for descending)'}), + sort: F.string({description: "property to sort by (prepend '-' for descending)"}), filter: F.string({description: 'filter property by partial string matching, ex: name=foo'}), csv: F.boolean({exclusive: ['no-truncate'], description: 'output is csv format [alias: --output=csv]'}), output: F.string({ @@ -334,8 +352,8 @@ export namespace table { type IncludeFlags = Pick export function flags(): IFlags - export function flags(opts: { except: Z | Z[] }): ExcludeFlags - export function flags(opts: { only: K | K[] }): IncludeFlags + export function flags(opts: {except: Z | Z[]}): ExcludeFlags + export function flags(opts: {only: K | K[]}): IncludeFlags export function flags(opts?: any): any { if (opts) { @@ -344,7 +362,7 @@ export namespace table { const e = (opts.except && typeof opts.except === 'string' ? [opts.except] : opts.except) || [] for (const key of o) { if (!(e as any[]).includes(key)) { - (f as any)[key] = (Flags as any)[key] + ;(f as any)[key] = (Flags as any)[key] } } @@ -355,33 +373,34 @@ export namespace table { } export interface Column> { - header: string; - extended: boolean; - minWidth: number; - get(row: T): any; + header: string + extended: boolean + minWidth: number + get(row: T): any } - export type Columns> = { [key: string]: Partial> } + export type Columns> = {[key: string]: Partial>} // export type OutputType = 'csv' | 'json' | 'yaml' export interface Options { - [key: string]: any; - sort?: string; - filter?: string; - columns?: string; - extended?: boolean; - 'no-truncate'?: boolean; - output?: string; - 'no-header'?: boolean; - printLine?(s: any): any; + [key: string]: any + sort?: string + filter?: string + columns?: string + extended?: boolean + 'no-truncate'?: boolean + output?: string + 'no-header'?: boolean + printLine?(s: any): any } } -const getWidestColumnWith = (data: any[], columnKey: string): number => data.reduce((previous, current) => { - const d = current[columnKey] - // convert multi-line cell to single longest line - // for width calculations - const manyLines = (d as string).split('\n') - return Math.max(previous, manyLines.length > 1 ? Math.max(...manyLines.map((r: string) => sw(r))) : sw(d)) -}, 0) +const getWidestColumnWith = (data: any[], columnKey: string): number => + data.reduce((previous, current) => { + const d = current[columnKey] + // convert multi-line cell to single longest line + // for width calculations + const manyLines = (d as string).split('\n') + return Math.max(previous, manyLines.length > 1 ? Math.max(...manyLines.map((r: string) => sw(r))) : sw(d)) + }, 0) diff --git a/src/cli-ux/styled/tree.ts b/src/cli-ux/styled/tree.ts index 75b2dbc4e..916261506 100644 --- a/src/cli-ux/styled/tree.ts +++ b/src/cli-ux/styled/tree.ts @@ -1,7 +1,7 @@ const treeify = require('object-treeify') export class Tree { - nodes: { [key: string]: Tree } = {} + nodes: {[key: string]: Tree} = {} insert(child: string, value: Tree = new Tree()): Tree { this.nodes[child] = value @@ -21,7 +21,7 @@ export class Tree { display(logger: any = console.log): void { const addNodes = function (nodes: any) { - const tree: { [key: string]: any } = {} + const tree: {[key: string]: any} = {} for (const p of Object.keys(nodes)) { tree[p] = addNodes(nodes[p].nodes) } diff --git a/src/cli-ux/wait.ts b/src/cli-ux/wait.ts index 7514385ff..ecf28356f 100644 --- a/src/cli-ux/wait.ts +++ b/src/cli-ux/wait.ts @@ -1,3 +1,4 @@ -export default (ms = 1000): Promise => new Promise(resolve => { - setTimeout(resolve, ms) -}) +export default (ms = 1000): Promise => + new Promise((resolve) => { + setTimeout(resolve, ms) + }) diff --git a/src/command.ts b/src/command.ts index 7a54e2136..2f0a61752 100644 --- a/src/command.ts +++ b/src/command.ts @@ -1,4 +1,3 @@ - import * as Errors from './errors' import * as Parser from './parser' import { @@ -17,7 +16,6 @@ import { } from './interfaces/parser' import {format, inspect} from 'node:util' import {formatCommandDeprecationWarning, formatFlagDeprecationWarning, normalizeArgv, toConfiguredId} from './help/util' -import {requireJson, uniq} from './util' import {stderr, stdout} from './cli-ux/stream' import {CommandError} from './interfaces/errors' import {Config} from './config' @@ -25,9 +23,11 @@ import {LoadOptions} from './interfaces/config' import {PJSON} from './interfaces' import {Plugin} from './interfaces/plugin' import {PrettyPrintableError} from './errors' -import {boolean} from './flags' +import {aggregateFlags} from './util/aggregate-flags' import chalk from 'chalk' import {fileURLToPath} from 'node:url' +import {requireJson} from './util/fs' +import {uniq} from './util/util' import {ux} from './cli-ux' const pjson = requireJson(__dirname, '..', 'package.json') @@ -37,18 +37,10 @@ const pjson = requireJson(__dirname, '..', 'package.json') * this occurs when stdout closes such as when piping to head */ stdout.on('error', (err: any) => { - if (err && err.code === 'EPIPE') - return + if (err && err.code === 'EPIPE') return throw err }) -const jsonFlag = { - json: boolean({ - description: 'Format output as json.', - helpGroup: 'GLOBAL', - }), -} - /** * An abstract class which acts as the base for each command * in your project. @@ -126,37 +118,7 @@ export abstract class Command { public static hasDynamicHelp = false - protected static '_--' = false - - protected static _enableJsonFlag = false - - public static get enableJsonFlag(): boolean { - return this._enableJsonFlag - } - - public static set enableJsonFlag(value: boolean) { - this._enableJsonFlag = value - if (value === true) { - this.baseFlags = jsonFlag - } else { - delete this.baseFlags?.json - this.flags = {} // force the flags setter to run - delete this.flags?.json - } - } - - public static get '--'(): boolean { - return Command['_--'] - } - - public static set '--'(value: boolean) { - Command['_--'] = value - } - - public get passThroughEnabled(): boolean { - return Command['_--'] - } - + public static enableJsonFlag = false /** * instantiate and run the command * @@ -165,7 +127,11 @@ export abstract class Command { * @param {LoadOptions} opts options * @returns {Promise} result */ - public static async run(this: new(argv: string[], config: Config) => T, argv?: string[], opts?: LoadOptions): Promise> { + public static async run( + this: new (argv: string[], config: Config) => T, + argv?: string[], + opts?: LoadOptions, + ): Promise> { if (!argv) argv = process.argv.slice(2) // Handle the case when a file URL string is passed in such as 'import.meta.url'; covert to file path. @@ -184,35 +150,19 @@ export abstract class Command { return cmd._run>() } - protected static _baseFlags: FlagInput - - static get baseFlags(): FlagInput { - return this._baseFlags - } - - static set baseFlags(flags: FlagInput) { - // eslint-disable-next-line prefer-object-spread - this._baseFlags = Object.assign({}, this.baseFlags, flags) - this.flags = {} // force the flags setter to run - } + public static baseFlags: FlagInput /** A hash of flags for the command */ - protected static _flags: FlagInput - - public static get flags(): FlagInput { - return this._flags - } - - public static set flags(flags: FlagInput) { - // eslint-disable-next-line prefer-object-spread - this._flags = Object.assign({}, this._flags ?? {}, this.baseFlags, flags) - } + public static flags: FlagInput public id: string | undefined protected debug: (...args: any[]) => void - public constructor(public argv: string[], public config: Config) { + public constructor( + public argv: string[], + public config: Config, + ) { this.id = this.ctor.id try { this.debug = require('debug')(this.id ? `${this.config.bin}:${this.id}` : this.config.bin) @@ -260,7 +210,10 @@ export abstract class Command { public error(input: string | Error, options?: {code?: string; exit?: number} & PrettyPrintableError): never - public error(input: string | Error, options: {code?: string; exit?: number | false} & PrettyPrintableError = {}): void { + public error( + input: string | Error, + options: {code?: string; exit?: number | false} & PrettyPrintableError = {}, + ): void { return Errors.error(input, options as any) } @@ -284,16 +237,19 @@ export abstract class Command { * @returns {boolean} true if the command supports json and the --json flag is present */ public jsonEnabled(): boolean { - // if the command doesn't support json, return false + // If the command doesn't support json, return false if (!this.ctor.enableJsonFlag) return false - // if the command parameter pass through is enabled, return true if the --json flag is before the '--' separator - if (this.passThroughEnabled) { - const ptIndex = this.argv.indexOf('--') - const jsonIndex = this.argv.indexOf('--json') - return jsonIndex > -1 && (ptIndex === -1 || jsonIndex < ptIndex) - } - return this.argv.includes('--json') || this.config.scopedEnvVar?.('CONTENT_TYPE')?.toLowerCase() === 'json' + // If the CONTENT_TYPE env var is set to json, return true + if (this.config.scopedEnvVar?.('CONTENT_TYPE')?.toLowerCase() === 'json') return true + + const passThroughIndex = this.argv.indexOf('--') + const jsonIndex = this.argv.indexOf('--json') + return passThroughIndex === -1 + ? // If '--' is not present, then check for `--json` in this.argv + jsonIndex > -1 + : // If '--' is present, return true only the --json flag exists and is before the '--' + jsonIndex > -1 && jsonIndex < passThroughIndex } /** @@ -312,8 +268,9 @@ export abstract class Command { } protected warnIfFlagDeprecated(flags: Record): void { + const allFlags = aggregateFlags(this.ctor.flags, this.ctor.baseFlags, this.ctor.enableJsonFlag) for (const flag of Object.keys(flags)) { - const flagDef = this.ctor.flags[flag] + const flagDef = allFlags[flag] const deprecated = flagDef?.deprecated if (deprecated) { this.warn(formatFlagDeprecationWarning(flag, deprecated)) @@ -321,10 +278,12 @@ export abstract class Command { const deprecateAliases = flagDef?.deprecateAliases if (deprecateAliases) { - const aliases = uniq([...flagDef?.aliases ?? [], ...flagDef?.charAliases ?? []]).map(a => a.length === 1 ? `-${a}` : `--${a}`) + const aliases = uniq([...(flagDef?.aliases ?? []), ...(flagDef?.charAliases ?? [])]).map((a) => + a.length === 1 ? `-${a}` : `--${a}`, + ) if (aliases.length === 0) return - const foundAliases = aliases.filter(alias => this.argv.some(a => a.startsWith(alias))) + const foundAliases = aliases.filter((alias) => this.argv.some((a) => a.startsWith(alias))) for (const alias of foundAliases) { let preferredUsage = `--${flagDef?.name}` if (flagDef?.char) { @@ -352,12 +311,18 @@ export abstract class Command { } } - protected async parse(options?: Input, argv = this.argv): Promise> { + protected async parse( + options?: Input, + argv = this.argv, + ): Promise> { if (!options) options = this.ctor as Input - const opts = {context: this, ...options} - // the spread operator doesn't work with getters so we have to manually add it here - opts.flags = options?.flags - opts.args = options?.args + + const opts = { + context: this, + ...options, + flags: aggregateFlags(options.flags, options.baseFlags, options.enableJsonFlag), + } + const results = await Parser.parse(argv, opts) this.warnIfFlagDeprecated(results.flags ?? {}) @@ -407,14 +372,14 @@ export abstract class Command { keys.push(this.config.scopedEnvVarKey(envVar)) } - keys.map(key => delete process.env[key]) + keys.map((key) => delete process.env[key]) } } export namespace Command { export type Class = typeof Command & { - id: string; - run(argv?: string[], config?: LoadOptions): Promise; + id: string + run(argv?: string[], config?: LoadOptions): Promise } export interface Loadable extends Cached { @@ -422,34 +387,35 @@ export namespace Command { } export type Cached = { - [key: string]: unknown; - id: string; - hidden: boolean; - state?: 'beta' | 'deprecated' | string; - deprecationOptions?: Deprecation; - aliases: string[]; - summary?: string; - description?: string; - usage?: string | string[]; - examples?: Example[]; - strict?: boolean; - type?: string; - pluginName?: string; - pluginType?: string; - pluginAlias?: string; - flags: {[name: string]: Flag.Cached}; - args: {[name: string]: Arg.Cached}; - hasDynamicHelp?: boolean; + [key: string]: unknown + id: string + hidden: boolean + state?: 'beta' | 'deprecated' | string + deprecationOptions?: Deprecation + aliases: string[] + summary?: string + description?: string + usage?: string | string[] + examples?: Example[] + strict?: boolean + type?: string + pluginName?: string + pluginType?: string + pluginAlias?: string + flags: {[name: string]: Flag.Cached} + args: {[name: string]: Arg.Cached} + hasDynamicHelp?: boolean permutations?: string[] - aliasPermutations?: string[]; - isESM?: boolean; - relativePath?: string[]; + aliasPermutations?: string[] + isESM?: boolean + relativePath?: string[] } export type Flag = IFlag export namespace Flag { - export type Cached = Omit & (BooleanFlagProps | OptionFlagProps) + export type Cached = Omit & + (BooleanFlagProps | OptionFlagProps) & {hasDynamicHelp?: boolean} export type Any = Flag | Cached } @@ -460,8 +426,10 @@ export namespace Command { export type Any = Arg | Cached } - export type Example = string | { - description: string; - command: string; - } + export type Example = + | string + | { + description: string + command: string + } } diff --git a/src/config/config.ts b/src/config/config.ts index cdbf481cf..a201a38f7 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -1,21 +1,22 @@ import * as ejs from 'ejs' import {ArchTypes, Config as IConfig, LoadOptions, PlatformTypes, VersionDetails} from '../interfaces/config' -import {Arg, OptionFlag} from '../interfaces/parser' import {CLIError, error, exit, warn} from '../errors' import {Debug, collectUsableIds, getCommandIdPermutations} from './util' import {Hook, Hooks, PJSON, Topic} from '../interfaces' import {Plugin as IPlugin, Options} from '../interfaces/plugin' import {URL, fileURLToPath} from 'node:url' import {arch, userInfo as osUserInfo, release, tmpdir, type} from 'node:os' -import {compact, ensureArgObject, getHomeDir, getPlatform, isProd, requireJson} from '../util' -import {join, sep} from 'node:path' -// eslint-disable-next-line sort-imports -import {OCLIF_MARKER_OWNER, Performance} from '../performance' +import {compact, isProd} from '../util/util' +import {getHomeDir, getPlatform} from '../util/os' +import { join, sep } from 'node:path' +import { OCLIF_MARKER_OWNER, Performance } from '../performance' import {Command} from '../command' import PluginLoader from './plugin-loader' +import WSL from 'is-wsl' import {format} from 'node:util' -import {getHelpFlagAdditions} from '../help' +import {getHelpFlagAdditions} from '../help/util' import {loadWithData} from '../module-loader' +import {requireJson} from '../util/fs' import {settings} from '../settings' import {stdout} from '../cli-ux/stream' @@ -30,8 +31,6 @@ function channelFromVersion(version: string) { return (m && m[1]) || 'stable' } -const WSL = require('is-wsl') - function isConfig(o: any): o is Config { return o && Boolean(o._base) } @@ -96,7 +95,7 @@ export class Config implements IConfig { public version!: string public windows!: boolean public binAliases?: string[] - public nsisCustomization?:string + public nsisCustomization?: string protected warned = false @@ -170,7 +169,7 @@ export class Config implements IConfig { this.channel = this.options.channel || channelFromVersion(this.version) this.valid = Config._rootPlugin.valid - this.arch = (arch() === 'ia32' ? 'x86' : arch() as any) + this.arch = arch() === 'ia32' ? 'x86' : (arch() as any) this.platform = WSL ? 'wsl' : getPlatform() this.windows = this.platform === 'win32' this.bin = this.pjson.oclif.bin || this.name @@ -179,7 +178,8 @@ export class Config implements IConfig { this.dirname = this.pjson.oclif.dirname || this.name this.flexibleTaxonomy = this.pjson.oclif.flexibleTaxonomy || false // currently, only colons or spaces are valid separators - if (this.pjson.oclif.topicSeparator && [':', ' '].includes(this.pjson.oclif.topicSeparator)) this.topicSeparator = this.pjson.oclif.topicSeparator! + if (this.pjson.oclif.topicSeparator && [':', ' '].includes(this.pjson.oclif.topicSeparator)) + this.topicSeparator = this.pjson.oclif.topicSeparator! if (this.platform === 'win32') this.dirname = this.dirname.replace('/', '\\') this.userAgent = `${this.name}/${this.version} ${this.platform}-${this.arch} node-${process.version}` this.shell = this._shell() @@ -204,17 +204,20 @@ export class Config implements IConfig { ...s3.templates, target: { baseDir: '<%- bin %>', - unversioned: "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %>-<%- platform %>-<%- arch %><%- ext %>", - versioned: "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %>-v<%- version %>/<%- bin %>-v<%- version %>-<%- platform %>-<%- arch %><%- ext %>", + unversioned: + "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %>-<%- platform %>-<%- arch %><%- ext %>", + versioned: + "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %>-v<%- version %>/<%- bin %>-v<%- version %>-<%- platform %>-<%- arch %><%- ext %>", manifest: "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- platform %>-<%- arch %>", - ...s3.templates && s3.templates.target, + ...(s3.templates && s3.templates.target), }, vanilla: { unversioned: "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %><%- ext %>", - versioned: "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %>-v<%- version %>/<%- bin %>-v<%- version %><%- ext %>", + versioned: + "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %>-v<%- version %>/<%- bin %>-v<%- version %><%- ext %>", baseDir: '<%- bin %>', manifest: "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %>version", - ...s3.templates && s3.templates.vanilla, + ...(s3.templates && s3.templates.vanilla), }, } @@ -278,7 +281,7 @@ export class Config implements IConfig { }, ms).unref() }) - return Promise.race([promise, timeout]).then(result => { + return Promise.race([promise, timeout]).then((result) => { clearTimeout(id) return result }) @@ -288,7 +291,7 @@ export class Config implements IConfig { successes: [], failures: [], } as Hook.Result - const promises = [...this.plugins.values()].map(async p => { + const promises = [...this.plugins.values()].map(async (p) => { const debug = require('debug')([this.bin, p.name, 'hooks', event].join(':')) const context: Hook.Context = { config: this, @@ -299,7 +302,7 @@ export class Config implements IConfig { log(message?: any, ...args: any[]) { stdout.write(format(message, ...args) + '\n') }, - error(message, options: { code?: string; exit?: number } = {}) { + error(message, options: {code?: string; exit?: number} = {}) { error(message, options) }, warn(message: string) { @@ -317,8 +320,8 @@ export class Config implements IConfig { debug('start', isESM ? '(import)' : '(require)', filePath) const result = timeout - ? await withTimeout(timeout, search(module).call(context, {...opts as any, config: this})) - : await search(module).call(context, {...opts as any, config: this}) + ? await withTimeout(timeout, search(module).call(context, {...(opts as any), config: this})) + : await search(module).call(context, {...(opts as any), config: this}) final.successes.push({plugin: p, result}) if (p.name === '@oclif/plugin-legacy' && event === 'init') { @@ -349,15 +352,20 @@ export class Config implements IConfig { return final } - public async runCommand(id: string, argv: string[] = [], cachedCommand: Command.Loadable | null = null): Promise { + public async runCommand( + id: string, + argv: string[] = [], + cachedCommand: Command.Loadable | null = null, + ): Promise { const marker = Performance.mark(OCLIF_MARKER_OWNER, `config.runCommand#${id}`) debug('runCommand %s %o', id, argv) let c = cachedCommand ?? this.findCommand(id) if (!c) { const matches = this.flexibleTaxonomy ? this.findMatches(id, argv) : [] - const hookResult = this.flexibleTaxonomy && matches.length > 0 - ? await this.runHook('command_incomplete', {id, argv, matches}) - : await this.runHook('command_not_found', {id, argv}) + const hookResult = + this.flexibleTaxonomy && matches.length > 0 + ? await this.runHook('command_incomplete', {id, argv, matches}) + : await this.runHook('command_not_found', {id, argv}) if (hookResult.successes[0]) return hookResult.successes[0].result as T if (hookResult.failures[0]) throw hookResult.failures[0].error @@ -399,11 +407,11 @@ export class Config implements IConfig { } public scopedEnvVar(k: string): string | undefined { - return process.env[this.scopedEnvVarKeys(k).find(k => process.env[k]) as string] + return process.env[this.scopedEnvVarKeys(k).find((k) => process.env[k]) as string] } public scopedEnvVarTrue(k: string): boolean { - const v = process.env[this.scopedEnvVarKeys(k).find(k => process.env[k]) as string] + const v = process.env[this.scopedEnvVarKeys(k).find((k) => process.env[k]) as string] return v === '1' || v === 'true' } @@ -414,9 +422,9 @@ export class Config implements IConfig { */ public scopedEnvVarKey(k: string): string { return [this.bin, k] - .map(p => p.replaceAll('@', '').replaceAll(/[/-]/g, '_')) - .join('_') - .toUpperCase() + .map((p) => p.replaceAll('@', '').replaceAll(/[/-]/g, '_')) + .join('_') + .toUpperCase() } /** @@ -425,26 +433,27 @@ export class Config implements IConfig { * @returns {string[]} e.g. ['SF_DEBUG', 'SFDX_DEBUG'] */ public scopedEnvVarKeys(k: string): string[] { - return [this.bin, ...this.binAliases ?? []].filter(Boolean).map(alias => - [alias.replaceAll('@', '').replaceAll(/[/-]/g, '_'), k].join('_').toUpperCase()) + return [this.bin, ...(this.binAliases ?? [])] + .filter(Boolean) + .map((alias) => [alias.replaceAll('@', '').replaceAll(/[/-]/g, '_'), k].join('_').toUpperCase()) } - public findCommand(id: string, opts: { must: true }): Command.Loadable + public findCommand(id: string, opts: {must: true}): Command.Loadable - public findCommand(id: string, opts?: { must: boolean }): Command.Loadable | undefined + public findCommand(id: string, opts?: {must: boolean}): Command.Loadable | undefined - public findCommand(id: string, opts: { must?: boolean } = {}): Command.Loadable | undefined { + public findCommand(id: string, opts: {must?: boolean} = {}): Command.Loadable | undefined { const lookupId = this.getCmdLookupId(id) const command = this._commands.get(lookupId) if (opts.must && !command) error(`command ${lookupId} not found`) return command } - public findTopic(id: string, opts: { must: true }): Topic + public findTopic(id: string, opts: {must: true}): Topic - public findTopic(id: string, opts?: { must: boolean }): Topic | undefined + public findTopic(id: string, opts?: {must: boolean}): Topic | undefined - public findTopic(name: string, opts: { must?: boolean } = {}): Topic | undefined { + public findTopic(name: string, opts: {must?: boolean} = {}): Topic | undefined { const lookupId = this.getTopicLookupId(name) const topic = this._topics.get(lookupId) if (topic) return topic @@ -465,14 +474,18 @@ export class Config implements IConfig { * @returns string[] */ public findMatches(partialCmdId: string, argv: string[]): Command.Loadable[] { - const flags = argv.filter(arg => !getHelpFlagAdditions(this).includes(arg) && arg.startsWith('-')).map(a => a.replaceAll('-', '')) - const possibleMatches = [...this.commandPermutations.get(partialCmdId)].map(k => this._commands.get(k)!) + const flags = argv + .filter((arg) => !getHelpFlagAdditions(this).includes(arg) && arg.startsWith('-')) + .map((a) => a.replaceAll('-', '')) + const possibleMatches = [...this.commandPermutations.get(partialCmdId)].map((k) => this._commands.get(k)!) - const matches = possibleMatches.filter(command => { - const cmdFlags = Object.entries(command.flags).flatMap(([flag, def]) => def.char ? [def.char, flag] : [flag]) as string[] + const matches = possibleMatches.filter((command) => { + const cmdFlags = Object.entries(command.flags).flatMap(([flag, def]) => + def.char ? [def.char, flag] : [flag], + ) as string[] // A command is a match if the provided flags belong to the full command - return flags.every(f => cmdFlags.includes(f)) + return flags.every((f) => cmdFlags.includes(f)) }) return matches @@ -500,7 +513,7 @@ export class Config implements IConfig { * @returns string[] */ public getAllCommandIDs(): string[] { - return this.getAllCommands().map(c => c.id) + return this.getAllCommands().map((c) => c.id) } public get commands(): Command.Loadable[] { @@ -509,7 +522,7 @@ export class Config implements IConfig { public get commandIDs(): string[] { if (this._commandIDs) return this._commandIDs - this._commandIDs = this.commands.map(c => c.id) + this._commandIDs = this.commands.map((c) => c.id) return this._commandIDs } @@ -523,18 +536,24 @@ export class Config implements IConfig { cliVersion, architecture, nodeVersion, - pluginVersions: Object.fromEntries([...this.plugins.values()].map(p => [p.name, {version: p.version, type: p.type, root: p.root}])), + pluginVersions: Object.fromEntries( + [...this.plugins.values()].map((p) => [p.name, {version: p.version, type: p.type, root: p.root}]), + ), osVersion: `${type()} ${release()}`, shell: this.shell, rootPath: this.root, } } - public s3Key(type: keyof PJSON.S3.Templates, ext?: '.tar.gz' | '.tar.xz' | IConfig.s3Key.Options, options: IConfig.s3Key.Options = {}): string { + public s3Key( + type: keyof PJSON.S3.Templates, + ext?: '.tar.gz' | '.tar.xz' | IConfig.s3Key.Options, + options: IConfig.s3Key.Options = {}, + ): string { if (typeof ext === 'object') options = ext else if (ext) options.ext = ext const template = this.pjson.oclif.update.s3.templates[options.platform ? 'target' : 'vanilla'][type] ?? '' - return ejs.render(template, {...this as any, ...options}) + return ejs.render(template, {...(this as any), ...options}) } public s3Url(key: string): string { @@ -550,9 +569,10 @@ export class Config implements IConfig { } protected dir(category: 'cache' | 'data' | 'config'): string { - const base = process.env[`XDG_${category.toUpperCase()}_HOME`] - || (this.windows && process.env.LOCALAPPDATA) - || join(this.home, category === 'data' ? '.local/share' : '.' + category) + const base = + process.env[`XDG_${category.toUpperCase()}_HOME`] || + (this.windows && process.env.LOCALAPPDATA) || + join(this.home, category === 'data' ? '.local/share' : '.' + category) return join(base, this.dirname) } @@ -561,7 +581,7 @@ export class Config implements IConfig { } protected windowsHomedriveHome(): string | undefined { - return (process.env.HOMEDRIVE && process.env.HOMEPATH && join(process.env.HOMEDRIVE!, process.env.HOMEPATH!)) + return process.env.HOMEDRIVE && process.env.HOMEPATH && join(process.env.HOMEDRIVE!, process.env.HOMEPATH!) } protected windowsUserprofileHome(): string | undefined { @@ -597,7 +617,7 @@ export class Config implements IConfig { return 0 } - protected warn(err: string | Error | { name: string; detail: string }, scope?: string): void { + protected warn(err: string | Error | {name: string; detail: string}, scope?: string): void { if (this.warned) return if (typeof err === 'string') { @@ -641,7 +661,10 @@ export class Config implements IConfig { private isJitPluginCommand(c: Command.Loadable): boolean { // Return true if the command's plugin is listed under oclif.jitPlugins AND if the plugin hasn't been loaded to this.plugins - return Object.keys(this.pjson.oclif.jitPlugins ?? {}).includes(c.pluginName ?? '') && Boolean(c?.pluginName && !this.plugins.has(c.pluginName)) + return ( + Object.keys(this.pjson.oclif.jitPlugins ?? {}).includes(c.pluginName ?? '') && + Boolean(c?.pluginName && !this.plugins.has(c.pluginName)) + ) } private getCmdLookupId(id: string): string { @@ -669,9 +692,10 @@ export class Config implements IConfig { // v3 moved command id permutations to the manifest, but some plugins may not have // the new manifest yet. For those, we need to calculate the permutations here. - const permutations = this.flexibleTaxonomy && command.permutations === undefined - ? getCommandIdPermutations(command.id) - : command.permutations ?? [command.id] + const permutations = + this.flexibleTaxonomy && command.permutations === undefined + ? getCommandIdPermutations(command.id) + : command.permutations ?? [command.id] // set every permutation for (const permutation of permutations) { this.commandPermutations.add(permutation, command.id) @@ -690,9 +714,10 @@ export class Config implements IConfig { // v3 moved command alias permutations to the manifest, but some plugins may not have // the new manifest yet. For those, we need to calculate the permutations here. - const aliasPermutations = this.flexibleTaxonomy && command.aliasPermutations === undefined - ? getCommandIdPermutations(alias) - : command.permutations ?? [alias] + const aliasPermutations = + this.flexibleTaxonomy && command.aliasPermutations === undefined + ? getCommandIdPermutations(alias) + : command.permutations ?? [alias] // set every permutation for (const permutation of aliasPermutations) { this.commandPermutations.add(permutation, command.id) @@ -722,7 +747,7 @@ export class Config implements IConfig { } // Add missing topics for displaying help when partial commands are entered. - for (const c of plugin.commands.filter(c => !c.hidden)) { + for (const c of plugin.commands.filter((c) => !c.hidden)) { const parts = c.id.split(':') while (parts.length > 0) { const name = parts.join(':') @@ -796,14 +821,14 @@ export class Config implements IConfig { } /** - * Insert legacy plugins - * - * Replace invalid CLI plugins (cli-engine plugins, mostly Heroku) loaded via `this.loadPlugins` - * with oclif-compatible ones returned by @oclif/plugin-legacy init hook. - * - * @param plugins array of oclif-compatible plugins - * @returns void - */ + * Insert legacy plugins + * + * Replace invalid CLI plugins (cli-engine plugins, mostly Heroku) loaded via `this.loadPlugins` + * with oclif-compatible ones returned by @oclif/plugin-legacy init hook. + * + * @param plugins array of oclif-compatible plugins + * @returns void + */ private insertLegacyPlugins(plugins: IPlugin[]) { for (const plugin of plugins) { this.plugins.set(plugin.name, plugin) @@ -811,148 +836,3 @@ export class Config implements IConfig { } } } - -// when no manifest exists, the default is calculated. This may throw, so we need to catch it -const defaultFlagToCached = async (flag: OptionFlag, respectNoCacheDefault: boolean) => { - if (respectNoCacheDefault && flag.noCacheDefault) return - // Prefer the defaultHelp function (returns a friendly string for complex types) - if (typeof flag.defaultHelp === 'function') { - try { - return await flag.defaultHelp({options: flag, flags: {}}) - } catch { - return - } - } - - // if not specified, try the default function - if (typeof flag.default === 'function') { - try { - return await flag.default({options: flag, flags: {}}) - } catch {} - } else { - return flag.default - } -} - -const defaultArgToCached = async (arg: Arg, respectNoCacheDefault: boolean): Promise => { - if (respectNoCacheDefault && arg.noCacheDefault) return - // Prefer the defaultHelp function (returns a friendly string for complex types) - if (typeof arg.defaultHelp === 'function') { - try { - return await arg.defaultHelp({options: arg, flags: {}}) - } catch { - return - } - } - - // if not specified, try the default function - if (typeof arg.default === 'function') { - try { - return await arg.default({options: arg, flags: {}}) - } catch {} - } else { - return arg.default - } -} - -export async function toCached(c: Command.Class, plugin?: IPlugin, respectNoCacheDefault = false): Promise { - const flags = {} as {[k: string]: Command.Flag.Cached} - - for (const [name, flag] of Object.entries(c.flags || {})) { - if (flag.type === 'boolean') { - flags[name] = { - name, - type: flag.type, - char: flag.char, - summary: flag.summary, - description: flag.description, - hidden: flag.hidden, - required: flag.required, - helpLabel: flag.helpLabel, - helpGroup: flag.helpGroup, - allowNo: flag.allowNo, - dependsOn: flag.dependsOn, - relationships: flag.relationships, - exclusive: flag.exclusive, - deprecated: flag.deprecated, - deprecateAliases: c.deprecateAliases, - aliases: flag.aliases, - charAliases: flag.charAliases, - delimiter: flag.delimiter, - noCacheDefault: flag.noCacheDefault, - } - } else { - flags[name] = { - name, - type: flag.type, - char: flag.char, - summary: flag.summary, - description: flag.description, - hidden: flag.hidden, - required: flag.required, - helpLabel: flag.helpLabel, - helpValue: flag.helpValue, - helpGroup: flag.helpGroup, - multiple: flag.multiple, - options: flag.options, - dependsOn: flag.dependsOn, - relationships: flag.relationships, - exclusive: flag.exclusive, - default: await defaultFlagToCached(flag, respectNoCacheDefault), - deprecated: flag.deprecated, - deprecateAliases: c.deprecateAliases, - aliases: flag.aliases, - charAliases: flag.charAliases, - delimiter: flag.delimiter, - noCacheDefault: flag.noCacheDefault, - } - // a command-level placeholder in the manifest so that oclif knows it should regenerate the command during help-time - if (typeof flag.defaultHelp === 'function') { - c.hasDynamicHelp = true - } - } - } - - const args = {} as {[k: string]: Command.Arg.Cached} - for (const [name, arg] of Object.entries(ensureArgObject(c.args))) { - args[name] = { - name, - description: arg.description, - required: arg.required, - options: arg.options, - default: await defaultArgToCached(arg, respectNoCacheDefault), - hidden: arg.hidden, - noCacheDefault: arg.noCacheDefault, - } - } - - const stdProperties = { - id: c.id, - summary: c.summary, - description: c.description, - strict: c.strict, - usage: c.usage, - pluginName: plugin && plugin.name, - pluginAlias: plugin && plugin.alias, - pluginType: plugin && plugin.type, - hidden: c.hidden, - state: c.state, - aliases: c.aliases || [], - examples: c.examples || (c as any).example, - deprecationOptions: c.deprecationOptions, - deprecateAliases: c.deprecateAliases, - flags, - args, - } - - // do not include these properties in manifest - const ignoreCommandProperties = ['plugin', '_flags', '_enableJsonFlag', '_globalFlags', '_baseFlags'] - const stdKeys = Object.keys(stdProperties) - const keysToAdd = Object.keys(c).filter(property => ![...stdKeys, ...ignoreCommandProperties].includes(property)) - const additionalProperties: Record = {} - for (const key of keysToAdd) { - additionalProperties[key] = (c as any)[key] - } - - return {...stdProperties, ...additionalProperties} -} diff --git a/src/config/index.ts b/src/config/index.ts index a79d4c3bf..e3e23deca 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,5 +1,3 @@ export {Config} from './config' -export {toCached} from './config' export {Plugin} from './plugin' export {tsPath} from './ts-node' - diff --git a/src/config/plugin-loader.ts b/src/config/plugin-loader.ts index 72872e46f..071550a6c 100644 --- a/src/config/plugin-loader.ts +++ b/src/config/plugin-loader.ts @@ -1,26 +1,26 @@ import * as Plugin from './plugin' import {Plugin as IPlugin, Options} from '../interfaces/plugin' -import {isProd, readJson} from '../util' -// eslint-disable-next-line sort-imports -import {OCLIF_MARKER_OWNER, Performance} from '../performance' import {Debug} from './util' import {PJSON} from '../interfaces' +import {OCLIF_MARKER_OWNER, Performance} from '../performance' +import {isProd} from '../util/util' import {join} from 'node:path' +import {readJson} from '../util/fs' // eslint-disable-next-line new-cap const debug = Debug() type PluginLoaderOptions = { - root: string; - plugins?: IPlugin[] | PluginsMap; + root: string + plugins?: IPlugin[] | PluginsMap } type LoadOpts = { - devPlugins?: boolean; - userPlugins?: boolean; - dataDir: string; - rootPlugin: IPlugin; - force?: boolean; + devPlugins?: boolean + userPlugins?: boolean + dataDir: string + rootPlugin: IPlugin + force?: boolean } type PluginsMap = Map @@ -33,7 +33,7 @@ export default class PluginLoader { constructor(public options: PluginLoaderOptions) { if (options.plugins) { this.pluginsProvided = true - this.plugins = Array.isArray(options.plugins) ? new Map(options.plugins.map(p => [p.name, p])) : options.plugins + this.plugins = Array.isArray(options.plugins) ? new Map(options.plugins.map((p) => [p.name, p])) : options.plugins } } @@ -41,10 +41,10 @@ export default class PluginLoader { let rootPlugin: IPlugin if (this.pluginsProvided) { const plugins = [...this.plugins.values()] - rootPlugin = plugins.find(p => p.root === this.options.root) ?? plugins[0] + rootPlugin = plugins.find((p) => p.root === this.options.root) ?? plugins[0] } else { const marker = Performance.mark(OCLIF_MARKER_OWNER, 'plugin.load#root') - rootPlugin = new Plugin.Plugin({root: this.options.root}) + rootPlugin = new Plugin.Plugin({root: this.options.root, isRoot: true}) await rootPlugin.load() marker?.addDetails({ hasManifest: rootPlugin.hasManifest ?? false, @@ -98,61 +98,76 @@ export default class PluginLoader { const pjson = await readJson(userPJSONPath) if (!pjson.oclif) pjson.oclif = {schema: 1} if (!pjson.oclif.plugins) pjson.oclif.plugins = [] - await this.loadPlugins(userPJSONPath, 'user', pjson.oclif.plugins.filter((p: any) => p.type === 'user')) - await this.loadPlugins(userPJSONPath, 'link', pjson.oclif.plugins.filter((p: any) => p.type === 'link')) + await this.loadPlugins( + userPJSONPath, + 'user', + pjson.oclif.plugins.filter((p: any) => p.type === 'user'), + ) + await this.loadPlugins( + userPJSONPath, + 'link', + pjson.oclif.plugins.filter((p: any) => p.type === 'link'), + ) } catch (error: any) { if (error.code !== 'ENOENT') process.emitWarning(error) } } } - private async loadPlugins(root: string, type: string, plugins: (string | { root?: string; name?: string; tag?: string })[], parent?: Plugin.Plugin): Promise { + private async loadPlugins( + root: string, + type: string, + plugins: (string | {root?: string; name?: string; tag?: string})[], + parent?: Plugin.Plugin, + ): Promise { if (!plugins || plugins.length === 0) return const mark = Performance.mark(OCLIF_MARKER_OWNER, `config.loadPlugins#${type}`) debug('loading plugins', plugins) - await Promise.all((plugins || []).map(async plugin => { - try { - const name = typeof plugin === 'string' ? plugin : plugin.name! - const opts: Options = { - name, - type, - root, - } - if (typeof plugin !== 'string') { - opts.tag = plugin.tag || opts.tag - opts.root = plugin.root || opts.root - } - - if (parent) { - opts.parent = parent - } - - if (this.plugins.has(name)) return + await Promise.all( + (plugins || []).map(async (plugin) => { + try { + const name = typeof plugin === 'string' ? plugin : plugin.name! + const opts: Options = { + name, + type, + root, + } + if (typeof plugin !== 'string') { + opts.tag = plugin.tag || opts.tag + opts.root = plugin.root || opts.root + } + + if (parent) { + opts.parent = parent + } + + if (this.plugins.has(name)) return const pluginMarker = Performance.mark(OCLIF_MARKER_OWNER, `plugin.load#${name}`) - const instance = new Plugin.Plugin(opts) - await instance.load() - pluginMarker?.addDetails({ - hasManifest: instance.hasManifest, - commandCount: instance.commands.length, - topicCount: instance.topics.length, - type: instance.type, - usesMain: Boolean(instance.pjson.main), - name: instance.name, - }) - pluginMarker?.stop() - - this.plugins.set(instance.name, instance) - if (parent) { - instance.parent = parent - if (!parent.children) parent.children = [] - parent.children.push(instance) + const instance = new Plugin.Plugin(opts) + await instance.load() + pluginMarker?.addDetails({ + hasManifest: instance.hasManifest, + commandCount: instance.commands.length, + topicCount: instance.topics.length, + type: instance.type, + usesMain: Boolean(instance.pjson.main), + name: instance.name, + }) + pluginMarker?.stop() + + this.plugins.set(instance.name, instance) + if (parent) { + instance.parent = parent + if (!parent.children) parent.children = [] + parent.children.push(instance) + } + + await this.loadPlugins(instance.root, type, instance.pjson.oclif.plugins || [], instance) + } catch (error: any) { + this.errors.push(error) } - - await this.loadPlugins(instance.root, type, instance.pjson.oclif.plugins || [], instance) - } catch (error: any) { - this.errors.push(error) - } - })) + }), + ) mark?.addDetails({pluginCount: plugins.length}) mark?.stop() diff --git a/src/config/plugin.ts b/src/config/plugin.ts index e3243963a..f88be6c9e 100644 --- a/src/config/plugin.ts +++ b/src/config/plugin.ts @@ -1,14 +1,9 @@ import {CLIError, error} from '../errors' -import { - Debug, - flatMap, - getCommandIdPermutations, - mapValues, - resolvePackage, -} from './util' +import {Debug, getCommandIdPermutations, resolvePackage} from './util' import {Plugin as IPlugin, PluginOptions} from '../interfaces/plugin' -import {compact, exists, isProd, readJson, requireJson} from '../util' +import {compact, isProd, mapValues} from '../util/util' import {dirname, join, parse, relative, sep} from 'node:path' +import {exists, readJson, requireJson} from '../util/fs' import {loadWithData, loadWithDataFromManifest} from '../module-loader' // eslint-disable-next-line sort-imports import {OCLIF_MARKER_OWNER, Performance} from '../performance' @@ -16,9 +11,9 @@ import {Command} from '../command' import {Manifest} from '../interfaces/manifest' import {PJSON} from '../interfaces/pjson' import {Topic} from '../interfaces/topic' +import {cacheCommand} from '../util/cache-command' import {inspect} from 'node:util' import {sync} from 'globby' -import {toCached} from './config' import {tsPath} from './ts-node' const _pjson = requireJson(__dirname, '..', '..', 'package.json') @@ -27,17 +22,17 @@ function topicsToArray(input: any, base?: string): Topic[] { if (!input) return [] base = base ? `${base}:` : '' if (Array.isArray(input)) { - return [...input, ...flatMap(input, t => topicsToArray(t.subtopics, `${base}${t.name}`))] + return [...input, input.flatMap((t) => topicsToArray(t.subtopics, `${base}${t.name}`))] } - return flatMap(Object.keys(input), k => { + return Object.keys(input).flatMap((k) => { input[k].name = k return [{...input[k], name: `${base}${k}`}, ...topicsToArray(input[k].subtopics, `${base}${input[k].name}`)] }) } // essentially just "cd .." -function * up(from: string) { +function* up(from: string) { while (dirname(from) !== from) { yield from from = dirname(from) @@ -99,7 +94,7 @@ async function findRoot(name: string | undefined, root: string) { } const cachedCommandCanBeUsed = (manifest: Manifest | undefined, id: string): boolean => - Boolean(manifest?.commands[id] && ('isESM' in manifest.commands[id] && 'relativePath' in manifest.commands[id])) + Boolean(manifest?.commands[id] && 'isESM' in manifest.commands[id] && 'relativePath' in manifest.commands[id]) const search = (cmd: any) => { if (typeof cmd.run === 'function') return cmd @@ -142,6 +137,8 @@ export class Plugin implements IPlugin { hasManifest = false + isRoot = false + private _commandsDir!: string | undefined private flexibleTaxonomy!: boolean @@ -156,11 +153,13 @@ export class Plugin implements IPlugin { public async load(): Promise { this.type = this.options.type || 'core' this.tag = this.options.tag + this.isRoot = this.options.isRoot ?? false if (this.options.parent) this.parent = this.options.parent as Plugin // Linked plugins already have a root so there's no need to search for it. // However there could be child plugins nested inside the linked plugin, in which // case we still need to search for the child plugin's root. - const root = this.type === 'link' && !this.parent ? this.options.root : await findRoot(this.options.name, this.options.root) + const root = + this.type === 'link' && !this.parent ? this.options.root : await findRoot(this.options.name, this.options.root) if (!root) throw new CLIError(`could not find package.json with ${inspect(this.options)}`) this.root = root this._debug('reading %s plugin %s', this.type, root) @@ -181,18 +180,17 @@ export class Plugin implements IPlugin { this.pjson.oclif = this.pjson['cli-engine'] || {} } - this.hooks = mapValues(this.pjson.oclif.hooks || {}, i => Array.isArray(i) ? i : [i]) + this.hooks = mapValues(this.pjson.oclif.hooks || {}, (i) => (Array.isArray(i) ? i : [i])) this.manifest = await this._manifest() - this.commands = Object - .entries(this.manifest.commands) - .map(([id, c]) => ({ - ...c, - pluginAlias: this.alias, - pluginType: c.pluginType === 'jit' ? 'jit' : this.type, - load: async () => this.findCommand(id, {must: true}), - })) - .sort((a, b) => a.id.localeCompare(b.id)) + this.commands = Object.entries(this.manifest.commands) + .map(([id, c]) => ({ + ...c, + pluginAlias: this.alias, + pluginType: c.pluginType === 'jit' ? 'jit' : this.type, + load: async () => this.findCommand(id, {must: true}), + })) + .sort((a, b) => a.id.localeCompare(b.id)) } public get topics(): Topic[] { @@ -211,12 +209,8 @@ export class Plugin implements IPlugin { const marker = Performance.mark(OCLIF_MARKER_OWNER, `plugin.commandIDs#${this.name}`, {plugin: this.name}) this._debug(`loading IDs from ${this.commandsDir}`) - const patterns = [ - '**/*.+(js|cjs|mjs|ts|tsx)', - '!**/*.+(d.ts|test.ts|test.js|spec.ts|spec.js)?(x)', - ] - const ids = sync(patterns, {cwd: this.commandsDir}) - .map(file => { + const patterns = ['**/*.+(js|cjs|mjs|ts|tsx)', '!**/*.+(d.ts|test.ts|test.js|spec.ts|spec.js)?(x)'] + const ids = sync(patterns, {cwd: this.commandsDir}).map((file) => { const p = parse(file) const topics = p.dir.split('/') const command = p.name !== 'index' && p.name @@ -242,7 +236,7 @@ export class Plugin implements IPlugin { let isESM: boolean | undefined let filePath: string | undefined try { - ({isESM, module, filePath} = cachedCommandCanBeUsed(this.manifest, id) + ;({isESM, module, filePath} = cachedCommandCanBeUsed(this.manifest, id) ? await loadWithDataFromManifest(this.manifest.commands[id], this.root) : await loadWithData(this, join(this.commandsDir ?? this.pjson.oclif.commands, ...id.split(':')))) this._debug(isESM ? '(import)' : '(require)', filePath) @@ -276,7 +270,9 @@ export class Plugin implements IPlugin { const p = join(this.root, `${dotfile ? '.' : ''}oclif.manifest.json`) const manifest = await readJson(p) if (!process.env.OCLIF_NEXT_VERSION && manifest.version.split('-')[0] !== this.version.split('-')[0]) { - process.emitWarning(`Mismatched version in ${this.name} plugin manifest. Expected: ${this.version} Received: ${manifest.version}\nThis usually means you have an oclif.manifest.json file that should be deleted in development. This file should be automatically generated when publishing.`) + process.emitWarning( + `Mismatched version in ${this.name} plugin manifest. Expected: ${this.version} Received: ${manifest.version}\nThis usually means you have an oclif.manifest.json file that should be deleted in development. This file should be automatically generated when publishing.`, + ) } else { this._debug('using manifest from', p) this.hasManifest = true @@ -303,28 +299,35 @@ export class Plugin implements IPlugin { const manifest = { version: this.version, - commands: (await Promise.all(this.commandIDs.map(async id => { - try { - const cached = await toCached(await this.findCommand(id, {must: true}), this, respectNoCacheDefault) - if (this.flexibleTaxonomy) { - const permutations = getCommandIdPermutations(id) - const aliasPermutations = cached.aliases.flatMap(a => getCommandIdPermutations(a)) - return [id, {...cached, permutations, aliasPermutations} as Command.Cached] - } - - return [id, cached] - } catch (error: any) { - const scope = 'toCached' - if (Boolean(errorOnManifestCreate) === false) this.warn(error, scope) - else throw this.addErrorScope(error, scope) - } - }))) - // eslint-disable-next-line unicorn/no-await-expression-member, unicorn/prefer-native-coercion-functions - .filter((f): f is [string, Command.Cached] => Boolean(f)) - .reduce((commands, [id, c]) => { - commands[id] = c - return commands - }, {} as {[k: string]: Command.Cached}), + commands: ( + await Promise.all( + this.commandIDs.map(async (id) => { + try { + const cached = await cacheCommand(await this.findCommand(id, {must: true}), this, respectNoCacheDefault) + if (this.flexibleTaxonomy) { + const permutations = getCommandIdPermutations(id) + const aliasPermutations = cached.aliases.flatMap((a) => getCommandIdPermutations(a)) + return [id, {...cached, permutations, aliasPermutations} as Command.Cached] + } + + return [id, cached] + } catch (error: any) { + const scope = 'cacheCommand' + if (Boolean(errorOnManifestCreate) === false) this.warn(error, scope) + else throw this.addErrorScope(error, scope) + } + }), + ) + ) + // eslint-disable-next-line unicorn/no-await-expression-member, unicorn/prefer-native-coercion-functions + .filter((f): f is [string, Command.Cached] => Boolean(f)) + .reduce( + (commands, [id, c]) => { + commands[id] = c + return commands + }, + {} as {[k: string]: Command.Cached}, + ), } marker?.addDetails({fromCache: false, commandCount: Object.keys(manifest.commands).length}) marker?.stop() @@ -339,8 +342,14 @@ export class Plugin implements IPlugin { private addErrorScope(err: any, scope?: string) { err.name = `${err.name} Plugin: ${this.name}` - err.detail = compact([err.detail, `module: ${this._base}`, scope && `task: ${scope}`, `plugin: ${this.name}`, `root: ${this.root}`, 'See more details with DEBUG=*']).join('\n') + err.detail = compact([ + err.detail, + `module: ${this._base}`, + scope && `task: ${scope}`, + `plugin: ${this.name}`, + `root: ${this.root}`, + 'See more details with DEBUG=*', + ]).join('\n') return err } } - diff --git a/src/config/ts-node.ts b/src/config/ts-node.ts index 2addcf5a9..87784578d 100644 --- a/src/config/ts-node.ts +++ b/src/config/ts-node.ts @@ -1,11 +1,11 @@ import * as TSNode from 'ts-node' import {Plugin, TSConfig} from '../interfaces' -import {isProd, readJsonSync} from '../util' import {join, relative as pathRelative} from 'node:path' -import {Config} from './config' import {Debug} from './util' import {existsSync} from 'node:fs' +import {isProd} from '../util/util' import {memoizedWarn} from '../errors' +import {readJsonSync} from '../util/fs' import {settings} from '../settings' // eslint-disable-next-line new-cap @@ -13,6 +13,11 @@ const debug = Debug('ts-node') export const TS_CONFIGS: Record = {} const REGISTERED = new Set() +/** + * Cache the root plugin so that we can reference it later when determining if + * we should skip ts-node registration for an ESM plugin. + */ +let ROOT_PLUGIN: Plugin | undefined function loadTSConfig(root: string): TSConfig | undefined { if (TS_CONFIGS[root]) return TS_CONFIGS[root] @@ -25,20 +30,20 @@ function loadTSConfig(root: string): TSConfig | undefined { typescript = require(join(root, 'node_modules', 'typescript')) } catch { debug(`Could not find typescript dependency. Skipping ts-node registration for ${root}.`) - memoizedWarn('Could not find typescript. Please ensure that typescript is a devDependency. Falling back to compiled source.') + memoizedWarn( + 'Could not find typescript. Please ensure that typescript is a devDependency. Falling back to compiled source.', + ) return } } if (existsSync(tsconfigPath) && typescript) { - const tsconfig = typescript.parseConfigFileTextToJson( - tsconfigPath, - readJsonSync(tsconfigPath, false), - ).config + const tsconfig = typescript.parseConfigFileTextToJson(tsconfigPath, readJsonSync(tsconfigPath, false)).config if (!tsconfig || !tsconfig.compilerOptions) { throw new Error( - `Could not read and parse tsconfig.json at ${tsconfigPath}, or it ` - + 'did not contain a "compilerOptions" section.') + `Could not read and parse tsconfig.json at ${tsconfigPath}, or it ` + + 'did not contain a "compilerOptions" section.', + ) } TS_CONFIGS[root] = tsconfig @@ -59,13 +64,13 @@ function registerTSNode(root: string): TSConfig | undefined { tsNode = require(tsNodePath) } catch { debug(`Could not find ts-node at ${tsNodePath}. Skipping ts-node registration for ${root}.`) - memoizedWarn(`Could not find ts-node at ${tsNodePath}. Please ensure that ts-node is a devDependency. Falling back to compiled source.`) + memoizedWarn( + `Could not find ts-node at ${tsNodePath}. Please ensure that ts-node is a devDependency. Falling back to compiled source.`, + ) return } - const typeRoots = [ - join(root, 'node_modules', '@types'), - ] + const typeRoots = [join(root, 'node_modules', '@types')] const rootDirs: string[] = [] @@ -123,6 +128,8 @@ function registerTSNode(root: string): TSConfig | undefined { export function tsPath(root: string, orig: string, plugin: Plugin): string export function tsPath(root: string, orig: string | undefined, plugin?: Plugin): string | undefined export function tsPath(root: string, orig: string | undefined, plugin?: Plugin): string | undefined { + if (plugin?.isRoot) ROOT_PLUGIN = plugin + if (!orig) return orig orig = orig.startsWith(root) ? orig : join(root, orig) @@ -146,10 +153,14 @@ export function tsPath(root: string, orig: string | undefined, plugin?: Plugin): * We still register ts-node for ESM plugins when NODE_ENV is "test" or "development" and root plugin is also ESM. * In other words, this allows plugins to be auto-transpiled when developing locally using `bin/dev.js`. */ - if ((isProduction || Config.rootPlugin?.moduleType === 'commonjs') && plugin?.moduleType === 'module') { - debug(`Skipping ts-node registration for ${root} because it's an ESM module (NODE_ENV: ${process.env.NODE_ENV}, root plugin module type: ${Config.rootPlugin?.moduleType})))`) + if ((isProduction || ROOT_PLUGIN?.moduleType === 'commonjs') && plugin?.moduleType === 'module') { + debug( + `Skipping ts-node registration for ${root} because it's an ESM module (NODE_ENV: ${process.env.NODE_ENV}, root plugin module type: ${ROOT_PLUGIN?.moduleType})))`, + ) if (plugin.type === 'link') - memoizedWarn(`${plugin.name} is a linked ESM module and cannot be auto-transpiled. Existing compiled source will be used instead.`) + memoizedWarn( + `${plugin.name} is a linked ESM module and cannot be auto-transpiled. Existing compiled source will be used instead.`, + ) return orig } diff --git a/src/config/util.ts b/src/config/util.ts index 182b336a5..bbcb2bed6 100644 --- a/src/config/util.ts +++ b/src/config/util.ts @@ -1,18 +1,6 @@ const debug = require('debug') -export function flatMap(arr: T[], fn: (i: T) => U[]): U[] { - return arr.reduce((arr, i) => [...arr, ...fn(i)], [] as U[]) -} - -export function mapValues, TResult>(obj: {[P in keyof T]: T[P]}, fn: (i: T[keyof T], k: keyof T) => TResult): {[P in keyof T]: TResult} { - return Object.entries(obj) - .reduce((o, [k, v]) => { - o[k] = fn(v as any, k as any) - return o - }, {} as any) -} - -export function resolvePackage(id: string, paths: { paths: string[] }): string { +export function resolvePackage(id: string, paths: {paths: string[]}): string { return require.resolve(id, paths) } @@ -25,9 +13,10 @@ function displayWarnings() { } export function Debug(...scope: string[]): (..._: any) => void { - if (!debug) return (..._: any[]) => { - // noop - } + if (!debug) + return (..._: any[]) => { + // noop + } const d = debug(['config', ...scope].join(':')) if (d.enabled) displayWarnings() @@ -59,7 +48,7 @@ export function getPermutations(arr: string[]): Array { } export function getCommandIdPermutations(commandId: string): string[] { - return getPermutations(commandId.split(':')).flatMap(c => c.join(':')) + return getPermutations(commandId.split(':')).flatMap((c) => c.join(':')) } /** @@ -82,4 +71,4 @@ export function getCommandIdPermutations(commandId: string): string[] { * @returns string[] */ export const collectUsableIds = (commandIds: string[]): Set => - new Set(commandIds.flatMap(id => id.split(':').map((_, i, a) => a.slice(0, i + 1).join(':')))) + new Set(commandIds.flatMap((id) => id.split(':').map((_, i, a) => a.slice(0, i + 1).join(':')))) diff --git a/src/errors/errors/cli.ts b/src/errors/errors/cli.ts index b4aa7e869..cb1bca95f 100644 --- a/src/errors/errors/cli.ts +++ b/src/errors/errors/cli.ts @@ -10,9 +10,9 @@ import wrap from 'wrap-ansi' * properties specific to internal oclif error handling */ -export function addOclifExitCode(error: Record, options?: { exit?: number | false }): OclifError { +export function addOclifExitCode(error: Record, options?: {exit?: number | false}): OclifError { if (!('oclif' in error)) { - (error as unknown as OclifError).oclif = {} + ;(error as unknown as OclifError).oclif = {} } error.oclif.exit = options?.exit === undefined ? 2 : options.exit @@ -25,7 +25,7 @@ export class CLIError extends Error implements OclifError { code?: string suggestions?: string[] - constructor(error: string | Error, options: { exit?: number | false } & PrettyPrintableError = {}) { + constructor(error: string | Error, options: {exit?: number | false} & PrettyPrintableError = {}) { super(error instanceof Error ? error.message : error) addOclifExitCode(this, options) this.code = options.code diff --git a/src/errors/errors/pretty-print.ts b/src/errors/errors/pretty-print.ts index 3067501db..46c6efb4e 100644 --- a/src/errors/errors/pretty-print.ts +++ b/src/errors/errors/pretty-print.ts @@ -5,7 +5,7 @@ import indent from 'indent-string' import wrap from 'wrap-ansi' // These exist for backwards compatibility with CLIError -type CLIErrorDisplayOptions = { name?: string; bang?: string } +type CLIErrorDisplayOptions = {name?: string; bang?: string} export function applyPrettyPrintOptions(error: Error, options: PrettyPrintableError): PrettyPrintableError { const prettyErrorKeys: (keyof PrettyPrintableError)[] = ['message', 'code', 'ref', 'suggestions'] @@ -13,7 +13,7 @@ export function applyPrettyPrintOptions(error: Error, options: PrettyPrintableEr for (const key of prettyErrorKeys) { const applyOptionsKey = !(key in error) && options[key] if (applyOptionsKey) { - (error as any)[key] = options[key] + ;(error as any)[key] = options[key] } } @@ -25,7 +25,7 @@ const formatSuggestions = (suggestions?: string[]): string | undefined => { if (!suggestions || suggestions.length === 0) return undefined if (suggestions.length === 1) return `${label} ${suggestions[0]}` - const multiple = suggestions.map(suggestion => `* ${suggestion}`).join('\n') + const multiple = suggestions.map((suggestion) => `* ${suggestion}`).join('\n') return `${label}\n${indent(multiple, 2)}` } @@ -44,8 +44,8 @@ export default function prettyPrint(error: Error & PrettyPrintableError & CLIErr const formattedReference = ref ? `Reference: ${ref}` : undefined const formatted = [formattedHeader, formattedCode, formattedSuggestions, formattedReference] - .filter(Boolean) - .join('\n') + .filter(Boolean) + .join('\n') let output = wrap(formatted, errtermwidth - 6, {trim: false, hard: true} as any) output = indent(output, 3) diff --git a/src/errors/handle.ts b/src/errors/handle.ts index 138e73d8e..c359c0e41 100644 --- a/src/errors/handle.ts +++ b/src/errors/handle.ts @@ -2,7 +2,7 @@ /* eslint-disable unicorn/no-process-exit */ import {OclifError, PrettyPrintableError} from '../interfaces' import {CLIError} from './errors/cli' -import {ExitError} from '.' +import {ExitError} from './errors/exit' import clean from 'clean-stack' import {config} from './config' import prettyPrint from './errors/pretty-print' @@ -40,9 +40,10 @@ export async function handle(err: ErrorToHandle): Promise { config.errorLogger.log(stack) } - await config.errorLogger.flush() - .then(() => Exit.exit(exitCode)) - .catch(console.error) + await config.errorLogger + .flush() + .then(() => Exit.exit(exitCode)) + .catch(console.error) } else Exit.exit(exitCode) } catch (error: any) { console.error(err.stack) diff --git a/src/errors/logger.ts b/src/errors/logger.ts index f2a79c329..83549ed32 100644 --- a/src/errors/logger.ts +++ b/src/errors/logger.ts @@ -4,10 +4,11 @@ import stripAnsi = require('strip-ansi') const timestamp = () => new Date().toISOString() let timer: any -const wait = (ms: number) => new Promise(resolve => { - if (timer) timer.unref() - timer = setTimeout(() => resolve(null), ms) -}) +const wait = (ms: number) => + new Promise((resolve) => { + if (timer) timer.unref() + timer = setTimeout(() => resolve(null), ms) + }) function chomp(s: string): string { if (s.endsWith('\n')) return s.replace(/\n$/, '') @@ -23,7 +24,7 @@ export class Logger { log(msg: string): void { msg = stripAnsi(chomp(msg)) - const lines = msg.split('\n').map(l => `${timestamp()} ${l}`.trimEnd()) + const lines = msg.split('\n').map((l) => `${timestamp()} ${l}`.trimEnd()) this.buffer.push(...lines) this.flush(50).catch(console.error) } diff --git a/src/execute.ts b/src/execute.ts index d5e95aa25..29180f46c 100644 --- a/src/execute.ts +++ b/src/execute.ts @@ -46,14 +46,12 @@ import {settings} from './settings' * })() * ``` */ -export async function execute( - options: { - dir: string; - args?: string[]; - loadOptions?: LoadOptions; - development?: boolean; - }, -): Promise { +export async function execute(options: { + dir: string + args?: string[] + loadOptions?: LoadOptions + development?: boolean +}): Promise { if (options.development) { // In dev mode -> use ts-node and dev plugins process.env.NODE_ENV = 'development' @@ -61,9 +59,9 @@ export async function execute( } return run(options.args ?? process.argv.slice(2), options.loadOptions ?? options.dir) - .then(async result => { - flush() - return result - }) - .catch(async error => handle(error)) + .then(async (result) => { + flush() + return result + }) + .catch(async (error) => handle(error)) } diff --git a/src/flags.ts b/src/flags.ts index 61df7db68..b338db976 100644 --- a/src/flags.ts +++ b/src/flags.ts @@ -1,45 +1,45 @@ /* eslint-disable valid-jsdoc */ import {BooleanFlag, CustomOptions, FlagDefinition, OptionFlag} from './interfaces' -import {dirExists, fileExists} from './util' +import {dirExists, fileExists} from './util/fs' import {CLIError} from './errors' import {URL} from 'node:url' import {loadHelpClass} from './help' -type NotArray = T extends Array ? never: T; +type NotArray = T extends Array ? never : T export function custom( defaults: Partial> & { multiple: true - } & ( - {required: true} | {default: OptionFlag['default']} - ), + } & ({required: true} | {default: OptionFlag['default']}), ): FlagDefinition export function custom( - defaults: Partial> & { - multiple?: false | undefined; - } & ( - {required: true} | {default: OptionFlag, P>['default']} - ), + defaults: Partial, P>> & { + multiple?: false | undefined + } & ({required: true} | {default: OptionFlag, P>['default']}), ): FlagDefinition export function custom( - defaults: Partial> & { - default?: OptionFlag, P>['default'] | undefined; - multiple?: false | undefined; - required?: false | undefined; + defaults: Partial, P>> & { + default?: OptionFlag, P>['default'] | undefined + multiple?: false | undefined + required?: false | undefined }, ): FlagDefinition export function custom( defaults: Partial> & { - multiple: true; - default?: OptionFlag['default'] | undefined; - required?: false | undefined; + multiple: true + default?: OptionFlag['default'] | undefined + required?: false | undefined }, ): FlagDefinition -export function custom(): FlagDefinition +export function custom(): FlagDefinition< + T, + P, + {multiple: false; requiredOrDefaulted: false} +> /** * Create a custom flag. * @@ -70,9 +70,7 @@ export function custom( }) } -export function boolean( - options: Partial> = {}, -): BooleanFlag { +export function boolean(options: Partial> = {}): BooleanFlag { return { parse: async (b, _) => b, ...options, @@ -81,10 +79,9 @@ export function boolean( } as BooleanFlag } -export const integer = custom({ +export const integer = custom({ async parse(input, _, opts) { - if (!/^-?\d+$/.test(input)) - throw new CLIError(`Expected an integer but received: ${input}`) + if (!/^-?\d+$/.test(input)) throw new CLIError(`Expected an integer but received: ${input}`) const num = Number.parseInt(input, 10) if (opts.min !== undefined && num < opts.min) throw new CLIError(`Expected an integer greater than or equal to ${opts.min} but received: ${input}`) @@ -126,64 +123,65 @@ export const url = custom({ export const string = custom() -export const version = (opts: Partial> = {}): BooleanFlag => boolean({ - description: 'Show CLI version.', - ...opts, - async parse(_, ctx) { - ctx.log(ctx.config.userAgent) - ctx.exit(0) - }, -}) +export const version = (opts: Partial> = {}): BooleanFlag => + boolean({ + description: 'Show CLI version.', + ...opts, + async parse(_, ctx) { + ctx.log(ctx.config.userAgent) + ctx.exit(0) + }, + }) -export const help = (opts: Partial> = {}): BooleanFlag => boolean({ - description: 'Show CLI help.', - ...opts, - async parse(_, cmd) { - const Help = await loadHelpClass(cmd.config) - await new Help(cmd.config, cmd.config.pjson.helpOptions).showHelp(cmd.id ? [cmd.id, ...cmd.argv] : cmd.argv) - cmd.exit(0) - }, -}) +export const help = (opts: Partial> = {}): BooleanFlag => + boolean({ + description: 'Show CLI help.', + ...opts, + async parse(_, cmd) { + const Help = await loadHelpClass(cmd.config) + await new Help(cmd.config, cmd.config.pjson.helpOptions).showHelp(cmd.id ? [cmd.id, ...cmd.argv] : cmd.argv) + cmd.exit(0) + }, + }) -type ElementType> = T[number]; +type ElementType> = T[number] export function option( defaults: Partial[], P>> & { - options: T; + options: T multiple: true } & ( - {required: true} | { - default: OptionFlag[], P>['default'] | undefined; - } - ), -): FlagDefinition + | {required: true} + | { + default: OptionFlag[], P>['default'] | undefined + } + ), +): FlagDefinition<(typeof defaults.options)[number], P, {multiple: true; requiredOrDefaulted: true}> export function option( defaults: Partial, P>> & { - options: T; - multiple?: false | undefined; - } & ( - {required: true} | {default: OptionFlag, P>['default']} - ), -): FlagDefinition + options: T + multiple?: false | undefined + } & ({required: true} | {default: OptionFlag, P>['default']}), +): FlagDefinition<(typeof defaults.options)[number], P, {multiple: false; requiredOrDefaulted: true}> export function option( defaults: Partial, P>> & { - options: T; - default?: OptionFlag, P>['default'] | undefined; - multiple?: false | undefined; - required?: false | undefined; + options: T + default?: OptionFlag, P>['default'] | undefined + multiple?: false | undefined + required?: false | undefined }, -): FlagDefinition +): FlagDefinition<(typeof defaults.options)[number], P, {multiple: false; requiredOrDefaulted: false}> export function option( defaults: Partial[], P>> & { - options: T; - multiple: true; - default?: OptionFlag[], P>['default'] | undefined; - required?: false | undefined; + options: T + multiple: true + default?: OptionFlag[], P>['default'] | undefined + required?: false | undefined }, -): FlagDefinition +): FlagDefinition<(typeof defaults.options)[number], P, {multiple: true; requiredOrDefaulted: false}> /** * Create a custom flag that infers the flag type from the provided options. @@ -199,7 +197,7 @@ export function option( */ export function option( defaults: Partial, P>> & {options: T}, -): FlagDefinition { +): FlagDefinition<(typeof defaults.options)[number], P, {multiple: boolean; requiredOrDefaulted: boolean}> { return (options: any = {}) => ({ parse: async (input, _ctx, _opts) => input, ...defaults, diff --git a/src/help/command.ts b/src/help/command.ts index 7faae8825..87d866068 100644 --- a/src/help/command.ts +++ b/src/help/command.ts @@ -1,9 +1,10 @@ import * as Interfaces from '../interfaces' import {HelpFormatter, HelpSection, HelpSectionRenderer} from './formatter' -import {castArray, compact, ensureArgObject, sortBy} from '../util' +import {castArray, compact, sortBy} from '../util/util' import {Command} from '../command' import {DocOpts} from './docopts' import chalk from 'chalk' +import {ensureArgObject} from '../util/ensure-arg-object' import stripAnsi from 'strip-ansi' // Don't use os.EOL because we need to ensure that a string @@ -11,9 +12,7 @@ import stripAnsi from 'strip-ansi' // split on any platform, not just the os specific EOL at runtime. const POSSIBLE_LINE_FEED = /\r\n|\n/ -let { - dim, -} = chalk +let {dim} = chalk if (process.env.ConEmuANSI === 'ON') { // eslint-disable-next-line unicorn/consistent-destructuring @@ -24,35 +23,46 @@ export class CommandHelp extends HelpFormatter { constructor( public command: Command.Class | Command.Loadable | Command.Cached, public config: Interfaces.Config, - public opts: Interfaces.HelpOptions) { + public opts: Interfaces.HelpOptions, + ) { super(config, opts) } generate(): string { const cmd = this.command - const flags = sortBy(Object.entries(cmd.flags || {}) - .filter(([, v]) => !v.hidden) - .map(([k, v]) => { - v.name = k - return v - }), f => [!f.char, f.char, f.name]) - - const args = Object.values(ensureArgObject(cmd.args)).filter(a => !a.hidden) - const output = compact(this.sections().map(({header, generate}) => { - const body = generate({cmd, flags, args}, header) - // Generate can return a list of sections - if (Array.isArray(body)) { - return body.map(helpSection => helpSection && helpSection.body && this.section(helpSection.header, helpSection.body)).join('\n\n') - } + const flags = sortBy( + Object.entries(cmd.flags || {}) + .filter(([, v]) => !v.hidden) + .map(([k, v]) => { + v.name = k + return v + }), + (f) => [!f.char, f.char, f.name], + ) + + const args = Object.values(ensureArgObject(cmd.args)).filter((a) => !a.hidden) + const output = compact( + this.sections().map(({header, generate}) => { + const body = generate({cmd, flags, args}, header) + // Generate can return a list of sections + if (Array.isArray(body)) { + return body + .map((helpSection) => helpSection && helpSection.body && this.section(helpSection.header, helpSection.body)) + .join('\n\n') + } - return body && this.section(header, body) - })).join('\n\n') + return body && this.section(header, body) + }), + ).join('\n\n') return output } - protected groupFlags(flags: Array): {mainFlags: Array; flagGroups: {[name: string]: Array}} { + protected groupFlags(flags: Array): { + mainFlags: Array + flagGroups: {[name: string]: Array} + } { const mainFlags: Array = [] - const flagGroups: { [index: string]: Array } = {} + const flagGroups: {[index: string]: Array} = {} for (const flag of flags) { const group = flag.helpGroup @@ -68,7 +78,7 @@ export class CommandHelp extends HelpFormatter { return {mainFlags, flagGroups} } - protected sections(): Array<{ header: string; generate: HelpSectionRenderer }> { + protected sections(): Array<{header: string; generate: HelpSectionRenderer}> { return [ { header: this.opts.usageHeader || 'USAGE', @@ -121,18 +131,21 @@ export class CommandHelp extends HelpFormatter { protected usage(): string { const {usage} = this.command const body = (usage ? castArray(usage) : [this.defaultUsage()]) - .map(u => { - const allowedSpacing = this.opts.maxWidth - this.indentSpacing - const line = `$ ${this.config.bin} ${u}`.trim() - if (line.length > allowedSpacing) { - const splitIndex = line.slice(0, Math.max(0, allowedSpacing)).lastIndexOf(' ') - return line.slice(0, Math.max(0, splitIndex)) + '\n' - + this.indent(this.wrap(line.slice(Math.max(0, splitIndex)), this.indentSpacing * 2)) - } + .map((u) => { + const allowedSpacing = this.opts.maxWidth - this.indentSpacing + const line = `$ ${this.config.bin} ${u}`.trim() + if (line.length > allowedSpacing) { + const splitIndex = line.slice(0, Math.max(0, allowedSpacing)).lastIndexOf(' ') + return ( + line.slice(0, Math.max(0, splitIndex)) + + '\n' + + this.indent(this.wrap(line.slice(Math.max(0, splitIndex)), this.indentSpacing * 2)) + ) + } - return this.wrap(line) - }) - .join('\n') + return this.wrap(line) + }) + .join('\n') return body } @@ -144,7 +157,10 @@ export class CommandHelp extends HelpFormatter { return compact([ this.command.id, - Object.values(this.command.args ?? {})?.filter(a => !a.hidden).map(a => this.arg(a)).join(' '), + Object.values(this.command.args ?? {}) + ?.filter((a) => !a.hidden) + .map((a) => this.arg(a)) + .join(' '), ]).join(' ') } @@ -156,10 +172,9 @@ export class CommandHelp extends HelpFormatter { description = (cmd.description || '').split(POSSIBLE_LINE_FEED).slice(1) } else if (cmd.description) { const summary = cmd.summary ? `${cmd.summary}\n` : null - description = summary ? [ - ...summary.split(POSSIBLE_LINE_FEED), - ...(cmd.description || '').split(POSSIBLE_LINE_FEED), - ] : (cmd.description || '').split(POSSIBLE_LINE_FEED) + description = summary + ? [...summary.split(POSSIBLE_LINE_FEED), ...(cmd.description || '').split(POSSIBLE_LINE_FEED)] + : (cmd.description || '').split(POSSIBLE_LINE_FEED) } if (description) { @@ -169,55 +184,56 @@ export class CommandHelp extends HelpFormatter { protected aliases(aliases: string[] | undefined): string | undefined { if (!aliases || aliases.length === 0) return - const body = aliases.map(a => ['$', this.config.bin, a].join(' ')).join('\n') + const body = aliases.map((a) => ['$', this.config.bin, a].join(' ')).join('\n') return body } protected examples(examples: Command.Example[] | undefined | string): string | undefined { if (!examples || examples.length === 0) return - const body = castArray(examples).map(a => { - let description - let commands - if (typeof a === 'string') { - const lines = a - .split(POSSIBLE_LINE_FEED) - .filter(Boolean) - // If the example is \n then format correctly - if (lines.length >= 2 && !this.isCommand(lines[0]) && lines.slice(1).every(i => this.isCommand(i))) { - description = lines[0] - commands = lines.slice(1) + const body = castArray(examples) + .map((a) => { + let description + let commands + if (typeof a === 'string') { + const lines = a.split(POSSIBLE_LINE_FEED).filter(Boolean) + // If the example is \n then format correctly + if (lines.length >= 2 && !this.isCommand(lines[0]) && lines.slice(1).every((i) => this.isCommand(i))) { + description = lines[0] + commands = lines.slice(1) + } else { + return lines.map((line) => this.formatIfCommand(line)).join('\n') + } } else { - return lines.map(line => this.formatIfCommand(line)).join('\n') + description = a.description + commands = [a.command] } - } else { - description = a.description - commands = [a.command] - } - const multilineSeparator - = this.config.platform === 'win32' - ? (this.config.shell.includes('powershell') ? '`' : '^') - : '\\' - - // The command will be indented in the section, which is also indented - const finalIndentedSpacing = this.indentSpacing * 2 - const multilineCommands = commands.map(c => - // First indent keeping room for escaped newlines - this.indent(this.wrap(this.formatIfCommand(c), finalIndentedSpacing + 4)) - // Then add the escaped newline - .split(POSSIBLE_LINE_FEED).join(` ${multilineSeparator}\n `), - ).join('\n') - - return `${this.wrap(description, finalIndentedSpacing)}\n\n${multilineCommands}` - }).join('\n\n') + const multilineSeparator = + this.config.platform === 'win32' ? (this.config.shell.includes('powershell') ? '`' : '^') : '\\' + + // The command will be indented in the section, which is also indented + const finalIndentedSpacing = this.indentSpacing * 2 + const multilineCommands = commands + .map((c) => + // First indent keeping room for escaped newlines + this.indent(this.wrap(this.formatIfCommand(c), finalIndentedSpacing + 4)) + // Then add the escaped newline + .split(POSSIBLE_LINE_FEED) + .join(` ${multilineSeparator}\n `), + ) + .join('\n') + + return `${this.wrap(description, finalIndentedSpacing)}\n\n${multilineCommands}` + }) + .join('\n\n') return body } protected args(args: Command.Arg.Any[]): [string, string | undefined][] | undefined { - if (args.filter(a => a.description).length === 0) return + if (args.filter((a) => a.description).length === 0) return - return args.map(a => { + return args.map((a) => { const name = a.name.toUpperCase() let description = a.description || '' if (a.default) description = `[default: ${a.default}] ${description}` @@ -266,7 +282,7 @@ export class CommandHelp extends HelpFormatter { protected flags(flags: Array): [string, string | undefined][] | undefined { if (flags.length === 0) return - return flags.map(flag => { + return flags.map((flag) => { const left = this.flagHelpLabel(flag) let right = flag.summary || flag.description || '' @@ -285,16 +301,21 @@ export class CommandHelp extends HelpFormatter { } protected flagsDescriptions(flags: Array): string | undefined { - const flagsWithExtendedDescriptions = flags.filter(flag => flag.summary && flag.description) + const flagsWithExtendedDescriptions = flags.filter((flag) => flag.summary && flag.description) if (flagsWithExtendedDescriptions.length === 0) return - const body = flagsWithExtendedDescriptions.map(flag => { - // Guaranteed to be set because of the filter above, but make ts happy - const summary = flag.summary || '' - let flagHelp = this.flagHelpLabel(flag, true) - flagHelp += flagHelp.length + summary.length + 2 < this.opts.maxWidth ? ' ' + summary : '\n\n' + this.indent(this.wrap(summary, this.indentSpacing * 2)) - return `${flagHelp}\n\n${this.indent(this.wrap(flag.description || '', this.indentSpacing * 2))}` - }).join('\n\n') + const body = flagsWithExtendedDescriptions + .map((flag) => { + // Guaranteed to be set because of the filter above, but make ts happy + const summary = flag.summary || '' + let flagHelp = this.flagHelpLabel(flag, true) + flagHelp += + flagHelp.length + summary.length + 2 < this.opts.maxWidth + ? ' ' + summary + : '\n\n' + this.indent(this.wrap(summary, this.indentSpacing * 2)) + return `${flagHelp}\n\n${this.indent(this.wrap(flag.description || '', this.indentSpacing * 2))}` + }) + .join('\n\n') return body } diff --git a/src/help/docopts.ts b/src/help/docopts.ts index 3ee8f8299..e1bc74d79 100644 --- a/src/help/docopts.ts +++ b/src/help/docopts.ts @@ -1,5 +1,5 @@ import {Command} from '../command' -import {ensureArgObject} from '../util' +import {ensureArgObject} from '../util/ensure-arg-object' /** * DocOpts - See http://docopt.org/. * @@ -65,11 +65,11 @@ export class DocOpts { // Create a new map with references to the flags that we can manipulate. this.flagMap = {} this.flagList = Object.entries(cmd.flags || {}) - .filter(([_, flag]) => !flag.hidden) - .map(([name, flag]) => { - this.flagMap[name] = flag - return flag - }) + .filter(([_, flag]) => !flag.hidden) + .map(([name, flag]) => { + this.flagMap[name] = flag + return flag + }) } public static generate(cmd: Command.Class | Command.Loadable | Command.Cached): string { @@ -79,7 +79,10 @@ export class DocOpts { public toString(): string { const opts = this.cmd.id === '.' || this.cmd.id === '' ? [] : ['<%= command.id %>'] if (this.cmd.args) { - const a = Object.values(ensureArgObject(this.cmd.args)).map(arg => arg.required ? arg.name.toUpperCase() : `[${arg.name.toUpperCase()}]`) || [] + const a = + Object.values(ensureArgObject(this.cmd.args)).map((arg) => + arg.required ? arg.name.toUpperCase() : `[${arg.name.toUpperCase()}]`, + ) || [] opts.push(...a) } @@ -87,11 +90,13 @@ export class DocOpts { opts.push(...Object.values(this.groupFlagElements())) } catch { // If there is an error, just return no usage so we don't fail command help. - opts.push(...this.flagList.map(flag => { - const name = flag.char ? `-${flag.char}` : `--${flag.name}` - if (flag.type === 'boolean') return name - return `${name}=` - })) + opts.push( + ...this.flagList.map((flag) => { + const name = flag.char ? `-${flag.char}` : `--${flag.name}` + if (flag.type === 'boolean') return name + return `${name}=` + }), + ) } return opts.join(' ') @@ -102,9 +107,15 @@ export class DocOpts { // Generate all doc opt elements for combining // Show required flags first - this.generateElements(elementMap, this.flagList.filter(flag => flag.required)) + this.generateElements( + elementMap, + this.flagList.filter((flag) => flag.required), + ) // Then show optional flags - this.generateElements(elementMap, this.flagList.filter(flag => !flag.required)) + this.generateElements( + elementMap, + this.flagList.filter((flag) => !flag.required), + ) for (const flag of this.flagList) { if (Array.isArray(flag.dependsOn)) { diff --git a/src/help/formatter.ts b/src/help/formatter.ts index ad039f851..a2d09239f 100644 --- a/src/help/formatter.ts +++ b/src/help/formatter.ts @@ -10,8 +10,13 @@ import width from 'string-width' import wrap from 'wrap-ansi' export type HelpSectionKeyValueTable = {name: string; description: string}[] -export type HelpSection = {header: string; body: string | HelpSectionKeyValueTable | [string, string | undefined][] | undefined} | undefined; -export type HelpSectionRenderer = (data: {cmd: Command.Class | Command.Loadable | Command.Cached; flags: Command.Flag.Any[]; args: Command.Arg.Any[]}, header: string) => HelpSection | HelpSection[] | string | undefined; +export type HelpSection = + | {header: string; body: string | HelpSectionKeyValueTable | [string, string | undefined][] | undefined} + | undefined +export type HelpSectionRenderer = ( + data: {cmd: Command.Class | Command.Loadable | Command.Cached; flags: Command.Flag.Any[]; args: Command.Arg.Any[]}, + header: string, +) => HelpSection | HelpSection[] | string | undefined export class HelpFormatter { indentSpacing = 2 @@ -101,7 +106,10 @@ export class HelpFormatter { return indent(body, spacing) } - public renderList(input: (string | undefined)[][], opts: {indentation: number; multiline?: boolean; stripAnsi?: boolean; spacer?: string}): string { + public renderList( + input: (string | undefined)[][], + opts: {indentation: number; multiline?: boolean; stripAnsi?: boolean; spacer?: string}, + ): string { if (input.length === 0) { return '' } @@ -128,7 +136,7 @@ export class HelpFormatter { } if (opts.multiline) return renderMultiline() - const maxLength = widestLine(input.map(i => i[0]).join('\n')) + const maxLength = widestLine(input.map((i) => i[0]).join('\n')) let output = '' let spacer = opts.spacer || '\n' let cur = '' @@ -149,7 +157,7 @@ export class HelpFormatter { if (opts.stripAnsi) right = stripAnsi(right) right = this.wrap(right.trim(), opts.indentation + maxLength + 2) - const [first, ...lines] = right!.split('\n').map(s => s.trim()) + const [first, ...lines] = right!.split('\n').map((s) => s.trim()) cur += ' '.repeat(maxLength - width(cur) + 2) cur += first if (lines.length === 0) { @@ -172,32 +180,37 @@ export class HelpFormatter { return output.trim() } - public section(header: string, body: string | HelpSection | HelpSectionKeyValueTable | [string, string | undefined][]): string { + public section( + header: string, + body: string | HelpSection | HelpSectionKeyValueTable | [string, string | undefined][], + ): string { // Always render template strings with the provided render function before wrapping and indenting let newBody: any if (typeof body! === 'string') { newBody = this.render(body!) } else if (Array.isArray(body)) { - newBody = (body! as [string, string | undefined | HelpSectionKeyValueTable][]).map(entry => { + newBody = (body! as [string, string | undefined | HelpSectionKeyValueTable][]).map((entry) => { if ('name' in entry) { const tableEntry = entry as unknown as {name: string; description: string} - return ([this.render(tableEntry.name), this.render(tableEntry.description)]) + return [this.render(tableEntry.name), this.render(tableEntry.description)] } const [left, right] = entry - return ([this.render(left), right && this.render(right as string)]) + return [this.render(left), right && this.render(right as string)] }) } else if ('header' in body!) { return this.section(body!.header, body!.body) } else { newBody = (body! as unknown as HelpSectionKeyValueTable) - .map((entry: { name: string; description: string }) => ([entry.name, entry.description])) - .map(([left, right]) => ([this.render(left), right && this.render(right)])) + .map((entry: {name: string; description: string}) => [entry.name, entry.description]) + .map(([left, right]) => [this.render(left), right && this.render(right)]) } const output = [ chalk.bold(header), - this.indent(Array.isArray(newBody) ? this.renderList(newBody, {stripAnsi: this.opts.stripAnsi, indentation: 2}) : newBody), + this.indent( + Array.isArray(newBody) ? this.renderList(newBody, {stripAnsi: this.opts.stripAnsi, indentation: 2}) : newBody, + ), ].join('\n') return this.opts.stripAnsi ? stripAnsi(output) : output } diff --git a/src/help/index.ts b/src/help/index.ts index 871d6c2f5..1c07b9bcb 100644 --- a/src/help/index.ts +++ b/src/help/index.ts @@ -1,19 +1,19 @@ import * as Interfaces from '../interfaces' -import {compact, sortBy, uniqBy} from '../util' +import {compact, sortBy, uniqBy} from '../util/util' import {formatCommandDeprecationWarning, getHelpFlagAdditions, standardizeIDFromArgv, toConfiguredId} from './util' import {Command} from '../command' import {CommandHelp} from './command' import {HelpFormatter} from './formatter' import RootHelp from './root' +import {cacheDefaultValue} from '../util/cache-default-value' import {error} from '../errors' import {format} from 'node:util' +import {load} from '../module-loader' import {stdout} from '../cli-ux/stream' -import {toCached} from '../config/config' - -import stripAnsi = require('strip-ansi') +import stripAnsi from 'strip-ansi' export {CommandHelp} from './command' -export {standardizeIDFromArgv, loadHelpClass, getHelpFlagAdditions, normalizeArgv} from './util' +export {standardizeIDFromArgv, getHelpFlagAdditions, normalizeArgv} from './util' function getHelpSubject(args: string[], config: Interfaces.Config): string | undefined { // for each help flag that starts with '--' create a new flag with same name sans '--' @@ -36,14 +36,14 @@ export abstract class HelpBase extends HelpFormatter { * Show help, used in multi-command CLIs * @param args passed into your command, useful for determining which type of help to display */ - public abstract showHelp(argv: string[]): Promise; + public abstract showHelp(argv: string[]): Promise /** * Show help for an individual command * @param command * @param topics */ - public abstract showCommandHelp(command: Command.Class, topics: Interfaces.Topic[]): Promise; + public abstract showCommandHelp(command: Command.Class, topics: Interfaces.Topic[]): Promise } export class Help extends HelpBase { @@ -58,7 +58,7 @@ export class Help extends HelpBase { private get _topics(): Interfaces.Topic[] { return this.config.topics.filter((topic: Interfaces.Topic) => { // it is assumed a topic has a child if it has children - const hasChild = this.config.topics.some(subTopic => subTopic.name.includes(`${topic.name}:`)) + const hasChild = this.config.topics.some((subTopic) => subTopic.name.includes(`${topic.name}:`)) return hasChild }) } @@ -66,18 +66,18 @@ export class Help extends HelpBase { protected get sortedCommands(): Command.Loadable[] { let {commands} = this.config - commands = commands.filter(c => this.opts.all || !c.hidden) - commands = sortBy(commands, c => c.id) - commands = uniqBy(commands, c => c.id) + commands = commands.filter((c) => this.opts.all || !c.hidden) + commands = sortBy(commands, (c) => c.id) + commands = uniqBy(commands, (c) => c.id) return commands } protected get sortedTopics(): Interfaces.Topic[] { let topics = this._topics - topics = topics.filter(t => this.opts.all || !t.hidden) - topics = sortBy(topics, t => t.name) - topics = uniqBy(topics, t => t.name) + topics = topics.filter((t) => this.opts.all || !t.hidden) + topics = sortBy(topics, (t) => t.name) + topics = uniqBy(topics, (t) => t.name) return topics } @@ -88,7 +88,7 @@ export class Help extends HelpBase { public async showHelp(argv: string[]): Promise { const originalArgv = argv.slice(1) - argv = argv.filter(arg => !getHelpFlagAdditions(this.config).includes(arg)) + argv = argv.filter((arg) => !getHelpFlagAdditions(this.config).includes(arg)) if (this.config.topicSeparator !== ':') argv = standardizeIDFromArgv(argv, this.config) const subject = getHelpSubject(argv, this.config) @@ -108,8 +108,14 @@ export class Help extends HelpBase { const command = this.config.findCommand(subject) if (command) { if (command.hasDynamicHelp && command.pluginType !== 'jit') { - const dynamicCommand = await toCached(await command.load()) - await this.showCommandHelp(dynamicCommand) + const loaded = await command.load() + for (const [name, flag] of Object.entries(loaded.flags)) { + if (flag.type === 'boolean' || !command.flags[name].hasDynamicHelp) continue + // eslint-disable-next-line no-await-in-loop + command.flags[name].default = await cacheDefaultValue(flag, false) + } + + await this.showCommandHelp(command) } else { await this.showCommandHelp(command) } @@ -128,7 +134,7 @@ export class Help extends HelpBase { if (matches.length > 0) { const result = await this.config.runHook('command_incomplete', { id: subject, - argv: originalArgv.filter(o => !subject.split(':').includes(o)), + argv: originalArgv.filter((o) => !subject.split(':').includes(o)), matches, }) if (result.successes.length > 0) return @@ -142,8 +148,12 @@ export class Help extends HelpBase { const name = command.id const depth = name.split(':').length - const subTopics = this.sortedTopics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) - const subCommands = this.sortedCommands.filter(c => c.id.startsWith(name + ':') && c.id.split(':').length === depth + 1) + const subTopics = this.sortedTopics.filter( + (t) => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1, + ) + const subCommands = this.sortedCommands.filter( + (c) => c.id.startsWith(name + ':') && c.id.split(':').length === depth + 1, + ) const plugin = this.config.plugins.get(command.pluginName!) const state = this.config.pjson?.oclif?.state || plugin?.pjson?.oclif?.state || command.state @@ -151,11 +161,17 @@ export class Help extends HelpBase { if (state) { this.log( state === 'deprecated' - ? `${formatCommandDeprecationWarning(toConfiguredId(name, this.config), command.deprecationOptions)}` + ? `${formatCommandDeprecationWarning(toConfiguredId(name, this.config), command.deprecationOptions)}\n` : `This command is in ${state}.\n`, ) } + if (command.deprecateAliases && command.aliases.includes(name)) { + const actualCmd = this.config.commands.find((c) => c.aliases.includes(name)) + const opts = {...command.deprecationOptions, ...(actualCmd ? {to: actualCmd.id} : {})} + this.log(`${formatCommandDeprecationWarning(toConfiguredId(name, this.config), opts)}\n`) + } + const summary = this.summary(command) if (summary) { this.log(summary + '\n') @@ -170,8 +186,8 @@ export class Help extends HelpBase { } if (subCommands.length > 0) { - const aliases:string[] = [] - const uniqueSubCommands: Command.Loadable[] = subCommands.filter(p => { + const aliases: string[] = [] + const uniqueSubCommands: Command.Loadable[] = subCommands.filter((p) => { aliases.push(...p.aliases) return !aliases.includes(p.id) }) @@ -186,19 +202,15 @@ export class Help extends HelpBase { const state = this.config.pjson?.oclif?.state if (state) { - this.log( - state === 'deprecated' - ? `${this.config.bin} is deprecated` - : `${this.config.bin} is in ${state}.\n`, - ) + this.log(state === 'deprecated' ? `${this.config.bin} is deprecated` : `${this.config.bin} is in ${state}.\n`) } this.log(this.formatRoot()) this.log('') if (!this.opts.all) { - rootTopics = rootTopics.filter(t => !t.name.includes(':')) - rootCommands = rootCommands.filter(c => !c.id.includes(':')) + rootTopics = rootTopics.filter((t) => !t.name.includes(':')) + rootCommands = rootCommands.filter((c) => !c.id.includes(':')) } if (rootTopics.length > 0) { @@ -207,7 +219,7 @@ export class Help extends HelpBase { } if (rootCommands.length > 0) { - rootCommands = rootCommands.filter(c => c.id) + rootCommands = rootCommands.filter((c) => c.id) this.log(this.formatCommands(rootCommands)) this.log('') } @@ -217,8 +229,12 @@ export class Help extends HelpBase { const {name} = topic const depth = name.split(':').length - const subTopics = this.sortedTopics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1) - const commands = this.sortedCommands.filter(c => c.id.startsWith(name + ':') && c.id.split(':').length === depth + 1) + const subTopics = this.sortedTopics.filter( + (t) => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1, + ) + const commands = this.sortedCommands.filter( + (c) => c.id.startsWith(name + ':') && c.id.split(':').length === depth + 1, + ) const state = this.config.pjson?.oclif?.state if (state) this.log(`This topic is in ${state}.\n`) @@ -244,7 +260,7 @@ export class Help extends HelpBase { protected formatCommand(command: Command.Class | Command.Loadable | Command.Cached): string { if (this.config.topicSeparator !== ':') { command.id = command.id.replaceAll(':', this.config.topicSeparator) - command.aliases = command.aliases && command.aliases.map(a => a.replaceAll(':', this.config.topicSeparator)) + command.aliases = command.aliases && command.aliases.map((a) => a.replaceAll(':', this.config.topicSeparator)) } const help = this.getCommandHelpClass(command) @@ -258,17 +274,17 @@ export class Help extends HelpBase { protected formatCommands(commands: Array): string { if (commands.length === 0) return '' - const body = this.renderList(commands.map(c => { - if (this.config.topicSeparator !== ':') c.id = c.id.replaceAll(':', this.config.topicSeparator) - return [ - c.id, - this.summary(c), - ] - }), { - spacer: '\n', - stripAnsi: this.opts.stripAnsi, - indentation: 2, - }) + const body = this.renderList( + commands.map((c) => { + if (this.config.topicSeparator !== ':') c.id = c.id.replaceAll(':', this.config.topicSeparator) + return [c.id, this.summary(c)] + }), + { + spacer: '\n', + stripAnsi: this.opts.stripAnsi, + indentation: 2, + }, + ) return this.section('COMMANDS', body) } @@ -305,17 +321,17 @@ export class Help extends HelpBase { protected formatTopics(topics: Interfaces.Topic[]): string { if (topics.length === 0) return '' - const body = this.renderList(topics.map(c => { - if (this.config.topicSeparator !== ':') c.name = c.name.replaceAll(':', this.config.topicSeparator) - return [ - c.name, - c.description && this.render(c.description.split('\n')[0]), - ] - }), { - spacer: '\n', - stripAnsi: this.opts.stripAnsi, - indentation: 2, - }) + const body = this.renderList( + topics.map((c) => { + if (this.config.topicSeparator !== ':') c.name = c.name.replaceAll(':', this.config.topicSeparator) + return [c.name, c.description && this.render(c.description.split('\n')[0])] + }), + { + spacer: '\n', + stripAnsi: this.opts.stripAnsi, + indentation: 2, + }, + ) return this.section('TOPICS', body) } @@ -327,3 +343,29 @@ export class Help extends HelpBase { stdout.write(format.apply(this, args) + '\n') } } + +interface HelpBaseDerived { + new (config: Interfaces.Config, opts?: Partial): HelpBase +} + +function extractClass(exported: any): HelpBaseDerived { + return exported && exported.default ? exported.default : exported +} + +export async function loadHelpClass(config: Interfaces.Config): Promise { + const {pjson} = config + const configuredClass = pjson.oclif?.helpClass + + if (configuredClass) { + try { + const exported = (await load(config, configuredClass)) as HelpBaseDerived + return extractClass(exported) as HelpBaseDerived + } catch (error: any) { + throw new Error( + `Unable to load configured help class "${configuredClass}", failed with message:\n${error.message}`, + ) + } + } + + return Help +} diff --git a/src/help/root.ts b/src/help/root.ts index 6aaa587f2..d8cd3754b 100644 --- a/src/help/root.ts +++ b/src/help/root.ts @@ -1,10 +1,13 @@ import * as Interfaces from '../interfaces' import {HelpFormatter} from './formatter' -import {compact} from '../util' -import stripAnsi = require('strip-ansi') +import {compact} from '../util/util' +import stripAnsi from 'strip-ansi' export default class RootHelp extends HelpFormatter { - constructor(public config: Interfaces.Config, public opts: Interfaces.HelpOptions) { + constructor( + public config: Interfaces.Config, + public opts: Interfaces.HelpOptions, + ) { super(config, opts) } @@ -12,12 +15,7 @@ export default class RootHelp extends HelpFormatter { let description = this.config.pjson.oclif.description || this.config.pjson.description || '' description = this.render(description) description = description.split('\n')[0] - let output = compact([ - description, - this.version(), - this.usage(), - this.description(), - ]).join('\n\n') + let output = compact([description, this.version(), this.usage(), this.description()]).join('\n\n') if (this.opts.stripAnsi) output = stripAnsi(output) return output } diff --git a/src/help/util.ts b/src/help/util.ts index 278c26050..0567e7e77 100644 --- a/src/help/util.ts +++ b/src/help/util.ts @@ -1,32 +1,6 @@ import * as ejs from 'ejs' -import {Deprecation, HelpOptions, Config as IConfig} from '../interfaces' -import {Help, HelpBase} from '.' +import {Deprecation, Config as IConfig} from '../interfaces' import {collectUsableIds} from '../config/util' -import {load} from '../module-loader' - -interface HelpBaseDerived { - new(config: IConfig, opts?: Partial): HelpBase; -} - -function extractClass(exported: any): HelpBaseDerived { - return exported && exported.default ? exported.default : exported -} - -export async function loadHelpClass(config: IConfig): Promise { - const {pjson} = config - const configuredClass = pjson && pjson.oclif && pjson.oclif.helpClass - - if (configuredClass) { - try { - const exported = await load(config, configuredClass) as HelpBaseDerived - return extractClass(exported) as HelpBaseDerived - } catch (error: any) { - throw new Error(`Unable to load configured help class "${configuredClass}", failed with message:\n${error.message}`) - } - } - - return Help -} export function template(context: any): (t: string) => string { function render(t: string): string { @@ -47,7 +21,7 @@ function collateSpacedCmdIDFromArgs(argv: string[], config: IConfig): string[] { const final: string[] = [] const idPresent = (id: string) => ids.has(id) - const finalizeId = (s?: string) => s ? [...final, s].join(':') : final.join(':') + const finalizeId = (s?: string) => (s ? [...final, s].join(':') : final.join(':')) const hasArgs = () => { const id = finalizeId() diff --git a/src/index.ts b/src/index.ts index 4eea78bfa..f7afe40b9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,12 +14,12 @@ checkCWD() export * as Args from './args' export * as Errors from './errors' -export * as Flags from './flags' export * as Interfaces from './interfaces' +export * as Flags from './flags' export * as Parser from './parser' export * as ux from './cli-ux' export {CommandHelp, HelpBase, Help, loadHelpClass} from './help' -export {Config, toCached, Plugin, tsPath} from './config' +export {Config, Plugin} from './config' export {HelpSection, HelpSectionRenderer, HelpSectionKeyValueTable} from './help/formatter' export {Settings, settings} from './settings' export {stdout, stderr} from './cli-ux/stream' diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index df7eebb5e..c476c9525 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -9,137 +9,142 @@ export type PlatformTypes = NodeJS.Platform | 'wsl' export type ArchTypes = 'arm' | 'arm64' | 'mips' | 'mipsel' | 'ppc' | 'ppc64' | 's390' | 's390x' | 'x32' | 'x64' | 'x86' export type PluginVersionDetail = { - version: string; - type: string; + version: string + type: string root: string -}; +} export type VersionDetails = { - cliVersion: string; - architecture: string; - nodeVersion: string; - pluginVersions?: Record; - osVersion?: string; - shell?: string; - rootPath?: string; + cliVersion: string + architecture: string + nodeVersion: string + pluginVersions?: Record + osVersion?: string + shell?: string + rootPath?: string } export interface Config { - readonly name: string; - readonly version: string; - readonly channel: string; - readonly pjson: PJSON.CLI; - readonly root: string; + readonly name: string + readonly version: string + readonly channel: string + readonly pjson: PJSON.CLI + readonly root: string /** * process.arch */ - readonly arch: ArchTypes; + readonly arch: ArchTypes /** * bin name of CLI command */ - readonly bin: string; + readonly bin: string /** * cache directory to use for CLI * * example ~/Library/Caches/mycli or ~/.cache/mycli */ - readonly cacheDir: string; + readonly cacheDir: string /** * config directory to use for CLI * * example: ~/.config/mycli */ - readonly configDir: string; + readonly configDir: string /** * data directory to use for CLI * * example: ~/.local/share/mycli */ - readonly dataDir: string; + readonly dataDir: string /** * base dirname to use in cacheDir/configDir/dataDir */ - readonly dirname: string; + readonly dirname: string /** * points to a file that should be appended to for error logs * * example: ~/Library/Caches/mycli/error.log */ - readonly errlog: string; + readonly errlog: string /** * path to home directory * * example: /home/myuser */ - readonly home: string; + readonly home: string /** * process.platform */ - readonly platform: PlatformTypes; + readonly platform: PlatformTypes /** * active shell */ - readonly shell: string; + readonly shell: string /** * user agent to use for http calls * * example: mycli/1.2.3 (darwin-x64) node-9.0.0 */ - readonly userAgent: string; + readonly userAgent: string /** * if windows */ - readonly windows: boolean; + readonly windows: boolean /** * debugging level * * set by ${BIN}_DEBUG or DEBUG=$BIN */ - readonly debug: number; + readonly debug: number /** * npm registry to use for installing plugins */ - readonly npmRegistry?: string; - readonly plugins: Map; - readonly binPath?: string; + readonly npmRegistry?: string + readonly plugins: Map + readonly binPath?: string /** * name of any bin aliases that will execute the cli */ - readonly binAliases?: string[]; - readonly nsisCustomization?: string; - readonly valid: boolean; - readonly flexibleTaxonomy?: boolean; - topicSeparator: ':' | ' '; - readonly commands: Command.Loadable[]; - readonly topics: Topic[]; - readonly commandIDs: string[]; + readonly binAliases?: string[] + readonly nsisCustomization?: string + readonly valid: boolean + readonly flexibleTaxonomy?: boolean + topicSeparator: ':' | ' ' + readonly commands: Command.Loadable[] + readonly topics: Topic[] + readonly commandIDs: string[] readonly versionDetails: VersionDetails - runCommand(id: string, argv?: string[], cachedCommand?: Command.Loadable): Promise; - runHook(event: T, opts: Hooks[T]['options'], timeout?: number, captureErrors?: boolean): Promise>; + runCommand(id: string, argv?: string[], cachedCommand?: Command.Loadable): Promise + runHook( + event: T, + opts: Hooks[T]['options'], + timeout?: number, + captureErrors?: boolean, + ): Promise> getAllCommandIDs(): string[] getAllCommands(): Command.Loadable[] - findCommand(id: string, opts: { must: true }): Command.Loadable; - findCommand(id: string, opts?: { must: boolean }): Command.Loadable | undefined; - findTopic(id: string, opts: { must: true }): Topic; - findTopic(id: string, opts?: { must: boolean }): Topic | undefined; - findMatches(id: string, argv: string[]): Command.Loadable[]; - scopedEnvVar(key: string): string | undefined; - scopedEnvVarKey(key: string): string; - scopedEnvVarKeys(key: string): string[]; - scopedEnvVarTrue(key: string): boolean; - s3Url(key: string): string; - s3Key(type: 'versioned' | 'unversioned', ext: '.tar.gz' | '.tar.xz', options?: Config.s3Key.Options): string; - s3Key(type: keyof PJSON.S3.Templates, options?: Config.s3Key.Options): string; - getPluginsList(): Plugin[]; + findCommand(id: string, opts: {must: true}): Command.Loadable + findCommand(id: string, opts?: {must: boolean}): Command.Loadable | undefined + findTopic(id: string, opts: {must: true}): Topic + findTopic(id: string, opts?: {must: boolean}): Topic | undefined + findMatches(id: string, argv: string[]): Command.Loadable[] + scopedEnvVar(key: string): string | undefined + scopedEnvVarKey(key: string): string + scopedEnvVarKeys(key: string): string[] + scopedEnvVarTrue(key: string): boolean + s3Url(key: string): string + s3Key(type: 'versioned' | 'unversioned', ext: '.tar.gz' | '.tar.xz', options?: Config.s3Key.Options): string + s3Key(type: keyof PJSON.S3.Templates, options?: Config.s3Key.Options): string + getPluginsList(): Plugin[] } export namespace Config { export namespace s3Key { export interface Options { - platform?: PlatformTypes; - arch?: ArchTypes; - [key: string]: any; + platform?: PlatformTypes + arch?: ArchTypes + [key: string]: any } } } diff --git a/src/interfaces/errors.ts b/src/interfaces/errors.ts index e0c083e3a..4486b7561 100644 --- a/src/interfaces/errors.ts +++ b/src/interfaces/errors.ts @@ -1,30 +1,30 @@ -export type CommandError = Error & {exitCode?: number}; +export type CommandError = Error & {exitCode?: number} export interface OclifError { oclif: { - exit?: number; - }; + exit?: number + } } export interface PrettyPrintableError { /** * message to display related to the error */ - message?: string; + message?: string /** * a unique error code for this error class */ - code?: string; + code?: string /** * a url to find out more information related to this error * or fixing the error */ - ref?: string; + ref?: string /** * a suggestion that may be useful or provide additional context */ - suggestions?: string[]; + suggestions?: string[] } diff --git a/src/interfaces/flags.ts b/src/interfaces/flags.ts index e8005e858..0fc8c4bb6 100644 --- a/src/interfaces/flags.ts +++ b/src/interfaces/flags.ts @@ -30,4 +30,4 @@ import {FlagInput} from './parser' * } * } */ -export type InferredFlags = T extends FlagInput ? F & { json: boolean | undefined; } : unknown +export type InferredFlags = T extends FlagInput ? F & {json: boolean | undefined} : unknown diff --git a/src/interfaces/help.ts b/src/interfaces/help.ts index 970aa9bea..8dacd3e70 100644 --- a/src/interfaces/help.ts +++ b/src/interfaces/help.ts @@ -1,20 +1,20 @@ export interface HelpOptions { - all?: boolean; - maxWidth: number; - stripAnsi?: boolean; + all?: boolean + maxWidth: number + stripAnsi?: boolean /** * By default, option values on flags are shown in the flag's description. This is because * long options list ruin the formatting of help. If a CLI knows all commands will not * do this, it can be turned off at a help level using this property. An individual flag * can set this using `flag.helpValue=options.join('|')`. */ - showFlagOptionsInTitle?: boolean; + showFlagOptionsInTitle?: boolean /** * By default, titles show flag values as ``. Some CLI developers may prefer titles * to show the flag name as the value. i.e. `--myflag=myflag` instead of `--myflag=`. * An individual flag can set this using `flag.helpValue=flag.name`. */ - showFlagNameInTitle?: boolean; + showFlagNameInTitle?: boolean /** * By default, the command summary is show at the top of the help and as the first line in * the command description. Repeating the summary in the command description improves readability @@ -22,14 +22,14 @@ export interface HelpOptions { * the description is treated as the summary. Some CLIs, especially with very simple commands, may * not want the duplication. */ - hideCommandSummaryInDescription?: boolean; + hideCommandSummaryInDescription?: boolean /** * Use USAGE, but some may want to use USAGE as used in gnu man pages. See help recommendations at * http://www.gnu.org/software/help2man/#--help-recommendations */ - usageHeader?: string; + usageHeader?: string /** * Use docopts as the usage. Defaults to true. */ - docopts?: boolean; + docopts?: boolean } diff --git a/src/interfaces/hooks.ts b/src/interfaces/hooks.ts index 928068c14..a6b526a5e 100644 --- a/src/interfaces/hooks.ts +++ b/src/interfaces/hooks.ts @@ -3,57 +3,60 @@ import {Config} from './config' import {Plugin} from './plugin' interface HookMeta { - options: Record; - return: any; + options: Record + return: any } export interface Hooks { - [event: string]: HookMeta; + [event: string]: HookMeta init: { - options: { id: string | undefined; argv: string[] }; - return: void; - }; + options: {id: string | undefined; argv: string[]} + return: void + } prerun: { - options: { Command: Command.Class; argv: string[] }; - return: void; - }; + options: {Command: Command.Class; argv: string[]} + return: void + } postrun: { options: { - Command: Command.Class; - result?: any; - argv: string[]; - }; - return: void; - }; + Command: Command.Class + result?: any + argv: string[] + } + return: void + } preupdate: { - options: {channel: string, version: string}; - return: void; - }; + options: {channel: string; version: string} + return: void + } update: { - options: {channel: string, version: string}; - return: void; - }; - 'command_not_found': { - options: {id: string; argv?: string[]}; - return: unknown; - }; - 'command_incomplete': { - options: {id: string; argv: string[], matches: Command.Loadable[]}; - return: unknown; - }; - 'jit_plugin_not_installed': { - options: {id: string; argv: string[]; command: Command.Loadable, pluginName: string; pluginVersion: string}; - return: unknown; - }; + options: {channel: string; version: string} + return: void + } + command_not_found: { + options: {id: string; argv?: string[]} + return: unknown + } + command_incomplete: { + options: {id: string; argv: string[]; matches: Command.Loadable[]} + return: unknown + } + jit_plugin_not_installed: { + options: {id: string; argv: string[]; command: Command.Loadable; pluginName: string; pluginVersion: string} + return: unknown + } 'plugins:preinstall': { options: { - plugin: { name: string; tag: string; type: 'npm' } | { url: string; type: 'repo' }; - }; - return: void; - }; + plugin: {name: string; tag: string; type: 'npm'} | {url: string; type: 'repo'} + } + return: void + } } -export type Hook = (this: Hook.Context, options: P[T]['options'] & {config: Config}) => Promise +export type Hook = ( + this: Hook.Context, + options: P[T]['options'] & {config: Config}, +) => Promise export namespace Hook { export type Init = Hook<'init'> @@ -67,17 +70,16 @@ export namespace Hook { export type JitPluginNotInstalled = Hook<'jit_plugin_not_installed'> export interface Context { - config: Config; - exit(code?: number): void; - error(message: string | Error, options?: {code?: string; exit?: number}): void; - warn(message: string): void; - log(message?: any, ...args: any[]): void; - debug(...args: any[]): void; + config: Config + exit(code?: number): void + error(message: string | Error, options?: {code?: string; exit?: number}): void + warn(message: string): void + log(message?: any, ...args: any[]): void + debug(...args: any[]): void } export interface Result { - successes: Array<{ result: T; plugin: Plugin }>; - failures: Array<{ error: Error; plugin: Plugin }>; + successes: Array<{result: T; plugin: Plugin}> + failures: Array<{error: Error; plugin: Plugin}> } } - diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 76e7ca5b4..bd22984ac 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -5,15 +5,7 @@ export type {HelpOptions} from './help' export type {Hook, Hooks} from './hooks' export type {Manifest} from './manifest' export type {S3Manifest} from './s3-manifest' -export type { - Arg, - BooleanFlag, - CustomOptions, - Deprecation, - Flag, - FlagDefinition, - OptionFlag, -} from './parser' +export type {Arg, BooleanFlag, CustomOptions, Deprecation, Flag, FlagDefinition, OptionFlag} from './parser' export type {PJSON} from './pjson' export type {Plugin, PluginOptions, Options} from './plugin' export type {Topic} from './topic' diff --git a/src/interfaces/manifest.ts b/src/interfaces/manifest.ts index 354a0d8ba..936068768 100644 --- a/src/interfaces/manifest.ts +++ b/src/interfaces/manifest.ts @@ -1,6 +1,6 @@ import {Command} from '../command' export type Manifest = { - version: string; - commands: {[id: string]: Command.Cached}; + version: string + commands: {[id: string]: Command.Cached} } diff --git a/src/interfaces/parser.ts b/src/interfaces/parser.ts index c95bfec7a..07c472891 100644 --- a/src/interfaces/parser.ts +++ b/src/interfaces/parser.ts @@ -1,48 +1,48 @@ import {AlphabetLowercase, AlphabetUppercase} from './alphabet' import {Command} from '../command' -export type FlagOutput = { [name: string]: any } -export type ArgOutput = { [name: string]: any } +export type FlagOutput = {[name: string]: any} +export type ArgOutput = {[name: string]: any} export type CLIParseErrorOptions = { parse: { - input?: ParserInput; - output?: ParserOutput; - }; + input?: ParserInput + output?: ParserOutput + } } -export type OutputArgs = { [P in keyof T]: any } -export type OutputFlags = { [P in keyof T]: any } +export type OutputArgs = {[P in keyof T]: any} +export type OutputFlags = {[P in keyof T]: any} export type ParserOutput< TFlags extends OutputFlags = any, BFlags extends OutputFlags = any, - TArgs extends OutputFlags = any + TArgs extends OutputFlags = any, > = { // Add in the --json flag so that it shows up in the types. // This is necessary because there's no way to optionally add the json flag based // on wether enableJsonFlag is set in the command. - flags: TFlags & BFlags & { json: boolean | undefined }; - args: TArgs; - argv: unknown[]; - raw: ParsingToken[]; - metadata: Metadata; - nonExistentFlags: string[]; + flags: TFlags & BFlags & {json: boolean | undefined} + args: TArgs + argv: unknown[] + raw: ParsingToken[] + metadata: Metadata + nonExistentFlags: string[] } -export type ArgToken = { type: 'arg'; arg: string; input: string } -export type FlagToken = { type: 'flag'; flag: string; input: string } +export type ArgToken = {type: 'arg'; arg: string; input: string} +export type FlagToken = {type: 'flag'; flag: string; input: string} export type ParsingToken = ArgToken | FlagToken -export type FlagUsageOptions = { displayRequired?: boolean } +export type FlagUsageOptions = {displayRequired?: boolean} export type Metadata = { - flags: { [key: string]: MetadataFlag }; + flags: {[key: string]: MetadataFlag} } export type MetadataFlag = { - setFromDefault?: boolean; - defaultHelp?: unknown; + setFromDefault?: boolean + defaultHelp?: unknown } export type ListItem = [string, string | undefined] @@ -51,8 +51,8 @@ export type List = ListItem[] export type CustomOptions = Record export type DefaultContext = { - options: T; - flags: Record; + options: T + flags: Record } /** @@ -68,7 +68,9 @@ export type FlagDefault = T | ((context: DefaultContext

= T | ((context: DefaultContext

>) => Promise) +export type FlagDefaultHelp = + | T + | ((context: DefaultContext

>) => Promise) /** * Type to define a default value for an arg. @@ -80,174 +82,185 @@ export type ArgDefault = T | ((context: DefaultContext = T | ((context: DefaultContext>) => Promise) +export type ArgDefaultHelp = + | T + | ((context: DefaultContext>) => Promise) -export type FlagRelationship = string | {name: string; when: (flags: Record) => Promise}; +export type FlagRelationship = string | {name: string; when: (flags: Record) => Promise} export type Relationship = { - type: 'all' | 'some' | 'none'; - flags: FlagRelationship[]; + type: 'all' | 'some' | 'none' + flags: FlagRelationship[] } export type Deprecation = { - to?: string; - message?: string; - version?: string | number; + to?: string + message?: string + version?: string | number } export type FlagProps = { - name: string; - char?: AlphabetLowercase | AlphabetUppercase; + name: string + char?: AlphabetLowercase | AlphabetUppercase /** * A short summary of flag usage to show in the flag list. * If not provided, description will be used. */ - summary?: string; + summary?: string /** * A description of flag usage. If summary is provided, the description * is assumed to be a longer description and will be shown in a separate * section within help. */ - description?: string; + description?: string /** * The flag label to show in help. Defaults to "[-] --" where - is * only displayed if the char is defined. */ - helpLabel?: string; + helpLabel?: string /** * Shows this flag in a separate list in the help. */ - helpGroup?: string; + helpGroup?: string /** * Accept an environment variable as input */ - env?: string; + env?: string /** * If true, the flag will not be shown in the help. */ - hidden?: boolean; + hidden?: boolean /** * If true, the flag will be required. */ - required?: boolean; + required?: boolean /** * List of flags that this flag depends on. */ - dependsOn?: string[]; + dependsOn?: string[] /** * List of flags that cannot be used with this flag. */ - exclusive?: string[]; + exclusive?: string[] /** * Exactly one of these flags must be provided. */ - exactlyOne?: string[]; + exactlyOne?: string[] /** * Define complex relationships between flags. */ - relationships?: Relationship[]; + relationships?: Relationship[] /** * Make the flag as deprecated. */ - deprecated?: true | Deprecation; + deprecated?: true | Deprecation /** * Alternate names that can be used for this flag. */ - aliases?: string[]; + aliases?: string[] /** - * Alternate short chars that can be used for this flag. - */ - charAliases?: (AlphabetLowercase | AlphabetUppercase)[]; + * Alternate short chars that can be used for this flag. + */ + charAliases?: (AlphabetLowercase | AlphabetUppercase)[] /** * Emit deprecation warning when a flag alias is provided */ deprecateAliases?: boolean - /** - * Delimiter to separate the values for a multiple value flag. - * Only respected if multiple is set to true. Default behavior is to - * separate on spaces. - */ - delimiter?: ',', /** * If true, the value returned by defaultHelp will not be cached in the oclif.manifest.json. * This is helpful if the default value contains sensitive data that shouldn't be published to npm. */ - noCacheDefault?: boolean; + noCacheDefault?: boolean } export type ArgProps = { - name: string; + name: string /** * A description of flag usage. If summary is provided, the description * is assumed to be a longer description and will be shown in a separate * section within help. */ - description?: string; + description?: string /** * If true, the flag will not be shown in the help. */ - hidden?: boolean; + hidden?: boolean /** * If true, the flag will be required. */ - required?: boolean; + required?: boolean - options?: string[]; - ignoreStdin?: boolean; + options?: string[] + ignoreStdin?: boolean /** * If true, the value returned by defaultHelp will not be cached in the oclif.manifest.json. * This is helpful if the default value contains sensitive data that shouldn't be published to npm. */ - noCacheDefault?: boolean; + noCacheDefault?: boolean } export type BooleanFlagProps = FlagProps & { - type: 'boolean'; - allowNo: boolean; + type: 'boolean' + allowNo: boolean } export type OptionFlagProps = FlagProps & { - type: 'option'; - helpValue?: string; - options?: readonly string[]; - multiple?: boolean; + type: 'option' + helpValue?: string + options?: readonly string[] + multiple?: boolean + /** + * Delimiter to separate the values for a multiple value flag. + * Only respected if multiple is set to true. Default behavior is to + * separate on spaces. + */ + delimiter?: ',' } export type FlagParserContext = Command & {token: FlagToken} -export type FlagParser = (input: I, context: FlagParserContext, opts: P & OptionFlag) => - T extends Array ? Promise : Promise +export type FlagParser = ( + input: I, + context: FlagParserContext, + opts: P & OptionFlag, +) => T extends Array ? Promise : Promise export type ArgParserContext = Command & {token: ArgToken} -export type ArgParser = (input: string, context: ArgParserContext, opts: P & Arg) => Promise +export type ArgParser = ( + input: string, + context: ArgParserContext, + opts: P & Arg, +) => Promise export type Arg = ArgProps & { - options?: T[]; - defaultHelp?: ArgDefaultHelp; - input: string[]; - default?: ArgDefault; - parse: ArgParser; + options?: T[] + defaultHelp?: ArgDefaultHelp + input: string[] + default?: ArgDefault + parse: ArgParser } export type ArgDefinition = { - (options: P & ({ required: true } | { default: ArgDefault }) & Partial>): Arg; - (options?: P & Partial>): Arg; + (options: P & ({required: true} | {default: ArgDefault}) & Partial>): Arg + (options?: P & Partial>): Arg } -export type BooleanFlag = FlagProps & BooleanFlagProps & { - /** - * specifying a default of false is the same as not specifying a default - */ - default?: FlagDefault; - parse: (input: boolean, context: FlagParserContext, opts: FlagProps & BooleanFlagProps) => Promise -} - -export type OptionFlag = FlagProps & OptionFlagProps & { - parse: FlagParser - defaultHelp?: FlagDefaultHelp; - input: string[]; - default?: FlagDefault; -} +export type BooleanFlag = FlagProps & + BooleanFlagProps & { + /** + * specifying a default of false is the same as not specifying a default + */ + default?: FlagDefault + parse: (input: boolean, context: FlagParserContext, opts: FlagProps & BooleanFlagProps) => Promise + } + +export type OptionFlag = FlagProps & + OptionFlagProps & { + parse: FlagParser + defaultHelp?: FlagDefaultHelp + input: string[] + default?: FlagDefault + } type ReturnTypeSwitches = {multiple: boolean; requiredOrDefaulted: boolean} @@ -260,16 +273,17 @@ type ReturnTypeSwitches = {multiple: boolean; requiredOrDefaulted: boolean} * - It's possible that T extends an Array, if so we want to return T so that the return isn't T[][] * - If requiredOrDefaulted is false && multiple is false, then the return type is T | undefined */ -type FlagReturnType = - R['requiredOrDefaulted'] extends true ? - R['multiple'] extends true ? - [T] extends [Array] ? T : - T[] : - T : - R['multiple'] extends true ? - [T] extends [Array] ? T | undefined : - T[] | undefined : - T | undefined +type FlagReturnType = R['requiredOrDefaulted'] extends true + ? R['multiple'] extends true + ? [T] extends [Array] + ? T + : T[] + : T + : R['multiple'] extends true + ? [T] extends [Array] + ? T | undefined + : T[] | undefined + : T | undefined /** * FlagDefinition types a function that takes `options` and returns an OptionFlag. @@ -291,95 +305,125 @@ type FlagReturnType = export type FlagDefinition< T, P = CustomOptions, - R extends ReturnTypeSwitches = {multiple: false, requiredOrDefaulted: false} + R extends ReturnTypeSwitches = {multiple: false; requiredOrDefaulted: false}, > = { ( // `multiple` is set to false and `required` is set to true in options, potentially overriding the default - options: P & { multiple: false; required: true } & Partial, P>> - ): OptionFlag>; + options: P & {multiple: false; required: true} & Partial< + OptionFlag, P> + >, + ): OptionFlag> ( // `multiple` is set to true and `required` is set to false in options, potentially overriding the default - options: P & { multiple: true; required: false } & Partial, P>> - ): OptionFlag>; + options: P & {multiple: true; required: false} & Partial< + OptionFlag, P> + >, + ): OptionFlag> ( // `multiple` is set to true and `required` is set to false in options, potentially overriding the default - options: P & { multiple: false; required: false } & Partial, P>> - ): OptionFlag>; + options: P & {multiple: false; required: false} & Partial< + OptionFlag, P> + >, + ): OptionFlag> ( - options: R['multiple'] extends true ? - // `multiple` is defaulted to true and either `required=true` or `default` are provided in options - P & ( - { required: true } | - { default: OptionFlag, P>['default'] } - ) & Partial, P>> : - // `multiple` is NOT defaulted to true and either `required=true` or `default` are provided in options - P & { multiple?: false | undefined } & ( - { required: true } | - { default: OptionFlag, P>['default'] } - ) & Partial, P>> - ): OptionFlag>; + options: R['multiple'] extends true + ? // `multiple` is defaulted to true and either `required=true` or `default` are provided in options + P & + ( + | {required: true} + | { + default: OptionFlag< + FlagReturnType, + P + >['default'] + } + ) & + Partial, P>> + : // `multiple` is NOT defaulted to true and either `required=true` or `default` are provided in options + P & {multiple?: false | undefined} & ( + | {required: true} + | { + default: OptionFlag< + FlagReturnType, + P + >['default'] + } + ) & + Partial, P>>, + ): OptionFlag> ( - options: R['multiple'] extends true ? - // `multiple` is defaulted to true and either `required=true` or `default` are provided in options - P & ( - { required: true } | - { default: OptionFlag, P>['default'] } - ) & Partial, P>> : - // `multiple` is NOT defaulted to true but `multiple=true` and either `required=true` or `default` are provided in options - P & { multiple: true } & ( - { required: true } | - { default: OptionFlag, P>['default'] } - ) & Partial, P>> - ): OptionFlag>; + options: R['multiple'] extends true + ? // `multiple` is defaulted to true and either `required=true` or `default` are provided in options + P & + ( + | {required: true} + | {default: OptionFlag, P>['default']} + ) & + Partial, P>> + : // `multiple` is NOT defaulted to true but `multiple=true` and either `required=true` or `default` are provided in options + P & {multiple: true} & ( + | {required: true} + | {default: OptionFlag, P>['default']} + ) & + Partial, P>>, + ): OptionFlag> ( // `multiple` is not provided in options but either `required=true` or `default` are provided - options: P & { multiple?: false | undefined; } & ( - { required: true } | - { default: OptionFlag, P>['default'] } - ) & Partial, P>> - ): OptionFlag>; + options: P & {multiple?: false | undefined} & ( + | {required: true} + | {default: OptionFlag, P>['default']} + ) & + Partial, P>>, + ): OptionFlag> ( // `required` is set to false in options, potentially overriding the default - options: P & { required: false } & Partial, P>> - ): OptionFlag>; + options: P & {required: false} & Partial< + OptionFlag, P> + >, + ): OptionFlag> ( // `multiple` is set to false in options, potentially overriding the default - options: P & { multiple: false } & Partial, P>> - ): OptionFlag>; + options: P & {multiple: false} & Partial< + OptionFlag, P> + >, + ): OptionFlag> ( // Catch all for when `multiple` is not set in the options - options?: P & { multiple?: false | undefined } & Partial, P>> - ): OptionFlag>; + options?: P & {multiple?: false | undefined} & Partial, P>>, + ): OptionFlag> ( // `multiple` is set to true in options, potentially overriding the default - options: P & { multiple: true } & Partial, P>> - ): OptionFlag>; + options: P & {multiple: true} & Partial< + OptionFlag, P> + >, + ): OptionFlag> } export type Flag = BooleanFlag | OptionFlag export type Input = { - flags?: FlagInput; - baseFlags?: FlagInput; - args?: ArgInput; - strict?: boolean; - context?: ParserContext; - '--'?: boolean; + flags?: FlagInput + baseFlags?: FlagInput + enableJsonFlag?: true | false + args?: ArgInput + strict?: boolean + context?: ParserContext + '--'?: boolean } export type ParserInput = { - argv: string[]; - flags: FlagInput; - args: ArgInput; - strict: boolean; - context: ParserContext | undefined; - '--'?: boolean; + argv: string[] + flags: FlagInput + args: ArgInput + strict: boolean + context: ParserContext | undefined + '--'?: boolean } export type ParserContext = Command & { - token?: FlagToken | ArgToken; + token?: FlagToken | ArgToken } -export type FlagInput = { [P in keyof T]: Flag } +export type FlagInput = {[P in keyof T]: Flag} -export type ArgInput = { [P in keyof T]: Arg } +export type ArgInput = {[P in keyof T]: Arg} diff --git a/src/interfaces/pjson.ts b/src/interfaces/pjson.ts index 22f0af21f..2da23f60f 100644 --- a/src/interfaces/pjson.ts +++ b/src/interfaces/pjson.ts @@ -1,115 +1,116 @@ import {HelpOptions} from './help' export interface PJSON { - [k: string]: any; - version: string; - dependencies?: {[name: string]: string}; - devDependencies?: {[name: string]: string}; + [k: string]: any + version: string + dependencies?: {[name: string]: string} + devDependencies?: {[name: string]: string} oclif: { - schema?: number; - bin?: string; - dirname?: string; - hooks?: Record; - plugins?: string[]; - }; + schema?: number + bin?: string + dirname?: string + hooks?: Record + plugins?: string[] + } } export namespace PJSON { export interface Plugin extends PJSON { - name: string; - version: string; + name: string + version: string oclif: PJSON['oclif'] & { - schema?: number; - description?: string; - topicSeparator?: ':' | ' '; - flexibleTaxonomy?: boolean; - hooks?: { [name: string]: (string | string[]) }; - commands?: string; - default?: string; - plugins?: string[]; - devPlugins?: string[]; - jitPlugins?: Record; - helpClass?: string; - helpOptions?: HelpOptions; - aliases?: { [name: string]: string | null }; - repositoryPrefix?: string; + schema?: number + description?: string + topicSeparator?: ':' | ' ' + flexibleTaxonomy?: boolean + hooks?: {[name: string]: string | string[]} + commands?: string + default?: string + plugins?: string[] + devPlugins?: string[] + jitPlugins?: Record + helpClass?: string + helpOptions?: HelpOptions + aliases?: {[name: string]: string | null} + repositoryPrefix?: string update: { - s3: S3; + s3: S3 autoupdate?: { - rollout?: number; - debounce?: number; - }; + rollout?: number + debounce?: number + } node: { - version?: string; - targets?: string[]; - }; - }; + version?: string + targets?: string[] + } + } topics?: { [k: string]: { - description?: string; - subtopics?: Plugin['oclif']['topics']; - hidden?: boolean; - }; - }; - additionalHelpFlags?: string[]; - additionalVersionFlags?: string[]; - state?: 'beta' | 'deprecated' | string; - }; + description?: string + subtopics?: Plugin['oclif']['topics'] + hidden?: boolean + } + } + additionalHelpFlags?: string[] + additionalVersionFlags?: string[] + state?: 'beta' | 'deprecated' | string + } } export interface S3 { - acl?: string; - bucket?: string; - host?: string; - xz?: boolean; - gz?: boolean; + acl?: string + bucket?: string + host?: string + xz?: boolean + gz?: boolean templates: { - target: S3.Templates; - vanilla: S3.Templates; - }; + target: S3.Templates + vanilla: S3.Templates + } } export namespace S3 { export interface Templates { - baseDir?: string; - versioned?: string; - unversioned?: string; - manifest?: string; + baseDir?: string + versioned?: string + unversioned?: string + manifest?: string } } export interface CLI extends Plugin { oclif: Plugin['oclif'] & { - schema?: number; - bin?: string; - binAliases?: string[]; - nsisCustomization?: string; - npmRegistry?: string; - scope?: string; - dirname?: string; - flexibleTaxonomy?: boolean; - jitPlugins?: Record; - }; + schema?: number + bin?: string + binAliases?: string[] + nsisCustomization?: string + npmRegistry?: string + scope?: string + dirname?: string + flexibleTaxonomy?: boolean + jitPlugins?: Record + } } export interface User extends PJSON { - private?: boolean; + private?: boolean oclif: PJSON['oclif'] & { - plugins?: (string | PluginTypes.User | PluginTypes.Link)[]; }; + plugins?: (string | PluginTypes.User | PluginTypes.Link)[] + } } export type PluginTypes = PluginTypes.User | PluginTypes.Link | {root: string} export namespace PluginTypes { export interface User { - type: 'user'; - name: string; - url?: string; - tag?: string; + type: 'user' + name: string + url?: string + tag?: string } export interface Link { - type: 'link'; - name: string; - root: string; + type: 'link' + name: string + root: string } } } diff --git a/src/interfaces/plugin.ts b/src/interfaces/plugin.ts index 50207bcba..eb1d8f42b 100644 --- a/src/interfaces/plugin.ts +++ b/src/interfaces/plugin.ts @@ -3,25 +3,26 @@ import {PJSON} from './pjson' import {Topic} from './topic' export interface PluginOptions { - root: string; - name?: string; - type?: string; - tag?: string; - ignoreManifest?: boolean; - errorOnManifestCreate?: boolean; - respectNoCacheDefault?: boolean; - parent?: Plugin; - children?: Plugin[]; - flexibleTaxonomy?: boolean; + root: string + name?: string + type?: string + tag?: string + ignoreManifest?: boolean + errorOnManifestCreate?: boolean + respectNoCacheDefault?: boolean + parent?: Plugin + children?: Plugin[] + flexibleTaxonomy?: boolean + isRoot?: boolean } export interface Options extends PluginOptions { - devPlugins?: boolean; - jitPlugins?: boolean; - userPlugins?: boolean; - channel?: string; - version?: string; - enablePerf?: boolean; + devPlugins?: boolean + jitPlugins?: boolean + userPlugins?: boolean + channel?: string + version?: string + enablePerf?: boolean plugins?: Map } @@ -29,57 +30,62 @@ export interface Plugin { /** * ../config version */ - _base: string; + _base: string /** * name from package.json */ - name: string; + name: string /** * aliases from package.json dependencies */ - alias: string; + alias: string /** * version from package.json * * example: 1.2.3 */ - version: string; + version: string /** * full package.json * * parsed with read-pkg */ - pjson: PJSON.Plugin | PJSON.CLI; + pjson: PJSON.Plugin | PJSON.CLI /** * used to tell the user how the plugin was installed * examples: core, link, user, dev */ - type: string; + type: string /** * Plugin is written in ESM or CommonJS */ - moduleType: 'module' | 'commonjs'; + moduleType: 'module' | 'commonjs' /** * base path of plugin */ - root: string; + root: string /** * npm dist-tag of plugin * only used for user plugins */ - tag?: string; + tag?: string /** * if it appears to be an npm package but does not look like it's really a CLI plugin, this is set to false */ - valid: boolean; + valid: boolean - commands: Command.Loadable[]; - hooks: { [k: string]: string[] }; - readonly commandIDs: string[]; - readonly topics: Topic[]; - readonly hasManifest: boolean; + /** + * True if the plugin is the root plugin. + */ + isRoot: boolean + + commands: Command.Loadable[] + hooks: {[k: string]: string[]} + readonly commandIDs: string[] + readonly topics: Topic[] + readonly hasManifest: boolean - findCommand(id: string, opts: { must: true }): Promise; - findCommand(id: string, opts?: { must: boolean }): Promise | undefined; - load(): Promise; + findCommand(id: string, opts: {must: true}): Promise + findCommand(id: string, opts?: {must: boolean}): Promise | undefined + load(): Promise } diff --git a/src/interfaces/s3-manifest.ts b/src/interfaces/s3-manifest.ts index 0c18366cb..d81e7c042 100644 --- a/src/interfaces/s3-manifest.ts +++ b/src/interfaces/s3-manifest.ts @@ -1,14 +1,14 @@ export interface S3Manifest { - version: string; - sha: string; - gz: string; - xz?: string; - sha256gz: string; - sha256xz?: string; - baseDir: string; - rollout?: number; + version: string + sha: string + gz: string + xz?: string + sha256gz: string + sha256xz?: string + baseDir: string + rollout?: number node: { - compatible: string; - recommended: string; - }; + compatible: string + recommended: string + } } diff --git a/src/interfaces/topic.ts b/src/interfaces/topic.ts index 3765d097c..4360f0247 100644 --- a/src/interfaces/topic.ts +++ b/src/interfaces/topic.ts @@ -1,5 +1,5 @@ export interface Topic { - name: string; - description?: string; - hidden?: boolean; + name: string + description?: string + hidden?: boolean } diff --git a/src/interfaces/ts-config.ts b/src/interfaces/ts-config.ts index d871120e2..31e88934d 100644 --- a/src/interfaces/ts-config.ts +++ b/src/interfaces/ts-config.ts @@ -1,20 +1,20 @@ export interface TSConfig { compilerOptions: { - rootDir?: string; - rootDirs?: string[]; - outDir?: string; - target?: string; - esModuleInterop?: boolean; - experimentalDecorators?: boolean; - emitDecoratorMetadata?: boolean; - module?: string; - moduleResolution?: string; - sourceMap?: boolean; - jsx?: boolean; - }; + rootDir?: string + rootDirs?: string[] + outDir?: string + target?: string + esModuleInterop?: boolean + experimentalDecorators?: boolean + emitDecoratorMetadata?: boolean + module?: string + moduleResolution?: string + sourceMap?: boolean + jsx?: boolean + } 'ts-node'?: { - esm?: boolean; - experimentalSpecifierResolution?: 'node' | 'explicit'; - scope?: boolean; + esm?: boolean + experimentalSpecifierResolution?: 'node' | 'explicit' + scope?: boolean } } diff --git a/src/module-loader.ts b/src/module-loader.ts index c3105d027..fc5ec8a42 100644 --- a/src/module-loader.ts +++ b/src/module-loader.ts @@ -4,7 +4,7 @@ import {extname, join, sep} from 'node:path' import {Command} from './command' import {ModuleLoadError} from './errors' import {pathToFileURL} from 'node:url' -import {tsPath} from './config' +import {tsPath} from './config/ts-node' const getPackageType = require('get-package-type') @@ -14,7 +14,7 @@ const getPackageType = require('get-package-type') // eslint-disable-next-line camelcase const s_EXTENSIONS: string[] = ['.ts', '.js', '.mjs', '.cjs'] -const isPlugin = (config: IConfig|IPlugin): config is IPlugin => (config).type !== undefined +const isPlugin = (config: IConfig | IPlugin): config is IPlugin => (config).type !== undefined /** * Loads and returns a module. @@ -32,11 +32,11 @@ const isPlugin = (config: IConfig|IPlugin): config is IPlugin => (confi * * @returns {Promise<*>} The entire ESM module from dynamic import or CJS module by require. */ -export async function load(config: IConfig|IPlugin, modulePath: string): Promise { +export async function load(config: IConfig | IPlugin, modulePath: string): Promise { let filePath: string | undefined let isESM: boolean | undefined try { - ({isESM, filePath} = resolvePath(config, modulePath)) + ;({isESM, filePath} = resolvePath(config, modulePath)) return isESM ? await import(pathToFileURL(filePath).href) : require(filePath) } catch (error: any) { if (error.code === 'MODULE_NOT_FOUND' || error.code === 'ERR_MODULE_NOT_FOUND') { @@ -64,11 +64,14 @@ export async function load(config: IConfig|IPlugin, modulePath: string): Promise * @returns {Promise<{isESM: boolean, module: *, filePath: string}>} An object with the loaded module & data including * file path and whether the module is ESM. */ -export async function loadWithData(config: IConfig|IPlugin, modulePath: string): Promise<{isESM: boolean; module: any; filePath: string}> { +export async function loadWithData( + config: IConfig | IPlugin, + modulePath: string, +): Promise<{isESM: boolean; module: any; filePath: string}> { let filePath: string | undefined let isESM: boolean | undefined try { - ({isESM, filePath} = resolvePath(config, modulePath)) + ;({isESM, filePath} = resolvePath(config, modulePath)) const module = isESM ? await import(pathToFileURL(filePath).href) : require(filePath) return {isESM, module, filePath} } catch (error: any) { @@ -97,7 +100,10 @@ export async function loadWithData(config: IConfig|IPlugin, modulePath: string): * @returns {Promise<{isESM: boolean, module: *, filePath: string}>} An object with the loaded module & data including * file path and whether the module is ESM. */ -export async function loadWithDataFromManifest(cached: Command.Cached, modulePath: string): Promise<{isESM: boolean; module: any; filePath: string}> { +export async function loadWithDataFromManifest( + cached: Command.Cached, + modulePath: string, +): Promise<{isESM: boolean; module: any; filePath: string}> { const {isESM, relativePath, id} = cached if (!relativePath) { throw new ModuleLoadError(`Cached command ${id} does not have a relative path`) @@ -123,33 +129,33 @@ export async function loadWithDataFromManifest(cached: Command.Cached, modulePat } /** - * For `.js` files uses `getPackageType` to determine if `type` is set to `module` in associated `package.json`. If - * the `modulePath` provided ends in `.mjs` it is assumed to be ESM. - * - * @param {string} filePath - File path to test. - * - * @returns {boolean} The modulePath is an ES Module. - * @see https://www.npmjs.com/package/get-package-type - */ + * For `.js` files uses `getPackageType` to determine if `type` is set to `module` in associated `package.json`. If + * the `modulePath` provided ends in `.mjs` it is assumed to be ESM. + * + * @param {string} filePath - File path to test. + * + * @returns {boolean} The modulePath is an ES Module. + * @see https://www.npmjs.com/package/get-package-type + */ export function isPathModule(filePath: string): boolean { const extension = extname(filePath).toLowerCase() switch (extension) { - case '.js': - case '.jsx': - case '.ts': - case '.tsx': { - return getPackageType.sync(filePath) === 'module' - } + case '.js': + case '.jsx': + case '.ts': + case '.tsx': { + return getPackageType.sync(filePath) === 'module' + } - case '.mjs': - case '.mts': { - return true - } + case '.mjs': + case '.mts': { + return true + } - default: { - return false - } + default: { + return false + } } } @@ -164,7 +170,7 @@ export function isPathModule(filePath: string): boolean { * * @returns {{isESM: boolean, filePath: string}} An object including file path and whether the module is ESM. */ -function resolvePath(config: IConfig|IPlugin, modulePath: string): {isESM: boolean; filePath: string} { +function resolvePath(config: IConfig | IPlugin, modulePath: string): {isESM: boolean; filePath: string} { let isESM: boolean let filePath: string | undefined @@ -172,7 +178,8 @@ function resolvePath(config: IConfig|IPlugin, modulePath: string): {isESM: boole filePath = require.resolve(modulePath) isESM = isPathModule(filePath) } catch { - filePath = (isPlugin(config) ? tsPath(config.root, modulePath, config) : tsPath(config.root, modulePath)) ?? modulePath + filePath = + (isPlugin(config) ? tsPath(config.root, modulePath, config) : tsPath(config.root, modulePath)) ?? modulePath let fileExists = false let isDirectory = false @@ -212,7 +219,7 @@ function resolvePath(config: IConfig|IPlugin, modulePath: string): {isESM: boole * * @returns {string | null} Modified file path including extension or null if file is not found. */ -function findFile(filePath: string) : string | null { +function findFile(filePath: string): string | null { // eslint-disable-next-line camelcase for (const extension of s_EXTENSIONS) { const testPath = `${filePath}${extension}` diff --git a/src/parser/errors.ts b/src/parser/errors.ts index 499120dbf..b52f20dd1 100644 --- a/src/parser/errors.ts +++ b/src/parser/errors.ts @@ -4,21 +4,21 @@ import {CLIError} from '../errors' import chalk from 'chalk' import {flagUsages} from './help' import {renderList} from '../cli-ux/list' -import {uniq} from '../util' +import {uniq} from '../util/util' export {CLIError} from '../errors' export type Validation = { - name: string; - status: 'success' | 'failed'; - validationFn: string; - reason?: string; + name: string + status: 'success' | 'failed' + validationFn: string + reason?: string } export class CLIParseError extends CLIError { public parse: CLIParseErrorOptions['parse'] - constructor(options: CLIParseErrorOptions & { message: string }) { + constructor(options: CLIParseErrorOptions & {message: string}) { options.message += '\nSee more help with --help' super(options.message) this.parse = options.parse @@ -28,11 +28,15 @@ export class CLIParseError extends CLIError { export class InvalidArgsSpecError extends CLIParseError { public args: ArgInput - constructor({args, parse}: CLIParseErrorOptions & { args: ArgInput }) { + constructor({args, parse}: CLIParseErrorOptions & {args: ArgInput}) { let message = 'Invalid argument spec' - const namedArgs = Object.values(args).filter(a => a.name) + const namedArgs = Object.values(args).filter((a) => a.name) if (namedArgs.length > 0) { - const list = renderList(namedArgs.map(a => [`${a.name} (${a.required ? 'required' : 'optional'})`, a.description] as [string, string])) + const list = renderList( + namedArgs.map( + (a) => [`${a.name} (${a.required ? 'required' : 'optional'})`, a.description] as [string, string], + ), + ) message += `:\n${list}` } @@ -44,17 +48,25 @@ export class InvalidArgsSpecError extends CLIParseError { export class RequiredArgsError extends CLIParseError { public args: Arg[] - constructor({args, parse, flagsWithMultiple}: CLIParseErrorOptions & { args: Arg[]; flagsWithMultiple?: string[] }) { + constructor({ + args, + parse, + flagsWithMultiple, + }: CLIParseErrorOptions & {args: Arg[]; flagsWithMultiple?: string[]}) { let message = `Missing ${args.length} required arg${args.length === 1 ? '' : 's'}` - const namedArgs = args.filter(a => a.name) + const namedArgs = args.filter((a) => a.name) if (namedArgs.length > 0) { - const list = renderList(namedArgs.map(a => [a.name, a.description] as [string, string])) + const list = renderList(namedArgs.map((a) => [a.name, a.description] as [string, string])) message += `:\n${list}` } if (flagsWithMultiple?.length) { - const flags = flagsWithMultiple.map(f => `--${f}`).join(', ') - message += `\n\nNote: ${flags} allow${flagsWithMultiple.length === 1 ? 's' : ''} multiple values. Because of this you need to provide all arguments before providing ${flagsWithMultiple.length === 1 ? 'that flag' : 'those flags'}.` + const flags = flagsWithMultiple.map((f) => `--${f}`).join(', ') + message += `\n\nNote: ${flags} allow${ + flagsWithMultiple.length === 1 ? 's' : '' + } multiple values. Because of this you need to provide all arguments before providing ${ + flagsWithMultiple.length === 1 ? 'that flag' : 'those flags' + }.` message += '\nAlternatively, you can use "--" to signify the end of the flags and the beginning of arguments.' } @@ -66,7 +78,7 @@ export class RequiredArgsError extends CLIParseError { export class RequiredFlagError extends CLIParseError { public flag: Flag - constructor({flag, parse}: CLIParseErrorOptions & { flag: Flag }) { + constructor({flag, parse}: CLIParseErrorOptions & {flag: Flag}) { const usage = renderList(flagUsages([flag], {displayRequired: false})) const message = `Missing required flag:\n${usage}` super({parse, message}) @@ -77,7 +89,7 @@ export class RequiredFlagError extends CLIParseError { export class UnexpectedArgsError extends CLIParseError { public args: unknown[] - constructor({parse, args}: CLIParseErrorOptions & { args: unknown[] }) { + constructor({parse, args}: CLIParseErrorOptions & {args: unknown[]}) { const message = `Unexpected argument${args.length === 1 ? '' : 's'}: ${args.join(', ')}` super({parse, message}) this.args = args @@ -87,7 +99,7 @@ export class UnexpectedArgsError extends CLIParseError { export class NonExistentFlagsError extends CLIParseError { public flags: string[] - constructor({parse, flags}: CLIParseErrorOptions & { flags: string[] }) { + constructor({parse, flags}: CLIParseErrorOptions & {flags: string[]}) { const message = `Nonexistent flag${flags.length === 1 ? '' : 's'}: ${flags.join(', ')}` super({parse, message}) this.flags = flags @@ -109,8 +121,8 @@ export class ArgInvalidOptionError extends CLIParseError { } export class FailedFlagValidationError extends CLIParseError { - constructor({parse, failed}: CLIParseErrorOptions & { failed: Validation[] }) { - const reasons = failed.map(r => r.reason) + constructor({parse, failed}: CLIParseErrorOptions & {failed: Validation[]}) { + const reasons = failed.map((r) => r.reason) const deduped = uniq(reasons) const errString = deduped.length === 1 ? 'error' : 'errors' const message = `The following ${errString} occurred:\n ${chalk.dim(deduped.join('\n '))}` diff --git a/src/parser/help.ts b/src/parser/help.ts index 7b6bfd396..7ade44bc4 100644 --- a/src/parser/help.ts +++ b/src/parser/help.ts @@ -1,6 +1,6 @@ import {Flag, FlagUsageOptions} from '../interfaces/parser' import chalk from 'chalk' -import {sortBy} from '../util' +import {sortBy} from '../util/util' export function flagUsage(flag: Flag, options: FlagUsageOptions = {}): [string, string | undefined] { const label = [] @@ -23,6 +23,5 @@ export function flagUsage(flag: Flag, options: FlagUsageOptions = {}): [str export function flagUsages(flags: Flag[], options: FlagUsageOptions = {}): [string, string | undefined][] { if (flags.length === 0) return [] - return sortBy(flags, f => [f.char ? -1 : 1, f.char, f.name]) - .map(f => flagUsage(f, options)) + return sortBy(flags, (f) => [f.char ? -1 : 1, f.char, f.name]).map((f) => flagUsage(f, options)) } diff --git a/src/parser/index.ts b/src/parser/index.ts index 95a62791e..ef3bccf30 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -7,7 +7,7 @@ export {flagUsages} from './help' export async function parse< TFlags extends OutputFlags, BFlags extends OutputFlags, - TArgs extends OutputArgs + TArgs extends OutputArgs, >(argv: string[], options: Input): Promise> { const input = { argv, @@ -22,4 +22,3 @@ export async function parse< await validate({input, output}) return output as ParserOutput } - diff --git a/src/parser/parse.ts b/src/parser/parse.ts index 6376ec80c..e7e45fcbb 100644 --- a/src/parser/parse.ts +++ b/src/parser/parse.ts @@ -17,14 +17,17 @@ import { ParserOutput, ParsingToken, } from '../interfaces/parser' -import {isTruthy, last, pickBy} from '../util' +import {isTruthy, last, pickBy} from '../util/util' import {createInterface} from 'node:readline' let debug: any try { - debug = process.env.CLI_FLAGS_DEBUG === '1' ? require('debug')('../parser') : () => { - // noop - } + debug = + process.env.CLI_FLAGS_DEBUG === '1' + ? require('debug')('../parser') + : () => { + // noop + } } catch { debug = () => { // noop @@ -42,7 +45,7 @@ const readStdin = async (): Promise => { if (stdin.isTTY) return null - return new Promise(resolve => { + return new Promise((resolve) => { let result = '' const ac = new AbortController() const {signal} = ac @@ -54,7 +57,7 @@ const readStdin = async (): Promise => { terminal: false, }) - rl.on('line', line => { + rl.on('line', (line) => { result += line }) @@ -64,12 +67,16 @@ const readStdin = async (): Promise => { resolve(result) }) - signal.addEventListener('abort', () => { - debug('stdin aborted') - clearTimeout(timeout) - rl.close() - resolve(null) - }, {once: true}) + signal.addEventListener( + 'abort', + () => { + debug('stdin aborted') + clearTimeout(timeout) + rl.close() + resolve(null) + }, + {once: true}, + ) }) } @@ -77,30 +84,38 @@ function isNegativeNumber(input: string): boolean { return /^-\d/g.test(input) } -const validateOptions = (flag: OptionFlag, input: string): string => { - if (flag.options && !flag.options.includes(input)) - throw new FlagInvalidOptionError(flag, input) +const validateOptions = (flag: OptionFlag, input: string): string => { + if (flag.options && !flag.options.includes(input)) throw new FlagInvalidOptionError(flag, input) return input } -export class Parser, BFlags extends OutputFlags, TArgs extends OutputArgs> { +export class Parser< + T extends ParserInput, + TFlags extends OutputFlags, + BFlags extends OutputFlags, + TArgs extends OutputArgs, +> { private readonly argv: string[] private readonly raw: ParsingToken[] = [] - private readonly booleanFlags: { [k: string]: BooleanFlag } - private readonly flagAliases: { [k: string]: BooleanFlag | OptionFlag } + private readonly booleanFlags: {[k: string]: BooleanFlag} + private readonly flagAliases: {[k: string]: BooleanFlag | OptionFlag} private readonly context: ParserContext private currentFlag?: OptionFlag constructor(private readonly input: T) { - this.context = input.context ?? {} as ParserContext + this.context = input.context ?? ({} as ParserContext) this.argv = [...input.argv] this._setNames() - this.booleanFlags = pickBy(input.flags, f => f.type === 'boolean') as any - this.flagAliases = Object.fromEntries(Object.values(input.flags).flatMap(flag => ([...flag.aliases ?? [], ...flag.charAliases ?? []]).map(a => [a, flag]))) + this.booleanFlags = pickBy(input.flags, (f) => f.type === 'boolean') as any + this.flagAliases = Object.fromEntries( + Object.values(input.flags).flatMap((flag) => + [...(flag.aliases ?? []), ...(flag.charAliases ?? [])].map((a) => [a, flag]), + ), + ) } public async parse(): Promise> { @@ -128,14 +143,14 @@ export class Parser o.type === 'flag' && o.flag === name)) { + if (!flag.multiple && this.raw.some((o) => o.type === 'flag' && o.flag === name)) { throw new CLIError(`Flag --${name} can only be specified once`) } this.currentFlag = flag - const input = isLong || arg.length < 3 ? this.argv.shift() : arg.slice(arg[2] === '=' ? 3 : 2) + const input = isLong || arg.length < 3 ? this.argv.shift() : arg.slice(arg[2] === '=' ? 3 : 2) // if the value ends up being one of the command's flags, the user didn't provide an input - if ((typeof input !== 'string') || this.findFlag(input).name) { + if (typeof input !== 'string' || this.findFlag(input).name) { throw new CLIError(`Flag --${name} expects a value`) } @@ -210,11 +225,17 @@ export class Parser { type ValueFunction = (fws: FlagWithStrategy, flags?: Record) => Promise - const parseFlagOrThrowError = async (input: any, flag: BooleanFlag | OptionFlag, context: ParserContext | undefined, token?: FlagToken) => { + const parseFlagOrThrowError = async ( + input: any, + flag: BooleanFlag | OptionFlag, + context: ParserContext | undefined, + token?: FlagToken, + ) => { if (!flag.parse) return input const ctx = { @@ -240,8 +261,8 @@ export class Parser { const tokenLength = fws.tokens?.length // user provided some input @@ -250,25 +271,42 @@ export class Parser parseFlagOrThrowError( - last(i.tokens)?.input !== `--no-${i.inputFlag.name}`, - i.inputFlag.flag, - this.context, - last(i.tokens), - ), + valueFunction: async (i) => + parseFlagOrThrowError( + last(i.tokens)?.input !== `--no-${i.inputFlag.name}`, + i.inputFlag.flag, + this.context, + last(i.tokens), + ), } } // multiple with custom delimiter if (fws.inputFlag.flag.type === 'option' && fws.inputFlag.flag.delimiter && fws.inputFlag.flag.multiple) { return { - ...fws, valueFunction: async (i: FlagWithStrategy) => (await Promise.all( - ((i.tokens ?? []).flatMap(token => (token.input as string).split(i.inputFlag.flag.delimiter as string))) - // trim, and remove surrounding doubleQuotes (which would hav been needed if the elements contain spaces) - .map(v => v.trim().replace(/^"(.*)"$/, '$1').replace(/^'(.*)'$/, '$1')) - .map(async v => parseFlagOrThrowError(v, i.inputFlag.flag, this.context, {...last(i.tokens) as FlagToken, input: v})), - // eslint-disable-next-line unicorn/no-await-expression-member - )).map(v => validateOptions(i.inputFlag.flag as OptionFlag, v)), + ...fws, + valueFunction: async (i) => + ( + await Promise.all( + (i.tokens ?? []) + .flatMap((token) => token.input.split((i.inputFlag.flag as OptionFlag).delimiter ?? ',')) + // trim, and remove surrounding doubleQuotes (which would hav been needed if the elements contain spaces) + .map((v) => + v + .trim() + .replace(/^"(.*)"$/, '$1') + .replace(/^'(.*)'$/, '$1'), + ) + .map(async (v) => + parseFlagOrThrowError(v, i.inputFlag.flag, this.context, { + ...(last(i.tokens) as FlagToken), + input: v, + }), + ), + ) + ) + // eslint-disable-next-line unicorn/no-await-expression-member + .map((v) => validateOptions(i.inputFlag.flag as OptionFlag, v)), } } @@ -278,12 +316,14 @@ export class Parser Promise.all( - (fws.tokens ?? []).map(token => parseFlagOrThrowError( - validateOptions(i.inputFlag.flag as OptionFlag, token.input as string), - i.inputFlag.flag, - this.context, - token, - )), + (fws.tokens ?? []).map((token) => + parseFlagOrThrowError( + validateOptions(i.inputFlag.flag as OptionFlag, token.input as string), + i.inputFlag.flag, + this.context, + token, + ), + ), ), } } @@ -292,12 +332,13 @@ export class Parser parseFlagOrThrowError( - validateOptions(i.inputFlag.flag as OptionFlag, last(fws.tokens)?.input as string), - i.inputFlag.flag, - this.context, - last(fws.tokens), - ), + valueFunction: async (i: FlagWithStrategy) => + parseFlagOrThrowError( + validateOptions(i.inputFlag.flag as OptionFlag, last(fws.tokens)?.input as string), + i.inputFlag.flag, + this.context, + last(fws.tokens), + ), } } } @@ -308,18 +349,20 @@ export class Parser parseFlagOrThrowError( - validateOptions(i.inputFlag.flag as OptionFlag, valueFromEnv), - i.inputFlag.flag, - this.context, - ), + valueFunction: async (i: FlagWithStrategy) => + parseFlagOrThrowError( + validateOptions(i.inputFlag.flag as OptionFlag, valueFromEnv), + i.inputFlag.flag, + this.context, + ), } } if (fws.inputFlag.flag.type === 'boolean') { return { ...fws, - valueFunction: async (i: FlagWithStrategy) => isTruthy(process.env[i.inputFlag.flag.env as string] ?? 'false'), + valueFunction: async (i: FlagWithStrategy) => + isTruthy(process.env[i.inputFlag.flag.env as string] ?? 'false'), } } } @@ -328,10 +371,13 @@ export class Parser fws.inputFlag.flag.default({options: i.inputFlag.flag, flags: allFlags}) - : async () => fws.inputFlag.flag.default, + ...fws, + metadata: {setFromDefault: true}, + valueFunction: + typeof fws.inputFlag.flag.default === 'function' + ? (i: FlagWithStrategy, allFlags = {}) => + fws.inputFlag.flag.default({options: i.inputFlag.flag, flags: allFlags}) + : async () => fws.inputFlag.flag.default, } } @@ -342,11 +388,14 @@ export class Parser { if (fws.inputFlag.flag.type === 'option' && fws.inputFlag.flag.defaultHelp) { return { - ...fws, helpFunction: typeof fws.inputFlag.flag.defaultHelp === 'function' - // @ts-expect-error flag type isn't specific enough to know defaultHelp will definitely be there - ? (i: FlagWithStrategy, flags: Record, ...context) => i.inputFlag.flag.defaultHelp({options: i.inputFlag, flags}, ...context) - // @ts-expect-error flag type isn't specific enough to know defaultHelp will definitely be there - : (i: FlagWithStrategy) => i.inputFlag.flag.defaultHelp, + ...fws, + helpFunction: + typeof fws.inputFlag.flag.defaultHelp === 'function' + ? (i: FlagWithStrategy, flags: Record, ...context) => + // @ts-expect-error flag type isn't specific enough to know defaultHelp will definitely be there + i.inputFlag.flag.defaultHelp({options: i.inputFlag, flags}, ...context) + : // @ts-expect-error flag type isn't specific enough to know defaultHelp will definitely be there + (i: FlagWithStrategy) => i.inputFlag.flag.defaultHelp, } } @@ -354,69 +403,89 @@ export class Parser => { - const valueReferenceForHelp = fwsArrayToObject(flagsWithAllValues.filter(fws => !fws.metadata?.setFromDefault)) - return Promise.all(fwsArray.map(async fws => { - try { - if (fws.helpFunction) { - return { - ...fws, - metadata: { - ...fws.metadata, - defaultHelp: await fws.helpFunction?.(fws, valueReferenceForHelp, this.context), - }, + const valueReferenceForHelp = fwsArrayToObject(flagsWithAllValues.filter((fws) => !fws.metadata?.setFromDefault)) + return Promise.all( + fwsArray.map(async (fws) => { + try { + if (fws.helpFunction) { + return { + ...fws, + metadata: { + ...fws.metadata, + defaultHelp: await fws.helpFunction?.(fws, valueReferenceForHelp, this.context), + }, + } } + } catch { + // no-op } - } catch { - // no-op - } - return fws - })) + return fws + }), + ) } - const fwsArrayToObject = (fwsArray: FlagWithStrategy[]) => Object.fromEntries( - fwsArray.filter(fws => fws.value !== undefined) - .map(fws => [fws.inputFlag.name, fws.value]), - ) as TFlags & BFlags & { json: boolean | undefined } + const fwsArrayToObject = (fwsArray: FlagWithStrategy[]) => + Object.fromEntries( + fwsArray.filter((fws) => fws.value !== undefined).map((fws) => [fws.inputFlag.name, fws.value]), + ) as TFlags & BFlags & {json: boolean | undefined} type FlagWithStrategy = { inputFlag: { - name: string, + name: string flag: Flag } - tokens?: FlagToken[], - valueFunction?: ValueFunction; - helpFunction?: (fws: FlagWithStrategy, flags: Record, ...args: any) => Promise; + tokens?: FlagToken[] + valueFunction?: ValueFunction + helpFunction?: (fws: FlagWithStrategy, flags: Record, ...args: any) => Promise metadata?: MetadataFlag - value?: any; + value?: any } const flagTokenMap = this.mapAndValidateFlags() - const flagsWithValues = await Promise.all(Object.entries(this.input.flags) - // we check them if they have a token, or might have env, default, or defaultHelp. Also include booleans so they get their default value - .filter(([name, flag]) => flag.type === 'boolean' || flag.env || flag.default !== undefined || 'defaultHelp' in flag || flagTokenMap.has(name)) - // match each possible flag to its token, if there is one - .map(([name, flag]): FlagWithStrategy => ({inputFlag: {name, flag}, tokens: flagTokenMap.get(name)})) - .map(fws => addValueFunction(fws)) - .filter(fws => fws.valueFunction !== undefined) - .map(fws => addHelpFunction(fws)) - // we can't apply the default values until all the other flags are resolved because `flag.default` can reference other flags - .map(async fws => (fws.metadata?.setFromDefault ? fws : {...fws, value: await fws.valueFunction?.(fws)}))) + const flagsWithValues = await Promise.all( + Object.entries(this.input.flags) + // we check them if they have a token, or might have env, default, or defaultHelp. Also include booleans so they get their default value + .filter( + ([name, flag]) => + flag.type === 'boolean' || + flag.env || + flag.default !== undefined || + 'defaultHelp' in flag || + flagTokenMap.has(name), + ) + // match each possible flag to its token, if there is one + .map(([name, flag]): FlagWithStrategy => ({inputFlag: {name, flag}, tokens: flagTokenMap.get(name)})) + .map((fws) => addValueFunction(fws)) + .filter((fws) => fws.valueFunction !== undefined) + .map((fws) => addHelpFunction(fws)) + // we can't apply the default values until all the other flags are resolved because `flag.default` can reference other flags + .map(async (fws) => (fws.metadata?.setFromDefault ? fws : {...fws, value: await fws.valueFunction?.(fws)})), + ) - const valueReference = fwsArrayToObject(flagsWithValues.filter(fws => !fws.metadata?.setFromDefault)) + const valueReference = fwsArrayToObject(flagsWithValues.filter((fws) => !fws.metadata?.setFromDefault)) - const flagsWithAllValues = await Promise.all(flagsWithValues - .map(async fws => (fws.metadata?.setFromDefault ? {...fws, value: await fws.valueFunction?.(fws, valueReference)} : fws))) + const flagsWithAllValues = await Promise.all( + flagsWithValues.map(async (fws) => + fws.metadata?.setFromDefault ? {...fws, value: await fws.valueFunction?.(fws, valueReference)} : fws, + ), + ) - const finalFlags = (flagsWithAllValues.some(fws => typeof fws.helpFunction === 'function')) ? await addDefaultHelp(flagsWithAllValues) : flagsWithAllValues + const finalFlags = flagsWithAllValues.some((fws) => typeof fws.helpFunction === 'function') + ? await addDefaultHelp(flagsWithAllValues) + : flagsWithAllValues return { flags: fwsArrayToObject(finalFlags), - metadata: {flags: Object.fromEntries(finalFlags.filter(fws => fws.metadata).map(fws => [fws.inputFlag.name, fws.metadata as MetadataFlag]))}, + metadata: { + flags: Object.fromEntries( + finalFlags.filter((fws) => fws.metadata).map((fws) => [fws.inputFlag.name, fws.metadata as MetadataFlag]), + ), + }, } } - private async _args(): Promise<{ argv: unknown[]; args: Record }> { + private async _args(): Promise<{argv: unknown[]; args: Record}> { const argv: unknown[] = [] const args = {} as Record const tokens = this._argTokens @@ -424,7 +493,7 @@ export class Parser t.arg === name) + const token = tokens.find((t) => t.arg === name) ctx.token = token! if (token) { @@ -492,13 +561,13 @@ export class Parser `--${f}`) - .join(' '), + .map((f) => `--${f}`) + .join(' '), ) } private get _argTokens(): ArgToken[] { - return this.raw.filter(o => o.type === 'arg') as ArgToken[] + return this.raw.filter((o) => o.type === 'arg') as ArgToken[] } private _setNames() { @@ -511,9 +580,9 @@ export class Parser { + private mapAndValidateFlags(): Map { const flagTokenMap = new Map() - for (const token of (this.raw.filter(o => o.type === 'flag') as FlagToken[])) { + for (const token of this.raw.filter((o) => o.type === 'flag') as FlagToken[]) { // fail fast if there are any invalid flags if (!(token.flag in this.input.flags)) { throw new CLIError(`Unexpected flag ${token.flag}`) @@ -542,18 +611,20 @@ export class Parser (this.input.flags[k].char === char && char !== undefined && this.input.flags[k].char !== undefined)) + return Object.keys(this.input.flags).find( + (k) => this.input.flags[k].char === char && char !== undefined && this.input.flags[k].char !== undefined, + ) } - private findFlag(arg: string): { name?: string, isLong: boolean } { + private findFlag(arg: string): {name?: string; isLong: boolean} { const isLong = arg.startsWith('--') const short = isLong ? false : arg.startsWith('-') - const name = isLong ? this.findLongFlag(arg) : (short ? this.findShortFlag(arg) : undefined) + const name = isLong ? this.findLongFlag(arg) : short ? this.findShortFlag(arg) : undefined return {name, isLong} } } diff --git a/src/parser/validate.ts b/src/parser/validate.ts index 0f7d0ea02..634e8c1b0 100644 --- a/src/parser/validate.ts +++ b/src/parser/validate.ts @@ -7,12 +7,9 @@ import { UnexpectedArgsError, Validation, } from './errors' -import {uniq} from '../util' +import {uniq} from '../util/util' -export async function validate(parse: { - input: ParserInput; - output: ParserOutput; -}): Promise { +export async function validate(parse: {input: ParserInput; output: ParserOutput}): Promise { let cachedResolvedFlags: Record | undefined function validateArgs() { @@ -45,44 +42,46 @@ export async function validate(parse: { if (missingRequiredArgs.length > 0) { const flagsWithMultiple = Object.entries(parse.input.flags) - .filter(([_, flagDef]) => flagDef.type === 'option' && Boolean(flagDef.multiple)) - .map(([name]) => name) + .filter(([_, flagDef]) => flagDef.type === 'option' && Boolean(flagDef.multiple)) + .map(([name]) => name) throw new RequiredArgsError({parse, args: missingRequiredArgs, flagsWithMultiple}) } } async function validateFlags() { - const promises = Object.entries(parse.input.flags).flatMap(([name, flag]): Array> => { - if (parse.output.flags[name] !== undefined) { - return [ - ...flag.relationships ? validateRelationships(name, flag) : [], - ...flag.dependsOn ? [validateDependsOn(name, flag.dependsOn)] : [], - ...flag.exclusive ? [validateExclusive(name, flag.exclusive)] : [], - ...flag.exactlyOne ? [validateExactlyOne(name, flag.exactlyOne)] : [], - ] - } - - if (flag.required) { - return [{status: 'failed', name, validationFn: 'required', reason: `Missing required flag ${name}`}] - } - - if (flag.exactlyOne && flag.exactlyOne.length > 0) { - return [validateAcrossFlags(flag)] - } - - return [] - }) - - const results = (await Promise.all(promises)) - - const failed = results.filter(r => r.status === 'failed') + const promises = Object.entries(parse.input.flags).flatMap( + ([name, flag]): Array> => { + if (parse.output.flags[name] !== undefined) { + return [ + ...(flag.relationships ? validateRelationships(name, flag) : []), + ...(flag.dependsOn ? [validateDependsOn(name, flag.dependsOn)] : []), + ...(flag.exclusive ? [validateExclusive(name, flag.exclusive)] : []), + ...(flag.exactlyOne ? [validateExactlyOne(name, flag.exactlyOne)] : []), + ] + } + + if (flag.required) { + return [{status: 'failed', name, validationFn: 'required', reason: `Missing required flag ${name}`}] + } + + if (flag.exactlyOne && flag.exactlyOne.length > 0) { + return [validateAcrossFlags(flag)] + } + + return [] + }, + ) + + const results = await Promise.all(promises) + + const failed = results.filter((r) => r.status === 'failed') if (failed.length > 0) throw new FailedFlagValidationError({parse, failed}) } async function resolveFlags(flags: FlagRelationship[]): Promise> { if (cachedResolvedFlags) return cachedResolvedFlags - const promises = flags.map(async flag => { + const promises = flags.map(async (flag) => { if (typeof flag === 'string') { return [flag, parse.output.flags[flag]] } @@ -91,21 +90,22 @@ export async function validate(parse: { return result ? [flag.name, parse.output.flags[flag.name]] : null }) const resolved = await Promise.all(promises) - cachedResolvedFlags = Object.fromEntries(resolved.filter(r => r !== null) as [string, unknown][]) + cachedResolvedFlags = Object.fromEntries(resolved.filter((r) => r !== null) as [string, unknown][]) return cachedResolvedFlags } - const getPresentFlags = (flags: Record): string[] => Object.keys(flags).filter(key => key !== undefined) + const getPresentFlags = (flags: Record): string[] => + Object.keys(flags).filter((key) => key !== undefined) function validateAcrossFlags(flag: Flag): Validation { const base = {name: flag.name, validationFn: 'validateAcrossFlags'} const intersection = Object.entries(parse.input.flags) - .map(entry => entry[0]) // array of flag names - .filter(flagName => parse.output.flags[flagName] !== undefined) // with values - .filter(flagName => flag.exactlyOne && flag.exactlyOne.includes(flagName)) // and in the exactlyOne list + .map((entry) => entry[0]) // array of flag names + .filter((flagName) => parse.output.flags[flagName] !== undefined) // with values + .filter((flagName) => flag.exactlyOne && flag.exactlyOne.includes(flagName)) // and in the exactlyOne list if (intersection.length === 0) { // the command's exactlyOne may or may not include itself, so we'll use Set to add + de-dupe - const deduped = uniq(flag.exactlyOne?.map(flag => `--${flag}`) ?? []).join(', ') + const deduped = uniq(flag.exactlyOne?.map((flag) => `--${flag}`) ?? []).join(', ') const reason = `Exactly one of the following must be provided: ${deduped}` return {...base, status: 'failed', reason} } @@ -119,13 +119,15 @@ export async function validate(parse: { const keys = getPresentFlags(resolved) for (const flag of keys) { // do not enforce exclusivity for flags that were defaulted - if (parse.output.metadata.flags && parse.output.metadata.flags[flag]?.setFromDefault) - continue - if (parse.output.metadata.flags && parse.output.metadata.flags[name]?.setFromDefault) - continue + if (parse.output.metadata.flags && parse.output.metadata.flags[flag]?.setFromDefault) continue + if (parse.output.metadata.flags && parse.output.metadata.flags[name]?.setFromDefault) continue if (parse.output.flags[flag] !== undefined) { const flagValue = parse.output.metadata.flags?.[flag]?.defaultHelp ?? parse.output.flags[flag] - return {...base, status: 'failed', reason: `--${flag}=${flagValue} cannot also be provided when using --${name}`} + return { + ...base, + status: 'failed', + reason: `--${flag}=${flagValue} cannot also be provided when using --${name}`, + } } } @@ -150,10 +152,16 @@ export async function validate(parse: { const base = {name, validationFn: 'validateDependsOn'} const resolved = await resolveFlags(flags) - const foundAll = Object.values(resolved).every(val => val !== undefined) + const foundAll = Object.values(resolved).every((val) => val !== undefined) if (!foundAll) { - const formattedFlags = Object.keys(resolved).map(f => `--${f}`).join(', ') - return {...base, status: 'failed', reason: `All of the following must be provided when using --${name}: ${formattedFlags}`} + const formattedFlags = Object.keys(resolved) + .map((f) => `--${f}`) + .join(', ') + return { + ...base, + status: 'failed', + reason: `All of the following must be provided when using --${name}: ${formattedFlags}`, + } } return {...base, status: 'success'} @@ -165,33 +173,39 @@ export async function validate(parse: { const resolved = await resolveFlags(flags) const foundAtLeastOne = Object.values(resolved).some(Boolean) if (!foundAtLeastOne) { - const formattedFlags = Object.keys(resolved).map(f => `--${f}`).join(', ') - return {...base, status: 'failed', reason: `One of the following must be provided when using --${name}: ${formattedFlags}`} + const formattedFlags = Object.keys(resolved) + .map((f) => `--${f}`) + .join(', ') + return { + ...base, + status: 'failed', + reason: `One of the following must be provided when using --${name}: ${formattedFlags}`, + } } return {...base, status: 'success'} } function validateRelationships(name: string, flag: Flag): Promise[] { - return ((flag.relationships ?? []).map(relationship => { + return (flag.relationships ?? []).map((relationship) => { switch (relationship.type) { - case 'all': { - return validateDependsOn(name, relationship.flags) - } + case 'all': { + return validateDependsOn(name, relationship.flags) + } - case 'some': { - return validateSome(name, relationship.flags) - } + case 'some': { + return validateSome(name, relationship.flags) + } - case 'none': { - return validateExclusive(name, relationship.flags) - } + case 'none': { + return validateExclusive(name, relationship.flags) + } - default: { - throw new Error(`Unknown relationship type: ${relationship.type}`) + default: { + throw new Error(`Unknown relationship type: ${relationship.type}`) + } } - } - })) + }) } validateArgs() diff --git a/src/performance.ts b/src/performance.ts index a271f2c84..abe202705 100644 --- a/src/performance.ts +++ b/src/performance.ts @@ -3,12 +3,12 @@ import {settings} from './settings' type Details = Record type PerfResult = { - name: string; + name: string duration: number details: Details - module: string; - method: string | undefined; - scope: string | undefined; + module: string + method: string | undefined + scope: string | undefined } type PerfHighlights = { @@ -122,12 +122,12 @@ export class Performance { const markers = [...Performance.markers.values()] if (markers.length === 0) return - for (const marker of markers.filter(m => !m.stopped)) { + for (const marker of markers.filter((m) => !m.stopped)) { marker.stop() } - return new Promise(resolve => { - const perfObserver = new PerformanceObserver(items => { + return new Promise((resolve) => { + const perfObserver = new PerformanceObserver((items) => { for (const entry of items.getEntries()) { const marker = Performance.markers.get(entry.name) if (marker) { @@ -167,8 +167,10 @@ export class Performance { acc[event].total = perfResult.duration } - return acc - }, {} as Record>) + return acc + }, + {} as Record>, + ) const pluginLoadTimeByType = Object.fromEntries(oclifResults .filter(({name}) => name.startsWith('config.loadPlugins#')) diff --git a/src/settings.ts b/src/settings.ts index 9f797f800..41ce4c569 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -5,20 +5,20 @@ export type Settings = { * Useful to set in the ./bin/dev script. * oclif.settings.debug = true; */ - debug?: boolean; + debug?: boolean /** * The path to the error.log file. * * NOTE: This is read-only and setting it will have no effect. */ - errlog?: string; + errlog?: string /** * Set the terminal width to a specified number of columns (characters) * * Environment Variable: * OCLIF_COLUMNS=80 */ - columns?: number; + columns?: number /** * Try to use ts-node to load typescript source files instead of * javascript files. @@ -29,13 +29,13 @@ export type Settings = { * Environment Variable: * NODE_ENV=development */ - tsnodeEnabled?: boolean; + tsnodeEnabled?: boolean /** * Enable performance tracking. Resulting data is available in the `perf` property of the `Config` class. * This will be overridden by the `enablePerf` property passed into Config constructor. */ - performanceEnabled?: boolean; -}; + performanceEnabled?: boolean +} // Set global.oclif to the new object if it wasn't set before if (!(global as any).oclif) (global as any).oclif = {} diff --git a/src/util.ts b/src/util.ts deleted file mode 100644 index a5c0eab5b..000000000 --- a/src/util.ts +++ /dev/null @@ -1,191 +0,0 @@ -import {access, stat} from 'node:fs/promises' -import {homedir, platform} from 'node:os' -import {readFile, readFileSync} from 'node:fs' -import {ArgInput} from './interfaces/parser' -import {Command} from './command' -import {join} from 'node:path' - -const debug = require('debug') - -export function pickBy>(obj: T, fn: (i: T[keyof T]) => boolean): Partial { - return Object.entries(obj) - .reduce((o, [k, v]) => { - if (fn(v)) o[k] = v - return o - }, {} as any) -} - -export function compact(a: (T | undefined)[]): T[] { - // eslint-disable-next-line unicorn/prefer-native-coercion-functions - return a.filter((a): a is T => Boolean(a)) -} - -export function uniqBy(arr: T[], fn: (cur: T) => any): T[] { - return arr.filter((a, i) => { - const aVal = fn(a) - return !arr.find((b, j) => j > i && fn(b) === aVal) - }) -} - -export function last(arr?: T[]): T | undefined { - if (!arr) return - return arr.at(-1) -} - -type SortTypes = string | number | undefined | boolean - -function compare(a: SortTypes | SortTypes[], b: SortTypes | SortTypes[]): number { - a = a === undefined ? 0 : a - b = b === undefined ? 0 : b - - if (Array.isArray(a) && Array.isArray(b)) { - if (a.length === 0 && b.length === 0) return 0 - const diff = compare(a[0], b[0]) - if (diff !== 0) return diff - return compare(a.slice(1), b.slice(1)) - } - - if (a < b) return -1 - if (a > b) return 1 - return 0 -} - -export function sortBy(arr: T[], fn: (i: T) => SortTypes | SortTypes[]): T[] { - return arr.sort((a, b) => compare(fn(a), fn(b))) -} - -export function castArray(input?: T | T[]): T[] { - if (input === undefined) return [] - return Array.isArray(input) ? input : [input] -} - -export function isProd(): boolean { - return !['development', 'test'].includes(process.env.NODE_ENV ?? '') -} - -export function maxBy(arr: T[], fn: (i: T) => number): T | undefined { - if (arr.length === 0) { - return undefined - } - - return arr.reduce((maxItem, i) => { - const curr = fn(i) - const max = fn(maxItem) - return curr > max ? i : maxItem - }) -} - -export function sumBy(arr: T[], fn: (i: T) => number): number { - return arr.reduce((sum, i) => sum + fn(i), 0) -} - -export function capitalize(s: string): string { - return s ? s.charAt(0).toUpperCase() + s.slice(1).toLowerCase() : '' -} - -export async function exists(path: string): Promise { - try { - await access(path) - return true - } catch { - return false - } -} - -export const dirExists = async (input: string): Promise => { - if (!await exists(input)) { - throw new Error(`No directory found at ${input}`) - } - - const fileStat = await stat(input) - if (!fileStat.isDirectory()) { - throw new Error(`${input} exists but is not a directory`) - } - - return input -} - -export const fileExists = async (input: string): Promise => { - if (!await exists(input)) { - throw new Error(`No file found at ${input}`) - } - - const fileStat = await stat(input) - if (!fileStat.isFile()) { - throw new Error(`${input} exists but is not a file`) - } - - return input -} - -export function isTruthy(input: string): boolean { - return ['true', '1', 'yes', 'y'].includes(input.toLowerCase()) -} - -export function isNotFalsy(input: string): boolean { - return !['false', '0', 'no', 'n'].includes(input.toLowerCase()) -} - -export function requireJson(...pathParts: string[]): T { - return JSON.parse(readFileSync(join(...pathParts), 'utf8')) -} - -/** - * Ensure that the provided args are an object. This is for backwards compatibility with v1 commands which - * defined args as an array. - * - * @param args Either an array of args or an object of args - * @returns ArgInput - */ -export function ensureArgObject(args?: any[] | ArgInput | { [name: string]: Command.Arg.Cached}): ArgInput { - return (Array.isArray(args) ? (args ?? []).reduce((x, y) => ({...x, [y.name]: y}), {} as ArgInput) : args ?? {}) as ArgInput -} - -export function uniq(arr: T[]): T[] { - return [...new Set(arr)].sort() -} - -/** - * Call os.homedir() and return the result - * - * Wrapping this allows us to stub these in tests since os.homedir() is - * non-configurable and non-writable. - * - * @returns The user's home directory - */ -export function getHomeDir(): string { - return homedir() -} - -/** - * Call os.platform() and return the result - * - * Wrapping this allows us to stub these in tests since os.platform() is - * non-configurable and non-writable. - * - * @returns The process' platform - */ -export function getPlatform(): NodeJS.Platform { - return platform() -} - -export function readJson(path: string): Promise { - debug('config')('readJson %s', path) - return new Promise((resolve, reject) => { - readFile(path, 'utf8', (err: any, d: any) => { - try { - if (err) reject(err) - else resolve(JSON.parse(d) as T) - } catch (error: any) { - reject(error) - } - }) - }) -} - -export function readJsonSync(path: string, parse: false): string -export function readJsonSync(path: string, parse?: true): T -export function readJsonSync(path: string, parse = true): T | string { - const contents = readFileSync(path, 'utf8') - return parse ? JSON.parse(contents) as T : contents -} diff --git a/src/util/aggregate-flags.ts b/src/util/aggregate-flags.ts new file mode 100644 index 000000000..704c9fd5c --- /dev/null +++ b/src/util/aggregate-flags.ts @@ -0,0 +1,16 @@ +import {FlagInput, FlagOutput} from '../interfaces/parser' +import {boolean} from '../flags' + +const json = boolean({ + description: 'Format output as json.', + helpGroup: 'GLOBAL', +}) + +export function aggregateFlags( + flags: FlagInput | undefined, + baseFlags: FlagInput | undefined, + enableJsonFlag: boolean | undefined, +): FlagInput { + const combinedFlags = {...baseFlags, ...flags} + return (enableJsonFlag ? {json, ...combinedFlags} : combinedFlags) as FlagInput +} diff --git a/src/util/cache-command.ts b/src/util/cache-command.ts new file mode 100644 index 000000000..ae378947c --- /dev/null +++ b/src/util/cache-command.ts @@ -0,0 +1,127 @@ +import {ArgInput, FlagInput} from '../interfaces/parser' +import {Command} from '../command' +import {Plugin as IPlugin} from '../interfaces/plugin' +import {aggregateFlags} from './aggregate-flags' +import {cacheDefaultValue} from './cache-default-value' +import {ensureArgObject} from './ensure-arg-object' +import {pickBy} from './util' + +// In order to collect static properties up the inheritance chain, we need to recursively +// access the prototypes until there's nothing left. This allows us to combine baseFlags +// and flags as well as add in the json flag if enableJsonFlag is enabled. +function mergePrototype(result: Command.Class, cmd: Command.Class): Command.Class { + const proto = Object.getPrototypeOf(cmd) + const filteredProto = pickBy(proto, (v) => v !== undefined) as Command.Class + return Object.keys(proto).length > 0 ? mergePrototype({...filteredProto, ...result} as Command.Class, proto) : result +} + +async function cacheFlags( + cmdFlags: FlagInput, + respectNoCacheDefault: boolean, +): Promise> { + const promises = Object.entries(cmdFlags).map(async ([name, flag]) => [ + name, + { + name, + char: flag.char, + summary: flag.summary, + hidden: flag.hidden, + required: flag.required, + helpLabel: flag.helpLabel, + helpGroup: flag.helpGroup, + description: flag.description, + dependsOn: flag.dependsOn, + relationships: flag.relationships, + exclusive: flag.exclusive, + deprecated: flag.deprecated, + deprecateAliases: flag.deprecateAliases, + aliases: flag.aliases, + charAliases: flag.charAliases, + noCacheDefault: flag.noCacheDefault, + ...(flag.type === 'boolean' + ? { + allowNo: flag.allowNo, + type: flag.type, + } + : { + type: flag.type, + helpValue: flag.helpValue, + multiple: flag.multiple, + options: flag.options, + delimiter: flag.delimiter, + default: await cacheDefaultValue(flag, respectNoCacheDefault), + hasDynamicHelp: typeof flag.defaultHelp === 'function', + }), + }, + ]) + return Object.fromEntries(await Promise.all(promises)) +} + +async function cacheArgs( + cmdArgs: ArgInput, + respectNoCacheDefault: boolean, +): Promise> { + const promises = Object.entries(cmdArgs).map(async ([name, arg]) => [ + name, + { + name, + description: arg.description, + required: arg.required, + options: arg.options, + default: await cacheDefaultValue(arg, respectNoCacheDefault), + hidden: arg.hidden, + noCacheDefault: arg.noCacheDefault, + }, + ]) + return Object.fromEntries(await Promise.all(promises)) +} + +export async function cacheCommand( + uncachedCmd: Command.Class, + plugin?: IPlugin, + respectNoCacheDefault = false, +): Promise { + const cmd = mergePrototype(uncachedCmd, uncachedCmd) + + const flags = await cacheFlags(aggregateFlags(cmd.flags, cmd.baseFlags, cmd.enableJsonFlag), respectNoCacheDefault) + const args = await cacheArgs(ensureArgObject(cmd.args), respectNoCacheDefault) + + const stdProperties = { + id: cmd.id, + summary: cmd.summary, + description: cmd.description, + strict: cmd.strict, + usage: cmd.usage, + pluginName: plugin && plugin.name, + pluginAlias: plugin && plugin.alias, + pluginType: plugin && plugin.type, + hidden: cmd.hidden, + state: cmd.state, + aliases: cmd.aliases || [], + examples: cmd.examples || (cmd as any).example, + deprecationOptions: cmd.deprecationOptions, + deprecateAliases: cmd.deprecateAliases, + flags, + args, + hasDynamicHelp: Object.values(flags).some((f) => f.hasDynamicHelp), + } + + // do not include these properties in manifest + const ignoreCommandProperties = [ + 'plugin', + '_flags', + '_enableJsonFlag', + '_globalFlags', + '_baseFlags', + 'baseFlags', + '_--', + '_base', + ] + + // Add in any additional properties that are not standard command properties. + const stdKeysAndIgnored = new Set([...Object.keys(stdProperties), ...ignoreCommandProperties]) + const keysToAdd = Object.keys(cmd).filter((property) => !stdKeysAndIgnored.has(property)) + const additionalProperties = Object.fromEntries(keysToAdd.map((key) => [key, (cmd as any)[key]])) + + return {...stdProperties, ...additionalProperties} +} diff --git a/src/util/cache-default-value.ts b/src/util/cache-default-value.ts new file mode 100644 index 000000000..990eb5179 --- /dev/null +++ b/src/util/cache-default-value.ts @@ -0,0 +1,23 @@ +import {Arg, OptionFlag} from '../interfaces/parser' + +// when no manifest exists, the default is calculated. This may throw, so we need to catch it +export const cacheDefaultValue = async (flagOrArg: OptionFlag | Arg, respectNoCacheDefault: boolean) => { + if (respectNoCacheDefault && flagOrArg.noCacheDefault) return + // Prefer the defaultHelp function (returns a friendly string for complex types) + if (typeof flagOrArg.defaultHelp === 'function') { + try { + return await flagOrArg.defaultHelp({options: flagOrArg, flags: {}}) + } catch { + return + } + } + + // if not specified, try the default function + if (typeof flagOrArg.default === 'function') { + try { + return await flagOrArg.default({options: flagOrArg, flags: {}}) + } catch {} + } else { + return flagOrArg.default + } +} diff --git a/src/util/ensure-arg-object.ts b/src/util/ensure-arg-object.ts new file mode 100644 index 000000000..46648494e --- /dev/null +++ b/src/util/ensure-arg-object.ts @@ -0,0 +1,15 @@ +import {ArgInput} from '../interfaces/parser' +import {Command} from '../command' + +/** + * Ensure that the provided args are an object. This is for backwards compatibility with v1 commands which + * defined args as an array. + * + * @param args Either an array of args or an object of args + * @returns ArgInput + */ +export function ensureArgObject(args?: any[] | ArgInput | {[name: string]: Command.Arg.Cached}): ArgInput { + return ( + Array.isArray(args) ? (args ?? []).reduce((x, y) => ({...x, [y.name]: y}), {} as ArgInput) : args ?? {} + ) as ArgInput +} diff --git a/src/util/fs.ts b/src/util/fs.ts new file mode 100644 index 000000000..89f5f1c0c --- /dev/null +++ b/src/util/fs.ts @@ -0,0 +1,57 @@ +import {access, readFile, stat} from 'node:fs/promises' +import {join} from 'node:path' +import {readFileSync} from 'node:fs' + +const debug = require('debug') + +export function requireJson(...pathParts: string[]): T { + return JSON.parse(readFileSync(join(...pathParts), 'utf8')) +} + +export async function exists(path: string): Promise { + try { + await access(path) + return true + } catch { + return false + } +} + +export const dirExists = async (input: string): Promise => { + if (!(await exists(input))) { + throw new Error(`No directory found at ${input}`) + } + + const fileStat = await stat(input) + if (!fileStat.isDirectory()) { + throw new Error(`${input} exists but is not a directory`) + } + + return input +} + +export const fileExists = async (input: string): Promise => { + if (!(await exists(input))) { + throw new Error(`No file found at ${input}`) + } + + const fileStat = await stat(input) + if (!fileStat.isFile()) { + throw new Error(`${input} exists but is not a file`) + } + + return input +} + +export async function readJson(path: string): Promise { + debug('config')('readJson %s', path) + const contents = await readFile(path, 'utf8') + return JSON.parse(contents) as T +} + +export function readJsonSync(path: string, parse: false): string +export function readJsonSync(path: string, parse?: true): T +export function readJsonSync(path: string, parse = true): T | string { + const contents = readFileSync(path, 'utf8') + return parse ? (JSON.parse(contents) as T) : contents +} diff --git a/src/util/os.ts b/src/util/os.ts new file mode 100644 index 000000000..2fb3cdf88 --- /dev/null +++ b/src/util/os.ts @@ -0,0 +1,25 @@ +import {homedir, platform} from 'node:os' + +/** + * Call os.homedir() and return the result + * + * Wrapping this allows us to stub these in tests since os.homedir() is + * non-configurable and non-writable. + * + * @returns The user's home directory + */ +export function getHomeDir(): string { + return homedir() +} + +/** + * Call os.platform() and return the result + * + * Wrapping this allows us to stub these in tests since os.platform() is + * non-configurable and non-writable. + * + * @returns The process' platform + */ +export function getPlatform(): NodeJS.Platform { + return platform() +} diff --git a/src/util/util.ts b/src/util/util.ts new file mode 100644 index 000000000..7f3ec1058 --- /dev/null +++ b/src/util/util.ts @@ -0,0 +1,99 @@ +export function pickBy>( + obj: T, + fn: (i: T[keyof T]) => boolean, +): Partial { + return Object.entries(obj).reduce((o, [k, v]) => { + if (fn(v)) o[k] = v + return o + }, {} as any) +} + +export function compact(a: (T | undefined)[]): T[] { + // eslint-disable-next-line unicorn/prefer-native-coercion-functions + return a.filter((a): a is T => Boolean(a)) +} + +export function uniqBy(arr: T[], fn: (cur: T) => any): T[] { + return arr.filter((a, i) => { + const aVal = fn(a) + return !arr.find((b, j) => j > i && fn(b) === aVal) + }) +} + +export function last(arr?: T[]): T | undefined { + if (!arr) return + return arr.at(-1) +} + +type SortTypes = string | number | undefined | boolean + +function compare(a: SortTypes | SortTypes[], b: SortTypes | SortTypes[]): number { + a = a === undefined ? 0 : a + b = b === undefined ? 0 : b + + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length === 0 && b.length === 0) return 0 + const diff = compare(a[0], b[0]) + if (diff !== 0) return diff + return compare(a.slice(1), b.slice(1)) + } + + if (a < b) return -1 + if (a > b) return 1 + return 0 +} + +export function sortBy(arr: T[], fn: (i: T) => SortTypes | SortTypes[]): T[] { + return arr.sort((a, b) => compare(fn(a), fn(b))) +} + +export function castArray(input?: T | T[]): T[] { + if (input === undefined) return [] + return Array.isArray(input) ? input : [input] +} + +export function isProd(): boolean { + return !['development', 'test'].includes(process.env.NODE_ENV ?? '') +} + +export function maxBy(arr: T[], fn: (i: T) => number): T | undefined { + if (arr.length === 0) { + return undefined + } + + return arr.reduce((maxItem, i) => { + const curr = fn(i) + const max = fn(maxItem) + return curr > max ? i : maxItem + }) +} + +export function sumBy(arr: T[], fn: (i: T) => number): number { + return arr.reduce((sum, i) => sum + fn(i), 0) +} + +export function capitalize(s: string): string { + return s ? s.charAt(0).toUpperCase() + s.slice(1).toLowerCase() : '' +} + +export function isTruthy(input: string): boolean { + return ['true', '1', 'yes', 'y'].includes(input.toLowerCase()) +} + +export function isNotFalsy(input: string): boolean { + return !['false', '0', 'no', 'n'].includes(input.toLowerCase()) +} + +export function uniq(arr: T[]): T[] { + return [...new Set(arr)].sort() +} + +export function mapValues, TResult>( + obj: {[P in keyof T]: T[P]}, + fn: (i: T[keyof T], k: keyof T) => TResult, +): {[P in keyof T]: TResult} { + return Object.entries(obj).reduce((o, [k, v]) => { + o[k] = fn(v as any, k as any) + return o + }, {} as any) +} diff --git a/test/cli-ux/export.test.ts b/test/cli-ux/export.test.ts index 4a62835b7..2e06dfb9d 100644 --- a/test/cli-ux/export.test.ts +++ b/test/cli-ux/export.test.ts @@ -20,4 +20,3 @@ describe('ux exports', () => { expect(typeof ExitError).to.be.equal('function') }) }) - diff --git a/test/cli-ux/fancy.ts b/test/cli-ux/fancy.ts index f6709c0d3..4cda54a82 100644 --- a/test/cli-ux/fancy.ts +++ b/test/cli-ux/fancy.ts @@ -7,16 +7,16 @@ import {ux} from '../../src/cli-ux' let count = 0 export const fancy = base -.do(async (ctx: {count: number; base: string}) => { - ctx.count = count++ - ctx.base = join(__dirname, '../tmp', `test-${ctx.count}`) - await rm(ctx.base, {recursive: true, force: true}) - const chalk = require('chalk') - chalk.level = 0 -}) -// eslint-disable-next-line unicorn/prefer-top-level-await -.finally(async () => { - await ux.done() -}) + .do(async (ctx: {count: number; base: string}) => { + ctx.count = count++ + ctx.base = join(__dirname, '../tmp', `test-${ctx.count}`) + await rm(ctx.base, {recursive: true, force: true}) + const chalk = require('chalk') + chalk.level = 0 + }) + // eslint-disable-next-line unicorn/prefer-top-level-await + .finally(async () => { + await ux.done() + }) export {FancyTypes, expect} from 'fancy-test' diff --git a/test/cli-ux/index.test.ts b/test/cli-ux/index.test.ts index 866449a9a..dbbe52e76 100644 --- a/test/cli-ux/index.test.ts +++ b/test/cli-ux/index.test.ts @@ -5,17 +5,16 @@ const hyperlinker = require('hyperlinker') describe('url', () => { fancy - .env({FORCE_HYPERLINK: '1'}, {clear: true}) - .stdout() - .do(() => ux.url('sometext', 'https://google.com')) - .it('renders hyperlink', ({stdout}) => { - expect(stdout).to.equal('sometext\n') - }) + .env({FORCE_HYPERLINK: '1'}, {clear: true}) + .stdout() + .do(() => ux.url('sometext', 'https://google.com')) + .it('renders hyperlink', ({stdout}) => { + expect(stdout).to.equal('sometext\n') + }) }) describe('hyperlinker', () => { - fancy - .it('renders hyperlink', () => { + fancy.it('renders hyperlink', () => { const link = hyperlinker('sometext', 'https://google.com', {}) // eslint-disable-next-line unicorn/escape-case const expected = '\u001b]8;;https://google.com\u0007sometext\u001b]8;;\u0007' diff --git a/test/cli-ux/prompt.test.ts b/test/cli-ux/prompt.test.ts index 59ae25ce6..25d399c37 100644 --- a/test/cli-ux/prompt.test.ts +++ b/test/cli-ux/prompt.test.ts @@ -8,68 +8,67 @@ import {fancy} from './fancy' describe('prompt', () => { fancy - .stdout() - .stderr() - .end('requires input', async () => { - const promptPromise = ux.prompt('Require input?') - process.stdin.emit('data', '') - process.stdin.emit('data', 'answer') - const answer = await promptPromise - await ux.done() - expect(answer).to.equal('answer') - }) + .stdout() + .stderr() + .end('requires input', async () => { + const promptPromise = ux.prompt('Require input?') + process.stdin.emit('data', '') + process.stdin.emit('data', 'answer') + const answer = await promptPromise + await ux.done() + expect(answer).to.equal('answer') + }) fancy - .stdout() - .stderr() - .stdin('y') - .end('confirm', async () => { - const promptPromise = ux.confirm('yes/no?') - const answer = await promptPromise - await ux.done() - expect(answer).to.equal(true) - }) + .stdout() + .stderr() + .stdin('y') + .end('confirm', async () => { + const promptPromise = ux.confirm('yes/no?') + const answer = await promptPromise + await ux.done() + expect(answer).to.equal(true) + }) fancy - .stdout() - .stderr() - .stdin('n') - .end('confirm', async () => { - const promptPromise = ux.confirm('yes/no?') - const answer = await promptPromise - await ux.done() - expect(answer).to.equal(false) - }) + .stdout() + .stderr() + .stdin('n') + .end('confirm', async () => { + const promptPromise = ux.confirm('yes/no?') + const answer = await promptPromise + await ux.done() + expect(answer).to.equal(false) + }) fancy - .stdout() - .stderr() - .stdin('x') - .end('gets anykey', async () => { - const promptPromise = ux.anykey() - const answer = await promptPromise - await ux.done() - expect(answer).to.equal('x') - }) + .stdout() + .stderr() + .stdin('x') + .end('gets anykey', async () => { + const promptPromise = ux.anykey() + const answer = await promptPromise + await ux.done() + expect(answer).to.equal('x') + }) fancy - .stdout() - .stderr() - .end('does not require input', async () => { - const promptPromise = ux.prompt('Require input?', { - required: false, + .stdout() + .stderr() + .end('does not require input', async () => { + const promptPromise = ux.prompt('Require input?', { + required: false, + }) + process.stdin.emit('data', '') + const answer = await promptPromise + await ux.done() + expect(answer).to.equal('') }) - process.stdin.emit('data', '') - const answer = await promptPromise - await ux.done() - expect(answer).to.equal('') - }) fancy - .stdout() - .stderr() - .it('timeouts with no input', async () => { - await expect(ux.prompt('Require input?', {timeout: 1})) - .to.eventually.be.rejectedWith('Prompt timeout') - }) + .stdout() + .stderr() + .it('timeouts with no input', async () => { + await expect(ux.prompt('Require input?', {timeout: 1})).to.eventually.be.rejectedWith('Prompt timeout') + }) }) diff --git a/test/cli-ux/styled/object.test.ts b/test/cli-ux/styled/object.test.ts index 8c35056bd..4e14957df 100644 --- a/test/cli-ux/styled/object.test.ts +++ b/test/cli-ux/styled/object.test.ts @@ -3,9 +3,7 @@ import {expect, fancy} from 'fancy-test' import {ux} from '../../../src/cli-ux' describe('styled/object', () => { - fancy - .stdout() - .end('shows a table', output => { + fancy.stdout().end('shows a table', (output) => { ux.styledObject([ {foo: 1, bar: 1}, {foo: 2, bar: 2}, diff --git a/test/cli-ux/styled/progress.test.ts b/test/cli-ux/styled/progress.test.ts index 63ecd754a..6233bfe86 100644 --- a/test/cli-ux/styled/progress.test.ts +++ b/test/cli-ux/styled/progress.test.ts @@ -3,8 +3,7 @@ import {ux} from '../../../src/cli-ux' describe('progress', () => { // single bar - fancy - .end('single bar has default settings', _ => { + fancy.end('single bar has default settings', (_) => { const b1 = ux.progress({format: 'Example 1: Progress {bar} | {percentage}%'}) // @ts-expect-error because private member expect(b1.options.format).to.contain('Example 1: Progress') @@ -13,8 +12,7 @@ describe('progress', () => { }) // testing no settings passed, default settings created - fancy - .end('single bar, no bars array', _ => { + fancy.end('single bar, no bars array', (_) => { const b1 = ux.progress({}) // @ts-expect-error because private member expect(b1.options.format).to.contain('progress') @@ -24,9 +22,8 @@ describe('progress', () => { expect(b1.options.noTTYOutput).to.not.be.null }) // testing getProgressBar returns correct type - fancy - .end('typeof progress bar is object', _ => { + fancy.end('typeof progress bar is object', (_) => { const b1 = ux.progress({format: 'Example 1: Progress {bar} | {percentage}%'}) - expect(typeof (b1)).to.equal('object') + expect(typeof b1).to.equal('object') }) }) diff --git a/test/cli-ux/styled/table.e2e.ts b/test/cli-ux/styled/table.e2e.ts index c590209f1..ad921f706 100644 --- a/test/cli-ux/styled/table.e2e.ts +++ b/test/cli-ux/styled/table.e2e.ts @@ -3,9 +3,7 @@ import {ux} from '../../../src/cli-ux' describe('styled/table', () => { describe('null/undefined handling', () => { - fancy - .stdout() - .end('omits nulls and undefined by default', output => { + fancy.stdout().end('omits nulls and undefined by default', (output) => { const data = [{a: 1, b: '2', c: null, d: undefined}] ux.table(data, {a: {}, b: {}, c: {}, d: {}}) expect(output.stdout).to.include('1') @@ -17,10 +15,11 @@ describe('styled/table', () => { describe('scale tests', () => { const bigRows = 150_000 - fancy - .stdout() - .end('very tall tables don\'t exceed stack depth', output => { - const data = Array.from({length: bigRows}).fill({id: '123', name: 'foo', value: 'bar'}) as Record[] + fancy.stdout().end("very tall tables don't exceed stack depth", (output) => { + const data = Array.from({length: bigRows}).fill({id: '123', name: 'foo', value: 'bar'}) as Record< + string, + unknown + >[] const tallColumns = { id: {header: 'ID'}, name: {}, @@ -31,17 +30,16 @@ describe('styled/table', () => { expect(output.stdout).to.include('ID') }) - fancy - .stdout() - .end('very tall, wide tables don\'t exceed stack depth', output => { + fancy.stdout().end("very tall, wide tables don't exceed stack depth", (output) => { const columns = 100 const row = Object.fromEntries(Array.from({length: columns}).map((_, i) => [`col${i}`, 'foo'])) const data = Array.from({length: bigRows}).fill(row) as Record[] - const bigColumns = Object.fromEntries(Array.from({length: columns}).map((_, i) => [`col${i}`, {header: `col${i}`.toUpperCase()}])) + const bigColumns = Object.fromEntries( + Array.from({length: columns}).map((_, i) => [`col${i}`, {header: `col${i}`.toUpperCase()}]), + ) ux.table(data, bigColumns) expect(output.stdout).to.include('COL1') }) }) }) - diff --git a/test/cli-ux/styled/table.test.ts b/test/cli-ux/styled/table.test.ts index 6fa148a51..15058461c 100644 --- a/test/cli-ux/styled/table.test.ts +++ b/test/cli-ux/styled/table.test.ts @@ -70,14 +70,12 @@ const extendedHeader = `ID Name${ws.padEnd(14)}Web url${ws.padEnd(34)}Stack${ws // truncation rules? describe('styled/table', () => { - fancy - .end('export flags and display()', () => { - expect(typeof (ux.table.flags())).to.eq('object') - expect(typeof (ux.table)).to.eq('function') + fancy.end('export flags and display()', () => { + expect(typeof ux.table.flags()).to.eq('object') + expect(typeof ux.table).to.eq('function') }) - fancy - .end('has optional flags', _ => { + fancy.end('has optional flags', (_) => { const flags = ux.table.flags() expect(flags.columns).to.exist expect(flags.sort).to.exist @@ -89,9 +87,7 @@ describe('styled/table', () => { expect(flags['no-header']).to.exist }) - fancy - .stdout() - .end('displays table', output => { + fancy.stdout().end('displays table', (output) => { ux.table(apps, columns) expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€${ws} @@ -100,16 +96,12 @@ describe('styled/table', () => { }) describe('columns', () => { - fancy - .stdout() - .end('use header value for id', output => { + fancy.stdout().end('use header value for id', (output) => { ux.table(apps, columns) expect(output.stdout.slice(1, 3)).to.equal('ID') }) - fancy - .stdout() - .end('shows extended columns/uses get() for value', output => { + fancy.stdout().end('shows extended columns/uses get() for value', (output) => { ux.table(apps, columns, {extended: true}) expect(output.stdout).to.equal(`${ws}${extendedHeader} โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€${ws} @@ -119,16 +111,12 @@ describe('styled/table', () => { }) describe('options', () => { - fancy - .stdout() - .end('shows extended columns', output => { + fancy.stdout().end('shows extended columns', (output) => { ux.table(apps, columns, {extended: true}) expect(output.stdout).to.contain(extendedHeader) }) - fancy - .stdout() - .end('shows title with divider', output => { + fancy.stdout().end('shows title with divider', (output) => { ux.table(apps, columns, {title: 'testing'}) expect(output.stdout).to.equal(`testing ======================= @@ -138,17 +126,13 @@ describe('styled/table', () => { | 321 supertable-test-2${ws}\n`) }) - fancy - .stdout() - .end('skips header', output => { + fancy.stdout().end('skips header', (output) => { ux.table(apps, columns, {'no-header': true}) expect(output.stdout).to.equal(` 123 supertable-test-1${ws} 321 supertable-test-2${ws}\n`) }) - fancy - .stdout() - .end('only displays given columns', output => { + fancy.stdout().end('only displays given columns', (output) => { ux.table(apps, columns, {columns: 'id'}) expect(output.stdout).to.equal(` ID${ws}${ws} โ”€โ”€โ”€${ws} @@ -156,36 +140,36 @@ describe('styled/table', () => { 321${ws}\n`) }) - fancy - .stdout() - .end('outputs in csv', output => { + fancy.stdout().end('outputs in csv', (output) => { ux.table(apps, columns, {output: 'csv'}) expect(output.stdout).to.equal(`ID,Name 123,supertable-test-1 321,supertable-test-2\n`) }) - fancy - .stdout() - .end('outputs in csv with escaped values', output => { - ux.table([ - { - id: '123\n2', - name: 'supertable-test-1', - }, - { - id: '12"3', - name: 'supertable-test-2', - }, - { - id: '123', - name: 'supertable-test-3,comma', - }, - { - id: '123', - name: 'supertable-test-4', - }, - ], columns, {output: 'csv'}) + fancy.stdout().end('outputs in csv with escaped values', (output) => { + ux.table( + [ + { + id: '123\n2', + name: 'supertable-test-1', + }, + { + id: '12"3', + name: 'supertable-test-2', + }, + { + id: '123', + name: 'supertable-test-3,comma', + }, + { + id: '123', + name: 'supertable-test-4', + }, + ], + columns, + {output: 'csv'}, + ) expect(output.stdout).to.equal(`ID,Name "123\n2","supertable-test-1" "12""3","supertable-test-2" @@ -193,26 +177,20 @@ describe('styled/table', () => { 123,supertable-test-4\n`) }) - fancy - .stdout() - .end('outputs in csv without headers', output => { + fancy.stdout().end('outputs in csv without headers', (output) => { ux.table(apps, columns, {output: 'csv', 'no-header': true}) expect(output.stdout).to.equal(`123,supertable-test-1 321,supertable-test-2\n`) }) - fancy - .stdout() - .end('outputs in csv with alias flag', output => { + fancy.stdout().end('outputs in csv with alias flag', (output) => { ux.table(apps, columns, {csv: true}) expect(output.stdout).to.equal(`ID,Name 123,supertable-test-1 321,supertable-test-2\n`) }) - fancy - .stdout() - .end('outputs in json', output => { + fancy.stdout().end('outputs in json', (output) => { ux.table(apps, columns, {output: 'json'}) expect(output.stdout).to.equal(`[ { @@ -227,9 +205,7 @@ describe('styled/table', () => { `) }) - fancy - .stdout() - .end('outputs in yaml', output => { + fancy.stdout().end('outputs in yaml', (output) => { ux.table(apps, columns, {output: 'yaml'}) expect(output.stdout).to.equal(`- id: '123' name: supertable-test-1 @@ -239,9 +215,7 @@ describe('styled/table', () => { `) }) - fancy - .stdout() - .end('sorts by property', output => { + fancy.stdout().end('sorts by property', (output) => { ux.table(apps, columns, {sort: '-name'}) expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€${ws} @@ -249,18 +223,14 @@ describe('styled/table', () => { 123 supertable-test-1${ws}\n`) }) - fancy - .stdout() - .end('filters by property & value (partial string match)', output => { + fancy.stdout().end('filters by property & value (partial string match)', (output) => { ux.table(apps, columns, {filter: 'id=123'}) expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€${ws} 123 supertable-test-1${ws}\n`) }) - fancy - .stdout() - .end('does not truncate', output => { + fancy.stdout().end('does not truncate', (output) => { const three = {...apps[0], id: '0'.repeat(80), name: 'supertable-test-3'} ux.table([...apps, three], columns, {filter: 'id=0', 'no-truncate': true}) expect(output.stdout).to.equal(` ID${ws.padEnd(78)} Name${ws.padEnd(14)} @@ -270,15 +240,13 @@ describe('styled/table', () => { }) describe('#flags', () => { - fancy - .end('includes only flags', _ => { + fancy.end('includes only flags', (_) => { const flags = ux.table.flags({only: 'columns'}) expect(flags.columns).to.be.a('object') expect((flags as any).sort).to.be.undefined }) - fancy - .end('excludes except flags', _ => { + fancy.end('excludes except flags', (_) => { const flags = ux.table.flags({except: 'columns'}) expect((flags as any).columns).to.be.undefined expect(flags.sort).to.be.a('object') @@ -286,9 +254,7 @@ describe('styled/table', () => { }) describe('edge cases', () => { - fancy - .stdout() - .end('ignores header case', output => { + fancy.stdout().end('ignores header case', (output) => { ux.table(apps, columns, {columns: 'iD,Name', filter: 'nAMe=supertable-test', sort: '-ID'}) expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€${ws} @@ -296,9 +262,7 @@ describe('styled/table', () => { 123 supertable-test-1${ws}\n`) }) - fancy - .stdout() - .end('displays multiline cell', output => { + fancy.stdout().end('displays multiline cell', (output) => { /* eslint-disable camelcase */ const app3 = { build_stack: { @@ -325,31 +289,31 @@ describe('styled/table', () => { } fancy - .do(() => { - Object.assign(screen, {stdtermwidth: 9}) - process.env.CLI_UX_SKIP_TTY_CHECK = 'true' - }) - .finally(() => { - Object.assign(screen, {stdtermwidth: orig.stdtermwidth}) - process.env.CLI_UX_SKIP_TTY_CHECK = orig.CLI_UX_SKIP_TTY_CHECK - }) - .stdout({stripColor: false}) - .end('correctly truncates columns with fullwidth characters or ansi escape sequences', output => { - /* eslint-disable camelcase */ - const app4 = { - build_stack: { - name: 'heroku-16', - }, - id: '456', - name: '\u001B[31m่ถ…็บง่กจๆ ผโ€”ๆต‹่ฏ•\u001B[0m', - web_url: 'https://supertable-test-1.herokuapp.com/', - } - /* eslint-enable camelcase */ - - ux.table([...apps, app4 as any], {name: {}}, {'no-header': true}) - expect(output.stdout).to.equal(` superโ€ฆ${ws} + .do(() => { + Object.assign(screen, {stdtermwidth: 9}) + process.env.CLI_UX_SKIP_TTY_CHECK = 'true' + }) + .finally(() => { + Object.assign(screen, {stdtermwidth: orig.stdtermwidth}) + process.env.CLI_UX_SKIP_TTY_CHECK = orig.CLI_UX_SKIP_TTY_CHECK + }) + .stdout({stripColor: false}) + .end('correctly truncates columns with fullwidth characters or ansi escape sequences', (output) => { + /* eslint-disable camelcase */ + const app4 = { + build_stack: { + name: 'heroku-16', + }, + id: '456', + name: '\u001B[31m่ถ…็บง่กจๆ ผโ€”ๆต‹่ฏ•\u001B[0m', + web_url: 'https://supertable-test-1.herokuapp.com/', + } + /* eslint-enable camelcase */ + + ux.table([...apps, app4 as any], {name: {}}, {'no-header': true}) + expect(output.stdout).to.equal(` superโ€ฆ${ws} superโ€ฆ${ws} \u001B[31m่ถ…็บง\u001B[39mโ€ฆ${ws}${ws}\n`) - }) + }) }) }) diff --git a/test/cli-ux/styled/tree.test.ts b/test/cli-ux/styled/tree.test.ts index 0706d0649..c628bdec0 100644 --- a/test/cli-ux/styled/tree.test.ts +++ b/test/cli-ux/styled/tree.test.ts @@ -3,9 +3,7 @@ import {expect, fancy} from 'fancy-test' import {ux} from '../../../src/cli-ux' describe('styled/tree', () => { - fancy - .stdout() - .end('shows the tree', output => { + fancy.stdout().end('shows the tree', (output) => { const tree = ux.tree() tree.insert('foo') tree.insert('bar') diff --git a/test/command/command.test.ts b/test/command/command.test.ts index ddbad21bb..9cb949e15 100644 --- a/test/command/command.test.ts +++ b/test/command/command.test.ts @@ -1,6 +1,7 @@ import {expect, fancy} from 'fancy-test' // import path = require('path') -import {Args, Command as Base, Flags, toCached} from '../../src' +import {Command as Base, Flags} from '../../src' +import {ensureArgObject} from '../../src/util/ensure-arg-object' // import {TestHelpClassConfig} from './helpers/test-help-in-src/src/test-help-plugin' // const pjson = require('../package.json') @@ -23,258 +24,54 @@ class CodeError extends Error { describe('command', () => { fancy - .stdout() - .do(() => Command.run([])) - .do(output => expect(output.stdout).to.equal('foo\n')) - .it('logs to stdout') + .stdout() + .do(() => Command.run([])) + .do((output) => expect(output.stdout).to.equal('foo\n')) + .it('logs to stdout') fancy - .do(async () => { - class Command extends Base { - static description = 'test command' + .do(async () => { + class Command extends Base { + static description = 'test command' - async run() { - return 101 + async run() { + return 101 + } } - } - expect(await Command.run([])).to.equal(101) - }) - .it('returns value') + expect(await Command.run([])).to.equal(101) + }) + .it('returns value') fancy - .do(() => { - class Command extends Base { - async run() { - throw new Error('new x error') + .do(() => { + class Command extends Base { + async run() { + throw new Error('new x error') + } } - } - return Command.run([]) - }) - .catch(/new x error/) - .it('errors out') + return Command.run([]) + }) + .catch(/new x error/) + .it('errors out') fancy - .stdout() - .do(() => { - class Command extends Base { - async run() { - this.exit(0) - } - } - - return Command.run([]) - }) - .catch(/EEXIT: 0/) - .it('exits with 0') - - describe('toCached', () => { - fancy - .do(async () => { - class C extends Command { - static id = 'foo:bar' - static title = 'cmd title' - static type = 'mytype' - static usage = ['$ usage'] - static description = 'test command' - static aliases = ['alias1', 'alias2'] - static hidden = true - // @ts-ignore - static flags = { - flaga: Flags.boolean(), - flagb: Flags.string({ - char: 'b', - hidden: true, - required: false, - description: 'flagb desc', - options: ['a', 'b'], - default: async () => 'a', - }), - flagc: Flags.integer({ - char: 'c', - min: 1, - max: 10, - required: false, - description: 'flagc desc', - options: ['a', 'b'], - // @ts-expect-error: context is any - default: async context => context.options.min + 1, - }), - } - - static args = { - arg1: Args.string({ - description: 'arg1 desc', - required: true, - hidden: false, - options: ['af', 'b'], - default: async () => 'a', - }), + .stdout() + .do(() => { + class Command extends Base { + async run() { + this.exit(0) } } - const c = await toCached(C, undefined, false) - - expect(c).to.deep.equal({ - id: 'foo:bar', - type: 'mytype', - hidden: true, - pluginName: undefined, - pluginAlias: undefined, - pluginType: undefined, - state: undefined, - description: 'test command', - aliases: ['alias1', 'alias2'], - title: 'cmd title', - usage: ['$ usage'], - examples: undefined, - deprecationOptions: undefined, - deprecateAliases: undefined, - summary: undefined, - strict: true, - flags: { - flaga: { - aliases: undefined, - char: undefined, - charAliases: undefined, - description: undefined, - dependsOn: undefined, - deprecateAliases: undefined, - deprecated: undefined, - exclusive: undefined, - helpGroup: undefined, - helpLabel: undefined, - summary: undefined, - name: 'flaga', - hidden: undefined, - required: undefined, - relationships: undefined, - allowNo: false, - type: 'boolean', - delimiter: undefined, - noCacheDefault: undefined, - }, - flagb: { - aliases: undefined, - char: 'b', - charAliases: undefined, - description: 'flagb desc', - dependsOn: undefined, - deprecateAliases: undefined, - deprecated: undefined, - exclusive: undefined, - helpGroup: undefined, - helpLabel: undefined, - summary: undefined, - name: 'flagb', - hidden: true, - required: false, - multiple: false, - relationships: undefined, - type: 'option', - helpValue: undefined, - default: 'a', - options: ['a', 'b'], - delimiter: undefined, - noCacheDefault: undefined, - }, - flagc: { - aliases: undefined, - char: 'c', - charAliases: undefined, - default: 2, - delimiter: undefined, - dependsOn: undefined, - deprecateAliases: undefined, - deprecated: undefined, - description: 'flagc desc', - exclusive: undefined, - helpGroup: undefined, - helpLabel: undefined, - helpValue: undefined, - hidden: undefined, - multiple: false, - name: 'flagc', - options: [ - 'a', - 'b', - ], - relationships: undefined, - required: false, - summary: undefined, - type: 'option', - noCacheDefault: undefined, - }, - - }, - args: { - arg1: { - description: 'arg1 desc', - name: 'arg1', - hidden: false, - required: true, - options: ['af', 'b'], - default: 'a', - noCacheDefault: undefined, - }, - }, - }) - }) - .it('converts to cached with everything set') - - fancy - // .skip() - .do(async () => { - // const c = class extends Command { - // }.convertToCached() - // expect(await c.load()).to.have.property('run') - // delete c.load - // expect(c).to.deep.equal({ - // _base: `@oclif/command@${pjson.version}`, - // id: undefined, - // type: undefined, - // hidden: undefined, - // pluginName: undefined, - // description: 'test command', - // aliases: [], - // title: undefined, - // usage: undefined, - // flags: {}, - // args: [], - // }) + return Command.run([]) }) - - .it('adds plugin name') - - fancy - // .skip() - // .do(async () => { - // const c = class extends Command { - // }.convertToCached({pluginName: 'myplugin'}) - // expect(await c.load()).to.have.property('run') - // delete c.load - // expect(c).to.deep.equal({ - // _base: `@oclif/command@${pjson.version}`, - // type: undefined, - // id: undefined, - // hidden: undefined, - // pluginName: 'myplugin', - // description: 'test command', - // aliases: [], - // title: undefined, - // usage: undefined, - // flags: {}, - // args: [], - // }) - // }) - .it('converts to cached with nothing set') - }) + .catch(/EEXIT: 0/) + .it('exits with 0') describe('parse', () => { - fancy - .stdout() - .it('has a flag', async ctx => { + fancy.stdout().it('has a flag', async (ctx) => { class CMD extends Base { static flags = { foo: Flags.string(), @@ -293,18 +90,18 @@ describe('command', () => { describe('.log()', () => { fancy - .stdout() - .do(async () => { - class CMD extends Command { - async run() { - this.log('json output: %j', {a: 'foobar'}) + .stdout() + .do(async () => { + class CMD extends Command { + async run() { + this.log('json output: %j', {a: 'foobar'}) + } } - } - await CMD.run([]) - }) - .do(ctx => expect(ctx.stdout).to.equal('json output: {"a":"foobar"}\n')) - .it('uses util.format()') + await CMD.run([]) + }) + .do((ctx) => expect(ctx.stdout).to.equal('json output: {"a":"foobar"}\n')) + .it('uses util.format()') }) describe('flags with deprecated aliases', () => { @@ -325,334 +122,350 @@ describe('command', () => { } fancy - .stdout() - .stderr() - .do(async () => CMD.run(['--username', 'astro'])) - .do(ctx => expect(ctx.stderr).to.include('Warning: The "--username" flag has been deprecated. Use "--name | -o"')) - .it('shows warning for deprecated flag alias') + .stdout() + .stderr() + .do(async () => CMD.run(['--username', 'astro'])) + .do((ctx) => + expect(ctx.stderr).to.include('Warning: The "--username" flag has been deprecated. Use "--name | -o"'), + ) + .it('shows warning for deprecated flag alias') fancy - .stdout() - .stderr() - .do(async () => CMD.run(['--target-user', 'astro'])) - .do(ctx => expect(ctx.stderr).to.include('Warning: The "--target-user" flag has been deprecated. Use "--name | -o"')) - .it('shows warning for deprecated flag alias') + .stdout() + .stderr() + .do(async () => CMD.run(['--target-user', 'astro'])) + .do((ctx) => + expect(ctx.stderr).to.include('Warning: The "--target-user" flag has been deprecated. Use "--name | -o"'), + ) + .it('shows warning for deprecated flag alias') fancy - .stdout() - .stderr() - .do(async () => CMD.run(['-u', 'astro'])) - .do(ctx => expect(ctx.stderr).to.include('Warning: The "-u" flag has been deprecated. Use "--name | -o"')) - .it('shows warning for deprecated short char flag alias') + .stdout() + .stderr() + .do(async () => CMD.run(['-u', 'astro'])) + .do((ctx) => expect(ctx.stderr).to.include('Warning: The "-u" flag has been deprecated. Use "--name | -o"')) + .it('shows warning for deprecated short char flag alias') fancy - .stdout() - .stderr() - .do(async () => CMD.run(['--name', 'username'])) - .do(ctx => expect(ctx.stderr).to.be.empty) - .it('shows no warning when using proper flag name with a value that matches a flag alias') + .stdout() + .stderr() + .do(async () => CMD.run(['--name', 'username'])) + .do((ctx) => expect(ctx.stderr).to.be.empty) + .it('shows no warning when using proper flag name with a value that matches a flag alias') fancy - .stdout() - .stderr() - .do(async () => CMD.run(['--other', 'target-user'])) - .do(ctx => expect(ctx.stderr).to.be.empty) - .it('shows no warning when using another flag with a value that matches a deprecated flag alias') + .stdout() + .stderr() + .do(async () => CMD.run(['--other', 'target-user'])) + .do((ctx) => expect(ctx.stderr).to.be.empty) + .it('shows no warning when using another flag with a value that matches a deprecated flag alias') fancy - .stdout() - .stderr() - .do(async () => CMD.run(['--name', 'u'])) - .do(ctx => expect(ctx.stderr).to.be.empty) - .it('shows no warning when proper flag name with a value that matches a short char flag alias') + .stdout() + .stderr() + .do(async () => CMD.run(['--name', 'u'])) + .do((ctx) => expect(ctx.stderr).to.be.empty) + .it('shows no warning when proper flag name with a value that matches a short char flag alias') }) describe('deprecated flags', () => { fancy - .stdout() - .stderr() - .do(async () => { - class CMD extends Command { - static flags = { - name: Flags.string({ - deprecated: { - to: '--full-name', - version: '2.0.0', - }, - }), - force: Flags.boolean(), - } - - async run() { - await this.parse(CMD) - this.log('running command') + .stdout() + .stderr() + .do(async () => { + class CMD extends Command { + static flags = { + name: Flags.string({ + deprecated: { + to: '--full-name', + version: '2.0.0', + }, + }), + force: Flags.boolean(), + } + + async run() { + await this.parse(CMD) + this.log('running command') + } } - } - await CMD.run(['--name', 'astro']) - }) - .do(ctx => expect(ctx.stderr).to.include('Warning: The "name" flag has been deprecated')) - .it('shows warning for deprecated flags') + await CMD.run(['--name', 'astro']) + }) + .do((ctx) => expect(ctx.stderr).to.include('Warning: The "name" flag has been deprecated')) + .it('shows warning for deprecated flags') }) describe('deprecated flags that are not provided', () => { fancy - .stdout() - .stderr() - .do(async () => { - class CMD extends Command { - static flags = { - name: Flags.string({ - deprecated: { - to: '--full-name', - version: '2.0.0', - }, - }), - force: Flags.boolean(), - } - - async run() { - await this.parse(CMD) - this.log('running command') + .stdout() + .stderr() + .do(async () => { + class CMD extends Command { + static flags = { + name: Flags.string({ + deprecated: { + to: '--full-name', + version: '2.0.0', + }, + }), + force: Flags.boolean(), + } + + async run() { + await this.parse(CMD) + this.log('running command') + } } - } - await CMD.run(['--force']) - }) - .do(ctx => expect(ctx.stderr).to.not.include('Warning: The "name" flag has been deprecated')) - .it('does not show warning for deprecated flags if they are not provided') + await CMD.run(['--force']) + }) + .do((ctx) => expect(ctx.stderr).to.not.include('Warning: The "name" flag has been deprecated')) + .it('does not show warning for deprecated flags if they are not provided') }) describe('deprecated state', () => { fancy - .stdout() - .stderr() - .do(async () => { - class CMD extends Command { - static id = 'my:command' - static state = 'deprecated' - - async run() { - this.log('running command') + .stdout() + .stderr() + .do(async () => { + class CMD extends Command { + static id = 'my:command' + static state = 'deprecated' + + async run() { + this.log('running command') + } } - } - await CMD.run([]) - }) - .do(ctx => expect(ctx.stderr).to.include('Warning: The "my:command" command has been deprecated')) - .it('shows warning for deprecated command') + await CMD.run([]) + }) + .do((ctx) => expect(ctx.stderr).to.include('Warning: The "my:command" command has been deprecated')) + .it('shows warning for deprecated command') }) describe('deprecated state with options', () => { fancy - .stdout() - .stderr() - .do(async () => { - class CMD extends Command { - static id = 'my:command' - static state = 'deprecated' - static deprecationOptions = { - version: '2.0.0', - to: 'my:other:command', + .stdout() + .stderr() + .do(async () => { + class CMD extends Command { + static id = 'my:command' + static state = 'deprecated' + static deprecationOptions = { + version: '2.0.0', + to: 'my:other:command', + } + + async run() { + this.log('running command') + } } - async run() { - this.log('running command') - } - } - - await CMD.run([]) - }) - .do(ctx => { - expect(ctx.stderr).to.include('Warning: The "my:command" command has been deprecated') - expect(ctx.stderr).to.include('in version 2.0.0') - expect(ctx.stderr).to.include('Use "my:other:command" instead') - }) - .it('shows warning for deprecated command with custom options') + await CMD.run([]) + }) + .do((ctx) => { + expect(ctx.stderr).to.include('Warning: The "my:command" command has been deprecated') + expect(ctx.stderr).to.include('in version 2.0.0') + expect(ctx.stderr).to.include('Use "my:other:command" instead') + }) + .it('shows warning for deprecated command with custom options') }) describe('stdout err', () => { fancy - .stdout() - .do(async () => { - class CMD extends Command { - async run() { - process.stdout.emit('error', new CodeError('dd')) + .stdout() + .do(async () => { + class CMD extends Command { + async run() { + process.stdout.emit('error', new CodeError('dd')) + } } - } - await CMD.run([]) - }) - .catch(/dd/) - .it('test stdout error throws') + await CMD.run([]) + }) + .catch(/dd/) + .it('test stdout error throws') fancy - .stdout() - .do(async () => { - class CMD extends Command { - async run() { - process.stdout.emit('error', new CodeError('EPIPE')) - this.log('json output: %j', {a: 'foobar'}) + .stdout() + .do(async () => { + class CMD extends Command { + async run() { + process.stdout.emit('error', new CodeError('EPIPE')) + this.log('json output: %j', {a: 'foobar'}) + } } - } - await CMD.run([]) - }) - .do(ctx => expect(ctx.stdout).to.equal('json output: {"a":"foobar"}\n')) - .it('test stdout EPIPE swallowed') + await CMD.run([]) + }) + .do((ctx) => expect(ctx.stdout).to.equal('json output: {"a":"foobar"}\n')) + .it('test stdout EPIPE swallowed') }) describe('json enabled and pass-through tests', () => { fancy - .stdout() - .do(async () => { - class CMD extends Command { - static enableJsonFlag = true - - async run() { - this.log('not json output') + .stdout() + .do(async () => { + class CMD extends Command { + static enableJsonFlag = true + + async run() { + this.log('not json output') + } } - } - const cmd = new CMD([], {} as any) - expect(cmd.jsonEnabled()).to.equal(false) - }) - .it('json enabled/pass through disabled/no --json flag/jsonEnabled() should be false') + const cmd = new CMD([], {} as any) + expect(cmd.jsonEnabled()).to.equal(false) + }) + .it('json enabled/pass through disabled/no --json flag/jsonEnabled() should be false') fancy - .stdout() - .do(async () => { - class CMD extends Command { - static enableJsonFlag = true + .stdout() + .do(async () => { + class CMD extends Command { + static enableJsonFlag = true - async run() {} - } + async run() {} + } - const cmd = new CMD(['--json'], {} as any) - expect(cmd.jsonEnabled()).to.equal(true) - }) - .it('json enabled/pass through disabled/--json flag before --/jsonEnabled() should be true') + const cmd = new CMD(['--json'], {} as any) + expect(cmd.jsonEnabled()).to.equal(true) + }) + .it('json enabled/pass through disabled/--json flag before --/jsonEnabled() should be true') fancy - .stdout() - .do(async () => { - class CMD extends Command { - static enableJsonFlag = true - async run() {} - } + .stdout() + .do(async () => { + class CMD extends Command { + static enableJsonFlag = true + async run() {} + } - // mock a scopedEnvVar being set to JSON - const cmd = new CMD([], { - bin: 'FOO', scopedEnvVar: (foo: string) => foo.includes('CONTENT_TYPE') ? 'json' : undefined, - } as any) - expect(cmd.jsonEnabled()).to.equal(true) - }) - .it('json enabled from env') + // mock a scopedEnvVar being set to JSON + const cmd = new CMD([], { + bin: 'FOO', + scopedEnvVar: (foo: string) => (foo.includes('CONTENT_TYPE') ? 'json' : undefined), + } as any) + expect(cmd.jsonEnabled()).to.equal(true) + }) + .it('json enabled from env') fancy - .stdout() - .do(async () => { - class CMD extends Command { - static enableJsonFlag = true - static '--' = true - - async run() { - const {flags} = await cmd.parse(CMD, ['--json']) - expect(flags.json).to.equal(true, 'json flag should be true') + .stdout() + .do(async () => { + class CMD extends Command { + static enableJsonFlag = true + + async run() { + const {flags} = await cmd.parse(CMD, ['--json']) + expect(flags.json).to.equal(true, 'json flag should be true') + } } - } - const cmd = new CMD(['--json'], {} as any) - expect(cmd.jsonEnabled()).to.equal(true) - }) - .it('json enabled/pass through enabled/--json flag before --/jsonEnabled() should be true') + const cmd = new CMD(['--json'], {} as any) + expect(cmd.jsonEnabled()).to.equal(true) + }) + .it('json enabled/pass through enabled/--json flag before --/jsonEnabled() should be true') fancy - .stdout() - .do(async () => { - class CMD extends Command { - // static initialization block is required whenever using ES2022 - static { - this.enableJsonFlag = true - this['--'] = true + .stdout() + .do(async () => { + class CMD extends Command { + static enableJsonFlag = true + + async run() { + const {flags} = await cmd.parse(CMD, ['--', '--json']) + expect(flags.json).to.equal(false, 'json flag should be false') + // expect(this.passThroughEnabled).to.equal(true, 'pass through should be true') + } } - async run() { - const {flags} = await cmd.parse(CMD, ['--', '--json']) - expect(flags.json).to.equal(false, 'json flag should be false') - expect(this.passThroughEnabled).to.equal(true, 'pass through should be true') + const cmd = new CMD(['--', '--json'], {} as any) + expect(cmd.jsonEnabled()).to.equal(false) + }) + .it('json enabled/pass through enabled/--json flag after --/jsonEnabled() should be false') + + fancy + .stdout() + .do(async () => { + class CMD extends Command { + static enableJsonFlag = true + + async run() { + const {flags} = await cmd.parse(CMD, ['--foo', '--json']) + expect(flags.json).to.equal(true, 'json flag should be true') + } } - } - const cmd = new CMD(['--', '--json'], {} as any) - expect(cmd.jsonEnabled()).to.equal(false) - }) - .it('json enabled/pass through enabled/--json flag after --/jsonEnabled() should be false') + const cmd = new CMD(['foo', '--json'], {} as any) + expect(cmd.jsonEnabled()).to.equal(true) + }) + .it('json enabled/pass through enabled/--json flag before --/extra param/jsonEnabled() should be true') fancy - .stdout() - .do(async () => { - class CMD extends Command { - static enableJsonFlag = true - static '--' = true - - async run() { - const {flags} = await cmd.parse(CMD, ['--foo', '--json']) - expect(flags.json).to.equal(true, 'json flag should be true') + .stdout() + .do(async () => { + class CMD extends Command { + static enableJsonFlag = true + + async run() { + const {flags} = await cmd.parse(CMD, ['--foo', '--', '--json']) + expect(flags.json).to.equal(false, 'json flag should be false') + // expect(this.passThroughEnabled).to.equal(true, 'pass through should be true') + } } - } - const cmd = new CMD(['foo', '--json'], {} as any) - expect(cmd.jsonEnabled()).to.equal(true) - }) - .it('json enabled/pass through enabled/--json flag before --/extra param/jsonEnabled() should be true') + const cmd = new CMD(['--foo', '--', '--json'], {} as any) + expect(cmd.jsonEnabled()).to.equal(false) + }) + .it('json enabled/pass through enabled/--json flag after --/extra param/jsonEnabled() should be false') fancy - .stdout() - .do(async () => { - class CMD extends Command { - static enableJsonFlag = true - static '--' = true + .stdout() + .do(async () => { + class CMD extends Command { + static enableJsonFlag = true - async run() { - const {flags} = await cmd.parse(CMD, ['--foo', '--', '--json']) - expect(flags.json).to.equal(false, 'json flag should be false') - expect(this.passThroughEnabled).to.equal(true, 'pass through should be true') + async run() {} } - } - const cmd = new CMD(['--foo', '--', '--json'], {} as any) - expect(cmd.jsonEnabled()).to.equal(false) - }) - .it('json enabled/pass through enabled/--json flag after --/extra param/jsonEnabled() should be false') + const cmd = new CMD(['--json', '--'], {} as any) + expect(cmd.jsonEnabled()).to.equal(true) + }) + .it('json enabled/pass through enabled/--json flag before --/jsonEnabled() should be true') fancy - .stdout() - .do(async () => { - class CMD extends Command { - static enableJsonFlag = true - static '--' = true - - async run() {} - } + .stdout() + .do(async () => { + class CMD extends Command { + static enableJsonFlag = false + static '--' = true - const cmd = new CMD(['--json', '--'], {} as any) - expect(cmd.jsonEnabled()).to.equal(true) - }) - .it('json enabled/pass through enabled/--json flag before --/jsonEnabled() should be true') + async run() {} + } - fancy - .stdout() - .do(async () => { - class CMD extends Command { - static enableJsonFlag = false - static '--' = true + const cmd = new CMD(['--json'], {} as any) + expect(cmd.jsonEnabled()).to.equal(false) + }) + .it('json disabled/pass through enable/--json flag before --/jsonEnabled() should be false') + }) +}) - async run() {} - } +describe('ensureArgObject', () => { + it('should convert array of arguments to an object', () => { + const args = [ + {name: 'foo', description: 'foo desc', required: true}, + {name: 'bar', description: 'bar desc'}, + ] + const expected = {foo: args[0], bar: args[1]} + expect(ensureArgObject(args)).to.deep.equal(expected) + }) - const cmd = new CMD(['--json'], {} as any) - expect(cmd.jsonEnabled()).to.equal(false) - }) - .it('json disabled/pass through enable/--json flag before --/jsonEnabled() should be false') + it('should do nothing to an arguments object', () => { + const args = { + foo: {name: 'foo', description: 'foo desc', required: true}, + bar: {name: 'bar', description: 'bar desc'}, + } + expect(ensureArgObject(args)).to.deep.equal(args) }) }) diff --git a/test/command/fixtures/typescript/tsconfig.json b/test/command/fixtures/typescript/tsconfig.json index 75c61de88..8d0083147 100644 --- a/test/command/fixtures/typescript/tsconfig.json +++ b/test/command/fixtures/typescript/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "outDir": "./lib", - "rootDirs": [ - "./src" - ] + "rootDirs": ["./src"] }, - "include": [ - "./src/**/*" - ] + "include": ["./src/**/*"] } diff --git a/test/command/helpers/test-help-in-lib/lib/test-help-plugin.js b/test/command/helpers/test-help-in-lib/lib/test-help-plugin.js index 77e76fd87..1bad603da 100644 --- a/test/command/helpers/test-help-in-lib/lib/test-help-plugin.js +++ b/test/command/helpers/test-help-in-lib/lib/test-help-plugin.js @@ -1,20 +1,20 @@ /* eslint-disable */ -'use strict'; -Object.defineProperty (exports, '__esModule', {value: true}); -const sinon_1 = require ('sinon'); +'use strict' +Object.defineProperty(exports, '__esModule', {value: true}) +const sinon_1 = require('sinon') class default_1 { - constructor (config, opts) { - this.showCommandHelp = sinon_1.spy (() => { - console.log ('hello from test-help-plugin #showCommandHelp in the lib folder and in compiled javascript'); - }); - this.showHelp = sinon_1.spy (() => { - console.log ('hello showHelp'); - }); - config.showCommandHelpSpy = this.showCommandHelp; - config.showHelpSpy = this.showHelp; + constructor(config, opts) { + this.showCommandHelp = sinon_1.spy(() => { + console.log('hello from test-help-plugin #showCommandHelp in the lib folder and in compiled javascript') + }) + this.showHelp = sinon_1.spy(() => { + console.log('hello showHelp') + }) + config.showCommandHelpSpy = this.showCommandHelp + config.showHelpSpy = this.showHelp } - command () { - throw new Error ('not needed for testing @oclif/command'); + command() { + throw new Error('not needed for testing @oclif/command') } } -exports.default = default_1; +exports.default = default_1 diff --git a/test/command/helpers/test-help-in-src/src/test-help-plugin.ts b/test/command/helpers/test-help-in-src/src/test-help-plugin.ts index de25f5e50..60ccafb70 100644 --- a/test/command/helpers/test-help-in-src/src/test-help-plugin.ts +++ b/test/command/helpers/test-help-in-src/src/test-help-plugin.ts @@ -1,7 +1,7 @@ import {SinonSpy, spy} from 'sinon' import {HelpBase, Interfaces} from '../../../../../src' -export type TestHelpClassConfig = Interfaces.Config & { showCommandHelpSpy?: SinonSpy; showHelpSpy?: SinonSpy } +export type TestHelpClassConfig = Interfaces.Config & {showCommandHelpSpy?: SinonSpy; showHelpSpy?: SinonSpy} export default class extends HelpBase { constructor(config: any, opts: any) { diff --git a/test/command/main-esm.test.ts b/test/command/main-esm.test.ts index d971a48aa..93525219d 100644 --- a/test/command/main-esm.test.ts +++ b/test/command/main-esm.test.ts @@ -15,21 +15,22 @@ root = convertToFileURL(root) describe('main-esm', () => { fancy - .stdout() - .do(() => run(['plugins'], root)) - .do((output: any) => expect(output.stdout).to.equal('No plugins installed.\n')) - .it('runs plugins') + .stdout() + .do(() => run(['plugins'], root)) + .do((output: any) => expect(output.stdout).to.equal('No plugins installed.\n')) + .it('runs plugins') fancy - .stdout() - .do(() => run(['--version'], root)) - .do((output: any) => expect(output.stdout).to.equal(version + '\n')) - .it('runs --version') + .stdout() + .do(() => run(['--version'], root)) + .do((output: any) => expect(output.stdout).to.equal(version + '\n')) + .it('runs --version') fancy - .stdout() - .do(() => run(['--help'], root)) - .do((output: any) => expect(output.stdout).to.equal(`base library for oclif CLIs + .stdout() + .do(() => run(['--help'], root)) + .do((output: any) => + expect(output.stdout).to.equal(`base library for oclif CLIs VERSION ${version} @@ -44,13 +45,15 @@ COMMANDS help Display help for oclif. plugins List installed plugins. -`)) - .it('runs --help') +`), + ) + .it('runs --help') fancy - .stdout() - .do(() => run(['--help', 'foo'], convertToFileURL(resolve(__dirname, 'fixtures/esm/package.json')))) - .do((output: any) => expect(output.stdout).to.equal(`foo topic description + .stdout() + .do(() => run(['--help', 'foo'], convertToFileURL(resolve(__dirname, 'fixtures/esm/package.json')))) + .do((output: any) => + expect(output.stdout).to.equal(`foo topic description USAGE $ oclif-esm foo COMMAND @@ -61,13 +64,15 @@ TOPICS COMMANDS foo baz foo baz description -`)) - .it('runs spaced topic help') +`), + ) + .it('runs spaced topic help') fancy - .stdout() - .do(() => run(['foo', 'bar', '--help'], convertToFileURL(resolve(__dirname, 'fixtures/esm/package.json')))) - .do((output: any) => expect(output.stdout).to.equal(`foo bar topic description + .stdout() + .do(() => run(['foo', 'bar', '--help'], convertToFileURL(resolve(__dirname, 'fixtures/esm/package.json')))) + .do((output: any) => + expect(output.stdout).to.equal(`foo bar topic description USAGE $ oclif-esm foo bar COMMAND @@ -76,18 +81,19 @@ COMMANDS foo bar fail fail description foo bar succeed succeed description -`)) - .it('runs spaced topic help v2') +`), + ) + .it('runs spaced topic help v2') fancy - .stdout() - .do(() => run(['foo', 'baz'], convertToFileURL(resolve(__dirname, 'fixtures/esm/package.json')))) - .do((output: any) => expect(output.stdout).to.equal('running Baz\n')) - .it('runs foo:baz with space separator') + .stdout() + .do(() => run(['foo', 'baz'], convertToFileURL(resolve(__dirname, 'fixtures/esm/package.json')))) + .do((output: any) => expect(output.stdout).to.equal('running Baz\n')) + .it('runs foo:baz with space separator') fancy - .stdout() - .do(() => run(['foo', 'bar', 'succeed'], convertToFileURL(resolve(__dirname, 'fixtures/esm/package.json')))) - .do((output: any) => expect(output.stdout).to.equal('it works!\n')) - .it('runs foo:bar:succeed with space separator') + .stdout() + .do(() => run(['foo', 'bar', 'succeed'], convertToFileURL(resolve(__dirname, 'fixtures/esm/package.json')))) + .do((output: any) => expect(output.stdout).to.equal('it works!\n')) + .it('runs foo:bar:succeed with space separator') }) diff --git a/test/command/main.test.ts b/test/command/main.test.ts index 1b9b45583..c93ca83c4 100644 --- a/test/command/main.test.ts +++ b/test/command/main.test.ts @@ -1,9 +1,8 @@ - import {expect} from 'chai' import {resolve} from 'node:path' import {SinonSandbox, SinonStub, createSandbox} from 'sinon' import stripAnsi = require('strip-ansi') -import {requireJson} from '../../src/util' +import {requireJson} from '../../src/util/fs' import {run} from '../../src/main' import {Interfaces, stdout} from '../../src/index' @@ -35,7 +34,7 @@ describe('main', () => { it('should run help', async () => { await run(['--help'], resolve(__dirname, '../../package.json')) - expect(stdoutStub.args.map(a => stripAnsi(a[0])).join('')).to.equal(`base library for oclif CLIs + expect(stdoutStub.args.map((a) => stripAnsi(a[0])).join('')).to.equal(`base library for oclif CLIs VERSION ${version} @@ -55,7 +54,7 @@ COMMANDS it('should show help for topics with spaces', async () => { await run(['--help', 'foo'], resolve(__dirname, 'fixtures/typescript/package.json')) - expect(stdoutStub.args.map(a => stripAnsi(a[0])).join('')).to.equal(`foo topic description + expect(stdoutStub.args.map((a) => stripAnsi(a[0])).join('')).to.equal(`foo topic description USAGE $ oclif foo COMMAND @@ -71,7 +70,7 @@ COMMANDS it('should run spaced topic help v2', async () => { await run(['foo', 'bar', '--help'], resolve(__dirname, 'fixtures/typescript/package.json')) - expect(stdoutStub.args.map(a => stripAnsi(a[0])).join('')).to.equal(`foo bar topic description + expect(stdoutStub.args.map((a) => stripAnsi(a[0])).join('')).to.equal(`foo bar topic description USAGE $ oclif foo bar COMMAND diff --git a/test/config/config.flexible.test.ts b/test/config/config.flexible.test.ts index 829b00463..1cbc711ca 100644 --- a/test/config/config.flexible.test.ts +++ b/test/config/config.flexible.test.ts @@ -5,16 +5,16 @@ import {expect, fancy} from './test' import {Flags, Interfaces} from '../../src' import {Command} from '../../src/command' import {getCommandIdPermutations} from '../../src/config/util' -import * as util from '../../src/util' +import * as os from '../../src/util/os' import {join} from 'node:path' interface Options { - pjson?: any; - homedir?: string; - platform?: string; - env?: {[k: string]: string}; - commandIds?: string[]; - types?: string[]; + pjson?: any + homedir?: string + platform?: string + env?: {[k: string]: string} + commandIds?: string[] + types?: string[] } class MyCommandClass extends Command { @@ -42,10 +42,10 @@ describe('Config with flexible taxonomy', () => { types = [], }: Options = {}) => { let test = fancy - .resetConfig() - .env(env, {clear: true}) - .stub(util, 'getHomeDir', stub => stub.returns(join(homedir))) - .stub(util, 'getPlatform', stub => stub.returns(platform)) + .resetConfig() + .env(env, {clear: true}) + .stub(os, 'getHomeDir', (stub) => stub.returns(join(homedir))) + .stub(os, 'getPlatform', (stub) => stub.returns(platform)) const load = async (): Promise => {} const findCommand = async (): Promise => MyCommandClass @@ -101,6 +101,7 @@ describe('Config with flexible taxonomy', () => { tag: 'tag', moduleType: 'commonjs', hasManifest: false, + isRoot: false, } const pluginB: IPlugin = { @@ -121,6 +122,7 @@ describe('Config with flexible taxonomy', () => { tag: 'tag', moduleType: 'commonjs', hasManifest: false, + isRoot: false, } const plugins = new Map().set(pluginA.name, pluginA).set(pluginB.name, pluginB) @@ -142,117 +144,115 @@ describe('Config with flexible taxonomy', () => { // @ts-ignore return { it(expectation: string, fn: (config: Interfaces.Config) => any) { - test - .do(({config}) => fn(config)) - .it(expectation) + test.do(({config}) => fn(config)).it(expectation) return this }, } } testConfig() - .it('has populated topic index', config => { - // @ts-expect-error because private member - const topics = config._topics - expect(topics.has('foo')).to.be.true - expect(topics.has('foo:bar')).to.be.true - expect(topics.has('foo:baz')).to.be.true - }) - .it('has populated command permutation index', config => { - // @ts-expect-error because private member - const {commandPermutations} = config - expect(commandPermutations.get('foo')).to.deep.equal(new Set(['foo:bar', 'foo:baz'])) - expect(commandPermutations.get('foo:bar')).to.deep.equal(new Set(['foo:bar'])) - expect(commandPermutations.get('bar')).to.deep.equal(new Set(['foo:bar'])) - expect(commandPermutations.get('bar:foo')).to.deep.equal(new Set(['foo:bar'])) - expect(commandPermutations.get('foo:baz')).to.deep.equal(new Set(['foo:baz'])) - expect(commandPermutations.get('baz')).to.deep.equal(new Set(['foo:baz'])) - expect(commandPermutations.get('baz:foo')).to.deep.equal(new Set(['foo:baz'])) - }) - .it('has populated command index', config => { - // @ts-expect-error because private member - const commands = config._commands - expect(commands.has('foo:bar')).to.be.true - expect(commands.has('foo:baz')).to.be.true - }) - .it('has all command id permutations', config => { - expect(config.getAllCommandIDs()).to.deep.equal([ - 'foo:bar', - 'foo:baz', - 'bar:foo', - 'baz:foo', - ]) - }) - - describe('findMatches', () => { - testConfig() - .it('finds command that contains a partial id', config => { - const matches = config.findMatches('foo', []) - expect(matches.length).to.equal(2) + .it('has populated topic index', (config) => { + // @ts-expect-error because private member + const topics = config._topics + expect(topics.has('foo')).to.be.true + expect(topics.has('foo:bar')).to.be.true + expect(topics.has('foo:baz')).to.be.true + }) + .it('has populated command permutation index', (config) => { + // @ts-expect-error because private member + const {commandPermutations} = config + expect(commandPermutations.get('foo')).to.deep.equal(new Set(['foo:bar', 'foo:baz'])) + expect(commandPermutations.get('foo:bar')).to.deep.equal(new Set(['foo:bar'])) + expect(commandPermutations.get('bar')).to.deep.equal(new Set(['foo:bar'])) + expect(commandPermutations.get('bar:foo')).to.deep.equal(new Set(['foo:bar'])) + expect(commandPermutations.get('foo:baz')).to.deep.equal(new Set(['foo:baz'])) + expect(commandPermutations.get('baz')).to.deep.equal(new Set(['foo:baz'])) + expect(commandPermutations.get('baz:foo')).to.deep.equal(new Set(['foo:baz'])) }) - .it('finds command that contains a partial id and matching full flag', config => { - const matches = config.findMatches('foo', ['--flagB']) - expect(matches.length).to.equal(1) - expect(matches[0].id).to.equal('foo:baz') + .it('has populated command index', (config) => { + // @ts-expect-error because private member + const commands = config._commands + expect(commands.has('foo:bar')).to.be.true + expect(commands.has('foo:baz')).to.be.true }) - .it('finds command that contains a partial id and matching short flag', config => { - const matches = config.findMatches('foo', ['-a']) - expect(matches.length).to.equal(1) - expect(matches[0].id).to.equal('foo:bar') + .it('has all command id permutations', (config) => { + expect(config.getAllCommandIDs()).to.deep.equal(['foo:bar', 'foo:baz', 'bar:foo', 'baz:foo']) }) + + describe('findMatches', () => { + testConfig() + .it('finds command that contains a partial id', (config) => { + const matches = config.findMatches('foo', []) + expect(matches.length).to.equal(2) + }) + .it('finds command that contains a partial id and matching full flag', (config) => { + const matches = config.findMatches('foo', ['--flagB']) + expect(matches.length).to.equal(1) + expect(matches[0].id).to.equal('foo:baz') + }) + .it('finds command that contains a partial id and matching short flag', (config) => { + const matches = config.findMatches('foo', ['-a']) + expect(matches.length).to.equal(1) + expect(matches[0].id).to.equal('foo:bar') + }) }) describe('findCommand', () => { - testConfig() - .it('find command with no duplicates', config => { + testConfig().it('find command with no duplicates', (config) => { const command = config.findCommand('foo:bar', {must: true}) expect(command).to.have.property('pluginAlias', '@My/plugina') }) - testConfig({commandIds: ['foo:bar', 'foo:bar']}) - .it('find command with duplicates and choose the one that appears first in oclif.plugins', config => { - const command = config.findCommand('foo:bar', {must: true}) - expect(command).to.have.property('pluginAlias', '@My/pluginb') - }) + testConfig({commandIds: ['foo:bar', 'foo:bar']}).it( + 'find command with duplicates and choose the one that appears first in oclif.plugins', + (config) => { + const command = config.findCommand('foo:bar', {must: true}) + expect(command).to.have.property('pluginAlias', '@My/pluginb') + }, + ) - testConfig({types: ['core', 'user']}) - .it('find command with no duplicates core/user', config => { + testConfig({types: ['core', 'user']}).it('find command with no duplicates core/user', (config) => { const command = config.findCommand('foo:bar', {must: true}) expect(command).to.have.property('id', 'foo:bar') expect(command).to.have.property('pluginType', 'core') expect(command).to.have.property('pluginAlias', '@My/plugina') }) - testConfig({types: ['user', 'core']}) - .it('find command with no duplicates user/core', config => { + testConfig({types: ['user', 'core']}).it('find command with no duplicates user/core', (config) => { const command = config.findCommand('foo:bar', {must: true}) expect(command).to.have.property('id', 'foo:bar') expect(command).to.have.property('pluginType', 'user') expect(command).to.have.property('pluginAlias', '@My/plugina') }) - testConfig({commandIds: ['foo:bar', 'foo:bar'], types: ['core', 'user']}) - .it('find command with duplicates core/user', config => { - const command = config.findCommand('foo:bar', {must: true}) - expect(command).to.have.property('id', 'foo:bar') - expect(command).to.have.property('pluginType', 'core') - expect(command).to.have.property('pluginAlias', '@My/plugina') - }) + testConfig({commandIds: ['foo:bar', 'foo:bar'], types: ['core', 'user']}).it( + 'find command with duplicates core/user', + (config) => { + const command = config.findCommand('foo:bar', {must: true}) + expect(command).to.have.property('id', 'foo:bar') + expect(command).to.have.property('pluginType', 'core') + expect(command).to.have.property('pluginAlias', '@My/plugina') + }, + ) - testConfig({commandIds: ['foo:bar', 'foo:bar'], types: ['user', 'core']}) - .it('find command with duplicates user/core', config => { - const command = config.findCommand('foo:bar', {must: true}) - expect(command).to.have.property('id', 'foo:bar') - expect(command).to.have.property('pluginType', 'core') - expect(command).to.have.property('pluginAlias', '@My/pluginb') - }) + testConfig({commandIds: ['foo:bar', 'foo:bar'], types: ['user', 'core']}).it( + 'find command with duplicates user/core', + (config) => { + const command = config.findCommand('foo:bar', {must: true}) + expect(command).to.have.property('id', 'foo:bar') + expect(command).to.have.property('pluginType', 'core') + expect(command).to.have.property('pluginAlias', '@My/pluginb') + }, + ) - testConfig({commandIds: ['foo:bar', 'foo:bar'], types: ['user', 'user']}) - .it('find command with duplicates user/user', config => { - const command = config.findCommand('foo:bar', {must: true}) - expect(command).to.have.property('id', 'foo:bar') - expect(command).to.have.property('pluginType', 'user') - expect(command).to.have.property('pluginAlias', '@My/plugina') - }) + testConfig({commandIds: ['foo:bar', 'foo:bar'], types: ['user', 'user']}).it( + 'find command with duplicates user/user', + (config) => { + const command = config.findCommand('foo:bar', {must: true}) + expect(command).to.have.property('id', 'foo:bar') + expect(command).to.have.property('pluginType', 'user') + expect(command).to.have.property('pluginAlias', '@My/plugina') + }, + ) }) }) diff --git a/test/config/config.test.ts b/test/config/config.test.ts index cd5aa9971..24f5d8571 100644 --- a/test/config/config.test.ts +++ b/test/config/config.test.ts @@ -1,19 +1,20 @@ import {join} from 'node:path' import {Plugin as IPlugin} from '../../src/interfaces' -import * as util from '../../src/util' +import * as os from '../../src/util/os' +import * as fs from '../../src/util/fs' import {expect, fancy} from './test' import {Config, Interfaces} from '../../src' import {Command} from '../../src/command' interface Options { - pjson?: any; - homedir?: string; - platform?: string; - env?: {[k: string]: string}; - commandIds?: string[]; - types?: string[]; + pjson?: any + homedir?: string + platform?: string + env?: {[k: string]: string} + commandIds?: string[] + types?: string[] } const pjson = { @@ -47,18 +48,17 @@ const pjson = { describe('Config', () => { const testConfig = ({pjson, homedir = '/my/home', platform = 'darwin', env = {}}: Options = {}) => { let test = fancy - .resetConfig() - .env(env, {clear: true}) - .stub(util, 'getHomeDir', stub => stub.returns(join(homedir))) - .stub(util, 'getPlatform', stub => stub.returns(platform)) - if (pjson) test = test.stub(util, 'readJson', stub => stub.resolves(pjson)) + .resetConfig() + .env(env, {clear: true}) + .stub(os, 'getHomeDir', (stub) => stub.returns(join(homedir))) + .stub(os, 'getPlatform', (stub) => stub.returns(platform)) + if (pjson) test = test.stub(fs, 'readJson', (stub) => stub.resolves(pjson)) test = test.add('config', () => Config.load()) return { hasS3Key(k: keyof Interfaces.PJSON.S3.Templates, expected: string, extra: any = {}) { - return this - .it(`renders ${k} template as ${expected}`, config => { + return this.it(`renders ${k} template as ${expected}`, (config) => { // Config.load reads the package.json to determine the version and channel // In order to allow prerelease branches to pass, we need to strip the prerelease // tag from the version and switch the channel to stable. @@ -79,13 +79,10 @@ describe('Config', () => { }) }, hasProperty(k: K | undefined, v: Interfaces.Config[K] | undefined) { - return this - .it(`has ${k}=${v}`, config => expect(config).to.have.property(k!, v)) + return this.it(`has ${k}=${v}`, (config) => expect(config).to.have.property(k!, v)) }, it(expectation: string, fn: (config: Interfaces.Config) => any) { - test - .do(({config}) => fn(config)) - .it(expectation) + test.do(({config}) => fn(config)).it(expectation) return this }, } @@ -93,62 +90,61 @@ describe('Config', () => { describe('darwin', () => { testConfig() - .hasProperty('cacheDir', join('/my/home/Library/Caches/@oclif/core')) - .hasProperty('configDir', join('/my/home/.config/@oclif/core')) - .hasProperty('errlog', join('/my/home/Library/Caches/@oclif/core/error.log')) - .hasProperty('dataDir', join('/my/home/.local/share/@oclif/core')) - .hasProperty('home', join('/my/home')) + .hasProperty('cacheDir', join('/my/home/Library/Caches/@oclif/core')) + .hasProperty('configDir', join('/my/home/.config/@oclif/core')) + .hasProperty('errlog', join('/my/home/Library/Caches/@oclif/core/error.log')) + .hasProperty('dataDir', join('/my/home/.local/share/@oclif/core')) + .hasProperty('home', join('/my/home')) }) describe('binAliases', () => { - testConfig({pjson}) - .it('will have binAliases set', config => { + testConfig({pjson}).it('will have binAliases set', (config) => { expect(config.binAliases).to.deep.equal(['bar', 'baz']) }) - testConfig({pjson}).it('will get scoped env vars with bin aliases', config => { + testConfig({pjson}).it('will get scoped env vars with bin aliases', (config) => { expect(config.scopedEnvVarKeys('abc')).to.deep.equal(['FOO_ABC', 'BAR_ABC', 'BAZ_ABC']) }) - testConfig({pjson}).it('will get scoped env vars', config => { + testConfig({pjson}).it('will get scoped env vars', (config) => { expect(config.scopedEnvVarKey('abc')).to.equal('FOO_ABC') }) - testConfig({pjson}).it('will get scopedEnvVar', config => { + testConfig({pjson}).it('will get scopedEnvVar', (config) => { process.env.FOO_ABC = 'find me' expect(config.scopedEnvVar('abc')).to.deep.equal('find me') delete process.env.FOO_ABC }) - testConfig({pjson}).it('will get scopedEnvVar via alias', config => { + testConfig({pjson}).it('will get scopedEnvVar via alias', (config) => { process.env.BAZ_ABC = 'find me' expect(config.scopedEnvVar('abc')).to.deep.equal('find me') delete process.env.BAZ_ABC }) - testConfig({pjson}).it('will get scoped env vars', config => { + testConfig({pjson}).it('will get scoped env vars', (config) => { expect(config.scopedEnvVarKey('abc')).to.equal('FOO_ABC') }) - testConfig({pjson}).it('will get scopedEnvVarTrue', config => { + testConfig({pjson}).it('will get scopedEnvVarTrue', (config) => { process.env.FOO_ABC = 'true' expect(config.scopedEnvVarTrue('abc')).to.equal(true) delete process.env.FOO_ABC }) - testConfig({pjson}).it('will get scopedEnvVarTrue via alias', config => { + testConfig({pjson}).it('will get scopedEnvVarTrue via alias', (config) => { process.env.BAR_ABC = 'true' expect(config.scopedEnvVarTrue('abc')).to.equal(true) delete process.env.BAR_ABC }) - testConfig({pjson}).it('will get scopedEnvVarTrue=1', config => { + testConfig({pjson}).it('will get scopedEnvVarTrue=1', (config) => { process.env.FOO_ABC = '1' expect(config.scopedEnvVarTrue('abc')).to.equal(true) delete process.env.FOO_ABC }) - testConfig({pjson}).it('will get scopedEnvVarTrue=1 via alias', config => { + testConfig({pjson}).it('will get scopedEnvVarTrue=1 via alias', (config) => { process.env.BAR_ABC = '1' expect(config.scopedEnvVarTrue('abc')).to.equal(true) delete process.env.BAR_ABC @@ -157,11 +153,11 @@ describe('Config', () => { describe('linux', () => { testConfig({platform: 'linux'}) - .hasProperty('cacheDir', join('/my/home/.cache/@oclif/core')) - .hasProperty('configDir', join('/my/home/.config/@oclif/core')) - .hasProperty('errlog', join('/my/home/.cache/@oclif/core/error.log')) - .hasProperty('dataDir', join('/my/home/.local/share/@oclif/core')) - .hasProperty('home', join('/my/home')) + .hasProperty('cacheDir', join('/my/home/.cache/@oclif/core')) + .hasProperty('configDir', join('/my/home/.config/@oclif/core')) + .hasProperty('errlog', join('/my/home/.cache/@oclif/core/error.log')) + .hasProperty('dataDir', join('/my/home/.local/share/@oclif/core')) + .hasProperty('home', join('/my/home')) }) describe('win32', () => { @@ -169,34 +165,37 @@ describe('Config', () => { platform: 'win32', env: {LOCALAPPDATA: '/my/home/localappdata'}, }) - .hasProperty('cacheDir', join('/my/home/localappdata/@oclif\\core')) - .hasProperty('configDir', join('/my/home/localappdata/@oclif\\core')) - .hasProperty('errlog', join('/my/home/localappdata/@oclif\\core/error.log')) - .hasProperty('dataDir', join('/my/home/localappdata/@oclif\\core')) - .hasProperty('home', join('/my/home')) + .hasProperty('cacheDir', join('/my/home/localappdata/@oclif\\core')) + .hasProperty('configDir', join('/my/home/localappdata/@oclif\\core')) + .hasProperty('errlog', join('/my/home/localappdata/@oclif\\core/error.log')) + .hasProperty('dataDir', join('/my/home/localappdata/@oclif\\core')) + .hasProperty('home', join('/my/home')) }) describe('s3Key', () => { const target = {platform: 'darwin', arch: 'x64'} const beta = {version: '2.0.0-beta', channel: 'beta'} testConfig() - .hasS3Key('baseDir', 'oclif-cli') - .hasS3Key('manifest', 'version') - .hasS3Key('manifest', 'channels/beta/version', beta) - .hasS3Key('manifest', 'darwin-x64', target) - .hasS3Key('manifest', 'channels/beta/darwin-x64', {...beta, ...target}) - .hasS3Key('unversioned', 'oclif-cli.tar.gz') - .hasS3Key('unversioned', 'oclif-cli.tar.gz') - .hasS3Key('unversioned', 'channels/beta/oclif-cli.tar.gz', beta) - .hasS3Key('unversioned', 'channels/beta/oclif-cli.tar.gz', beta) - .hasS3Key('unversioned', 'oclif-cli-darwin-x64.tar.gz', target) - .hasS3Key('unversioned', 'oclif-cli-darwin-x64.tar.gz', target) - .hasS3Key('unversioned', 'channels/beta/oclif-cli-darwin-x64.tar.gz', {...beta, ...target}) - .hasS3Key('unversioned', 'channels/beta/oclif-cli-darwin-x64.tar.gz', {...beta, ...target}) - .hasS3Key('versioned', 'oclif-cli-v1.0.0/oclif-cli-v1.0.0.tar.gz') - .hasS3Key('versioned', 'oclif-cli-v1.0.0/oclif-cli-v1.0.0-darwin-x64.tar.gz', target) - .hasS3Key('versioned', 'channels/beta/oclif-cli-v2.0.0-beta/oclif-cli-v2.0.0-beta.tar.gz', beta) - .hasS3Key('versioned', 'channels/beta/oclif-cli-v2.0.0-beta/oclif-cli-v2.0.0-beta-darwin-x64.tar.gz', {...beta, ...target}) + .hasS3Key('baseDir', 'oclif-cli') + .hasS3Key('manifest', 'version') + .hasS3Key('manifest', 'channels/beta/version', beta) + .hasS3Key('manifest', 'darwin-x64', target) + .hasS3Key('manifest', 'channels/beta/darwin-x64', {...beta, ...target}) + .hasS3Key('unversioned', 'oclif-cli.tar.gz') + .hasS3Key('unversioned', 'oclif-cli.tar.gz') + .hasS3Key('unversioned', 'channels/beta/oclif-cli.tar.gz', beta) + .hasS3Key('unversioned', 'channels/beta/oclif-cli.tar.gz', beta) + .hasS3Key('unversioned', 'oclif-cli-darwin-x64.tar.gz', target) + .hasS3Key('unversioned', 'oclif-cli-darwin-x64.tar.gz', target) + .hasS3Key('unversioned', 'channels/beta/oclif-cli-darwin-x64.tar.gz', {...beta, ...target}) + .hasS3Key('unversioned', 'channels/beta/oclif-cli-darwin-x64.tar.gz', {...beta, ...target}) + .hasS3Key('versioned', 'oclif-cli-v1.0.0/oclif-cli-v1.0.0.tar.gz') + .hasS3Key('versioned', 'oclif-cli-v1.0.0/oclif-cli-v1.0.0-darwin-x64.tar.gz', target) + .hasS3Key('versioned', 'channels/beta/oclif-cli-v2.0.0-beta/oclif-cli-v2.0.0-beta.tar.gz', beta) + .hasS3Key('versioned', 'channels/beta/oclif-cli-v2.0.0-beta/oclif-cli-v2.0.0-beta-darwin-x64.tar.gz', { + ...beta, + ...target, + }) }) describe('options', () => { @@ -208,8 +207,7 @@ describe('Config', () => { }) }) - testConfig() - .it('has s3Url', config => { + testConfig().it('has s3Url', (config) => { const orig = config.pjson.oclif.update.s3.host config.pjson.oclif.update.s3.host = 'https://bar.com/a/' expect(config.s3Url('/b/c')).to.equal('https://bar.com/a/b/c') @@ -218,13 +216,13 @@ describe('Config', () => { testConfig({ pjson, - }) - .it('has subtopics', config => { - expect(config.topics.map(t => t.name)).to.have.members(['t1', 't1:t1-1', 't1:t1-1:t1-1-1', 't1:t1-1:t1-1-2']) + }).it('has subtopics', (config) => { + expect(config.topics.map((t) => t.name)).to.have.members(['t1', 't1:t1-1', 't1:t1-1:t1-1-1', 't1:t1-1:t1-1-2']) }) describe('findCommand', () => { - const findCommandTestConfig = ({pjson, + const findCommandTestConfig = ({ + pjson, homedir = '/my/home', platform = 'darwin', env = {}, @@ -250,7 +248,12 @@ describe('Config', () => { const commandPluginA: Command.Loadable = { strict: false, - aliases: [], args: {}, flags: {}, hidden: false, id: commandIds[0], async load(): Promise { + aliases: [], + args: {}, + flags: {}, + hidden: false, + id: commandIds[0], + async load(): Promise { return MyCommandClass }, pluginType: types[0] ?? 'core', @@ -258,14 +261,20 @@ describe('Config', () => { } const commandPluginB: Command.Loadable = { strict: false, - aliases: [], args: {}, flags: {}, hidden: false, id: commandIds[1], async load(): Promise { + aliases: [], + args: {}, + flags: {}, + hidden: false, + id: commandIds[1], + async load(): Promise { return MyCommandClass }, pluginType: types[1] ?? 'core', pluginAlias: '@My/pluginb', } const hooks = {} - const pluginA: IPlugin = {load, + const pluginA: IPlugin = { + load, findCommand, name: '@My/plugina', alias: '@My/plugina', @@ -282,6 +291,7 @@ describe('Config', () => { tag: 'tag', moduleType: 'commonjs', hasManifest: false, + isRoot: false, } const pluginB: IPlugin = { @@ -302,15 +312,16 @@ describe('Config', () => { tag: 'tag', moduleType: 'commonjs', hasManifest: false, + isRoot: false, } const plugins = new Map().set(pluginA.name, pluginA).set(pluginB.name, pluginB) let test = fancy - .resetConfig() - .env(env, {clear: true}) - .stub(util, 'getHomeDir', stub => stub.returns(join(homedir))) - .stub(util, 'getPlatform', stub => stub.returns(platform)) + .resetConfig() + .env(env, {clear: true}) + .stub(os, 'getHomeDir', (stub) => stub.returns(join(homedir))) + .stub(os, 'getPlatform', (stub) => stub.returns(platform)) - if (pjson) test = test.stub(util, 'readJson', stub => stub.resolves(pjson)) + if (pjson) test = test.stub(fs, 'readJson', (stub) => stub.resolves(pjson)) test = test.add('config', async () => { const config = await Config.load() config.plugins = plugins @@ -328,58 +339,61 @@ describe('Config', () => { // @ts-ignore return { it(expectation: string, fn: (config: Interfaces.Config) => any) { - test - .do(({config}) => fn(config)) - .it(expectation) + test.do(({config}) => fn(config)).it(expectation) return this }, } } - findCommandTestConfig() - .it('find command with no duplicates', config => { + findCommandTestConfig().it('find command with no duplicates', (config) => { const command = config.findCommand('foo:bar', {must: true}) expect(command).to.have.property('pluginAlias', '@My/plugina') }) - findCommandTestConfig({commandIds: ['foo:bar', 'foo:bar']}) - .it('find command with duplicates and choose the one that appears first in oclif.plugins', config => { - const command = config.findCommand('foo:bar', {must: true}) - expect(command).to.have.property('pluginAlias', '@My/pluginb') - }) - findCommandTestConfig({types: ['core', 'user']}) - .it('find command with no duplicates core/user', config => { - const command = config.findCommand('foo:bar', {must: true}) - expect(command).to.have.property('id', 'foo:bar') - expect(command).to.have.property('pluginType', 'core') - expect(command).to.have.property('pluginAlias', '@My/plugina') - }) - findCommandTestConfig({types: ['user', 'core']}) - .it('find command with no duplicates user/core', config => { - const command = config.findCommand('foo:bar', {must: true}) - expect(command).to.have.property('id', 'foo:bar') - expect(command).to.have.property('pluginType', 'user') - expect(command).to.have.property('pluginAlias', '@My/plugina') - }) - findCommandTestConfig({commandIds: ['foo:bar', 'foo:bar'], types: ['core', 'user']}) - .it('find command with duplicates core/user', config => { + findCommandTestConfig({commandIds: ['foo:bar', 'foo:bar']}).it( + 'find command with duplicates and choose the one that appears first in oclif.plugins', + (config) => { + const command = config.findCommand('foo:bar', {must: true}) + expect(command).to.have.property('pluginAlias', '@My/pluginb') + }, + ) + findCommandTestConfig({types: ['core', 'user']}).it('find command with no duplicates core/user', (config) => { const command = config.findCommand('foo:bar', {must: true}) expect(command).to.have.property('id', 'foo:bar') expect(command).to.have.property('pluginType', 'core') expect(command).to.have.property('pluginAlias', '@My/plugina') }) - findCommandTestConfig({commandIds: ['foo:bar', 'foo:bar'], types: ['user', 'core']}) - .it('find command with duplicates user/core', config => { - const command = config.findCommand('foo:bar', {must: true}) - expect(command).to.have.property('id', 'foo:bar') - expect(command).to.have.property('pluginType', 'core') - expect(command).to.have.property('pluginAlias', '@My/pluginb') - }) - findCommandTestConfig({commandIds: ['foo:bar', 'foo:bar'], types: ['user', 'user']}) - .it('find command with duplicates user/user', config => { + findCommandTestConfig({types: ['user', 'core']}).it('find command with no duplicates user/core', (config) => { const command = config.findCommand('foo:bar', {must: true}) expect(command).to.have.property('id', 'foo:bar') expect(command).to.have.property('pluginType', 'user') expect(command).to.have.property('pluginAlias', '@My/plugina') }) + findCommandTestConfig({commandIds: ['foo:bar', 'foo:bar'], types: ['core', 'user']}).it( + 'find command with duplicates core/user', + (config) => { + const command = config.findCommand('foo:bar', {must: true}) + expect(command).to.have.property('id', 'foo:bar') + expect(command).to.have.property('pluginType', 'core') + expect(command).to.have.property('pluginAlias', '@My/plugina') + }, + ) + findCommandTestConfig({commandIds: ['foo:bar', 'foo:bar'], types: ['user', 'core']}).it( + 'find command with duplicates user/core', + (config) => { + const command = config.findCommand('foo:bar', {must: true}) + expect(command).to.have.property('id', 'foo:bar') + expect(command).to.have.property('pluginType', 'core') + expect(command).to.have.property('pluginAlias', '@My/pluginb') + }, + ) + findCommandTestConfig({commandIds: ['foo:bar', 'foo:bar'], types: ['user', 'user']}).it( + 'find command with duplicates user/user', + (config) => { + const command = config.findCommand('foo:bar', {must: true}) + expect(command).to.have.property('id', 'foo:bar') + expect(command).to.have.property('pluginType', 'user') + expect(command).to.have.property('pluginAlias', '@My/plugina') + }, + ) }) }) diff --git a/test/config/esm.test.ts b/test/config/esm.test.ts index 2d3b1a0d2..54dc4978d 100644 --- a/test/config/esm.test.ts +++ b/test/config/esm.test.ts @@ -11,27 +11,21 @@ const p = (p: string) => join(root, p) // This tests file URL / import.meta.url simulation. const rootAsFileURL = url.pathToFileURL(root).toString() -const withConfig = fancy -.add('config', () => Config.load(rootAsFileURL)) +const withConfig = fancy.add('config', () => Config.load(rootAsFileURL)) describe('esm', () => { - withConfig - .it('has commandsDir', ({config}) => { + withConfig.it('has commandsDir', ({config}) => { expect([...config.plugins.values()][0]).to.deep.include({ commandsDir: p('src/commands'), }) }) - withConfig - .stdout() - .it('runs esm command and prerun & postrun hooks', async ctx => { + withConfig.stdout().it('runs esm command and prerun & postrun hooks', async (ctx) => { await ctx.config.runCommand('foo:bar:baz') expect(ctx.stdout).to.equal('running esm prerun hook\nit works!\nrunning esm postrun hook\n') }) - withConfig - .stdout() - .it('runs faulty command, only prerun hook triggers', async ctx => { + withConfig.stdout().it('runs faulty command, only prerun hook triggers', async (ctx) => { try { await ctx.config.runCommand('foo:bar:fail') } catch { @@ -41,16 +35,12 @@ describe('esm', () => { expect(ctx.stdout).to.equal('running esm prerun hook\nit fails!\ncaught error\n') }) - withConfig - .stdout() - .it('runs esm command, postrun hook captures command result', async ctx => { + withConfig.stdout().it('runs esm command, postrun hook captures command result', async (ctx) => { await ctx.config.runCommand('foo:bar:test-result') expect(ctx.stdout).to.equal('running esm prerun hook\nit works!\nrunning esm postrun hook\nreturned success!\n') }) - withConfig - .stdout() - .it('runs init hook', async ctx => { + withConfig.stdout().it('runs init hook', async (ctx) => { await (ctx.config.runHook as any)('init', {id: 'myid', argv: ['foo']}) expect(ctx.stdout).to.equal('running esm init hook\n') }) diff --git a/test/config/fixtures/help/package.json b/test/config/fixtures/help/package.json index 832fdfa34..a19b46139 100644 --- a/test/config/fixtures/help/package.json +++ b/test/config/fixtures/help/package.json @@ -6,7 +6,14 @@ "files": [], "oclif": { "commands": "./src/commands", - "additionalHelpFlags": ["-h", "--mycommandhelp"], - "additionalVersionFlags": ["-v", "myversion", "version"] + "additionalHelpFlags": [ + "-h", + "--mycommandhelp" + ], + "additionalVersionFlags": [ + "-v", + "myversion", + "version" + ] } } diff --git a/test/config/fixtures/mixed-esm-cjs/src/commands/foo/bar/test-result.js b/test/config/fixtures/mixed-esm-cjs/src/commands/foo/bar/test-result.js index 755ba23aa..5b93fd61e 100644 --- a/test/config/fixtures/mixed-esm-cjs/src/commands/foo/bar/test-result.js +++ b/test/config/fixtures/mixed-esm-cjs/src/commands/foo/bar/test-result.js @@ -4,4 +4,3 @@ export default class Command { return 'returned success!' } } - diff --git a/test/config/fixtures/typescript/src/hooks/postrun.ts b/test/config/fixtures/typescript/src/hooks/postrun.ts index f78977809..98a0d304a 100644 --- a/test/config/fixtures/typescript/src/hooks/postrun.ts +++ b/test/config/fixtures/typescript/src/hooks/postrun.ts @@ -1,4 +1,3 @@ - export default function postrun(options: any): void { console.log('running ts postrun hook') if (options.Command.id === 'foo:bar:test-result') { diff --git a/test/config/fixtures/typescript/tsconfig.json b/test/config/fixtures/typescript/tsconfig.json index 75c61de88..8d0083147 100644 --- a/test/config/fixtures/typescript/tsconfig.json +++ b/test/config/fixtures/typescript/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "outDir": "./lib", - "rootDirs": [ - "./src" - ] + "rootDirs": ["./src"] }, - "include": [ - "./src/**/*" - ] + "include": ["./src/**/*"] } diff --git a/test/config/help.config.test.ts b/test/config/help.config.test.ts index 3742c9455..71cf27819 100644 --- a/test/config/help.config.test.ts +++ b/test/config/help.config.test.ts @@ -13,16 +13,14 @@ const root = resolve(__dirname, 'fixtures/help') // This tests file URL / import.meta.url simulation. const rootAsFileURL = pathToFileURL(root).toString() -const withConfig = fancy -.add('config', () => Config.load(rootAsFileURL)) +const withConfig = fancy.add('config', () => Config.load(rootAsFileURL)) describe('help and version flag additions', () => { - withConfig - .it('has help and version additions', ({config}) => { + withConfig.it('has help and version additions', ({config}) => { expect(config.pjson.oclif.additionalHelpFlags).to.have.lengthOf(2) expect(config.pjson.oclif.additionalVersionFlags).to.have.lengthOf(3) const mergedHelpFlags = getHelpFlagAdditions(config) - expect(mergedHelpFlags).to.deep.equal(['--help', ...config.pjson.oclif.additionalHelpFlags as string[]]) + expect(mergedHelpFlags).to.deep.equal(['--help', ...(config.pjson.oclif.additionalHelpFlags as string[])]) expect(helpAddition(['-h'], config)).to.be.true expect(helpAddition(['help'], config)).to.be.false expect(helpAddition(['--mycommandhelp'], config)).to.be.true @@ -34,18 +32,18 @@ describe('help and version flag additions', () => { }) withConfig - .do(({config}) => delete config.pjson.oclif.additionalHelpFlags) - .it('has version additions', ({config}) => { - expect(config.pjson.oclif.additionalHelpFlags).to.not.be.ok - expect(config.pjson.oclif.additionalVersionFlags).to.have.lengthOf(3) - const mergedHelpFlags = getHelpFlagAdditions(config) - expect(mergedHelpFlags).to.deep.equal(['--help']) - expect(helpAddition(['-h'], config)).to.be.false - expect(helpAddition(['help'], config)).to.be.false - expect(helpAddition(['mycommandhelp'], config)).to.be.false - expect(versionAddition(['-v'], config)).to.be.true - expect(versionAddition(['version'], config)).to.be.true - expect(versionAddition(['myversion'], config)).to.be.true - expect(versionAddition(['notmyversion'], config)).to.be.false - }) + .do(({config}) => delete config.pjson.oclif.additionalHelpFlags) + .it('has version additions', ({config}) => { + expect(config.pjson.oclif.additionalHelpFlags).to.not.be.ok + expect(config.pjson.oclif.additionalVersionFlags).to.have.lengthOf(3) + const mergedHelpFlags = getHelpFlagAdditions(config) + expect(mergedHelpFlags).to.deep.equal(['--help']) + expect(helpAddition(['-h'], config)).to.be.false + expect(helpAddition(['help'], config)).to.be.false + expect(helpAddition(['mycommandhelp'], config)).to.be.false + expect(versionAddition(['-v'], config)).to.be.true + expect(versionAddition(['version'], config)).to.be.true + expect(versionAddition(['myversion'], config)).to.be.true + expect(versionAddition(['notmyversion'], config)).to.be.false + }) }) diff --git a/test/config/mixed-cjs-esm.test.ts b/test/config/mixed-cjs-esm.test.ts index 411275fde..b8f65f8fb 100644 --- a/test/config/mixed-cjs-esm.test.ts +++ b/test/config/mixed-cjs-esm.test.ts @@ -7,27 +7,21 @@ import {expect, fancy} from './test' const root = resolve(__dirname, 'fixtures/mixed-cjs-esm') const p = (p: string) => join(root, p) -const withConfig = fancy -.add('config', () => Config.load(root)) +const withConfig = fancy.add('config', () => Config.load(root)) describe('mixed-cjs-esm', () => { - withConfig - .it('has commandsDir', ({config}) => { + withConfig.it('has commandsDir', ({config}) => { expect([...config.plugins.values()][0]).to.deep.include({ commandsDir: p('src/commands'), }) }) - withConfig - .stdout() - .it('runs mixed-cjs-esm command and prerun & postrun hooks', async ctx => { + withConfig.stdout().it('runs mixed-cjs-esm command and prerun & postrun hooks', async (ctx) => { await ctx.config.runCommand('foo:bar:baz') expect(ctx.stdout).to.equal('running mixed-cjs-esm prerun hook\nit works!\nrunning mixed-cjs-esm postrun hook\n') }) - withConfig - .stdout() - .it('runs faulty command, only prerun hook triggers', async ctx => { + withConfig.stdout().it('runs faulty command, only prerun hook triggers', async (ctx) => { try { await ctx.config.runCommand('foo:bar:fail') } catch { @@ -37,16 +31,14 @@ describe('mixed-cjs-esm', () => { expect(ctx.stdout).to.equal('running mixed-cjs-esm prerun hook\nit fails!\ncaught error\n') }) - withConfig - .stdout() - .it('runs mixed-cjs-esm command, postrun hook captures command result', async ctx => { + withConfig.stdout().it('runs mixed-cjs-esm command, postrun hook captures command result', async (ctx) => { await ctx.config.runCommand('foo:bar:test-result') - expect(ctx.stdout).to.equal('running mixed-cjs-esm prerun hook\nit works!\nrunning mixed-cjs-esm postrun hook\nreturned success!\n') + expect(ctx.stdout).to.equal( + 'running mixed-cjs-esm prerun hook\nit works!\nrunning mixed-cjs-esm postrun hook\nreturned success!\n', + ) }) - withConfig - .stdout() - .it('runs init hook', async ctx => { + withConfig.stdout().it('runs init hook', async (ctx) => { await (ctx.config.runHook as any)('init', {id: 'myid', argv: ['foo']}) expect(ctx.stdout).to.equal('running mixed-cjs-esm init hook\n') }) diff --git a/test/config/mixed-esm-cjs.test.ts b/test/config/mixed-esm-cjs.test.ts index 4d6ed3703..cceb363bf 100644 --- a/test/config/mixed-esm-cjs.test.ts +++ b/test/config/mixed-esm-cjs.test.ts @@ -7,27 +7,21 @@ import {expect, fancy} from './test' const root = resolve(__dirname, 'fixtures/mixed-esm-cjs') const p = (p: string) => join(root, p) -const withConfig = fancy -.add('config', () => Config.load(root)) +const withConfig = fancy.add('config', () => Config.load(root)) describe('mixed-cjs-esm', () => { - withConfig - .it('has commandsDir', ({config}) => { + withConfig.it('has commandsDir', ({config}) => { expect([...config.plugins.values()][0]).to.deep.include({ commandsDir: p('src/commands'), }) }) - withConfig - .stdout() - .it('runs mixed-esm-cjs command and prerun & postrun hooks', async ctx => { + withConfig.stdout().it('runs mixed-esm-cjs command and prerun & postrun hooks', async (ctx) => { await ctx.config.runCommand('foo:bar:baz') expect(ctx.stdout).to.equal('running mixed-esm-cjs prerun hook\nit works!\nrunning mixed-esm-cjs postrun hook\n') }) - withConfig - .stdout() - .it('runs faulty command, only prerun hook triggers', async ctx => { + withConfig.stdout().it('runs faulty command, only prerun hook triggers', async (ctx) => { try { await ctx.config.runCommand('foo:bar:fail') } catch { @@ -37,16 +31,14 @@ describe('mixed-cjs-esm', () => { expect(ctx.stdout).to.equal('running mixed-esm-cjs prerun hook\nit fails!\ncaught error\n') }) - withConfig - .stdout() - .it('runs mixed-esm-cjs command, postrun hook captures command result', async ctx => { + withConfig.stdout().it('runs mixed-esm-cjs command, postrun hook captures command result', async (ctx) => { await ctx.config.runCommand('foo:bar:test-result') - expect(ctx.stdout).to.equal('running mixed-esm-cjs prerun hook\nit works!\nrunning mixed-esm-cjs postrun hook\nreturned success!\n') + expect(ctx.stdout).to.equal( + 'running mixed-esm-cjs prerun hook\nit works!\nrunning mixed-esm-cjs postrun hook\nreturned success!\n', + ) }) - withConfig - .stdout() - .it('runs init hook', async ctx => { + withConfig.stdout().it('runs init hook', async (ctx) => { await (ctx.config.runHook as any)('init', {id: 'myid', argv: ['foo']}) expect(ctx.stdout).to.equal('running mixed-esm-cjs init hook\n') }) diff --git a/test/config/test.ts b/test/config/test.ts index 1e7866017..fe843c7dd 100644 --- a/test/config/test.ts +++ b/test/config/test.ts @@ -2,8 +2,7 @@ import {fancy as base} from 'fancy-test' import {Interfaces} from '../../src' -export const fancy = base -.register('resetConfig', () => ({ +export const fancy = base.register('resetConfig', () => ({ run(ctx: {config: Interfaces.Config}) { // @ts-ignore delete ctx.config diff --git a/test/config/ts-node.test.ts b/test/config/ts-node.test.ts index 6742238e4..111794bac 100644 --- a/test/config/ts-node.test.ts +++ b/test/config/ts-node.test.ts @@ -5,7 +5,7 @@ import {SinonSandbox, createSandbox} from 'sinon' import {Interfaces, settings} from '../../src' import * as configTsNode from '../../src/config/ts-node' -import * as util from '../../src/util' +import * as util from '../../src/util/fs' import {expect} from 'chai' const root = resolve(__dirname, 'fixtures/typescript') diff --git a/test/config/typescript.test.ts b/test/config/typescript.test.ts index 27d20c5cb..bfd8c85bd 100644 --- a/test/config/typescript.test.ts +++ b/test/config/typescript.test.ts @@ -7,27 +7,21 @@ import {expect, fancy} from './test' const root = resolve(__dirname, 'fixtures/typescript') const p = (p: string) => join(root, p) -const withConfig = fancy -.add('config', () => Config.load(root)) +const withConfig = fancy.add('config', () => Config.load(root)) describe('typescript', () => { - withConfig - .it('has commandsDir', ({config}) => { + withConfig.it('has commandsDir', ({config}) => { expect([...config.plugins.values()][0]).to.deep.include({ commandsDir: p('src/commands'), }) }) - withConfig - .stdout() - .it('runs ts command and prerun & postrun hooks', async ctx => { + withConfig.stdout().it('runs ts command and prerun & postrun hooks', async (ctx) => { await ctx.config.runCommand('foo:bar:baz') expect(ctx.stdout).to.equal('running ts prerun hook\nit works!\nrunning ts postrun hook\n') }) - withConfig - .stdout() - .it('runs faulty command, only prerun hook triggers', async ctx => { + withConfig.stdout().it('runs faulty command, only prerun hook triggers', async (ctx) => { try { await ctx.config.runCommand('foo:bar:fail') } catch { @@ -37,16 +31,12 @@ describe('typescript', () => { expect(ctx.stdout).to.equal('running ts prerun hook\nit fails!\ncaught error\n') }) - withConfig - .stdout() - .it('runs ts command, postrun hook captures command result', async ctx => { + withConfig.stdout().it('runs ts command, postrun hook captures command result', async (ctx) => { await ctx.config.runCommand('foo:bar:test-result') expect(ctx.stdout).to.equal('running ts prerun hook\nit works!\nrunning ts postrun hook\nreturned success!\n') }) - withConfig - .stdout() - .it('runs init hook', async ctx => { + withConfig.stdout().it('runs init hook', async (ctx) => { // to-do: fix union types await (ctx.config.runHook as any)('init', {id: 'myid', argv: ['foo']}) expect(ctx.stdout).to.equal('running ts init hook\n') diff --git a/test/config/util.test.ts b/test/config/util.test.ts index 6fad20e6a..3f05fcffe 100644 --- a/test/config/util.test.ts +++ b/test/config/util.test.ts @@ -3,23 +3,14 @@ import {collectUsableIds, getCommandIdPermutations} from '../../src/config/util' describe('util', () => { describe('collectUsableIds', () => { - test - .it('returns all usable command ids', async () => { + test.it('returns all usable command ids', async () => { const ids = collectUsableIds(['foo:bar:baz', 'one:two:three']) - expect(ids).to.deep.equal(new Set([ - 'foo', - 'foo:bar', - 'foo:bar:baz', - 'one', - 'one:two', - 'one:two:three', - ])) + expect(ids).to.deep.equal(new Set(['foo', 'foo:bar', 'foo:bar:baz', 'one', 'one:two', 'one:two:three'])) }) }) describe('getCommandIdPermutations', () => { - test - .it('returns all usable command ids', async () => { + test.it('returns all usable command ids', async () => { const permutations = getCommandIdPermutations('foo:bar:baz') expect(permutations).to.deep.equal([ 'foo:bar:baz', @@ -35,22 +26,30 @@ describe('util', () => { const numberOfPermutations = (commandID: string): number => { const num = commandID.split(':').length let result = 1 - for (let i = 2; i <= num; i++) - result *= i + for (let i = 2; i <= num; i++) result *= i return result } - test - .it('returns the correct number of permutations', async () => { + test.it('returns the correct number of permutations', async () => { expect(getCommandIdPermutations('one').length).to.equal(numberOfPermutations('one')) expect(getCommandIdPermutations('one:two').length).to.equal(numberOfPermutations('one:two')) expect(getCommandIdPermutations('one:two:three').length).to.equal(numberOfPermutations('one:two:three')) expect(getCommandIdPermutations('one:two:three:four').length).to.equal(numberOfPermutations('one:two:three:four')) - expect(getCommandIdPermutations('one:two:three:four:five').length).to.equal(numberOfPermutations('one:two:three:four:five')) - expect(getCommandIdPermutations('one:two:three:four:five:six').length).to.equal(numberOfPermutations('one:two:three:four:five:six')) - expect(getCommandIdPermutations('one:two:three:four:five:six:seven').length).to.equal(numberOfPermutations('one:two:three:four:five:six:seven')) - expect(getCommandIdPermutations('one:two:three:four:five:six:seven:eight').length).to.equal(numberOfPermutations('one:two:three:four:five:six:seven:eight')) - expect(getCommandIdPermutations('one:two:three:four:five:six:seven:eight:nine').length).to.equal(numberOfPermutations('one:two:three:four:five:six:seven:eight:nine')) + expect(getCommandIdPermutations('one:two:three:four:five').length).to.equal( + numberOfPermutations('one:two:three:four:five'), + ) + expect(getCommandIdPermutations('one:two:three:four:five:six').length).to.equal( + numberOfPermutations('one:two:three:four:five:six'), + ) + expect(getCommandIdPermutations('one:two:three:four:five:six:seven').length).to.equal( + numberOfPermutations('one:two:three:four:five:six:seven'), + ) + expect(getCommandIdPermutations('one:two:three:four:five:six:seven:eight').length).to.equal( + numberOfPermutations('one:two:three:four:five:six:seven:eight'), + ) + expect(getCommandIdPermutations('one:two:three:four:five:six:seven:eight:nine').length).to.equal( + numberOfPermutations('one:two:three:four:five:six:seven:eight:nine'), + ) }) }) }) diff --git a/test/errors/error.test.ts b/test/errors/error.test.ts index 15ab20a52..534b2d98a 100644 --- a/test/errors/error.test.ts +++ b/test/errors/error.test.ts @@ -4,98 +4,102 @@ import {PrettyPrintableError} from '../../src/interfaces/errors' describe('error', () => { fancy - .do(() => { - error('An error happened!') - }) - .catch((error: PrettyPrintableError) => { - expect(error.message).to.equal('An error happened!') - }) - .it('throws an error using a string argument') - - fancy - .do(() => { - error('An error happened!', {code: 'ERR', ref: 'https://oclif.com/error', suggestions: ['rm -rf node_modules']}) - }) - .catch((error: PrettyPrintableError) => { - expect(error.message).to.equal('An error happened!') - expect(error.code).to.equal('ERR') - expect(error.ref).to.equal('https://oclif.com/error') - expect(error.suggestions).to.deep.equal(['rm -rf node_modules']) - }) - .it('attaches pretty print properties to a new error from options') + .do(() => { + error('An error happened!') + }) + .catch((error: PrettyPrintableError) => { + expect(error.message).to.equal('An error happened!') + }) + .it('throws an error using a string argument') fancy - .do(() => { - error(new Error('An existing error object error!'), {code: 'ERR', ref: 'https://oclif.com/error', suggestions: ['rm -rf node_modules']}) - }) - .catch((error: PrettyPrintableError) => { - expect(error.message).to.equal('An existing error object error!') - expect(error.code).to.equal('ERR') - expect(error.ref).to.equal('https://oclif.com/error') - expect(error.suggestions).to.deep.equal(['rm -rf node_modules']) - }) - .it('attached pretty print properties from options to an existing error object') + .do(() => { + error('An error happened!', {code: 'ERR', ref: 'https://oclif.com/error', suggestions: ['rm -rf node_modules']}) + }) + .catch((error: PrettyPrintableError) => { + expect(error.message).to.equal('An error happened!') + expect(error.code).to.equal('ERR') + expect(error.ref).to.equal('https://oclif.com/error') + expect(error.suggestions).to.deep.equal(['rm -rf node_modules']) + }) + .it('attaches pretty print properties to a new error from options') fancy - .do(() => { - const e: any = new Error('An existing error object error!') - e.code = 'ORIG_ERR' - e.ref = 'ORIG_REF' - e.suggestions = ['ORIG_SUGGESTION'] - error(e, {code: 'ERR', ref: 'https://oclif.com/error', suggestions: ['rm -rf node_modules']}) - }) - .catch((error: PrettyPrintableError) => { - expect(error.code).to.equal('ORIG_ERR') - expect(error.ref).to.equal('ORIG_REF') - expect(error.suggestions).to.deep.equal(['ORIG_SUGGESTION']) - }) - .it('preserves original pretty printable properties and is not overwritten by options') + .do(() => { + error(new Error('An existing error object error!'), { + code: 'ERR', + ref: 'https://oclif.com/error', + suggestions: ['rm -rf node_modules'], + }) + }) + .catch((error: PrettyPrintableError) => { + expect(error.message).to.equal('An existing error object error!') + expect(error.code).to.equal('ERR') + expect(error.ref).to.equal('https://oclif.com/error') + expect(error.suggestions).to.deep.equal(['rm -rf node_modules']) + }) + .it('attached pretty print properties from options to an existing error object') fancy - .stdout() - .stderr() - .do(() => { - error('an error is reported but is not rethrown', {exit: false}) - }) - // there is no .catch here because the error is not rethrown - // however it should be outputted - .it('does not rethrow error when exit: false option is set', ctx => { - expect(ctx.stderr).to.contain('Error: an error is reported but is not rethrown') - expect(ctx.stdout).to.equal('') - }) - - describe('applying oclif errors', () => { - fancy .do(() => { - error(new Error('An existing error object error!')) + const e: any = new Error('An existing error object error!') + e.code = 'ORIG_ERR' + e.ref = 'ORIG_REF' + e.suggestions = ['ORIG_SUGGESTION'] + error(e, {code: 'ERR', ref: 'https://oclif.com/error', suggestions: ['rm -rf node_modules']}) }) - .catch((error: any) => { - const defaultErrorCode = 2 - expect(error.oclif.exit).to.equal(defaultErrorCode) + .catch((error: PrettyPrintableError) => { + expect(error.code).to.equal('ORIG_ERR') + expect(error.ref).to.equal('ORIG_REF') + expect(error.suggestions).to.deep.equal(['ORIG_SUGGESTION']) }) - .it('adds oclif exit code to errors by default') + .it('preserves original pretty printable properties and is not overwritten by options') - fancy + fancy + .stdout() + .stderr() .do(() => { - error(new Error('An existing error object error!'), {exit: 9001}) + error('an error is reported but is not rethrown', {exit: false}) }) - .catch((error: any) => { - expect(error.oclif.exit).to.equal(9001) + // there is no .catch here because the error is not rethrown + // however it should be outputted + .it('does not rethrow error when exit: false option is set', (ctx) => { + expect(ctx.stderr).to.contain('Error: an error is reported but is not rethrown') + expect(ctx.stdout).to.equal('') }) - .it('applies the exit property on options to the error object') + describe('applying oclif errors', () => { fancy - .do(() => { - const e: any = new Error('An existing error object error!') - e.oclif = { - code: 'ORIG_EXIT_CODE', - } + .do(() => { + error(new Error('An existing error object error!')) + }) + .catch((error: any) => { + const defaultErrorCode = 2 + expect(error.oclif.exit).to.equal(defaultErrorCode) + }) + .it('adds oclif exit code to errors by default') - error(e) - }) - .catch((error: any) => { - expect(error.oclif.code).to.equal('ORIG_EXIT_CODE') - }) - .it('preserves original oclif exitable error properties and is not overwritten by options') + fancy + .do(() => { + error(new Error('An existing error object error!'), {exit: 9001}) + }) + .catch((error: any) => { + expect(error.oclif.exit).to.equal(9001) + }) + .it('applies the exit property on options to the error object') + + fancy + .do(() => { + const e: any = new Error('An existing error object error!') + e.oclif = { + code: 'ORIG_EXIT_CODE', + } + + error(e) + }) + .catch((error: any) => { + expect(error.oclif.code).to.equal('ORIG_EXIT_CODE') + }) + .it('preserves original oclif exitable error properties and is not overwritten by options') }) }) diff --git a/test/errors/handle.test.ts b/test/errors/handle.test.ts index 9582cde10..df89a323f 100644 --- a/test/errors/handle.test.ts +++ b/test/errors/handle.test.ts @@ -24,111 +24,111 @@ describe('handle', () => { }) fancy - .stdout() - .stderr() - .it('hides an exit error', async ctx => { - await handle(new ExitError(0)) - expect(ctx.stdout).to.equal('') - expect(ctx.stderr).to.equal('') - expect(exitStub.firstCall.firstArg).to.equal(0) - }) + .stdout() + .stderr() + .it('hides an exit error', async (ctx) => { + await handle(new ExitError(0)) + expect(ctx.stdout).to.equal('') + expect(ctx.stderr).to.equal('') + expect(exitStub.firstCall.firstArg).to.equal(0) + }) fancy - .stdout() - .stderr() - .it('prints error', async ctx => { - const error = new Error('foo bar baz') as Error & {skipOclifErrorHandling: boolean} - error.skipOclifErrorHandling = false - await handle(error) - expect(ctx.stdout).to.equal('') - expect(ctx.stderr).to.include('foo bar baz') - }) + .stdout() + .stderr() + .it('prints error', async (ctx) => { + const error = new Error('foo bar baz') as Error & {skipOclifErrorHandling: boolean} + error.skipOclifErrorHandling = false + await handle(error) + expect(ctx.stdout).to.equal('') + expect(ctx.stderr).to.include('foo bar baz') + }) fancy - .stdout() - .stderr() - .it('should not print error when skipOclifErrorHandling is true', async ctx => { - const error = new Error('foo bar baz') as Error & {skipOclifErrorHandling: boolean} - error.skipOclifErrorHandling = true - await handle(error) - expect(ctx.stdout).to.equal('') - expect(ctx.stderr).to.equal('') - }) + .stdout() + .stderr() + .it('should not print error when skipOclifErrorHandling is true', async (ctx) => { + const error = new Error('foo bar baz') as Error & {skipOclifErrorHandling: boolean} + error.skipOclifErrorHandling = true + await handle(error) + expect(ctx.stdout).to.equal('') + expect(ctx.stderr).to.equal('') + }) fancy - .stderr() - .do(() => { - config.errlog = errlog - }) - .finally(() => { - config.errlog = undefined - }) - .it('logs when errlog is set', async ctx => { - await handle(new CLIError('uh oh!')) - expect(ctx.stderr).to.equal(` ${x} Error: uh oh!\n`) - await config.errorLogger!.flush() - expect(readFileSync(errlog, 'utf8')).to.contain('Error: uh oh!') - expect(exitStub.firstCall.firstArg).to.equal(2) - }) + .stderr() + .do(() => { + config.errlog = errlog + }) + .finally(() => { + config.errlog = undefined + }) + .it('logs when errlog is set', async (ctx) => { + await handle(new CLIError('uh oh!')) + expect(ctx.stderr).to.equal(` ${x} Error: uh oh!\n`) + await config.errorLogger!.flush() + expect(readFileSync(errlog, 'utf8')).to.contain('Error: uh oh!') + expect(exitStub.firstCall.firstArg).to.equal(2) + }) fancy - .stdout() - .stderr() - .it('should use default exit code for Error (1)', async ctx => { - const error = new Error('foo bar baz') - await handle(error) - expect(ctx.stdout).to.equal('') - expect(ctx.stderr).to.include('foo bar baz') - expect(exitStub.firstCall.firstArg).to.equal(1) - }) + .stdout() + .stderr() + .it('should use default exit code for Error (1)', async (ctx) => { + const error = new Error('foo bar baz') + await handle(error) + expect(ctx.stdout).to.equal('') + expect(ctx.stderr).to.include('foo bar baz') + expect(exitStub.firstCall.firstArg).to.equal(1) + }) fancy - .stdout() - .stderr() - .it('should use default exit code for CLIError (2)', async ctx => { - const error = new CLIError('foo bar baz') - await handle(error) - expect(ctx.stdout).to.equal('') - expect(ctx.stderr).to.include('foo bar baz') - expect(exitStub.firstCall.firstArg).to.equal(2) - }) + .stdout() + .stderr() + .it('should use default exit code for CLIError (2)', async (ctx) => { + const error = new CLIError('foo bar baz') + await handle(error) + expect(ctx.stdout).to.equal('') + expect(ctx.stderr).to.include('foo bar baz') + expect(exitStub.firstCall.firstArg).to.equal(2) + }) fancy - .stdout() - .stderr() - .it('should use exit code provided by CLIError (0)', async ctx => { - const error = new CLIError('foo bar baz', {exit: 0}) - await handle(error) - expect(ctx.stdout).to.equal('') - expect(ctx.stderr).to.include('foo bar baz') - expect(exitStub.firstCall.firstArg).to.equal(0) - }) + .stdout() + .stderr() + .it('should use exit code provided by CLIError (0)', async (ctx) => { + const error = new CLIError('foo bar baz', {exit: 0}) + await handle(error) + expect(ctx.stdout).to.equal('') + expect(ctx.stderr).to.include('foo bar baz') + expect(exitStub.firstCall.firstArg).to.equal(0) + }) fancy - .stdout() - .stderr() - .it('should use exit code provided by CLIError (9999)', async ctx => { - const error = new CLIError('foo bar baz', {exit: 9999}) - await handle(error) - expect(ctx.stdout).to.equal('') - expect(ctx.stderr).to.include('foo bar baz') - expect(exitStub.firstCall.firstArg).to.equal(9999) - }) + .stdout() + .stderr() + .it('should use exit code provided by CLIError (9999)', async (ctx) => { + const error = new CLIError('foo bar baz', {exit: 9999}) + await handle(error) + expect(ctx.stdout).to.equal('') + expect(ctx.stderr).to.include('foo bar baz') + expect(exitStub.firstCall.firstArg).to.equal(9999) + }) describe('exit', () => { fancy - .stderr() - .stdout() - .it('exits without displaying anything', async ctx => { - try { - exitErrorThrower(9000) - } catch (error: any) { - await handle(error) - } + .stderr() + .stdout() + .it('exits without displaying anything', async (ctx) => { + try { + exitErrorThrower(9000) + } catch (error: any) { + await handle(error) + } - expect(ctx.stdout).to.equal('') - expect(ctx.stderr).to.equal('') - expect(exitStub.firstCall.firstArg).to.equal(9000) - }) + expect(ctx.stdout).to.equal('') + expect(ctx.stderr).to.equal('') + expect(exitStub.firstCall.firstArg).to.equal(9000) + }) }) }) diff --git a/test/errors/pretty-print.test.ts b/test/errors/pretty-print.test.ts index 5b7259419..cea73bcd4 100644 --- a/test/errors/pretty-print.test.ts +++ b/test/errors/pretty-print.test.ts @@ -6,45 +6,39 @@ import {config} from '../../src/errors/config' const stripAnsi = require('strip-ansi') describe('pretty-print', () => { - fancy - .it('pretty prints an error', async () => { + fancy.it('pretty prints an error', async () => { const sampleError: Error & PrettyPrintableError = new Error('Something very serious has gone wrong with the flags!') sampleError.ref = 'https://oclif.io/docs/flags' sampleError.code = 'OCLIF_BAD_FLAG' sampleError.suggestions = ['Try using using a good flag'] - expect( - stripAnsi(prettyPrint(sampleError)), - ).to.equal(` Error: Something very serious has gone wrong with the flags! + expect(stripAnsi(prettyPrint(sampleError))).to + .equal(` Error: Something very serious has gone wrong with the flags! Code: OCLIF_BAD_FLAG Try this: Try using using a good flag Reference: https://oclif.io/docs/flags`) }) - fancy - .it('pretty prints multiple suggestions', async () => { + fancy.it('pretty prints multiple suggestions', async () => { const sampleError: Error & PrettyPrintableError = new Error('Something very serious has gone wrong with the flags!') sampleError.suggestions = ['Use a good flag', 'Use no flags'] - expect( - stripAnsi(prettyPrint(sampleError)), - ).to.equal(` Error: Something very serious has gone wrong with the flags! + expect(stripAnsi(prettyPrint(sampleError))).to + .equal(` Error: Something very serious has gone wrong with the flags! Try this: * Use a good flag * Use no flags`) }) - fancy - .it('pretty prints with omitted fields', async () => { + fancy.it('pretty prints with omitted fields', async () => { const sampleError = new Error('Something very serious has gone wrong with the flags!') - expect( - stripAnsi(prettyPrint(sampleError)), - ).to.equal(' Error: Something very serious has gone wrong with the flags!') + expect(stripAnsi(prettyPrint(sampleError))).to.equal( + ' Error: Something very serious has gone wrong with the flags!', + ) }) describe('CLI Error properties', () => { - fancy - .it('supports the bang property', async () => { + fancy.it('supports the bang property', async () => { class SampleCLIError extends CLIError { get bang() { return '>>>' @@ -55,8 +49,7 @@ describe('pretty-print', () => { expect(stripAnsi(prettyPrint(sampleError))).to.equal(' >>> Error: This is a CLI error') }) - fancy - .it('supports the \'name\' message prefix property', async () => { + fancy.it("supports the 'name' message prefix property", async () => { const defaultBang = process.platform === 'win32' ? 'ยป' : 'โ€บ' const sampleError = new CLIError('This is a CLI error') sampleError.name = 'Errorz' @@ -76,8 +69,7 @@ describe('pretty-print', () => { config.debug = initialConfigDebug }) - fancy - .it('shows the stack for an error', async () => { + fancy.it('shows the stack for an error', async () => { const error = new Error('oh no!') error.stack = 'this is the error stack property' expect(prettyPrint(error)).to.equal('this is the error stack property') diff --git a/test/errors/warn.test.ts b/test/errors/warn.test.ts index 90fb1b7ea..d51827aa5 100644 --- a/test/errors/warn.test.ts +++ b/test/errors/warn.test.ts @@ -8,17 +8,17 @@ const errlog = join(__dirname, '../tmp/mytest/warn.log') describe('warn', () => { fancy - .stderr() - .do(() => { - config.errlog = errlog - }) - .finally(() => { - config.errlog = undefined - }) - .it('warns', async ctx => { - warn('foo!') - expect(ctx.stderr).to.contain('Warning: foo!') - await config.errorLogger!.flush() - expect(await readFile(errlog, 'utf8')).to.contain('Warning: foo!') - }) + .stderr() + .do(() => { + config.errlog = errlog + }) + .finally(() => { + config.errlog = undefined + }) + .it('warns', async (ctx) => { + warn('foo!') + expect(ctx.stderr).to.contain('Warning: foo!') + await config.errorLogger!.flush() + expect(await readFile(errlog, 'utf8')).to.contain('Warning: foo!') + }) }) diff --git a/test/help/_test-help-class.ts b/test/help/_test-help-class.ts index be8391651..44eea87e0 100644 --- a/test/help/_test-help-class.ts +++ b/test/help/_test-help-class.ts @@ -5,7 +5,7 @@ import {HelpBase} from '../../src' -export default class extends HelpBase { +export default class extends HelpBase { async showHelp(): Promise { console.log('help') } diff --git a/test/help/docopts.test.ts b/test/help/docopts.test.ts index 12eaca7ff..658197747 100644 --- a/test/help/docopts.test.ts +++ b/test/help/docopts.test.ts @@ -4,90 +4,104 @@ import {Flags} from '../../src' describe('doc opts', () => { it('shows required string field', async () => { - const usage = DocOpts.generate({flags: { - testFlag: Flags.string({ - name: 'testFlag', - description: 'test', - required: true, - char: 'f', - }), - }} as any) + const usage = DocOpts.generate({ + flags: { + testFlag: Flags.string({ + name: 'testFlag', + description: 'test', + required: true, + char: 'f', + }), + }, + } as any) expect(usage).to.contain(' -f ') }) it('shows optional boolean field', () => { - const usage = DocOpts.generate({flags: { - testFlag: Flags.boolean({ - name: 'testFlag', - description: 'test', - char: 'f', - }), - }} as any) + const usage = DocOpts.generate({ + flags: { + testFlag: Flags.boolean({ + name: 'testFlag', + description: 'test', + char: 'f', + }), + }, + } as any) // boolean fields don't have a value expect(usage).to.contain(' [-f]') }) it('shows no short char', () => { - const usage = DocOpts.generate({flags: { - testFlag: Flags.string({ - name: 'testFlag', - description: 'test', - options: ['a', 'b'], - }), - }} as any) + const usage = DocOpts.generate({ + flags: { + testFlag: Flags.string({ + name: 'testFlag', + description: 'test', + options: ['a', 'b'], + }), + }, + } as any) expect(usage).to.contain(' [--testFlag a|b]') }) it('shows url type', () => { - const usage = DocOpts.generate({flags: { - testFlag: Flags.url({ - name: 'testFlag', - description: 'test', - char: 's', - }), - }} as any) + const usage = DocOpts.generate({ + flags: { + testFlag: Flags.url({ + name: 'testFlag', + description: 'test', + char: 's', + }), + }, + } as any) expect(usage).to.contain(' [-s ]') }) it('does not show hidden type', () => { - const usage = DocOpts.generate({flags: { - testFlag: Flags.url({ - name: 'testFlag', - description: 'test', - char: 's', - hidden: true, - }), - }} as any) + const usage = DocOpts.generate({ + flags: { + testFlag: Flags.url({ + name: 'testFlag', + description: 'test', + char: 's', + hidden: true, + }), + }, + } as any) expect(usage).to.not.contain(' [-s ]') }) it('shows optional one-way depended fields', () => { - const usage = DocOpts.generate({flags: { - testFlag: Flags.url({ - name: 'testFlag', - description: 'test', - char: 's', - }), - testFlag2: Flags.string({ - name: 'testFlag2', - description: 'test', - char: 'f', - dependsOn: ['testFlag'], - }), - }} as any) + const usage = DocOpts.generate({ + flags: { + testFlag: Flags.url({ + name: 'testFlag', + description: 'test', + char: 's', + }), + testFlag2: Flags.string({ + name: 'testFlag2', + description: 'test', + char: 'f', + dependsOn: ['testFlag'], + }), + }, + } as any) expect(usage).to.contain(' [-f -s ]') }) it('shows one-way depended field on required field', () => { - const usage = DocOpts.generate({flags: { - testFlag: Flags.url({ - name: 'testFlag', - description: 'test', - char: 's', - required: true, - }), - testFlag2: Flags.string({ - name: 'testFlag2', - description: 'test', - char: 'f', - dependsOn: ['testFlag'], - }), - }} as any) + const usage = DocOpts.generate({ + flags: { + testFlag: Flags.url({ + name: 'testFlag', + description: 'test', + char: 's', + required: true, + }), + testFlag2: Flags.string({ + name: 'testFlag2', + description: 'test', + char: 'f', + dependsOn: ['testFlag'], + }), + }, + } as any) // If a flag depends on a required flag, then it is optional. // So this should technically be "(-f [-s ])" but // does that even make sense anymore since -f will always be there? @@ -95,143 +109,159 @@ describe('doc opts', () => { expect(usage).to.contain(' (-f -s )') }) it('shows required one-way depended field on optional field', () => { - const usage = DocOpts.generate({flags: { - testFlag: Flags.url({ - name: 'testFlag', - description: 'test', - char: 's', - }), - testFlag2: Flags.string({ - name: 'testFlag2', - description: 'test', - char: 'f', - required: true, - dependsOn: ['testFlag'], - }), - }} as any) + const usage = DocOpts.generate({ + flags: { + testFlag: Flags.url({ + name: 'testFlag', + description: 'test', + char: 's', + }), + testFlag2: Flags.string({ + name: 'testFlag2', + description: 'test', + char: 'f', + required: true, + dependsOn: ['testFlag'], + }), + }, + } as any) // If the required flag depends on an optional, it isn't really optional. expect(usage).to.contain(' (-f -s )') }) it('shows optional one-way exclusive fields', () => { - const usage = DocOpts.generate({flags: { - testFlag: Flags.url({ - name: 'testFlag', - description: 'test', - char: 's', - }), - testFlag2: Flags.string({ - name: 'testFlag2', - description: 'test', - char: 'f', - exclusive: ['testFlag'], - }), - }} as any) + const usage = DocOpts.generate({ + flags: { + testFlag: Flags.url({ + name: 'testFlag', + description: 'test', + char: 's', + }), + testFlag2: Flags.string({ + name: 'testFlag2', + description: 'test', + char: 'f', + exclusive: ['testFlag'], + }), + }, + } as any) expect(usage).to.contain(' [-f | -s ]') }) it('shows one-way exclusive field on required field', () => { - const usage = DocOpts.generate({flags: { - testFlag: Flags.url({ - name: 'testFlag', - description: 'test', - char: 's', - required: true, - }), - testFlag2: Flags.string({ - name: 'testFlag2', - description: 'test', - char: 'f', - exclusive: ['testFlag'], - }), - }} as any) + const usage = DocOpts.generate({ + flags: { + testFlag: Flags.url({ + name: 'testFlag', + description: 'test', + char: 's', + required: true, + }), + testFlag2: Flags.string({ + name: 'testFlag2', + description: 'test', + char: 'f', + exclusive: ['testFlag'], + }), + }, + } as any) expect(usage).to.contain(' (-f | -s )') }) it('shows required one-way exclusive field on optional field', () => { - const usage = DocOpts.generate({flags: { - testFlag: Flags.url({ - name: 'testFlag', - description: 'test', - char: 's', - }), - testFlag2: Flags.string({ - name: 'testFlag2', - description: 'test', - char: 'f', - required: true, - exclusive: ['testFlag'], - }), - }} as any) + const usage = DocOpts.generate({ + flags: { + testFlag: Flags.url({ + name: 'testFlag', + description: 'test', + char: 's', + }), + testFlag2: Flags.string({ + name: 'testFlag2', + description: 'test', + char: 'f', + required: true, + exclusive: ['testFlag'], + }), + }, + } as any) expect(usage).to.contain(' (-f | -s )') }) it('shows option one-way exclusive field on optional field', () => { - const usage = DocOpts.generate({flags: { - testFlag: Flags.url({ - name: 'testFlag', - description: 'test', - char: 's', - }), - testFlag2: Flags.string({ - name: 'testFlag2', - description: 'test', - char: 'f', - required: true, - exclusive: ['testFlag'], - }), - }} as any) + const usage = DocOpts.generate({ + flags: { + testFlag: Flags.url({ + name: 'testFlag', + description: 'test', + char: 's', + }), + testFlag2: Flags.string({ + name: 'testFlag2', + description: 'test', + char: 'f', + required: true, + exclusive: ['testFlag'], + }), + }, + } as any) expect(usage).to.contain(' (-f | -s )') }) it('shows optional exclusive fields defined twice', () => { - const usage = DocOpts.generate({flags: { - testFlag: Flags.url({ - name: 'testFlag', - description: 'test', - char: 's', - exclusive: ['testFlag2'], - }), - testFlag2: Flags.string({ - name: 'testFlag2', - description: 'test', - char: 'f', - exclusive: ['testFlag'], - }), - }} as any) + const usage = DocOpts.generate({ + flags: { + testFlag: Flags.url({ + name: 'testFlag', + description: 'test', + char: 's', + exclusive: ['testFlag2'], + }), + testFlag2: Flags.string({ + name: 'testFlag2', + description: 'test', + char: 'f', + exclusive: ['testFlag'], + }), + }, + } as any) expect(usage).to.contain(' [-s | -f ]') }) it('shows optional two-way depended fields', () => { - const usage = DocOpts.generate({flags: { - testFlag: Flags.url({ - name: 'testFlag', - description: 'test', - char: 's', - dependsOn: ['testFlag2'], - }), - testFlag2: Flags.string({ - name: 'testFlag2', - description: 'test', - char: 'f', - dependsOn: ['testFlag'], - }), - }} as any) + const usage = DocOpts.generate({ + flags: { + testFlag: Flags.url({ + name: 'testFlag', + description: 'test', + char: 's', + dependsOn: ['testFlag2'], + }), + testFlag2: Flags.string({ + name: 'testFlag2', + description: 'test', + char: 'f', + dependsOn: ['testFlag'], + }), + }, + } as any) expect(usage).to.contain(' [-s -f ]') }) it('shows required two-way depended fields', () => { - const usage = DocOpts.generate({flags: { - testFlag: Flags.url({ - name: 'testFlag', - description: 'test', - char: 's', - required: true, - dependsOn: ['testFlag2'], - }), - testFlag2: Flags.string({ - name: 'testFlag2', - description: 'test', - char: 'f', - required: true, - dependsOn: ['testFlag'], - }), - }} as any) + const usage = DocOpts.generate({ + flags: { + testFlag: Flags.url({ + name: 'testFlag', + description: 'test', + char: 's', + required: true, + dependsOn: ['testFlag2'], + }), + testFlag2: Flags.string({ + name: 'testFlag2', + description: 'test', + char: 'f', + required: true, + dependsOn: ['testFlag'], + }), + }, + } as any) expect(usage).to.contain(' (-s -f )') }) }) diff --git a/test/help/fixtures/fixtures.ts b/test/help/fixtures/fixtures.ts index 4a1661891..228b80a8a 100644 --- a/test/help/fixtures/fixtures.ts +++ b/test/help/fixtures/fixtures.ts @@ -126,3 +126,20 @@ export const DbTopic: Topic = { name: 'db', description: 'This topic is for the db topic', } + +// deprecateAliases +export class DeprecateAliases extends Command { + static id = 'foo:bar' + + static aliases = ['foo:bar:alias'] + + static deprecateAliases = true + + static flags = {} + + static args = {} + + async run(): Promise { + 'run' + } +} diff --git a/test/help/format-command-with-options.test.ts b/test/help/format-command-with-options.test.ts index f3a816dc6..ba0fd0e49 100644 --- a/test/help/format-command-with-options.test.ts +++ b/test/help/format-command-with-options.test.ts @@ -13,35 +13,38 @@ class Command extends Base { } const test = base -.loadConfig() -.add('help', ctx => new TestHelp(ctx.config as any)) -.register('commandHelp', commandHelp) + .loadConfig() + .add('help', (ctx) => new TestHelp(ctx.config as any)) + .register('commandHelp', commandHelp) describe('formatCommand', () => { test - .commandHelp(class extends Command { - static id = 'apps:create' + .commandHelp( + class extends Command { + static id = 'apps:create' - static aliases = ['app:init', 'create'] + static aliases = ['app:init', 'create'] - static description = `first line + static description = `first line multiline help` - static args = { - // eslint-disable-next-line camelcase - app_name: Args.string({description: 'app to use'}), - } - - static flags = { - app: flags.string({char: 'a', hidden: true}), - foo: flags.string({char: 'f', description: 'foobar'.repeat(18)}), - force: flags.boolean({description: 'force it '.repeat(15)}), - ss: flags.boolean({description: 'newliney\n'.repeat(4)}), - remote: flags.string({char: 'r'}), - label: flags.string({char: 'l', helpLabel: '-l'}), - } - }) - .it('handles multi-line help output', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + static args = { + // eslint-disable-next-line camelcase + app_name: Args.string({description: 'app to use'}), + } + + static flags = { + app: flags.string({char: 'a', hidden: true}), + foo: flags.string({char: 'f', description: 'foobar'.repeat(18)}), + force: flags.boolean({description: 'force it '.repeat(15)}), + ss: flags.boolean({description: 'newliney\n'.repeat(4)}), + remote: flags.string({char: 'r'}), + label: flags.string({char: 'l', helpLabel: '-l'}), + } + }, + ) + .it('handles multi-line help output', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [APP_NAME] [-f ] [--force] [--ss] [-r ] [-l ] @@ -66,31 +69,35 @@ DESCRIPTION ALIASES $ oclif app:init - $ oclif create`)) + $ oclif create`), + ) describe('arg and flag multiline handling', () => { test - .commandHelp(class extends Command { - static id = 'apps:create' - - static description = 'description of apps:create' - - static aliases = ['app:init', 'create'] - - static args = { - // eslint-disable-next-line camelcase - app_name: Args.string({description: 'app to use'.repeat(35)}), - } - - static flags = { - app: flags.string({char: 'a', hidden: true}), - foo: flags.string({char: 'f', description: 'foobar'.repeat(15)}), - force: flags.boolean({description: 'force it '.repeat(15)}), - ss: flags.boolean({description: 'newliney\n'.repeat(4)}), - remote: flags.string({char: 'r'}), - } - }) - .it('show args and flags side by side when their output do not exceed 4 lines ', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + .commandHelp( + class extends Command { + static id = 'apps:create' + + static description = 'description of apps:create' + + static aliases = ['app:init', 'create'] + + static args = { + // eslint-disable-next-line camelcase + app_name: Args.string({description: 'app to use'.repeat(35)}), + } + + static flags = { + app: flags.string({char: 'a', hidden: true}), + foo: flags.string({char: 'f', description: 'foobar'.repeat(15)}), + force: flags.boolean({description: 'force it '.repeat(15)}), + ss: flags.boolean({description: 'newliney\n'.repeat(4)}), + remote: flags.string({char: 'r'}), + } + }, + ) + .it('show args and flags side by side when their output do not exceed 4 lines ', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [APP_NAME] [-f ] [--force] [--ss] [-r ] @@ -116,30 +123,34 @@ OPTIONS ALIASES $ oclif app:init - $ oclif create`)) + $ oclif create`), + ) test - .commandHelp(class extends Command { - static id = 'apps:create' - - static description = 'description of apps:create' - - static aliases = ['app:init', 'create'] - - static args = { - // eslint-disable-next-line camelcase - app_name: Args.string({description: 'app to use'.repeat(35)}), - } - - static flags = { - app: flags.string({char: 'a', hidden: true}), - foo: flags.string({char: 'f', description: 'foobar'.repeat(20)}), - force: flags.boolean({description: 'force it '.repeat(29)}), - ss: flags.boolean({description: 'newliney\n'.repeat(5)}), - remote: flags.string({char: 'r'}), - } - }) - .it('shows stacked args and flags when the lines exceed 4', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + .commandHelp( + class extends Command { + static id = 'apps:create' + + static description = 'description of apps:create' + + static aliases = ['app:init', 'create'] + + static args = { + // eslint-disable-next-line camelcase + app_name: Args.string({description: 'app to use'.repeat(35)}), + } + + static flags = { + app: flags.string({char: 'a', hidden: true}), + foo: flags.string({char: 'f', description: 'foobar'.repeat(20)}), + force: flags.boolean({description: 'force it '.repeat(29)}), + ss: flags.boolean({description: 'newliney\n'.repeat(5)}), + remote: flags.string({char: 'r'}), + } + }, + ) + .it('shows stacked args and flags when the lines exceed 4', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [APP_NAME] [-f ] [--force] [--ss] [-r ] @@ -173,28 +184,33 @@ OPTIONS ALIASES $ oclif app:init - $ oclif create`)) + $ oclif create`), + ) }) describe('description', () => { test - .commandHelp(class extends Command { - static id = 'apps:create' - - static description = 'description of apps:create\nthese values are after and will show up in the command description' - - static aliases = ['app:init', 'create'] - - static args = { - // eslint-disable-next-line camelcase - app_name: Args.string({description: 'app to use'}), - } - - static flags = { - force: flags.boolean({description: 'forces'}), - } - }) - .it('outputs command description with values after a \\n newline character', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + .commandHelp( + class extends Command { + static id = 'apps:create' + + static description = + 'description of apps:create\nthese values are after and will show up in the command description' + + static aliases = ['app:init', 'create'] + + static args = { + // eslint-disable-next-line camelcase + app_name: Args.string({description: 'app to use'}), + } + + static flags = { + force: flags.boolean({description: 'forces'}), + } + }, + ) + .it('outputs command description with values after a \\n newline character', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [APP_NAME] [--force] ARGUMENTS @@ -208,54 +224,67 @@ DESCRIPTION ALIASES $ oclif app:init - $ oclif create`)) + $ oclif create`), + ) test - .commandHelp(class extends Command { - static id = 'apps:create' - - static description = 'root part of the description\nThe <%= config.bin %> CLI has <%= command.id %>' - }) - .it('renders template string from description', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + .commandHelp( + class extends Command { + static id = 'apps:create' + + static description = 'root part of the description\nThe <%= config.bin %> CLI has <%= command.id %>' + }, + ) + .it('renders template string from description', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create DESCRIPTION - The oclif CLI has apps:create`)) + The oclif CLI has apps:create`), + ) }) - describe(('flags'), () => { + describe('flags', () => { test - .commandHelp(class extends Command { - static id = 'apps:create' - - static flags = { - myenum: flags.string({ - options: ['a', 'b', 'c'], - }), - } - }) - .it('outputs flag enum', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + .commandHelp( + class extends Command { + static id = 'apps:create' + + static flags = { + myenum: flags.string({ + options: ['a', 'b', 'c'], + }), + } + }, + ) + .it('outputs flag enum', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [--myenum a|b|c] OPTIONS - --myenum=a|b|c`)) + --myenum=a|b|c`), + ) test - .commandHelp(class extends Command { - static id = 'apps:create' - - static args = { - arg1: Args.string({default: '.'}), - arg2: Args.string({default: '.', description: 'arg2 desc'}), - arg3: Args.string({description: 'arg3 desc'}), - } - - static flags = { - flag1: flags.string({default: '.'}), - flag2: flags.string({default: '.', description: 'flag2 desc'}), - flag3: flags.string({description: 'flag3 desc'}), - } - }).it('outputs with default flag options', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + .commandHelp( + class extends Command { + static id = 'apps:create' + + static args = { + arg1: Args.string({default: '.'}), + arg2: Args.string({default: '.', description: 'arg2 desc'}), + arg3: Args.string({description: 'arg3 desc'}), + } + + static flags = { + flag1: flags.string({default: '.'}), + flag2: flags.string({default: '.', description: 'flag2 desc'}), + flag3: flags.string({description: 'flag3 desc'}), + } + }, + ) + .it('outputs with default flag options', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [ARG1] [ARG2] [ARG3] [--flag1 ] [--flag2 ] [--flag3 ] @@ -267,144 +296,189 @@ ARGUMENTS OPTIONS --flag1=flag1 [default: .] --flag2=flag2 [default: .] flag2 desc - --flag3=flag3 flag3 desc`)) + --flag3=flag3 flag3 desc`), + ) test - .commandHelp(class extends Command { - static id = 'apps:create' - - static flags = { - opt: flags.boolean({allowNo: true}), - } - }) - .it('outputs with with no options', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + .commandHelp( + class extends Command { + static id = 'apps:create' + + static flags = { + opt: flags.boolean({allowNo: true}), + } + }, + ) + .it('outputs with with no options', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [--opt] OPTIONS - --[no-]opt`)) + --[no-]opt`), + ) }) - describe('args', () => { + describe('args', () => { test - .commandHelp(class extends Command { - static id = 'apps:create' - - static args = { - arg1: Args.string({description: 'Show the options', options: ['option1', 'option2']}), - } - }) - .it('outputs with arg options', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + .commandHelp( + class extends Command { + static id = 'apps:create' + + static args = { + arg1: Args.string({description: 'Show the options', options: ['option1', 'option2']}), + } + }, + ) + .it('outputs with arg options', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [ARG1] ARGUMENTS - ARG1 (option1|option2) Show the options`)) + ARG1 (option1|option2) Show the options`), + ) }) describe('usage', () => { test - .commandHelp(class extends Command { - static id = 'apps:create' - - static usage = '<%= config.bin %> <%= command.id %> usage' - }) - .it('outputs usage with templates', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE - $ oclif oclif apps:create usage`)) + .commandHelp( + class extends Command { + static id = 'apps:create' + + static usage = '<%= config.bin %> <%= command.id %> usage' + }, + ) + .it('outputs usage with templates', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE + $ oclif oclif apps:create usage`), + ) test - .commandHelp(class extends Command { - static id = 'apps:create' - - static usage = ['<%= config.bin %>', '<%= command.id %> usage'] - }) - .it('outputs usage arrays with templates', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + .commandHelp( + class extends Command { + static id = 'apps:create' + + static usage = ['<%= config.bin %>', '<%= command.id %> usage'] + }, + ) + .it('outputs usage arrays with templates', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE $ oclif oclif - $ oclif apps:create usage`)) + $ oclif apps:create usage`), + ) test - .commandHelp(class extends Command { - static id = 'apps:create' - - static usage = undefined - }) - .it('defaults usage when not specified', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE - $ oclif apps:create`)) + .commandHelp( + class extends Command { + static id = 'apps:create' + + static usage = undefined + }, + ) + .it('defaults usage when not specified', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE + $ oclif apps:create`), + ) }) describe('examples', () => { test - .commandHelp(class extends Command { - static examples = ['it handles a list of examples', 'more example text'] - }) - .it('outputs multiple examples', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + .commandHelp( + class extends Command { + static examples = ['it handles a list of examples', 'more example text'] + }, + ) + .it('outputs multiple examples', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE $ oclif EXAMPLES it handles a list of examples - more example text`)) + more example text`), + ) test - .commandHelp(class extends Command { - static examples = ['it handles a single example'] - }) - .it('outputs a single example', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + .commandHelp( + class extends Command { + static examples = ['it handles a single example'] + }, + ) + .it('outputs a single example', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE $ oclif EXAMPLES - it handles a single example`)) + it handles a single example`), + ) test - .commandHelp(class extends Command { - static id = 'oclif:command' - - static examples = ['the bin is <%= config.bin %>', 'the command id is <%= command.id %>'] - }) - .it('outputs examples using templates', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + .commandHelp( + class extends Command { + static id = 'oclif:command' + + static examples = ['the bin is <%= config.bin %>', 'the command id is <%= command.id %>'] + }, + ) + .it('outputs examples using templates', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE $ oclif oclif:command EXAMPLES the bin is oclif - the command id is oclif:command`)) + the command id is oclif:command`), + ) test - .commandHelp(class extends Command { - static id = 'oclif:command' - - static examples = ['<%= config.bin %> <%= command.id %> --help'] - }) - .it('formats if command', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + .commandHelp( + class extends Command { + static id = 'oclif:command' + + static examples = ['<%= config.bin %> <%= command.id %> --help'] + }, + ) + .it('formats if command', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE $ oclif oclif:command EXAMPLES - $ oclif oclif:command --help`)) + $ oclif oclif:command --help`), + ) test - .commandHelp(class extends Command { - static id = 'oclif:command' - - static examples = ['Prints out help.\n<%= config.bin %> <%= command.id %> --help'] - }) - .it('formats if command with description', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + .commandHelp( + class extends Command { + static id = 'oclif:command' + + static examples = ['Prints out help.\n<%= config.bin %> <%= command.id %> --help'] + }, + ) + .it('formats if command with description', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE $ oclif oclif:command EXAMPLES Prints out help. - $ oclif oclif:command --help`)) + $ oclif oclif:command --help`), + ) test - .commandHelp(class extends Command { - static id = 'oclif:command' - - static examples = [{description: 'Prints out help.', command: '<%= config.bin %> <%= command.id %> --help'}] - }) - .it('formats example object', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + .commandHelp( + class extends Command { + static id = 'oclif:command' + + static examples = [{description: 'Prints out help.', command: '<%= config.bin %> <%= command.id %> --help'}] + }, + ) + .it('formats example object', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE $ oclif oclif:command EXAMPLES Prints out help. - $ oclif oclif:command --help`)) + $ oclif oclif:command --help`), + ) }) }) diff --git a/test/help/format-command.test.ts b/test/help/format-command.test.ts index 72bcfb258..4df21cd93 100644 --- a/test/help/format-command.test.ts +++ b/test/help/format-command.test.ts @@ -13,40 +13,43 @@ class Command extends Base { } const test = base -.loadConfig() -.add('help', ctx => new TestHelp(ctx.config as any)) -.register('commandHelp', commandHelp) + .loadConfig() + .add('help', (ctx) => new TestHelp(ctx.config as any)) + .register('commandHelp', commandHelp) describe('formatCommand', () => { test - .commandHelp(class extends Command { - static { - this.id = 'apps:create' + .commandHelp( + class extends Command { + static { + this.id = 'apps:create' - this.aliases = ['app:init', 'create'] + this.aliases = ['app:init', 'create'] - this.description = `first line + this.description = `first line multiline help` - this.enableJsonFlag = true - - this.args = { - // eslint-disable-next-line camelcase - app_name: Args.string({description: 'app to use'}), - } - - this.flags = { - app: flags.string({char: 'a', hidden: true}), - foo: flags.string({char: 'f', description: 'foobar'.repeat(18)}), - force: flags.boolean({description: 'force it '.repeat(15)}), - ss: flags.boolean({description: 'newliney\n'.repeat(4)}), - remote: flags.string({char: 'r'}), - label: flags.string({char: 'l', helpLabel: '-l'}), - } - } - }) - .it('handles multi-line help output', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + this.enableJsonFlag = true + + this.args = { + // eslint-disable-next-line camelcase + app_name: Args.string({description: 'app to use'}), + } + + this.flags = { + app: flags.string({char: 'a', hidden: true}), + foo: flags.string({char: 'f', description: 'foobar'.repeat(18)}), + force: flags.boolean({description: 'force it '.repeat(15)}), + ss: flags.boolean({description: 'newliney\n'.repeat(4)}), + remote: flags.string({char: 'r'}), + label: flags.string({char: 'l', helpLabel: '-l'}), + } + } + }, + ) + .it('handles multi-line help output', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [APP_NAME] [--json] [-f ] [--force] [--ss] [-r ] [-l ] @@ -76,35 +79,39 @@ DESCRIPTION ALIASES $ oclif app:init - $ oclif create`)) + $ oclif create`), + ) describe('arg and flag multiline handling', () => { test - .commandHelp(class extends Command { - static { - this.id = 'apps:create' - - this.description = 'description of apps:create' - - this.aliases = ['app:init', 'create'] - - this.enableJsonFlag = true - - this.args = { - // eslint-disable-next-line camelcase - app_name: Args.string({description: 'app to use'.repeat(35)}), - } - - this.flags = { - app: flags.string({char: 'a', hidden: true}), - foo: flags.string({char: 'f', description: 'foobar'.repeat(15)}), - force: flags.boolean({description: 'force it '.repeat(15)}), - ss: flags.boolean({description: 'newliney\n'.repeat(4)}), - remote: flags.string({char: 'r'}), - } - } - }) - .it('show args and flags side by side when their output do not exceed 4 lines ', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + .commandHelp( + class extends Command { + static { + this.id = 'apps:create' + + this.description = 'description of apps:create' + + this.aliases = ['app:init', 'create'] + + this.enableJsonFlag = true + + this.args = { + // eslint-disable-next-line camelcase + app_name: Args.string({description: 'app to use'.repeat(35)}), + } + + this.flags = { + app: flags.string({char: 'a', hidden: true}), + foo: flags.string({char: 'f', description: 'foobar'.repeat(15)}), + force: flags.boolean({description: 'force it '.repeat(15)}), + ss: flags.boolean({description: 'newliney\n'.repeat(4)}), + remote: flags.string({char: 'r'}), + } + } + }, + ) + .it('show args and flags side by side when their output do not exceed 4 lines ', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [APP_NAME] [--json] [-f ] [--force] [--ss] [-r ] @@ -136,34 +143,38 @@ DESCRIPTION ALIASES $ oclif app:init - $ oclif create`)) + $ oclif create`), + ) test - .commandHelp(class extends Command { - static { - this.id = 'apps:create' - - this.description = 'description of apps:create' - - this.aliases = ['app:init', 'create'] - - this.enableJsonFlag = true - - this.args = { - // eslint-disable-next-line camelcase - app_name: Args.string({description: 'app to use'.repeat(35)}), - } - - this.flags = { - app: flags.string({char: 'a', hidden: true}), - foo: flags.string({char: 'f', description: 'foobar'.repeat(20)}), - force: flags.boolean({description: 'force it '.repeat(29)}), - ss: flags.boolean({description: 'newliney\n'.repeat(5)}), - remote: flags.string({char: 'r'}), - } - } - }) - .it('shows stacked args and flags when the lines exceed 4', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + .commandHelp( + class extends Command { + static { + this.id = 'apps:create' + + this.description = 'description of apps:create' + + this.aliases = ['app:init', 'create'] + + this.enableJsonFlag = true + + this.args = { + // eslint-disable-next-line camelcase + app_name: Args.string({description: 'app to use'.repeat(35)}), + } + + this.flags = { + app: flags.string({char: 'a', hidden: true}), + foo: flags.string({char: 'f', description: 'foobar'.repeat(20)}), + force: flags.boolean({description: 'force it '.repeat(29)}), + ss: flags.boolean({description: 'newliney\n'.repeat(5)}), + remote: flags.string({char: 'r'}), + } + } + }, + ) + .it('shows stacked args and flags when the lines exceed 4', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [APP_NAME] [--json] [-f ] [--force] [--ss] [-r ] @@ -203,59 +214,72 @@ DESCRIPTION ALIASES $ oclif app:init - $ oclif create`)) + $ oclif create`), + ) }) describe('summary', () => { test - .commandHelp(class extends Command { - static id = 'test:summary' - - static summary = 'one line summary' - }) - .it('no description header if only a summary', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE - $ oclif test:summary`)) + .commandHelp( + class extends Command { + static id = 'test:summary' + + static summary = 'one line summary' + }, + ) + .it('no description header if only a summary', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE + $ oclif test:summary`), + ) test - .commandHelp(class extends Command { - static id = 'test:summary' + .commandHelp( + class extends Command { + static id = 'test:summary' - static summary = 'one line summary' + static summary = 'one line summary' - static description = 'description that is much longer than the summary' - }) - .it('outputs the summary at the top of the help and description', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + static description = 'description that is much longer than the summary' + }, + ) + .it('outputs the summary at the top of the help and description', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE $ oclif test:summary DESCRIPTION one line summary - description that is much longer than the summary`)) + description that is much longer than the summary`), + ) }) describe('description', () => { test - .commandHelp(class extends Command { - static { - this.id = 'apps:create' - - this.description = 'description of apps:create\n\nthese values are after and will show up in the command description' - - this.aliases = ['app:init', 'create'] - - this.enableJsonFlag = true - - this.args = { - // eslint-disable-next-line camelcase - app_name: Args.string({description: 'app to use'}), - } - - this.flags = { - force: flags.boolean({description: 'forces'}), - } - } - }) - .it('outputs command description with values after a \\n newline character', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + .commandHelp( + class extends Command { + static { + this.id = 'apps:create' + + this.description = + 'description of apps:create\n\nthese values are after and will show up in the command description' + + this.aliases = ['app:init', 'create'] + + this.enableJsonFlag = true + + this.args = { + // eslint-disable-next-line camelcase + app_name: Args.string({description: 'app to use'}), + } + + this.flags = { + force: flags.boolean({description: 'forces'}), + } + } + }, + ) + .it('outputs command description with values after a \\n newline character', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [APP_NAME] [--json] [--force] ARGUMENTS @@ -274,29 +298,37 @@ DESCRIPTION ALIASES $ oclif app:init - $ oclif create`)) + $ oclif create`), + ) test - .commandHelp(class extends Command { - static id = 'apps:create' - - static description = 'root part of the description\n\nThe <%= config.bin %> CLI has <%= command.id %>' - }) - .it('renders template string from description', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + .commandHelp( + class extends Command { + static id = 'apps:create' + + static description = 'root part of the description\n\nThe <%= config.bin %> CLI has <%= command.id %>' + }, + ) + .it('renders template string from description', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create DESCRIPTION root part of the description - The oclif CLI has apps:create`)) + The oclif CLI has apps:create`), + ) test - .commandHelp(class extends Command { - static id = 'apps:create' - - static description = 'root part of the description\r\n\nusing both carriage \n\nreturn and new line' - }) - .it('splits on carriage return and new lines', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + .commandHelp( + class extends Command { + static id = 'apps:create' + + static description = 'root part of the description\r\n\nusing both carriage \n\nreturn and new line' + }, + ) + .it('splits on carriage return and new lines', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create DESCRIPTION @@ -304,64 +336,77 @@ DESCRIPTION using both carriage - return and new line`)) + return and new line`), + ) }) const myEnumValues = ['a', 'b', 'c'] - describe(('flags'), () => { + describe('flags', () => { test - .commandHelp(class extends Command { - static { - this.id = 'apps:create' - - this.flags = { - myenum: flags.string({ - description: 'the description', - options: myEnumValues, - }), - } - } - }) - .it('outputs flag enum', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE + .commandHelp( + class extends Command { + static { + this.id = 'apps:create' + + this.flags = { + myenum: flags.string({ + description: 'the description', + options: myEnumValues, + }), + } + } + }, + ) + .it('outputs flag enum', (ctx: any) => + expect(ctx.commandHelp).to.equal(`USAGE $ oclif apps:create [--myenum a|b|c] FLAGS --myenum=