diff --git a/.eslintrc.json b/.eslintrc.json index 1af383ee4..5275deb76 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,13 +1,14 @@ { "extends": ["oclif", "oclif-typescript", "prettier"], "rules": { - "unicorn/prefer-module": "off", - "unicorn/no-array-reduce": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-namespace": "off", + "import/no-named-as-default-member": "off", "no-useless-constructor": "off", "perfectionist/sort-object-types": "off", "perfectionist/sort-union-types": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-namespace": "off", + "unicorn/no-array-reduce": "off", + "unicorn/prefer-module": "off", "valid-jsdoc": "off" } } diff --git a/.mocharc.json b/.mocharc.json index dc60a4ab5..d78fd6511 100644 --- a/.mocharc.json +++ b/.mocharc.json @@ -1,16 +1,9 @@ { "recursive": true, "reporter": "spec", - "require": [ - "test/helpers/init.js", - "ts-node/register" - ], + "require": ["test/helpers/init.js", "ts-node/register"], "timeout": 60000, - "watch-extensions": [ - "ts" - ], - "watch-files": [ - "src", - "test" - ] + "watch-extensions": ["ts"], + "watch-files": ["src", "test"], + "watch-ignore": ["test/tmp"] } diff --git a/package.json b/package.json index 29dfd0ced..6371e212f 100644 --- a/package.json +++ b/package.json @@ -1,83 +1,61 @@ { "name": "@oclif/core", "description": "base library for oclif CLIs", - "version": "4.0.0-beta.0", + "version": "4.0.0-beta.1", "author": "Salesforce", "bugs": "https://github.com/oclif/core/issues", "dependencies": { - "@types/cli-progress": "^3.11.5", "ansi-escapes": "^4.3.2", - "ansi-styles": "^4.3.0", - "cardinal": "^2.1.1", - "chalk": "^4.1.2", + "ansis": "^3.0.1", "clean-stack": "^3.0.1", - "cli-progress": "^3.12.0", - "color": "^4.2.3", + "cli-spinners": "^2.9.2", "debug": "^4.3.4", "ejs": "^3.1.10", "get-package-type": "^0.1.0", "globby": "^11.1.0", - "hyperlinker": "^1.0.0", "indent-string": "^4.0.0", "is-wsl": "^2.2.0", - "js-yaml": "^3.14.1", "minimatch": "^9.0.4", - "natural-orderby": "^2.0.3", - "object-treeify": "^1.1.33", - "password-prompt": "^1.1.3", - "slice-ansi": "^4.0.0", "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "supports-color": "^8.1.1", - "supports-hyperlinks": "^2.2.0", "widest-line": "^3.1.0", "wordwrap": "^1.0.0", "wrap-ansi": "^7.0.0" }, "devDependencies": { - "@commitlint/config-conventional": "^17.8.1", + "@commitlint/config-conventional": "^19", "@oclif/plugin-help": "^6", - "@oclif/plugin-plugins": "^4", + "@oclif/plugin-plugins": "^5", "@oclif/prettier-config": "^0.2.1", - "@oclif/test": "^3.2.9", - "@types/ansi-styles": "^3.2.1", "@types/benchmark": "^2.1.5", "@types/chai": "^4.3.11", "@types/chai-as-promised": "^7.1.8", "@types/clean-stack": "^2.1.1", - "@types/color": "^3.0.6", "@types/debug": "^4.1.10", "@types/ejs": "^3.1.5", "@types/indent-string": "^4.0.1", - "@types/js-yaml": "^3.12.7", "@types/mocha": "^10.0.6", "@types/node": "^18", - "@types/node-notifier": "^8.0.5", "@types/pnpapi": "^0.0.5", - "@types/slice-ansi": "^4.0.0", - "@types/strip-ansi": "^5.2.1", - "@types/supports-color": "^8.1.1", + "@types/sinon": "^17.0.3", "@types/wordwrap": "^1.0.3", "@types/wrap-ansi": "^3.0.0", "benchmark": "^2.1.4", "chai": "^4.4.1", "chai-as-promised": "^7.1.1", - "commitlint": "^17.8.1", + "commitlint": "^19", "cross-env": "^7.0.3", "eslint": "^8.57.0", "eslint-config-oclif": "^5.1.1", "eslint-config-oclif-typescript": "^3.1.4", "eslint-config-prettier": "^9.1.0", - "fancy-test": "^3.0.14", - "globby": "^11.1.0", - "husky": "^8", - "lint-staged": "^14.0.1", + "husky": "^9", + "lint-staged": "^15", "madge": "^6.1.0", "mocha": "^10.4.0", "nyc": "^15.1.0", "prettier": "^3.2.5", "shx": "^0.3.4", - "sinon": "^16.1.3", + "sinon": "^17", "ts-node": "^10.9.2", "tsd": "^0.31.0", "typescript": "^5" @@ -122,7 +100,7 @@ "lint": "eslint . --ext .ts", "posttest": "yarn lint && yarn test:circular-deps", "prepack": "yarn run build", - "prepare": "husky install", + "prepare": "husky", "pretest": "yarn build && tsc -p test --noEmit --skipLibCheck", "test:circular-deps": "madge lib/ -c", "test:debug": "nyc mocha --debug-brk --inspect \"test/**/*.test.ts\"", diff --git a/src/cli-ux/README.md b/src/cli-ux/README.md deleted file mode 100644 index 7a3a289bd..000000000 --- a/src/cli-ux/README.md +++ /dev/null @@ -1,310 +0,0 @@ -CLI UX -====== - -cli UX utilities. - -**This module has been deprecated** and will be majorly rewritten the next major version. See [discussion](https://github.com/oclif/core/discussions/999) - -# Usage - -The following example assumes you've installed `@oclif/core` to your project with `npm install @oclif/core` or `yarn add @oclif/core` and have it required in your script (TypeScript example): - -```typescript -import {ux} from '@oclif/core' -ux.prompt('What is your name?') -``` - -JavaScript: - -```javascript -const {ux} = require('@oclif/core') - -ux.prompt('What is your name?') -``` - -# ux.prompt() - -Prompt for user input. - -```typescript -// just prompt for input -await ux.prompt('What is your name?') - -// mask input after enter is pressed -await ux.prompt('What is your two-factor token?', {type: 'mask'}) - -// mask input on keypress (before enter is pressed) -await ux.prompt('What is your password?', {type: 'hide'}) - -// yes/no confirmation -await ux.confirm('Continue?') - -// "press any key to continue" -await ux.anykey() -``` - -![prompt demo](assets/prompt.gif) - -# ux.url(text, uri) - -Create a hyperlink (if supported in the terminal) - -```typescript -await ux.url('sometext', 'https://google.com') -// shows sometext as a hyperlink in supported terminals -// shows https://google.com in unsupported terminals -``` - -![url demo](assets/url.gif) - -# ux.action - -Shows a spinner - -```typescript -// start the spinner -ux.action.start('starting a process') -// show on stdout instead of stderr -ux.action.start('starting a process', 'initializing', {stdout: true}) - -// stop the spinner -ux.action.stop() // shows 'starting a process... done' -ux.action.stop('custom message') // shows 'starting a process... custom message' -``` - -This degrades gracefully when not connected to a TTY. It queues up any writes to stdout/stderr so they are displayed above the spinner. - -![action demo](assets/action.gif) - -# ux.annotation - -Shows an iterm annotation - -```typescript -ux.annotation('sometext', 'annotated with this text') -``` - -![annotation demo](assets/annotation.png) - -# ux.wait - -Waits for 1 second or given milliseconds - -```typescript -await ux.wait() -await ux.wait(3000) -``` - -# ux.table - -Displays tabular data - -```typescript -ux.table(data, columns, options) -``` - -Where: - -- `data`: array of data objects to display -- `columns`: [Table.Columns](./src/styled/table.ts) -- `options`: [Table.Options](./src/styled/table.ts) - -`ux.table.flags()` returns an object containing all the table flags to include in your command. - -```typescript -{ - columns: Flags.string({exclusive: ['additional'], description: 'only show provided columns (comma-separated)'}), - sort: Flags.string({description: 'property to sort by (prepend '-' for descending)'}), - filter: Flags.string({description: 'filter property by partial string matching, ex: name=foo'}), - csv: Flags.boolean({exclusive: ['no-truncate'], description: 'output is csv format'}), - extended: Flags.boolean({char: 'x', description: 'show extra columns'}), - 'no-truncate': Flags.boolean({exclusive: ['csv'], description: 'do not truncate output to fit screen'}), - 'no-header': Flags.boolean({exclusive: ['csv'], description: 'hide table header from output'}), -} -``` - -Passing `{only: ['columns']}` or `{except: ['columns']}` as an argument into `ux.table.flags()` allows or blocks, respectively, those flags from the returned object. - -`Table.Columns` defines the table columns and their display options. - -```typescript -const columns: ux.Table.Columns = { - // where `.name` is a property of a data object - name: {}, // "Name" inferred as the column header - id: { - header: 'ID', // override column header - minWidth: '10', // column must display at this width or greater - extended: true, // only display this column when the --extended flag is present - get: row => `US-O1-${row.id}`, // custom getter for data row object - }, -} -``` - -`Table.Options` defines the table options, most of which are the parsed flags from the user for display customization, all of which are optional. - -```typescript -const options: ux.Table.Options = { - printLine: myLogger, // custom logger - columns: flags.columns, - sort: flags.sort, - filter: flags.filter, - csv: flags.csv, - extended: flags.extended, - 'no-truncate': flags['no-truncate'], - 'no-header': flags['no-header'], -} -``` - -Example class: - -```typescript -import {Command, ux} from '@oclif/core' -import axios from 'axios' - -export default class Users extends Command { - static flags = { - ...ux.table.flags() - } - - async run() { - const {flags} = this.parse(Users) - const {data: users} = await axios.get('https://jsonplaceholder.typicode.com/users') - - ux.table(users, { - name: { - minWidth: 7, - }, - company: { - get: row => row.company && row.company.name - }, - id: { - header: 'ID', - extended: true - } - }, { - printLine: this.log.bind(this), - ...flags, // parsed flags - }) - } -} -``` - -Displays: - -```shell -$ example-cli users -Name Company -Leanne Graham Romaguera-Crona -Ervin Howell Deckow-Crist -Clementine Bauch Romaguera-Jacobson -Patricia Lebsack Robel-Corkery -Chelsey Dietrich Keebler LLC -Mrs. Dennis Schulist Considine-Lockman -Kurtis Weissnat Johns Group -Nicholas Runolfsdottir V Abernathy Group -Glenna Reichert Yost and Sons -Clementina DuBuque Hoeger LLC - -$ example-cli users --extended -Name Company ID -Leanne Graham Romaguera-Crona 1 -Ervin Howell Deckow-Crist 2 -Clementine Bauch Romaguera-Jacobson 3 -Patricia Lebsack Robel-Corkery 4 -Chelsey Dietrich Keebler LLC 5 -Mrs. Dennis Schulist Considine-Lockman 6 -Kurtis Weissnat Johns Group 7 -Nicholas Runolfsdottir V Abernathy Group 8 -Glenna Reichert Yost and Sons 9 -Clementina DuBuque Hoeger LLC 10 - -$ example-cli users --columns=name -Name -Leanne Graham -Ervin Howell -Clementine Bauch -Patricia Lebsack -Chelsey Dietrich -Mrs. Dennis Schulist -Kurtis Weissnat -Nicholas Runolfsdottir V -Glenna Reichert -Clementina DuBuque - -$ example-cli users --filter="company=Group" -Name Company -Kurtis Weissnat Johns Group -Nicholas Runolfsdottir V Abernathy Group - -$ example-cli users --sort=company -Name Company -Nicholas Runolfsdottir V Abernathy Group -Mrs. Dennis Schulist Considine-Lockman -Ervin Howell Deckow-Crist -Clementina DuBuque Hoeger LLC -Kurtis Weissnat Johns Group -Chelsey Dietrich Keebler LLC -Patricia Lebsack Robel-Corkery -Leanne Graham Romaguera-Crona -Clementine Bauch Romaguera-Jacobson -Glenna Reichert Yost and Sons -``` - -# ux.tree - -Generate a tree and display it - -```typescript -let tree = ux.tree() -tree.insert('foo') -tree.insert('bar') - -let subtree = ux.tree() -subtree.insert('qux') -tree.nodes.bar.insert('baz', subtree) - -tree.display() -``` - -Outputs: -```shell -├─ foo -└─ bar - └─ baz - └─ qux -``` - -# ux.progress - -Generate a customizable progress bar and display it - -```typescript -const simpleBar = ux.progress() -simpleBar.start() - -const customBar = ux.progress({ - format: 'PROGRESS | {bar} | {value}/{total} Files', - barCompleteChar: '\u2588', - barIncompleteChar: '\u2591', - }) -customBar.start() -``` - -Outputs: -```shell -bar1: -progress [=====================-------------------] 53% | ETA: 1s | 53/100 -bar2: -PROGRESS | █████████████████████████████░░░░░░░░░░░ | 146/204 Files -``` - -To see a more detailed example, run -```shell script -$ ts-node examples/progress.ts -``` - -This example extends [cli-progress](https://www.npmjs.com/package/cli-progress). -See the cli-progress module for all the options and the customizations that can be passed in with the options object. -Only the single bar variant of cli-progress is currently supported. - - diff --git a/src/cli-ux/action/spinners.ts b/src/cli-ux/action/spinners.ts deleted file mode 100644 index 26280ce97..000000000 --- a/src/cli-ux/action/spinners.ts +++ /dev/null @@ -1,372 +0,0 @@ -export default { - arc: { - frames: ['◜', '◠', '◝', '◞', '◡', '◟'], - interval: 100, - }, - arrow: { - frames: ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙'], - interval: 100, - }, - arrow2: { - frames: ['⬆️ ', '↗️ ', '➡️ ', '↘️ ', '⬇️ ', '↙️ ', '⬅️ ', '↖️ '], - interval: 80, - }, - arrow3: { - frames: ['▹▹▹▹▹', '▸▹▹▹▹', '▹▸▹▹▹', '▹▹▸▹▹', '▹▹▹▸▹', '▹▹▹▹▸'], - interval: 120, - }, - balloon: { - frames: [' ', '.', 'o', 'O', '@', '*', ' '], - interval: 140, - }, - balloon2: { - frames: ['.', 'o', 'O', '°', 'O', 'o', '.'], - interval: 120, - }, - bounce: { - frames: ['⠁', '⠂', '⠄', '⠂'], - interval: 120, - }, - bouncingBall: { - frames: [ - '( ● )', - '( ● )', - '( ● )', - '( ● )', - '( ●)', - '( ● )', - '( ● )', - '( ● )', - '( ● )', - '(● )', - ], - interval: 80, - }, - bouncingBar: { - frames: ['[ ]', '[ =]', '[ ==]', '[ ===]', '[====]', '[=== ]', '[== ]', '[= ]'], - interval: 80, - }, - boxBounce: { - frames: ['▖', '▘', '▝', '▗'], - interval: 120, - }, - boxBounce2: { - frames: ['▌', '▀', '▐', '▄'], - interval: 100, - }, - circle: { - frames: ['◡', '⊙', '◠'], - interval: 120, - }, - circleHalves: { - frames: ['◐', '◓', '◑', '◒'], - interval: 50, - }, - circleQuarters: { - frames: ['◴', '◷', '◶', '◵'], - interval: 120, - }, - clock: { - frames: ['🕐 ', '🕑 ', '🕒 ', '🕓 ', '🕔 ', '🕕 ', '🕖 ', '🕗 ', '🕘 ', '🕙 ', '🕚 '], - interval: 100, - }, - dots: { - frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'], - interval: 80, - }, - dots2: { - frames: ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'], - interval: 80, - }, - dots3: { - frames: ['⠋', '⠙', '⠚', '⠞', '⠖', '⠦', '⠴', '⠲', '⠳', '⠓'], - interval: 80, - }, - dots4: { - frames: ['⠄', '⠆', '⠇', '⠋', '⠙', '⠸', '⠰', '⠠', '⠰', '⠸', '⠙', '⠋', '⠇', '⠆'], - interval: 80, - }, - dots5: { - frames: ['⠋', '⠙', '⠚', '⠒', '⠂', '⠂', '⠒', '⠲', '⠴', '⠦', '⠖', '⠒', '⠐', '⠐', '⠒', '⠓', '⠋'], - interval: 80, - }, - dots6: { - frames: [ - '⠁', - '⠉', - '⠙', - '⠚', - '⠒', - '⠂', - '⠂', - '⠒', - '⠲', - '⠴', - '⠤', - '⠄', - '⠄', - '⠤', - '⠴', - '⠲', - '⠒', - '⠂', - '⠂', - '⠒', - '⠚', - '⠙', - '⠉', - '⠁', - ], - interval: 80, - }, - dots7: { - frames: [ - '⠈', - '⠉', - '⠋', - '⠓', - '⠒', - '⠐', - '⠐', - '⠒', - '⠖', - '⠦', - '⠤', - '⠠', - '⠠', - '⠤', - '⠦', - '⠖', - '⠒', - '⠐', - '⠐', - '⠒', - '⠓', - '⠋', - '⠉', - '⠈', - ], - interval: 80, - }, - dots8: { - frames: [ - '⠁', - '⠁', - '⠉', - '⠙', - '⠚', - '⠒', - '⠂', - '⠂', - '⠒', - '⠲', - '⠴', - '⠤', - '⠄', - '⠄', - '⠤', - '⠠', - '⠠', - '⠤', - '⠦', - '⠖', - '⠒', - '⠐', - '⠐', - '⠒', - '⠓', - '⠋', - '⠉', - '⠈', - '⠈', - ], - interval: 80, - }, - dots9: { - frames: ['⢹', '⢺', '⢼', '⣸', '⣇', '⡧', '⡗', '⡏'], - interval: 80, - }, - dots10: { - frames: ['⢄', '⢂', '⢁', '⡁', '⡈', '⡐', '⡠'], - interval: 80, - }, - dots11: { - frames: ['⠁', '⠂', '⠄', '⡀', '⢀', '⠠', '⠐', '⠈'], - interval: 100, - }, - earth: { - frames: ['🌍 ', '🌎 ', '🌏 '], - interval: 180, - }, - flip: { - frames: ['_', '_', '_', '-', '`', '`', "'", '´', '-', '_', '_', '_'], - interval: 70, - }, - growHorizontal: { - frames: ['▏', '▎', '▍', '▌', '▋', '▊', '▉', '▊', '▋', '▌', '▍', '▎'], - interval: 120, - }, - growVertical: { - frames: ['▁', '▃', '▄', '▅', '▆', '▇', '▆', '▅', '▄', '▃'], - interval: 120, - }, - hamburger: { - frames: ['☱', '☲', '☴'], - interval: 100, - }, - hearts: { - frames: ['💛 ', '💙 ', '💜 ', '💚 ', '❤️ '], - interval: 100, - }, - hexagon: { - frames: ['⬡', '⬢'], - interval: 400, - }, - line: { - frames: ['-', '\\', '|', '/'], - interval: 130, - }, - line2: { - frames: ['⠂', '-', '–', '—', '–', '-'], - interval: 100, - }, - monkey: { - frames: ['🙈 ', '🙈 ', '🙉 ', '🙊 '], - interval: 300, - }, - moon: { - frames: ['🌑 ', '🌒 ', '🌓 ', '🌔 ', '🌕 ', '🌖 ', '🌗 ', '🌘 '], - interval: 80, - }, - noise: { - frames: ['▓', '▒', '░'], - interval: 100, - }, - pipe: { - frames: ['┤', '┘', '┴', '└', '├', '┌', '┬', '┐'], - interval: 100, - }, - pong: { - frames: [ - '▐⠂ ▌', - '▐⠈ ▌', - '▐ ⠂ ▌', - '▐ ⠠ ▌', - '▐ ⡀ ▌', - '▐ ⠠ ▌', - '▐ ⠂ ▌', - '▐ ⠈ ▌', - '▐ ⠂ ▌', - '▐ ⠠ ▌', - '▐ ⡀ ▌', - '▐ ⠠ ▌', - '▐ ⠂ ▌', - '▐ ⠈ ▌', - '▐ ⠂▌', - '▐ ⠠▌', - '▐ ⡀▌', - '▐ ⠠ ▌', - '▐ ⠂ ▌', - '▐ ⠈ ▌', - '▐ ⠂ ▌', - '▐ ⠠ ▌', - '▐ ⡀ ▌', - '▐ ⠠ ▌', - '▐ ⠂ ▌', - '▐ ⠈ ▌', - '▐ ⠂ ▌', - '▐ ⠠ ▌', - '▐ ⡀ ▌', - '▐⠠ ▌', - ], - interval: 80, - }, - runner: { - frames: ['🚶 ', '🏃 '], - interval: 140, - }, - simpleDots: { - frames: ['. ', '.. ', '...', ' '], - interval: 400, - }, - simpleDotsScrolling: { - frames: ['. ', '.. ', '...', ' ..', ' .', ' '], - interval: 200, - }, - smiley: { - frames: ['😄 ', '😝 '], - interval: 200, - }, - squareCorners: { - frames: ['◰', '◳', '◲', '◱'], - interval: 180, - }, - squish: { - frames: ['╫', '╪'], - interval: 100, - }, - star: { - frames: ['✶', '✸', '✹', '✺', '✹', '✷'], - interval: 70, - }, - star2: { - frames: ['+', 'x', '*'], - interval: 80, - }, - toggle: { - frames: ['⊶', '⊷'], - interval: 250, - }, - toggle2: { - frames: ['▫', '▪'], - interval: 80, - }, - toggle3: { - frames: ['□', '■'], - interval: 120, - }, - toggle4: { - frames: ['■', '□', '▪', '▫'], - interval: 100, - }, - toggle5: { - frames: ['▮', '▯'], - interval: 100, - }, - toggle6: { - frames: ['ဝ', '၀'], - interval: 300, - }, - toggle7: { - frames: ['⦾', '⦿'], - interval: 80, - }, - toggle8: { - frames: ['◍', '◌'], - interval: 100, - }, - toggle9: { - frames: ['◉', '◎'], - interval: 100, - }, - toggle10: { - frames: ['㊂', '㊀', '㊁'], - interval: 100, - }, - toggle11: { - frames: ['⧇', '⧆'], - interval: 50, - }, - toggle12: { - frames: ['☗', '☖'], - interval: 120, - }, - toggle13: { - frames: ['=', '*', '-'], - interval: 80, - }, - triangle: { - frames: ['◢', '◣', '◤', '◥'], - interval: 50, - }, -} diff --git a/src/cli-ux/action/types.ts b/src/cli-ux/action/types.ts deleted file mode 100644 index 7fe562ed1..000000000 --- a/src/cli-ux/action/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import spinners from './spinners' - -export type Options = { - stdout?: boolean - style?: keyof typeof spinners -} diff --git a/src/cli-ux/config.ts b/src/cli-ux/config.ts deleted file mode 100644 index a2c9ddaa7..000000000 --- a/src/cli-ux/config.ts +++ /dev/null @@ -1,67 +0,0 @@ -import Cache from '../cache' -import {ActionBase} from './action/base' -import simple from './action/simple' -import spinner from './action/spinner' - -export type Levels = 'debug' | 'error' | 'fatal' | 'info' | 'trace' | 'warn' - -export interface ConfigMessage { - prop: string - type: 'config' - 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 Action = actionType === 'spinner' ? spinner : simple - -/** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ -export class Config { - action: ActionBase = new Action() - - errorsHandled = false - - outputLevel: Levels = 'info' - - showStackTrace = true - - get context(): any { - return globals.context || {} - } - - set context(v: unknown) { - globals.context = v - } - - get debug(): boolean { - return globals.debug || process.env.DEBUG === '*' - } - - set debug(v: boolean) { - globals.debug = v - } -} - -function fetch() { - const core = Cache.getInstance().get('@oclif/core') - const major = core?.version.split('.')[0] || 'unknown' - if (globals[major]) return globals[major] - globals[major] = new Config() - return globals[major] -} - -/** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ -export const config: Config = fetch() -export default config diff --git a/src/cli-ux/exit.ts b/src/cli-ux/exit.ts deleted file mode 100644 index 4a23e4e68..000000000 --- a/src/cli-ux/exit.ts +++ /dev/null @@ -1,17 +0,0 @@ -export class ExitError extends Error { - public code: 'EEXIT' - - public error?: Error - - public ux: { - exit: number - } - - constructor(status: number, error?: Error) { - const code = 'EEXIT' - super(error ? error.message : `${code}: ${status}`) - this.error = error - this.ux = {exit: status} - this.code = code - } -} diff --git a/src/cli-ux/global.d.ts b/src/cli-ux/global.d.ts deleted file mode 100644 index 503b2265d..000000000 --- a/src/cli-ux/global.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare namespace NodeJS { - interface Global { - ux: any - } -} diff --git a/src/cli-ux/index.ts b/src/cli-ux/index.ts deleted file mode 100644 index d85026a59..000000000 --- a/src/cli-ux/index.ts +++ /dev/null @@ -1,267 +0,0 @@ -import chalk from 'chalk' -import {format as utilFormat} from 'node:util' - -import * as Errors from '../errors' -import {ActionBase} from './action/base' -import {config} from './config' -import {flush as _flush} from './flush' -import * as uxPrompt from './prompt' -import * as styled from './styled' -import uxWait from './wait' -import write from './write' -const hyperlinker = require('hyperlinker') - -export class ux { - public static config = config - - public static get action(): ActionBase { - return config.action - } - - public static annotation(text: string, annotation: string): void { - const supports = require('supports-hyperlinks') - if (supports.stdout) { - // \u001b]8;;https://google.com\u0007sometext\u001b]8;;\u0007 - this.log(`\u001B]1337;AddAnnotation=${text.length}|${annotation}\u0007${text}`) - } else { - this.log(text) - } - } - - /** - * "press anykey to continue" - */ - public static get anykey(): typeof uxPrompt.anykey { - return uxPrompt.anykey - } - - public static get confirm(): typeof uxPrompt.confirm { - return uxPrompt.confirm - } - - public static debug(format: string, ...args: string[]): void { - if (['debug', 'trace'].includes(this.config.outputLevel)) { - this.info(utilFormat(format, ...args) + '\n') - } - } - - public static async done(): Promise { - config.action.stop() - } - - public static async flush(ms = 10_000): Promise { - await _flush(ms) - } - - public static info(format: string, ...args: string[]): void { - write.stdout(utilFormat(format, ...args) + '\n') - } - - public static log(format?: string, ...args: string[]): void { - this.info(format || '', ...args) - } - - public static logToStderr(format?: string, ...args: string[]): void { - write.stderr(utilFormat(format, ...args) + '\n') - } - - public static get progress(): typeof styled.progress { - return styled.progress - } - - public static get prompt(): typeof uxPrompt.prompt { - return uxPrompt.prompt - } - - public static styledHeader(header: string): void { - this.info(chalk.dim('=== ') + chalk.bold(header) + '\n') - } - - public static styledJSON(obj: unknown): void { - const json = JSON.stringify(obj, null, 2) - if (!chalk.level) { - this.info(json) - return - } - - const cardinal = require('cardinal') - const theme = require('cardinal/themes/jq') - this.info(cardinal.highlight(json, {json: true, theme})) - } - - public static styledObject(obj: any, keys?: string[]): void { - this.info(styled.styledObject(obj, keys)) - } - - public static get table(): typeof styled.Table.table { - return styled.Table.table - } - - public static trace(format: string, ...args: string[]): void { - if (this.config.outputLevel === 'trace') { - this.info(utilFormat(format, ...args) + '\n') - } - } - - public static get tree(): typeof styled.tree { - return styled.tree - } - - public static url(text: string, uri: string, params = {}): void { - const supports = require('supports-hyperlinks') - if (supports.stdout) { - this.log(hyperlinker(text, uri, params)) - } else { - this.log(uri) - } - } - - public static get wait(): typeof uxWait { - return uxWait - } -} - -const { - action, - annotation, - anykey, - confirm, - debug, - done, - flush, - info, - log, - logToStderr, - progress, - prompt, - styledHeader, - styledJSON, - styledObject, - table, - trace, - tree, - url, - wait, -} = ux - -const {error, exit, warn} = Errors - -export { - /** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ - action, - /** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ - annotation, - /** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ - anykey, - /** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ - confirm, - /** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ - debug, - /** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ - done, - /** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ - error, - /** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ - exit, - /** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ - flush, - /** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ - info, - /** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ - log, - /** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ - logToStderr, - /** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ - progress, - /** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ - prompt, - /** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ - styledHeader, - /** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ - styledJSON, - /** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ - styledObject, - /** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ - table, - /** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ - trace, - /** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ - tree, - /** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ - url, - /** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ - wait, - /** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ - warn, -} - -const uxProcessExitHandler = async () => { - try { - await ux.done() - } catch (error) { - console.error(error) - process.exitCode = 1 - } -} - -// to avoid MaxListenersExceededWarning -// only attach named listener once -const uxListener = process.listeners('exit').find((fn) => fn.name === uxProcessExitHandler.name) -if (!uxListener) { - process.once('exit', uxProcessExitHandler) -} - -export {ActionBase} from './action/base' -export {Config, config} from './config' -export {ExitError} from './exit' -export {IPromptOptions} from './prompt' -export {Table} from './styled' - -export {colorize} from './theme' -export {default as write} from './write' diff --git a/src/cli-ux/prompt.ts b/src/cli-ux/prompt.ts deleted file mode 100644 index 0b7560e23..000000000 --- a/src/cli-ux/prompt.ts +++ /dev/null @@ -1,187 +0,0 @@ -import chalk from 'chalk' - -import * as Errors from '../errors' -import {config} from './config' - -/** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ -export interface IPromptOptions { - default?: string - prompt?: string - /** - * Requires user input if true, otherwise allows empty input - */ - required?: boolean - timeout?: number - type?: 'hide' | 'mask' | 'normal' | 'single' -} - -interface IPromptConfig { - default?: string - isTTY: boolean - name: string - prompt: string - required: boolean - timeout?: number - type: 'hide' | 'mask' | 'normal' | 'single' -} - -function normal(options: IPromptConfig, retries = 100): Promise { - if (retries < 0) throw new Error('no input') - return new Promise((resolve, reject) => { - let timer: NodeJS.Timeout - if (options.timeout) { - timer = setTimeout(() => { - process.stdin.pause() - reject(new Error('Prompt timeout')) - }, options.timeout) - timer.unref() - } - - process.stdin.setEncoding('utf8') - process.stderr.write(options.prompt) - process.stdin.resume() - 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)) - } - }) - }) -} - -function getPrompt(name: string, type?: string, defaultValue?: string) { - let prompt = '> ' - - if (defaultValue && type === 'hide') { - defaultValue = '*'.repeat(defaultValue.length) - } - - if (name && defaultValue) prompt = name + ' ' + chalk.yellow('[' + defaultValue + ']') + ': ' - else if (name) prompt = `${name}: ` - - return prompt -} - -async function single(options: IPromptConfig): Promise { - const raw = process.stdin.isRaw - if (process.stdin.setRawMode) process.stdin.setRawMode(true) - options.required = options.required ?? false - const response = await normal(options) - if (process.stdin.setRawMode) process.stdin.setRawMode(Boolean(raw)) - return response -} - -function replacePrompt(prompt: string) { - const ansiEscapes = require('ansi-escapes') - process.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 { - const prompt = getPrompt(name, inputOptions.type, inputOptions.default) - const options: IPromptConfig = { - default: '', - isTTY: Boolean(process.env.TERM !== 'dumb' && process.stdin.isTTY), - name, - prompt, - required: true, - type: 'normal', - ...inputOptions, - } - const passwordPrompt = require('password-prompt') - - switch (options.type) { - case 'normal': { - return normal(options) - } - - case 'single': { - return single(options) - } - - case 'mask': { - return passwordPrompt(options.prompt, { - default: options.default, - method: options.type, - required: options.required, - }).then((value: string) => { - replacePrompt(getPrompt(name, 'hide', inputOptions.default)) - return value - }) - } - - case 'hide': { - return passwordPrompt(options.prompt, { - default: options.default, - method: options.type, - required: options.required, - }) - } - - default: { - throw new Error(`unexpected type ${options.type}`) - } - } -} - -/** - * prompt for input - * @param name - prompt text - * @param options - @see IPromptOptions - * @returns Promise - */ -export async function prompt(name: string, options: IPromptOptions = {}): Promise { - return config.action.pauseAsync(() => _prompt(name, options), chalk.cyan('?')) -} - -/** - * confirmation prompt (yes/no) - * @param message - confirmation text - * @returns Promise - */ -export function confirm(message: string): Promise { - return config.action.pauseAsync(async () => { - const confirm = async (): Promise => { - const raw = await _prompt(message) - const response = raw.toLowerCase() - if (['n', 'no'].includes(response)) return false - if (['y', 'yes'].includes(response)) return true - return confirm() - } - - return confirm() - }, chalk.cyan('?')) -} - -/** - * "press anykey to continue" - * @param message - optional message to display to user - * @returns Promise - */ -export async function anykey(message?: string): Promise { - const tty = Boolean(process.stdin.setRawMode) - if (!message) { - message = tty - ? `Press any key to continue or ${chalk.yellow('q')} to exit` - : `Press enter to continue or ${chalk.yellow('q')} to exit` - } - - const char = await prompt(message, {required: false, type: 'single'}) - if (tty) process.stderr.write('\n') - if (char === 'q') Errors.error('quit') - if (char === '\u0003') Errors.error('ctrl-c') - return char -} diff --git a/src/cli-ux/stream.ts b/src/cli-ux/stream.ts deleted file mode 100644 index 1172c8b9d..000000000 --- a/src/cli-ux/stream.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * A wrapper around process.stdout and process.stderr that allows us to mock out the streams for testing. - */ -class Stream { - public constructor(public channel: 'stderr' | 'stdout') {} - - public get isTTY(): boolean { - return process[this.channel].isTTY - } - - public emit(event: string, ...args: any[]): boolean { - return process[this.channel].emit(event, ...args) - } - - public getWindowSize(): number[] { - return process[this.channel].getWindowSize() - } - - public on(event: string, listener: (...args: any[]) => void): Stream { - process[this.channel].on(event, listener) - return this - } - - public once(event: string, listener: (...args: any[]) => void): Stream { - process[this.channel].once(event, listener) - return this - } - - public read(): boolean { - return process[this.channel].read() - } - - public write(data: string): boolean { - return process[this.channel].write(data) - } -} - -/** - * @deprecated Use process.stdout directly. This will be removed in the next major version - */ -export const stdout = new Stream('stdout') -/** - * @deprecated Use process.stderr directly. This will be removed in the next major version - */ -export const stderr = new Stream('stderr') diff --git a/src/cli-ux/styled/index.ts b/src/cli-ux/styled/index.ts deleted file mode 100644 index 768a59be6..000000000 --- a/src/cli-ux/styled/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export {default as styledObject} from './object' -export {default as progress} from './progress' -export * as Table from './table' -export {default as tree} from './tree' diff --git a/src/cli-ux/styled/object.ts b/src/cli-ux/styled/object.ts deleted file mode 100644 index 272f66bcc..000000000 --- a/src/cli-ux/styled/object.ts +++ /dev/null @@ -1,37 +0,0 @@ -import chalk from 'chalk' -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 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(', ') - } - - return inspect(obj) - } - - 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] - if (Array.isArray(value)) { - if (value.length > 0) { - output.push(logKeyValue(key, value[0])) - for (const e of value.slice(1)) { - output.push(' '.repeat(maxKeyLength) + pp(e)) - } - } - } else if (value !== null && value !== undefined) { - output.push(logKeyValue(key, value)) - } - } - - return output.join('\n') -} diff --git a/src/cli-ux/styled/progress.ts b/src/cli-ux/styled/progress.ts deleted file mode 100644 index d680ee986..000000000 --- a/src/cli-ux/styled/progress.ts +++ /dev/null @@ -1,6 +0,0 @@ -// 3pp -import {Options, SingleBar} from 'cli-progress' - -export default function progress(options: Options = {}): SingleBar { - return new SingleBar({noTTYOutput: Boolean(process.env.TERM === 'dumb' || !process.stdin.isTTY), ...options}) -} diff --git a/src/cli-ux/styled/table.ts b/src/cli-ux/styled/table.ts deleted file mode 100644 index a43f3cb47..000000000 --- a/src/cli-ux/styled/table.ts +++ /dev/null @@ -1,410 +0,0 @@ -import chalk from 'chalk' -import {safeDump} from 'js-yaml' -import {orderBy} from 'natural-orderby' -import {inspect} from 'node:util' -import sliceAnsi from 'slice-ansi' -import sw from 'string-width' - -import * as F from '../../flags' -import * as Interfaces from '../../interfaces' -import {stdtermwidth} from '../../screen' -import {capitalize, sumBy} from '../../util/util' -import write from '../write' - -/** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ -class Table> { - columns: (table.Column & {key: string; maxWidth?: number; width?: number})[] - - options: table.Options & {printLine(s: any): any} - - constructor( - private data: T[], - columns: table.Columns, - options: table.Options = {}, - ) { - // assign columns - this.columns = Object.keys(columns).map((key: string) => { - const col = columns[key] - const extended = col.extended ?? false - // turn null and undefined into empty strings by default - const get = col.get ?? ((row: any) => row[key] ?? '') - const header = typeof col.header === 'string' ? col.header : capitalize(key.replaceAll('_', ' ')) - const minWidth = Math.max(col.minWidth ?? 0, sw(header) + 1) - - return { - extended, - get, - header, - key, - minWidth, - } - }) - - // assign options - const {columns: cols, csv, extended, filter, output, printLine, sort, title} = options - this.options = { - columns: cols, - extended, - filter, - 'no-header': options['no-header'] ?? false, - 'no-truncate': options['no-truncate'] ?? false, - output: csv ? 'csv' : output, - printLine: printLine ?? ((s: any) => write.stdout(s + '\n')), - rowStart: ' ', - sort, - title, - } - } - - display() { - // build table rows from input array data - let rows = this.data.map((d) => { - const row: any = {} - for (const col of this.columns) { - let val = col.get(d) - if (typeof val !== 'string') val = inspect(val, {breakLength: Number.POSITIVE_INFINITY}) - row[col.key] = val - } - - return row - }) - - // filter rows - if (this.options.filter) { - let [header, regex] = this.options.filter!.split('=') - const isNot = header[0] === '-' - if (isNot) header = header.slice(1) - const col = this.findColumnFromHeader(header) - if (!col || !regex) throw new Error('Filter flag has an invalid value') - rows = rows.filter((d: any) => { - const re = new RegExp(regex) - const val = d[col!.key] - const match = val.match(re) - return isNot ? !match : match - }) - } - - // 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')) - rows = orderBy(rows, sortKeys, sortKeysOrder) - } - - // and filter columns - if (this.options.columns) { - const filters = this.options.columns!.split(',') - this.columns = this.filterColumnsFromHeaders(filters) - } else if (!this.options.extended) { - // show extended columns/properties - this.columns = this.columns.filter((c) => !c.extended) - } - - this.data = rows - - switch (this.options.output) { - case 'csv': { - this.outputCSV() - break - } - - case 'json': { - this.outputJSON() - break - } - - case 'yaml': { - this.outputYAML() - break - } - - default: { - this.outputTable() - } - } - } - - private filterColumnsFromHeaders( - filters: string[], - ): (table.Column & {key: string; maxWidth?: number; width?: number})[] { - // unique - filters = [...new Set(filters)] - const cols: (table.Column & {key: string; maxWidth?: number; width?: number})[] = [] - for (const f of filters) { - const c = this.columns.find((c) => c.header.toLowerCase() === f.toLowerCase()) - if (c) cols.push(c) - } - - return cols - } - - private findColumnFromHeader( - header: string, - ): (table.Column & {key: string; maxWidth?: number; width?: number}) | undefined { - return this.columns.find((c) => c.header.toLowerCase() === header.toLowerCase()) - } - - 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.replaceAll('"', '""')}"` : e)) - } - - private outputCSV() { - const {columns, data, options} = this - - if (!options['no-header']) { - options.printLine(columns.map((c) => c.header).join(',')) - } - - for (const d of data) { - const row = this.getCSVRow(d) - options.printLine(row.join(',')) - } - } - - private outputJSON() { - this.options.printLine(JSON.stringify(this.resolveColumnsToObjectArray(), undefined, 2)) - } - - private outputTable() { - const {data, options} = this - // column truncation - // - // find max width for each column - 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, - maxWidth, - width: maxWidth, - } - }) - - // terminal width - const maxWidth = stdtermwidth - 2 - // truncation logic - const shouldShorten = () => { - // don't shorten if full mode - if (options['no-truncate'] || (!process.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 overWidth = dataMaxWidth - maxWidth - if (overWidth <= 0) return - - // not enough room, short all columns to minWidth - for (const col of columns) { - col.width = col.minWidth - } - - // 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!) - 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) - for (const {key, needs} of needyCols) { - if (!needs) continue - const col = columns.find((c) => key === c.key) - if (!col) continue - if (wiggleRoom > needs) { - col.width = col.width! + needs - wiggleRoom -= needs - } else if (wiggleRoom) { - col.width = col.width! + wiggleRoom - wiggleRoom = 0 - } - } - } - - shouldShorten() - - // print table title - if (options.title) { - options.printLine(options.title) - // print title divider - options.printLine( - ''.padEnd( - columns.reduce((sum, col) => sum + col.width!, 1), - '=', - ), - ) - - options.rowStart = '| ' - } - - // print headers - if (!options['no-header']) { - let headers = options.rowStart - for (const col of columns) { - const header = col.header! - headers += header.padEnd(col.width!) - } - - options.printLine(chalk.bold(headers)) - - // print header dividers - let dividers = options.rowStart - for (const col of columns) { - const divider = ''.padEnd(col.width! - 1, '─') + ' ' - dividers += divider.padEnd(col.width!) - } - - options.printLine(chalk.bold(dividers)) - } - - // print rows - for (const row of data) { - // find max number of lines - // for all cells in a row - // with multi-line strings - let numOfLines = 1 - for (const col of columns) { - const d = (row as any)[col.key] - const lines = d.split('\n').length - if (lines > numOfLines) numOfLines = lines - } - - // eslint-disable-next-line unicorn/no-new-array - const linesIndexess = [...new Array(numOfLines).keys()] - - // print row - // including multi-lines - for (const i of linesIndexess) { - let l = options.rowStart - for (const col of columns) { - const width = col.width! - let d = (row as any)[col.key] - d = d.split('\n')[i] || '' - const visualWidth = sw(d) - const colorWidth = d.length - visualWidth - let cell = d.padEnd(width + colorWidth) - 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) + '… ' - // pad with spaces; this is necessary in case the original string - // contained fullwidth characters which cannot be split - cell += ' '.repeat(width - sw(cell)) - } - - l += cell - } - - options.printLine(l) - } - } - } - - private outputYAML() { - this.options.printLine(safeDump(this.resolveColumnsToObjectArray())) - } - - private resolveColumnsToObjectArray() { - const {columns, data} = this - return data.map((d: any) => Object.fromEntries(columns.map((col) => [col.key, d[col.key] ?? '']))) - } -} - -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 - csv: Interfaces.BooleanFlag - extended: Interfaces.BooleanFlag - filter: Interfaces.OptionFlag - 'no-header': Interfaces.BooleanFlag - 'no-truncate': Interfaces.BooleanFlag - output: Interfaces.OptionFlag - sort: Interfaces.OptionFlag - } = { - columns: F.string({description: 'only show provided columns (comma-separated)', exclusive: ['extended']}), - csv: F.boolean({description: 'output is csv format [alias: --output=csv]', exclusive: ['no-truncate']}), - extended: F.boolean({char: 'x', description: 'show extra columns', exclusive: ['columns']}), - filter: F.string({description: 'filter property by partial string matching, ex: name=foo'}), - 'no-header': F.boolean({description: 'hide table header from output', exclusive: ['csv']}), - 'no-truncate': F.boolean({description: 'do not truncate output to fit screen', exclusive: ['csv']}), - output: F.string({ - description: 'output in a more machine friendly format', - exclusive: ['no-truncate', 'csv'], - options: ['csv', 'json', 'yaml'], - }), - sort: F.string({description: "property to sort by (prepend '-' for descending)"}), - } - - type IFlags = typeof Flags - type ExcludeFlags = Pick> - 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?: any): any { - if (opts) { - const f = {} - const o = (opts.only && typeof opts.only === 'string' ? [opts.only] : opts.only) || Object.keys(Flags) - 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] - } - } - - return f - } - - return Flags - } - - export interface Column> { - extended: boolean - get(row: T): any - header: string - minWidth: number - } - - export type Columns> = {[key: string]: Partial>} - - // export type OutputType = 'csv' | 'json' | 'yaml' - - export interface Options { - [key: string]: any - columns?: string - extended?: boolean - filter?: string - 'no-header'?: boolean - 'no-truncate'?: boolean - output?: string - printLine?(s: any): any - sort?: string - } -} - -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 deleted file mode 100644 index 46017a9e8..000000000 --- a/src/cli-ux/styled/tree.ts +++ /dev/null @@ -1,39 +0,0 @@ -const treeify = require('object-treeify') - -export class Tree { - nodes: {[key: string]: Tree} = {} - - display(logger: any = console.log): void { - const addNodes = function (nodes: any) { - const tree: {[key: string]: any} = {} - for (const p of Object.keys(nodes)) { - tree[p] = addNodes(nodes[p].nodes) - } - - return tree - } - - const tree = addNodes(this.nodes) - logger(treeify(tree)) - } - - insert(child: string, value: Tree = new Tree()): Tree { - this.nodes[child] = value - return this - } - - search(key: string): Tree | undefined { - for (const child of Object.keys(this.nodes)) { - if (child === key) { - return this.nodes[child] - } - - const c = this.nodes[child].search(key) - if (c) return c - } - } -} - -export default function tree(): Tree { - return new Tree() -} diff --git a/src/cli-ux/theme.ts b/src/cli-ux/theme.ts deleted file mode 100644 index 02e81ca8b..000000000 --- a/src/cli-ux/theme.ts +++ /dev/null @@ -1,37 +0,0 @@ -import chalk from 'chalk' -import * as Color from 'color' - -import {STANDARD_CHALK, StandardChalk, Theme} from '../interfaces/theme' - -function isStandardChalk(color: any): color is StandardChalk { - return STANDARD_CHALK.includes(color) -} - -/** - * Add color to text. - * @param color color to use. Can be hex code (e.g. `#ff0000`), rgb (e.g. `rgb(255, 255, 255)`) or a chalk color (e.g. `red`) - * @param text string to colorize - * @returns colorized string - */ -export function colorize(color: string | StandardChalk | undefined, text: string): string { - if (isStandardChalk(color)) return chalk[color](text) - - return color ? chalk.hex(color)(text) : text -} - -export function parseTheme(theme: Record): Theme { - return Object.fromEntries( - Object.entries(theme) - .map(([key, value]) => [key, getColor(value)]) - .filter(([_, value]) => value), - ) -} - -export function getColor(color: string): string -export function getColor(color: StandardChalk): StandardChalk -export function getColor(color: string | StandardChalk): string | StandardChalk | undefined { - try { - // eslint-disable-next-line new-cap - return isStandardChalk(color) ? color : new Color.default(color).hex() - } catch {} -} diff --git a/src/cli-ux/wait.ts b/src/cli-ux/wait.ts deleted file mode 100644 index ecf28356f..000000000 --- a/src/cli-ux/wait.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default (ms = 1000): Promise => - new Promise((resolve) => { - setTimeout(resolve, ms) - }) diff --git a/src/cli-ux/write.ts b/src/cli-ux/write.ts deleted file mode 100644 index 56c195270..000000000 --- a/src/cli-ux/write.ts +++ /dev/null @@ -1,15 +0,0 @@ -const stdout = (msg: string): void => { - process.stdout.write(msg) -} - -const stderr = (msg: string): void => { - process.stderr.write(msg) -} - -/** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ -export default { - stderr, - stdout, -} diff --git a/src/command.ts b/src/command.ts index 392bb6e7d..c75281a44 100644 --- a/src/command.ts +++ b/src/command.ts @@ -1,9 +1,8 @@ -import chalk from 'chalk' +import ansis from 'ansis' import {fileURLToPath} from 'node:url' import {inspect} from 'node:util' import Cache from './cache' -import {ux} from './cli-ux' import {Config} from './config' import * as Errors from './errors' import {PrettyPrintableError} from './errors' @@ -29,6 +28,7 @@ import * as Parser from './parser' import {aggregateFlags} from './util/aggregate-flags' import {toConfiguredId} from './util/ids' import {uniq} from './util/util' +import ux from './ux' const pjson = Cache.getInstance().get('@oclif/core') @@ -192,7 +192,7 @@ export abstract class Command { } else { if (!err.message) throw err try { - ux.action.stop(chalk.bold.red('!')) + ux.action.stop(ansis.bold.red('!')) } catch {} throw err @@ -257,18 +257,18 @@ export abstract class Command { public log(message = '', ...args: any[]): void { if (!this.jsonEnabled()) { message = typeof message === 'string' ? message : inspect(message) - ux.info(message, ...args) + ux.stdout(message, ...args) } } protected logJson(json: unknown): void { - ux.styledJSON(json) + ux.stdout(ux.colorizeJson(json, {pretty: true, theme: this.config.theme?.json})) } public logToStderr(message = '', ...args: any[]): void { if (!this.jsonEnabled()) { message = typeof message === 'string' ? message : inspect(message) - ux.logToStderr(message, ...args) + ux.stderr(message, ...args) } } diff --git a/src/config/config.ts b/src/config/config.ts index b32c2fe9f..0bc59c16b 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -5,8 +5,6 @@ import {join, resolve, sep} from 'node:path' import {URL, fileURLToPath} from 'node:url' import Cache from '../cache' -import {ux} from '../cli-ux' -import {parseTheme} from '../cli-ux/theme' import {Command} from '../command' import {CLIError, error, exit, warn} from '../errors' import {getHelpFlagAdditions} from '../help/util' @@ -20,6 +18,8 @@ import {settings} from '../settings' import {safeReadJson} from '../util/fs' import {getHomeDir, getPlatform} from '../util/os' import {compact, isProd} from '../util/util' +import ux from '../ux' +import {parseTheme} from '../ux/theme' import PluginLoader from './plugin-loader' import {tsPath} from './ts-path' import {Debug, collectUsableIds, getCommandIdPermutations} from './util' @@ -97,11 +97,13 @@ export class Config implements IConfig { public shell!: string public theme?: Theme public topicSeparator: ' ' | ':' = ':' + public updateConfig!: NonNullable public userAgent!: string public userPJSON?: PJSON.User public valid!: boolean public version!: string protected warned = false + public windows!: boolean private _base = BASE @@ -117,11 +119,9 @@ export class Config implements IConfig { private pluginLoader!: PluginLoader private rootPlugin!: IPlugin - private topicPermutations = new Permutations() constructor(public options: Options) {} - static async load(opts: LoadOptions = module.filename || __dirname): Promise { // Handle the case when a file URL string is passed in such as 'import.meta.url'; covert to file path. if (typeof opts === 'string' && opts.startsWith('file://')) { @@ -201,7 +201,6 @@ export class Config implements IConfig { } 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 { @@ -333,37 +332,14 @@ export class Config implements IConfig { this.npmRegistry = this.scopedEnvVar('NPM_REGISTRY') || this.pjson.oclif.npmRegistry - if (!this.scopedEnvVarTrue('DISABLE_THEME')) { - const {theme} = await this.loadThemes() - this.theme = theme - } + this.theme = await this.loadTheme() - this.pjson.oclif.update = this.pjson.oclif.update || {} - this.pjson.oclif.update.node = this.pjson.oclif.update.node || {} - const s3 = this.pjson.oclif.update.s3 || {} - this.pjson.oclif.update.s3 = s3 - s3.bucket = this.scopedEnvVar('S3_BUCKET') || s3.bucket - if (s3.bucket && !s3.host) s3.host = `https://${s3.bucket}.s3.amazonaws.com` - s3.templates = { - ...s3.templates, - target: { - baseDir: '<%- bin %>', - manifest: "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- platform %>-<%- arch %>", - unversioned: - "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %>-<%- platform %>-<%- arch %><%- ext %>", - versioned: - "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %>-v<%- version %>/<%- bin %>-v<%- version %>-<%- platform %>-<%- arch %><%- ext %>", - ...(s3.templates && s3.templates.target), - }, - vanilla: { - baseDir: '<%- bin %>', - manifest: "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %>version", - unversioned: "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %><%- ext %>", - versioned: - "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %>-v<%- version %>/<%- bin %>-v<%- version %><%- ext %>", - ...(s3.templates && s3.templates.vanilla), - }, + this.updateConfig = { + ...this.pjson.oclif.update, + node: this.pjson.oclif.update?.node ?? {}, + s3: this.buildS3Config(), } + this.isSingleCommandCLI = Boolean( this.pjson.oclif.default || (typeof this.pjson.oclif.commands !== 'string' && @@ -410,10 +386,8 @@ export class Config implements IConfig { } } - public async loadThemes(): Promise<{ - file: string | undefined - theme: Theme | undefined - }> { + public async loadTheme(): Promise { + if (this.scopedEnvVarTrue('DISABLE_THEME')) return const defaultThemeFile = this.pjson.oclif.theme ? resolve(this.root, this.pjson.oclif.theme) : this.pjson.oclif.theme @@ -426,12 +400,7 @@ export class Config implements IConfig { // Merge the default theme with the user theme, giving the user theme precedence. const merged = {...defaultTheme, ...userTheme} - return { - // Point to the user file if it exists, otherwise use the default file. - // This doesn't really serve a purpose to anyone but removing it would be a breaking change. - file: userTheme ? userThemeFile : defaultThemeFile, - theme: Object.keys(merged).length > 0 ? parseTheme(merged) : undefined, - } + return Object.keys(merged).length > 0 ? parseTheme(merged) : undefined } protected macosCacheDir(): string | undefined { @@ -545,7 +514,7 @@ export class Config implements IConfig { exit(code) }, log(message?: any, ...args: any[]) { - ux.info(message, ...args) + ux.stdout(message, ...args) }, warn(message: string) { warn(message) @@ -618,12 +587,12 @@ export class Config implements IConfig { ): 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] ?? '' + const template = this.updateConfig.s3.templates[options.platform ? 'target' : 'vanilla'][type] ?? '' return ejs.render(template, {...(this as any), ...options}) } public s3Url(key: string): string { - const {host} = this.pjson.oclif.update.s3 + const {host} = this.updateConfig.s3 ?? {host: undefined} if (!host) throw new Error('no s3 host is set') const url = new URL(host) url.pathname = join(url.pathname, key) @@ -737,6 +706,37 @@ export class Config implements IConfig { return shellPath.at(-1) ?? 'unknown' } + private buildS3Config() { + const s3 = this.pjson.oclif.update?.s3 + const bucket = this.scopedEnvVar('S3_BUCKET') ?? s3?.bucket + const host = s3?.host ?? (bucket && `https://${bucket}.s3.amazonaws.com`) + const templates = { + ...s3?.templates, + target: { + baseDir: '<%- bin %>', + manifest: "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- platform %>-<%- arch %>", + unversioned: + "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %>-<%- platform %>-<%- arch %><%- ext %>", + versioned: + "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %>-v<%- version %>/<%- bin %>-v<%- version %>-<%- platform %>-<%- arch %><%- ext %>", + ...(s3?.templates && s3?.templates.target), + }, + vanilla: { + baseDir: '<%- bin %>', + manifest: "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %>version", + unversioned: "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %><%- ext %>", + versioned: + "<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %>-v<%- version %>/<%- bin %>-v<%- version %><%- ext %>", + ...(s3?.templates && s3?.templates.vanilla), + }, + } + return { + bucket, + host, + templates, + } + } + /** * This method is responsible for locating the correct plugin to use for a named command id * It searches the {Config} registered commands to match either the raw command id or the command alias diff --git a/src/config/ts-path.ts b/src/config/ts-path.ts index a37a094ff..b06a38811 100644 --- a/src/config/ts-path.ts +++ b/src/config/ts-path.ts @@ -3,7 +3,7 @@ import {join, relative as pathRelative, sep} from 'node:path' import * as TSNode from 'ts-node' import Cache from '../cache' -import {memoizedWarn} from '../errors/warn' +import {warn} from '../errors/warn' import {Plugin, TSConfig} from '../interfaces' import {settings} from '../settings' import {existsSync} from '../util/fs' @@ -72,7 +72,7 @@ async function loadTSConfig(root: string): Promise { if (isErrno(error)) return debug(`Could not parse tsconfig.json. Skipping typescript path lookup for ${root}.`) - memoizedWarn(`Could not parse tsconfig.json for ${root}. Falling back to compiled source.`) + warn(`Could not parse tsconfig.json for ${root}. Falling back to compiled source.`) } } @@ -88,7 +88,7 @@ async function registerTSNode(root: string, tsconfig: TSConfig): Promise { tsNode = require(tsNodePath) } catch { debug(`Could not find ts-node at ${tsNodePath}. Skipping ts-node registration for ${root}.`) - memoizedWarn( + warn( `Could not find ts-node at ${tsNodePath}. Please ensure that ts-node is a devDependency. Falling back to compiled source.`, ) return @@ -235,7 +235,7 @@ async function determinePath(root: string, orig: string): Promise { } debug(`No source file found. Returning default path ${orig}`) - if (!isProd()) memoizedWarn(`Could not find source for ${orig} based on tsconfig. Defaulting to compiled source.`) + if (!isProd()) warn(`Could not find source for ${orig} based on tsconfig. Defaulting to compiled source.`) return orig } @@ -275,7 +275,7 @@ export async function tsPath(root: string, orig: string | undefined, plugin?: Pl `Skipping typescript path lookup for ${root} because it's an ESM module (NODE_ENV: ${process.env.NODE_ENV}, root plugin module type: ${rootPlugin?.moduleType})`, ) if (plugin?.type === 'link') - memoizedWarn( + warn( `${plugin?.name} is a linked ESM module and cannot be auto-transpiled. Existing compiled source will be used instead.`, ) return orig @@ -283,7 +283,7 @@ export async function tsPath(root: string, orig: string | undefined, plugin?: Pl if (cannotUseTsNode(root, plugin, isProduction)) { debug(`Skipping typescript path lookup for ${root} because ts-node is run in node version ${process.version}"`) - memoizedWarn( + warn( `ts-node executable cannot transpile ESM in Node 20. Existing compiled source will be used instead. See https://github.com/oclif/core/issues/817.`, ) return orig diff --git a/src/errors/error.ts b/src/errors/error.ts index fa238f739..c85e3a3ef 100644 --- a/src/errors/error.ts +++ b/src/errors/error.ts @@ -1,5 +1,5 @@ -import write from '../cli-ux/write' import {OclifError, PrettyPrintableError} from '../interfaces' +import {stderr} from '../ux/write' import {config} from './config' import {CLIError, addOclifExitCode} from './errors/cli' import prettyPrint, {applyPrettyPrintOptions} from './errors/pretty-print' @@ -21,7 +21,7 @@ export function error(input: Error | string, options: {exit?: false | number} & if (options.exit === false) { const message = prettyPrint(err) - if (message) write.stderr(message + '\n') + if (message) stderr(message) if (config.errorLogger) config.errorLogger.log(err?.stack ?? '') } else throw err } diff --git a/src/errors/errors/cli.ts b/src/errors/errors/cli.ts index 0852bfef4..cacd5df84 100644 --- a/src/errors/errors/cli.ts +++ b/src/errors/errors/cli.ts @@ -1,4 +1,4 @@ -import chalk from 'chalk' +import ansis from 'ansis' import cs from 'clean-stack' import indent from 'indent-string' import wrap from 'wrap-ansi' @@ -36,7 +36,7 @@ export class CLIError extends Error implements OclifError { get bang(): string | undefined { try { - return chalk.red(process.platform === 'win32' ? '»' : '›') + return ansis.red(process.platform === 'win32' ? '»' : '›') } catch {} } @@ -71,7 +71,7 @@ export namespace CLIError { get bang(): string | undefined { try { - return chalk.yellow(process.platform === 'win32' ? '»' : '›') + return ansis.yellow(process.platform === 'win32' ? '»' : '›') } catch {} } } diff --git a/src/errors/exit.ts b/src/errors/exit.ts new file mode 100644 index 000000000..8e1dc05d6 --- /dev/null +++ b/src/errors/exit.ts @@ -0,0 +1,5 @@ +import {ExitError} from './errors/exit' + +export function exit(code = 0): never { + throw new ExitError(code) +} diff --git a/src/errors/index.ts b/src/errors/index.ts index 18e552291..612318b46 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -1,15 +1,11 @@ -import {ExitError} from './errors/exit' - export {PrettyPrintableError} from '../interfaces' export {config} from './config' export {error} from './error' export {CLIError} from './errors/cli' export {ExitError} from './errors/exit' export {ModuleLoadError} from './errors/module-load' +export {exit} from './exit' export {handle} from './handle' export {Logger} from './logger' -export function exit(code = 0): never { - throw new ExitError(code) -} export {warn} from './warn' diff --git a/src/errors/logger.ts b/src/errors/logger.ts index 961ded8f8..2791a9061 100644 --- a/src/errors/logger.ts +++ b/src/errors/logger.ts @@ -1,8 +1,7 @@ +import ansis from 'ansis' import {appendFile, mkdir} from 'node:fs/promises' import {dirname} from 'node:path' -import stripAnsi = require('strip-ansi') - const timestamp = () => new Date().toISOString() let timer: any const wait = (ms: number) => @@ -36,7 +35,7 @@ export class Logger { } log(msg: string): void { - msg = stripAnsi(chomp(msg)) + msg = ansis.strip(chomp(msg)) const lines = msg.split('\n').map((l) => `${timestamp()} ${l}`.trimEnd()) this.buffer.push(...lines) this.flush(50).catch(console.error) diff --git a/src/errors/warn.ts b/src/errors/warn.ts index 8f6af173f..dd7f4c690 100644 --- a/src/errors/warn.ts +++ b/src/errors/warn.ts @@ -1,10 +1,29 @@ -import write from '../cli-ux/write' import {OclifError} from '../interfaces' +import {stderr} from '../ux/write' import {config} from './config' import {CLIError, addOclifExitCode} from './errors/cli' import prettyPrint from './errors/pretty-print' -export function warn(input: Error | string): void { +const WARNINGS = new Set() + +type Options = { + /** + * If true, will only print the same warning once. + */ + ignoreDuplicates?: boolean +} + +/** + * Prints a pretty warning message to stderr. + * + * @param input The error or string to print. + * @param options.ignoreDuplicates If true, will only print the same warning once. + */ +export function warn(input: Error | string, options?: Options): void { + const ignoreDuplicates = options?.ignoreDuplicates ?? true + if (ignoreDuplicates && WARNINGS.has(input)) return + WARNINGS.add(input) + let err: Error & OclifError if (typeof input === 'string') { @@ -16,15 +35,8 @@ export function warn(input: Error | string): void { } const message = prettyPrint(err) - if (message) write.stderr(message + '\n') + if (message) stderr(message) if (config.errorLogger) config.errorLogger.log(err?.stack ?? '') } -const WARNINGS = new Set() -export function memoizedWarn(input: Error | string): void { - if (!WARNINGS.has(input)) warn(input) - - WARNINGS.add(input) -} - export default warn diff --git a/src/execute.ts b/src/execute.ts index a787aa1fa..148acdd20 100644 --- a/src/execute.ts +++ b/src/execute.ts @@ -1,6 +1,6 @@ -import {flush} from './cli-ux/flush' import {CLIError} from './errors' import {handle} from './errors/handle' +import {flush} from './flush' import {LoadOptions} from './interfaces' import {run} from './main' import {settings} from './settings' diff --git a/src/cli-ux/flush.ts b/src/flush.ts similarity index 94% rename from src/cli-ux/flush.ts rename to src/flush.ts index 2e15540fb..b09d260ee 100644 --- a/src/cli-ux/flush.ts +++ b/src/flush.ts @@ -1,4 +1,4 @@ -import {error} from '../errors' +import {error} from './errors' function timeout(p: Promise, ms: number) { function wait(ms: number, unref = false) { diff --git a/src/help/command.ts b/src/help/command.ts index c047d2d39..1c5085f1b 100644 --- a/src/help/command.ts +++ b/src/help/command.ts @@ -1,12 +1,11 @@ -import chalk from 'chalk' -import stripAnsi from 'strip-ansi' +import ansis from 'ansis' -import {colorize} from '../cli-ux/theme' import {Command} from '../command' import * as Interfaces from '../interfaces' import {ensureArgObject} from '../util/ensure-arg-object' import {toConfiguredId, toStandardizedId} from '../util/ids' import {castArray, compact, sortBy} from '../util/util' +import {colorize} from '../ux/theme' import {DocOpts} from './docopts' import {HelpFormatter, HelpSection, HelpSectionRenderer} from './formatter' @@ -172,7 +171,7 @@ export class CommandHelp extends HelpFormatter { } if (flag.multiple) value += '...' - if (!value.includes('|')) value = chalk.underline(value) + if (!value.includes('|')) value = ansis.underline(value) label += `=${value}` } @@ -377,9 +376,9 @@ export class CommandHelp extends HelpFormatter { } private isCommand(example: string): boolean { - return stripAnsi(this.formatIfCommand(example)).startsWith( - `${colorize(this.config?.theme?.dollarSign, '$')} ${this.config.bin}`, - ) + return ansis + .strip(this.formatIfCommand(example)) + .startsWith(`${colorize(this.config?.theme?.dollarSign, '$')} ${this.config.bin}`) } } export default CommandHelp diff --git a/src/help/formatter.ts b/src/help/formatter.ts index 94f6926c6..c9e6ad5c5 100644 --- a/src/help/formatter.ts +++ b/src/help/formatter.ts @@ -1,14 +1,13 @@ -import chalk from 'chalk' +import ansis from 'ansis' import indent from 'indent-string' import width from 'string-width' -import stripAnsi from 'strip-ansi' import widestLine from 'widest-line' import wrap from 'wrap-ansi' -import {colorize} from '../cli-ux/theme' import {Command} from '../command' import * as Interfaces from '../interfaces' import {stdtermwidth} from '../screen' +import {colorize} from '../ux/theme' import {template} from './util' export type HelpSectionKeyValueTable = {description: string; name: string}[] @@ -89,12 +88,12 @@ export class HelpFormatter { for (let [left, right] of input) { if (!left && !right) continue if (left) { - if (opts.stripAnsi) left = stripAnsi(left) + if (opts.stripAnsi) left = ansis.strip(left) output += this.wrap(left.trim(), opts.indentation) } if (right) { - if (opts.stripAnsi) right = stripAnsi(right) + if (opts.stripAnsi) right = ansis.strip(right) output += '\n' output += this.indent(this.wrap(right.trim(), opts.indentation + 2), 4) } @@ -118,13 +117,13 @@ export class HelpFormatter { } cur = left || '' - if (opts.stripAnsi) cur = stripAnsi(cur) + if (opts.stripAnsi) cur = ansis.strip(cur) if (!right) { cur = cur.trim() continue } - if (opts.stripAnsi) right = stripAnsi(right) + if (opts.stripAnsi) right = ansis.strip(right) right = this.wrap(right.trim(), opts.indentation + maxLength + 2) const [first, ...lines] = right!.split('\n').map((s) => s.trim()) @@ -177,7 +176,7 @@ export class HelpFormatter { } const output = [ - colorize(this.config?.theme?.sectionHeader, chalk.bold(header)), + colorize(this.config?.theme?.sectionHeader, ansis.bold(header)), colorize( this.config?.theme?.sectionDescription, this.indent( @@ -186,7 +185,7 @@ export class HelpFormatter { ), ].join('\n') - return this.opts.stripAnsi ? stripAnsi(output) : output + return this.opts.stripAnsi ? ansis.strip(output) : output } /** diff --git a/src/help/index.ts b/src/help/index.ts index 63635bb0a..2baca5d0d 100644 --- a/src/help/index.ts +++ b/src/help/index.ts @@ -1,8 +1,5 @@ -import {format} from 'node:util' -import stripAnsi from 'strip-ansi' +import ansis from 'ansis' -import {colorize} from '../cli-ux/theme' -import write from '../cli-ux/write' import {Command} from '../command' import {tsPath} from '../config/ts-path' import {error} from '../errors/error' @@ -12,11 +9,12 @@ import {SINGLE_COMMAND_CLI_SYMBOL} from '../symbols' import {cacheDefaultValue} from '../util/cache-default-value' import {toConfiguredId} from '../util/ids' import {compact, sortBy, uniqBy} from '../util/util' +import ux from '../ux' +import {colorize} from '../ux/theme' import {CommandHelp} from './command' import {HelpFormatter} from './formatter' import RootHelp from './root' import {formatCommandDeprecationWarning, getHelpFlagAdditions, standardizeIDFromArgv} from './util' - export {CommandHelp} from './command' export {getHelpFlagAdditions, normalizeArgv, standardizeIDFromArgv} from './util' @@ -110,7 +108,7 @@ export class Help extends HelpBase { const summary = this.summary(c) return [ colorize(this.config?.theme?.command, c.id), - summary && colorize(this.config?.theme?.sectionDescription, stripAnsi(summary)), + summary && colorize(this.config?.theme?.sectionDescription, ansis.strip(summary)), ] }), { @@ -146,7 +144,7 @@ export class Help extends HelpBase { description && this.section('DESCRIPTION', this.wrap(colorize(this.config?.theme?.sectionDescription, description))), ]).join('\n\n') - if (this.opts.stripAnsi) output = stripAnsi(output) + if (this.opts.stripAnsi) output = ansis.strip(output) return output + '\n' } @@ -174,9 +172,7 @@ export class Help extends HelpBase { } protected log(...args: string[]) { - this.opts.sendToStderr - ? write.stderr(format.apply(this, args) + '\n') - : write.stdout(format.apply(this, args) + '\n') + this.opts.sendToStderr ? ux.stderr(args) : ux.stdout(args) } public async showCommandHelp(command: Command.Loadable): Promise { diff --git a/src/help/root.ts b/src/help/root.ts index 37ef88ac2..becb845bd 100644 --- a/src/help/root.ts +++ b/src/help/root.ts @@ -1,8 +1,8 @@ -import stripAnsi from 'strip-ansi' +import ansis from 'ansis' -import {colorize} from '../cli-ux/theme' import * as Interfaces from '../interfaces' import {compact} from '../util/util' +import {colorize} from '../ux/theme' import {HelpFormatter} from './formatter' export default class RootHelp extends HelpFormatter { @@ -31,7 +31,7 @@ export default class RootHelp extends HelpFormatter { this.usage(), this.description(), ]).join('\n\n') - if (this.opts.stripAnsi) output = stripAnsi(output) + if (this.opts.stripAnsi) output = ansis.strip(output) return output } diff --git a/src/index.ts b/src/index.ts index 98b3bd36b..1b7ac5824 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,9 @@ -import write from './cli-ux/write' - function checkCWD() { try { process.cwd() } catch (error: any) { if (error.code === 'ENOENT') { - write.stderr('WARNING: current directory does not exist\n') + process.stderr.write('WARNING: current directory does not exist\n') } } } @@ -13,15 +11,13 @@ function checkCWD() { checkCWD() export * as Args from './args' -export * as ux from './cli-ux' -export {flush} from './cli-ux/flush' -export {stderr, stdout} from './cli-ux/stream' // Remove these in the next major version export {Command} from './command' export {Config, Plugin} from './config' export * as Errors from './errors' export {handle} from './errors/handle' export {execute} from './execute' export * as Flags from './flags' +export {flush} from './flush' export {CommandHelp, Help, HelpBase, loadHelpClass} from './help' export {HelpSection, HelpSectionKeyValueTable, HelpSectionRenderer} from './help/formatter' export * as Interfaces from './interfaces' @@ -32,3 +28,4 @@ export * as Parser from './parser' export {Performance} from './performance' export {Settings, settings} from './settings' export {toConfiguredId, toStandardizedId} from './util/ids' +export {methods as ux} from './ux' diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index 3a0df636d..bbe693999 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -127,6 +127,7 @@ export interface Config { readonly theme?: Theme topicSeparator: ' ' | ':' readonly topics: Topic[] + readonly updateConfig: NonNullable /** * user agent to use for http calls * diff --git a/src/interfaces/pjson.ts b/src/interfaces/pjson.ts index bf5f515f2..cfabd7317 100644 --- a/src/interfaces/pjson.ts +++ b/src/interfaces/pjson.ts @@ -144,7 +144,7 @@ export namespace PJSON { subtopics?: Plugin['oclif']['topics'] } } - update: { + update?: { autoupdate?: { debounce?: number rollout?: number diff --git a/src/interfaces/theme.ts b/src/interfaces/theme.ts index bb2453f70..618c2b653 100644 --- a/src/interfaces/theme.ts +++ b/src/interfaces/theme.ts @@ -1,5 +1,4 @@ -// chalk doesn't export a list of standard colors, so we have to supply our own -export const STANDARD_CHALK = [ +export const STANDARD_ANSI = [ 'white', 'black', 'blue', @@ -41,41 +40,36 @@ export const STANDARD_CHALK = [ 'strikethrough', ] as const -export type StandardChalk = (typeof STANDARD_CHALK)[number] +export type StandardAnsi = (typeof STANDARD_ANSI)[number] -export const THEME_KEYS = [ - 'alias', - 'bin', - 'command', - 'commandSummary', - 'dollarSign', - 'flag', - 'flagDefaultValue', - 'flagOptions', - 'flagRequired', - 'flagSeparator', - 'sectionDescription', - 'sectionHeader', - 'topic', - 'version', -] as const - -export type ThemeKey = (typeof THEME_KEYS)[number] +export type JsonTheme = { + brace?: string | StandardAnsi + bracket?: string | StandardAnsi + colon?: string | StandardAnsi + comma?: string | StandardAnsi + key?: string | StandardAnsi + string?: string | StandardAnsi + number?: string | StandardAnsi + boolean?: string | StandardAnsi + null?: string | StandardAnsi +} export type Theme = { - [key: string | ThemeKey]: string | StandardChalk | undefined - alias?: string | StandardChalk - bin?: string | StandardChalk - command?: string | StandardChalk - commandSummary?: string | StandardChalk - dollarSign?: string | StandardChalk - flag?: string | StandardChalk - flagDefaultValue?: string | StandardChalk - flagOptions?: string | StandardChalk - flagRequired?: string | StandardChalk - flagSeparator?: string | StandardChalk - sectionDescription?: string | StandardChalk - sectionHeader?: string | StandardChalk - topic?: string | StandardChalk - version?: string | StandardChalk + [key: string]: string | StandardAnsi | Theme | undefined + alias?: string | StandardAnsi + bin?: string | StandardAnsi + command?: string | StandardAnsi + commandSummary?: string | StandardAnsi + dollarSign?: string | StandardAnsi + flag?: string | StandardAnsi + flagDefaultValue?: string | StandardAnsi + flagOptions?: string | StandardAnsi + flagRequired?: string | StandardAnsi + flagSeparator?: string | StandardAnsi + json?: JsonTheme + sectionDescription?: string | StandardAnsi + sectionHeader?: string | StandardAnsi + spinner?: string | StandardAnsi + topic?: string | StandardAnsi + version?: string | StandardAnsi } diff --git a/src/main.ts b/src/main.ts index 8d4c82c6b..02f946e6c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,12 +1,12 @@ import {URL, fileURLToPath} from 'node:url' import Cache from './cache' -import {ux} from './cli-ux' import {Config} from './config' import {getHelpFlagAdditions, loadHelpClass, normalizeArgv} from './help' import * as Interfaces from './interfaces' import {OCLIF_MARKER_OWNER, Performance} from './performance' import {SINGLE_COMMAND_CLI_SYMBOL} from './symbols' +import ux from './ux' const debug = require('debug')('oclif:main') @@ -70,7 +70,7 @@ export async function run(argv?: string[], options?: Interfaces.LoadOptions): Pr // display version if applicable if (versionAddition(argv, config)) { - ux.log(config.userAgent) + ux.stdout(config.userAgent) await collectPerf() return } diff --git a/src/parser/errors.ts b/src/parser/errors.ts index 3b3de0063..65b01339b 100644 --- a/src/parser/errors.ts +++ b/src/parser/errors.ts @@ -1,10 +1,10 @@ -import chalk from 'chalk' +import ansis from 'ansis' import Cache from '../cache' -import {renderList} from '../cli-ux/list' import {CLIError} from '../errors' import {Arg, ArgInput, CLIParseErrorOptions, OptionFlag} from '../interfaces/parser' import {uniq} from '../util/util' +import renderList from '../ux/list' export {CLIError} from '../errors' @@ -124,7 +124,7 @@ export class FailedFlagValidationError extends CLIParseError { 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 '))}` + const message = `The following ${errString} occurred:\n ${ansis.dim(deduped.join('\n '))}` super({exit: Cache.getInstance().get('exitCodes')?.failedFlagValidation ?? exit, message, parse}) } } diff --git a/src/parser/help.ts b/src/parser/help.ts index a2da21dda..750e3c353 100644 --- a/src/parser/help.ts +++ b/src/parser/help.ts @@ -1,4 +1,4 @@ -import chalk from 'chalk' +import ansis from 'ansis' import {Flag, FlagUsageOptions} from '../interfaces/parser' import {sortBy} from '../util/util' @@ -17,7 +17,7 @@ export function flagUsage(flag: Flag, options: FlagUsageOptions = {}): [str let description: string | undefined = flag.summary || flag.description || '' if (options.displayRequired && flag.required) description = `(required) ${description}` - description = description ? chalk.dim(description) : undefined + description = description ? ansis.dim(description) : undefined return [` ${label.join(',').trim()}${usage}`, description] as [string, string | undefined] } diff --git a/src/util/read-tsconfig.ts b/src/util/read-tsconfig.ts index dfedc09b6..9ffd86f9f 100644 --- a/src/util/read-tsconfig.ts +++ b/src/util/read-tsconfig.ts @@ -2,7 +2,7 @@ import makeDebug from 'debug' import {readFile, readdir} from 'node:fs/promises' import {dirname, join} from 'node:path' -import {memoizedWarn} from '../errors/warn' +import {warn} from '../errors/warn' import {TSConfig} from '../interfaces' import {mergeNestedObjects} from './util' @@ -45,7 +45,7 @@ export async function readTSConfig(root: string, tsconfigName = 'tsconfig.json') } if (!typescript) { - memoizedWarn( + warn( 'Could not find typescript. Please ensure that typescript is a devDependency. Falling back to compiled source.', ) return diff --git a/src/cli-ux/action/base.ts b/src/ux/action/base.ts similarity index 94% rename from src/cli-ux/action/base.ts rename to src/ux/action/base.ts index b92fa1abe..f6c2b85fd 100644 --- a/src/cli-ux/action/base.ts +++ b/src/ux/action/base.ts @@ -3,7 +3,7 @@ import {inspect} from 'node:util' import {castArray} from '../../util/util' import {Options} from './types' -export interface ITask { +type Task = { action: string active: boolean status: string | undefined @@ -11,9 +11,6 @@ export interface ITask { export type ActionType = 'debug' | 'simple' | 'spinner' -/** - * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999 - */ export class ActionBase { std: 'stderr' | 'stdout' = 'stderr' @@ -56,11 +53,11 @@ export class ActionBase { task.status = status } - public get task(): ITask | undefined { + public get task(): Task | undefined { return this.globals.action.task } - public set task(task: ITask | undefined) { + public set task(task: Task | undefined) { this.globals.action.task = task } @@ -213,7 +210,7 @@ export class ActionBase { } } - private get globals(): {action: {task?: ITask}; output: string | undefined} { + private get globals(): {action: {task?: Task}; output: string | undefined} { ;(global as any).ux = (global as any).ux || {} const globals = (global as any).ux globals.action = globals.action || {} diff --git a/src/cli-ux/action/simple.ts b/src/ux/action/simple.ts similarity index 100% rename from src/cli-ux/action/simple.ts rename to src/ux/action/simple.ts diff --git a/src/cli-ux/action/spinner.ts b/src/ux/action/spinner.ts similarity index 75% rename from src/cli-ux/action/spinner.ts rename to src/ux/action/spinner.ts index 20ee3dfa2..a1d7fda78 100644 --- a/src/cli-ux/action/spinner.ts +++ b/src/ux/action/spinner.ts @@ -1,29 +1,24 @@ -import ansiStyles from 'ansi-styles' -import chalk from 'chalk' -import stripAnsi from 'strip-ansi' -import * as supportsColor from 'supports-color' +import ansis from 'ansis' +import spinners from 'cli-spinners' +import Cache from '../../cache' import {errtermwidth} from '../../screen' +import {colorize} from '../theme' import {ActionBase, ActionType} from './base' -import spinners from './spinners' import {Options} from './types' 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') - return has256 ? `\u001B[38;5;104m${s}${ansiStyles.reset.open}` : chalk.magenta(s) -} - export default class SpinnerAction extends ActionBase { - frameIndex: number + public type: ActionType = 'spinner' - frames: string[] + private color = 'magenta' - spinner?: NodeJS.Timeout + private frameIndex: number - public type: ActionType = 'spinner' + private frames: string[] + + private spinner?: NodeJS.Timeout constructor() { super() @@ -31,14 +26,20 @@ export default class SpinnerAction extends ActionBase { this.frameIndex = 0 } + protected colorize(s: string): string { + return colorize(this.color, s) + } + protected _frame(): string { const frame = this.frames[this.frameIndex] this.frameIndex = ++this.frameIndex % this.frames.length - return color(frame) + return this.colorize(frame) } 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 (ansis.strip(s).split('\n') as any[]) + .map((l) => Math.ceil(l.length / errtermwidth)) + .reduce((c, i) => c + i, 0) } protected _pause(icon?: string): void { @@ -67,6 +68,7 @@ export default class SpinnerAction extends ActionBase { } protected _start(opts: Options): void { + this.color = (Cache.getInstance().get('config')?.theme?.spinner as string | undefined) ?? this.color if (opts.style) this.frames = this.getFrames(opts) this._reset() diff --git a/src/ux/action/types.ts b/src/ux/action/types.ts new file mode 100644 index 000000000..95fd45968 --- /dev/null +++ b/src/ux/action/types.ts @@ -0,0 +1,7 @@ +// Use * as for import to ensure the types are portable +import * as spinners from 'cli-spinners' + +export type Options = { + stdout?: boolean + style?: spinners.SpinnerName | 'random' +} diff --git a/src/ux/colorize-json.ts b/src/ux/colorize-json.ts new file mode 100644 index 000000000..5354f5e14 --- /dev/null +++ b/src/ux/colorize-json.ts @@ -0,0 +1,74 @@ +import {colorize} from './theme' +const tokenTypes = [ + {regex: /^\s+/, tokenType: 'whitespace'}, + {regex: /^[{}]/, tokenType: 'brace'}, + {regex: /^[[\]]/, tokenType: 'bracket'}, + {regex: /^:/, tokenType: 'colon'}, + {regex: /^,/, tokenType: 'comma'}, + {regex: /^-?\d+(?:\.\d+)?(?:e[+-]?\d+)?/i, tokenType: 'number'}, + {regex: /^"(?:\\.|[^"\\])*"(?=\s*:)/, tokenType: 'key'}, + {regex: /^"(?:\\.|[^"\\])*"/, tokenType: 'string'}, + {regex: /^true|^false/, tokenType: 'boolean'}, + {regex: /^null/, tokenType: 'null'}, +] + +type Options = { + pretty?: boolean + theme?: Record +} + +function formatInput(json?: unknown, options?: Options) { + return options?.pretty + ? JSON.stringify(typeof json === 'string' ? JSON.parse(json) : json, null, 2) + : typeof json === 'string' + ? json + : JSON.stringify(json) +} + +export function tokenize(json?: unknown, options?: Options) { + let input = formatInput(json, options) + + const tokens = [] + let foundToken = false + + do { + for (const tokenType of tokenTypes) { + const match = tokenType.regex.exec(input) + if (match) { + tokens.push({type: tokenType.tokenType, value: match[0]}) + input = input.slice(match[0].length) + foundToken = true + break + } + } + } while (hasRemainingTokens(input, foundToken)) + + return tokens +} + +function hasRemainingTokens(input: string | undefined, foundToken: boolean) { + return (input?.length ?? 0) > 0 && foundToken +} + +/** + * Add color to JSON. + * + * options + * pretty: set to true to pretty print the JSON (defaults to true) + * theme: theme to use for colorizing. See keys below for available options. All keys are optional and must be valid colors (e.g. hex code, rgb, or standard ansi color). + * + * Available theme keys: + * - brace + * - bracket + * - colon + * - comma + * - key + * - string + * - number + * - boolean + * - null + */ +export default function colorizeJson(json: unknown, options?: Options) { + const opts = {...options, pretty: options?.pretty ?? true} + return tokenize(json, opts).reduce((acc, token) => acc + colorize(options?.theme?.[token.type], token.value), '') +} diff --git a/src/ux/index.ts b/src/ux/index.ts new file mode 100644 index 000000000..e3b744fc6 --- /dev/null +++ b/src/ux/index.ts @@ -0,0 +1,74 @@ +import {error} from '../errors/error' +import {exit} from '../errors/exit' +import {warn} from '../errors/warn' +import Simple from './action/simple' +import Spinner from './action/spinner' +import colorizeJson from './colorize-json' +import {colorize} from './theme' +import {stderr, stdout} from './write' + +const ACTION_TYPE = + (Boolean(process.stderr.isTTY) && + !process.env.CI && + !['dumb', 'emacs-color'].includes(process.env.TERM!) && + 'spinner') || + 'simple' + +export const methods = { + action: ACTION_TYPE === 'spinner' ? new Spinner() : new Simple(), + /** + * Add color to text. + * @param color color to use. Can be hex code (e.g. `#ff0000`), rgb (e.g. `rgb(255, 255, 255)`) or a standard ansi color (e.g. `red`) + * @param text string to colorize + * @returns colorized string + */ + colorize, + /** + * Add color to JSON. + * + * options + * pretty: set to true to pretty print the JSON (defaults to true) + * theme: theme to use for colorizing. See keys below for available options. All keys are optional and must be valid colors (e.g. hex code, rgb, or standard ansi color). + * + * Available theme keys: + * - brace + * - bracket + * - colon + * - comma + * - key + * - string + * - number + * - boolean + * - null + */ + colorizeJson, + /** + * Throw an error. + * + * If `exit` option is `false`, the error will be logged to stderr but not exit the process. + * If `exit` is set to a number, the process will exit with that code. + */ + error, + /** + * Exit the process with provided exit code (defaults to 0). + */ + exit, + /** + * Log a formatted string to stderr. + * + * See node's util.format() for formatting options. + */ + stderr, + /** + * Log a formatted string to stdout. + * + * See node's util.format() for formatting options. + */ + stdout, + /** + * Prints a pretty warning message to stderr. + */ + warn, +} + +export default methods diff --git a/src/cli-ux/list.ts b/src/ux/list.ts similarity index 91% rename from src/cli-ux/list.ts rename to src/ux/list.ts index 86f443841..4b99732c2 100644 --- a/src/cli-ux/list.ts +++ b/src/ux/list.ts @@ -10,7 +10,7 @@ function linewrap(length: number, s: string): string { export type IListItem = [string, string | undefined] export type IList = IListItem[] -export function renderList(items: IListItem[]): string { +export default function renderList(items: IListItem[]): string { if (items.length === 0) { return '' } diff --git a/src/ux/theme.ts b/src/ux/theme.ts new file mode 100644 index 000000000..588855cbd --- /dev/null +++ b/src/ux/theme.ts @@ -0,0 +1,40 @@ +import ansis from 'ansis' + +import {STANDARD_ANSI, StandardAnsi, Theme} from '../interfaces/theme' + +function isStandardChalk(color: any): color is StandardAnsi { + return STANDARD_ANSI.includes(color) +} + +/** + * Add color to text. + * @param color color to use. Can be hex code (e.g. `#ff0000`), rgb (e.g. `rgb(255, 255, 255)`) or a standard ansi color (e.g. `red`) + * @param text string to colorize + * @returns colorized string + */ +export function colorize(color: string | StandardAnsi | undefined, text: string): string { + if (!color) return text + if (isStandardChalk(color)) return ansis[color](text) + if (color.startsWith('#')) return ansis.hex(color)(text) + if (color.startsWith('rgb')) { + const [red, green, blue] = color + .slice(4, -1) + .split(',') + .map((c) => Number.parseInt(c.trim(), 10)) + return ansis.rgb(red, green, blue)(text) + } + + return text +} + +export function parseTheme(theme: Record): Theme { + return Object.fromEntries( + Object.entries(theme) + .map(([key, value]) => [key, typeof value === 'string' ? isValid(value) : parseTheme(value)]) + .filter(([_, value]) => value), + ) +} + +function isValid(color: string): string | undefined { + return color.startsWith('#') || color.startsWith('rgb') || isStandardChalk(color) ? color : undefined +} diff --git a/src/ux/write.ts b/src/ux/write.ts new file mode 100644 index 000000000..f1eb27707 --- /dev/null +++ b/src/ux/write.ts @@ -0,0 +1,17 @@ +import {format} from 'node:util' + +export const stdout = (str: string | string[] | undefined, ...args: string[]): void => { + if (typeof str === 'string' || !str) { + process.stdout.write(format(str, ...args) + '\n') + } else { + process.stdout.write(format(...str, ...args) + '\n') + } +} + +export const stderr = (str: string | string[] | undefined, ...args: string[]): void => { + if (typeof str === 'string' || !str) { + process.stderr.write(format(str, ...args) + '\n') + } else { + process.stderr.write(format(...str, ...args) + '\n') + } +} diff --git a/test/cli-ux/export.test.ts b/test/cli-ux/export.test.ts deleted file mode 100644 index 596798f51..000000000 --- a/test/cli-ux/export.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {expect} from 'chai' - -import {ActionBase, Config, ExitError, IPromptOptions, Table, ux} from '../../src/cli-ux' - -type MyColumns = Record -const options: Table.table.Options = {} -const columns: Table.table.Columns = {} -const iPromptOptions: IPromptOptions = {} - -describe('ux exports', () => { - it('should have exported members on par with old cli-ux module', () => { - expect(options).to.be.ok - expect(columns).to.be.ok - expect(iPromptOptions).to.be.ok - expect(Table.table.Flags).to.be.ok - expect(typeof Table.table.flags).to.be.equal('function') - expect(typeof Table.table).to.be.equal('function') - expect(ux.config).to.be.ok - expect(typeof Config).to.be.equal('function') - expect(typeof ActionBase).to.be.equal('function') - expect(typeof ExitError).to.be.equal('function') - }) -}) diff --git a/test/cli-ux/fancy.ts b/test/cli-ux/fancy.ts deleted file mode 100644 index 4cda54a82..000000000 --- a/test/cli-ux/fancy.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {fancy as base} from 'fancy-test' -import {rm} from 'node:fs/promises' -import {join} from 'node:path' - -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() - }) - -export {FancyTypes, expect} from 'fancy-test' diff --git a/test/cli-ux/helpers/init.js b/test/cli-ux/helpers/init.js deleted file mode 100644 index cd57a3d48..000000000 --- a/test/cli-ux/helpers/init.js +++ /dev/null @@ -1,3 +0,0 @@ -const path = require('path') -process.env.TS_NODE_PROJECT = path.resolve('test/tsconfig.json') -global.columns = '80' diff --git a/test/cli-ux/index.test.ts b/test/cli-ux/index.test.ts deleted file mode 100644 index c9d175b77..000000000 --- a/test/cli-ux/index.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {ux} from '../../src/cli-ux' -import {expect, fancy} from './fancy' -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') - }) -}) - -describe('hyperlinker', () => { - 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' - expect(link).to.equal(expected) - }) -}) diff --git a/test/cli-ux/prompt.test.ts b/test/cli-ux/prompt.test.ts deleted file mode 100644 index 7f34e83d8..000000000 --- a/test/cli-ux/prompt.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import * as chai from 'chai' - -const {expect} = chai - -import {ux} from '../../src/cli-ux' -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') - }) - - 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) - }) - - 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) - }) - - 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') - }) - - fancy - .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('') - }) - - fancy - .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/header.test.ts b/test/cli-ux/styled/header.test.ts deleted file mode 100644 index b5c409078..000000000 --- a/test/cli-ux/styled/header.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {expect} from 'chai' -import {SinonSandbox, SinonStub, createSandbox} from 'sinon' -import stripAnsi from 'strip-ansi' - -import {ux} from '../../../src' - -describe('styled/header', () => { - let sandbox: SinonSandbox - let stdoutStub: SinonStub - - beforeEach(() => { - sandbox = createSandbox() - stdoutStub = sandbox.stub(ux.write, 'stdout') - }) - - afterEach(() => { - sandbox.restore() - }) - - it('shows a styled header', () => { - ux.styledHeader('A styled header') - expect(stripAnsi(stdoutStub.firstCall.firstArg)).to.include('=== A styled header\n') - }) -}) diff --git a/test/cli-ux/styled/object.test.ts b/test/cli-ux/styled/object.test.ts deleted file mode 100644 index 4e14957df..000000000 --- a/test/cli-ux/styled/object.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {expect, fancy} from 'fancy-test' - -import {ux} from '../../../src/cli-ux' - -describe('styled/object', () => { - fancy.stdout().end('shows a table', (output) => { - ux.styledObject([ - {foo: 1, bar: 1}, - {foo: 2, bar: 2}, - {foo: 3, bar: 3}, - ]) - expect(output.stdout).to.equal(`0: foo: 1, bar: 1 -1: foo: 2, bar: 2 -2: foo: 3, bar: 3 -`) - }) -}) diff --git a/test/cli-ux/styled/progress.test.ts b/test/cli-ux/styled/progress.test.ts deleted file mode 100644 index ff91b35b5..000000000 --- a/test/cli-ux/styled/progress.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {expect, fancy} from 'fancy-test' - -import {ux} from '../../../src/cli-ux' - -describe('progress', () => { - // single bar - 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') - // @ts-expect-error because private member - expect(b1.bars).to.not.have - }) - - // testing no settings passed, default settings created - fancy.end('single bar, no bars array', (_) => { - const b1 = ux.progress({}) - // @ts-expect-error because private member - expect(b1.options.format).to.contain('progress') - // @ts-expect-error because private member - expect(b1.bars).to.not.have - // @ts-expect-error because private member - expect(b1.options.noTTYOutput).to.not.be.null - }) - // testing getProgressBar returns correct type - fancy.end('typeof progress bar is object', (_) => { - const b1 = ux.progress({format: 'Example 1: Progress {bar} | {percentage}%'}) - expect(typeof b1).to.equal('object') - }) -}) diff --git a/test/cli-ux/styled/table.e2e.ts b/test/cli-ux/styled/table.e2e.ts deleted file mode 100644 index e6ba0e78f..000000000 --- a/test/cli-ux/styled/table.e2e.ts +++ /dev/null @@ -1,46 +0,0 @@ -import {expect, fancy} from 'fancy-test' - -import {ux} from '../../../src/cli-ux' - -describe('styled/table', () => { - describe('null/undefined handling', () => { - 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') - expect(output.stdout).to.include('2') - expect(output.stdout).to.not.include('null') - expect(output.stdout).to.not.include('undefined') - }) - }) - - 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< - string, - unknown - >[] - const tallColumns = { - id: {header: 'ID'}, - name: {}, - value: {header: 'TEST'}, - } - - ux.table(data, tallColumns) - expect(output.stdout).to.include('ID') - }) - - 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()}]), - ) - - 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 deleted file mode 100644 index c32ef23c3..000000000 --- a/test/cli-ux/styled/table.test.ts +++ /dev/null @@ -1,324 +0,0 @@ -import {expect, fancy} from 'fancy-test' - -import {ux} from '../../../src/cli-ux' -import * as screen from '../../../src/screen' - -/* eslint-disable camelcase */ -const apps = [ - { - build_stack: { - id: '123', - name: 'heroku-16', - }, - created_at: '2000-01-01T22:34:46Z', - id: '123', - git_url: 'https://git.heroku.com/supertable-test-1.git', - name: 'supertable-test-1', - owner: { - email: 'example@heroku.com', - id: '1', - }, - region: {id: '123', name: 'us'}, - released_at: '2000-01-01T22:34:46Z', - stack: { - id: '123', - name: 'heroku-16', - }, - updated_at: '2000-01-01T22:34:46Z', - web_url: 'https://supertable-test-1.herokuapp.com/', - }, - { - build_stack: { - id: '321', - name: 'heroku-16', - }, - created_at: '2000-01-01T22:34:46Z', - id: '321', - git_url: 'https://git.heroku.com/phishing-demo.git', - name: 'supertable-test-2', - owner: { - email: 'example@heroku.com', - id: '1', - }, - region: {id: '321', name: 'us'}, - released_at: '2000-01-01T22:34:46Z', - stack: { - id: '321', - name: 'heroku-16', - }, - updated_at: '2000-01-01T22:34:46Z', - web_url: 'https://supertable-test-2.herokuapp.com/', - }, -] - -const columns = { - id: {header: 'ID'}, - name: {}, - web_url: {extended: true}, - stack: {extended: true, get: (r: any) => r.stack && r.stack.name}, -} -/* eslint-enable camelcase */ - -const ws = ' ' - -// ignore me -// stored up here for line wrapping reasons -const extendedHeader = `ID Name${ws.padEnd(14)}Web url${ws.padEnd(34)}Stack${ws.padEnd(5)}` - -// tests to-do: -// no-truncate -// 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('has optional flags', (_) => { - const flags = ux.table.flags() - expect(flags.columns).to.exist - expect(flags.sort).to.exist - expect(flags.filter).to.exist - expect(flags.csv).to.exist - expect(flags.output).to.exist - expect(flags.extended).to.exist - expect(flags['no-truncate']).to.exist - expect(flags['no-header']).to.exist - }) - - fancy.stdout().end('displays table', (output) => { - ux.table(apps, columns) - expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} - ─── ─────────────────${ws} - 123 supertable-test-1${ws} - 321 supertable-test-2${ws}\n`) - }) - - describe('columns', () => { - 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) => { - ux.table(apps, columns, {extended: true}) - expect(output.stdout).to.equal(`${ws}${extendedHeader} - ─── ───────────────── ──────────────────────────────────────── ─────────${ws} - 123 supertable-test-1 https://supertable-test-1.herokuapp.com/ heroku-16${ws} - 321 supertable-test-2 https://supertable-test-2.herokuapp.com/ heroku-16${ws}\n`) - }) - }) - - describe('options', () => { - 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) => { - ux.table(apps, columns, {title: 'testing'}) - expect(output.stdout).to.equal(`testing -======================= -| ID Name${ws.padEnd(14)} -| ─── ─────────────────${ws} -| 123 supertable-test-1${ws} -| 321 supertable-test-2${ws}\n`) - }) - - 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) => { - ux.table(apps, columns, {columns: 'id'}) - expect(output.stdout).to.equal(` ID${ws}${ws} - ───${ws} - 123${ws} - 321${ws}\n`) - }) - - 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: '1"2"3', - name: 'supertable-test-3', - }, - { - id: '123', - name: 'supertable-test-4,comma', - }, - { - id: '123', - name: 'supertable-test-5', - }, - ], - columns, - {output: 'csv'}, - ) - expect(output.stdout).to.equal(`ID,Name -"123\n2","supertable-test-1" -"12""3","supertable-test-2" -"1""2""3","supertable-test-3" -"123","supertable-test-4,comma" -123,supertable-test-5\n`) - }) - - 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) => { - 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) => { - ux.table(apps, columns, {output: 'json'}) - expect(output.stdout).to.equal(`[ - { - "id": "123", - "name": "supertable-test-1" - }, - { - "id": "321", - "name": "supertable-test-2" - } -] -`) - }) - - fancy.stdout().end('outputs in yaml', (output) => { - ux.table(apps, columns, {output: 'yaml'}) - expect(output.stdout).to.equal(`- id: '123' - name: supertable-test-1 -- id: '321' - name: supertable-test-2 - -`) - }) - - fancy.stdout().end('sorts by property', (output) => { - ux.table(apps, columns, {sort: '-name'}) - expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} - ─── ─────────────────${ws} - 321 supertable-test-2${ws} - 123 supertable-test-1${ws}\n`) - }) - - 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) => { - 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)} - ${''.padEnd(three.id.length, '─')} ─────────────────${ws} - ${three.id} supertable-test-3${ws}\n`) - }) - }) - - describe('#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', (_) => { - const flags = ux.table.flags({except: 'columns'}) - expect((flags as any).columns).to.be.undefined - expect(flags.sort).to.be.a('object') - }) - }) - - describe('edge cases', () => { - 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} - 321 supertable-test-2${ws} - 123 supertable-test-1${ws}\n`) - }) - - fancy.stdout().end('displays multiline cell', (output) => { - /* eslint-disable camelcase */ - const app3 = { - build_stack: { - name: 'heroku-16', - }, - id: '456', - name: 'supertable-test\n3', - web_url: 'https://supertable-test-1.herokuapp.com/', - } - /* eslint-enable camelcase */ - - ux.table([...apps, app3 as any], columns, {sort: '-ID'}) - expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} - ─── ─────────────────${ws} - 456 supertable-test${ws.padEnd(3)} - 3${ws.padEnd(17)} - 321 supertable-test-2${ws} - 123 supertable-test-1${ws}\n`) - }) - - const orig = { - stdtermwidth: screen.stdtermwidth, - CLI_UX_SKIP_TTY_CHECK: process.env.CLI_UX_SKIP_TTY_CHECK, - } - - 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} - 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 deleted file mode 100644 index c628bdec0..000000000 --- a/test/cli-ux/styled/tree.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {expect, fancy} from 'fancy-test' - -import {ux} from '../../../src/cli-ux' - -describe('styled/tree', () => { - fancy.stdout().end('shows the tree', (output) => { - const tree = ux.tree() - tree.insert('foo') - tree.insert('bar') - - const subtree = ux.tree() - subtree.insert('qux') - tree.nodes.bar.insert('baz', subtree) - - tree.display() - expect(output.stdout).to.equal(`├─ foo -└─ bar - └─ baz - └─ qux -`) - }) -}) diff --git a/test/command/command.test.ts b/test/command/command.test.ts index 35f33ea4d..c1f686270 100644 --- a/test/command/command.test.ts +++ b/test/command/command.test.ts @@ -1,12 +1,7 @@ -import {expect, fancy} from 'fancy-test' +import {expect} from 'chai' -// import path = require('path') 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') -// const root = path.resolve(__dirname, '../../package.json') +import {captureOutput} from '../test' class Command extends Base { static description = 'test command' @@ -24,55 +19,49 @@ class CodeError extends Error { } describe('command', () => { - fancy - .stdout() - .do(() => Command.run([])) - .do((output) => expect(output.stdout).to.equal('foo\n')) - .it('logs to stdout') + it('logs to stdout', async () => { + const {stdout} = await captureOutput(async () => Command.run([])) + expect(stdout).to.equal('foo\n') + }) - fancy - .do(async () => { - class Command extends Base { - static description = 'test command' + it('returns value', 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') + const {result} = await captureOutput(async () => Command.run([])) + expect(result).to.equal(101) + }) - fancy - .do(() => { - class Command extends Base { - async run() { - throw new Error('new x error') - } + it('errors out', async () => { + class Command extends Base { + async run() { + throw new Error('new x error') } + } - return Command.run([]) - }) - .catch(/new x error/) - .it('errors out') + const {error} = await captureOutput(async () => Command.run([])) + expect(error?.message).to.equal('new x error') + }) - fancy - .stdout() - .do(() => { - class Command extends Base { - async run() { - this.exit(0) - } + it('exits with 0', async () => { + class Command extends Base { + async run() { + this.exit(0) } + } - return Command.run([]) - }) - .catch(/EEXIT: 0/) - .it('exits with 0') + const {error} = await captureOutput(async () => Command.run([])) + expect(error?.code).to.equal('EEXIT') + expect(error?.oclif?.exit).to.equal(0) + }) describe('parse', () => { - fancy.stdout().it('has a flag', async (ctx) => { + it('has a flag', async () => { class CMD extends Base { static flags = { foo: Flags.string(), @@ -84,25 +73,22 @@ describe('command', () => { } } - await CMD.run(['--foo=bar']) - expect(ctx.stdout).to.equal('bar\n') + const {stdout} = await captureOutput(async () => CMD.run(['--foo=bar'])) + expect(stdout).to.equal('bar\n') }) }) describe('.log()', () => { - fancy - .stdout() - .do(async () => { - class CMD extends Command { - async run() { - this.log('json output: %j', {a: 'foobar'}) - } + it('uses util.format()', async () => { + class CMD extends Base { + 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()') + const {stdout} = await captureOutput(async () => CMD.run([])) + expect(stdout).to.equal('json output: {"a":"foobar"}\n') + }) }) describe('flags with deprecated aliases', () => { @@ -122,352 +108,263 @@ 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') - - 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') - - 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') - - 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') - - 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') - - 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') + it('shows warning for deprecated flag alias', async () => { + const {stderr} = await captureOutput(async () => CMD.run(['--username', 'astro'])) + expect(stderr).to.include('Warning: The "--username" flag has been deprecated. Use "--name | -o"') + }) + + it('shows warning for deprecated flag alias', async () => { + const {stderr} = await captureOutput(async () => CMD.run(['--target-user', 'astro'])) + expect(stderr).to.include('Warning: The "--target-user" flag has been deprecated. Use "--name | -o"') + }) + + it('shows warning for deprecated short char flag alias', async () => { + const {stderr} = await captureOutput(async () => CMD.run(['-u', 'astro'])) + expect(stderr).to.include('Warning: The "-u" flag has been deprecated. Use "--name | -o"') + }) + + it('shows no warning when using proper flag name with a value that matches a flag alias', async () => { + const {stderr} = await captureOutput(async () => CMD.run(['--name', 'username'])) + expect(stderr).to.be.empty + }) + + it('shows no warning when using another flag with a value that matches a deprecated flag alias', async () => { + const {stderr} = await captureOutput(async () => CMD.run(['--other', 'target-user'])) + expect(stderr).to.be.empty + }) + + it('shows no warning when proper flag name with a value that matches a short char flag alias', async () => { + const {stderr} = await captureOutput(async () => CMD.run(['--name', 'u'])) + expect(stderr).to.be.empty + }) }) 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') - } + it('shows warning for deprecated flags', async () => { + class CMD extends Command { + static flags = { + name: Flags.string({ + deprecated: { + to: '--full-name', + }, + }), + 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') + const {stderr} = await captureOutput(async () => CMD.run(['--name', 'astro'])) + expect(stderr).to.include('Warning: The "name" flag has been deprecated. Use "--full-name"') + }) }) 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') - } + it('does not show warning for deprecated flags if they are not provided', async () => { + class CMD extends Command { + static flags = { + name: Flags.string({ + deprecated: { + to: '--full-name', + version: '2.0.0', + }, + }), + force: Flags.boolean(), } - 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') + async run() { + await this.parse(CMD) + this.log('running command') + } + } + + const {stderr} = await captureOutput(async () => CMD.run(['--force'])) + expect(stderr).to.not.include('Warning: The "name" flag has been deprecated') + }) }) 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') - } + it('shows warning for deprecated command', 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') + const {stderr} = await captureOutput(async () => CMD.run([])) + expect(stderr).to.include('Warning: The "my:command" command has been deprecated') + }) }) describe('deprecated state with options', () => { - fancy - .stdout() - .stderr() - .do(async () => { - class CMD extends Command { - static deprecationOptions = { - version: '2.0.0', - to: 'my:other:command', - } - - static id = 'my:command' - static state = 'deprecated' - - async run() { - this.log('running command') - } + it('shows warning for deprecated command with custom options', async () => { + class CMD extends Command { + static deprecationOptions = { + version: '2.0.0', + to: 'my:other: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') - 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') + const {stderr} = await captureOutput(async () => CMD.run([])) + expect(stderr).to.include('Warning: The "my:command" command has been deprecated') + expect(stderr).to.include('in version 2.0.0') + expect(stderr).to.include('Use "my:other:command" instead') + }) }) describe('stdout err', () => { - fancy - .stdout() - .do(async () => { - class CMD extends Command { - async run() { - process.stdout.emit('error', new CodeError('dd')) - } + it('handles errors thrown from stdout', async () => { + class CMD extends Command { + async run() { + process.stdout.emit('error', new CodeError('dd')) } + } - 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'}) - } + const {error} = await captureOutput(async () => CMD.run([])) + expect(error?.message).to.equal('dd') + }) + + it('handles EPIPE errors', 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') + const {stdout} = await captureOutput(async () => CMD.run([])) + expect(stdout).to.equal('json output: {"a":"foobar"}\n') + }) }) + 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') - } + it('json enabled/pass through disabled/no --json flag/jsonEnabled() should be false', 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) + }) - fancy - .stdout() - .do(async () => { - class CMD extends Command { - static enableJsonFlag = true + it('json enabled/pass through disabled/--json flag before --/jsonEnabled() should be true', 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') - - fancy - .stdout() - .do(async () => { - class CMD extends Command { - static enableJsonFlag = true - async run() {} - } + const cmd = new CMD(['--json'], {} as any) + expect(cmd.jsonEnabled()).to.equal(true) + }) - // 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 - - async run() { - const {flags} = await cmd.parse(CMD, ['--json']) - expect(flags.json).to.equal(true, 'json flag should be true') - } - } + it('json enabled from env', async () => { + class CMD extends Command { + static enableJsonFlag = true + async run() {} + } - 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 - - 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') - } - } + // 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/pass through enabled/--json flag before --/jsonEnabled() should be true', async () => { + class CMD extends Command { + static enableJsonFlag = 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') - } + 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) + }) - 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 - - 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') - } + it('json enabled/pass through enabled/--json flag after --/jsonEnabled() should be false', 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') } + } - 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(false) + }) - fancy - .stdout() - .do(async () => { - class CMD extends Command { - static enableJsonFlag = true + it('json enabled/pass through enabled/--json flag before --/extra param/jsonEnabled() should be true', async () => { + class CMD extends Command { + static enableJsonFlag = true - async run() {} + 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(true) - }) - .it('json enabled/pass through enabled/--json flag before --/jsonEnabled() should be true') + const cmd = new CMD(['foo', '--json'], {} as any) + expect(cmd.jsonEnabled()).to.equal(true) + }) - fancy - .stdout() - .do(async () => { - class CMD extends Command { - static '--' = true - static enableJsonFlag = false + it('json enabled/pass through enabled/--json flag after --/extra param/jsonEnabled() should be false', async () => { + class CMD extends Command { + static enableJsonFlag = true - async run() {} + async run() { + const {flags} = await cmd.parse(CMD, ['--foo', '--', '--json']) + expect(flags.json).to.equal(false, 'json flag should be false') } + } - 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') - }) -}) + const cmd = new CMD(['--foo', '--', '--json'], {} as any) + expect(cmd.jsonEnabled()).to.equal(false) + }) -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) - }) + it('json enabled/pass through enabled/--json flag before --/jsonEnabled() should be true', async () => { + class CMD extends Command { + static enableJsonFlag = true - 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) + async run() {} + } + + const cmd = new CMD(['--json', '--'], {} as any) + expect(cmd.jsonEnabled()).to.equal(true) + }) + + it('json disabled/pass through enable/--json flag before --/jsonEnabled() should be false', async () => { + class CMD extends Command { + static enableJsonFlag = false + + async run() {} + } + + const cmd = new CMD(['--json'], {} as any) + expect(cmd.jsonEnabled()).to.equal(false) + }) }) }) diff --git a/test/command/explicit-command-strategy.test.ts b/test/command/explicit-command-strategy.test.ts index 8de55ac1c..f1594ff26 100644 --- a/test/command/explicit-command-strategy.test.ts +++ b/test/command/explicit-command-strategy.test.ts @@ -1,28 +1,15 @@ import {expect} from 'chai' import {resolve} from 'node:path' -import {SinonSandbox, SinonStub, createSandbox} from 'sinon' -import stripAnsi from 'strip-ansi' -import {run, ux} from '../../src/index' +import {runCommand} from '../test' -describe('explicit command discovery strategy', () => { - let sandbox: SinonSandbox - let stdoutStub: SinonStub - - beforeEach(() => { - sandbox = createSandbox() - stdoutStub = sandbox.stub(ux.write, 'stdout') - }) - - afterEach(() => { - sandbox.restore() - }) +const root = resolve(__dirname, 'fixtures/bundled-cli/package.json') +describe('explicit command discovery strategy', () => { it('should show help for commands', async () => { - await run(['--help', 'foo'], resolve(__dirname, 'fixtures/bundled-cli/package.json')) - const [first, ...rest] = stdoutStub.args.map((a) => stripAnsi(a[0])) - expect(first).to.equal('example hook running --help\n') - expect(rest.join('')).to.equal(`foo topic description + const {stdout} = await runCommand(['--help', 'foo'], {root}) + expect(stdout).to.include('example hook running --help') + expect(stdout).to.include(`foo topic description USAGE $ oclif foo COMMAND @@ -36,16 +23,12 @@ COMMANDS }) it('should run command', async () => { - await run(['foo:bar'], resolve(__dirname, 'fixtures/bundled-cli/package.json')) - const [first, second] = stdoutStub.args.map((a) => stripAnsi(a[0])) - expect(first).to.equal('example hook running foo:bar\n') - expect(second).to.equal('hello world!\n') + const {stdout} = await runCommand(['foo:bar'], {root}) + expect(stdout).to.equal('example hook running foo:bar\nhello world!\n') }) it('should run alias', async () => { - await run(['foo:alias'], resolve(__dirname, 'fixtures/bundled-cli/package.json')) - const [first, second] = stdoutStub.args.map((a) => stripAnsi(a[0])) - expect(first).to.equal('example hook running foo:alias\n') - expect(second).to.equal('hello world!\n') + const {stdout} = await runCommand(['foo:alias'], {root}) + expect(stdout).to.equal('example hook running foo:alias\nhello world!\n') }) }) diff --git a/test/command/fixtures/bundled-cli/src/hooks/init.ts b/test/command/fixtures/bundled-cli/src/hooks/init.ts index 16eede0e8..d4a32e8e8 100644 --- a/test/command/fixtures/bundled-cli/src/hooks/init.ts +++ b/test/command/fixtures/bundled-cli/src/hooks/init.ts @@ -1,7 +1,7 @@ import {Hook, ux} from '../../../../../../src/index' -const hook: Hook<'init'> = async function (opts) { - ux.log(`example hook running ${opts.id}`) +const hook: Hook.Init = async function (opts) { + ux.stdout(`example hook running ${opts.id}`) } export default hook diff --git a/test/command/main-esm.test.ts b/test/command/main-esm.test.ts index 93525219d..bfc25feb9 100644 --- a/test/command/main-esm.test.ts +++ b/test/command/main-esm.test.ts @@ -1,8 +1,8 @@ -import {expect, fancy} from 'fancy-test' +import {expect} from 'chai' import {resolve} from 'node:path' import {pathToFileURL} from 'node:url' -import {run} from '../../src/main' +import {runCommand} from '../test' // This tests file URL / import.meta.url simulation. const convertToFileURL = (filepath: string) => pathToFileURL(filepath).toString() @@ -14,23 +14,19 @@ const version = `@oclif/core/${pjson.version} ${process.platform}-${process.arch 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') - - fancy - .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 + it('runs plugins', async () => { + const {stdout} = await runCommand(['plugins'], {root}) + expect(stdout).to.equal('No plugins installed.\n') + }) + + it('runs --version', async () => { + const {stdout} = await runCommand(['--version'], {root}) + expect(stdout).to.equal(version + '\n') + }) + + it('runs --help', async () => { + const {stdout} = await runCommand(['--help'], {root}) + expect(stdout).to.equal(`base library for oclif CLIs VERSION ${version} @@ -45,15 +41,15 @@ COMMANDS help Display help for oclif. plugins List installed plugins. -`), - ) - .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 + it('runs spaced topic help', async () => { + const {stdout} = await runCommand( + ['--help', 'foo'], + convertToFileURL(resolve(__dirname, 'fixtures/esm/package.json')), + ) + expect(stdout).to.equal(`foo topic description USAGE $ oclif-esm foo COMMAND @@ -64,15 +60,15 @@ TOPICS COMMANDS foo baz foo baz description -`), - ) - .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 + it('runs spaced topic help v2', async () => { + const {stdout} = await runCommand( + ['foo', 'bar', '--help'], + convertToFileURL(resolve(__dirname, 'fixtures/esm/package.json')), + ) + expect(stdout).to.equal(`foo bar topic description USAGE $ oclif-esm foo bar COMMAND @@ -81,19 +77,19 @@ COMMANDS foo bar fail fail description foo bar succeed succeed description -`), +`) + }) + + it('runs foo:baz with space separator', async () => { + const {stdout} = await runCommand(['foo', 'baz'], convertToFileURL(resolve(__dirname, 'fixtures/esm/package.json'))) + expect(stdout).to.equal('running Baz\n') + }) + + it('runs foo:bar:succeed with space separator', async () => { + const {stdout} = await runCommand( + ['foo', 'bar', 'succeed'], + convertToFileURL(resolve(__dirname, 'fixtures/esm/package.json')), ) - .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') - - 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') + expect(stdout).to.equal('it works!\n') + }) }) diff --git a/test/command/main.test.ts b/test/command/main.test.ts index 1c6d6c19a..1f0e30829 100644 --- a/test/command/main.test.ts +++ b/test/command/main.test.ts @@ -1,37 +1,24 @@ import {expect} from 'chai' import {readFileSync} from 'node:fs' import {join, resolve} from 'node:path' -import {SinonSandbox, SinonStub, createSandbox} from 'sinon' -import stripAnsi from 'strip-ansi' -import {ux} from '../../src/index' -import {run} from '../../src/main' +import {runCommand} from '../test' const pjson = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf8')) const version = `@oclif/core/${pjson.version} ${process.platform}-${process.arch} node-${process.version}` describe('main', () => { - let sandbox: SinonSandbox - let stdoutStub: SinonStub - - beforeEach(() => { - sandbox = createSandbox() - stdoutStub = sandbox.stub(ux.write, 'stdout') - }) - - afterEach(() => { - sandbox.restore() - }) - it('should run plugins', async () => { - const result = (await run(['plugins'], resolve(__dirname, '../../package.json'))) as Array<{ - name: string - type: string - }> - expect(result.length).to.equal(3) - const rootPlugin = result.find((r) => r.name === '@oclif/core') - const pluginHelp = result.find((r) => r.name === '@oclif/plugin-help') - const pluginPlugins = result.find((r) => r.name === '@oclif/plugin-plugins') + const {result} = await runCommand< + Array<{ + name: string + type: string + }> + >(['plugins']) + expect(result?.length).to.equal(3) + const rootPlugin = result?.find((r) => r.name === '@oclif/core') + const pluginHelp = result?.find((r) => r.name === '@oclif/plugin-help') + const pluginPlugins = result?.find((r) => r.name === '@oclif/plugin-plugins') expect(rootPlugin).to.exist expect(pluginHelp).to.exist @@ -39,13 +26,13 @@ describe('main', () => { }) it('should run version', async () => { - await run(['--version'], resolve(__dirname, '../../package.json')) - expect(stdoutStub.firstCall.firstArg).to.equal(`${version}\n`) + const {stdout} = await runCommand(['--version']) + expect(stdout).to.equal(`${version}\n`) }) 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 + const {stdout} = await runCommand(['--help']) + expect(stdout).to.equal(`base library for oclif CLIs VERSION ${version} @@ -64,8 +51,8 @@ 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 + const {stdout} = await runCommand(['--help', 'foo'], {root: resolve(__dirname, 'fixtures/typescript/package.json')}) + expect(stdout).to.equal(`foo topic description USAGE $ oclif foo COMMAND @@ -80,8 +67,10 @@ 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 + const {stdout} = await runCommand(['foo', 'bar', '--help'], { + root: resolve(__dirname, 'fixtures/typescript/package.json'), + }) + expect(stdout).to.equal(`foo bar topic description USAGE $ oclif foo bar COMMAND @@ -94,14 +83,14 @@ COMMANDS }) it('should run foo:baz with space separator', async () => { - const consoleLogStub = sandbox.stub(console, 'log').returns() - await run(['foo', 'baz'], resolve(__dirname, 'fixtures/typescript/package.json')) - expect(consoleLogStub.firstCall.firstArg).to.equal('running Baz') + const {stdout} = await runCommand(['foo', 'baz'], {root: resolve(__dirname, 'fixtures/typescript/package.json')}) + expect(stdout).to.equal('running Baz\n') }) it('should run foo:bar:succeed with space separator', async () => { - const consoleLogStub = sandbox.stub(console, 'log').returns() - await run(['foo', 'bar', 'succeed'], resolve(__dirname, 'fixtures/typescript/package.json')) - expect(consoleLogStub.firstCall.firstArg).to.equal('it works!') + const {stdout} = await runCommand(['foo', 'bar', 'succeed'], { + root: resolve(__dirname, 'fixtures/typescript/package.json'), + }) + expect(stdout).to.equal('it works!\n') }) }) diff --git a/test/command/single-command-cli.test.ts b/test/command/single-command-cli.test.ts index 59eaf3286..1fc6a6b09 100644 --- a/test/command/single-command-cli.test.ts +++ b/test/command/single-command-cli.test.ts @@ -1,26 +1,12 @@ import {expect} from 'chai' import {resolve} from 'node:path' -import {SinonSandbox, SinonStub, createSandbox} from 'sinon' -import stripAnsi from 'strip-ansi' -import {run, ux} from '../../src/index' +import {runCommand} from '../test' describe('single command cli', () => { - let sandbox: SinonSandbox - let stdoutStub: SinonStub - - beforeEach(() => { - sandbox = createSandbox() - stdoutStub = sandbox.stub(ux.write, 'stdout') - }) - - afterEach(() => { - sandbox.restore() - }) - it('should show help for commands', async () => { - await run(['--help'], resolve(__dirname, 'fixtures/single-cmd-cli/package.json')) - expect(stdoutStub.args.map((a) => stripAnsi(a[0])).join('')).to.equal(`Description of single command CLI. + const {stdout} = await runCommand(['--help'], {root: resolve(__dirname, 'fixtures/single-cmd-cli/package.json')}) + expect(stdout).to.equal(`Description of single command CLI. USAGE $ single-cmd-cli @@ -32,27 +18,17 @@ DESCRIPTION }) it('should run command', async () => { - await run([], resolve(__dirname, 'fixtures/single-cmd-cli/package.json')) - expect(stdoutStub.firstCall.firstArg).to.equal('hello world!\n') + const {stdout} = await runCommand([], {root: resolve(__dirname, 'fixtures/single-cmd-cli/package.json')}) + expect(stdout).to.equal('hello world!\n') }) }) describe('single command cli (deprecated)', () => { - let sandbox: SinonSandbox - let stdoutStub: SinonStub - - beforeEach(() => { - sandbox = createSandbox() - stdoutStub = sandbox.stub(ux.write, 'stdout') - }) - - afterEach(() => { - sandbox.restore() - }) - it('should show help for commands', async () => { - await run(['--help'], resolve(__dirname, 'fixtures/single-cmd-cli-deprecated/package.json')) - expect(stdoutStub.args.map((a) => stripAnsi(a[0])).join('')).to.equal(`Description of single command CLI. + const {stdout} = await runCommand(['--help'], { + root: resolve(__dirname, 'fixtures/single-cmd-cli-deprecated/package.json'), + }) + expect(stdout).to.equal(`Description of single command CLI. USAGE $ single-cmd-cli @@ -64,7 +40,7 @@ DESCRIPTION }) it('should run command', async () => { - await run([], resolve(__dirname, 'fixtures/single-cmd-cli-deprecated/package.json')) - expect(stdoutStub.firstCall.firstArg).to.equal('hello world!\n') + const {stdout} = await runCommand([], {root: resolve(__dirname, 'fixtures/single-cmd-cli-deprecated/package.json')}) + expect(stdout).to.equal('hello world!\n') }) }) diff --git a/test/config/config.flexible.test.ts b/test/config/config.flexible.test.ts index a2b0b7486..b8e8a0ff2 100644 --- a/test/config/config.flexible.test.ts +++ b/test/config/config.flexible.test.ts @@ -1,19 +1,15 @@ -import {join} from 'node:path' +import {expect} from 'chai' +import sinon from 'sinon' -import {Flags, Interfaces} from '../../src' +import {Flags} from '../../src' import {Command} from '../../src/command' import {Config} from '../../src/config/config' import {getCommandIdPermutations} from '../../src/config/util' import {Plugin as IPlugin} from '../../src/interfaces' import * as os from '../../src/util/os' -import {expect, fancy} from './test' interface Options { commandIds?: string[] - env?: {[k: string]: string} - homedir?: string - pjson?: any - platform?: string types?: string[] } @@ -34,18 +30,22 @@ class MyCommandClass extends Command { } describe('Config with flexible taxonomy', () => { - const testConfig = ({ - homedir = '/my/home', - platform = 'darwin', - env = {}, - commandIds = ['foo:bar', 'foo:baz'], - types = [], - }: Options = {}) => { - let test = fancy - .resetConfig() - .env(env, {clear: true}) - .stub(os, 'getHomeDir', (stub) => stub.returns(join(homedir))) - .stub(os, 'getPlatform', (stub) => stub.returns(platform)) + const originalEnv = {...process.env} + let sandbox: sinon.SinonSandbox + + beforeEach(() => { + sandbox = sinon.createSandbox() + process.env = {} + }) + + afterEach(() => { + process.env = originalEnv + sandbox.restore() + }) + + async function loadConfig({commandIds = ['foo:bar', 'foo:baz'], types = []}: Options = {}) { + sandbox.stub(os, 'getHomeDir').returns('/my/home') + sandbox.stub(os, 'getPlatform').returns('darwin') const load = async (): Promise => {} const findCommand = async (): Promise => MyCommandClass @@ -132,133 +132,129 @@ describe('Config with flexible taxonomy', () => { } const plugins = new Map().set(pluginA.name, pluginA).set(pluginB.name, pluginB) - test = test.add('config', async () => { - const config = await Config.load() - config.flexibleTaxonomy = true - config.plugins = plugins - config.pjson.oclif.plugins = ['@My/pluginb', '@My/plugina'] - config.pjson.dependencies = {'@My/pluginb': '0.0.0', '@My/plugina': '0.0.0'} - for (const plugin of config.plugins.values()) { - // @ts-expect-error private method - config.loadCommands(plugin) - // @ts-expect-error private method - config.loadTopics(plugin) - } - - return config - }) - // @ts-ignore - return { - it(expectation: string, fn: (config: Interfaces.Config) => any) { - test.do(({config}) => fn(config)).it(expectation) - return this - }, + const config = await Config.load() + config.flexibleTaxonomy = true + config.plugins = plugins + config.pjson.oclif.plugins = ['@My/pluginb', '@My/plugina'] + config.pjson.dependencies = {'@My/pluginb': '0.0.0', '@My/plugina': '0.0.0'} + for (const plugin of config.plugins.values()) { + // @ts-expect-error private method + config.loadCommands(plugin) + // @ts-expect-error private method + config.loadTopics(plugin) } + + return config } - 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 populated topic index', async () => { + const config = await loadConfig() + // @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', async () => { + const config = await loadConfig() + // @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', async () => { + const config = await loadConfig() + // @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', async () => { + const config = await loadConfig() + expect(config.getAllCommandIDs()).to.deep.equal(['foo:bar', 'foo:baz', 'bar:foo', 'baz:foo']) + }) + + describe('findMatches', () => { + it('finds command that contains a partial id', async () => { + const config = await loadConfig() + const matches = config.findMatches('foo', []) + expect(matches.length).to.equal(2) }) - .it('has all command id permutations', (config) => { - expect(config.getAllCommandIDs()).to.deep.equal(['foo:bar', 'foo:baz', 'bar:foo', 'baz:foo']) + + it('finds command that contains a partial id and matching full flag', async () => { + const config = await loadConfig() + const matches = config.findMatches('foo', ['--flagB']) + expect(matches.length).to.equal(1) + expect(matches[0].id).to.equal('foo:baz') }) - 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') - }) + it('finds command that contains a partial id and matching short flag', async () => { + const config = await loadConfig() + 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) => { + it('find command with no duplicates', async () => { + const config = await loadConfig() 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') - }, - ) + it('find command with duplicates and choose the one that appears first in oclif.plugins', async () => { + const config = await loadConfig({commandIds: ['foo:bar', 'foo:bar']}) + 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) => { + it('find command with no duplicates core/user', async () => { + const config = await loadConfig({types: ['core', 'user']}) 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) => { + it('find command with no duplicates user/core', async () => { + const config = await loadConfig({types: ['user', 'core']}) 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') - }, - ) + it('find command with duplicates core/user', async () => { + const config = await loadConfig({commandIds: ['foo:bar', 'foo:bar'], types: ['core', 'user']}) + 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') - }, - ) + it('find command with duplicates user/core', async () => { + const config = await loadConfig({commandIds: ['foo:bar', 'foo:bar'], types: ['user', 'core']}) + 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') - }, - ) + it('find command with duplicates user/user', async () => { + const config = await loadConfig({commandIds: ['foo:bar', 'foo:bar'], types: ['user', 'user']}) + 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.shell.test.ts b/test/config/config.shell.test.ts index d6d3c90dc..cb8ce96e2 100644 --- a/test/config/config.shell.test.ts +++ b/test/config/config.shell.test.ts @@ -9,7 +9,7 @@ const getShell = () => osUserInfo().shell?.split(sep)?.pop() || 'unknown' describe('config shell', () => { it('has a default shell', () => { const config = new Config({root: '/tmp'}) - // @ts-ignore + // @ts-expect-error because _shell is private expect(config._shell()).to.equal(getShell(), `SHELL: ${process.env.SHELL} COMSPEC: ${process.env.COMSPEC}`) }) }) diff --git a/test/config/config.test.ts b/test/config/config.test.ts index c8b2805ff..9c07fe07f 100644 --- a/test/config/config.test.ts +++ b/test/config/config.test.ts @@ -1,18 +1,15 @@ -import {join} from 'node:path' +import {expect} from 'chai' +import {join, resolve} from 'node:path' +import sinon from 'sinon' import {Config, Interfaces} from '../../src' import {Command} from '../../src/command' import {Plugin as IPlugin} from '../../src/interfaces' import * as fs from '../../src/util/fs' import * as os from '../../src/util/os' -import {expect, fancy} from './test' interface Options { commandIds?: string[] - env?: {[k: string]: string} - homedir?: string - pjson?: any - platform?: string types?: string[] } @@ -45,191 +42,216 @@ const pjson = { } describe('Config', () => { - const testConfig = ({pjson, homedir = '/my/home', platform = 'darwin', env = {}}: Options = {}, theme?: any) => { - let test = fancy - .resetConfig() - .env(env, {clear: true}) - .stub(os, 'getHomeDir', (stub) => stub.returns(join(homedir))) - .stub(os, 'getPlatform', (stub) => stub.returns(platform)) - - if (theme) test = test.stub(fs, 'safeReadJson', (stub) => stub.resolves(theme)) - 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) => { - // 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. - // @ts-expect-error because readonly property - config.version = config.version.replaceAll(/-beta\.\d/g, '') - // @ts-expect-error because readonly property - config.channel = 'stable' - - let {ext, ...options} = extra - options = { - bin: 'oclif-cli', - version: '1.0.0', - ext: '.tar.gz', - ...options, - } - const o = ext ? config.s3Key(k as any, ext, options) : config.s3Key(k, options) - expect(o).to.equal(expected) - }) - }, - hasProperty(k: K | undefined, v: Interfaces.Config[K] | undefined) { - 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) - return this - }, - } - } + let sandbox: sinon.SinonSandbox + const originalEnv = {...process.env} + const root = resolve(__dirname, '..') + beforeEach(() => { + sandbox = sinon.createSandbox() + process.env = {} + }) - 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')) + afterEach(() => { + sandbox.restore() + process.env = originalEnv }) describe('binAliases', () => { - testConfig({pjson}).it('will have binAliases set', (config) => { + it('will have binAliases set', async () => { + const config = await Config.load({pjson, root}) + expect(config.binAliases).to.deep.equal(['bar', 'baz']) }) - testConfig({pjson}).it('will get scoped env vars with bin aliases', (config) => { + it('will get scoped env vars with bin aliases', async () => { + const config = await Config.load({pjson, root}) expect(config.scopedEnvVarKeys('abc')).to.deep.equal(['FOO_ABC', 'BAR_ABC', 'BAZ_ABC']) }) - testConfig({pjson}).it('will get scoped env vars', (config) => { + it('will get scoped env vars', async () => { + const config = await Config.load({pjson, root}) expect(config.scopedEnvVarKey('abc')).to.equal('FOO_ABC') }) - testConfig({pjson}).it('will get scopedEnvVar', (config) => { + it('will get scopedEnvVar', async () => { process.env.FOO_ABC = 'find me' + const config = await Config.load({pjson, root}) expect(config.scopedEnvVar('abc')).to.deep.equal('find me') - delete process.env.FOO_ABC }) - testConfig({pjson}).it('will get scopedEnvVar via alias', (config) => { + it('will get scopedEnvVar via alias', async () => { process.env.BAZ_ABC = 'find me' + const config = await Config.load({pjson, root}) expect(config.scopedEnvVar('abc')).to.deep.equal('find me') - delete process.env.BAZ_ABC }) - testConfig({pjson}).it('will get scoped env vars', (config) => { + it('will get scoped env vars', async () => { + const config = await Config.load({pjson, root}) expect(config.scopedEnvVarKey('abc')).to.equal('FOO_ABC') }) - testConfig({pjson}).it('will get scopedEnvVarTrue', (config) => { + it('will get scopedEnvVarTrue', async () => { process.env.FOO_ABC = 'true' + const config = await Config.load({pjson, root}) expect(config.scopedEnvVarTrue('abc')).to.equal(true) - delete process.env.FOO_ABC }) - testConfig({pjson}).it('will get scopedEnvVarTrue via alias', (config) => { + it('will get scopedEnvVarTrue via alias', async () => { process.env.BAR_ABC = 'true' + const config = await Config.load({pjson, root}) expect(config.scopedEnvVarTrue('abc')).to.equal(true) - delete process.env.BAR_ABC }) - testConfig({pjson}).it('will get scopedEnvVarTrue=1', (config) => { + it('will get scopedEnvVarTrue=1', async () => { process.env.FOO_ABC = '1' + const config = await Config.load({pjson, root}) expect(config.scopedEnvVarTrue('abc')).to.equal(true) - delete process.env.FOO_ABC }) - testConfig({pjson}).it('will get scopedEnvVarTrue=1 via alias', (config) => { + it('will get scopedEnvVarTrue=1 via alias', async () => { process.env.BAR_ABC = '1' + const config = await Config.load({pjson, root}) expect(config.scopedEnvVarTrue('abc')).to.equal(true) - delete process.env.BAR_ABC + }) + }) + + describe('darwin', () => { + it('should have darwin specific paths', async () => { + sandbox.stub(os, 'getHomeDir').returns(join('/my/home')) + sandbox.stub(os, 'getPlatform').returns('darwin') + const config = await Config.load() + + expect(config).to.have.property('cacheDir', join('/my/home/Library/Caches/@oclif/core')) + expect(config).to.have.property('configDir', join('/my/home/.config/@oclif/core')) + expect(config).to.have.property('errlog', join('/my/home/Library/Caches/@oclif/core/error.log')) + expect(config).to.have.property('dataDir', join('/my/home/.local/share/@oclif/core')) + expect(config).to.have.property('home', join('/my/home')) }) }) 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')) + it('should have linux specific paths', async () => { + sandbox.stub(os, 'getHomeDir').returns(join('/my/home')) + sandbox.stub(os, 'getPlatform').returns('linux') + const config = await Config.load() + + expect(config).to.have.property('cacheDir', join('/my/home/.cache/@oclif/core')) + expect(config).to.have.property('configDir', join('/my/home/.config/@oclif/core')) + expect(config).to.have.property('errlog', join('/my/home/.cache/@oclif/core/error.log')) + expect(config).to.have.property('dataDir', join('/my/home/.local/share/@oclif/core')) + expect(config).to.have.property('home', join('/my/home')) + }) }) describe('win32', () => { - testConfig({ - platform: 'win32', - env: {LOCALAPPDATA: '/my/home/localappdata'}, + it('should have win32 specific paths', async () => { + sandbox.stub(os, 'getHomeDir').returns(join('/my/home')) + sandbox.stub(os, 'getPlatform').returns('win32') + process.env.LOCALAPPDATA = '/my/home/localappdata' + const config = await Config.load() + + expect(config).to.have.property('cacheDir', join('/my/home/localappdata/@oclif\\core')) + expect(config).to.have.property('configDir', join('/my/home/localappdata/@oclif\\core')) + expect(config).to.have.property('errlog', join('/my/home/localappdata/@oclif\\core/error.log')) + expect(config).to.have.property('dataDir', join('/my/home/localappdata/@oclif\\core')) + expect(config).to.have.property('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', () => { + describe('s3Key', async () => { 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, + let config: Config + + before(async () => { + config = await Config.load() + // 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. + config.version = config.version.replaceAll(/-beta\.\d/g, '') + config.channel = 'stable' + }) + + const tests: Array<{ + key: keyof Interfaces.PJSON.S3.Templates + expected: string + extra?: Record & {ext?: '.tar.gz' | '.tar.xz' | Interfaces.Config.s3Key.Options} + }> = [ + {key: 'baseDir', expected: 'oclif-cli'}, + {key: 'manifest', expected: 'version'}, + {key: 'manifest', expected: 'channels/beta/version', extra: beta}, + {key: 'manifest', expected: 'darwin-x64', extra: target}, + {key: 'manifest', expected: 'channels/beta/darwin-x64', extra: {...beta, ...target}}, + {key: 'unversioned', expected: 'oclif-cli.tar.gz'}, + {key: 'unversioned', expected: 'oclif-cli.tar.gz'}, + {key: 'unversioned', expected: 'channels/beta/oclif-cli.tar.gz', extra: beta}, + {key: 'unversioned', expected: 'channels/beta/oclif-cli.tar.gz', extra: beta}, + {key: 'unversioned', expected: 'oclif-cli-darwin-x64.tar.gz', extra: target}, + {key: 'unversioned', expected: 'oclif-cli-darwin-x64.tar.gz', extra: target}, + {key: 'unversioned', expected: 'channels/beta/oclif-cli-darwin-x64.tar.gz', extra: {...beta, ...target}}, + {key: 'unversioned', expected: 'channels/beta/oclif-cli-darwin-x64.tar.gz', extra: {...beta, ...target}}, + {key: 'versioned', expected: 'oclif-cli-v1.0.0/oclif-cli-v1.0.0.tar.gz'}, + {key: 'versioned', expected: 'oclif-cli-v1.0.0/oclif-cli-v1.0.0-darwin-x64.tar.gz', extra: target}, + {key: 'versioned', expected: 'channels/beta/oclif-cli-v2.0.0-beta/oclif-cli-v2.0.0-beta.tar.gz', extra: beta}, + { + key: 'versioned', + expected: 'channels/beta/oclif-cli-v2.0.0-beta/oclif-cli-v2.0.0-beta-darwin-x64.tar.gz', + extra: {...beta, ...target}, + }, + ] + + for (const testCase of tests) { + const {key, expected, extra} = testCase + let {ext, ...options} = extra ?? {} + options = { + bin: 'oclif-cli', + version: '1.0.0', + ext: '.tar.gz', + ...options, + } + it(`renders ${key} template as ${expected}`, () => { + const o = ext ? config.s3Key(key, ext, options) : config.s3Key(key, options) + expect(o).to.equal(expected) }) + } }) describe('options', () => { it('should set the channel and version', async () => { - const config = new Config({root: process.cwd(), channel: 'test-channel', version: '0.1.2-test'}) - await config.load() + const config = await Config.load({root, channel: 'test-channel', version: '0.1.2-test'}) expect(config).to.have.property('channel', 'test-channel') expect(config).to.have.property('version', '0.1.2-test') }) }) - testConfig().it('has s3Url', (config) => { - const orig = config.pjson.oclif.update.s3.host - config.pjson.oclif.update.s3.host = 'https://bar.com/a/' + it('has s3Url', async () => { + const config = await Config.load({ + root, + pjson: { + ...pjson, + oclif: { + ...pjson.oclif, + update: { + // @ts-expect-error - not worth stubbing out every single required prop on s3 + s3: { + host: 'https://bar.com/a/', + }, + }, + }, + }, + }) expect(config.s3Url('/b/c')).to.equal('https://bar.com/a/b/c') - config.pjson.oclif.update.s3.host = orig }) - testConfig({ - pjson, - }).it('has subtopics', (config) => { + it('has subtopics', async () => { + const config = await Config.load({root, pjson}) 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, - homedir = '/my/home', - platform = 'darwin', - env = {}, - commandIds = ['foo:bar', 'foo:baz'], - types = [], - }: Options = {}) => { + async function loadConfig({commandIds = ['foo:bar', 'foo:baz'], types = []}: Options = {}) { + sandbox.stub(os, 'getHomeDir').returns('/my/home') + sandbox.stub(os, 'getPlatform').returns('darwin') + class MyCommandClass extends Command { aliases: string[] = [] @@ -322,129 +344,125 @@ describe('Config', () => { commandsDir: './lib/commands', } const plugins = new Map().set(pluginA.name, pluginA).set(pluginB.name, pluginB) - let test = fancy - .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', async () => { - const config = await Config.load() - config.plugins = plugins - config.pjson.oclif.plugins = ['@My/pluginb', '@My/plugina'] - config.pjson.dependencies = {'@My/pluginb': '0.0.0', '@My/plugina': '0.0.0'} - for (const plugin of config.plugins.values()) { - // @ts-expect-error private method - config.loadCommands(plugin) - // @ts-expect-error private method - config.loadTopics(plugin) - } - return config + const config = await Config.load({ + root, + pjson, }) - // @ts-ignore - return { - it(expectation: string, fn: (config: Interfaces.Config) => any) { - test.do(({config}) => fn(config)).it(expectation) - return this + config.plugins = plugins + config.pjson = { + ...pjson, + dependencies: { + '@My/pluginb': '0.0.0', + '@My/plugina': '0.0.0', }, + oclif: { + ...pjson.oclif, + plugins: ['@My/pluginb', '@My/plugina'], + }, + } + for (const plugin of config.plugins.values()) { + // @ts-expect-error private method + config.loadCommands(plugin) + // @ts-expect-error private method + config.loadTopics(plugin) } + + return config } - findCommandTestConfig().it('find command with no duplicates', (config) => { + it('find command with no duplicates', async () => { + const config = await loadConfig() 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) => { + + it('find command with duplicates and choose the one that appears first in oclif.plugins', async () => { + const config = await loadConfig({commandIds: ['foo:bar', 'foo:bar']}) + const command = config.findCommand('foo:bar', {must: true}) + expect(command).to.have.property('pluginAlias', '@My/pluginb') + }) + + it('find command with no duplicates core/user', async () => { + const config = await loadConfig({types: ['core', 'user']}) 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) => { + + it('find command with no duplicates user/core', async () => { + const config = await loadConfig({types: ['user', 'core']}) + 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') + }) + + it('find command with duplicates core/user', async () => { + const config = await loadConfig({commandIds: ['foo:bar', 'foo:bar'], types: ['core', 'user']}) + 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') + }) + + it('find command with duplicates user/core', async () => { + const config = await loadConfig({commandIds: ['foo:bar', 'foo:bar'], types: ['user', 'core']}) + 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') + }) + + it('find command with duplicates user/user', async () => { + const config = await loadConfig({commandIds: ['foo:bar', 'foo:bar'], types: ['user', 'user']}) 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') - }, - ) }) describe('theme', () => { - testConfig({pjson, env: {FOO_DISABLE_THEME: 'true'}}, {bin: '#FF0000'}).it( - 'should not be set when DISABLE_THEME is true and theme.json exists', - (config) => { - expect(config).to.have.property('theme', undefined) - }, - ) + it('should not be set when DISABLE_THEME is true and theme.json exists', async () => { + process.env.FOO_DISABLE_THEME = 'true' + sandbox.stub(fs, 'safeReadJson').resolves({bin: '#FF0000'}) + const config = await Config.load({root, pjson}) + expect(config).to.have.property('theme', undefined) + }) - testConfig({pjson, env: {FOO_DISABLE_THEME: 'false'}}, {bin: '#FF0000'}).it( - 'should be set when DISABLE_THEME is false and theme.json exists', - (config) => { - expect(config.theme).to.have.property('bin', '#FF0000') - }, - ) + it('should be set when DISABLE_THEME is false and theme.json exists', async () => { + process.env.FOO_DISABLE_THEME = 'false' + sandbox.stub(fs, 'safeReadJson').resolves({bin: '#FF0000'}) + const config = await Config.load({root, pjson}) + expect(config.theme).to.have.property('bin', '#FF0000') + }) - testConfig({pjson, env: {}}, {bin: '#FF0000'}).it( - 'should be set when DISABLE_THEME is unset and theme.json exists', - (config) => { - expect(config.theme).to.have.property('bin', '#FF0000') - }, - ) + it('should be set when DISABLE_THEME is unset and theme.json exists', async () => { + sandbox.stub(fs, 'safeReadJson').resolves({bin: '#FF0000'}) + const config = await Config.load({root, pjson}) + expect(config.theme).to.have.property('bin', '#FF0000') + }) - testConfig({pjson, env: {FOO_DISABLE_THEME: 'true'}}).it( - 'should not be set when DISABLE_THEME is true and theme.json does not exist', - (config) => { - expect(config).to.have.property('theme', undefined) - }, - ) + it('should not be set when DISABLE_THEME is true and theme.json does not exist', async () => { + process.env.FOO_DISABLE_THEME = 'true' + sandbox.stub(fs, 'safeReadJson').resolves() + const config = await Config.load({root, pjson}) + expect(config).to.have.property('theme', undefined) + }) - testConfig({pjson, env: {FOO_DISABLE_THEME: 'false'}}).it( - 'should not be set when DISABLE_THEME is false and theme.json does not exist', - (config) => { - expect(config).to.have.property('theme', undefined) - }, - ) + it('should not be set when DISABLE_THEME is false and theme.json does not exist', async () => { + process.env.FOO_DISABLE_THEME = 'false' + sandbox.stub(fs, 'safeReadJson').resolves() + const config = await Config.load({root, pjson}) + expect(config).to.have.property('theme', undefined) + }) - testConfig({pjson, env: {}}).it( - 'should not be set when DISABLE_THEME is unset and theme.json does not exist', - (config) => { - expect(config).to.have.property('theme', undefined) - }, - ) + it('should not be set when DISABLE_THEME is unset and theme.json does not exist', async () => { + sandbox.stub(fs, 'safeReadJson').resolves() + const config = await Config.load({root, pjson}) + expect(config).to.have.property('theme', undefined) + }) }) }) diff --git a/test/config/esm.test.ts b/test/config/esm.test.ts index 1424f848d..89e62415d 100644 --- a/test/config/esm.test.ts +++ b/test/config/esm.test.ts @@ -1,46 +1,42 @@ +import {expect} from 'chai' import {join, resolve} from 'node:path' -import * as url from 'node:url' +import url from 'node:url' import {Config} from '../../src/config' -import {expect, fancy} from './test' +import {runCommand, runHook} from '../test' const root = resolve(__dirname, 'fixtures/esm') -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)) - describe('esm', () => { - withConfig.it('has commandsDir', ({config}) => { + it('has commandsDir', async () => { + const config = await Config.load(rootAsFileURL) expect([...config.plugins.values()][0]).to.deep.include({ - commandsDir: p('src/commands'), + commandsDir: join(root, 'src/commands'), }) }) - 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') + it('runs esm command and prerun & postrun hooks', async () => { + const {stdout} = await runCommand(['foo:bar:baz'], root) + expect(stdout).to.equal('running esm init hook\nrunning esm prerun hook\nit works!\nrunning esm postrun hook\n') }) - withConfig.stdout().it('runs faulty command, only prerun hook triggers', async (ctx) => { - try { - await ctx.config.runCommand('foo:bar:fail') - } catch { - console.log('caught error') - } - - expect(ctx.stdout).to.equal('running esm prerun hook\nit fails!\ncaught error\n') + it('runs faulty command, only prerun hook triggers', async () => { + const {stdout} = await runCommand(['foo:bar:fail'], root) + expect(stdout).to.equal('running esm init hook\nrunning esm prerun hook\nit fails!\n') }) - 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') + it('runs esm command, postrun hook captures command result', async () => { + const {stdout} = await runCommand(['foo:bar:test-result'], root) + expect(stdout).to.equal( + 'running esm init hook\nrunning esm prerun hook\nit works!\nrunning esm postrun hook\nreturned success!\n', + ) }) - 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') + it('runs init hook', async () => { + const {stdout} = await runHook('init', {id: 'myid', argv: ['foo']}, {root}) + expect(stdout).to.equal('running esm init hook\n') }) }) diff --git a/test/config/help.config.test.ts b/test/config/help.config.test.ts index 2d9499bbd..413ed9435 100644 --- a/test/config/help.config.test.ts +++ b/test/config/help.config.test.ts @@ -1,21 +1,24 @@ +import {expect} from 'chai' import {resolve} from 'node:path' import {pathToFileURL} from 'node:url' import {Config} from '../../src/config' import {getHelpFlagAdditions} from '../../src/help' import {helpAddition, versionAddition} from '../../src/main' -import {expect, fancy} from './test' const root = resolve(__dirname, 'fixtures/help') -// const p = (p: string) => path.join(root, p) // This tests file URL / import.meta.url simulation. const rootAsFileURL = pathToFileURL(root).toString() -const withConfig = fancy.add('config', () => Config.load(rootAsFileURL)) - describe('help and version flag additions', () => { - withConfig.it('has help and version additions', ({config}) => { + let config: Config + + beforeEach(async () => { + config = await Config.load(rootAsFileURL) + }) + + it('has help and version additions', () => { expect(config.pjson.oclif.additionalHelpFlags).to.have.lengthOf(2) expect(config.pjson.oclif.additionalVersionFlags).to.have.lengthOf(3) const mergedHelpFlags = getHelpFlagAdditions(config) @@ -30,19 +33,18 @@ describe('help and version flag additions', () => { expect(versionAddition(['notmyversion'], config)).to.be.false }) - 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 - }) + it('has version additions', () => { + delete config.pjson.oclif.additionalHelpFlags + 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 386683765..e30feac59 100644 --- a/test/config/mixed-cjs-esm.test.ts +++ b/test/config/mixed-cjs-esm.test.ts @@ -1,44 +1,40 @@ +import {expect} from 'chai' import {join, resolve} from 'node:path' import {Config} from '../../src/config' -import {expect, fancy} from './test' +import {runCommand, runHook} 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)) describe('mixed-cjs-esm', () => { - withConfig.it('has commandsDir', ({config}) => { + it('has commandsDir', async () => { + const config = await Config.load(root) expect([...config.plugins.values()][0]).to.deep.include({ - commandsDir: p('src/commands'), + commandsDir: join(root, 'src/commands'), }) }) - 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') + it('runs mixed-cjs-esm command and prerun & postrun hooks', async () => { + const {stdout} = await runCommand(['foo:bar:baz'], root) + expect(stdout).to.equal( + 'running mixed-cjs-esm init hook\nrunning 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) => { - try { - await ctx.config.runCommand('foo:bar:fail') - } catch { - console.log('caught error') - } - - expect(ctx.stdout).to.equal('running mixed-cjs-esm prerun hook\nit fails!\ncaught error\n') + it('runs faulty command, only prerun hook triggers', async () => { + const {stdout} = await runCommand(['foo:bar:fail'], root) + expect(stdout).to.equal('running mixed-cjs-esm init hook\nrunning mixed-cjs-esm prerun hook\nit fails!\n') }) - 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', + it('runs mixed-cjs-esm command, postrun hook captures command result', async () => { + const {stdout} = await runCommand(['foo:bar:test-result'], root) + expect(stdout).to.equal( + 'running mixed-cjs-esm init hook\nrunning mixed-cjs-esm prerun hook\nit works!\nrunning mixed-cjs-esm postrun hook\nreturned success!\n', ) }) - 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') + it('runs init hook', async () => { + const {stdout} = await runHook('init', {id: 'myid', argv: ['foo']}, {root}) + expect(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 3df7db923..96e07ec61 100644 --- a/test/config/mixed-esm-cjs.test.ts +++ b/test/config/mixed-esm-cjs.test.ts @@ -1,44 +1,40 @@ +import {expect} from 'chai' import {join, resolve} from 'node:path' import {Config} from '../../src/config' -import {expect, fancy} from './test' +import {runCommand, runHook} 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)) - -describe('mixed-cjs-esm', () => { - withConfig.it('has commandsDir', ({config}) => { +describe('mixed-esm-cjs', () => { + it('has commandsDir', async () => { + const config = await Config.load(root) expect([...config.plugins.values()][0]).to.deep.include({ - commandsDir: p('src/commands'), + commandsDir: join(root, 'src/commands'), }) }) - 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') + it('runs mixed-esm-cjs command and prerun & postrun hooks', async () => { + const {stdout} = await runCommand(['foo:bar:baz'], root) + expect(stdout).to.equal( + 'running mixed-esm-cjs init hook\nrunning 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) => { - try { - await ctx.config.runCommand('foo:bar:fail') - } catch { - console.log('caught error') - } - - expect(ctx.stdout).to.equal('running mixed-esm-cjs prerun hook\nit fails!\ncaught error\n') + it('runs faulty command, only prerun hook triggers', async () => { + const {stdout} = await runCommand(['foo:bar:fail'], root) + expect(stdout).to.equal('running mixed-esm-cjs init hook\nrunning mixed-esm-cjs prerun hook\nit fails!\n') }) - 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', + it('runs mixed-esm-cjs command, postrun hook captures command result', async () => { + const {stdout} = await runCommand(['foo:bar:test-result'], root) + expect(stdout).to.equal( + 'running mixed-esm-cjs init hook\nrunning mixed-esm-cjs prerun hook\nit works!\nrunning mixed-esm-cjs postrun hook\nreturned success!\n', ) }) - 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') + it('runs init hook', async () => { + const {stdout} = await runHook('init', {id: 'myid', argv: ['foo']}, {root}) + expect(stdout).to.equal('running mixed-esm-cjs init hook\n') }) }) diff --git a/test/config/test.ts b/test/config/test.ts deleted file mode 100644 index fe843c7dd..000000000 --- a/test/config/test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import {fancy as base} from 'fancy-test' - -import {Interfaces} from '../../src' - -export const fancy = base.register('resetConfig', () => ({ - run(ctx: {config: Interfaces.Config}) { - // @ts-ignore - delete ctx.config - }, -})) - -export {FancyTypes, expect} from 'fancy-test' diff --git a/test/config/typescript.test.ts b/test/config/typescript.test.ts index 441e9fb75..7d3d16d7c 100644 --- a/test/config/typescript.test.ts +++ b/test/config/typescript.test.ts @@ -1,42 +1,38 @@ +import {expect} from 'chai' import {join, resolve} from 'node:path' import {Config} from '../../src/config' -import {expect, fancy} from './test' +import {runCommand, runHook} from '../test' const root = resolve(__dirname, 'fixtures/typescript') -const p = (p: string) => join(root, p) - -const withConfig = fancy.add('config', () => Config.load(root)) describe('typescript', () => { - withConfig.it('has commandsDir', ({config}) => { + it('has commandsDir', async () => { + const config = await Config.load(root) expect([...config.plugins.values()][0]).to.deep.include({ - commandsDir: p('src/commands'), + commandsDir: join(root, 'src/commands'), }) }) - 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') + it('runs ts command and prerun & postrun hooks', async () => { + const {stdout} = await runCommand(['foo:bar:baz'], root) + expect(stdout).to.equal('running ts init hook\nrunning ts prerun hook\nit works!\nrunning ts postrun hook\n') }) - withConfig.stdout().it('runs faulty command, only prerun hook triggers', async (ctx) => { - try { - await ctx.config.runCommand('foo:bar:fail') - } catch { - console.log('caught error') - } - - expect(ctx.stdout).to.equal('running ts prerun hook\nit fails!\ncaught error\n') + it('runs faulty command, only prerun hook triggers', async () => { + const {stdout} = await runCommand(['foo:bar:fail'], root) + expect(stdout).to.equal('running ts init hook\nrunning ts prerun hook\nit fails!\n') }) - 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') + it('runs ts command, postrun hook captures command result', async () => { + const {stdout} = await runCommand(['foo:bar:test-result'], root) + expect(stdout).to.equal( + 'running ts init hook\nrunning ts prerun hook\nit works!\nrunning ts postrun hook\nreturned success!\n', + ) }) - withConfig.stdout().it('runs init hook', async (ctx) => { - await ctx.config.runHook('init', {id: 'myid', argv: ['foo']}) - expect(ctx.stdout).to.equal('running ts init hook\n') + it('runs init hook', async () => { + const {stdout} = await runHook('init', {id: 'myid', argv: ['foo']}, {root}) + expect(stdout).to.equal('running ts init hook\n') }) }) diff --git a/test/config/util.test.ts b/test/config/util.test.ts index 60e7f4e4d..c49ea0e35 100644 --- a/test/config/util.test.ts +++ b/test/config/util.test.ts @@ -1,17 +1,17 @@ -import {expect, test} from '@oclif/test' +import {expect} from 'chai' import {collectUsableIds, getCommandIdPermutations} from '../../src/config/util' describe('util', () => { describe('collectUsableIds', () => { - test.it('returns all usable command ids', async () => { + 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'])) }) }) describe('getCommandIdPermutations', () => { - test.it('returns all usable command ids', async () => { + it('returns all usable command ids', async () => { const permutations = getCommandIdPermutations('foo:bar:baz') expect(permutations).to.deep.equal([ 'foo:bar:baz', @@ -31,7 +31,7 @@ describe('util', () => { return result } - test.it('returns the correct number of permutations', async () => { + 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')) diff --git a/test/config/wildcard-plugins.test.ts b/test/config/wildcard-plugins.test.ts index f381d9285..1d6b189c6 100644 --- a/test/config/wildcard-plugins.test.ts +++ b/test/config/wildcard-plugins.test.ts @@ -1,34 +1,22 @@ import {expect} from 'chai' import {resolve} from 'node:path' -import {SinonSandbox, createSandbox} from 'sinon' -import {run, ux} from '../../src/index' +import {runCommand} from '../test' describe('plugins defined as patterns in package.json', () => { - let sandbox: SinonSandbox - - beforeEach(() => { - sandbox = createSandbox() - sandbox.stub(ux.write, 'stdout') - }) - - afterEach(() => { - sandbox.restore() - }) - it('should load all core plugins in dependencies that match pattern', async () => { - const result = (await run(['plugins', '--core'], { + const {result} = await runCommand>(['plugins', '--core'], { root: resolve(__dirname, 'fixtures/wildcard/package.json'), pluginAdditions: { core: ['@oclif/plugin-*'], path: resolve(__dirname, '..', '..'), }, - })) as Array<{name: string; type: string}> + }) - expect(result.length).to.equal(3) - const rootPlugin = result.find((r) => r.name === 'wildcard-plugins-fixture') - const pluginHelp = result.find((r) => r.name === '@oclif/plugin-help') - const pluginPlugins = result.find((r) => r.name === '@oclif/plugin-plugins') + expect(result?.length).to.equal(3) + const rootPlugin = result?.find((r) => r.name === 'wildcard-plugins-fixture') + const pluginHelp = result?.find((r) => r.name === '@oclif/plugin-help') + const pluginPlugins = result?.find((r) => r.name === '@oclif/plugin-plugins') expect(rootPlugin).to.exist expect(pluginHelp).to.exist @@ -36,18 +24,18 @@ describe('plugins defined as patterns in package.json', () => { }) it('should load all dev plugins in dependencies and devDependencies that match pattern', async () => { - const result = (await run(['plugins', '--core'], { + const {result} = await runCommand>(['plugins', '--core'], { root: resolve(__dirname, 'fixtures/wildcard/package.json'), pluginAdditions: { dev: ['@oclif/plugin-*'], path: resolve(__dirname, '..', '..'), }, - })) as Array<{name: string; type: string}> + }) - expect(result.length).to.equal(3) - const rootPlugin = result.find((r) => r.name === 'wildcard-plugins-fixture') - const pluginHelp = result.find((r) => r.name === '@oclif/plugin-help') - const pluginPlugins = result.find((r) => r.name === '@oclif/plugin-plugins') + expect(result?.length).to.equal(3) + const rootPlugin = result?.find((r) => r.name === 'wildcard-plugins-fixture') + const pluginHelp = result?.find((r) => r.name === '@oclif/plugin-help') + const pluginPlugins = result?.find((r) => r.name === '@oclif/plugin-plugins') expect(rootPlugin).to.exist expect(pluginHelp).to.exist diff --git a/test/errors/error.test.ts b/test/errors/error.test.ts index 9bb94c022..a1c3524de 100644 --- a/test/errors/error.test.ts +++ b/test/errors/error.test.ts @@ -1,96 +1,96 @@ -import {expect, fancy} from 'fancy-test' +import {expect} from 'chai' import {error} from '../../src/errors' import {PrettyPrintableError} from '../../src/interfaces/errors' +import {captureOutput} from '../test' + +function isPrettyPrintableError(error: any): error is PrettyPrintableError { + return error.code !== undefined && error.ref !== undefined && error.suggestions !== undefined +} describe('error', () => { - fancy - .do(() => { + it('throws an error using a string argument', () => { + expect(() => { error('An error happened!') - }) - .catch((error: PrettyPrintableError) => { - expect(error.message).to.equal('An error happened!') - }) - .it('throws an error using a string argument') + }).to.throw('An error happened!') + }) - fancy - .do(() => { + it('attaches pretty print properties to a new error from options', () => { + try { 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') + } catch (error) { + if (isPrettyPrintableError(error)) { + 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']) + } else { + throw new Error('error is not a PrettyPrintableError') + } + } + }) - fancy - .do(() => { + it('attached pretty print properties from options to an existing error object', () => { + try { 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') + } catch (error) { + if (isPrettyPrintableError(error)) { + 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']) + } else { + throw new Error('error is not a PrettyPrintableError') + } + } + }) - fancy - .do(() => { - const e: any = new Error('An existing error object error!') - e.code = 'ORIG_ERR' - e.ref = 'ORIG_REF' - e.suggestions = ['ORIG_SUGGESTION'] + it('preserves original pretty printable properties and is not overwritten by options', () => { + const e: any = new Error('An existing error object error!') + e.code = 'ORIG_ERR' + e.ref = 'ORIG_REF' + e.suggestions = ['ORIG_SUGGESTION'] + + try { 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') + } catch (error) { + if (isPrettyPrintableError(error)) { + expect(error.code).to.equal('ORIG_ERR') + expect(error.ref).to.equal('ORIG_REF') + expect(error.suggestions).to.deep.equal(['ORIG_SUGGESTION']) + } else { + throw new Error('error is not a PrettyPrintableError') + } + } + }) - 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('') - }) + it('does not rethrow error when exit: false option is set', async () => { + const {stdout, stderr} = await captureOutput(async () => + error('an error is reported but is not rethrown', {exit: false}), + ) + expect(stderr).to.contain('Error: an error is reported but is not rethrown') + expect(stdout).to.be.empty + }) describe('applying oclif errors', () => { - fancy - .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') + it('adds oclif exit code to errors by default', async () => { + const {error: err} = await captureOutput(async () => error(new Error('An existing error object error!'))) + expect(err?.oclif?.exit).to.equal(2) + }) - 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') + it('applies the exit property on options to the error object', async () => { + const {error: err} = await captureOutput(async () => + error(new Error('An existing error object error!'), {exit: 9001}), + ) + expect(err?.oclif?.exit).to.equal(9001) + }) - fancy - .do(() => { + it('preserves original oclif exitable error properties and is not overwritten by options', async () => { + const {error: err} = await captureOutput(async () => { const e: any = new Error('An existing error object error!') e.oclif = { code: 'ORIG_EXIT_CODE', @@ -98,9 +98,9 @@ describe('error', () => { 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') + + // @ts-expect-error because we intentionally added a property that doesn't exist on Error + expect(err?.oclif?.code).to.equal('ORIG_EXIT_CODE') + }) }) }) diff --git a/test/errors/handle.test.ts b/test/errors/handle.test.ts index 88486ab17..7ec273b11 100644 --- a/test/errors/handle.test.ts +++ b/test/errors/handle.test.ts @@ -1,4 +1,4 @@ -import {expect, fancy} from 'fancy-test' +import {expect} from 'chai' import {readFileSync} from 'node:fs' import {join} from 'node:path' import * as process from 'node:process' @@ -6,6 +6,7 @@ import {SinonSandbox, SinonStub, createSandbox} from 'sinon' import {CLIError, ExitError, config, exit as exitErrorThrower} from '../../src/errors' import {Exit, handle} from '../../src/errors/handle' +import {captureOutput} from '../test' const errlog = join(__dirname, '../tmp/mytest/error.log') const x = process.platform === 'win32' ? '»' : '›' @@ -23,112 +24,94 @@ describe('handle', () => { sandbox.restore() }) - 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) - }) + it('hides an exit error', async () => { + const {stdout, stderr} = await captureOutput(() => handle(new ExitError(0))) + expect(stdout).to.be.empty + expect(stderr).to.be.empty + 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') - }) + it('prints error', async () => { + const error = new Error('foo bar baz') as Error & {skipOclifErrorHandling: boolean} + error.skipOclifErrorHandling = false - 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('') - }) + const {stdout, stderr} = await captureOutput(() => handle(error)) + + expect(stdout).to.be.empty + expect(stderr).to.include('foo bar baz') + }) - fancy - .stderr() - .do(() => { + it('should not print error when skipOclifErrorHandling is true', async () => { + const error = new Error('foo bar baz') as Error & {skipOclifErrorHandling: boolean} + error.skipOclifErrorHandling = true + const {stdout, stderr} = await captureOutput(() => handle(error)) + expect(stdout).to.be.empty + expect(stderr).to.be.empty + }) + + describe('errlog', () => { + beforeEach(() => { config.errlog = errlog }) - .finally(() => { + + afterEach(() => { 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`) + + it('logs when errlog is set', async () => { + const {stderr} = await captureOutput(() => handle(new CLIError('uh oh!'))) + expect(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) - }) + it('should use default exit code for Error (1)', async () => { + const error = new Error('foo bar baz') + const {stdout, stderr} = await captureOutput(() => handle(error)) + expect(stdout).to.be.empty + expect(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) - }) + it('should use default exit code for CLIError (2)', async () => { + const error = new CLIError('foo bar baz') + const {stdout, stderr} = await captureOutput(() => handle(error)) + expect(stdout).to.be.empty + expect(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) - }) + it('should use exit code provided by CLIError (0)', async () => { + const error = new CLIError('foo bar baz', {exit: 0}) + const {stdout, stderr} = await captureOutput(() => handle(error)) + expect(stdout).to.be.empty + expect(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) - }) + it('should use exit code provided by CLIError (9999)', async () => { + const error = new CLIError('foo bar baz', {exit: 9999}) + const {stdout, stderr} = await captureOutput(() => handle(error)) + expect(stdout).to.be.empty + expect(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) => { + it('exits without displaying anything', async () => { + const {stdout, stderr} = await captureOutput(async () => { 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(stdout).to.be.empty + expect(stderr).to.be.empty + expect(exitStub.firstCall.firstArg).to.equal(9000) + }) }) }) diff --git a/test/errors/pretty-print.test.ts b/test/errors/pretty-print.test.ts index 80b195eed..c75a2038d 100644 --- a/test/errors/pretty-print.test.ts +++ b/test/errors/pretty-print.test.ts @@ -1,45 +1,45 @@ -import {expect, fancy} from 'fancy-test' +import ansis from 'ansis' +import {expect} from 'chai' import {CLIError} from '../../src/errors' import {config} from '../../src/errors/config' import prettyPrint from '../../src/errors/errors/pretty-print' import {PrettyPrintableError} from '../../src/interfaces/errors' -const stripAnsi = require('strip-ansi') describe('pretty-print', () => { - fancy.it('pretty prints an error', async () => { + 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 + expect(ansis.strip(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 () => { + 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 + expect(ansis.strip(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 () => { + 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( + expect(ansis.strip(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 () => { + it('supports the bang property', async () => { class SampleCLIError extends CLIError { get bang() { return '>>>' @@ -47,14 +47,14 @@ describe('pretty-print', () => { } const sampleError = new SampleCLIError('This is a CLI error') - expect(stripAnsi(prettyPrint(sampleError))).to.equal(' >>> Error: This is a CLI error') + expect(ansis.strip(prettyPrint(sampleError) ?? '')).to.equal(' >>> Error: This is a CLI error') }) - fancy.it("supports the 'name' message prefix property", async () => { + 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' - expect(stripAnsi(prettyPrint(sampleError))).to.equal(` ${defaultBang} Errorz: This is a CLI error`) + expect(ansis.strip(prettyPrint(sampleError) ?? '')).to.equal(` ${defaultBang} Errorz: This is a CLI error`) }) }) @@ -70,7 +70,7 @@ describe('pretty-print', () => { config.debug = initialConfigDebug }) - fancy.it('shows the stack for an error', async () => { + 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 d51827aa5..4c0a50b03 100644 --- a/test/errors/warn.test.ts +++ b/test/errors/warn.test.ts @@ -1,24 +1,25 @@ -import {expect, fancy} from 'fancy-test' +import {expect} from 'chai' import {readFile} from 'node:fs/promises' import {join} from 'node:path' import {config, warn} from '../../src/errors' +import {captureOutput} from '../test' 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!') - }) + beforeEach(() => { + config.errlog = errlog + }) + + afterEach(() => { + config.errlog = undefined + }) + + it('warns', async () => { + const {stderr} = await captureOutput(async () => warn('foo!')) + expect(stderr).to.contain('Warning: foo!') + await config.errorLogger!.flush() + expect(await readFile(errlog, 'utf8')).to.contain('Warning: foo!') + }) }) diff --git a/test/help/format-commands.test.ts b/test/help/format-commands.test.ts index 9e7f475e4..54e09d289 100644 --- a/test/help/format-commands.test.ts +++ b/test/help/format-commands.test.ts @@ -11,6 +11,10 @@ import {makeLoadable} from './help-test-utils' // extensions to expose method as public for testing class TestHelp extends Help { + constructor(public config: Config) { + super(config, {stripAnsi: true}) + } + public formatCommands(commands: Command.Loadable[]) { return super.formatCommands(commands) } diff --git a/test/help/format-root.test.ts b/test/help/format-root.test.ts index 33210ae9b..5ea9cf9aa 100644 --- a/test/help/format-root.test.ts +++ b/test/help/format-root.test.ts @@ -1,140 +1,120 @@ -import {test as base, expect} from '@oclif/test' +import ansis from 'ansis' +import {expect} from 'chai' +import sinon from 'sinon' -import {Interfaces} from '../../src' +import {Config} from '../../src' import {Help} from '../../src/help' -import stripAnsi = require('strip-ansi') - -const g: any = global -g.oclif.columns = 80 - const VERSION = require('../../package.json').version -const UA = `@oclif/core/${VERSION} ${process.platform}-${process.arch} node-${process.version}` +const USER_AGENT = `@oclif/core/${VERSION} ${process.platform}-${process.arch} node-${process.version}` -// extensions to expose method as public for testing -class TestHelp extends Help { - public formatRoot() { - return super.formatRoot() - } -} +describe('formatRoot', () => { + let config: Config + let sandbox: sinon.SinonSandbox -const rootHelp = (ctxOverride?: (config: Interfaces.Config) => Interfaces.Config) => ({ - run(ctx: {config: Interfaces.Config; help: Help; commandHelp: string; expectation: string}) { - const config = ctxOverride ? ctxOverride(ctx.config) : ctx.config + beforeEach(async () => { + config = await Config.load() + sandbox = sinon.createSandbox() + }) - const help = new TestHelp(config as any) - const root = help.formatRoot() - if (process.env.TEST_OUTPUT === '1') { - console.log(help) - } + afterEach(() => { + sandbox.restore() + }) - ctx.commandHelp = stripAnsi(root) + function getRootHelp(): string { + // @ts-expect-error private member + const root = new Help(config).formatRoot() + return ansis + .strip(root) .split('\n') .map((s) => s.trimEnd()) .join('\n') - }, -}) - -const test = base.loadConfig().register('rootHelp', rootHelp) + } -describe('formatRoot', () => { - test.rootHelp().it('renders the root help', (ctx) => - expect(ctx.commandHelp).to.equal(`base library for oclif CLIs + it('renders the root help', async () => { + expect(getRootHelp()).to.equal(`base library for oclif CLIs VERSION - ${UA} + ${USER_AGENT} USAGE - $ oclif [COMMAND]`), - ) + $ oclif [COMMAND]`) + }) describe('description', () => { - test - .rootHelp((config) => ({ - ...config, - pjson: { - ...config.pjson, - description: - 'This is the top-level description that appears in the root\nThis appears in the description section after usage', - }, - })) - .it('splits on \\n for the description into the top-level and description sections', (ctx) => { - expect(ctx.commandHelp).to.equal(`This is the top-level description that appears in the root + it('splits on \\n for the description into the top-level and description sections', () => { + sandbox + .stub(config.pjson, 'description') + .value( + 'This is the top-level description that appears in the root\nThis appears in the description section after usage', + ) + + const output = getRootHelp() + + expect(output).to.equal(`This is the top-level description that appears in the root VERSION - ${UA} + ${USER_AGENT} USAGE $ oclif [COMMAND] DESCRIPTION This appears in the description section after usage`) - }) + }) - test - .rootHelp((config) => ({ - ...config, - pjson: { - ...config.pjson, - description: - 'This is the top-level description for <%= config.bin %>\nThis <%= config.bin %> appears in the description section after usage', - }, - })) - .it('shows description from a template', (ctx) => { - expect(ctx.commandHelp).to.equal(`This is the top-level description for oclif + it('shows description from a template', () => { + sandbox + .stub(config.pjson, 'description') + .value( + 'This is the top-level description for <%= config.bin %>\nThis <%= config.bin %> appears in the description section after usage', + ) + const output = getRootHelp() + + expect(output).to.equal(`This is the top-level description for oclif VERSION - ${UA} + ${USER_AGENT} USAGE $ oclif [COMMAND] DESCRIPTION This oclif appears in the description section after usage`) + }) + + it('prefers the oclif description over the package.json description', () => { + sandbox.stub(config.pjson, 'description').value('THIS IS THE PJSON DESCRIPTION') + sandbox.stub(config.pjson, 'oclif').value({ + ...config.pjson.oclif, + description: 'THIS IS THE OCLIF DESCRIPTION IN PJSON', }) + const output = getRootHelp() - test - .rootHelp((config) => ({ - ...config, - pjson: { - ...config.pjson, - description: 'THIS IS THE PJSON DESCRIPTION', - oclif: { - ...config.pjson?.oclif, - description: 'THIS IS THE OCLIF DESCRIPTION IN PJSON', - }, - }, - })) - .it('prefers the oclif description over the package.json description', (ctx) => { - expect(ctx.commandHelp).to.equal(`THIS IS THE OCLIF DESCRIPTION IN PJSON + expect(output).to.equal(`THIS IS THE OCLIF DESCRIPTION IN PJSON VERSION - ${UA} + ${USER_AGENT} USAGE $ oclif [COMMAND]`) + }) + + it('uses package.json description when the oclif description is not set', () => { + sandbox.stub(config.pjson, 'description').value('THIS IS THE PJSON DESCRIPTION') + sandbox.stub(config.pjson, 'oclif').value({ + ...config.pjson.oclif, + description: undefined, }) + const output = getRootHelp() - test - .rootHelp((config) => ({ - ...config, - pjson: { - ...config.pjson, - description: 'THIS IS THE PJSON DESCRIPTION', - oclif: { - ...config.pjson?.oclif, - description: undefined, - }, - }, - })) - .it('uses package.json description when the oclif description is not set', (ctx) => { - expect(ctx.commandHelp).to.equal(`THIS IS THE PJSON DESCRIPTION + expect(output).to.equal(`THIS IS THE PJSON DESCRIPTION VERSION - ${UA} + ${USER_AGENT} USAGE $ oclif [COMMAND]`) - }) + }) }) }) diff --git a/test/help/format-topic.test.ts b/test/help/format-topic.test.ts index 63010639a..88c8a2bf5 100644 --- a/test/help/format-topic.test.ts +++ b/test/help/format-topic.test.ts @@ -1,72 +1,83 @@ -import {test as base, expect} from '@oclif/test' +import ansis from 'ansis' +import {expect} from 'chai' -import {TestHelp, topicHelp} from './help-test-utils' +import {Config, Interfaces} from '../../src' +import {TestHelp} from './help-test-utils' -const g: any = global -g.oclif.columns = 80 +describe('formatHelp', () => { + let config: Config -const test = base - .loadConfig() - .add('help', (ctx) => new TestHelp(ctx.config as any)) - .register('topicHelp', topicHelp) + beforeEach(async () => { + config = await Config.load() + }) -describe('formatHelp', () => { - test - .topicHelp({ + function getTopicHelp(topic: Interfaces.Topic): string { + const help = new TestHelp(config) + const root = help.formatTopic(topic) + return ansis + .strip(root) + .split('\n') + .map((s) => s.trimEnd()) + .join('\n') + } + + it('shows topic output', () => { + const topic = { name: 'topic', description: 'this is a description of my topic', hidden: false, - }) - .it('shows topic output', (ctx) => - expect(ctx.commandHelp).to.equal(`this is a description of my topic + } + const output = getTopicHelp(topic) + expect(output).to.equal(`this is a description of my topic USAGE $ oclif topic:COMMAND -`), - ) +`) + }) - test - .topicHelp({ + it('shows topic without a description', () => { + const topic = { name: 'topic', hidden: false, - }) - .it('shows topic without a description', (ctx) => - expect(ctx.commandHelp).to.equal(`USAGE + } + const output = getTopicHelp(topic) + expect(output).to.equal(`USAGE $ oclif topic:COMMAND -`), - ) +`) + }) - test - .topicHelp({ + it('shows topic descriptions split from \\n for top-level and description section descriptions', () => { + const topic = { name: 'topic', hidden: false, description: 'This is the top level description\nDescription that shows up in the DESCRIPTION section', - }) - .it('shows topic descriptions split from \\n for top-level and description section descriptions', (ctx) => - expect(ctx.commandHelp).to.equal(`This is the top level description + } + const output = getTopicHelp(topic) + expect(output).to.equal(`This is the top level description USAGE $ oclif topic:COMMAND DESCRIPTION Description that shows up in the DESCRIPTION section -`), - ) - test - .topicHelp({ +`) + }) + + it('shows templated topic descriptions split from \\n for top-level and description section descriptions', () => { + const topic = { name: 'topic', hidden: false, description: '<%= config.bin %>: This is the top level description\n<%= config.bin %>: Description that shows up in the DESCRIPTION section', - }) - .it('shows topic descriptions split from \\n for top-level and description section descriptions', (ctx) => - expect(ctx.commandHelp).to.equal(`oclif: This is the top level description + } + const output = getTopicHelp(topic) + expect(output).to.equal(`oclif: This is the top level description USAGE $ oclif topic:COMMAND DESCRIPTION oclif: Description that shows up in the DESCRIPTION section -`), - ) +`) + }) }) diff --git a/test/help/format-topics.test.ts b/test/help/format-topics.test.ts index f842a404f..d3b4fc14f 100644 --- a/test/help/format-topics.test.ts +++ b/test/help/format-topics.test.ts @@ -1,30 +1,40 @@ -import {test as base, expect} from '@oclif/test' +import ansis from 'ansis' +import {expect} from 'chai' -import {TestHelp, topicsHelp} from './help-test-utils' +import {Config, Interfaces} from '../../src' +import {TestHelp} from './help-test-utils' -const g: any = global -g.oclif.columns = 80 +describe('formatTopics', () => { + let config: Config -const test = base - .loadConfig() - .add('help', (ctx) => new TestHelp(ctx.config as any)) - .register('topicsHelp', topicsHelp) + beforeEach(async () => { + config = await Config.load() + }) -describe('formatTopics', () => { - test - .topicsHelp([ + function getTopicsHelp(topic: Interfaces.Topic[]): string { + const help = new TestHelp(config) + const root = help.formatTopics(topic) ?? '' + return ansis + .strip(root) + .split('\n') + .map((s) => s.trimEnd()) + .join('\n') + } + + it('shows a single topic in the list', () => { + const topic = [ { name: 'topic', description: 'this is a description of my topic', }, - ]) - .it('shows ouputs a single topic in the list', (ctx) => - expect(ctx.commandHelp).to.equal(`TOPICS - topic this is a description of my topic`), - ) + ] + const output = getTopicsHelp(topic) + expect(output).to.equal(`TOPICS + topic this is a description of my topic`) + }) - test - .topicsHelp([ + it('shows multiple topics in list', () => { + const topic = [ { name: 'topic', description: 'this is a description of my topic', @@ -37,11 +47,11 @@ describe('formatTopics', () => { name: 'thirdtopic', description: 'description for thirdtopic', }, - ]) - .it('shows ouputs for multiple topics in the list', (ctx) => - expect(ctx.commandHelp).to.equal(`TOPICS + ] + const output = getTopicsHelp(topic) + expect(output).to.equal(`TOPICS topic this is a description of my topic othertopic here we have a description for othertopic - thirdtopic description for thirdtopic`), - ) + thirdtopic description for thirdtopic`) + }) }) diff --git a/test/help/help-test-utils.ts b/test/help/help-test-utils.ts index 8da2d08d2..57926b861 100644 --- a/test/help/help-test-utils.ts +++ b/test/help/help-test-utils.ts @@ -1,4 +1,4 @@ -import stripAnsi from 'strip-ansi' +import ansis from 'ansis' import {Interfaces} from '../../src' import {Command} from '../../src/command' @@ -46,7 +46,8 @@ export class TestHelp extends Help { } function cleanOutput(output: string) { - return stripAnsi(output) + return ansis + .strip(output) .split('\n') .map((s) => s.trimEnd()) .join('\n') @@ -71,40 +72,10 @@ export function makeCommandClass(cmdProps: Partial ({ - run(ctx: {help: TestHelp; commandHelp: string; expectation: string}) { - const topicsHelpOutput = ctx.help.formatTopics(topics) || '' - - if (process.env.TEST_OUTPUT === '1') { - console.log(topicsHelpOutput) - } - - ctx.commandHelp = stripAnsi(topicsHelpOutput) - .split('\n') - .map((s) => s.trimEnd()) - .join('\n') - ctx.expectation = 'has topicsHelp' - }, -}) - -export const topicHelp = (topic: Interfaces.Topic) => ({ - run(ctx: {help: TestHelp; commandHelp: string; expectation: string}) { - const topicHelpOutput = ctx.help.formatTopic(topic) - if (process.env.TEST_OUTPUT === '1') { - console.log(topicHelpOutput) - } - - ctx.commandHelp = stripAnsi(topicHelpOutput) - .split('\n') - .map((s) => s.trimEnd()) - .join('\n') - ctx.expectation = 'has topicHelp' - }, -}) - export function monkeyPatchCommands( config: any, plugins: Array<{name: string; commands: Command.Class[]; topics: Interfaces.Topic[]}>, + override: boolean = true, ) { const pluginsMap = new Map() for (const plugin of plugins) { @@ -112,10 +83,17 @@ export function monkeyPatchCommands( } config.plugins = pluginsMap - config._commands = new Map() - config._topics = new Map() + if (override) { + // // @ts-expect-error private member + config._commands = new Map() + // // @ts-expect-error private member + config._topics = new Map() + } + for (const plugin of config.plugins.values()) { + // // @ts-expect-error private method config.loadCommands(plugin) + // // @ts-expect-error private method config.loadTopics(plugin) } } diff --git a/test/help/show-customized-help.test.ts b/test/help/show-customized-help.test.ts index b59493f87..787d4255b 100644 --- a/test/help/show-customized-help.test.ts +++ b/test/help/show-customized-help.test.ts @@ -1,6 +1,4 @@ -import {test as base, expect} from '@oclif/test' -import {resolve} from 'node:path' -import {SinonStub, stub} from 'sinon' +import {expect} from 'chai' import {Config, Interfaces} from '../../src' import {Command} from '../../src/command' @@ -8,9 +6,6 @@ import {CommandHelp, Help} from '../../src/help' import {AppsAdminAdd, AppsAdminTopic, AppsCreate, AppsDestroy, AppsIndexWithDesc, AppsTopic} from './fixtures/fixtures' import {monkeyPatchCommands} from './help-test-utils' -const g: any = global -g.oclif.columns = 80 - // Allow overriding section headers class TestCommandHelp extends CommandHelp { protected sections() { @@ -32,13 +27,24 @@ ${this.indent(this.wrap('force it '.repeat(29)))}`, class TestHelp extends Help { CommandHelpClass = TestCommandHelp - public declare config: any - + public declare config: Config + public output: string[] = [] constructor(config: Interfaces.Config, opts: Partial = {}) { + opts.stripAnsi = true super(config, opts) this.opts.usageHeader = 'SYNOPSIS' } + public getOutput() { + return this.output.join('\n').trimEnd() + } + + // push logs to this.output instead of logging to the console + // this makes it easier to test the output + protected log(...args: any[]) { + this.output.push(...args) + } + public async showRootHelp() { return super.showRootHelp() } @@ -53,55 +59,25 @@ class TestHelp extends Help { } } -const test = base - .register('setupHelp', () => ({ - async run(ctx: {help: TestHelp; stubs: {[k: string]: SinonStub}}) { - ctx.stubs = { - showRootHelp: stub(TestHelp.prototype, 'showRootHelp').resolves(), - showTopicHelp: stub(TestHelp.prototype, 'showTopicHelp').resolves(), - showCommandHelp: stub(TestHelp.prototype, 'showCommandHelp').resolves(), - } - - // use devPlugins: true to bring in plugins-plugin with topic commands for testing - const config = await Config.load({devPlugins: true, root: resolve(__dirname, '..')}) - ctx.help = new TestHelp(config) - }, - finally(ctx) { - for (const stub of Object.values(ctx.stubs)) stub.restore() - }, - })) - .register('makeTopicsWithoutCommand', () => ({ - async run(ctx: {help: TestHelp; makeTopicOnlyStub: SinonStub}) { - // by returning no matching command for a subject, it becomes a topic only - // with no corresponding command (in which case the showCommandHelp is shown) - // eslint-disable-next-line unicorn/no-useless-undefined - ctx.makeTopicOnlyStub = stub(ctx.help.config, 'findCommand').returns(undefined) - }, - finally(ctx) { - ctx.makeTopicOnlyStub.restore() - }, - })) - describe('showHelp for root', () => { - test - .loadConfig() - .stdout() - .do(async (ctx) => { - const {config} = ctx - - monkeyPatchCommands(config, [ - { - name: 'plugin-1', - commands: [AppsIndexWithDesc, AppsCreate, AppsDestroy], - topics: [], - }, - ]) - - const help = new TestHelp(config as any) - await help.showHelp([]) - }) - .it('shows a command and topic when the index has siblings', ({stdout, config}) => { - expect(stdout.trim()).to.equal(`base library for oclif CLIs + let config: Config + + beforeEach(async () => { + config = await Config.load() + }) + + it('shows a command and topic when the index has siblings', async () => { + monkeyPatchCommands(config, [ + { + name: 'plugin-1', + commands: [AppsIndexWithDesc, AppsCreate, AppsDestroy], + topics: [], + }, + ]) + const help = new TestHelp(config) + await help.showHelp([]) + const output = help.getOutput() + expect(output).to.equal(`base library for oclif CLIs VERSION ${config.userAgent} @@ -115,27 +91,20 @@ TOPICS COMMANDS apps List all apps (app index command) this only shows up in command help under DESCRIPTION`) - }) - - test - .loadConfig() - .stdout() - .do(async (ctx) => { - const {config} = ctx - - monkeyPatchCommands(config, [ - { - name: 'plugin-1', - commands: [AppsIndexWithDesc], - topics: [], - }, - ]) - - const help = new TestHelp(config as any) - await help.showHelp([]) - }) - .it('shows a command only when the topic only contains an index', ({stdout, config}) => { - expect(stdout.trim()).to.equal(`base library for oclif CLIs + }) + + it('shows a command only when the topic only contains an index', async () => { + monkeyPatchCommands(config, [ + { + name: 'plugin-1', + commands: [AppsIndexWithDesc], + topics: [], + }, + ]) + const help = new TestHelp(config) + await help.showHelp([]) + const output = help.getOutput() + expect(output).to.equal(`base library for oclif CLIs VERSION ${config.userAgent} @@ -146,29 +115,29 @@ SYNOPSIS COMMANDS apps List all apps (app index command) this only shows up in command help under DESCRIPTION`) - }) + }) }) describe('showHelp for a command', () => { - test - .loadConfig() - .stdout() - .do(async (ctx) => { - const {config} = ctx - - monkeyPatchCommands(config, [ - { - name: 'plugin-1', - commands: [AppsCreate], - topics: [AppsTopic], - }, - ]) - - const help = new TestHelp(config as any) - await help.showHelp(['apps:create']) - }) - .it('shows help for a leaf (or childless) command', ({stdout}) => { - expect(stdout.trim()).to.equal(`this only shows up in command help under DESCRIPTION + let config: Config + + beforeEach(async () => { + config = await Config.load() + }) + + it('shows help for a leaf (or childless) command', async () => { + monkeyPatchCommands(config, [ + { + name: 'plugin-1', + commands: [AppsCreate], + topics: [AppsTopic], + }, + ]) + + const help = new TestHelp(config) + await help.showHelp(['apps:create']) + const output = help.getOutput() + expect(output).to.equal(`this only shows up in command help under DESCRIPTION SYNOPSIS $ oclif apps:create @@ -180,27 +149,21 @@ CUSTOM it force it force it force it force it force it force it force it force it force it force it force it force it force it force it force it force it force it force it force it force it force it`) - }) - - test - .loadConfig() - .stdout() - .do(async (ctx) => { - const {config} = ctx - - monkeyPatchCommands(config, [ - { - name: 'plugin-1', - commands: [AppsIndexWithDesc, AppsCreate, AppsAdminAdd], - topics: [AppsTopic, AppsAdminTopic], - }, - ]) - - const help = new TestHelp(config as any) - await help.showHelp(['apps']) - }) - .it('shows help for a command that has children topics and commands', ({stdout}) => { - expect(stdout.trim()).to.equal(`List all apps (app index command) + }) + + it('shows help for a command that has children topics and commands', async () => { + monkeyPatchCommands(config, [ + { + name: 'plugin-1', + commands: [AppsIndexWithDesc, AppsCreate, AppsAdminAdd], + topics: [AppsTopic, AppsAdminTopic], + }, + ]) + + const help = new TestHelp(config) + await help.showHelp(['apps']) + const output = help.getOutput() + expect(output).to.equal(`List all apps (app index command) this only shows up in command help under DESCRIPTION SYNOPSIS @@ -219,5 +182,5 @@ TOPICS COMMANDS apps:create this only shows up in command help under DESCRIPTION`) - }) + }) }) diff --git a/test/help/show-help.test.ts b/test/help/show-help.test.ts index 01db70f07..0d89c22c5 100644 --- a/test/help/show-help.test.ts +++ b/test/help/show-help.test.ts @@ -1,7 +1,5 @@ -import {test as base} from '@oclif/test' import {expect} from 'chai' -import {resolve} from 'node:path' -import {SinonStub, stub} from 'sinon' +import sinon from 'sinon' import {Config, Interfaces} from '../../src' import {Help} from '../../src/help' @@ -20,12 +18,25 @@ import { } from './fixtures/fixtures' import {monkeyPatchCommands} from './help-test-utils' -const g: any = global -g.oclif.columns = 80 - // extension makes previously protected methods public class TestHelp extends Help { - public declare config: any + public declare config: Config + public output: string[] = [] + + constructor(config: Interfaces.Config, opts: Partial = {}) { + opts.stripAnsi = true + super(config, opts) + } + + public getOutput() { + return this.output.join('\n').trimEnd() + } + + // push logs to this.output instead of logging to the console + // this makes it easier to test the output + protected log(...args: any[]) { + this.output.push(...args) + } public async showRootHelp() { return super.showRootHelp() @@ -36,60 +47,29 @@ class TestHelp extends Help { } } -const test = base - .register('setupHelp', () => ({ - async run(ctx: {help: TestHelp; stubs: {[k: string]: SinonStub}}) { - ctx.stubs = { - showRootHelp: stub(TestHelp.prototype, 'showRootHelp').resolves(), - showTopicHelp: stub(TestHelp.prototype, 'showTopicHelp').resolves(), - showCommandHelp: stub(TestHelp.prototype, 'showCommandHelp').resolves(), - } - - // use devPlugins: true to bring in plugins-plugin with topic commands for testing - const config = await Config.load({devPlugins: true, root: resolve(__dirname, '..')}) - ctx.help = new TestHelp(config) - }, - finally(ctx) { - for (const stub of Object.values(ctx.stubs)) stub.restore() - }, - })) - .register('makeTopicsWithoutCommand', () => ({ - async run(ctx: {help: TestHelp; makeTopicOnlyStub: SinonStub}) { - // by returning no matching command for a subject, it becomes a topic only - // with no corresponding command (in which case the showCommandHelp is shown) - // eslint-disable-next-line unicorn/no-useless-undefined - ctx.makeTopicOnlyStub = stub(ctx.help.config, 'findCommand').returns(undefined) - }, - finally(ctx) { - ctx.makeTopicOnlyStub.restore() - }, - })) - describe('showHelp for root', () => { - test - .loadConfig() - .stdout() - .do(async () => { - const config = await Config.load({root: resolve(__dirname, '..')}) + let config: Config + + beforeEach(async () => { + config = await Config.load() + }) - ;(config as any).plugins = [ + it('shows a command and topic when the index has siblings', async () => { + monkeyPatchCommands( + config, + [ { + name: 'plugin-1', commands: [AppsIndex, AppsCreate, AppsDestroy], topics: [], }, - ] - for (const plugin of config.plugins) { - // @ts-expect-error private method - config.loadCommands(plugin) - // @ts-expect-error private method - config.loadTopics(plugin) - } - - const help = new TestHelp(config as any) - await help.showHelp([]) - }) - .it('shows a command and topic when the index has siblings', ({stdout, config}) => { - expect(stdout.trim()).to.equal(`base library for oclif CLIs + ], + false, + ) + const help = new TestHelp(config) + await help.showHelp([]) + const output = help.getOutput() + expect(output).to.equal(`base library for oclif CLIs VERSION ${config.userAgent} @@ -105,27 +85,20 @@ COMMANDS apps List all apps (app index command) help Display help for oclif. plugins List installed plugins.`) - }) - - test - .loadConfig() - .stdout() - .do(async (ctx) => { - const {config} = ctx + }) - monkeyPatchCommands(config, [ - { - name: 'plugin-1', - commands: [AppsIndex], - topics: [], - }, - ]) - - const help = new TestHelp(config as any) - await help.showHelp([]) - }) - .it('shows a command only when the topic only contains an index', ({stdout, config}) => { - expect(stdout.trim()).to.equal(`base library for oclif CLIs + it('shows a command only when the topic only contains an index', async () => { + monkeyPatchCommands(config, [ + { + name: 'plugin-1', + commands: [AppsIndex], + topics: [], + }, + ]) + const help = new TestHelp(config) + await help.showHelp([]) + const output = help.getOutput() + expect(output).to.equal(`base library for oclif CLIs VERSION ${config.userAgent} @@ -135,27 +108,21 @@ USAGE COMMANDS apps List all apps (app index command)`) - }) + }) - test - .loadConfig() - .stdout() - .do(async (ctx) => { - const {config} = ctx + it('shows root help without aliases if hideAliasesFromRoot=true', async () => { + monkeyPatchCommands(config, [ + { + name: 'plugin-1', + commands: [CommandWithAliases], + topics: [], + }, + ]) - monkeyPatchCommands(config, [ - { - name: 'plugin-1', - commands: [CommandWithAliases], - topics: [], - }, - ]) - - const help = new TestHelp(config as any, {hideAliasesFromRoot: true}) - await help.showHelp([]) - }) - .it('shows root help without aliases if hideAliasesFromRoot=true', ({stdout, config}) => { - expect(stdout.trim()).to.equal(`base library for oclif CLIs + const help = new TestHelp(config as any, {hideAliasesFromRoot: true}) + await help.showHelp([]) + const output = help.getOutput() + expect(output).to.equal(`base library for oclif CLIs VERSION ${config.userAgent} @@ -165,27 +132,21 @@ USAGE COMMANDS foo This is a command with aliases`) - }) + }) - test - .loadConfig() - .stdout() - .do(async (ctx) => { - const {config} = ctx + it('shows root help with aliases commands by default', async () => { + monkeyPatchCommands(config, [ + { + name: 'plugin-1', + commands: [CommandWithAliases], + topics: [], + }, + ]) - monkeyPatchCommands(config, [ - { - name: 'plugin-1', - commands: [CommandWithAliases], - topics: [], - }, - ]) - - const help = new TestHelp(config as any) - await help.showHelp([]) - }) - .it('shows root help with aliases commands by default', ({stdout, config}) => { - expect(stdout.trim()).to.equal(`base library for oclif CLIs + const help = new TestHelp(config as any) + await help.showHelp([]) + const output = help.getOutput() + expect(output).to.equal(`base library for oclif CLIs VERSION ${config.userAgent} @@ -198,29 +159,29 @@ COMMANDS baz This is a command with aliases foo This is a command with aliases qux This is a command with aliases`) - }) + }) }) describe('showHelp for a topic', () => { - test - .loadConfig() - .stdout() - .do(async (ctx) => { - const {config} = ctx + let config: Config - monkeyPatchCommands(config, [ - { - name: 'plugin-1', - commands: [AppsCreate, AppsDestroy], - topics: [AppsTopic], - }, - ]) + beforeEach(async () => { + config = await Config.load() + }) - const help = new TestHelp(config as any) - await help.showHelp(['apps']) - }) - .it('shows topic help with commands', ({stdout}) => { - expect(stdout.trim()).to.equal(`This topic is for the apps topic + it('shows topic help with commands', async () => { + monkeyPatchCommands(config, [ + { + name: 'plugin-1', + commands: [AppsCreate, AppsDestroy], + topics: [AppsTopic], + }, + ]) + + const help = new TestHelp(config as any) + await help.showHelp(['apps']) + const output = help.getOutput() + expect(output).to.equal(`This topic is for the apps topic USAGE $ oclif apps:COMMAND @@ -228,27 +189,21 @@ USAGE COMMANDS apps:create Create an app apps:destroy Destroy an app`) - }) - - test - .loadConfig() - .stdout() - .do(async (ctx) => { - const {config} = ctx + }) - monkeyPatchCommands(config, [ - { - name: 'plugin-1', - commands: [AppsCreate, AppsDestroy, AppsAdminAdd], - topics: [AppsTopic, AppsAdminTopic], - }, - ]) + it('shows topic help with topic and commands', async () => { + monkeyPatchCommands(config, [ + { + name: 'plugin-1', + commands: [AppsCreate, AppsDestroy, AppsAdminAdd], + topics: [AppsTopic, AppsAdminTopic], + }, + ]) - const help = new TestHelp(config as any) - await help.showHelp(['apps']) - }) - .it('shows topic help with topic and commands', ({stdout}) => { - expect(stdout.trim()).to.equal(`This topic is for the apps topic + const help = new TestHelp(config as any) + await help.showHelp(['apps']) + const output = help.getOutput() + expect(output).to.equal(`This topic is for the apps topic USAGE $ oclif apps:COMMAND @@ -259,27 +214,21 @@ TOPICS COMMANDS apps:create Create an app apps:destroy Destroy an app`) - }) - - test - .loadConfig() - .stdout() - .do(async (ctx) => { - const {config} = ctx + }) - monkeyPatchCommands(config, [ - { - name: 'plugin-1', - commands: [AppsCreate, AppsDestroy, AppsAdminIndex, AppsAdminAdd], - topics: [AppsTopic, AppsAdminTopic], - }, - ]) + it('shows topic help with topic and commands and topic command', async () => { + monkeyPatchCommands(config, [ + { + name: 'plugin-1', + commands: [AppsCreate, AppsDestroy, AppsAdminIndex, AppsAdminAdd], + topics: [AppsTopic, AppsAdminTopic], + }, + ]) - const help = new TestHelp(config as any) - await help.showHelp(['apps']) - }) - .it('shows topic help with topic and commands and topic command', ({stdout}) => { - expect(stdout.trim()).to.equal(`This topic is for the apps topic + const help = new TestHelp(config as any) + await help.showHelp(['apps']) + const output = help.getOutput() + expect(output).to.equal(`This topic is for the apps topic USAGE $ oclif apps:COMMAND @@ -291,26 +240,21 @@ COMMANDS apps:admin List of admins for an app apps:create Create an app apps:destroy Destroy an app`) - }) + }) - test - .loadConfig() - .stdout() - .do(async (ctx) => { - const {config} = ctx - monkeyPatchCommands(config, [ - { - name: 'plugin-1', - commands: [AppsCreate, AppsDestroy, AppsAdminAdd, DbCreate], - topics: [AppsTopic, AppsAdminTopic, DbTopic], - }, - ]) + it('ignores other topics and commands', async () => { + monkeyPatchCommands(config, [ + { + name: 'plugin-1', + commands: [AppsCreate, AppsDestroy, AppsAdminAdd, DbCreate], + topics: [AppsTopic, AppsAdminTopic, DbTopic], + }, + ]) - const help = new TestHelp(config as any) - await help.showHelp(['apps']) - }) - .it('ignores other topics and commands', ({stdout}) => { - expect(stdout.trim()).to.equal(`This topic is for the apps topic + const help = new TestHelp(config as any) + await help.showHelp(['apps']) + const output = help.getOutput() + expect(output).to.equal(`This topic is for the apps topic USAGE $ oclif apps:COMMAND @@ -321,54 +265,50 @@ TOPICS COMMANDS apps:create Create an app apps:destroy Destroy an app`) - }) + }) - test - .loadConfig() - .stdout() - .do(async (ctx) => { - const {config} = ctx - monkeyPatchCommands(config, [ - { - name: 'plugin-1', - commands: [DeprecateAliases], - topics: [], - }, - ]) + it('show deprecation warning when using alias', async () => { + monkeyPatchCommands(config, [ + { + name: 'plugin-1', + commands: [DeprecateAliases], + topics: [], + }, + ]) - const help = new TestHelp(config as any) - await help.showHelp(['foo:bar:alias']) - }) - .it('show deprecation warning when using alias', ({stdout}) => { - expect(stdout.trim()).to.equal(`The "foo:bar:alias" command has been deprecated. Use "foo:bar" instead. + const help = new TestHelp(config as any) + await help.showHelp(['foo:bar:alias']) + const output = help.getOutput() + expect(output).to.equal(`The "foo:bar:alias" command has been deprecated. Use "foo:bar" instead. USAGE $ oclif foo:bar:alias ALIASES $ oclif foo:bar:alias`) - }) + }) }) describe('showHelp for a command', () => { - test - .loadConfig() - .stdout() - .do(async (ctx) => { - const {config} = ctx - monkeyPatchCommands(config, [ - { - name: 'plugin-1', - commands: [AppsCreate], - topics: [AppsTopic], - }, - ]) + let config: Config - const help = new TestHelp(config as any) - await help.showHelp(['apps:create']) - }) - .it('shows help for a leaf (or childless) command', ({stdout}) => { - expect(stdout.trim()).to.equal(`Create an app + beforeEach(async () => { + config = await Config.load() + }) + + it('shows help for a leaf (or childless) command', async () => { + monkeyPatchCommands(config, [ + { + name: 'plugin-1', + commands: [AppsCreate], + topics: [AppsTopic], + }, + ]) + + const help = new TestHelp(config) + await help.showHelp(['apps:create']) + const output = help.getOutput() + expect(output).to.equal(`Create an app USAGE $ oclif apps:create @@ -377,26 +317,21 @@ DESCRIPTION Create an app this only shows up in command help under DESCRIPTION`) - }) + }) - test - .loadConfig() - .stdout() - .do(async (ctx) => { - const {config} = ctx - monkeyPatchCommands(config, [ - { - name: 'plugin-1', - commands: [AppsIndex, AppsCreate, AppsAdminAdd], - topics: [AppsTopic, AppsAdminTopic], - }, - ]) + it('shows help for a command that has children topics and commands', async () => { + monkeyPatchCommands(config, [ + { + name: 'plugin-1', + commands: [AppsIndex, AppsCreate, AppsAdminAdd], + topics: [AppsTopic, AppsAdminTopic], + }, + ]) - const help = new TestHelp(config as any) - await help.showHelp(['apps']) - }) - .it('shows help for a command that has children topics and commands', ({stdout}) => { - expect(stdout.trim()).to.equal(`List all apps (app index command) + const help = new TestHelp(config as any) + await help.showHelp(['apps']) + const output = help.getOutput() + expect(output).to.equal(`List all apps (app index command) USAGE $ oclif apps @@ -406,12 +341,34 @@ TOPICS COMMANDS apps:create Create an app`) - }) + }) }) describe('showHelp routing', () => { + let config: Config + let sandbox: sinon.SinonSandbox + let help: TestHelp + const stubs = { + showRootHelp: sinon.stub(), + showTopicHelp: sinon.stub(), + showCommandHelp: sinon.stub(), + } + + beforeEach(async () => { + config = await Config.load() + sandbox = sinon.createSandbox() + stubs.showCommandHelp = sandbox.stub(TestHelp.prototype, 'showCommandHelp').resolves() + stubs.showRootHelp = sandbox.stub(TestHelp.prototype, 'showRootHelp').resolves() + stubs.showTopicHelp = sandbox.stub(TestHelp.prototype, 'showTopicHelp').resolves() + help = new TestHelp(config) + }) + + afterEach(() => { + sandbox.restore() + }) + describe('shows root help', () => { - test.setupHelp().it('shows root help when no subject is provided', async ({help, stubs}) => { + it('shows root help when no subject is provided', async () => { await help.showHelp([]) expect(stubs.showRootHelp.called).to.be.true @@ -419,7 +376,7 @@ describe('showHelp routing', () => { expect(stubs.showTopicHelp.called).to.be.false }) - test.setupHelp().it('shows root help when help is the only arg', async ({help, stubs}) => { + it('shows root help when help is the only arg', async () => { await help.showHelp(['help']) expect(stubs.showRootHelp.called).to.be.true @@ -429,44 +386,38 @@ describe('showHelp routing', () => { }) describe('shows topic help', () => { - test - .setupHelp() - .makeTopicsWithoutCommand() - .it('shows the topic help when a topic has no matching command', async ({help, stubs}) => { - await help.showHelp(['plugins']) - expect(stubs.showTopicHelp.called).to.be.true - - expect(stubs.showRootHelp.called).to.be.false - expect(stubs.showCommandHelp.called).to.be.false - }) - - test - .setupHelp() - .makeTopicsWithoutCommand() - .it( - 'shows the topic help when a topic has no matching command and is preceded by help', - async ({help, stubs}) => { - await help.showHelp(['help', 'plugins']) - expect(stubs.showTopicHelp.called).to.be.true - - expect(stubs.showRootHelp.called).to.be.false - expect(stubs.showCommandHelp.called).to.be.false - }, - ) + beforeEach(() => { + // eslint-disable-next-line unicorn/no-useless-undefined + sandbox.stub(config, 'findCommand').returns(undefined) + }) + + it('shows the topic help when a topic has no matching command', async () => { + await help.showHelp(['plugins']) + expect(stubs.showTopicHelp.called).to.be.true + + expect(stubs.showRootHelp.called).to.be.false + expect(stubs.showCommandHelp.called).to.be.false + }) + + it('shows the topic help when a topic has no matching command and is preceded by help', async () => { + await help.showHelp(['help', 'plugins']) + expect(stubs.showTopicHelp.called).to.be.true + + expect(stubs.showRootHelp.called).to.be.false + expect(stubs.showCommandHelp.called).to.be.false + }) }) describe('shows command help', () => { - test - .setupHelp() - .it('calls showCommandHelp when a topic that is also a command is called', async ({help, stubs}) => { - await help.showHelp(['plugins']) - expect(stubs.showCommandHelp.called).to.be.true + it('calls showCommandHelp when a topic that is also a command is called', async () => { + await help.showHelp(['plugins']) + expect(stubs.showCommandHelp.called).to.be.true - expect(stubs.showRootHelp.called).to.be.false - expect(stubs.showTopicHelp.called).to.be.false - }) + expect(stubs.showRootHelp.called).to.be.false + expect(stubs.showTopicHelp.called).to.be.false + }) - test.setupHelp().it('calls showCommandHelp when a command is called', async ({help, stubs}) => { + it('calls showCommandHelp when a command is called', async () => { await help.showHelp(['plugins:install']) expect(stubs.showCommandHelp.called).to.be.true @@ -474,7 +425,7 @@ describe('showHelp routing', () => { expect(stubs.showTopicHelp.called).to.be.false }) - test.setupHelp().it('calls showCommandHelp when a command is preceded by the help arg', async ({help, stubs}) => { + it('calls showCommandHelp when a command is preceded by the help arg', async () => { await help.showHelp(['help', 'plugins:install']) expect(stubs.showCommandHelp.called).to.be.true @@ -484,10 +435,8 @@ describe('showHelp routing', () => { }) describe('errors', () => { - test - .setupHelp() - .it('shows an error when there is a subject but it does not match a topic or command', async ({help}) => { - await expect(help.showHelp(['meow'])).to.be.rejectedWith('Command meow not found') - }) + it('shows an error when there is a subject but it does not match a topic or command', async () => { + await expect(help.showHelp(['meow'])).to.be.rejectedWith('Command meow not found') + }) }) }) diff --git a/test/help/util.test.ts b/test/help/util.test.ts index 1c3eee7ee..95dbd8f10 100644 --- a/test/help/util.test.ts +++ b/test/help/util.test.ts @@ -1,21 +1,33 @@ -import {test} from '@oclif/test' import {expect} from 'chai' import {resolve} from 'node:path' +import sinon from 'sinon' -import {Config, Interfaces} from '../../src' +import {Args, Command, Config} from '../../src' import * as util from '../../src/config/util' import {loadHelpClass, standardizeIDFromArgv} from '../../src/help' import configuredHelpClass from './_test-help-class' describe('util', () => { - let config: Interfaces.Config + let config: Config + let sandbox: sinon.SinonSandbox beforeEach(async () => { config = await Config.load() + config.topicSeparator = ' ' + sandbox = sinon.createSandbox() }) + afterEach(() => { + sandbox.restore() + }) + + function stubCommands(...commands: Array>) { + // @ts-expect-error private member + sandbox.stub(config, '_commands').value(new Map(commands.map((cmd) => [cmd.id, cmd]))) + } + describe('#loadHelpClass', () => { - test.it('defaults to the class exported', async () => { + it('defaults to the native help class', async () => { delete config.pjson.oclif.helpClass const helpClass = await loadHelpClass(config) @@ -25,9 +37,8 @@ describe('util', () => { expect(helpClass.prototype.formatRoot) }) - test.it('loads help class defined in pjson.oclif.helpClass', async () => { + it('loads help class defined in pjson.oclif.helpClass', async () => { config.pjson.oclif.helpClass = '../test/help/_test-help-class' - // @ts-expect-error readonly property config.root = resolve(__dirname, '..') expect(configuredHelpClass).to.not.be.undefined @@ -35,7 +46,7 @@ describe('util', () => { }) describe('error cases', () => { - test.it('throws an error when failing to load the help class defined in pjson.oclif.helpClass', async () => { + it('throws an error when failing to load the help class defined in pjson.oclif.helpClass', async () => { config.pjson.oclif.helpClass = './lib/does-not-exist-help-class' await expect(loadHelpClass(config)).to.be.rejectedWith( 'Unable to load configured help class "./lib/does-not-exist-help-class", failed with message:', @@ -45,148 +56,124 @@ describe('util', () => { }) describe('#standardizeIDFromArgv', () => { - test.it('should return standardized id when topic separator is a colon', () => { - config.pjson.oclif.topicSeparator = ':' + it('should return standardized id when topic separator is a colon', () => { + config.topicSeparator = ':' const actual = standardizeIDFromArgv(['foo:bar', '--baz'], config) expect(actual).to.deep.equal(['foo:bar', '--baz']) }) - test.it('should return standardized id when topic separator is a space', () => { - config.topicSeparator = ' ' + it('should return standardized id when topic separator is a space', () => { const actual = standardizeIDFromArgv(['foo', '', '--baz'], config) expect(actual).to.deep.equal(['foo', '', '--baz']) }) - test - .stub(util, 'collectUsableIds', (stub) => stub.returns(new Set(['foo', 'foo:bar']))) - .it('should return standardized id when topic separator is a space', () => { - config.topicSeparator = ' ' - const actual = standardizeIDFromArgv(['foo', 'bar', '--baz'], config) - expect(actual).to.deep.equal(['foo:bar', '--baz']) - }) + it('should return standardized id when topic separator is a space', () => { + sandbox.stub(util, 'collectUsableIds').returns(new Set(['foo', 'foo:bar'])) + const actual = standardizeIDFromArgv(['foo', 'bar', '--baz'], config) + expect(actual).to.deep.equal(['foo:bar', '--baz']) + }) + + it('should return standardized id when topic separator is a space and command is misspelled', () => { + sandbox.stub(util, 'collectUsableIds').returns(new Set(['foo', 'foo:bar'])) + const actual = standardizeIDFromArgv(['foo', 'ba', '--baz'], config) + expect(actual).to.deep.equal(['foo:ba', '--baz']) + }) - test - .stub(util, 'collectUsableIds', (stub) => stub.returns(new Set(['foo', 'foo:bar']))) - .it('should return standardized id when topic separator is a space and command is misspelled', () => { - config.topicSeparator = ' ' - const actual = standardizeIDFromArgv(['foo', 'ba', '--baz'], config) - expect(actual).to.deep.equal(['foo:ba', '--baz']) + it('should return standardized id when topic separator is a space and has args and command is misspelled', () => { + sandbox.stub(util, 'collectUsableIds').returns(new Set(['foo', 'foo:bar'])) + stubCommands({ + id: 'foo:bar', + args: { + name: Args.string(), + }, }) + const actual = standardizeIDFromArgv(['foo', 'ba', 'baz'], config) + expect(actual).to.deep.equal(['foo:ba:baz']) + }) - test - .stub(util, 'collectUsableIds', (stub) => stub.returns(new Set(['foo', 'foo:bar']))) - .it( - 'should return standardized id when topic separator is a space and has args and command is misspelled', - () => { - config.topicSeparator = ' ' - // @ts-expect-error private member - config._commands.set('foo:bar', { - id: 'foo:bar', - args: [{name: 'first'}], - }) - const actual = standardizeIDFromArgv(['foo', 'ba', 'baz'], config) - expect(actual).to.deep.equal(['foo:ba:baz']) + it('should return standardized id when topic separator is a space and has args', () => { + sandbox.stub(util, 'collectUsableIds').returns(new Set(['foo', 'foo:bar'])) + stubCommands({ + id: 'foo:bar', + args: { + name: Args.string(), }, - ) - - test - .stub(util, 'collectUsableIds', (stub) => stub.returns(new Set(['foo', 'foo:bar']))) - .it('should return standardized id when topic separator is a space and has args', () => { - config.topicSeparator = ' ' - // @ts-expect-error private member - config._commands.set('foo:bar', { - id: 'foo:bar', - args: [{name: 'first'}], - }) - const actual = standardizeIDFromArgv(['foo', 'bar', 'baz'], config) - expect(actual).to.deep.equal(['foo:bar', 'baz']) }) + const actual = standardizeIDFromArgv(['foo', 'bar', 'baz'], config) + expect(actual).to.deep.equal(['foo:bar', 'baz']) + }) - test - .stub(util, 'collectUsableIds', (stub) => stub.returns(new Set(['foo', 'foo:bar']))) - .it('should return standardized id when topic separator is a space and has variable arguments', () => { - config.topicSeparator = ' ' - // @ts-expect-error private member - config._commands.set('foo:bar', { - id: 'foo:bar', - strict: false, - }) - const actual = standardizeIDFromArgv(['foo', 'bar', 'baz'], config) - expect(actual).to.deep.equal(['foo:bar', 'baz']) + it('should return standardized id when topic separator is a space and has variable arguments', () => { + sandbox.stub(util, 'collectUsableIds').returns(new Set(['foo', 'foo:bar'])) + stubCommands({ + id: 'foo:bar', + strict: false, }) + const actual = standardizeIDFromArgv(['foo', 'bar', 'baz'], config) + expect(actual).to.deep.equal(['foo:bar', 'baz']) + }) - test - .stub(util, 'collectUsableIds', (stub) => stub.returns(new Set(['foo', 'foo:bar']))) - .it('should return standardized id when topic separator is a space and has variable arguments and flags', () => { - config.topicSeparator = ' ' - // @ts-expect-error private member - config._commands.set('foo:bar', { - id: 'foo:bar', - strict: false, - }) - const actual = standardizeIDFromArgv(['foo', 'bar', 'baz', '--hello'], config) - expect(actual).to.deep.equal(['foo:bar', 'baz', '--hello']) + it('should return standardized id when topic separator is a space and has variable arguments and flags', () => { + sandbox.stub(util, 'collectUsableIds').returns(new Set(['foo', 'foo:bar'])) + stubCommands({ + id: 'foo:bar', + strict: false, }) + const actual = standardizeIDFromArgv(['foo', 'bar', 'baz', '--hello'], config) + expect(actual).to.deep.equal(['foo:bar', 'baz', '--hello']) + }) - test - .stub(util, 'collectUsableIds', (stub) => stub.returns(new Set(['foo', 'foo:bar']))) - .it('should return full id when topic separator is a space and does not have arguments', () => { - config.topicSeparator = ' ' - // @ts-expect-error private member - config._commands.set('foo:bar', { - id: 'foo:bar', - args: [], - strict: true, - }) - const actual = standardizeIDFromArgv(['foo', 'bar', 'baz'], config) - expect(actual).to.deep.equal(['foo:bar:baz']) + it('should return full id when topic separator is a space and does not have arguments', () => { + sandbox.stub(util, 'collectUsableIds').returns(new Set(['foo', 'foo:bar'])) + stubCommands({ + id: 'foo:bar', + args: {}, + strict: true, }) + const actual = standardizeIDFromArgv(['foo', 'bar', 'baz'], config) + expect(actual).to.deep.equal(['foo:bar:baz']) + }) - test - .stub(util, 'collectUsableIds', (stub) => stub.returns(new Set(['foo', 'foo:bar']))) - .it('should return standardized id when topic separator is a space and has arg with value', () => { - config.topicSeparator = ' ' - // @ts-expect-error private member - config._commands.set('foo:bar', {id: 'foo:bar'}) - const actual = standardizeIDFromArgv(['foo', 'bar', 'hello=world'], config) - expect(actual).to.deep.equal(['foo:bar', 'hello=world']) + it('should return standardized id when topic separator is a space and has arg with value', () => { + sandbox.stub(util, 'collectUsableIds').returns(new Set(['foo', 'foo:bar'])) + stubCommands({ + id: 'foo:bar', }) + const actual = standardizeIDFromArgv(['foo', 'bar', 'hello=world'], config) + expect(actual).to.deep.equal(['foo:bar', 'hello=world']) + }) - test - .stub(util, 'collectUsableIds', (stub) => stub.returns(new Set(['foo', 'foo:bar']))) - .it('should return standardized id when topic separator is a space and has variable args with value', () => { - config.topicSeparator = ' ' - // @ts-expect-error private member - config._commands.set('foo:bar', {id: 'foo:bar', strict: false}) - const actual = standardizeIDFromArgv(['foo', 'bar', 'hello=world', 'my-arg=value'], config) - expect(actual).to.deep.equal(['foo:bar', 'hello=world', 'my-arg=value']) + it('should return standardized id when topic separator is a space and has variable args with value', () => { + sandbox.stub(util, 'collectUsableIds').returns(new Set(['foo', 'foo:bar'])) + stubCommands({ + id: 'foo:bar', + strict: false, }) + const actual = standardizeIDFromArgv(['foo', 'bar', 'hello=world', 'my-arg=value'], config) + expect(actual).to.deep.equal(['foo:bar', 'hello=world', 'my-arg=value']) + }) - test - .stub(util, 'collectUsableIds', (stub) => stub.returns(new Set(['foo', 'foo:bar']))) - .it('should return standardized id when topic separator is a space and has flags', () => { - config.topicSeparator = ' ' - // @ts-expect-error private member - config._commands.set('foo:bar', {id: 'foo:bar'}) - const actual = standardizeIDFromArgv(['foo', 'bar', '--baz'], config) - expect(actual).to.deep.equal(['foo:bar', '--baz']) + it('should return standardized id when topic separator is a space and has flags', () => { + sandbox.stub(util, 'collectUsableIds').returns(new Set(['foo', 'foo:bar'])) + stubCommands({ + id: 'foo:bar', + strict: false, }) + const actual = standardizeIDFromArgv(['foo', 'bar', '--baz'], config) + expect(actual).to.deep.equal(['foo:bar', '--baz']) + }) - test - .stub(util, 'collectUsableIds', (stub) => stub.returns(new Set(['foo', 'foo:bar']))) - .it( - 'should return standardized id when topic separator is a space and has flags, arg, and arg with value', - () => { - config.topicSeparator = ' ' - // @ts-expect-error private member - config._commands.set('foo:bar', { - id: 'foo:bar', - args: [{name: 'my-arg'}], - strict: true, - }) - const actual = standardizeIDFromArgv(['foo', 'bar', 'my-arg', 'hello=world', '--baz'], config) - expect(actual).to.deep.equal(['foo:bar', 'my-arg', 'hello=world', '--baz']) + it('should return standardized id when topic separator is a space and has flags, arg, and arg with value', () => { + sandbox.stub(util, 'collectUsableIds').returns(new Set(['foo', 'foo:bar'])) + stubCommands({ + id: 'foo:bar', + args: { + 'my-arg': Args.string(), }, - ) + strict: true, + }) + const actual = standardizeIDFromArgv(['foo', 'bar', 'my-arg', 'hello=world', '--baz'], config) + expect(actual).to.deep.equal(['foo:bar', 'my-arg', 'hello=world', '--baz']) + }) }) }) diff --git a/test/integration/interop.ts b/test/integration/interop.ts index ca27dc65e..c8d5ddb8f 100644 --- a/test/integration/interop.ts +++ b/test/integration/interop.ts @@ -6,8 +6,8 @@ * Instead of spending more time diagnosing the root cause, we are just going to * run these integration tests using ts-node and a lightweight homemade test runner. */ +import ansis from 'ansis' import {expect} from 'chai' -import chalk from 'chalk' import fs from 'node:fs/promises' import path from 'node:path' @@ -139,10 +139,10 @@ async function testRunner({ try { await fn() passed.push(name) - console.log(chalk.green('✓'), name) + console.log(ansis.green('✓'), name) } catch (error) { failed.push(name) - console.log(chalk.red('𐄂'), name) + console.log(ansis.red('𐄂'), name) console.log(error) } } @@ -490,14 +490,14 @@ class InteropTest extends Command { private processResults({failed, passed}: {failed: string[]; passed: string[]}): never { this.log() - this.log(chalk.bold('#### Summary ####')) + this.log(ansis.bold('#### Summary ####')) - for (const name of passed) this.log(chalk.green('✓'), name) + for (const name of passed) this.log(ansis.green('✓'), name) - for (const name of failed) this.log(chalk.red('𐄂'), name) + for (const name of failed) this.log(ansis.red('𐄂'), name) - this.log(`${chalk.green('Passed:')} ${passed.length}`) - this.log(`${chalk.red('Failed:')} ${failed.length}`) + this.log(`${ansis.green('Passed:')} ${passed.length}`) + this.log(`${ansis.red('Failed:')} ${failed.length}`) this.exit(failed.length) } diff --git a/test/integration/sf.integration.ts b/test/integration/sf.integration.ts index 529941670..3c79b236e 100644 --- a/test/integration/sf.integration.ts +++ b/test/integration/sf.integration.ts @@ -1,21 +1,17 @@ +import ansis from 'ansis' import {expect} from 'chai' import {arch} from 'node:os' import {Executor, setup} from './util' -const stripAnsi = require('strip-ansi') - -const chalk = require('chalk') -chalk.level = 0 - function parseJson(json: string) { - return JSON.parse(stripAnsi(json)) + return JSON.parse(ansis.strip(json)) } describe('Salesforce CLI (sf)', () => { let executor: Executor before(async () => { - process.env.SFDX_TELEMETRY_DISABLE_ACKNOWLEDGEMENT = 'true' + process.env.SF_TELEMETRY_DISABLE_ACKNOWLEDGEMENT = 'true' executor = await setup(__filename, { repo: 'https://github.com/salesforcecli/cli', }) diff --git a/test/integration/util.ts b/test/integration/util.ts index a0ec14b01..8b5d5d624 100644 --- a/test/integration/util.ts +++ b/test/integration/util.ts @@ -1,4 +1,4 @@ -import chalk from 'chalk' +import ansis from 'ansis' import {ExecException, ExecSyncOptionsWithBufferEncoding, execSync} from 'node:child_process' import {existsSync, readFileSync, writeFileSync} from 'node:fs' import {mkdir, rm} from 'node:fs/promises' @@ -77,7 +77,7 @@ export class Executor { const cwd = options?.cwd ?? process.cwd() const silent = options?.silent ?? true return new Promise((resolve) => { - this.debug(cmd, chalk.dim(`(cwd: ${cwd})`)) + this.debug(cmd, ansis.dim(`(cwd: ${cwd})`)) if (silent) { try { const r = execSync(cmd, {...options, stdio: 'pipe', cwd}) @@ -148,7 +148,7 @@ export async function setup(testFile: string, options: SetupOptions): Promise { it('shows usages', () => { const f = [ @@ -15,7 +14,7 @@ describe('flagUsage', () => { flags.string({name: 'foo', char: 'f', helpLabel: '-f'}), flags.boolean({char: 'g', description: 'goo'}), ] - expect(flagUsages(f).map(([name, desc]) => [name, desc && stripAnsi(desc)])).to.deep.equal([ + expect(flagUsages(f).map(([name, desc]) => [name, desc && ansis.strip(desc)])).to.deep.equal([ [' -b, --bar BAR', 'bar'], [' -f, --foo FOO', 'desc'], [' -f FOO', undefined], diff --git a/test/parser/parse.test.ts b/test/parser/parse.test.ts index c5022e4c5..80e786fbb 100644 --- a/test/parser/parse.test.ts +++ b/test/parser/parse.test.ts @@ -1,3 +1,4 @@ +import ansis from 'ansis' import {assert, config, expect} from 'chai' import * as fs from 'node:fs' import {URL} from 'node:url' @@ -10,7 +11,6 @@ import {parse} from '../../src/parser' import * as parser from '../../src/parser/parse' config.truncateThreshold = 0 -const stripAnsi = require('strip-ansi') describe('parse', () => { it('--bool', async () => { @@ -265,7 +265,7 @@ describe('parse', () => { }, }) } catch (error: any) { - message = stripAnsi(error.message) + message = ansis.strip(error.message) } expect(message).to.include('Missing required flag myflag') diff --git a/test/test.ts b/test/test.ts new file mode 100644 index 000000000..8509dc320 --- /dev/null +++ b/test/test.ts @@ -0,0 +1,171 @@ +import ansis from 'ansis' +import makeDebug from 'debug' +import {dirname} from 'node:path' + +import {Config} from '../src' +import {CLIError} from '../src/errors' +import {LoadOptions} from '../src/interfaces' +import {run} from '../src/main' + +const debug = makeDebug('test') + +type CaptureOptions = { + print?: boolean + stripAnsi?: boolean +} + +const RECORD_OPTIONS: CaptureOptions = { + print: false, + stripAnsi: true, +} + +const originals = { + stderr: process.stderr.write, + stdout: process.stdout.write, +} + +const output: Record<'stderr' | 'stdout', Array> = { + stderr: [], + stdout: [], +} + +function mockedStdout(str: Uint8Array | string, cb?: (err?: Error) => void): boolean +function mockedStdout(str: Uint8Array | string, encoding?: BufferEncoding, cb?: (err?: Error) => void): boolean +function mockedStdout( + str: Uint8Array | string, + encoding?: BufferEncoding | ((err?: Error) => void), + cb?: (err?: Error) => void, +): boolean { + output.stdout.push(str) + if (!RECORD_OPTIONS.print) return true + + if (typeof encoding === 'string') { + return originals.stdout.bind(process.stdout)(str, encoding, cb) + } + + return originals.stdout.bind(process.stdout)(str, cb) +} + +function mockedStderr(str: Uint8Array | string, cb?: (err?: Error) => void): boolean +function mockedStderr(str: Uint8Array | string, encoding?: BufferEncoding, cb?: (err?: Error) => void): boolean +function mockedStderr( + str: Uint8Array | string, + encoding?: BufferEncoding | ((err?: Error) => void), + cb?: (err?: Error) => void, +): boolean { + output.stderr.push(str) + if (!RECORD_OPTIONS.print) return true + if (typeof encoding === 'string') { + return originals.stdout.bind(process.stderr)(str, encoding, cb) + } + + return originals.stdout.bind(process.stderr)(str, cb) +} + +const restore = (): void => { + process.stderr.write = originals.stderr + process.stdout.write = originals.stdout +} + +const reset = (): void => { + output.stderr = [] + output.stdout = [] +} + +const toString = (str: string | Uint8Array): string => + RECORD_OPTIONS.stripAnsi ? ansis.strip(str.toString()) : str.toString() + +const getStderr = (): string => output.stderr.map((b) => toString(b)).join('') +const getStdout = (): string => output.stdout.map((b) => toString(b)).join('') + +function traverseFilePathUntil(filename: string, predicate: (filename: string) => boolean): string { + let current = filename + while (!predicate(current)) { + current = dirname(current) + } + + return current +} + +function makeLoadOptions(loadOpts?: LoadOptions): LoadOptions { + return ( + loadOpts ?? { + root: traverseFilePathUntil( + require.main?.path ?? module.path, + (p) => !(p.includes('node_modules') || p.includes('.pnpm') || p.includes('.yarn')), + ), + } + ) +} + +export async function captureOutput( + fn: () => Promise, + opts?: CaptureOptions, +): Promise<{ + stdout: string + stderr: string + result?: T + error?: Error & Partial +}> { + RECORD_OPTIONS.print = opts?.print ?? false + RECORD_OPTIONS.stripAnsi = opts?.stripAnsi ?? true + process.stderr.write = mockedStderr + process.stdout.write = mockedStdout + + try { + const result = await fn() + return { + result: result as T, + stderr: getStderr(), + stdout: getStdout(), + } + } catch (error) { + return { + ...(error instanceof CLIError && {error}), + ...(error instanceof Error && {error}), + stderr: getStderr(), + stdout: getStdout(), + } + } finally { + restore() + reset() + } +} + +export async function runCommand( + args: string[], + loadOpts?: LoadOptions, + captureOpts?: CaptureOptions, +): Promise<{ + stdout: string + stderr: string + // code: number, + result?: T + error?: Error & Partial +}> { + const loadOptions = makeLoadOptions(loadOpts) + debug('loadOpts: %O', loadOpts) + return captureOutput(async () => run(args, loadOptions), captureOpts) +} + +export async function runHook( + hook: string, + options: Record, + loadOpts?: LoadOptions, + recordOpts?: CaptureOptions, +): Promise<{ + stdout: string + stderr: string + // code: number, + result?: T + error?: Error & Partial +}> { + const loadOptions = makeLoadOptions(loadOpts) + + debug('loadOpts: %O', loadOpts) + + return captureOutput(async () => { + const config = await Config.load(loadOptions) + return config.runHook(hook, options) + }, recordOpts) +} diff --git a/test/util/ensure-arg-object.ts b/test/util/ensure-arg-object.ts new file mode 100644 index 000000000..adc556d2d --- /dev/null +++ b/test/util/ensure-arg-object.ts @@ -0,0 +1,21 @@ +import {expect} from 'chai' + +import {ensureArgObject} from '../../src/util/ensure-arg-object' +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) + }) + + 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/ux/colorize-json.test.ts b/test/ux/colorize-json.test.ts new file mode 100644 index 000000000..a2eb2b1a3 --- /dev/null +++ b/test/ux/colorize-json.test.ts @@ -0,0 +1,127 @@ +import {expect} from 'chai' + +import {tokenize} from '../../src/ux/colorize-json' + +describe('colorizeJson', () => { + it('tokenizes a basic JSON object', () => { + const result = tokenize({ + foo: 'bar', + }) + + expect(result).to.deep.equal([ + {type: 'brace', value: '{'}, + {type: 'key', value: '"foo"'}, + {type: 'colon', value: ':'}, + {type: 'string', value: '"bar"'}, + {type: 'brace', value: '}'}, + ]) + }) + + it('tokenizes a basic JSON string', () => { + const result = tokenize('{"foo":"bar"}') + + expect(result).to.deep.equal([ + {type: 'brace', value: '{'}, + {type: 'key', value: '"foo"'}, + {type: 'colon', value: ':'}, + {type: 'string', value: '"bar"'}, + {type: 'brace', value: '}'}, + ]) + }) + + it('tokenizes an array', () => { + const result = tokenize(['foo', 'bar']) + + expect(result).to.deep.equal([ + {type: 'bracket', value: '['}, + {type: 'string', value: '"foo"'}, + {type: 'comma', value: ','}, + {type: 'string', value: '"bar"'}, + {type: 'bracket', value: ']'}, + ]) + }) + + it('includes whitespace', () => { + const result = tokenize('{\n "foo": "bar"\n}') + + expect(result).to.deep.equal([ + {type: 'brace', value: '{'}, + {type: 'whitespace', value: '\n '}, + {type: 'key', value: '"foo"'}, + {type: 'colon', value: ':'}, + {type: 'whitespace', value: ' '}, + {type: 'string', value: '"bar"'}, + {type: 'whitespace', value: '\n'}, + {type: 'brace', value: '}'}, + ]) + }) + + it('tokenizes boolean values', () => { + let result = tokenize('true') + expect(result).to.deep.equal([{type: 'boolean', value: 'true'}]) + + result = tokenize('false') + expect(result).to.deep.equal([{type: 'boolean', value: 'false'}]) + }) + + it('tokenizes integer values', () => { + let result = tokenize('123') + expect(result).to.deep.equal([{type: 'number', value: '123'}]) + + result = tokenize('-10') + expect(result).to.deep.equal([{type: 'number', value: '-10'}]) + }) + + it('tokenizes a decimal number', () => { + const result = tokenize('1.234') + expect(result).to.deep.equal([{type: 'number', value: '1.234'}]) + }) + + it('tokenizes a scientific notation number', () => { + let result = tokenize('12e5') + expect(result).to.deep.equal([{type: 'number', value: '12e5'}]) + + result = tokenize('12e+5') + expect(result).to.deep.equal([{type: 'number', value: '12e+5'}]) + + result = tokenize('12E-5') + expect(result).to.deep.equal([{type: 'number', value: '12E-5'}]) + }) + + it('tokenizes null', () => { + const result = tokenize('null') + expect(result).to.deep.equal([{type: 'null', value: 'null'}]) + }) + + it('tokenizes a string literal with brace characters', () => { + const result = tokenize('"{hello}"') + expect(result).to.deep.equal([{type: 'string', value: '"{hello}"'}]) + }) + + it('tokenizes a string literal with bracket characters', () => { + const result = tokenize('"[hello]"') + expect(result).to.deep.equal([{type: 'string', value: '"[hello]"'}]) + }) + + it('tokenizes a string literal with an escaped quote', () => { + const result = tokenize('"a\\"b"') + expect(result).to.deep.equal([{type: 'string', value: '"a\\"b"'}]) + }) + + it('tokenizes a key-value pair with whitespace between the :', () => { + const result = tokenize('"foo" : "bar"') + expect(result).to.deep.equal([ + {type: 'key', value: '"foo"'}, + {type: 'whitespace', value: ' '}, + {type: 'colon', value: ':'}, + {type: 'whitespace', value: ' '}, + {type: 'string', value: '"bar"'}, + ]) + }) + + it('given an undefined json when get token should have no results', () => { + const result = tokenize() + + expect(result).to.deep.equal([]) + }) +}) diff --git a/test/cli-ux/theme.test.ts b/test/ux/theme.test.ts similarity index 71% rename from test/cli-ux/theme.test.ts rename to test/ux/theme.test.ts index 051918d62..ad2459cae 100644 --- a/test/cli-ux/theme.test.ts +++ b/test/ux/theme.test.ts @@ -1,20 +1,19 @@ +import ansis from 'ansis' import {config, expect} from 'chai' -import chalk from 'chalk' config.truncateThreshold = 0 -import {colorize, getColor, parseTheme} from '../../src/cli-ux/theme' -import {THEME_KEYS} from '../../src/interfaces/theme' +import {colorize, parseTheme} from '../../src/ux/theme' describe('colorize', () => { it('should return text with ansi characters when given hex code', () => { - const color = getColor('#FF0000') + const color = '#FF0000' const text = colorize(color, 'brazil') - expect(text).to.equal(chalk.hex(color)('brazil')) + expect(text).to.equal(ansis.hex(color)('brazil')) }) - it('should return text with ansi characters when standard chalk color', () => { + it('should return text with ansi characters when standard ansis color', () => { const text = colorize('red', 'brazil') - expect(text).to.equal(chalk.red('brazil')) + expect(text).to.equal(ansis.red('brazil')) }) it('should return text without ansi characters when given undefined', () => { @@ -23,7 +22,7 @@ describe('colorize', () => { }) it('should return empty text without ansi characters when given color', () => { - const color = getColor('#FF0000') + const color = '#FF0000' const text = colorize(color, '') expect(text).to.equal('') }) @@ -65,10 +64,10 @@ describe('theme parsing', () => { const theme = parseTheme(untypedTheme) - expect(theme).to.deep.equal({alias: '#FFFFFF'}) + expect(theme).to.deep.equal({alias: 'rgb(255, 255, 255)'}) }) - it('should parse untyped theme json to theme using chalk standard colors', () => { + it('should parse untyped theme json to theme using ansis standard colors', () => { const untypedTheme = { alias: 'cyan', bin: 'cyan', @@ -101,24 +100,3 @@ describe('theme parsing', () => { expect(theme).to.deep.equal({}) }) }) - -describe('THEME_KEYS', () => { - it('should always have native theme keys', () => { - expect(THEME_KEYS).deep.equal([ - 'alias', - 'bin', - 'command', - 'commandSummary', - 'dollarSign', - 'flag', - 'flagDefaultValue', - 'flagOptions', - 'flagRequired', - 'flagSeparator', - 'sectionDescription', - 'sectionHeader', - 'topic', - 'version', - ]) - }) -}) diff --git a/yarn.lock b/yarn.lock index eafcf5e27..257dec274 100644 --- a/yarn.lock +++ b/yarn.lock @@ -220,166 +220,161 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== -"@commitlint/cli@^17.8.1": - version "17.8.1" - resolved "https://registry.yarnpkg.com/@commitlint/cli/-/cli-17.8.1.tgz#10492114a022c91dcfb1d84dac773abb3db76d33" - integrity sha512-ay+WbzQesE0Rv4EQKfNbSMiJJ12KdKTDzIt0tcK4k11FdsWmtwP0Kp1NWMOUswfIWo6Eb7p7Ln721Nx9FLNBjg== - dependencies: - "@commitlint/format" "^17.8.1" - "@commitlint/lint" "^17.8.1" - "@commitlint/load" "^17.8.1" - "@commitlint/read" "^17.8.1" - "@commitlint/types" "^17.8.1" - execa "^5.0.0" - lodash.isfunction "^3.0.9" - resolve-from "5.0.0" - resolve-global "1.0.0" +"@commitlint/cli@^19.2.1": + version "19.2.1" + resolved "https://registry.yarnpkg.com/@commitlint/cli/-/cli-19.2.1.tgz#8f00d27a8b7c7780e75b06fd4658fdc1e9209f1b" + integrity sha512-cbkYUJsLqRomccNxvoJTyv5yn0bSy05BBizVyIcLACkRbVUqYorC351Diw/XFSWC/GtpwiwT2eOvQgFZa374bg== + dependencies: + "@commitlint/format" "^19.0.3" + "@commitlint/lint" "^19.1.0" + "@commitlint/load" "^19.2.0" + "@commitlint/read" "^19.2.1" + "@commitlint/types" "^19.0.3" + execa "^8.0.1" yargs "^17.0.0" -"@commitlint/config-conventional@^17.8.1": - version "17.8.1" - resolved "https://registry.yarnpkg.com/@commitlint/config-conventional/-/config-conventional-17.8.1.tgz#e5bcf0cfec8da7ac50bc04dc92e0a4ea74964ce0" - integrity sha512-NxCOHx1kgneig3VLauWJcDWS40DVjg7nKOpBEEK9E5fjJpQqLCilcnKkIIjdBH98kEO1q3NpE5NSrZ2kl/QGJg== +"@commitlint/config-conventional@^19": + version "19.1.0" + resolved "https://registry.yarnpkg.com/@commitlint/config-conventional/-/config-conventional-19.1.0.tgz#6b4b7938aa3bc308214a683247520f602e55961e" + integrity sha512-KIKD2xrp6Uuk+dcZVj3++MlzIr/Su6zLE8crEDQCZNvWHNQSeeGbzOlNtsR32TUy6H3JbP7nWgduAHCaiGQ6EA== dependencies: - conventional-changelog-conventionalcommits "^6.1.0" + "@commitlint/types" "^19.0.3" + conventional-changelog-conventionalcommits "^7.0.2" -"@commitlint/config-validator@^17.8.1": - version "17.8.1" - resolved "https://registry.yarnpkg.com/@commitlint/config-validator/-/config-validator-17.8.1.tgz#5cc93b6b49d5524c9cc345a60e5bf74bcca2b7f9" - integrity sha512-UUgUC+sNiiMwkyiuIFR7JG2cfd9t/7MV8VB4TZ+q02ZFkHoduUS4tJGsCBWvBOGD9Btev6IecPMvlWUfJorkEA== +"@commitlint/config-validator@^19.0.3": + version "19.0.3" + resolved "https://registry.yarnpkg.com/@commitlint/config-validator/-/config-validator-19.0.3.tgz#052b181a30da6b4fc16dc5230f4589ac95e0bc81" + integrity sha512-2D3r4PKjoo59zBc2auodrSCaUnCSALCx54yveOFwwP/i2kfEAQrygwOleFWswLqK0UL/F9r07MFi5ev2ohyM4Q== dependencies: - "@commitlint/types" "^17.8.1" + "@commitlint/types" "^19.0.3" ajv "^8.11.0" -"@commitlint/ensure@^17.8.1": - version "17.8.1" - resolved "https://registry.yarnpkg.com/@commitlint/ensure/-/ensure-17.8.1.tgz#59183557844999dbb6aab6d03629a3d104d01a8d" - integrity sha512-xjafwKxid8s1K23NFpL8JNo6JnY/ysetKo8kegVM7c8vs+kWLP8VrQq+NbhgVlmCojhEDbzQKp4eRXSjVOGsow== +"@commitlint/ensure@^19.0.3": + version "19.0.3" + resolved "https://registry.yarnpkg.com/@commitlint/ensure/-/ensure-19.0.3.tgz#d172b1b72ca88cbd317ea1ee79f3a03dbaccc76e" + integrity sha512-SZEpa/VvBLoT+EFZVb91YWbmaZ/9rPH3ESrINOl0HD2kMYsjvl0tF7nMHh0EpTcv4+gTtZBAe1y/SS6/OhfZzQ== dependencies: - "@commitlint/types" "^17.8.1" + "@commitlint/types" "^19.0.3" lodash.camelcase "^4.3.0" lodash.kebabcase "^4.1.1" lodash.snakecase "^4.1.1" lodash.startcase "^4.4.0" lodash.upperfirst "^4.3.1" -"@commitlint/execute-rule@^17.8.1": - version "17.8.1" - resolved "https://registry.yarnpkg.com/@commitlint/execute-rule/-/execute-rule-17.8.1.tgz#504ed69eb61044eeb84fdfd10cc18f0dab14f34c" - integrity sha512-JHVupQeSdNI6xzA9SqMF+p/JjrHTcrJdI02PwesQIDCIGUrv04hicJgCcws5nzaoZbROapPs0s6zeVHoxpMwFQ== +"@commitlint/execute-rule@^19.0.0": + version "19.0.0" + resolved "https://registry.yarnpkg.com/@commitlint/execute-rule/-/execute-rule-19.0.0.tgz#928fb239ae8deec82a6e3b05ec9cfe20afa83856" + integrity sha512-mtsdpY1qyWgAO/iOK0L6gSGeR7GFcdW7tIjcNFxcWkfLDF5qVbPHKuGATFqRMsxcO8OUKNj0+3WOHB7EHm4Jdw== -"@commitlint/format@^17.8.1": - version "17.8.1" - resolved "https://registry.yarnpkg.com/@commitlint/format/-/format-17.8.1.tgz#6108bb6b4408e711006680649927e1b559bdc5f8" - integrity sha512-f3oMTyZ84M9ht7fb93wbCKmWxO5/kKSbwuYvS867duVomoOsgrgljkGGIztmT/srZnaiGbaK8+Wf8Ik2tSr5eg== +"@commitlint/format@^19.0.3": + version "19.0.3" + resolved "https://registry.yarnpkg.com/@commitlint/format/-/format-19.0.3.tgz#6e3dcdc028b39d370ba717b8bde0853705c467dc" + integrity sha512-QjjyGyoiVWzx1f5xOteKHNLFyhyweVifMgopozSgx1fGNrGV8+wp7k6n1t6StHdJ6maQJ+UUtO2TcEiBFRyR6Q== dependencies: - "@commitlint/types" "^17.8.1" - chalk "^4.1.0" + "@commitlint/types" "^19.0.3" + chalk "^5.3.0" -"@commitlint/is-ignored@^17.8.1": - version "17.8.1" - resolved "https://registry.yarnpkg.com/@commitlint/is-ignored/-/is-ignored-17.8.1.tgz#cf25bcd8409c79684b63f8bdeb35df48edda244e" - integrity sha512-UshMi4Ltb4ZlNn4F7WtSEugFDZmctzFpmbqvpyxD3la510J+PLcnyhf9chs7EryaRFJMdAKwsEKfNK0jL/QM4g== - dependencies: - "@commitlint/types" "^17.8.1" - semver "7.5.4" - -"@commitlint/lint@^17.8.1": - version "17.8.1" - resolved "https://registry.yarnpkg.com/@commitlint/lint/-/lint-17.8.1.tgz#bfc21215f6b18d41d4d43e2aa3cb79a5d7726cd8" - integrity sha512-aQUlwIR1/VMv2D4GXSk7PfL5hIaFSfy6hSHV94O8Y27T5q+DlDEgd/cZ4KmVI+MWKzFfCTiTuWqjfRSfdRllCA== - dependencies: - "@commitlint/is-ignored" "^17.8.1" - "@commitlint/parse" "^17.8.1" - "@commitlint/rules" "^17.8.1" - "@commitlint/types" "^17.8.1" - -"@commitlint/load@^17.8.1": - version "17.8.1" - resolved "https://registry.yarnpkg.com/@commitlint/load/-/load-17.8.1.tgz#fa061e7bfa53281eb03ca8517ca26d66a189030c" - integrity sha512-iF4CL7KDFstP1kpVUkT8K2Wl17h2yx9VaR1ztTc8vzByWWcbO/WaKwxsnCOqow9tVAlzPfo1ywk9m2oJ9ucMqA== - dependencies: - "@commitlint/config-validator" "^17.8.1" - "@commitlint/execute-rule" "^17.8.1" - "@commitlint/resolve-extends" "^17.8.1" - "@commitlint/types" "^17.8.1" - "@types/node" "20.5.1" - chalk "^4.1.0" - cosmiconfig "^8.0.0" - cosmiconfig-typescript-loader "^4.0.0" +"@commitlint/is-ignored@^19.0.3": + version "19.0.3" + resolved "https://registry.yarnpkg.com/@commitlint/is-ignored/-/is-ignored-19.0.3.tgz#a64e0e217044f2d916127369d21ea12324a834fe" + integrity sha512-MqDrxJaRSVSzCbPsV6iOKG/Lt52Y+PVwFVexqImmYYFhe51iVJjK2hRhOG2jUAGiUHk4jpdFr0cZPzcBkSzXDQ== + dependencies: + "@commitlint/types" "^19.0.3" + semver "^7.6.0" + +"@commitlint/lint@^19.1.0": + version "19.1.0" + resolved "https://registry.yarnpkg.com/@commitlint/lint/-/lint-19.1.0.tgz#0f4b26b1452d59a92a28b5fa6de9bdbee18399a1" + integrity sha512-ESjaBmL/9cxm+eePyEr6SFlBUIYlYpI80n+Ltm7IA3MAcrmiP05UMhJdAD66sO8jvo8O4xdGn/1Mt2G5VzfZKw== + dependencies: + "@commitlint/is-ignored" "^19.0.3" + "@commitlint/parse" "^19.0.3" + "@commitlint/rules" "^19.0.3" + "@commitlint/types" "^19.0.3" + +"@commitlint/load@^19.2.0": + version "19.2.0" + resolved "https://registry.yarnpkg.com/@commitlint/load/-/load-19.2.0.tgz#3ca51fdead4f1e1e09c9c7df343306412b1ef295" + integrity sha512-XvxxLJTKqZojCxaBQ7u92qQLFMMZc4+p9qrIq/9kJDy8DOrEa7P1yx7Tjdc2u2JxIalqT4KOGraVgCE7eCYJyQ== + dependencies: + "@commitlint/config-validator" "^19.0.3" + "@commitlint/execute-rule" "^19.0.0" + "@commitlint/resolve-extends" "^19.1.0" + "@commitlint/types" "^19.0.3" + chalk "^5.3.0" + cosmiconfig "^9.0.0" + cosmiconfig-typescript-loader "^5.0.0" lodash.isplainobject "^4.0.6" lodash.merge "^4.6.2" lodash.uniq "^4.5.0" - resolve-from "^5.0.0" - ts-node "^10.8.1" - typescript "^4.6.4 || ^5.2.2" - -"@commitlint/message@^17.8.1": - version "17.8.1" - resolved "https://registry.yarnpkg.com/@commitlint/message/-/message-17.8.1.tgz#a5cd226c419be20ee03c3d237db6ac37b95958b3" - integrity sha512-6bYL1GUQsD6bLhTH3QQty8pVFoETfFQlMn2Nzmz3AOLqRVfNNtXBaSY0dhZ0dM6A2MEq4+2d7L/2LP8TjqGRkA== - -"@commitlint/parse@^17.8.1": - version "17.8.1" - resolved "https://registry.yarnpkg.com/@commitlint/parse/-/parse-17.8.1.tgz#6e00b8f50ebd63562d25dcf4230da2c9f984e626" - integrity sha512-/wLUickTo0rNpQgWwLPavTm7WbwkZoBy3X8PpkUmlSmQJyWQTj0m6bDjiykMaDt41qcUbfeFfaCvXfiR4EGnfw== - dependencies: - "@commitlint/types" "^17.8.1" - conventional-changelog-angular "^6.0.0" - conventional-commits-parser "^4.0.0" - -"@commitlint/read@^17.8.1": - version "17.8.1" - resolved "https://registry.yarnpkg.com/@commitlint/read/-/read-17.8.1.tgz#b3f28777607c756078356cc133368b0e8c08092f" - integrity sha512-Fd55Oaz9irzBESPCdMd8vWWgxsW3OWR99wOntBDHgf9h7Y6OOHjWEdS9Xzen1GFndqgyoaFplQS5y7KZe0kO2w== - dependencies: - "@commitlint/top-level" "^17.8.1" - "@commitlint/types" "^17.8.1" - fs-extra "^11.0.0" - git-raw-commits "^2.0.11" - minimist "^1.2.6" -"@commitlint/resolve-extends@^17.8.1": - version "17.8.1" - resolved "https://registry.yarnpkg.com/@commitlint/resolve-extends/-/resolve-extends-17.8.1.tgz#9af01432bf2fd9ce3dd5a00d266cce14e4c977e7" - integrity sha512-W/ryRoQ0TSVXqJrx5SGkaYuAaE/BUontL1j1HsKckvM6e5ZaG0M9126zcwL6peKSuIetJi7E87PRQF8O86EW0Q== - dependencies: - "@commitlint/config-validator" "^17.8.1" - "@commitlint/types" "^17.8.1" - import-fresh "^3.0.0" +"@commitlint/message@^19.0.0": + version "19.0.0" + resolved "https://registry.yarnpkg.com/@commitlint/message/-/message-19.0.0.tgz#f789dd1b7a1f9c784578e0111f46cc3fecf5a531" + integrity sha512-c9czf6lU+9oF9gVVa2lmKaOARJvt4soRsVmbR7Njwp9FpbBgste5i7l/2l5o8MmbwGh4yE1snfnsy2qyA2r/Fw== + +"@commitlint/parse@^19.0.3": + version "19.0.3" + resolved "https://registry.yarnpkg.com/@commitlint/parse/-/parse-19.0.3.tgz#a2d09876d458e17ad0e1695b04f41af8b50a41c2" + integrity sha512-Il+tNyOb8VDxN3P6XoBBwWJtKKGzHlitEuXA5BP6ir/3loWlsSqDr5aecl6hZcC/spjq4pHqNh0qPlfeWu38QA== + dependencies: + "@commitlint/types" "^19.0.3" + conventional-changelog-angular "^7.0.0" + conventional-commits-parser "^5.0.0" + +"@commitlint/read@^19.2.1": + version "19.2.1" + resolved "https://registry.yarnpkg.com/@commitlint/read/-/read-19.2.1.tgz#7296b99c9a989e60e5927fff8388a1dd44299c2f" + integrity sha512-qETc4+PL0EUv7Q36lJbPG+NJiBOGg7SSC7B5BsPWOmei+Dyif80ErfWQ0qXoW9oCh7GTpTNRoaVhiI8RbhuaNw== + dependencies: + "@commitlint/top-level" "^19.0.0" + "@commitlint/types" "^19.0.3" + execa "^8.0.1" + git-raw-commits "^4.0.0" + minimist "^1.2.8" + +"@commitlint/resolve-extends@^19.1.0": + version "19.1.0" + resolved "https://registry.yarnpkg.com/@commitlint/resolve-extends/-/resolve-extends-19.1.0.tgz#fa5b8f921e9c8d76f53624c35bf25b9676bd73fa" + integrity sha512-z2riI+8G3CET5CPgXJPlzftH+RiWYLMYv4C9tSLdLXdr6pBNimSKukYP9MS27ejmscqCTVA4almdLh0ODD2KYg== + dependencies: + "@commitlint/config-validator" "^19.0.3" + "@commitlint/types" "^19.0.3" + global-directory "^4.0.1" + import-meta-resolve "^4.0.0" lodash.mergewith "^4.6.2" resolve-from "^5.0.0" - resolve-global "^1.0.0" -"@commitlint/rules@^17.8.1": - version "17.8.1" - resolved "https://registry.yarnpkg.com/@commitlint/rules/-/rules-17.8.1.tgz#da49cab1b7ebaf90d108de9f58f684dc4ccb65a0" - integrity sha512-2b7OdVbN7MTAt9U0vKOYKCDsOvESVXxQmrvuVUZ0rGFMCrCPJWWP1GJ7f0lAypbDAhaGb8zqtdOr47192LBrIA== +"@commitlint/rules@^19.0.3": + version "19.0.3" + resolved "https://registry.yarnpkg.com/@commitlint/rules/-/rules-19.0.3.tgz#de647a9055847cae4f3ae32b4798096b604584f3" + integrity sha512-TspKb9VB6svklxNCKKwxhELn7qhtY1rFF8ls58DcFd0F97XoG07xugPjjbVnLqmMkRjZDbDIwBKt9bddOfLaPw== dependencies: - "@commitlint/ensure" "^17.8.1" - "@commitlint/message" "^17.8.1" - "@commitlint/to-lines" "^17.8.1" - "@commitlint/types" "^17.8.1" - execa "^5.0.0" + "@commitlint/ensure" "^19.0.3" + "@commitlint/message" "^19.0.0" + "@commitlint/to-lines" "^19.0.0" + "@commitlint/types" "^19.0.3" + execa "^8.0.1" -"@commitlint/to-lines@^17.8.1": - version "17.8.1" - resolved "https://registry.yarnpkg.com/@commitlint/to-lines/-/to-lines-17.8.1.tgz#a5c4a7cf7dff3dbdd69289fc0eb19b66f3cfe017" - integrity sha512-LE0jb8CuR/mj6xJyrIk8VLz03OEzXFgLdivBytoooKO5xLt5yalc8Ma5guTWobw998sbR3ogDd+2jed03CFmJA== +"@commitlint/to-lines@^19.0.0": + version "19.0.0" + resolved "https://registry.yarnpkg.com/@commitlint/to-lines/-/to-lines-19.0.0.tgz#aa6618eb371bafbc0cd3b48f0db565c4a40462c6" + integrity sha512-vkxWo+VQU5wFhiP9Ub9Sre0FYe019JxFikrALVoD5UGa8/t3yOJEpEhxC5xKiENKKhUkTpEItMTRAjHw2SCpZw== -"@commitlint/top-level@^17.8.1": - version "17.8.1" - resolved "https://registry.yarnpkg.com/@commitlint/top-level/-/top-level-17.8.1.tgz#206d37d6782f33c9572e44fbe3758392fdeea7bc" - integrity sha512-l6+Z6rrNf5p333SHfEte6r+WkOxGlWK4bLuZKbtf/2TXRN+qhrvn1XE63VhD8Oe9oIHQ7F7W1nG2k/TJFhx2yA== +"@commitlint/top-level@^19.0.0": + version "19.0.0" + resolved "https://registry.yarnpkg.com/@commitlint/top-level/-/top-level-19.0.0.tgz#9c44d7cec533bb9598bfae9658737e2d6a903605" + integrity sha512-KKjShd6u1aMGNkCkaX4aG1jOGdn7f8ZI8TR1VEuNqUOjWTOdcDSsmglinglJ18JTjuBX5I1PtjrhQCRcixRVFQ== dependencies: - find-up "^5.0.0" + find-up "^7.0.0" -"@commitlint/types@^17.8.1": - version "17.8.1" - resolved "https://registry.yarnpkg.com/@commitlint/types/-/types-17.8.1.tgz#883a0ad35c5206d5fef7bc6ce1bbe648118af44e" - integrity sha512-PXDQXkAmiMEG162Bqdh9ChML/GJZo6vU+7F03ALKDK8zYc6SuAr47LjG7hGYRqUOz+WK0dU7bQ0xzuqFMdxzeQ== +"@commitlint/types@^19.0.3": + version "19.0.3" + resolved "https://registry.yarnpkg.com/@commitlint/types/-/types-19.0.3.tgz#feff4ecac2b5c359f2a57f9ab094b2ac80ef0266" + integrity sha512-tpyc+7i6bPG9mvaBbtKUeghfyZSDgWquIDfMgqYtTbmZ9Y9VzEm2je9EYcQ0aoz5o7NvGS+rcDec93yO08MHYA== dependencies: - chalk "^4.1.0" + "@types/conventional-commits-parser" "^5.0.0" + chalk "^5.3.0" "@cspotcode/source-map-support@^0.8.0": version "0.8.1" @@ -708,13 +703,6 @@ dependencies: which "^4.0.0" -"@npmcli/promise-spawn@^7.0.1": - version "7.0.1" - resolved "https://registry.yarnpkg.com/@npmcli/promise-spawn/-/promise-spawn-7.0.1.tgz#a836de2f42a2245d629cf6fbb8dd6c74c74c55af" - integrity sha512-P4KkF9jX3y+7yFUxgcUdDtLy+t4OlDGuEBLNs57AZsfSfg+uV6MLndqGpnl4831ggaEdXwR50XFoZP4VFtHolg== - dependencies: - which "^4.0.0" - "@npmcli/query@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@npmcli/query/-/query-3.0.1.tgz#77d63ceb7d27ed748da3cc8b50d45fc341448ed6" @@ -733,18 +721,7 @@ read-package-json-fast "^3.0.0" which "^4.0.0" -"@npmcli/run-script@^7.0.4": - version "7.0.4" - resolved "https://registry.yarnpkg.com/@npmcli/run-script/-/run-script-7.0.4.tgz#9f29aaf4bfcf57f7de2a9e28d1ef091d14b2e6eb" - integrity sha512-9ApYM/3+rBt9V80aYg6tZfzj3UWdiYyCt7gJUD1VJKvWF5nwKDSICXbYIQbspFTq6TOpbsEtIC0LArB8d9PFmg== - dependencies: - "@npmcli/node-gyp" "^3.0.0" - "@npmcli/package-json" "^5.0.0" - "@npmcli/promise-spawn" "^7.0.0" - node-gyp "^10.0.0" - which "^4.0.0" - -"@oclif/core@^3.25.2", "@oclif/core@^3.26.0": +"@oclif/core@^3.26.0": version "3.26.0" resolved "https://registry.yarnpkg.com/@oclif/core/-/core-3.26.0.tgz#959d5e9f13f4ad6a4e98235ad125189df9ee4279" integrity sha512-TpMdfD4tfA2tVVbd4l0PrP02o5KoUXYmudBbTC7CeguDo/GLoprw4uL8cMsaVA26+cbcy7WYtOEydQiHVtJixA== @@ -785,35 +762,25 @@ dependencies: "@oclif/core" "^3.26.0" -"@oclif/plugin-plugins@^4": - version "4.3.10" - resolved "https://registry.yarnpkg.com/@oclif/plugin-plugins/-/plugin-plugins-4.3.10.tgz#f58ca275b4bfb841d87d22b030fbd2a0c4fe9bb8" - integrity sha512-DYdkGDFNRVgdTW4RUPk+i5JTjQGZphDKs3j1uL/bxRYO7dIRSrohgeso6jAiPSE+igQtAXa8KqKhsanK4za0XA== +"@oclif/plugin-plugins@^5": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@oclif/plugin-plugins/-/plugin-plugins-5.0.5.tgz#7ecceb7b146b228ba5f591bea11c1807379932e2" + integrity sha512-2Mi87LYSj8LBPXR/j+DffFoyaTmaWiKXayyomyA3Q/TOk8/FkMSNzaUxYzno3sFXy/LsPRd9DZXm+UO1dHV1jw== dependencies: - "@oclif/core" "^3.25.2" + "@oclif/core" "^3.26.0" chalk "^5.3.0" debug "^4.3.4" - npm "10.5.0" - npm-run-path "^4.0.1" + npm "10.2.4" + npm-package-arg "^11.0.1" + npm-run-path "^5.3.0" semver "^7.6.0" - shelljs "^0.8.5" validate-npm-package-name "^5.0.0" - yarn "^1.22.21" "@oclif/prettier-config@^0.2.1": version "0.2.1" resolved "https://registry.yarnpkg.com/@oclif/prettier-config/-/prettier-config-0.2.1.tgz#1def9f38134f9bfb229257f48a35f7d0d183dc78" integrity sha512-XB8kwQj8zynXjIIWRm+6gO/r8Qft2xKtwBMSmq1JRqtA6TpwpqECqiu8LosBCyg2JBXuUy2lU23/L98KIR7FrQ== -"@oclif/test@^3.2.9": - version "3.2.9" - resolved "https://registry.yarnpkg.com/@oclif/test/-/test-3.2.9.tgz#73b40d80f8e94b15dded0dd9333f5cf95a136f4c" - integrity sha512-eVZO4I29Ipp7WlwAZOmQ/Z0Dm/J9qnTUTAqKHlylEmp9JYfVQ3C+SqZVNNp3KJ+D/XJMC7xQ748JDBQFUZebbA== - dependencies: - "@oclif/core" "^3.26.0" - chai "^4.4.1" - fancy-test "^3.0.14" - "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -875,6 +842,14 @@ "@sigstore/protobuf-specs" "^0.2.1" tuf-js "^2.1.0" +"@sigstore/tuf@^2.2.0": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@sigstore/tuf/-/tuf-2.3.2.tgz#e9c5bffc2a5f3434f87195902d7f9cd7f48c70fa" + integrity sha512-mwbY1VrEGU4CO55t+Kl6I7WZzIl+ysSzEYdA1Nv/FTrl2bkeaPXo5PnWZAVfcY2zSdhOpsUTJW67/M2zHXGn5w== + dependencies: + "@sigstore/protobuf-specs" "^0.3.0" + tuf-js "^2.2.0" + "@sigstore/tuf@^2.3.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@sigstore/tuf/-/tuf-2.3.0.tgz#de64925ea10b16f3a7e77535d91eaf22be4dd904" @@ -883,14 +858,6 @@ "@sigstore/protobuf-specs" "^0.2.1" tuf-js "^2.2.0" -"@sigstore/tuf@^2.3.1": - version "2.3.1" - resolved "https://registry.yarnpkg.com/@sigstore/tuf/-/tuf-2.3.1.tgz#86ff3c3c907e271696c88de0108d9063a8cbcc45" - integrity sha512-9Iv40z652td/QbV0o5n/x25H9w6IYRt2pIGbTX55yFDYlApDQn/6YZomjz6+KBx69rXHLzHcbtTS586mDdFD+Q== - dependencies: - "@sigstore/protobuf-specs" "^0.3.0" - tuf-js "^2.2.0" - "@sigstore/verify@^0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@sigstore/verify/-/verify-0.1.0.tgz#c017aadb1a516ab4a10651cece29463aa9540bfe" @@ -919,10 +886,10 @@ dependencies: type-detect "4.0.8" -"@sinonjs/fake-timers@^10.0.2", "@sinonjs/fake-timers@^10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66" - integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== +"@sinonjs/fake-timers@^11.2.2": + version "11.2.2" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz#50063cc3574f4a27bd8453180a04171c85cc9699" + integrity sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw== dependencies: "@sinonjs/commons" "^3.0.0" @@ -935,7 +902,7 @@ lodash.get "^4.4.2" type-detect "^4.0.8" -"@sinonjs/text-encoding@^0.7.1": +"@sinonjs/text-encoding@^0.7.2": version "0.7.2" resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz#5981a8db18b56ba38ef0efb7d995b12aa7b51918" integrity sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ== @@ -978,13 +945,6 @@ "@tufjs/canonical-json" "2.0.0" minimatch "^9.0.3" -"@types/ansi-styles@^3.2.1": - version "3.2.1" - resolved "https://registry.yarnpkg.com/@types/ansi-styles/-/ansi-styles-3.2.1.tgz#49e996bb6e0b7957ca831205df31eb9a0702492c" - integrity sha512-UFa7mfKgSutXdT+elzJo8Ulr7FHgLNAyglVIOZYXFNJVQERm8DPrcwPret5BYk66LBE7fwm1XoVGi76MJkQ6ow== - dependencies: - "@types/color-name" "*" - "@types/benchmark@^2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@types/benchmark/-/benchmark-2.1.5.tgz#940c1850c18fdfdaee3fd6ed29cd92ae0d445b45" @@ -1016,24 +976,12 @@ dependencies: "@types/node" "*" -"@types/color-convert@*": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@types/color-convert/-/color-convert-2.0.2.tgz#a5fa5da9b866732f8bf86b01964869011e2a2356" - integrity sha512-KGRIgCxwcgazts4MXRCikPbIMzBpjfdgEZSy8TRHU/gtg+f9sOfHdtK8unPfxIoBtyd2aTTwINVLSNENlC8U8A== - dependencies: - "@types/color-name" "*" - -"@types/color-name@*": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.3.tgz#c488ac2e519c9795faa0d54e8156d54e66adc4e6" - integrity sha512-87W6MJCKZYDhLAx/J1ikW8niMvmGRyY+rpUxWpL1cO7F8Uu5CHuQoFv+R0/L5pgNdW4jTyda42kv60uwVIPjLw== - -"@types/color@^3.0.6": - version "3.0.6" - resolved "https://registry.yarnpkg.com/@types/color/-/color-3.0.6.tgz#29c27a99d4de2975e1676712679a0bd7f646a3fb" - integrity sha512-NMiNcZFRUAiUUCCf7zkAelY8eV3aKqfbzyFQlXpPIEeoNDbsEHGpb854V3gzTsGKYj830I5zPuOwU/TP5/cW6A== +"@types/conventional-commits-parser@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz#8c9d23e0b415b24b91626d07017303755d542dc8" + integrity sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ== dependencies: - "@types/color-convert" "*" + "@types/node" "*" "@types/debug@^4.1.10": version "4.1.12" @@ -1067,11 +1015,6 @@ dependencies: indent-string "*" -"@types/js-yaml@^3.12.7": - version "3.12.7" - resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.7.tgz#330c5d97a3500e9c903210d6e49f02964af04a0e" - integrity sha512-S6+8JAYTE1qdsc9HMVsfY7+SgSuUU/Tp6TYTmITW0PZxiyIMvol3Gy//y69Wkhs0ti4py5qgR3uZH6uz/DNzJQ== - "@types/json-schema@*": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" @@ -1087,11 +1030,6 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/lodash@*": - version "4.14.199" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.199.tgz#c3edb5650149d847a277a8961a7ad360c474e9bf" - integrity sha512-Vrjz5N5Ia4SEzWWgIVwnHNEnb1UE1XMkvY5DGXrAeOGE9imk0hgTHh5GyDjLDJi9OTCn9oo9dXH1uToK1VRfrg== - "@types/minimist@^1.2.0": version "1.2.2" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" @@ -1107,13 +1045,6 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.33.tgz#80bf1da64b15f21fd8c1dc387c31929317d99ee9" integrity sha512-AuHIyzR5Hea7ij0P9q7vx7xu4z0C28ucwjAZC0ja7JhINyCnOw8/DnvAPQQ9TfOlCtZAmCERKQX9+o1mgQhuOQ== -"@types/node-notifier@^8.0.5": - version "8.0.5" - resolved "https://registry.yarnpkg.com/@types/node-notifier/-/node-notifier-8.0.5.tgz#c5786810d16bbff10550e2d10efe5a17eaf1095e" - integrity sha512-LX7+8MtTsv6szumAp6WOy87nqMEdGhhry/Qfprjm1Ma6REjVzeF7SCyvPtp5RaF6IkXCS9V4ra8g5fwvf2ZAYg== - dependencies: - "@types/node" "*" - "@types/node@*": version "20.8.6" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.6.tgz#0dbd4ebcc82ad0128df05d0e6f57e05359ee47fa" @@ -1121,11 +1052,6 @@ dependencies: undici-types "~5.25.1" -"@types/node@20.5.1": - version "20.5.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.1.tgz#178d58ee7e4834152b0e8b4d30cbfab578b9bb30" - integrity sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg== - "@types/node@^18": version "18.19.28" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.28.tgz#c64a2c992c8ebbf61100a4570e4eebc1934ae030" @@ -1148,34 +1074,17 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.2.tgz#31f6eec1ed7ec23f4f05608d3a2d381df041f564" integrity sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw== -"@types/sinon@*": - version "10.0.19" - resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.19.tgz#752b752bc40bb5af0bb1aec29bde49b139b91d35" - integrity sha512-MWZNGPSchIdDfb5FL+VFi4zHsHbNOTQEgjqFQk7HazXSXwUU9PAX3z9XBqb3AJGYr9YwrtCtaSMsT3brYsN/jQ== +"@types/sinon@^17.0.3": + version "17.0.3" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-17.0.3.tgz#9aa7e62f0a323b9ead177ed23a36ea757141a5fa" + integrity sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw== dependencies: "@types/sinonjs__fake-timers" "*" "@types/sinonjs__fake-timers@*": - version "8.1.3" - resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.3.tgz#575789c5cf6d410cb288b0b4affaf7e6da44ca51" - integrity sha512-4g+2YyWe0Ve+LBh+WUm1697PD0Kdi6coG1eU0YjQbwx61AZ8XbEpL1zIT6WjuUKrCMCROpEaYQPDjBnDouBVAQ== - -"@types/slice-ansi@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/slice-ansi/-/slice-ansi-4.0.0.tgz#eb40dfbe3ac5c1de61f6bcb9ed471f54baa989d6" - integrity sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ== - -"@types/strip-ansi@^5.2.1": - version "5.2.1" - resolved "https://registry.yarnpkg.com/@types/strip-ansi/-/strip-ansi-5.2.1.tgz#acd97f1f091e332bb7ce697c4609eb2370fa2a92" - integrity sha512-1l5iM0LBkVU8JXxnIoBqNvg+yyOXxPeN6DNoD+7A9AN1B8FhYPSeIXgyNqwIqg1uzipTgVC2hmuDzB0u9qw/PA== - dependencies: - strip-ansi "*" - -"@types/supports-color@^8.1.1": - version "8.1.1" - resolved "https://registry.yarnpkg.com/@types/supports-color/-/supports-color-8.1.1.tgz#1b44b1b096479273adf7f93c75fc4ecc40a61ee4" - integrity sha512-dPWnWsf+kzIG140B8z2w3fr5D03TLWbOAFQl45xUpI3vcizeXriNR5VYkWZ+WTMsUHqZ9Xlt3hrxGNANFyNQfw== + version "8.1.5" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz#5fd3592ff10c1e9695d377020c033116cc2889f2" + integrity sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ== "@types/wordwrap@^1.0.3": version "1.0.3" @@ -1460,12 +1369,10 @@ ansi-escapes@^4.2.1, ansi-escapes@^4.3.2: dependencies: type-fest "^0.21.3" -ansi-escapes@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-5.0.0.tgz#b6a0caf0eef0c41af190e9a749e0c00ec04bb2a6" - integrity sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA== - dependencies: - type-fest "^1.0.2" +ansi-escapes@^6.2.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-6.2.1.tgz#76c54ce9b081dad39acec4b5d53377913825fb0f" + integrity sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig== ansi-regex@^5.0.1: version "5.0.1" @@ -1496,7 +1403,7 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== -ansi-styles@^6.0.0, ansi-styles@^6.1.0: +ansi-styles@^6.0.0, ansi-styles@^6.1.0, ansi-styles@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== @@ -1506,6 +1413,11 @@ ansicolors@~0.3.2: resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" integrity sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk= +ansis@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.0.1.tgz#46e731a2973e55378a18b87713906153c23c4229" + integrity sha512-NMqcJGNewJ+tVAK5fJFY7kBo698ViFSH8ceUfEwBHGZb4SNxHnbJPDBW0wNQU6rYjnBGBwBfUSmeIab/KrA5/A== + any-promise@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" @@ -1804,24 +1716,6 @@ cacache@^18.0.0: tar "^6.1.11" unique-filename "^3.0.0" -cacache@^18.0.2: - version "18.0.2" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-18.0.2.tgz#fd527ea0f03a603be5c0da5805635f8eef00c60c" - integrity sha512-r3NU8h/P+4lVUHfeRw1dtgQYar3DZMm4/cm2bZgOvrFC/su7budSOeqh52VJIC4U4iG1WWwV6vRW0znqBvxNuw== - dependencies: - "@npmcli/fs" "^3.1.0" - fs-minipass "^3.0.0" - glob "^10.2.2" - lru-cache "^10.0.1" - minipass "^7.0.3" - minipass-collect "^2.0.1" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - p-map "^4.0.0" - ssri "^10.0.0" - tar "^6.1.11" - unique-filename "^3.0.0" - caching-transform@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/caching-transform/-/caching-transform-4.0.0.tgz#00d297a4206d71e2163c39eaffa8157ac0651f0f" @@ -2023,6 +1917,11 @@ cli-spinners@^2.5.0: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.1.tgz#9c0b9dad69a6d47cbb4333c14319b060ed395a35" integrity sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ== +cli-spinners@^2.9.2: + version "2.9.2" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" + integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== + cli-table3@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2" @@ -2032,13 +1931,13 @@ cli-table3@^0.6.3: optionalDependencies: "@colors/colors" "1.5.0" -cli-truncate@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-3.1.0.tgz#3f23ab12535e3d73e839bb43e73c9de487db1389" - integrity sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA== +cli-truncate@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-4.0.0.tgz#6cc28a2924fee9e25ce91e973db56c7066e6172a" + integrity sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA== dependencies: slice-ansi "^5.0.0" - string-width "^5.0.0" + string-width "^7.0.0" cliui@^6.0.0: version "6.0.0" @@ -2135,10 +2034,10 @@ columnify@^1.6.0: strip-ansi "^6.0.1" wcwidth "^1.0.0" -commander@11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-11.0.0.tgz#43e19c25dbedc8256203538e8d7e9346877a6f67" - integrity sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ== +commander@11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906" + integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ== commander@^2.16.0, commander@^2.20.3, commander@^2.8.1: version "2.20.3" @@ -2155,13 +2054,13 @@ commander@^9.5.0: resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== -commitlint@^17.8.1: - version "17.8.1" - resolved "https://registry.yarnpkg.com/commitlint/-/commitlint-17.8.1.tgz#0a0b9b952f34d9718f06502ee8496785bf3dd8a3" - integrity sha512-X+VPJwZsQDeGj/DG1NsxhZEl+oMHKNC+1myZ/zauNDoo+7OuLHfTOUU1C1a4CjKW4b6T7NuoFcYfK0kRCjCtbA== +commitlint@^19: + version "19.2.1" + resolved "https://registry.yarnpkg.com/commitlint/-/commitlint-19.2.1.tgz#c6d490e9f4987ea1a2fb7592957aff3eabdc3df2" + integrity sha512-avW7E38gbdbUK7NIi/7gfkaKwlw8tjXy2HlmEdBS9A9+HeoHV1o/i96Sc9qjX0rKyPzKEsLi6cP3Wt3xM0kjEA== dependencies: - "@commitlint/cli" "^17.8.1" - "@commitlint/types" "^17.8.1" + "@commitlint/cli" "^19.2.1" + "@commitlint/types" "^19.0.3" common-ancestor-path@^1.0.1: version "1.0.1" @@ -2196,29 +2095,29 @@ console-control-strings@^1.1.0: resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== -conventional-changelog-angular@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-6.0.0.tgz#a9a9494c28b7165889144fd5b91573c4aa9ca541" - integrity sha512-6qLgrBF4gueoC7AFVHu51nHL9pF9FRjXrH+ceVf7WmAfH3gs+gEYOkvxhjMPjZu57I4AGUGoNTY8V7Hrgf1uqg== +conventional-changelog-angular@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz#5eec8edbff15aa9b1680a8dcfbd53e2d7eb2ba7a" + integrity sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ== dependencies: compare-func "^2.0.0" -conventional-changelog-conventionalcommits@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-6.1.0.tgz#3bad05f4eea64e423d3d90fc50c17d2c8cf17652" - integrity sha512-3cS3GEtR78zTfMzk0AizXKKIdN4OvSh7ibNz6/DPbhWWQu7LqE/8+/GqSodV+sywUR2gpJAdP/1JFf4XtN7Zpw== +conventional-changelog-conventionalcommits@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz#aa5da0f1b2543094889e8cf7616ebe1a8f5c70d5" + integrity sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w== dependencies: compare-func "^2.0.0" -conventional-commits-parser@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-4.0.0.tgz#02ae1178a381304839bce7cea9da5f1b549ae505" - integrity sha512-WRv5j1FsVM5FISJkoYMR6tPk07fkKT0UodruX4je86V4owk451yjXAKzKAPOs9l7y59E2viHUS9eQ+dfUA9NSg== +conventional-commits-parser@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz#57f3594b81ad54d40c1b4280f04554df28627d9a" + integrity sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA== dependencies: JSONStream "^1.3.5" - is-text-path "^1.0.1" - meow "^8.1.2" - split2 "^3.2.2" + is-text-path "^2.0.0" + meow "^12.0.1" + split2 "^4.0.0" convert-source-map@^1.7.0: version "1.9.0" @@ -2230,20 +2129,22 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== -cosmiconfig-typescript-loader@^4.0.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-4.4.0.tgz#f3feae459ea090f131df5474ce4b1222912319f9" - integrity sha512-BabizFdC3wBHhbI4kJh0VkQP9GkBfoHPydD0COMce1nJ1kJAB3F2TmJ/I7diULBKtmEWSwEbuN/KDtgnmUUVmw== +cosmiconfig-typescript-loader@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-5.0.0.tgz#0d3becfe022a871f7275ceb2397d692e06045dc8" + integrity sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA== + dependencies: + jiti "^1.19.1" -cosmiconfig@^8.0.0: - version "8.3.6" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" - integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== +cosmiconfig@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-9.0.0.tgz#34c3fc58287b915f3ae905ab6dc3de258b55ad9d" + integrity sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg== dependencies: + env-paths "^2.2.1" import-fresh "^3.3.0" js-yaml "^4.1.0" parse-json "^5.2.0" - path-type "^4.0.0" create-require@^1.1.0: version "1.1.1" @@ -2271,10 +2172,10 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -dargs@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" - integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== +dargs@^8.0.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/dargs/-/dargs-8.1.0.tgz#a34859ea509cbce45485e5aa356fef70bfcc7272" + integrity sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw== debug@4, debug@4.3.4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: version "4.3.4" @@ -2583,6 +2484,11 @@ electron-to-chromium@^1.4.530: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.531.tgz#22966d894c4680726c17cf2908ee82ff5d26ac25" integrity sha512-H6gi5E41Rn3/mhKlPaT1aIMg/71hTAqn0gYEllSuw9igNWtvQwu185jiCZoZD29n7Zukgh7GVZ3zGf0XvkhqjQ== +emoji-regex@^10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.3.0.tgz#76998b9268409eb3dae3de989254d456e70cfe23" + integrity sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -2608,7 +2514,7 @@ enhanced-resolve@^5.12.0, enhanced-resolve@^5.8.3: graceful-fs "^4.2.4" tapable "^2.2.0" -env-paths@^2.2.0: +env-paths@^2.2.0, env-paths@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== @@ -3040,56 +2946,26 @@ events@^3.3.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -execa@7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-7.2.0.tgz#657e75ba984f42a70f38928cedc87d6f2d4fe4e9" - integrity sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA== +execa@8.0.1, execa@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" + integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== dependencies: cross-spawn "^7.0.3" - get-stream "^6.0.1" - human-signals "^4.3.0" + get-stream "^8.0.1" + human-signals "^5.0.0" is-stream "^3.0.0" merge-stream "^2.0.0" npm-run-path "^5.1.0" onetime "^6.0.0" - signal-exit "^3.0.7" + signal-exit "^4.1.0" strip-final-newline "^3.0.0" -execa@^5.0.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" - integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== - dependencies: - cross-spawn "^7.0.3" - get-stream "^6.0.0" - human-signals "^2.1.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.1" - onetime "^5.1.2" - signal-exit "^3.0.3" - strip-final-newline "^2.0.0" - exponential-backoff@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== -fancy-test@^3.0.14: - version "3.0.14" - resolved "https://registry.yarnpkg.com/fancy-test/-/fancy-test-3.0.14.tgz#c5007291d7c36b07f0a8e7f235f2aa454bd66aac" - integrity sha512-FkiDltQA8PBZzw5tUnMrP1QtwIdNQWxxbZK5C22M9wu6HfFNciwkG3D9siT4l4s1fBTDaEG+Fdkbpi9FDTxtrg== - dependencies: - "@types/chai" "*" - "@types/lodash" "*" - "@types/node" "*" - "@types/sinon" "*" - lodash "^4.17.13" - mock-stdin "^1.0.0" - nock "^13.5.4" - sinon "^16.1.3" - stdout-stderr "^0.1.9" - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -3204,6 +3080,15 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +find-up@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-7.0.0.tgz#e8dec1455f74f78d888ad65bf7ca13dd2b4e66fb" + integrity sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g== + dependencies: + locate-path "^7.2.0" + path-exists "^5.0.0" + unicorn-magic "^0.1.0" + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -3255,15 +3140,6 @@ fromentries@^1.2.0: resolved "https://registry.yarnpkg.com/fromentries/-/fromentries-1.3.2.tgz#e4bca6808816bf8f93b52750f1127f5a6fd86e3a" integrity sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg== -fs-extra@^11.0.0: - version "11.1.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d" - integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - fs-minipass@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" @@ -3353,6 +3229,11 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-east-asian-width@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz#5e6ebd9baee6fb8b7b6bd505221065f0cd91f64e" + integrity sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA== + get-func-name@^2.0.0, get-func-name@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" @@ -3378,10 +3259,10 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== -get-stream@^6.0.0, get-stream@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +get-stream@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" + integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== get-symbol-description@^1.0.0: version "1.0.0" @@ -3398,16 +3279,14 @@ get-tsconfig@^4.5.0: dependencies: resolve-pkg-maps "^1.0.0" -git-raw-commits@^2.0.11: - version "2.0.11" - resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-2.0.11.tgz#bc3576638071d18655e1cc60d7f524920008d723" - integrity sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A== +git-raw-commits@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-4.0.0.tgz#b212fd2bff9726d27c1283a1157e829490593285" + integrity sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ== dependencies: - dargs "^7.0.0" - lodash "^4.17.15" - meow "^8.0.0" - split2 "^3.0.0" - through2 "^4.0.0" + dargs "^8.0.0" + meow "^12.0.1" + split2 "^4.0.0" glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" @@ -3457,12 +3336,12 @@ glob@^7.0.0, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" -global-dirs@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" - integrity sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU= +global-directory@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/global-directory/-/global-directory-4.0.1.tgz#4d7ac7cfd2cb73f304c53b8810891748df5e361e" + integrity sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q== dependencies: - ini "^1.3.4" + ini "4.1.1" globals@^11.1.0: version "11.12.0" @@ -3516,7 +3395,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.15, graceful-fs@^4.1.6, graceful-fs@^4.2.0: +graceful-fs@^4.1.15: version "4.2.6" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== @@ -3652,20 +3531,15 @@ https-proxy-agent@^7.0.1: agent-base "^7.0.2" debug "4" -human-signals@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" - integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== - -human-signals@^4.3.0: - version "4.3.1" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2" - integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ== +human-signals@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" + integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== -husky@^8: - version "8.0.3" - resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184" - integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg== +husky@^9: + version "9.0.11" + resolved "https://registry.yarnpkg.com/husky/-/husky-9.0.11.tgz#fc91df4c756050de41b3e478b2158b87c1e79af9" + integrity sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw== hyperlinker@^1.0.0: version "1.0.0" @@ -3696,7 +3570,7 @@ ignore@^5.1.1, ignore@^5.2.0, ignore@^5.2.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== -import-fresh@^3.0.0, import-fresh@^3.2.1, import-fresh@^3.3.0: +import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -3704,6 +3578,11 @@ import-fresh@^3.0.0, import-fresh@^3.2.1, import-fresh@^3.3.0: parent-module "^1.0.0" resolve-from "^4.0.0" +import-meta-resolve@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz#0b1195915689f60ab00f830af0f15cc841e8919e" + integrity sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA== + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -3737,16 +3616,16 @@ inherits@2, inherits@^2.0.3, inherits@^2.0.4: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ini@^1.3.4, ini@~1.3.0: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -ini@^4.1.0, ini@^4.1.1: +ini@4.1.1, ini@^4.1.0, ini@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.1.tgz#d95b3d843b1e906e56d6747d5447904ff50ce7a1" integrity sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g== +ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + init-package-json@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/init-package-json/-/init-package-json-6.0.0.tgz#7d4daeaacc72be300c616481e5c155d5048a18b4" @@ -3897,6 +3776,13 @@ is-fullwidth-code-point@^4.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz#fae3167c729e7463f8461ce512b080a49268aa88" integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== +is-fullwidth-code-point@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz#9609efced7c2f97da7b60145ef481c787c7ba704" + integrity sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA== + dependencies: + get-east-asian-width "^1.0.0" + is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -4005,12 +3891,12 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" -is-text-path@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-text-path/-/is-text-path-1.0.1.tgz#4e1aa0fb51bfbcb3e92688001397202c1775b66e" - integrity sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4= +is-text-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-text-path/-/is-text-path-2.0.0.tgz#b2484e2b720a633feb2e85b67dc193ff72c75636" + integrity sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw== dependencies: - text-extensions "^1.0.0" + text-extensions "^2.0.0" is-typed-array@^1.1.10, is-typed-array@^1.1.12, is-typed-array@^1.1.9: version "1.1.12" @@ -4058,11 +3944,6 @@ is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== - isarray@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" @@ -4172,6 +4053,11 @@ jest-get-type@^29.6.3: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== +jiti@^1.19.1: + version "1.21.0" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" + integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -4217,11 +4103,6 @@ json-parse-even-better-errors@^3.0.0: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz#2cb2ee33069a78870a0c7e3da560026b89669cf7" integrity sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA== -json-parse-even-better-errors@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz#02bb29fb5da90b5444581749c22cedd3597c6cb0" - integrity sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg== - json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -4242,11 +4123,6 @@ json-stringify-nice@^1.1.4: resolved "https://registry.yarnpkg.com/json-stringify-nice/-/json-stringify-nice-1.1.4.tgz#2c937962b80181d3f317dd39aa323e14f5a60a67" integrity sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw== -json-stringify-safe@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== - json5@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" @@ -4259,15 +4135,6 @@ json5@^2.2.3: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - jsonparse@^1.2.0, jsonparse@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" @@ -4283,10 +4150,10 @@ just-diff@^6.0.0: resolved "https://registry.yarnpkg.com/just-diff/-/just-diff-6.0.2.tgz#03b65908543ac0521caf6d8eb85035f7d27ea285" integrity sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA== -just-extend@^4.0.2: - version "4.2.1" - resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744" - integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg== +just-extend@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-6.2.0.tgz#b816abfb3d67ee860482e7401564672558163947" + integrity sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw== kind-of@^6.0.3: version "6.0.3" @@ -4414,43 +4281,43 @@ libnpmversion@^5.0.1: proc-log "^3.0.0" semver "^7.3.7" -lilconfig@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" - integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== +lilconfig@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.0.0.tgz#f8067feb033b5b74dab4602a5f5029420be749bc" + integrity sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g== lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= -lint-staged@^14.0.1: - version "14.0.1" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-14.0.1.tgz#57dfa3013a3d60762d9af5d9c83bdb51291a6232" - integrity sha512-Mw0cL6HXnHN1ag0mN/Dg4g6sr8uf8sn98w2Oc1ECtFto9tvRF7nkXGJRbx8gPlHyoR0pLyBr2lQHbWwmUHe1Sw== +lint-staged@^15: + version "15.2.2" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.2.tgz#ad7cbb5b3ab70e043fa05bff82a09ed286bc4c5f" + integrity sha512-TiTt93OPh1OZOsb5B7k96A/ATl2AjIZo+vnzFZ6oHK5FuTk63ByDtxGQpHm+kFETjEWqgkF95M8FRXKR/LEBcw== dependencies: chalk "5.3.0" - commander "11.0.0" + commander "11.1.0" debug "4.3.4" - execa "7.2.0" - lilconfig "2.1.0" - listr2 "6.6.1" + execa "8.0.1" + lilconfig "3.0.0" + listr2 "8.0.1" micromatch "4.0.5" pidtree "0.6.0" string-argv "0.3.2" - yaml "2.3.1" + yaml "2.3.4" -listr2@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/listr2/-/listr2-6.6.1.tgz#08b2329e7e8ba6298481464937099f4a2cd7f95d" - integrity sha512-+rAXGHh0fkEWdXBmX+L6mmfmXmXvDGEKzkjxO+8mP3+nI/r/CWznVBvsibXdxda9Zz0OW2e2ikphN3OwCT/jSg== +listr2@8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.0.1.tgz#4d3f50ae6cec3c62bdf0e94f5c2c9edebd4b9c34" + integrity sha512-ovJXBXkKGfq+CwmKTjluEqFi3p4h8xvkxGQQAQan22YCgef4KZ1mKGjzfGh6PL6AW5Csw0QiQPNuQyH+6Xk3hA== dependencies: - cli-truncate "^3.1.0" + cli-truncate "^4.0.0" colorette "^2.0.20" eventemitter3 "^5.0.1" - log-update "^5.0.1" + log-update "^6.0.0" rfdc "^1.3.0" - wrap-ansi "^8.1.0" + wrap-ansi "^9.0.0" locate-path@^5.0.0: version "5.0.0" @@ -4466,6 +4333,13 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +locate-path@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-7.2.0.tgz#69cb1779bd90b35ab1e771e1f2f89a202c2a8a8a" + integrity sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA== + dependencies: + p-locate "^6.0.0" + lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" @@ -4481,11 +4355,6 @@ lodash.get@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== -lodash.isfunction@^3.0.9: - version "3.0.9" - resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051" - integrity sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw== - lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" @@ -4526,7 +4395,7 @@ lodash.upperfirst@^4.3.1: resolved "https://registry.yarnpkg.com/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz#1365edf431480481ef0d1c68957a5ed99d49f7ce" integrity sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg== -lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.21, lodash@^4.17.4: +lodash@^4.17.21, lodash@^4.17.4: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -4539,16 +4408,16 @@ log-symbols@4.1.0, log-symbols@^4.0.0, log-symbols@^4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" -log-update@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/log-update/-/log-update-5.0.1.tgz#9e928bf70cb183c1f0c9e91d9e6b7115d597ce09" - integrity sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw== +log-update@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-6.0.0.tgz#0ddeb7ac6ad658c944c1de902993fce7c33f5e59" + integrity sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw== dependencies: - ansi-escapes "^5.0.0" + ansi-escapes "^6.2.0" cli-cursor "^4.0.0" - slice-ansi "^5.0.0" - strip-ansi "^7.0.1" - wrap-ansi "^8.0.1" + slice-ansi "^7.0.0" + strip-ansi "^7.1.0" + wrap-ansi "^9.0.0" loupe@^2.3.6: version "2.3.6" @@ -4655,22 +4524,10 @@ map-obj@^4.0.0: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.2.1.tgz#e4ea399dbc979ae735c83c863dd31bdf364277b7" integrity sha512-+WA2/1sPmDj1dlvvJmB5G6JKfY9dpn7EVBUL06+y6PoljPkh+6V1QihwxNkbcGxCRjt2b0F9K0taiCuo7MbdFQ== -meow@^8.0.0, meow@^8.1.2: - version "8.1.2" - resolved "https://registry.yarnpkg.com/meow/-/meow-8.1.2.tgz#bcbe45bda0ee1729d350c03cffc8395a36c4e897" - integrity sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q== - dependencies: - "@types/minimist" "^1.2.0" - camelcase-keys "^6.2.2" - decamelize-keys "^1.1.0" - hard-rejection "^2.1.0" - minimist-options "4.1.0" - normalize-package-data "^3.0.0" - read-pkg-up "^7.0.1" - redent "^3.0.0" - trim-newlines "^3.0.0" - type-fest "^0.18.0" - yargs-parser "^20.2.3" +meow@^12.0.1: + version "12.1.1" + resolved "https://registry.yarnpkg.com/meow/-/meow-12.1.1.tgz#e558dddbab12477b69b2e9a2728c327f191bace6" + integrity sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw== meow@^9.0.0: version "9.0.0" @@ -4775,7 +4632,7 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: +minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -4893,11 +4750,6 @@ mocha@^10.4.0: yargs-parser "20.2.4" yargs-unparser "2.0.0" -mock-stdin@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/mock-stdin/-/mock-stdin-1.0.0.tgz#efcfaf4b18077e14541742fd758b9cae4e5365ea" - integrity sha512-tukRdb9Beu27t6dN+XztSRHq9J0B/CoAOySGzHfn8UTfmqipA5yNT/sDUEyYdAV3Hpka6Wx6kOMxuObdOex60Q== - module-definition@^3.3.1: version "3.4.0" resolved "https://registry.yarnpkg.com/module-definition/-/module-definition-3.4.0.tgz#953a3861f65df5e43e80487df98bb35b70614c2b" @@ -4965,25 +4817,16 @@ negotiator@^0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== -nise@^5.1.4: - version "5.1.4" - resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.4.tgz#491ce7e7307d4ec546f5a659b2efe94a18b4bbc0" - integrity sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg== +nise@^5.1.5: + version "5.1.9" + resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.9.tgz#0cb73b5e4499d738231a473cd89bd8afbb618139" + integrity sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww== dependencies: - "@sinonjs/commons" "^2.0.0" - "@sinonjs/fake-timers" "^10.0.2" - "@sinonjs/text-encoding" "^0.7.1" - just-extend "^4.0.2" - path-to-regexp "^1.7.0" - -nock@^13.5.4: - version "13.5.4" - resolved "https://registry.yarnpkg.com/nock/-/nock-13.5.4.tgz#8918f0addc70a63736170fef7106a9721e0dc479" - integrity sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw== - dependencies: - debug "^4.1.0" - json-stringify-safe "^5.0.1" - propagate "^2.0.0" + "@sinonjs/commons" "^3.0.0" + "@sinonjs/fake-timers" "^11.2.2" + "@sinonjs/text-encoding" "^0.7.2" + just-extend "^6.2.0" + path-to-regexp "^6.2.1" node-gyp@^10.0.0, node-gyp@^10.0.1: version "10.0.1" @@ -5148,13 +4991,6 @@ npm-registry-fetch@^16.0.0, npm-registry-fetch@^16.1.0: npm-package-arg "^11.0.0" proc-log "^3.0.0" -npm-run-path@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== - dependencies: - path-key "^3.0.0" - npm-run-path@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.1.0.tgz#bc62f7f3f6952d9894bd08944ba011a6ee7b7e00" @@ -5162,15 +4998,22 @@ npm-run-path@^5.1.0: dependencies: path-key "^4.0.0" +npm-run-path@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.3.0.tgz#e23353d0ebb9317f174e93417e4a4d82d0249e9f" + integrity sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ== + dependencies: + path-key "^4.0.0" + npm-user-validate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/npm-user-validate/-/npm-user-validate-2.0.0.tgz#7b69bbbff6f7992a1d9a8968d52fd6b6db5431b6" integrity sha512-sSWeqAYJ2dUPStJB+AEj0DyLRltr/f6YNcvCA7phkB8/RMLMnVsQ41GMwHo/ERZLYNDsyB2wPm7pZo1mqPOl7Q== -npm@10.5.0: - version "10.5.0" - resolved "https://registry.yarnpkg.com/npm/-/npm-10.5.0.tgz#726f91df5b1b14d9637c8819d7e71cb873c395a1" - integrity sha512-Ejxwvfh9YnWVU2yA5FzoYLTW52vxHCz+MHrOFg9Cc8IFgF/6f5AGPAvb5WTay5DIUP1NIfN3VBZ0cLlGO0Ys+A== +npm@10.2.4: + version "10.2.4" + resolved "https://registry.yarnpkg.com/npm/-/npm-10.2.4.tgz#5ada7389d5f32b7a8e2900a9fd2f5eb5ed7e71c2" + integrity sha512-umEuYneVEYO9KoEEI8n2sSGmNQeqco/3BSeacRlqIkCzw4E7XGtYSWMeJobxzr6hZ2n9cM+u5TsMTcC5bAgoWA== dependencies: "@isaacs/string-locale-compare" "^1.1.0" "@npmcli/arborist" "^7.2.1" @@ -5178,12 +5021,12 @@ npm@10.5.0: "@npmcli/fs" "^3.1.0" "@npmcli/map-workspaces" "^3.0.4" "@npmcli/package-json" "^5.0.0" - "@npmcli/promise-spawn" "^7.0.1" - "@npmcli/run-script" "^7.0.4" - "@sigstore/tuf" "^2.3.1" + "@npmcli/promise-spawn" "^7.0.0" + "@npmcli/run-script" "^7.0.2" + "@sigstore/tuf" "^2.2.0" abbrev "^2.0.0" archy "~1.0.0" - cacache "^18.0.2" + cacache "^18.0.0" chalk "^5.3.0" ci-info "^4.0.0" cli-columns "^4.0.0" @@ -5197,7 +5040,7 @@ npm@10.5.0: ini "^4.1.1" init-package-json "^6.0.0" is-cidr "^5.0.3" - json-parse-even-better-errors "^3.0.1" + json-parse-even-better-errors "^3.0.0" libnpmaccess "^8.0.1" libnpmdiff "^6.0.3" libnpmexec "^7.0.4" @@ -5226,14 +5069,15 @@ npm@10.5.0: npm-user-validate "^2.0.0" npmlog "^7.0.1" p-map "^4.0.0" - pacote "^17.0.6" + pacote "^17.0.4" parse-conflict-json "^3.0.1" proc-log "^3.0.0" qrcode-terminal "^0.12.0" read "^2.1.0" - semver "^7.6.0" + semver "^7.5.4" spdx-expression-parse "^3.0.1" ssri "^10.0.5" + strip-ansi "^7.1.0" supports-color "^9.4.0" tar "^6.2.0" text-table "~0.2.0" @@ -5346,7 +5190,7 @@ once@^1.3.0: dependencies: wrappy "1" -onetime@^5.1.0, onetime@^5.1.2: +onetime@^5.1.0: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== @@ -5401,6 +5245,13 @@ p-limit@^3.0.2: dependencies: yocto-queue "^0.1.0" +p-limit@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-4.0.0.tgz#914af6544ed32bfa54670b061cafcbd04984b644" + integrity sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ== + dependencies: + yocto-queue "^1.0.0" + p-locate@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" @@ -5415,6 +5266,13 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" +p-locate@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-6.0.0.tgz#3da9a49d4934b901089dca3302fa65dc5a05c04f" + integrity sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw== + dependencies: + p-limit "^4.0.0" + p-map@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-3.0.0.tgz#d704d9af8a2ba684e2600d9a215983d4141a979d" @@ -5468,30 +5326,6 @@ pacote@^17.0.0, pacote@^17.0.4: ssri "^10.0.0" tar "^6.1.11" -pacote@^17.0.6: - version "17.0.6" - resolved "https://registry.yarnpkg.com/pacote/-/pacote-17.0.6.tgz#874bb59cda5d44ab784d0b6530fcb4a7d9b76a60" - integrity sha512-cJKrW21VRE8vVTRskJo78c/RCvwJCn1f4qgfxL4w77SOWrTCRcmfkYHlHtS0gqpgjv3zhXflRtgsrUCX5xwNnQ== - dependencies: - "@npmcli/git" "^5.0.0" - "@npmcli/installed-package-contents" "^2.0.1" - "@npmcli/promise-spawn" "^7.0.0" - "@npmcli/run-script" "^7.0.0" - cacache "^18.0.0" - fs-minipass "^3.0.0" - minipass "^7.0.2" - npm-package-arg "^11.0.0" - npm-packlist "^8.0.0" - npm-pick-manifest "^9.0.0" - npm-registry-fetch "^16.0.0" - proc-log "^3.0.0" - promise-retry "^2.0.1" - read-package-json "^7.0.0" - read-package-json-fast "^3.0.0" - sigstore "^2.2.0" - ssri "^10.0.0" - tar "^6.1.11" - parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -5536,12 +5370,17 @@ path-exists@^4.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== +path-exists@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-5.0.0.tgz#a6aad9489200b21fab31e49cf09277e5116fb9e7" + integrity sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ== + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= -path-key@^3.0.0, path-key@^3.1.0: +path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== @@ -5564,12 +5403,10 @@ path-scurry@^1.10.1: lru-cache "^9.1.1 || ^10.0.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" -path-to-regexp@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" - integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== - dependencies: - isarray "0.0.1" +path-to-regexp@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.1.tgz#d54934d6798eb9e5ef14e7af7962c945906918e5" + integrity sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw== path-type@^4.0.0: version "4.0.0" @@ -5770,11 +5607,6 @@ promzard@^1.0.0: dependencies: read "^2.0.0" -propagate@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" - integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== - punycode@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -5876,15 +5708,6 @@ read@^2.0.0, read@^2.1.0: dependencies: mute-stream "~1.0.0" -readable-stream@3, readable-stream@^3.0.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - readable-stream@^3.4.0: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" @@ -6000,22 +5823,15 @@ resolve-dependency-path@^2.0.0: resolved "https://registry.yarnpkg.com/resolve-dependency-path/-/resolve-dependency-path-2.0.0.tgz#11700e340717b865d216c66cabeb4a2a3c696736" integrity sha512-DIgu+0Dv+6v2XwRaNWnumKu7GPufBBOr5I1gRPJHkvghrfCGOooJODFvgFimX/KRxk9j0whD2MnKHzM1jYvk9w== -resolve-from@5.0.0, resolve-from@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" - integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve-global@1.0.0, resolve-global@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/resolve-global/-/resolve-global-1.0.0.tgz#a2a79df4af2ca3f49bf77ef9ddacd322dad19255" - integrity sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw== - dependencies: - global-dirs "^0.1.1" +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== resolve-pkg-maps@^1.0.0: version "1.0.0" @@ -6134,13 +5950,6 @@ sass-lookup@^3.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== -semver@7.5.4: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== - dependencies: - lru-cache "^6.0.0" - semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" @@ -6212,12 +6021,12 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" -signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: +signal-exit@^3.0.2: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== -signal-exit@^4.0.1: +signal-exit@^4.0.1, signal-exit@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== @@ -6251,16 +6060,16 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" -sinon@^16.1.3: - version "16.1.3" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-16.1.3.tgz#b760ddafe785356e2847502657b4a0da5501fba8" - integrity sha512-mjnWWeyxcAf9nC0bXcPmiDut+oE8HYridTNzBbF98AYVLmWwGRp2ISEpyhYflG1ifILT+eNn3BmKUJPxjXUPlA== +sinon@^17: + version "17.0.1" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-17.0.1.tgz#26b8ef719261bf8df43f925924cccc96748e407a" + integrity sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g== dependencies: "@sinonjs/commons" "^3.0.0" - "@sinonjs/fake-timers" "^10.3.0" + "@sinonjs/fake-timers" "^11.2.2" "@sinonjs/samsam" "^8.0.0" diff "^5.1.0" - nise "^5.1.4" + nise "^5.1.5" supports-color "^7.2.0" slash@^3.0.0: @@ -6285,6 +6094,14 @@ slice-ansi@^5.0.0: ansi-styles "^6.0.0" is-fullwidth-code-point "^4.0.0" +slice-ansi@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-7.1.0.tgz#cd6b4655e298a8d1bdeb04250a433094b347b9a9" + integrity sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg== + dependencies: + ansi-styles "^6.2.1" + is-fullwidth-code-point "^5.0.0" + smart-buffer@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" @@ -6355,12 +6172,10 @@ spdx-license-ids@^3.0.0: resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz#8a595135def9592bda69709474f1cbeea7c2467f" integrity sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ== -split2@^3.0.0, split2@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" - integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== - dependencies: - readable-stream "^3.0.0" +split2@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== sprintf-js@~1.0.2: version "1.0.3" @@ -6374,14 +6189,6 @@ ssri@^10.0.0, ssri@^10.0.5: dependencies: minipass "^7.0.3" -stdout-stderr@^0.1.9: - version "0.1.13" - resolved "https://registry.yarnpkg.com/stdout-stderr/-/stdout-stderr-0.1.13.tgz#54e3450f3d4c54086a49c0c7f8786a44d1844b6f" - integrity sha512-Xnt9/HHHYfjZ7NeQLvuQDyL1LnbsbddgMFKCuaQKwGCdJm8LnstZIXop+uOY36UR1UXXoHXfMbC1KlVdVd2JLA== - dependencies: - debug "^4.1.1" - strip-ansi "^6.0.0" - stream-to-array@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/stream-to-array/-/stream-to-array-2.3.0.tgz#bbf6b39f5f43ec30bc71babcb37557acecf34353" @@ -6403,7 +6210,7 @@ string-argv@0.3.2: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: +string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== @@ -6412,6 +6219,15 @@ string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +string-width@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.1.0.tgz#d994252935224729ea3719c49f7206dc9c46550a" + integrity sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw== + dependencies: + emoji-regex "^10.3.0" + get-east-asian-width "^1.0.0" + strip-ansi "^7.1.0" + string.prototype.trim@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz#f9ac6f8af4bd55ddfa8895e6aea92a96395393bd" @@ -6462,7 +6278,7 @@ stringify-object@^3.2.1: dependencies: ansi-regex "^5.0.1" -strip-ansi@*, strip-ansi@^7.0.1: +strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== @@ -6479,11 +6295,6 @@ strip-bom@^4.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - strip-final-newline@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" @@ -6591,23 +6402,16 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" -text-extensions@^1.0.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26" - integrity sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ== +text-extensions@^2.0.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-2.4.0.tgz#a1cfcc50cf34da41bfd047cc744f804d1680ea34" + integrity sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g== text-table@^0.2.0, text-table@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== -through2@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/through2/-/through2-4.0.2.tgz#a7ce3ac2a7a8b0b966c80e7c49f0484c3b239764" - integrity sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw== - dependencies: - readable-stream "3" - "through@>=2.2.7 <3": version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -6650,7 +6454,7 @@ ts-graphviz@^1.5.0: resolved "https://registry.yarnpkg.com/ts-graphviz/-/ts-graphviz-1.8.1.tgz#5d95e58120be8b571847331516327d4840cc44f7" integrity sha512-54/fe5iu0Jb6X0pmDmzsA2UHLfyHjUEUwfHtZcEOR0fZ6Myf+dFoO6eNsyL8CBDMJ9u7WWEewduVaiaXlvjSVw== -ts-node@^10.8.1, ts-node@^10.9.2: +ts-node@^10.9.2: version "10.9.2" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== @@ -6769,11 +6573,6 @@ type-fest@^0.8.0, type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-fest@^1.0.2: - version "1.4.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" - integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== - typed-array-buffer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz#18de3e7ed7974b0a729d3feecb94338d1472cd60" @@ -6830,10 +6629,10 @@ typescript@^4.0.0, typescript@^4.9.5: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== -"typescript@^4.6.4 || ^5.2.2", typescript@^5: - version "5.4.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" - integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== +typescript@^5: + version "5.4.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.3.tgz#5c6fedd4c87bee01cd7a528a30145521f8e0feff" + integrity sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg== unbox-primitive@^1.0.2: version "1.0.2" @@ -6855,6 +6654,11 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +unicorn-magic@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/unicorn-magic/-/unicorn-magic-0.1.0.tgz#1bb9a51c823aaf9d73a8bfcd3d1a23dde94b0ce4" + integrity sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ== + uniq@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" @@ -6874,11 +6678,6 @@ unique-slug@^4.0.0: dependencies: imurmurhash "^0.1.4" -universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== - update-browserslist-db@^1.0.13: version "1.0.13" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" @@ -7024,7 +6823,7 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: +wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== @@ -7033,6 +6832,15 @@ wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: string-width "^5.0.1" strip-ansi "^7.0.1" +wrap-ansi@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-9.0.0.tgz#1a3dc8b70d85eeb8398ddfb1e4a02cd186e58b3e" + integrity sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q== + dependencies: + ansi-styles "^6.2.1" + string-width "^7.0.0" + strip-ansi "^7.1.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -7076,10 +6884,10 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b" - integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ== +yaml@2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" + integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== yargs-parser@20.2.4: version "20.2.4" @@ -7157,11 +6965,6 @@ yargs@^17.0.0: y18n "^5.0.5" yargs-parser "^21.1.1" -yarn@^1.22.21: - version "1.22.21" - resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.21.tgz#1959a18351b811cdeedbd484a8f86c3cc3bbaf72" - integrity sha512-ynXaJsADJ9JiZ84zU25XkPGOvVMmZ5b7tmTSpKURYwgELdjucAOydqIOrOfTxVYcNXe91xvLZwcRh68SR3liCg== - yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" @@ -7171,3 +6974,8 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +yocto-queue@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" + integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==