From 36d7506db725ae3e7f216cccd1daa7141fa27ec6 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Tue, 16 Apr 2024 09:39:26 -0600 Subject: [PATCH 01/28] feat: copy oclif/core ux methods --- package.json | 10 +- src/ux/styledJSON.ts | 22 ++ src/ux/styledObject.ts | 54 +++++ src/ux/table.ts | 443 ++++++++++++++++++++++++++++++++++++ src/ux/ux.ts | 32 ++- src/ux/write.ts | 28 +++ test/unit/ux/object.test.ts | 22 ++ test/unit/ux/table.test.ts | 303 ++++++++++++++++++++++++ test/unit/ux/ux.test.ts | 30 +-- yarn.lock | 140 ++++++++++-- 10 files changed, 1038 insertions(+), 46 deletions(-) create mode 100644 src/ux/styledJSON.ts create mode 100644 src/ux/styledObject.ts create mode 100644 src/ux/table.ts create mode 100644 src/ux/write.ts create mode 100644 test/unit/ux/object.test.ts create mode 100644 test/unit/ux/table.test.ts diff --git a/package.json b/package.json index 0852fb290..85e856547 100644 --- a/package.json +++ b/package.json @@ -49,11 +49,19 @@ "@salesforce/core": "^7.2.0", "@salesforce/kit": "^3.1.0", "@salesforce/ts-types": "^2.0.9", - "chalk": "^5.3.0" + "cardinal": "^2.1.1", + "chalk": "^5.3.0", + "js-yaml": "^4.1.0", + "natural-orderby": "^3.0.2", + "slice-ansi": "^7.1.0", + "string-width": "^7.1.0", + "terminal-link": "^3.0.0" }, "devDependencies": { "@inquirer/type": "^1.2.1", "@salesforce/dev-scripts": "^8.5.0", + "@types/cardinal": "^2.1.1", + "@types/js-yaml": "^4.0.9", "eslint-plugin-sf-plugin": "^1.18.0", "ts-node": "^10.9.2", "typescript": "^5.4.5" diff --git a/src/ux/styledJSON.ts b/src/ux/styledJSON.ts new file mode 100644 index 000000000..aae194512 --- /dev/null +++ b/src/ux/styledJSON.ts @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { createRequire } from 'node:module'; +import { highlight, CardinalTheme } from 'cardinal'; +import chalk from 'chalk'; + +// @oclif/core v4 will have native support for coloring JSON so we won't need this then. +export default function styledJSON(obj: unknown): string { + const json = JSON.stringify(obj, null, 2); + if (!chalk.level) { + return json; + } + + const require = createRequire(import.meta.url); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const theme = require('cardinal/themes/jq') as CardinalTheme; + return highlight(json, { theme }); +} diff --git a/src/ux/styledObject.ts b/src/ux/styledObject.ts new file mode 100644 index 000000000..f4e2873d3 --- /dev/null +++ b/src/ux/styledObject.ts @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { inspect } from 'node:util'; +import chalk from 'chalk'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +function pp(obj: any): any { + if (typeof obj === 'string' || typeof obj === 'number') return obj; + if (typeof obj === 'object') { + return ( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + Object.keys(obj) + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + .map((k) => k + ': ' + inspect(obj[k])) + .join(', ') + ); + } + + return inspect(obj); +} + +// @oclif/core v4 will have native support for coloring JSON so we won't need this then. +export default function styledObject(obj: any, keys?: string[]): string { + const output: string[] = []; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const keyLengths = Object.keys(obj).map((key) => key.toString().length); + const maxKeyLength = Math.max(...keyLengths) + 2; + + const logKeyValue = (key: string, value: any): string => + `${chalk.blue(key)}:` + ' '.repeat(maxKeyLength - key.length - 1) + pp(value); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + for (const key of keys ?? Object.keys(obj).sort()) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + 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/ux/table.ts b/src/ux/table.ts new file mode 100644 index 000000000..322bb94e4 --- /dev/null +++ b/src/ux/table.ts @@ -0,0 +1,443 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { inspect } from 'node:util'; +import chalk from 'chalk'; +import jsyaml from 'js-yaml'; +import { orderBy } from 'natural-orderby'; +import sliceAnsi from 'slice-ansi'; +import sw from 'string-width'; +import { Interfaces, Flags as F } from '@oclif/core'; + +import write from './write.js'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +function sumBy(arr: T[], fn: (i: T) => number): number { + return arr.reduce((sum, i) => sum + fn(i), 0); +} + +function capitalize(s: string): string { + return s ? s.charAt(0).toUpperCase() + s.slice(1).toLowerCase() : ''; +} + +function termwidth(stream: NodeJS.WriteStream): number { + if (!stream.isTTY) { + return 80; + } + + const width = stream.getWindowSize()[0]; + if (width < 1) { + return 80; + } + + if (width < 40) { + return 40; + } + + return width; +} + +const stdtermwidth = Number.parseInt(process.env.OCLIF_COLUMNS!, 10) || termwidth(process.stdout); + +class Table> { + private columns: Array & { key: string; maxWidth?: number; width?: number }>; + + private options: table.Options & { printLine(s: any): any }; + + public 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 + // eslint-disable-next-line @typescript-eslint/unbound-method + 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: string): void => write.stdout(s)), + rowStart: ' ', + sort, + title, + }; + } + + public 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) { + // eslint-disable-next-line prefer-const + let [header, regex] = this.options.filter.split('='); + const isNot = header.startsWith('-'); + 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] as string; + 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.startsWith('-') ? k.slice(1) : k)); + const sortKeys = this.filterColumnsFromHeaders(sortHeaders).map((c) => (v: any) => v[c.key]); + const sortKeysOrder = sorters.map((k) => (k.startsWith('-') ? '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[] + ): Array & { key: string; maxWidth?: number; width?: number }> { + // unique + filters = [...new Set(filters)]; + const cols: Array & { key: string; maxWidth?: number; width?: number }> = []; + for (const f of filters) { + const c = this.columns.find((i) => i.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] || '') as string[]; + 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] as string; + 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] as string; + 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(): void { + this.options.printLine(jsyaml.dump(this.resolveColumnsToObjectArray())); + } + + private resolveColumnsToObjectArray(): Array<{ [k: string]: any }> { + 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 type Column> = { + extended: boolean; + header: string; + minWidth: number; + get(row: T): unknown; + }; + + export type Columns> = { [key: string]: Partial> }; + + // export type OutputType = 'csv' | 'json' | 'yaml' + + export type Options = { + [key: string]: any; + columns?: string; + extended?: boolean; + filter?: string; + 'no-header'?: boolean; + 'no-truncate'?: boolean; + output?: string; + sort?: string; + title?: string; + printLine?(s: any): any; + }; +} + +const getWidestColumnWith = (data: any[], columnKey: string): number => + data.reduce((previous, current) => { + const d = current[columnKey]; + // convert multi-line cell to single longest line + // for width calculations + const manyLines = (d as string).split('\n'); + return Math.max(previous, manyLines.length > 1 ? Math.max(...manyLines.map((r: string) => sw(r))) : sw(d)); + }, 0); diff --git a/src/ux/ux.ts b/src/ux/ux.ts index bfaa9ac18..3fc016d0c 100644 --- a/src/ux/ux.ts +++ b/src/ux/ux.ts @@ -5,10 +5,16 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ +import chalk from 'chalk'; import { ux } from '@oclif/core'; import { AnyJson } from '@salesforce/ts-types'; +import terminalLink from 'terminal-link'; import { UxBase } from './base.js'; import { Spinner } from './spinner.js'; +import { table } from './table.js'; +import styledJSON from './styledJSON.js'; +import styledObject from './styledObject.js'; +import write from './write.js'; /** * UX methods for plugins. Automatically suppress console output if outputEnabled is set to false. @@ -43,7 +49,17 @@ export class Ux extends UxBase { * @param args Args to be used for formatting. */ public log(message?: string, ...args: string[]): void { - this.maybeNoop(() => ux.log(message, ...args)); + this.maybeNoop(() => write.stdout(message, ...args)); + } + + /** + * Log a message to stderr. This will be automatically suppressed if output is disabled. + * + * @param message Message to log. Formatting is supported. + * @param args Args to be used for formatting. + */ + public logToStderr(message?: string, ...args: string[]): void { + this.maybeNoop(() => write.stderr(message, ...args)); } /** @@ -63,7 +79,7 @@ export class Ux extends UxBase { * @param options Options for how the table should be displayed */ public table(data: T[], columns: Ux.Table.Columns, options?: Ux.Table.Options): void { - this.maybeNoop(() => ux.table(data, columns, { 'no-truncate': true, ...options })); + this.maybeNoop(() => table(data, columns, { 'no-truncate': true, ...options })); } /** @@ -74,7 +90,7 @@ export class Ux extends UxBase { * @param params */ public url(text: string, uri: string, params = {}): void { - this.maybeNoop(() => ux.url(text, uri, params)); + this.maybeNoop(() => write.stdout(terminalLink(text, uri, { fallback: () => uri, ...params }))); } /** @@ -83,7 +99,7 @@ export class Ux extends UxBase { * @param obj JSON to display */ public styledJSON(obj: AnyJson): void { - this.maybeNoop(() => ux.styledJSON(obj)); + this.maybeNoop(() => write.stdout(styledJSON(obj))); } /** @@ -93,7 +109,7 @@ export class Ux extends UxBase { * @param keys Keys of object to display */ public styledObject(obj: AnyJson, keys?: string[]): void { - this.maybeNoop(() => ux.styledObject(obj, keys)); + this.maybeNoop(() => write.stdout(styledObject(obj, keys))); } /** @@ -102,14 +118,14 @@ export class Ux extends UxBase { * @param text header to display */ public styledHeader(text: string): void { - this.maybeNoop(() => ux.styledHeader(text)); + this.maybeNoop(() => write.stdout(chalk.dim('=== ') + chalk.bold(text) + '\n')); } } export namespace Ux { export namespace Table { export type Data = Record; - export type Columns = ux.Table.table.Columns; - export type Options = ux.Table.table.Options; + export type Columns = table.Columns; + export type Options = table.Options; } } diff --git a/src/ux/write.ts b/src/ux/write.ts new file mode 100644 index 000000000..eda9fe001 --- /dev/null +++ b/src/ux/write.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { format } from 'node:util'; + +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'); + } +}; + +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'); + } +}; + +export default { + stdout, + stderr, +}; diff --git a/test/unit/ux/object.test.ts b/test/unit/ux/object.test.ts new file mode 100644 index 000000000..7902e323c --- /dev/null +++ b/test/unit/ux/object.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { expect } from 'chai'; +import stripAnsi from 'strip-ansi'; +import styledObject from '../../../src/ux/styledObject.js'; + +describe('styledObject', () => { + it('should show a table', () => { + const result = styledObject([ + { foo: 1, bar: 1 }, + { foo: 2, bar: 2 }, + { foo: 3, bar: 3 }, + ]); + expect(stripAnsi(result)).to.equal(`0: foo: 1, bar: 1 +1: foo: 2, bar: 2 +2: foo: 3, bar: 3`); + }); +}); diff --git a/test/unit/ux/table.test.ts b/test/unit/ux/table.test.ts new file mode 100644 index 000000000..aaced1abf --- /dev/null +++ b/test/unit/ux/table.test.ts @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { expect } from 'chai'; +import stripAnsi from 'strip-ansi'; +import { table } from '../../../src/ux/table.js'; + +/* 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 }, + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access + stack: { extended: true, get: (r: any) => r.stack?.name }, +}; + +const ws = ' '; +const extendedHeader = `ID Name${ws.padEnd(14)}Web url${ws.padEnd(34)}Stack${ws.padEnd(5)}`; + +describe('table', () => { + let output = ''; + function printLine(line: string) { + output += stripAnsi(line) + '\n'; + } + + afterEach(() => { + output = ''; + }); + + it('should display a table', () => { + table(apps, columns, { printLine }); + expect(output).to.equal(` ID Name${ws.padEnd(14)} + ─── ─────────────────${ws} + 123 supertable-test-1${ws} + 321 supertable-test-2${ws}\n`); + }); + + describe('columns', () => { + it('should user header value for id', () => { + table(apps, columns, { printLine }); + expect(output.slice(1, 3)).to.equal('ID'); + }); + + it('should show extended columns and use get() for value', () => { + table(apps, columns, { printLine, extended: true }); + expect(output).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`); + }); + }); + + it('should omit nulls and undefined by default', () => { + const data = [{ a: 1, b: '2', c: null, d: undefined }]; + table(data, { a: {}, b: {}, c: {}, d: {} }, { printLine }); + expect(output).to.include('1'); + expect(output).to.include('2'); + expect(output).to.not.include('null'); + expect(output).to.not.include('undefined'); + }); + + describe('options', () => { + it('should show extended columns', () => { + table(apps, columns, { printLine, extended: true }); + expect(output).to.contain(extendedHeader); + }); + + it('should show title with divider', () => { + table(apps, columns, { printLine, title: 'testing' }); + expect(output).to.equal(`testing +======================= +| ID Name${ws.padEnd(14)} +| ─── ─────────────────${ws} +| 123 supertable-test-1${ws} +| 321 supertable-test-2${ws}\n`); + }); + + it('should skip header', () => { + table(apps, columns, { printLine, 'no-header': true }); + expect(output).to.equal(` 123 supertable-test-1${ws} + 321 supertable-test-2${ws}\n`); + }); + + it('should only display given columns', () => { + table(apps, columns, { printLine, columns: 'id' }); + expect(output).to.equal(` ID${ws}${ws} + ───${ws} + 123${ws} + 321${ws}\n`); + }); + + it('should output in csv', () => { + table(apps, columns, { printLine, output: 'csv' }); + expect(output).to.equal(`ID,Name +123,supertable-test-1 +321,supertable-test-2\n`); + }); + + it('should output in csv with escaped values', () => { + 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', printLine } + ); + expect(output).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`); + }); + + it('should output in csv without headers', () => { + table(apps, columns, { printLine, output: 'csv', 'no-header': true }); + expect(output).to.equal(`123,supertable-test-1 +321,supertable-test-2\n`); + }); + + it('should output in csv with alias flag', () => { + table(apps, columns, { printLine, csv: true }); + expect(output).to.equal(`ID,Name +123,supertable-test-1 +321,supertable-test-2\n`); + }); + + it('should output in json', () => { + table(apps, columns, { printLine, output: 'json' }); + expect(output).to.equal(`[ + { + "id": "123", + "name": "supertable-test-1" + }, + { + "id": "321", + "name": "supertable-test-2" + } +] +`); + }); + + it('should output in yaml', () => { + table(apps, columns, { printLine, output: 'yaml' }); + expect(output).to.equal(`- id: '123' + name: supertable-test-1 +- id: '321' + name: supertable-test-2 + +`); + }); + + it('should sort by property', () => { + table(apps, columns, { printLine, sort: '-name' }); + expect(output).to.equal(` ID Name${ws.padEnd(14)} + ─── ─────────────────${ws} + 321 supertable-test-2${ws} + 123 supertable-test-1${ws}\n`); + }); + + it('should filter by property and value (partial string match)', () => { + table(apps, columns, { printLine, filter: 'id=123' }); + expect(output).to.equal(` ID Name${ws.padEnd(14)} + ─── ─────────────────${ws} + 123 supertable-test-1${ws}\n`); + }); + + // This test doesn't work because tests have process.stdout.isTTY=undefined when wireit runs the tests + // `yarn mocha test` works fine since process.stdout.isTTY=true + it.skip('should not truncate', () => { + const three = { ...apps[0], id: '0'.repeat(80), name: 'supertable-test-3' }; + table([...apps, three], columns, { filter: 'id=0', printLine, truncate: false }); + expect(output).to.equal(` ID${ws.padEnd(78)} Name${ws.padEnd(14)} + ${''.padEnd(three.id.length, '─')} ─────────────────${ws} + ${three.id} supertable-test-3${ws}\n`); + }); + }); + + describe('edge cases', () => { + it('ignores header case', () => { + table(apps, columns, { columns: 'iD,Name', filter: 'nAMe=supertable-test', sort: '-ID', printLine }); + expect(output).to.equal(` ID Name${ws.padEnd(14)} + ─── ─────────────────${ws} + 321 supertable-test-2${ws} + 123 supertable-test-1${ws}\n`); + }); + + it('displays multiline cell', () => { + const app3 = { + build_stack: { + name: 'heroku-16', + }, + id: '456', + name: 'supertable-test\n3', + web_url: 'https://supertable-test-1.herokuapp.com/', + }; + + table([...apps, app3], columns, { sort: '-ID', printLine }); + expect(output).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`); + }); + + it('does not exceed stack depth on very tall tables', () => { + const data = Array.from({ length: 150_000 }).fill({ id: '123', name: 'foo', value: 'bar' }) as Array< + Record + >; + const tallColumns = { + id: { header: 'ID' }, + name: {}, + value: { header: 'TEST' }, + }; + + table(data, tallColumns, { printLine }); + expect(output).to.include('ID'); + }); + + // Skip because it takes too long + it.skip('does not exceed stack depth on very tall, wide tables', () => { + const columnsLength = 100; + const row = Object.fromEntries(Array.from({ length: columnsLength }).map((_, i) => [`col${i}`, 'foo'])); + const data = Array.from({ length: 150_000 }).fill(row) as Array>; + const bigColumns = Object.fromEntries( + Array.from({ length: columnsLength }).map((_, i) => [`col${i}`, { header: `col${i}`.toUpperCase() }]) + ); + + table(data, bigColumns, { printLine }); + expect(output).to.include('COL1'); + }); + }); +}); diff --git a/test/unit/ux/ux.test.ts b/test/unit/ux/ux.test.ts index ff70c3cff..953aec889 100644 --- a/test/unit/ux/ux.test.ts +++ b/test/unit/ux/ux.test.ts @@ -7,16 +7,16 @@ import { expect } from 'chai'; import sinon from 'sinon'; -import { ux as coreUx } from '@oclif/core'; import { Ux } from '../../../src/ux/ux.js'; +import write from '../../../src/ux/write.js'; describe('Ux', () => { let sandbox: sinon.SinonSandbox; - let infoStub: sinon.SinonStub; + let stdoutStub: sinon.SinonStub; beforeEach(() => { sandbox = sinon.createSandbox(); - infoStub = sandbox.stub(coreUx, 'info').callsFake(() => {}); + stdoutStub = sandbox.stub(write, 'stdout').callsFake(() => {}); }); afterEach(() => { @@ -26,8 +26,8 @@ describe('Ux', () => { describe('table', () => { it('should log a table', () => { const ux = new Ux(); - ux.table([{ key: 'foo', value: 'bar' }], { key: {}, value: {} }, { printLine: coreUx.info }); - expect(infoStub.args).to.deep.equal([ + ux.table([{ key: 'foo', value: 'bar' }], { key: {}, value: {} }); + expect(stdoutStub.args).to.deep.equal([ ['\u001b[1m Key Value \u001b[22m'], ['\u001b[1m ─── ───── \u001b[22m'], [' foo bar '], @@ -37,7 +37,7 @@ describe('Ux', () => { it('should not log anything if output is not enabled', () => { const ux = new Ux({ jsonEnabled: true }); ux.table([{ key: 'foo', value: 'bar' }], { key: {}, value: {} }); - expect(infoStub.callCount).to.equal(0); + expect(stdoutStub.callCount).to.equal(0); }); }); @@ -45,13 +45,13 @@ describe('Ux', () => { it('should log a url', () => { const ux = new Ux(); ux.url('Salesforce', 'https://developer.salesforce.com/'); - expect(infoStub.firstCall.args).to.deep.equal(['https://developer.salesforce.com/']); + expect(stdoutStub.firstCall.args).to.deep.equal(['https://developer.salesforce.com/']); }); it('should not log anything if output is not enabled', () => { const ux = new Ux({ jsonEnabled: true }); ux.url('Salesforce', 'https://developer.salesforce.com/'); - expect(infoStub.callCount).to.equal(0); + expect(stdoutStub.callCount).to.equal(0); }); }); @@ -59,7 +59,7 @@ describe('Ux', () => { it('should log stylized json', () => { const ux = new Ux(); ux.styledJSON({ foo: 'bar' }); - expect(infoStub.firstCall.args).to.deep.equal([ + expect(stdoutStub.firstCall.args).to.deep.equal([ '\x1B[97m{\x1B[39m\n \x1B[94m"foo"\x1B[39m\x1B[93m:\x1B[39m \x1B[92m"bar"\x1B[39m\n\x1B[97m}\x1B[39m', ]); }); @@ -67,7 +67,7 @@ describe('Ux', () => { it('should not log anything if output is not enabled', () => { const ux = new Ux({ jsonEnabled: true }); ux.styledJSON({ foo: 'bar' }); - expect(infoStub.callCount).to.equal(0); + expect(stdoutStub.callCount).to.equal(0); }); }); @@ -75,13 +75,13 @@ describe('Ux', () => { it('should log stylized object', () => { const ux = new Ux(); ux.styledObject({ foo: 'bar' }); - expect(infoStub.firstCall.args).to.deep.equal(['\u001b[34mfoo\u001b[39m: bar']); + expect(stdoutStub.firstCall.args).to.deep.equal(['\u001b[34mfoo\u001b[39m: bar']); }); it('should not log anything if output is not enabled', () => { const ux = new Ux({ jsonEnabled: true }); ux.styledObject({ foo: 'bar' }); - expect(infoStub.callCount).to.equal(0); + expect(stdoutStub.callCount).to.equal(0); }); }); @@ -89,13 +89,15 @@ describe('Ux', () => { it('should log stylized header', () => { const ux = new Ux(); ux.styledHeader('A Stylized Header'); - expect(infoStub.firstCall.args).to.deep.equal(['\u001b[2m=== \u001b[22m\u001b[1mA Stylized Header\u001b[22m\n']); + expect(stdoutStub.firstCall.args).to.deep.equal([ + '\u001b[2m=== \u001b[22m\u001b[1mA Stylized Header\u001b[22m\n', + ]); }); it('should not log anything if output is not enabled', () => { const ux = new Ux({ jsonEnabled: true }); ux.styledHeader('A Stylized Header'); - expect(infoStub.callCount).to.equal(0); + expect(stdoutStub.callCount).to.equal(0); }); }); }); diff --git a/yarn.lock b/yarn.lock index 10e82f9a6..8fec4000a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -585,9 +585,9 @@ fastq "^1.6.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== + version "3.26.3" + resolved "https://registry.yarnpkg.com/@oclif/core/-/core-3.26.3.tgz#2416f4f5e3e5b9434999edb2f94983c5ac07e8a2" + integrity sha512-e6Vwu+cb2Sn4qFFpmY1fQLRWIY5ugruMuN94xb7+kyUzxrirYjJATPhuCT1G5xj9Dk+hTMH+Sp6XcHcVTS1lHg== dependencies: "@types/cli-progress" "^3.11.5" ansi-escapes "^4.3.2" @@ -598,14 +598,14 @@ cli-progress "^3.12.0" color "^4.2.3" debug "^4.3.4" - ejs "^3.1.9" + 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.3" + minimatch "^9.0.4" natural-orderby "^2.0.3" object-treeify "^1.1.33" password-prompt "^1.1.3" @@ -761,6 +761,11 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== +"@types/cardinal@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@types/cardinal/-/cardinal-2.1.1.tgz#dce628abdc123d2e03f4fdfc8bca688cb9590986" + integrity sha512-/xCVwg8lWvahHsV2wXZt4i64H1sdL+sN1Uoq7fAc8/FA6uYHjuIveDwPwvGUYp4VZiv85dVl6J/Bum3NDAOm8g== + "@types/chai@^4.3.14": version "4.3.14" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.14.tgz#ae3055ea2be43c91c9fd700a36d67820026d96e6" @@ -773,6 +778,11 @@ dependencies: "@types/node" "*" +"@types/js-yaml@^4.0.9": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2" + integrity sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg== + "@types/json-schema@^7.0.12": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" @@ -800,10 +810,10 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@^20.10.7": - version "20.11.16" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.16.tgz#4411f79411514eb8e2926f036c86c9f0e4ec6708" - integrity sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ== +"@types/node@*": + version "20.12.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.7.tgz#04080362fa3dd6c5822061aa3124f5c152cff384" + integrity sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg== dependencies: undici-types "~5.26.4" @@ -814,6 +824,13 @@ dependencies: undici-types "~5.26.4" +"@types/node@^20.10.7": + version "20.11.16" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.16.tgz#4411f79411514eb8e2926f036c86c9f0e4ec6708" + integrity sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ== + dependencies: + undici-types "~5.26.4" + "@types/normalize-package-data@^2.4.0": version "2.4.1" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" @@ -1016,6 +1033,13 @@ 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-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -1045,7 +1069,7 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0, ansi-styles@^4.3.0: dependencies: color-convert "^2.0.1" -ansi-styles@^6.1.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== @@ -1191,9 +1215,9 @@ astral-regex@^2.0.0: integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== async@^3.2.3: - version "3.2.4" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" - integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + version "3.2.5" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" + integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== asynckit@^0.4.0: version "0.4.0" @@ -1851,10 +1875,10 @@ ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer "^5.0.1" -ejs@^3.1.9: - version "3.1.9" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361" - integrity sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ== +ejs@^3.1.10: + version "3.1.10" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" + integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== dependencies: jake "^10.8.5" @@ -1863,6 +1887,11 @@ electron-to-chromium@^1.4.668: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.715.tgz#bb16bcf2a3537962fccfa746b5c98c5f7404ff46" integrity sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg== +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" @@ -2257,7 +2286,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.11, fast-glob@^3.2.9: +fast-glob@^3.2.11: version "3.3.1" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== @@ -2268,6 +2297,17 @@ fast-glob@^3.2.11, fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" +fast-glob@^3.2.9: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -2289,9 +2329,9 @@ fast-safe-stringify@^2.1.1: integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== fastq@^1.6.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" - integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== dependencies: reusify "^1.0.4" @@ -2490,6 +2530,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.1, 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" @@ -2943,6 +2988,13 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +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" @@ -3630,7 +3682,7 @@ minimatch@5.0.1: dependencies: brace-expansion "^2.0.1" -minimatch@9.0.3, minimatch@^9.0.1, minimatch@^9.0.3: +minimatch@9.0.3, minimatch@^9.0.1: version "9.0.3" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== @@ -3651,6 +3703,13 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" +minimatch@^9.0.3, minimatch@^9.0.4: + version "9.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" + integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== + dependencies: + brace-expansion "^2.0.1" + minimist-options@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" @@ -3734,6 +3793,11 @@ natural-orderby@^2.0.3: resolved "https://registry.yarnpkg.com/natural-orderby/-/natural-orderby-2.0.3.tgz#8623bc518ba162f8ff1cdb8941d74deb0fdcc016" integrity sha512-p7KTHxU0CUrcOXe62Zfrb5Z13nLvPhSWR/so3kFulUQU0sgUll2Z0LwpsLN351eOOD+hRGu/F1g+6xDfPeD++Q== +natural-orderby@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/natural-orderby/-/natural-orderby-3.0.2.tgz#1b874d685fbd68beab2c6e7d14f298e03d631ec3" + integrity sha512-x7ZdOwBxZCEm9MM7+eQCjkrNLrW3rkBKNHVr78zbtqnMGVNlnDi6C/eUEYgxHNrcbu0ymvjzcwIL/6H1iHri9g== + nise@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/nise/-/nise-4.1.0.tgz#8fb75a26e90b99202fa1e63f448f58efbcdedaf6" @@ -4646,6 +4710,14 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" +slice-ansi@^7.1.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" + snake-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" @@ -4760,6 +4832,15 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +string-width@^7.1.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" @@ -4808,7 +4889,7 @@ string_decoder@~1.1.1: dependencies: ansi-regex "^5.0.1" -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== @@ -4876,6 +4957,14 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +terminal-link@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-3.0.0.tgz#91c82a66b52fc1684123297ce384429faf72ac5c" + integrity sha512-flFL3m4wuixmf6IfhFJd1YPiLiMuxEc8uHRM1buzIeZPm22Au2pDqBJQgdo7n1WfPU1ONFGv7YDwpFBmHGF6lg== + dependencies: + ansi-escapes "^5.0.0" + supports-hyperlinks "^2.2.0" + test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" @@ -5034,6 +5123,11 @@ 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" From 4fa9d7ed1cc8ce3e0b5af3f6e6dfcf7edec7ff1d Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Tue, 16 Apr 2024 09:45:59 -0600 Subject: [PATCH 02/28] feat: use cli-progress instead of ux.progress --- package.json | 2 ++ src/ux/progress.ts | 12 ++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 85e856547..063bef702 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@salesforce/ts-types": "^2.0.9", "cardinal": "^2.1.1", "chalk": "^5.3.0", + "cli-progress": "^3.12.0", "js-yaml": "^4.1.0", "natural-orderby": "^3.0.2", "slice-ansi": "^7.1.0", @@ -61,6 +62,7 @@ "@inquirer/type": "^1.2.1", "@salesforce/dev-scripts": "^8.5.0", "@types/cardinal": "^2.1.1", + "@types/cli-progress": "^3.11.5", "@types/js-yaml": "^4.0.9", "eslint-plugin-sf-plugin": "^1.18.0", "ts-node": "^10.9.2", diff --git a/src/ux/progress.ts b/src/ux/progress.ts index c575f3cad..b7170a4ad 100644 --- a/src/ux/progress.ts +++ b/src/ux/progress.ts @@ -5,10 +5,14 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import * as util from 'node:util'; -import { ux } from '@oclif/core'; +import util from 'node:util'; +import { Options, SingleBar } from 'cli-progress'; import { UxBase } from './base.js'; +function progress(options: Options = {}): Progress.Bar { + return new SingleBar({ noTTYOutput: Boolean(process.env.TERM === 'dumb' || !process.stdin.isTTY), ...options }); +} + /** * Class for display a progress bar to the console. Will automatically be suppressed if the --json flag is present. */ @@ -50,10 +54,10 @@ export class Progress extends UxBase { this.maybeNoop(() => { const { title, ...rest } = { ...Progress.DEFAULT_OPTIONS, ...options }; - this.bar = ux.progress({ + this.bar = progress({ ...rest, format: util.format(rest.format, title), - }) as Progress.Bar; + }); this.bar.setTotal(total); this.bar.start(total, 0); From 03568ce04cd613fd8d8521742d2476c90d403f9c Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Tue, 16 Apr 2024 10:07:08 -0600 Subject: [PATCH 03/28] test: add integration tests for table --- .github/workflows/test.yml | 21 +++++ package.json | 1 + test/integration/ux/table.integration.ts | 114 +++++++++++++++++++++++ test/tsconfig.json | 2 +- test/unit/ux/table.test.ts | 37 -------- 5 files changed, 137 insertions(+), 38 deletions(-) create mode 100644 test/integration/ux/table.integration.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b65150fbc..0ad8c19d4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,6 +17,27 @@ jobs: needs: linux-unit-tests uses: salesforcecli/github-workflows/.github/workflows/unitTestsWindows.yml@main + integration: + needs: linux-unit-tests + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + node_version: [lts/*, latest] + exclude: + - os: windows-latest + node_version: lts/* + fail-fast: false + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node_version }} + cache: yarn + - uses: salesforcecli/github-workflows/.github/actions/yarnInstallWithRetries@main + - run: yarn build + - run: yarn test:integration + external-nuts-deploy-retrieve: name: external-nuts-deploy-retrieve needs: linux-unit-tests diff --git a/package.json b/package.json index 063bef702..8da43c02b 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "prepack": "sf-prepack", "prepare": "sf-install", "test": "wireit", + "test:integration": "mocha test/**/*.integration.ts --timeout 30000", "test:only": "wireit" }, "exports": { diff --git a/test/integration/ux/table.integration.ts b/test/integration/ux/table.integration.ts new file mode 100644 index 000000000..ed841845f --- /dev/null +++ b/test/integration/ux/table.integration.ts @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { expect } from 'chai'; +import stripAnsi from 'strip-ansi'; +import { table } from '../../../src/ux/table.js'; + +/* 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 }, + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access + stack: { extended: true, get: (r: any) => r.stack?.name }, +}; + +const ws = ' '; + +describe('table', () => { + let output = ''; + function printLine(line: string) { + output += stripAnsi(line) + '\n'; + } + + afterEach(() => { + output = ''; + }); + + // This can't be in the unit tests since wireit changes process.stdout.isTTY, which alters the behavior of table + it('should not truncate in TTY env', () => { + const three = { ...apps[0], id: '0'.repeat(80), name: 'supertable-test-3' }; + table([...apps, three], columns, { filter: 'id=0', printLine, truncate: false }); + expect(output).to.equal(` ID${ws.padEnd(78)} Name${ws.padEnd(14)} + ${''.padEnd(three.id.length, '─')} ─────────────────${ws} + ${three.id} supertable-test-3${ws}\n`); + }); + + it('does not exceed stack depth on very tall tables', () => { + const data = Array.from({ length: 150_000 }).fill({ id: '123', name: 'foo', value: 'bar' }) as Array< + Record + >; + const tallColumns = { + id: { header: 'ID' }, + name: {}, + value: { header: 'TEST' }, + }; + + table(data, tallColumns, { printLine }); + expect(output).to.include('ID'); + }); + + // skip because it's too slow + it.skip('does not exceed stack depth on very tall, wide tables', () => { + const columnsLength = 100; + const row = Object.fromEntries(Array.from({ length: columnsLength }).map((_, i) => [`col${i}`, 'foo'])); + const data = Array.from({ length: 150_000 }).fill(row) as Array>; + const bigColumns = Object.fromEntries( + Array.from({ length: columnsLength }).map((_, i) => [`col${i}`, { header: `col${i}`.toUpperCase() }]) + ); + + table(data, bigColumns, { printLine }); + expect(output).to.include('COL1'); + }); +}); diff --git a/test/tsconfig.json b/test/tsconfig.json index 069e50279..72c1d88ab 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "@salesforce/dev-config/tsconfig-test-strict-esm", - "include": ["unit/**/*.ts", "../node_modules/@types/**/*.d.ts"], + "include": ["unit/**/*.ts", "../node_modules/@types/**/*.d.ts", "integration/**/*.ts"], "compilerOptions": { "noEmit": true, "skipLibCheck": true diff --git a/test/unit/ux/table.test.ts b/test/unit/ux/table.test.ts index aaced1abf..b8ef41709 100644 --- a/test/unit/ux/table.test.ts +++ b/test/unit/ux/table.test.ts @@ -233,16 +233,6 @@ describe('table', () => { ─── ─────────────────${ws} 123 supertable-test-1${ws}\n`); }); - - // This test doesn't work because tests have process.stdout.isTTY=undefined when wireit runs the tests - // `yarn mocha test` works fine since process.stdout.isTTY=true - it.skip('should not truncate', () => { - const three = { ...apps[0], id: '0'.repeat(80), name: 'supertable-test-3' }; - table([...apps, three], columns, { filter: 'id=0', printLine, truncate: false }); - expect(output).to.equal(` ID${ws.padEnd(78)} Name${ws.padEnd(14)} - ${''.padEnd(three.id.length, '─')} ─────────────────${ws} - ${three.id} supertable-test-3${ws}\n`); - }); }); describe('edge cases', () => { @@ -272,32 +262,5 @@ describe('table', () => { 321 supertable-test-2${ws} 123 supertable-test-1${ws}\n`); }); - - it('does not exceed stack depth on very tall tables', () => { - const data = Array.from({ length: 150_000 }).fill({ id: '123', name: 'foo', value: 'bar' }) as Array< - Record - >; - const tallColumns = { - id: { header: 'ID' }, - name: {}, - value: { header: 'TEST' }, - }; - - table(data, tallColumns, { printLine }); - expect(output).to.include('ID'); - }); - - // Skip because it takes too long - it.skip('does not exceed stack depth on very tall, wide tables', () => { - const columnsLength = 100; - const row = Object.fromEntries(Array.from({ length: columnsLength }).map((_, i) => [`col${i}`, 'foo'])); - const data = Array.from({ length: 150_000 }).fill(row) as Array>; - const bigColumns = Object.fromEntries( - Array.from({ length: columnsLength }).map((_, i) => [`col${i}`, { header: `col${i}`.toUpperCase() }]) - ); - - table(data, bigColumns, { printLine }); - expect(output).to.include('COL1'); - }); }); }); From 32597ddb3f16971a98b1666ffb7afc1aa118dc67 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Tue, 16 Apr 2024 10:12:51 -0600 Subject: [PATCH 04/28] test: remove test --- test/integration/ux/table.integration.ts | 67 ------------------------ 1 file changed, 67 deletions(-) diff --git a/test/integration/ux/table.integration.ts b/test/integration/ux/table.integration.ts index ed841845f..ce342efe8 100644 --- a/test/integration/ux/table.integration.ts +++ b/test/integration/ux/table.integration.ts @@ -8,64 +8,6 @@ import { expect } from 'chai'; import stripAnsi from 'strip-ansi'; import { table } from '../../../src/ux/table.js'; -/* 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 }, - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access - stack: { extended: true, get: (r: any) => r.stack?.name }, -}; - -const ws = ' '; - describe('table', () => { let output = ''; function printLine(line: string) { @@ -76,15 +18,6 @@ describe('table', () => { output = ''; }); - // This can't be in the unit tests since wireit changes process.stdout.isTTY, which alters the behavior of table - it('should not truncate in TTY env', () => { - const three = { ...apps[0], id: '0'.repeat(80), name: 'supertable-test-3' }; - table([...apps, three], columns, { filter: 'id=0', printLine, truncate: false }); - expect(output).to.equal(` ID${ws.padEnd(78)} Name${ws.padEnd(14)} - ${''.padEnd(three.id.length, '─')} ─────────────────${ws} - ${three.id} supertable-test-3${ws}\n`); - }); - it('does not exceed stack depth on very tall tables', () => { const data = Array.from({ length: 150_000 }).fill({ id: '123', name: 'foo', value: 'bar' }) as Array< Record From 8efcdb868f37bfa6f9ae5a9cda3cc813edd9969f Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Tue, 16 Apr 2024 10:52:14 -0600 Subject: [PATCH 05/28] refactor: re-enable eslint rules --- src/ux/table.ts | 186 ++++++++++++++++-------------------------------- src/ux/ux.ts | 6 +- 2 files changed, 66 insertions(+), 126 deletions(-) diff --git a/src/ux/table.ts b/src/ux/table.ts index 322bb94e4..d7e8bbd80 100644 --- a/src/ux/table.ts +++ b/src/ux/table.ts @@ -10,17 +10,9 @@ import jsyaml from 'js-yaml'; import { orderBy } from 'natural-orderby'; import sliceAnsi from 'slice-ansi'; import sw from 'string-width'; -import { Interfaces, Flags as F } from '@oclif/core'; import write from './write.js'; -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ - function sumBy(arr: T[], fn: (i: T) => number): number { return arr.reduce((sum, i) => sum + fn(i), 0); } @@ -49,17 +41,19 @@ function termwidth(stream: NodeJS.WriteStream): number { const stdtermwidth = Number.parseInt(process.env.OCLIF_COLUMNS!, 10) || termwidth(process.stdout); class Table> { - private columns: Array & { key: string; maxWidth?: number; width?: number }>; + private columns: Array & { key: string; maxWidth?: number; width?: number }>; + + private options: Options & { printLine: (s: unknown) => void }; - private options: table.Options & { printLine(s: any): any }; + private data: Array>; - public constructor(private data: T[], columns: table.Columns, options: table.Options = {}) { + public constructor(data: T[], columns: Columns, options: 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 get = col.get ?? ((row: Record): unknown => row[key] ?? ''); const header = typeof col.header === 'string' ? col.header : capitalize(key.replaceAll('_', ' ')); const minWidth = Math.max(col.minWidth ?? 0, sw(header) + 1); @@ -87,20 +81,17 @@ class Table> { sort, title, }; - } - public 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; - }); + let rows = data.map((d) => + Object.fromEntries( + this.columns.map((col) => { + let val = col.get(d); + if (typeof val !== 'string') val = inspect(val, { breakLength: Number.POSITIVE_INFINITY }); + return [col.key, val]; + }) + ) + ); // filter rows if (this.options.filter) { @@ -110,7 +101,7 @@ class Table> { 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) => { + rows = rows.filter((d) => { const re = new RegExp(regex); const val = d[col.key] as string; const match = val.match(re); @@ -122,7 +113,11 @@ class Table> { if (this.options.sort) { const sorters = this.options.sort.split(','); const sortHeaders = sorters.map((k) => (k.startsWith('-') ? k.slice(1) : k)); - const sortKeys = this.filterColumnsFromHeaders(sortHeaders).map((c) => (v: any) => v[c.key]); + const sortKeys = this.filterColumnsFromHeaders(sortHeaders).map( + (c) => + (v: Record): unknown => + v[c.key] + ); const sortKeysOrder = sorters.map((k) => (k.startsWith('-') ? 'desc' : 'asc')); rows = orderBy(rows, sortKeys, sortKeysOrder); } @@ -137,7 +132,9 @@ class Table> { } this.data = rows; + } + public display(): void { switch (this.options.output) { case 'csv': { this.outputCSV(); @@ -162,11 +159,9 @@ class Table> { private filterColumnsFromHeaders( filters: string[] - ): Array & { key: string; maxWidth?: number; width?: number }> { - // unique - filters = [...new Set(filters)]; - const cols: Array & { key: string; maxWidth?: number; width?: number }> = []; - for (const f of filters) { + ): Array & { key: string; maxWidth?: number; width?: number }> { + const cols: Array & { key: string; maxWidth?: number; width?: number }> = []; + for (const f of [...new Set(filters)]) { const c = this.columns.find((i) => i.header.toLowerCase() === f.toLowerCase()); if (c) cols.push(c); } @@ -176,11 +171,11 @@ class Table> { private findColumnFromHeader( header: string - ): (table.Column & { key: string; maxWidth?: number; width?: number }) | undefined { + ): (Column & { key: string; maxWidth?: number; width?: number }) | undefined { return this.columns.find((c) => c.header.toLowerCase() === header.toLowerCase()); } - private getCSVRow(d: any): string[] { + private getCSVRow(d: Record): string[] { const values = this.columns.map((col) => d[col.key] || '') as string[]; const lineToBeEscaped = values.find( (e: string) => e.includes('"') || e.includes('\n') || e.includes('\r\n') || e.includes('\r') || e.includes(',') @@ -188,7 +183,7 @@ class Table> { return values.map((e) => (lineToBeEscaped ? `"${e.replaceAll('"', '""')}"` : e)); } - private outputCSV() { + private outputCSV(): void { const { columns, data, options } = this; if (!options['no-header']) { @@ -201,11 +196,11 @@ class Table> { } } - private outputJSON() { + private outputJSON(): void { this.options.printLine(JSON.stringify(this.resolveColumnsToObjectArray(), undefined, 2)); } - private outputTable() { + private outputTable(): void { const { data, options } = this; // column truncation // @@ -222,7 +217,7 @@ class Table> { // terminal width const maxWidth = stdtermwidth - 2; // truncation logic - const shouldShorten = () => { + const shouldShorten = (): void => { // don't shorten if full mode if (options['no-truncate'] ?? (!process.stdout.isTTY && !process.env.CLI_UX_SKIP_TTY_CHECK)) return; @@ -304,7 +299,7 @@ class Table> { // with multi-line strings let numOfLines = 1; for (const col of columns) { - const d = (row as any)[col.key] as string; + const d = row[col.key] as string; const lines = d.split('\n').length; if (lines > numOfLines) numOfLines = lines; } @@ -318,7 +313,7 @@ class Table> { let l = options.rowStart; for (const col of columns) { const width = col.width; - let d = (row as any)[col.key] as string; + let d = row[col.key] as string; d = d.split('\n')[i] || ''; const visualWidth = sw(d); const colorWidth = d.length - visualWidth; @@ -344,100 +339,45 @@ class Table> { this.options.printLine(jsyaml.dump(this.resolveColumnsToObjectArray())); } - private resolveColumnsToObjectArray(): Array<{ [k: string]: any }> { + private resolveColumnsToObjectArray(): Array> { const { columns, data } = this; - return data.map((d: any) => Object.fromEntries(columns.map((col) => [col.key, d[col.key] ?? '']))); + return data.map((d) => Object.fromEntries(columns.map((col) => [col.key, d[col.key] ?? '']))); } } -export function table>( - data: T[], - columns: table.Columns, - options: table.Options = {} -): void { +export function table>(data: T[], columns: Columns, options: 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 type Column> = { - extended: boolean; - header: string; - minWidth: number; - get(row: T): unknown; - }; - - export type Columns> = { [key: string]: Partial> }; - - // export type OutputType = 'csv' | 'json' | 'yaml' - - export type Options = { - [key: string]: any; - columns?: string; - extended?: boolean; - filter?: string; - 'no-header'?: boolean; - 'no-truncate'?: boolean; - output?: string; - sort?: string; - title?: string; - printLine?(s: any): any; - }; -} - -const getWidestColumnWith = (data: any[], columnKey: string): number => +export type Column> = { + extended: boolean; + header: string; + minWidth: number; + get(row: T): unknown; +}; + +export type Columns> = { [key: string]: Partial> }; + +export type Options = { + [key: string]: unknown; + columns?: string; + extended?: boolean; + filter?: string; + 'no-header'?: boolean; + 'no-truncate'?: boolean; + output?: string; + rowStart?: string; + sort?: string; + title?: string; + printLine?(s: unknown): void; +}; + +const getWidestColumnWith = (data: Array>, columnKey: string): number => data.reduce((previous, current) => { const d = current[columnKey]; + if (typeof d !== 'string') return previous; // convert multi-line cell to single longest line // for width calculations - const manyLines = (d as string).split('\n'); + const manyLines = d.split('\n'); return Math.max(previous, manyLines.length > 1 ? Math.max(...manyLines.map((r: string) => sw(r))) : sw(d)); }, 0); diff --git a/src/ux/ux.ts b/src/ux/ux.ts index 3fc016d0c..13c252b9c 100644 --- a/src/ux/ux.ts +++ b/src/ux/ux.ts @@ -11,7 +11,7 @@ import { AnyJson } from '@salesforce/ts-types'; import terminalLink from 'terminal-link'; import { UxBase } from './base.js'; import { Spinner } from './spinner.js'; -import { table } from './table.js'; +import { table, Columns as TableColumns, Options as TableOptions } from './table.js'; import styledJSON from './styledJSON.js'; import styledObject from './styledObject.js'; import write from './write.js'; @@ -125,7 +125,7 @@ export class Ux extends UxBase { export namespace Ux { export namespace Table { export type Data = Record; - export type Columns = table.Columns; - export type Options = table.Options; + export type Columns = TableColumns; + export type Options = TableOptions; } } From 9e07e9c2a5742e2b4b2df462ce8b149c64084198 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Tue, 16 Apr 2024 11:18:39 -0600 Subject: [PATCH 06/28] refactor: use AnyJson for styledObject --- src/ux/styledObject.ts | 41 ++++++++++++++++--------------- test/unit/ux/object.test.ts | 48 ++++++++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 22 deletions(-) diff --git a/src/ux/styledObject.ts b/src/ux/styledObject.ts index f4e2873d3..1930611b8 100644 --- a/src/ux/styledObject.ts +++ b/src/ux/styledObject.ts @@ -6,43 +6,42 @@ */ import { inspect } from 'node:util'; import chalk from 'chalk'; +import { AnyJson } from '@salesforce/ts-types'; -/* eslint-disable @typescript-eslint/no-explicit-any */ - -function pp(obj: any): any { - if (typeof obj === 'string' || typeof obj === 'number') return obj; +function prettyPrint(obj: AnyJson): string { + if (!obj) return inspect(obj); + if (typeof obj === 'string') return obj; + if (typeof obj === 'number') return obj.toString(); + if (typeof obj === 'boolean') return obj.toString(); if (typeof obj === 'object') { - return ( - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - Object.keys(obj) - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - .map((k) => k + ': ' + inspect(obj[k])) - .join(', ') - ); + return Object.entries(obj) + .map(([key, value]) => `${key}: ${inspect(value)}`) + .join(', '); } return inspect(obj); } -// @oclif/core v4 will have native support for coloring JSON so we won't need this then. -export default function styledObject(obj: any, keys?: string[]): string { +export default function styledObject(obj: AnyJson, keys?: string[]): string { + if (!obj) return inspect(obj); + if (typeof obj === 'string') return obj; + if (typeof obj === 'number') return obj.toString(); + if (typeof obj === 'boolean') return obj.toString(); + const output: string[] = []; - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument const keyLengths = Object.keys(obj).map((key) => key.toString().length); const maxKeyLength = Math.max(...keyLengths) + 2; - const logKeyValue = (key: string, value: any): string => - `${chalk.blue(key)}:` + ' '.repeat(maxKeyLength - key.length - 1) + pp(value); + const logKeyValue = (key: string, value: AnyJson): string => + `${chalk.blue(key)}:` + ' '.repeat(maxKeyLength - key.length - 1) + prettyPrint(value); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - for (const key of keys ?? Object.keys(obj).sort()) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access - const value = obj[key]; + for (const [key, value] of Object.entries(obj)) { + if (keys && !keys.includes(key)) continue; 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)); + output.push(' '.repeat(maxKeyLength) + prettyPrint(e)); } } } else if (value !== null && value !== undefined) { diff --git a/test/unit/ux/object.test.ts b/test/unit/ux/object.test.ts index 7902e323c..069bc51bd 100644 --- a/test/unit/ux/object.test.ts +++ b/test/unit/ux/object.test.ts @@ -9,7 +9,27 @@ import stripAnsi from 'strip-ansi'; import styledObject from '../../../src/ux/styledObject.js'; describe('styledObject', () => { - it('should show a table', () => { + it('should handle simple object', () => { + const result = styledObject({ foo: 1, bar: 2 }); + expect(stripAnsi(result)).to.equal('foo: 1\nbar: 2'); + }); + + it('should handle object with select keys', () => { + const result = styledObject({ foo: 1, bar: 2 }, ['foo']); + expect(stripAnsi(result)).to.equal('foo: 1'); + }); + + it('should handle deeply nested object', () => { + const result = styledObject({ foo: { bar: { baz: 1 } } }); + expect(stripAnsi(result)).to.equal('foo: bar: { baz: 1 }'); + }); + + it('should handle deeply nested objects with arrays', () => { + const result = styledObject({ foo: { bar: [{ baz: 1 }, { baz: 2 }] } }); + expect(stripAnsi(result)).to.equal('foo: bar: [ { baz: 1 }, { baz: 2 } ]'); + }); + + it('should show array input as table', () => { const result = styledObject([ { foo: 1, bar: 1 }, { foo: 2, bar: 2 }, @@ -19,4 +39,30 @@ describe('styledObject', () => { 1: foo: 2, bar: 2 2: foo: 3, bar: 3`); }); + + it('should handle nulls', () => { + const result = styledObject([{ foo: 1, bar: 1 }, null, { foo: 3, bar: 3 }]); + expect(stripAnsi(result)).to.equal(`0: foo: 1, bar: 1 +2: foo: 3, bar: 3`); + }); + + it('should handle null input', () => { + const result = styledObject(null); + expect(stripAnsi(result)).to.equal('null'); + }); + + it('should handle string input', () => { + const result = styledObject('foo'); + expect(stripAnsi(result)).to.equal('foo'); + }); + + it('should handle number input', () => { + const result = styledObject(1); + expect(stripAnsi(result)).to.equal('1'); + }); + + it('should handle boolean input', () => { + const result = styledObject(true); + expect(stripAnsi(result)).to.equal('true'); + }); }); From 18dcba8f6d32c942bd9a88e7b55525aae52510e8 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Thu, 18 Apr 2024 10:56:44 -0600 Subject: [PATCH 07/28] feat!: use next major of oclif/core BREAKING CHANGE: remove csv, json and yaml from table options --- package.json | 8 +-- src/errorFormatting.ts | 4 +- src/sfCommand.ts | 4 +- src/ux/standardColors.ts | 10 +-- src/ux/styledJSON.ts | 22 ------ src/ux/styledObject.ts | 4 +- src/ux/table.ts | 97 +++++--------------------- src/ux/ux.ts | 21 ++++-- test/unit/ux/table.test.ts | 80 ---------------------- test/unit/ux/ux.test.ts | 4 +- yarn.lock | 135 +++++-------------------------------- 11 files changed, 64 insertions(+), 325 deletions(-) delete mode 100644 src/ux/styledJSON.ts diff --git a/package.json b/package.json index 8da43c02b..7e29a8066 100644 --- a/package.json +++ b/package.json @@ -46,14 +46,12 @@ "dependencies": { "@inquirer/confirm": "^2.0.17", "@inquirer/password": "^1.1.16", - "@oclif/core": "^3.26.0", + "@oclif/core": "^4.0.0-beta.1", "@salesforce/core": "^7.2.0", "@salesforce/kit": "^3.1.0", "@salesforce/ts-types": "^2.0.9", - "cardinal": "^2.1.1", - "chalk": "^5.3.0", + "ansis": "^3.1.1", "cli-progress": "^3.12.0", - "js-yaml": "^4.1.0", "natural-orderby": "^3.0.2", "slice-ansi": "^7.1.0", "string-width": "^7.1.0", @@ -62,9 +60,7 @@ "devDependencies": { "@inquirer/type": "^1.2.1", "@salesforce/dev-scripts": "^8.5.0", - "@types/cardinal": "^2.1.1", "@types/cli-progress": "^3.11.5", - "@types/js-yaml": "^4.0.9", "eslint-plugin-sf-plugin": "^1.18.0", "ts-node": "^10.9.2", "typescript": "^5.4.5" diff --git a/src/errorFormatting.ts b/src/errorFormatting.ts index 1573299e5..8ba9137b3 100644 --- a/src/errorFormatting.ts +++ b/src/errorFormatting.ts @@ -6,7 +6,7 @@ */ import { Mode, Messages, envVars } from '@salesforce/core'; -import type { ChalkInstance } from 'chalk'; +import type { Ansis } from 'ansis'; import { StandardColors } from './ux/standardColors.js'; import { SfCommandError } from './types.js'; @@ -31,7 +31,7 @@ const messages = Messages.loadMessages('@salesforce/sf-plugins-core', 'messages' */ export const formatActions = ( actions: string[], - options: { actionColor: ChalkInstance } = { actionColor: StandardColors.info } + options: { actionColor: Ansis } = { actionColor: StandardColors.info } ): string[] => actions.length ? [ diff --git a/src/sfCommand.ts b/src/sfCommand.ts index 40db1d33f..9908a07af 100644 --- a/src/sfCommand.ts +++ b/src/sfCommand.ts @@ -256,7 +256,7 @@ export abstract class SfCommand extends Command { * @param obj The JSON to log. */ public styledJSON(obj: AnyJson): void { - this.ux.styledJSON(obj); + this.ux.styledJSON(obj, this.config.theme?.json); } /** @@ -283,7 +283,7 @@ export abstract class SfCommand extends Command { // If `--json` is enabled, then the ux instance on the class will disable output, which // means that the logJson method will not output anything. So, we need to create a new // instance of the ux class that does not have output disabled in order to log the json. - new Ux().styledJSON(json as AnyJson); + new Ux().styledJSON(json as AnyJson, this.config.theme?.json); } /** diff --git a/src/ux/standardColors.ts b/src/ux/standardColors.ts index 08d254c89..bd2f8ce39 100644 --- a/src/ux/standardColors.ts +++ b/src/ux/standardColors.ts @@ -5,11 +5,11 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import chalk from 'chalk'; +import ansis from 'ansis'; export const StandardColors = { - error: chalk.bold.red, - warning: chalk.bold.yellow, - info: chalk.dim, - success: chalk.bold.green, + error: ansis.bold.red, + warning: ansis.bold.yellow, + info: ansis.dim, + success: ansis.bold.green, }; diff --git a/src/ux/styledJSON.ts b/src/ux/styledJSON.ts deleted file mode 100644 index aae194512..000000000 --- a/src/ux/styledJSON.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (c) 2023, salesforce.com, inc. - * All rights reserved. - * Licensed under the BSD 3-Clause license. - * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -import { createRequire } from 'node:module'; -import { highlight, CardinalTheme } from 'cardinal'; -import chalk from 'chalk'; - -// @oclif/core v4 will have native support for coloring JSON so we won't need this then. -export default function styledJSON(obj: unknown): string { - const json = JSON.stringify(obj, null, 2); - if (!chalk.level) { - return json; - } - - const require = createRequire(import.meta.url); - // eslint-disable-next-line @typescript-eslint/no-var-requires - const theme = require('cardinal/themes/jq') as CardinalTheme; - return highlight(json, { theme }); -} diff --git a/src/ux/styledObject.ts b/src/ux/styledObject.ts index 1930611b8..0c4d1398c 100644 --- a/src/ux/styledObject.ts +++ b/src/ux/styledObject.ts @@ -5,7 +5,7 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import { inspect } from 'node:util'; -import chalk from 'chalk'; +import ansis from 'ansis'; import { AnyJson } from '@salesforce/ts-types'; function prettyPrint(obj: AnyJson): string { @@ -33,7 +33,7 @@ export default function styledObject(obj: AnyJson, keys?: string[]): string { const maxKeyLength = Math.max(...keyLengths) + 2; const logKeyValue = (key: string, value: AnyJson): string => - `${chalk.blue(key)}:` + ' '.repeat(maxKeyLength - key.length - 1) + prettyPrint(value); + `${ansis.blue(key)}:` + ' '.repeat(maxKeyLength - key.length - 1) + prettyPrint(value); for (const [key, value] of Object.entries(obj)) { if (keys && !keys.includes(key)) continue; diff --git a/src/ux/table.ts b/src/ux/table.ts index d7e8bbd80..ba0302374 100644 --- a/src/ux/table.ts +++ b/src/ux/table.ts @@ -5,8 +5,7 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import { inspect } from 'node:util'; -import chalk from 'chalk'; -import jsyaml from 'js-yaml'; +import ansis from 'ansis'; import { orderBy } from 'natural-orderby'; import sliceAnsi from 'slice-ansi'; import sw from 'string-width'; @@ -68,14 +67,13 @@ class Table> { // assign options // eslint-disable-next-line @typescript-eslint/unbound-method - const { columns: cols, csv, extended, filter, output, printLine, sort, title } = options; + const { columns: cols, extended, filter, 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: string): void => write.stdout(s)), rowStart: ' ', sort, @@ -135,72 +133,6 @@ class Table> { } public display(): void { - 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[] - ): Array & { key: string; maxWidth?: number; width?: number }> { - const cols: Array & { key: string; maxWidth?: number; width?: number }> = []; - for (const f of [...new Set(filters)]) { - const c = this.columns.find((i) => i.header.toLowerCase() === f.toLowerCase()); - if (c) cols.push(c); - } - - return cols; - } - - private findColumnFromHeader( - header: string - ): (Column & { key: string; maxWidth?: number; width?: number }) | undefined { - return this.columns.find((c) => c.header.toLowerCase() === header.toLowerCase()); - } - - private getCSVRow(d: Record): string[] { - const values = this.columns.map((col) => d[col.key] || '') as string[]; - 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(): void { - 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(): void { - this.options.printLine(JSON.stringify(this.resolveColumnsToObjectArray(), undefined, 2)); - } - - private outputTable(): void { const { data, options } = this; // column truncation // @@ -280,7 +212,7 @@ class Table> { headers += header.padEnd(col.width); } - options.printLine(chalk.bold(headers)); + if (headers) options.printLine(ansis.bold(headers)); // print header dividers let dividers = options.rowStart; @@ -289,7 +221,7 @@ class Table> { dividers += divider.padEnd(col.width); } - options.printLine(chalk.bold(dividers)); + if (dividers) options.printLine(ansis.bold(dividers)); } // print rows @@ -335,13 +267,22 @@ class Table> { } } - private outputYAML(): void { - this.options.printLine(jsyaml.dump(this.resolveColumnsToObjectArray())); + private filterColumnsFromHeaders( + filters: string[] + ): Array & { key: string; maxWidth?: number; width?: number }> { + const cols: Array & { key: string; maxWidth?: number; width?: number }> = []; + for (const f of [...new Set(filters)]) { + const c = this.columns.find((i) => i.header.toLowerCase() === f.toLowerCase()); + if (c) cols.push(c); + } + + return cols; } - private resolveColumnsToObjectArray(): Array> { - const { columns, data } = this; - return data.map((d) => Object.fromEntries(columns.map((col) => [col.key, d[col.key] ?? '']))); + private findColumnFromHeader( + header: string + ): (Column & { key: string; maxWidth?: number; width?: number }) | undefined { + return this.columns.find((c) => c.header.toLowerCase() === header.toLowerCase()); } } @@ -359,13 +300,11 @@ export type Column> = { export type Columns> = { [key: string]: Partial> }; export type Options = { - [key: string]: unknown; columns?: string; extended?: boolean; filter?: string; 'no-header'?: boolean; 'no-truncate'?: boolean; - output?: string; rowStart?: string; sort?: string; title?: string; diff --git a/src/ux/ux.ts b/src/ux/ux.ts index 13c252b9c..2799c7752 100644 --- a/src/ux/ux.ts +++ b/src/ux/ux.ts @@ -5,14 +5,13 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import chalk from 'chalk'; +import ansis from 'ansis'; import { ux } from '@oclif/core'; import { AnyJson } from '@salesforce/ts-types'; import terminalLink from 'terminal-link'; import { UxBase } from './base.js'; import { Spinner } from './spinner.js'; import { table, Columns as TableColumns, Options as TableOptions } from './table.js'; -import styledJSON from './styledJSON.js'; import styledObject from './styledObject.js'; import write from './write.js'; @@ -98,8 +97,20 @@ export class Ux extends UxBase { * * @param obj JSON to display */ - public styledJSON(obj: AnyJson): void { - this.maybeNoop(() => write.stdout(styledJSON(obj))); + public styledJSON(obj: AnyJson, theme?: Record): void { + // Default theme if sf's theme.json does not have the json property set. This will allow us + // to ship sf-plugins-core before the theme.json is updated. + const defaultTheme = { + key: 'blueBright', + string: 'greenBright', + number: 'blue', + boolean: 'redBright', + null: 'blackBright', + }; + + const mergedTheme = { ...defaultTheme, ...theme }; + + this.maybeNoop(() => write.stdout(ux.colorizeJson(obj, { theme: mergedTheme }))); } /** @@ -118,7 +129,7 @@ export class Ux extends UxBase { * @param text header to display */ public styledHeader(text: string): void { - this.maybeNoop(() => write.stdout(chalk.dim('=== ') + chalk.bold(text) + '\n')); + this.maybeNoop(() => write.stdout(ansis.dim('=== ') + ansis.bold(text) + '\n')); } } diff --git a/test/unit/ux/table.test.ts b/test/unit/ux/table.test.ts index b8ef41709..15adaa209 100644 --- a/test/unit/ux/table.test.ts +++ b/test/unit/ux/table.test.ts @@ -139,86 +139,6 @@ describe('table', () => { 321${ws}\n`); }); - it('should output in csv', () => { - table(apps, columns, { printLine, output: 'csv' }); - expect(output).to.equal(`ID,Name -123,supertable-test-1 -321,supertable-test-2\n`); - }); - - it('should output in csv with escaped values', () => { - 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', printLine } - ); - expect(output).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`); - }); - - it('should output in csv without headers', () => { - table(apps, columns, { printLine, output: 'csv', 'no-header': true }); - expect(output).to.equal(`123,supertable-test-1 -321,supertable-test-2\n`); - }); - - it('should output in csv with alias flag', () => { - table(apps, columns, { printLine, csv: true }); - expect(output).to.equal(`ID,Name -123,supertable-test-1 -321,supertable-test-2\n`); - }); - - it('should output in json', () => { - table(apps, columns, { printLine, output: 'json' }); - expect(output).to.equal(`[ - { - "id": "123", - "name": "supertable-test-1" - }, - { - "id": "321", - "name": "supertable-test-2" - } -] -`); - }); - - it('should output in yaml', () => { - table(apps, columns, { printLine, output: 'yaml' }); - expect(output).to.equal(`- id: '123' - name: supertable-test-1 -- id: '321' - name: supertable-test-2 - -`); - }); - it('should sort by property', () => { table(apps, columns, { printLine, sort: '-name' }); expect(output).to.equal(` ID Name${ws.padEnd(14)} diff --git a/test/unit/ux/ux.test.ts b/test/unit/ux/ux.test.ts index 953aec889..ba48987fe 100644 --- a/test/unit/ux/ux.test.ts +++ b/test/unit/ux/ux.test.ts @@ -59,9 +59,7 @@ describe('Ux', () => { it('should log stylized json', () => { const ux = new Ux(); ux.styledJSON({ foo: 'bar' }); - expect(stdoutStub.firstCall.args).to.deep.equal([ - '\x1B[97m{\x1B[39m\n \x1B[94m"foo"\x1B[39m\x1B[93m:\x1B[39m \x1B[92m"bar"\x1B[39m\n\x1B[97m}\x1B[39m', - ]); + expect(stdoutStub.firstCall.args).to.deep.equal(['{\n \u001b[94m"foo"\u001b[39m: \u001b[92m"bar"\u001b[39m\n}']); }); it('should not log anything if output is not enabled', () => { diff --git a/yarn.lock b/yarn.lock index 8fec4000a..80a3d6fe2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -584,36 +584,23 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@oclif/core@^3.26.0": - version "3.26.3" - resolved "https://registry.yarnpkg.com/@oclif/core/-/core-3.26.3.tgz#2416f4f5e3e5b9434999edb2f94983c5ac07e8a2" - integrity sha512-e6Vwu+cb2Sn4qFFpmY1fQLRWIY5ugruMuN94xb7+kyUzxrirYjJATPhuCT1G5xj9Dk+hTMH+Sp6XcHcVTS1lHg== +"@oclif/core@^4.0.0-beta.1": + version "4.0.0-beta.1" + resolved "https://registry.yarnpkg.com/@oclif/core/-/core-4.0.0-beta.1.tgz#c5ebf9e788466da2a44cb9686773ca4f6c01f90a" + integrity sha512-iOrJTq9AXJBu34UR61LagMzxtityFST/w48Lm60ewH+xf3UWaQ7f8zmn5e1nBPxxhTJRhS/r5rROky4Di/s7JA== 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" @@ -761,11 +748,6 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== -"@types/cardinal@^2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@types/cardinal/-/cardinal-2.1.1.tgz#dce628abdc123d2e03f4fdfc8bca688cb9590986" - integrity sha512-/xCVwg8lWvahHsV2wXZt4i64H1sdL+sN1Uoq7fAc8/FA6uYHjuIveDwPwvGUYp4VZiv85dVl6J/Bum3NDAOm8g== - "@types/chai@^4.3.14": version "4.3.14" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.14.tgz#ae3055ea2be43c91c9fd700a36d67820026d96e6" @@ -778,11 +760,6 @@ dependencies: "@types/node" "*" -"@types/js-yaml@^4.0.9": - version "4.0.9" - resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2" - integrity sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg== - "@types/json-schema@^7.0.12": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" @@ -1062,7 +1039,7 @@ ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@^4.0.0, ansi-styles@^4.1.0, ansi-styles@^4.3.0: +ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== @@ -1074,10 +1051,10 @@ ansi-styles@^6.1.0, ansi-styles@^6.2.1: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== -ansicolors@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" - integrity sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg== +ansis@^3.0.1, ansis@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.1.1.tgz#3e155f10362575dbfe1c298295a8d8fe4ebe140f" + integrity sha512-91DBlFEMcOgzaHhMQP4SaknYUZ3zKfroLfAQT4rHBdJW1lFgDeFQphtkDQ7jOtutDObROmDxQ/6an/j5D1Bprw== anymatch@~3.1.2: version "3.1.3" @@ -1209,11 +1186,6 @@ assertion-error@^1.1.0: resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== - async@^3.2.3: version "3.2.5" resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" @@ -1379,14 +1351,6 @@ capital-case@^1.0.4: tslib "^2.0.3" upper-case-first "^2.0.2" -cardinal@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/cardinal/-/cardinal-2.1.1.tgz#7cc1055d822d212954d07b085dea251cc7bc5505" - integrity sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw== - dependencies: - ansicolors "~0.3.2" - redeyed "~2.1.0" - chai@^4.3.10: version "4.3.10" resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.10.tgz#d784cec635e3b7e2ffb66446a63b4e33bd390384" @@ -1417,7 +1381,7 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^5.0.0, chalk@^5.3.0: +chalk@^5.0.0: version "5.3.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== @@ -1549,27 +1513,11 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== -color-name@^1.0.0, color-name@~1.1.4: +color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.9.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" - integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== - dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" - -color@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" - integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== - dependencies: - color-convert "^2.0.1" - color-string "^1.9.0" - colorette@^2.0.7: version "2.0.20" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" @@ -2202,7 +2150,7 @@ espree@^9.6.0, espree@^9.6.1: acorn-jsx "^5.3.2" eslint-visitor-keys "^3.4.1" -esprima@^4.0.0, esprima@~4.0.0: +esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== @@ -2836,11 +2784,6 @@ husky@^7.0.4: resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535" integrity sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ== -hyperlinker@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/hyperlinker/-/hyperlinker-1.0.0.tgz#23dc9e38a206b208ee49bc2d6c8ef47027df0c0e" - integrity sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ== - ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -2920,11 +2863,6 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== -is-arrayish@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" - integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== - is-bigint@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" @@ -3232,7 +3170,7 @@ js-yaml@4.1.0, js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -js-yaml@^3.13.1, js-yaml@^3.14.1: +js-yaml@^3.13.1: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== @@ -3788,11 +3726,6 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -natural-orderby@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/natural-orderby/-/natural-orderby-2.0.3.tgz#8623bc518ba162f8ff1cdb8941d74deb0fdcc016" - integrity sha512-p7KTHxU0CUrcOXe62Zfrb5Z13nLvPhSWR/so3kFulUQU0sgUll2Z0LwpsLN351eOOD+hRGu/F1g+6xDfPeD++Q== - natural-orderby@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/natural-orderby/-/natural-orderby-3.0.2.tgz#1b874d685fbd68beab2c6e7d14f298e03d631ec3" @@ -3911,11 +3844,6 @@ object-keys@^1.1.1: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object-treeify@^1.1.33: - version "1.1.33" - resolved "https://registry.yarnpkg.com/object-treeify/-/object-treeify-1.1.33.tgz#f06fece986830a3cba78ddd32d4c11d1f76cdf40" - integrity sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A== - object.assign@^4.1.4: version "4.1.4" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" @@ -4073,14 +4001,6 @@ pascal-case@^3.1.2: no-case "^3.0.4" tslib "^2.0.3" -password-prompt@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/password-prompt/-/password-prompt-1.1.3.tgz#05e539f4e7ca4d6c865d479313f10eb9db63ee5f" - integrity sha512-HkrjG2aJlvF0t2BMH0e2LB/EHf3Lcq3fNMzy4GYHcQblAvOl+QQji1Lx7WRBMqpVK8p+KR7bCg7oqAMXtdgqyw== - dependencies: - ansi-escapes "^4.3.2" - cross-spawn "^7.0.3" - path-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/path-case/-/path-case-3.0.4.tgz#9168645334eb942658375c56f80b4c0cb5f82c6f" @@ -4394,13 +4314,6 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" -redeyed@~2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-2.1.1.tgz#8984b5815d99cb220469c99eeeffe38913e6cc0b" - integrity sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ== - dependencies: - esprima "~4.0.0" - regexp-tree@^0.1.27: version "0.1.27" resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd" @@ -4677,13 +4590,6 @@ signal-exit@^4.0.1, signal-exit@^4.1.0: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== -simple-swizzle@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" - integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== - dependencies: - is-arrayish "^0.3.1" - sinon@10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/sinon/-/sinon-10.0.0.tgz#52279f97e35646ff73d23207d0307977c9b81430" @@ -4701,15 +4607,6 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -slice-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" - integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - slice-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-7.1.0.tgz#cd6b4655e298a8d1bdeb04250a433094b347b9a9" @@ -4923,7 +4820,7 @@ strip-json-comments@3.1.1, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -supports-color@8.1.1, supports-color@^8.1.1: +supports-color@8.1.1: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== From 2a5c355b9d83f111cd470cdae286dfe572e5a72c Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Thu, 18 Apr 2024 11:18:52 -0600 Subject: [PATCH 08/28] chore: use ux.stdout from core --- src/ux/table.ts | 5 ++--- src/ux/ux.ts | 13 ++++++------- src/ux/write.ts | 28 ---------------------------- test/unit/ux/ux.test.ts | 4 ++-- 4 files changed, 10 insertions(+), 40 deletions(-) delete mode 100644 src/ux/write.ts diff --git a/src/ux/table.ts b/src/ux/table.ts index ba0302374..df080ebe7 100644 --- a/src/ux/table.ts +++ b/src/ux/table.ts @@ -9,8 +9,7 @@ import ansis from 'ansis'; import { orderBy } from 'natural-orderby'; import sliceAnsi from 'slice-ansi'; import sw from 'string-width'; - -import write from './write.js'; +import { ux } from '@oclif/core'; function sumBy(arr: T[], fn: (i: T) => number): number { return arr.reduce((sum, i) => sum + fn(i), 0); @@ -74,7 +73,7 @@ class Table> { filter, 'no-header': options['no-header'] ?? false, 'no-truncate': options['no-truncate'] ?? false, - printLine: printLine ?? ((s: string): void => write.stdout(s)), + printLine: printLine ?? ((s: string): void => ux.stdout(s)), rowStart: ' ', sort, title, diff --git a/src/ux/ux.ts b/src/ux/ux.ts index 2799c7752..18dca5236 100644 --- a/src/ux/ux.ts +++ b/src/ux/ux.ts @@ -13,7 +13,6 @@ import { UxBase } from './base.js'; import { Spinner } from './spinner.js'; import { table, Columns as TableColumns, Options as TableOptions } from './table.js'; import styledObject from './styledObject.js'; -import write from './write.js'; /** * UX methods for plugins. Automatically suppress console output if outputEnabled is set to false. @@ -48,7 +47,7 @@ export class Ux extends UxBase { * @param args Args to be used for formatting. */ public log(message?: string, ...args: string[]): void { - this.maybeNoop(() => write.stdout(message, ...args)); + this.maybeNoop(() => ux.stdout(message, ...args)); } /** @@ -58,7 +57,7 @@ export class Ux extends UxBase { * @param args Args to be used for formatting. */ public logToStderr(message?: string, ...args: string[]): void { - this.maybeNoop(() => write.stderr(message, ...args)); + this.maybeNoop(() => ux.stderr(message, ...args)); } /** @@ -89,7 +88,7 @@ export class Ux extends UxBase { * @param params */ public url(text: string, uri: string, params = {}): void { - this.maybeNoop(() => write.stdout(terminalLink(text, uri, { fallback: () => uri, ...params }))); + this.maybeNoop(() => ux.stdout(terminalLink(text, uri, { fallback: () => uri, ...params }))); } /** @@ -110,7 +109,7 @@ export class Ux extends UxBase { const mergedTheme = { ...defaultTheme, ...theme }; - this.maybeNoop(() => write.stdout(ux.colorizeJson(obj, { theme: mergedTheme }))); + this.maybeNoop(() => ux.stdout(ux.colorizeJson(obj, { theme: mergedTheme }))); } /** @@ -120,7 +119,7 @@ export class Ux extends UxBase { * @param keys Keys of object to display */ public styledObject(obj: AnyJson, keys?: string[]): void { - this.maybeNoop(() => write.stdout(styledObject(obj, keys))); + this.maybeNoop(() => ux.stdout(styledObject(obj, keys))); } /** @@ -129,7 +128,7 @@ export class Ux extends UxBase { * @param text header to display */ public styledHeader(text: string): void { - this.maybeNoop(() => write.stdout(ansis.dim('=== ') + ansis.bold(text) + '\n')); + this.maybeNoop(() => ux.stdout(ansis.dim('=== ') + ansis.bold(text) + '\n')); } } diff --git a/src/ux/write.ts b/src/ux/write.ts deleted file mode 100644 index eda9fe001..000000000 --- a/src/ux/write.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2023, salesforce.com, inc. - * All rights reserved. - * Licensed under the BSD 3-Clause license. - * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -import { format } from 'node:util'; - -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'); - } -}; - -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'); - } -}; - -export default { - stdout, - stderr, -}; diff --git a/test/unit/ux/ux.test.ts b/test/unit/ux/ux.test.ts index ba48987fe..2b592ea90 100644 --- a/test/unit/ux/ux.test.ts +++ b/test/unit/ux/ux.test.ts @@ -7,8 +7,8 @@ import { expect } from 'chai'; import sinon from 'sinon'; +import { ux as coreUx } from '@oclif/core'; import { Ux } from '../../../src/ux/ux.js'; -import write from '../../../src/ux/write.js'; describe('Ux', () => { let sandbox: sinon.SinonSandbox; @@ -16,7 +16,7 @@ describe('Ux', () => { beforeEach(() => { sandbox = sinon.createSandbox(); - stdoutStub = sandbox.stub(write, 'stdout').callsFake(() => {}); + stdoutStub = sandbox.stub(coreUx, 'stdout').callsFake(() => {}); }); afterEach(() => { From a56f19adc0de938eda9258d2a846664c0910d324 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 24 Apr 2024 09:06:53 -0600 Subject: [PATCH 09/28] chore: bump @oclif/core --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 7e29a8066..943ecb9a3 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "dependencies": { "@inquirer/confirm": "^2.0.17", "@inquirer/password": "^1.1.16", - "@oclif/core": "^4.0.0-beta.1", + "@oclif/core": "^4.0.0-beta.2", "@salesforce/core": "^7.2.0", "@salesforce/kit": "^3.1.0", "@salesforce/ts-types": "^2.0.9", diff --git a/yarn.lock b/yarn.lock index 80a3d6fe2..ae9a389fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -584,10 +584,10 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@oclif/core@^4.0.0-beta.1": - version "4.0.0-beta.1" - resolved "https://registry.yarnpkg.com/@oclif/core/-/core-4.0.0-beta.1.tgz#c5ebf9e788466da2a44cb9686773ca4f6c01f90a" - integrity sha512-iOrJTq9AXJBu34UR61LagMzxtityFST/w48Lm60ewH+xf3UWaQ7f8zmn5e1nBPxxhTJRhS/r5rROky4Di/s7JA== +"@oclif/core@^4.0.0-beta.2": + version "4.0.0-beta.2" + resolved "https://registry.yarnpkg.com/@oclif/core/-/core-4.0.0-beta.2.tgz#80f4470f09fe3438ca11ea67490a2daf9e162b33" + integrity sha512-BKK1WMe4hZ1TiCk2pVlYPaa1Deg05YtobG2iCkF1XZIxOSyjX9GySHZHrBZFp+pcI/TJTRM8sY3Ol0Q9AKGSKg== dependencies: ansi-escapes "^4.3.2" ansis "^3.0.1" From bdb83015426f9424684bbd1446d8d3c64c1b9552 Mon Sep 17 00:00:00 2001 From: svc-cli-bot Date: Wed, 24 Apr 2024 15:08:09 +0000 Subject: [PATCH 10/28] chore(release): 9.0.3-beta.0 [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 943ecb9a3..eab0c0a36 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@salesforce/sf-plugins-core", - "version": "9.0.2", + "version": "9.0.3-beta.0", "description": "Utils for writing Salesforce CLI plugins", "main": "lib/exported", "types": "lib/exported.d.ts", From 005e5987650e2209dbd0c0993d139f6da2413350 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 24 Apr 2024 09:11:55 -0600 Subject: [PATCH 11/28] chore: bump @oclif/core --- package.json | 2 +- yarn.lock | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index eab0c0a36..29a4cc68c 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "dependencies": { "@inquirer/confirm": "^2.0.17", "@inquirer/password": "^1.1.16", - "@oclif/core": "^4.0.0-beta.2", + "@oclif/core": "^4.0.0-beta.3", "@salesforce/core": "^7.2.0", "@salesforce/kit": "^3.1.0", "@salesforce/ts-types": "^2.0.9", diff --git a/yarn.lock b/yarn.lock index ae9a389fb..e17c69c72 100644 --- a/yarn.lock +++ b/yarn.lock @@ -584,10 +584,10 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@oclif/core@^4.0.0-beta.2": - version "4.0.0-beta.2" - resolved "https://registry.yarnpkg.com/@oclif/core/-/core-4.0.0-beta.2.tgz#80f4470f09fe3438ca11ea67490a2daf9e162b33" - integrity sha512-BKK1WMe4hZ1TiCk2pVlYPaa1Deg05YtobG2iCkF1XZIxOSyjX9GySHZHrBZFp+pcI/TJTRM8sY3Ol0Q9AKGSKg== +"@oclif/core@^4.0.0-beta.3": + version "4.0.0-beta.3" + resolved "https://registry.yarnpkg.com/@oclif/core/-/core-4.0.0-beta.3.tgz#37c79177d554532d4059450562725417b2093394" + integrity sha512-zl8gL2aC3QxuBJw+x6ji65wSvu4biaythyZ5laIJ2c0YbG+J9iIYUJEfokN8/xib9qRjg6b0/lPaLdF5UyeDyQ== dependencies: ansi-escapes "^4.3.2" ansis "^3.0.1" @@ -601,6 +601,7 @@ is-wsl "^2.2.0" minimatch "^9.0.4" string-width "^4.2.3" + supports-color "^9.4.0" widest-line "^3.1.0" wordwrap "^1.0.0" wrap-ansi "^7.0.0" @@ -4841,6 +4842,11 @@ supports-color@^7.0.0, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +supports-color@^9.4.0: + version "9.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-9.4.0.tgz#17bfcf686288f531db3dea3215510621ccb55954" + integrity sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw== + supports-hyperlinks@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz#3943544347c1ff90b15effb03fc14ae45ec10624" From 428c34a60871e6f3e0ea33d79dd5bd8b5d433735 Mon Sep 17 00:00:00 2001 From: svc-cli-bot Date: Wed, 24 Apr 2024 15:12:52 +0000 Subject: [PATCH 12/28] chore(release): 9.0.3-beta.1 [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 29a4cc68c..d16bec578 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@salesforce/sf-plugins-core", - "version": "9.0.3-beta.0", + "version": "9.0.3-beta.1", "description": "Utils for writing Salesforce CLI plugins", "main": "lib/exported", "types": "lib/exported.d.ts", From 4307176cc4e40020f871df2c40cba491eb3e9a7f Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 24 Apr 2024 09:26:31 -0600 Subject: [PATCH 13/28] ci: update preBuildCommands --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0ad8c19d4..255280fe2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,7 +59,7 @@ jobs: with: packageName: '@salesforce/sf-plugins-core' externalProjectGitUrl: 'https://github.com/salesforcecli/plugin-deploy-retrieve' - preBuildCommands: 'shx rm -rf node_modules/@oclif/core; shx rm -rf node_modules/@salesforce/kit; shx rm -rf node_modules/@salesforce/core; shx rm -rf node_modules/@salesforce/ts-types; shx rm -rf node_modules/@salesforce/cli-plugins-testkit' + preBuildCommands: 'shx rm -rf node_modules/@salesforce/kit node_modules/@salesforce/core node_modules/@salesforce/ts-types node_modules/@salesforce/cli-plugins-testkit' command: ${{ matrix.command }} os: ${{ matrix.os }} secrets: inherit @@ -88,7 +88,7 @@ jobs: with: packageName: '@salesforce/sf-plugins-core' externalProjectGitUrl: 'https://github.com/salesforcecli/${{matrix.repo}}' - preBuildCommands: 'shx rm -rf node_modules/@oclif/core; shx rm -rf node_modules/@salesforce/kit; shx rm -rf node_modules/@salesforce/core; shx rm -rf node_modules/@salesforce/ts-types' + preBuildCommands: 'shx rm -rf node_modules/@salesforce/kit node_modules/@salesforce/core node_modules/@salesforce/ts-types' command: yarn test:nuts os: ${{ matrix.os }} secrets: inherit From 80e989b971c5582f2c8c248b6c113b3e56dd1631 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 24 Apr 2024 09:29:56 -0600 Subject: [PATCH 14/28] ci: update preBuildCommands --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 255280fe2..32eb1bfe8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,7 +59,7 @@ jobs: with: packageName: '@salesforce/sf-plugins-core' externalProjectGitUrl: 'https://github.com/salesforcecli/plugin-deploy-retrieve' - preBuildCommands: 'shx rm -rf node_modules/@salesforce/kit node_modules/@salesforce/core node_modules/@salesforce/ts-types node_modules/@salesforce/cli-plugins-testkit' + preBuildCommands: 'shx rm -rf node_modules/@oclif/core node_modules/@salesforce/kit node_modules/@salesforce/core node_modules/@salesforce/ts-types node_modules/@salesforce/cli-plugins-testkit' command: ${{ matrix.command }} os: ${{ matrix.os }} secrets: inherit @@ -88,7 +88,7 @@ jobs: with: packageName: '@salesforce/sf-plugins-core' externalProjectGitUrl: 'https://github.com/salesforcecli/${{matrix.repo}}' - preBuildCommands: 'shx rm -rf node_modules/@salesforce/kit node_modules/@salesforce/core node_modules/@salesforce/ts-types' + preBuildCommands: 'shx rm -rf node_modules/@oclif/core node_modules/@salesforce/kit node_modules/@salesforce/core node_modules/@salesforce/ts-types' command: yarn test:nuts os: ${{ matrix.os }} secrets: inherit From 5f9dadcce2a5e60b9364eb2c25904bfd75872b1d Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Thu, 23 May 2024 09:07:16 -0600 Subject: [PATCH 15/28] chore: bump core --- package.json | 2 +- src/errorHandling.ts | 4 ++-- test/unit/sfCommand.test.ts | 2 +- yarn.lock | 24 ++++++++++++++++++++---- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index d16bec578..bb9efa88e 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "dependencies": { "@inquirer/confirm": "^2.0.17", "@inquirer/password": "^1.1.16", - "@oclif/core": "^4.0.0-beta.3", + "@oclif/core": "4.0.0-beta.9", "@salesforce/core": "^7.2.0", "@salesforce/kit": "^3.1.0", "@salesforce/ts-types": "^2.0.9", diff --git a/src/errorHandling.ts b/src/errorHandling.ts index e4de61cd6..1b3b377a4 100644 --- a/src/errorHandling.ts +++ b/src/errorHandling.ts @@ -6,7 +6,7 @@ */ import { SfError } from '@salesforce/core'; -import { OclifError } from '@oclif/core/lib/interfaces/errors.js'; +import { CLIError } from '@oclif/core/errors'; import { SfCommandError } from './types.js'; import { removeEmpty } from './util.js'; @@ -89,5 +89,5 @@ export const errorToSfCommandError = ( }); /** custom typeGuard for handling the fact the SfCommand doesn't know about oclif error structure */ -const isOclifError = (e: T): e is T & OclifError => +const isOclifError = (e: T): e is T & CLIError => 'oclif' in e ? true : false; diff --git a/test/unit/sfCommand.test.ts b/test/unit/sfCommand.test.ts index 4dd3d5a64..39108e978 100644 --- a/test/unit/sfCommand.test.ts +++ b/test/unit/sfCommand.test.ts @@ -9,7 +9,7 @@ import { Lifecycle } from '@salesforce/core'; import { TestContext } from '@salesforce/core/testSetup'; import { assert, expect } from 'chai'; import { SfError } from '@salesforce/core'; -import { Config } from '@oclif/core/lib/interfaces'; +import { Config } from '@oclif/core/interfaces'; import { SfCommand } from '../../src/sfCommand.js'; import { StandardColors } from '../../src/ux/standardColors.js'; import { stubSfCommandUx, stubSpinner } from '../../src/stubUx.js'; diff --git a/yarn.lock b/yarn.lock index e17c69c72..f5073e2a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -584,15 +584,16 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@oclif/core@^4.0.0-beta.3": - version "4.0.0-beta.3" - resolved "https://registry.yarnpkg.com/@oclif/core/-/core-4.0.0-beta.3.tgz#37c79177d554532d4059450562725417b2093394" - integrity sha512-zl8gL2aC3QxuBJw+x6ji65wSvu4biaythyZ5laIJ2c0YbG+J9iIYUJEfokN8/xib9qRjg6b0/lPaLdF5UyeDyQ== +"@oclif/core@4.0.0-beta.9": + version "4.0.0-beta.9" + resolved "https://registry.yarnpkg.com/@oclif/core/-/core-4.0.0-beta.9.tgz#c502c67e92b809704e883670974de1d6acf6bfc2" + integrity sha512-OHPVfKzRQbNwFrVDA9T5LU6/751bmyPCbG5RaAuMpAzdCYq/6RKna4wMXq4tBfr6V4Nj9PU5NTwpf0Wz6/SWEQ== dependencies: ansi-escapes "^4.3.2" ansis "^3.0.1" clean-stack "^3.0.1" cli-spinners "^2.9.2" + cosmiconfig "^9.0.0" debug "^4.3.4" ejs "^3.1.10" get-package-type "^0.1.0" @@ -1622,6 +1623,16 @@ cosmiconfig@^8.0.0, cosmiconfig@^8.3.6: parse-json "^5.2.0" path-type "^4.0.0" +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" + create-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" @@ -1863,6 +1874,11 @@ entities@^4.2.0, entities@^4.5.0: resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== +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== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" From 309ed320969219d8463d882322a68dbdc82daf43 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Thu, 23 May 2024 10:20:23 -0600 Subject: [PATCH 16/28] chore: bump core --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index bb9efa88e..6762a97a3 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "dependencies": { "@inquirer/confirm": "^2.0.17", "@inquirer/password": "^1.1.16", - "@oclif/core": "4.0.0-beta.9", + "@oclif/core": "4.0.0-beta.10", "@salesforce/core": "^7.2.0", "@salesforce/kit": "^3.1.0", "@salesforce/ts-types": "^2.0.9", diff --git a/yarn.lock b/yarn.lock index f5073e2a8..b2a4d6415 100644 --- a/yarn.lock +++ b/yarn.lock @@ -584,10 +584,10 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@oclif/core@4.0.0-beta.9": - version "4.0.0-beta.9" - resolved "https://registry.yarnpkg.com/@oclif/core/-/core-4.0.0-beta.9.tgz#c502c67e92b809704e883670974de1d6acf6bfc2" - integrity sha512-OHPVfKzRQbNwFrVDA9T5LU6/751bmyPCbG5RaAuMpAzdCYq/6RKna4wMXq4tBfr6V4Nj9PU5NTwpf0Wz6/SWEQ== +"@oclif/core@4.0.0-beta.10": + version "4.0.0-beta.10" + resolved "https://registry.yarnpkg.com/@oclif/core/-/core-4.0.0-beta.10.tgz#809c9f7b5639aacc163944b24ec0ca706cdbff3c" + integrity sha512-Nuzh6ySmNSQQYc/nfup8quaK6ZEZQq59ddljVgCqyYyHP4AvpeGmIh/9aBUUjdI2jScpj6Sck4erKFrVPKbLsQ== dependencies: ansi-escapes "^4.3.2" ansis "^3.0.1" From 8c909893782ac69999f4485abd1e945aa736691d Mon Sep 17 00:00:00 2001 From: svc-cli-bot Date: Thu, 30 May 2024 14:43:14 +0000 Subject: [PATCH 17/28] chore(release): 9.0.14-beta.1 [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c7d5b7d51..63363fe33 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@salesforce/sf-plugins-core", - "version": "9.0.14-beta.0", + "version": "9.0.14-beta.1", "description": "Utils for writing Salesforce CLI plugins", "main": "lib/exported", "types": "lib/exported.d.ts", From edf5d394db058984a3ebd7ba35f6039498319ff8 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Tue, 4 Jun 2024 10:55:05 -0600 Subject: [PATCH 18/28] fix: core v4 --- package.json | 2 +- yarn.lock | 30 ++++++++++++++++-------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 63363fe33..0b6e70959 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "dependencies": { "@inquirer/confirm": "^2.0.17", "@inquirer/password": "^1.1.16", - "@oclif/core": "^4.0.0-beta.13", + "@oclif/core": "^4", "@salesforce/core": "^7.3.9", "@salesforce/kit": "^3.1.2", "@salesforce/ts-types": "^2.0.9", diff --git a/yarn.lock b/yarn.lock index 0511a9e9b..488d65176 100644 --- a/yarn.lock +++ b/yarn.lock @@ -584,17 +584,17 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@oclif/core@^4.0.0-beta.13": - version "4.0.0-beta.13" - resolved "https://registry.yarnpkg.com/@oclif/core/-/core-4.0.0-beta.13.tgz#0e0a6431cfe612db77073d2f9ef361f14e4090b8" - integrity sha512-ug8CZUCJphgetSZVgd4HQgyewlYVGGG1LIeFXGxjgYsjZ/f5I3nSCj7xpAMEDqjVD/lwmSujtVwa7tvEgLGICw== +"@oclif/core@^4": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@oclif/core/-/core-4.0.0.tgz#c47783c9803cb440e3fd325157437400765eae3f" + integrity sha512-BMWGvJrzn5PnG60gTNFEvaBT0jvGNiJCKN4aJBYP6E7Bq/Y5XPnxPrkj7ZZs/Jsd1oVn6K/JRmF6gWpv72DOew== dependencies: ansi-escapes "^4.3.2" - ansis "^3.0.1" + ansis "^3.1.1" clean-stack "^3.0.1" cli-spinners "^2.9.2" cosmiconfig "^9.0.0" - debug "^4.3.4" + debug "^4.3.5" ejs "^3.1.10" get-package-type "^0.1.0" globby "^11.1.0" @@ -602,7 +602,7 @@ is-wsl "^2.2.0" minimatch "^9.0.4" string-width "^4.2.3" - supports-color "^9.4.0" + supports-color "^8" widest-line "^3.1.0" wordwrap "^1.0.0" wrap-ansi "^7.0.0" @@ -1053,7 +1053,7 @@ ansi-styles@^6.1.0, ansi-styles@^6.2.1: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== -ansis@^3.0.1, ansis@^3.1.1: +ansis@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.1.1.tgz#3e155f10362575dbfe1c298295a8d8fe4ebe140f" integrity sha512-91DBlFEMcOgzaHhMQP4SaknYUZ3zKfroLfAQT4rHBdJW1lFgDeFQphtkDQ7jOtutDObROmDxQ/6an/j5D1Bprw== @@ -1688,6 +1688,13 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.3.5: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + decamelize-keys@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.1.tgz#04a2d523b2f18d80d0158a43b895d56dff8d19d8" @@ -4835,7 +4842,7 @@ strip-json-comments@3.1.1, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -supports-color@8.1.1: +supports-color@8.1.1, supports-color@^8: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== @@ -4856,11 +4863,6 @@ supports-color@^7.0.0, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-color@^9.4.0: - version "9.4.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-9.4.0.tgz#17bfcf686288f531db3dea3215510621ccb55954" - integrity sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw== - supports-hyperlinks@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz#3943544347c1ff90b15effb03fc14ae45ec10624" From 59c80673551f9c1ad0b3a1f979524db6660d8c5c Mon Sep 17 00:00:00 2001 From: svc-cli-bot Date: Tue, 4 Jun 2024 16:56:58 +0000 Subject: [PATCH 19/28] chore(release): 9.1.1-beta.1 [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6261ad264..4250587c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@salesforce/sf-plugins-core", - "version": "9.1.1-beta.0", + "version": "9.1.1-beta.1", "description": "Utils for writing Salesforce CLI plugins", "main": "lib/exported", "types": "lib/exported.d.ts", From e7fbd29db10d873cd2bbc32203154a7e3ff62eac Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Tue, 4 Jun 2024 11:12:34 -0600 Subject: [PATCH 20/28] test: dont rm oclif/core for external nuts --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 32eb1bfe8..255280fe2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,7 +59,7 @@ jobs: with: packageName: '@salesforce/sf-plugins-core' externalProjectGitUrl: 'https://github.com/salesforcecli/plugin-deploy-retrieve' - preBuildCommands: 'shx rm -rf node_modules/@oclif/core node_modules/@salesforce/kit node_modules/@salesforce/core node_modules/@salesforce/ts-types node_modules/@salesforce/cli-plugins-testkit' + preBuildCommands: 'shx rm -rf node_modules/@salesforce/kit node_modules/@salesforce/core node_modules/@salesforce/ts-types node_modules/@salesforce/cli-plugins-testkit' command: ${{ matrix.command }} os: ${{ matrix.os }} secrets: inherit @@ -88,7 +88,7 @@ jobs: with: packageName: '@salesforce/sf-plugins-core' externalProjectGitUrl: 'https://github.com/salesforcecli/${{matrix.repo}}' - preBuildCommands: 'shx rm -rf node_modules/@oclif/core node_modules/@salesforce/kit node_modules/@salesforce/core node_modules/@salesforce/ts-types' + preBuildCommands: 'shx rm -rf node_modules/@salesforce/kit node_modules/@salesforce/core node_modules/@salesforce/ts-types' command: yarn test:nuts os: ${{ matrix.os }} secrets: inherit From 44191a181d08808fc5137625cd1dbe650c08645f Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Tue, 4 Jun 2024 12:11:20 -0600 Subject: [PATCH 21/28] test: remove ocilf/core in prebuild --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 255280fe2..32eb1bfe8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,7 +59,7 @@ jobs: with: packageName: '@salesforce/sf-plugins-core' externalProjectGitUrl: 'https://github.com/salesforcecli/plugin-deploy-retrieve' - preBuildCommands: 'shx rm -rf node_modules/@salesforce/kit node_modules/@salesforce/core node_modules/@salesforce/ts-types node_modules/@salesforce/cli-plugins-testkit' + preBuildCommands: 'shx rm -rf node_modules/@oclif/core node_modules/@salesforce/kit node_modules/@salesforce/core node_modules/@salesforce/ts-types node_modules/@salesforce/cli-plugins-testkit' command: ${{ matrix.command }} os: ${{ matrix.os }} secrets: inherit @@ -88,7 +88,7 @@ jobs: with: packageName: '@salesforce/sf-plugins-core' externalProjectGitUrl: 'https://github.com/salesforcecli/${{matrix.repo}}' - preBuildCommands: 'shx rm -rf node_modules/@salesforce/kit node_modules/@salesforce/core node_modules/@salesforce/ts-types' + preBuildCommands: 'shx rm -rf node_modules/@oclif/core node_modules/@salesforce/kit node_modules/@salesforce/core node_modules/@salesforce/ts-types' command: yarn test:nuts os: ${{ matrix.os }} secrets: inherit From 5f0b39f0e431e11f9cfbccb780605b094385b851 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Tue, 4 Jun 2024 12:22:01 -0600 Subject: [PATCH 22/28] chore: make new major --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4250587c6..93d5f88e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@salesforce/sf-plugins-core", - "version": "9.1.1-beta.1", + "version": "10.0.0-beta.0", "description": "Utils for writing Salesforce CLI plugins", "main": "lib/exported", "types": "lib/exported.d.ts", From 704b2cf46f7675db056dadd1265209984af85028 Mon Sep 17 00:00:00 2001 From: svc-cli-bot Date: Tue, 4 Jun 2024 18:30:49 +0000 Subject: [PATCH 23/28] chore(release): 10.0.0-beta.1 [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 93d5f88e8..7fa385e06 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@salesforce/sf-plugins-core", - "version": "10.0.0-beta.0", + "version": "10.0.0-beta.1", "description": "Utils for writing Salesforce CLI plugins", "main": "lib/exported", "types": "lib/exported.d.ts", From aa63615fe813e73c368adb3fb59dd201f2e91669 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 5 Jun 2024 09:39:03 -0600 Subject: [PATCH 24/28] chore: code review --- src/ux/table.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/ux/table.ts b/src/ux/table.ts index df080ebe7..b5822810d 100644 --- a/src/ux/table.ts +++ b/src/ux/table.ts @@ -24,7 +24,7 @@ function termwidth(stream: NodeJS.WriteStream): number { return 80; } - const width = stream.getWindowSize()[0]; + const [width] = stream.getWindowSize(); if (width < 1) { return 80; } @@ -36,7 +36,10 @@ function termwidth(stream: NodeJS.WriteStream): number { return width; } -const stdtermwidth = Number.parseInt(process.env.OCLIF_COLUMNS!, 10) || termwidth(process.stdout); +const stdtermwidth = + typeof process.env.OCLIF_COLUMNS === 'string' + ? Number.parseInt(process.env.OCLIF_COLUMNS, 10) + : termwidth(process.stdout); class Table> { private columns: Array & { key: string; maxWidth?: number; width?: number }>; @@ -47,8 +50,7 @@ class Table> { public constructor(data: T[], columns: Columns, options: Options = {}) { // assign columns - this.columns = Object.keys(columns).map((key: string) => { - const col = columns[key]; + this.columns = Object.entries(columns).map(([key, col]) => { const extended = col.extended ?? false; // turn null and undefined into empty strings by default const get = col.get ?? ((row: Record): unknown => row[key] ?? ''); @@ -148,7 +150,7 @@ class Table> { // terminal width const maxWidth = stdtermwidth - 2; // truncation logic - const shouldShorten = (): void => { + const maybeShorten = (): void => { // don't shorten if full mode if (options['no-truncate'] ?? (!process.stdout.isTTY && !process.env.CLI_UX_SKIP_TTY_CHECK)) return; @@ -187,7 +189,7 @@ class Table> { } }; - shouldShorten(); + maybeShorten(); // print table title if (options.title) { From 744666f51862bd1268a2d419e0c056140981c1cc Mon Sep 17 00:00:00 2001 From: mshanemc Date: Wed, 5 Jun 2024 13:21:28 -0500 Subject: [PATCH 25/28] style: todo for mutation bug --- src/ux/table.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ux/table.ts b/src/ux/table.ts index b5822810d..d30a5995c 100644 --- a/src/ux/table.ts +++ b/src/ux/table.ts @@ -201,7 +201,7 @@ class Table> { '=' ) ); - + // TODO: avoid mutating the passed in options to prevent sideeffects where this table changes the options to other tables options.rowStart = '| '; } From ad634d19a74db6c3c7bd012fb0db8bf9d0857dfa Mon Sep 17 00:00:00 2001 From: mshanemc Date: Wed, 5 Jun 2024 13:37:55 -0500 Subject: [PATCH 26/28] test: try slow integration test --- test/integration/ux/table.integration.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/integration/ux/table.integration.ts b/test/integration/ux/table.integration.ts index ce342efe8..887c7c36f 100644 --- a/test/integration/ux/table.integration.ts +++ b/test/integration/ux/table.integration.ts @@ -32,8 +32,7 @@ describe('table', () => { expect(output).to.include('ID'); }); - // skip because it's too slow - it.skip('does not exceed stack depth on very tall, wide tables', () => { + it('does not exceed stack depth on very tall, wide tables', () => { const columnsLength = 100; const row = Object.fromEntries(Array.from({ length: columnsLength }).map((_, i) => [`col${i}`, 'foo'])); const data = Array.from({ length: 150_000 }).fill(row) as Array>; From 7832d2b4bed6f757f8e2dc2318507e4a8e4b22fb Mon Sep 17 00:00:00 2001 From: mshanemc Date: Wed, 5 Jun 2024 13:43:59 -0500 Subject: [PATCH 27/28] chore: sfdx-core bump for xnuts --- package.json | 4 +- yarn.lock | 133 +++++++++++++++++++++++---------------------------- 2 files changed, 63 insertions(+), 74 deletions(-) diff --git a/package.json b/package.json index 7fa385e06..a7ff27094 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "@inquirer/confirm": "^3.1.9", "@inquirer/password": "^2.1.9", "@oclif/core": "^4", - "@salesforce/core": "^7.3.9", + "@salesforce/core": "^7.3.10", "@salesforce/kit": "^3.1.2", "@salesforce/ts-types": "^2.0.9", "ansis": "^3.1.1", @@ -59,8 +59,8 @@ }, "devDependencies": { "@inquirer/type": "^1.3.3", - "@types/cli-progress": "^3.11.5", "@salesforce/dev-scripts": "^9.1.2", + "@types/cli-progress": "^3.11.5", "eslint-plugin-sf-plugin": "^1.18.5", "ts-node": "^10.9.2", "typescript": "^5.4.5" diff --git a/yarn.lock b/yarn.lock index d8ef445ac..a71dbd487 100644 --- a/yarn.lock +++ b/yarn.lock @@ -614,16 +614,16 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@salesforce/core@^7.3.9": - version "7.3.9" - resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-7.3.9.tgz#8abe2b3e2393989d11e92b7a6b96043fc9d5b9c8" - integrity sha512-eJqDiA5b7wU50Ee/xjmGzSnHrNVJ8S77B7enfX30gm7gxU3i3M3QeBdiV6XAOPLSIL96DseofP6Tv6c+rljlKA== +"@salesforce/core@^7.3.10", "@salesforce/core@^7.3.9": + version "7.3.10" + resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-7.3.10.tgz#83da85c4e93ca625e2c13118aad9c1df2931bc0f" + integrity sha512-kEKoqkmhWNoiucAE3Ylv6FpC4iVgk4aE0dmcwSmNrMjxSbtjQJGUybprfO/itrLJv+56eM7/4FARQQ2gDbRzQQ== dependencies: "@jsforce/jsforce-node" "^3.2.0" "@salesforce/kit" "^3.1.1" "@salesforce/schemas" "^1.9.0" "@salesforce/ts-types" "^2.0.9" - ajv "^8.13.0" + ajv "^8.15.0" change-case "^4.1.2" faye "^1.4.0" form-data "^4.0.0" @@ -631,11 +631,11 @@ jsonwebtoken "9.0.2" jszip "3.10.1" pino "^8.21.0" - pino-abstract-transport "^1.1.0" + pino-abstract-transport "^1.2.0" pino-pretty "^10.3.1" proper-lockfile "^4.1.2" semver "^7.6.2" - ts-retry-promise "^0.7.1" + ts-retry-promise "^0.8.1" "@salesforce/dev-config@^4.1.0": version "4.1.0" @@ -799,9 +799,9 @@ undici-types "~5.26.4" "@types/node@^18.15.3", "@types/node@^18.19.32": - version "18.19.33" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.33.tgz#98cd286a1b8a5e11aa06623210240bcc28e95c48" - integrity sha512-NR9+KrpSajr2qBVp/Yt5TU/rp+b5Mayi3+OlMlcg2cVCfRmcG5PWZ7S4+MG9PZ5gWBoc9Pd0BKSRViuBCRPu0A== + version "18.19.34" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.34.tgz#c3fae2bbbdb94b4a52fe2d229d0dccce02ef3d27" + integrity sha512-eXF4pfBNV5DAMKGbI02NnDtWrQ40hAN558/2vvS4gMpMIxaf6JmD7YjnZbq0Q9TDSSkKBamime8ewRoomHdt4g== dependencies: undici-types "~5.26.4" @@ -985,10 +985,10 @@ ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.11.0, ajv@^8.13.0: - version "8.13.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.13.0.tgz#a3939eaec9fb80d217ddf0c3376948c023f28c91" - integrity sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA== +ajv@^8.11.0, ajv@^8.15.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.16.0.tgz#22e2a92b94f005f7e0f9c9d39652ef0b8f6f0cb4" + integrity sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw== dependencies: fast-deep-equal "^3.1.3" json-schema-traverse "^1.0.0" @@ -1650,14 +1650,14 @@ csprng@*: sequin "*" csv-parse@^5.5.2: - version "5.5.5" - resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-5.5.5.tgz#68a271a9092877b830541805e14c8a80e6a22517" - integrity sha512-erCk7tyU3yLWAhk6wvKxnyPtftuy/6Ak622gOO7BCJ05+TYffnPCJF905wmOQm+BpkX54OdAl8pveJwUdpnCXQ== + version "5.5.6" + resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-5.5.6.tgz#0d726d58a60416361358eec291a9f93abe0b6b1a" + integrity sha512-uNpm30m/AGSkLxxy7d9yRXpJQFrZzVWLFBkS+6ngPcZkw/5k3L/jjFuj7tVnEpRn+QgmiXr21nDlhCiUK4ij2A== csv-stringify@^6.4.4: - version "6.4.6" - resolved "https://registry.yarnpkg.com/csv-stringify/-/csv-stringify-6.4.6.tgz#9ccf87cb8b017c96673a9fa061768c8ba83e8b98" - integrity sha512-h2V2XZ3uOTLilF5dPIptgUfN/o2ia/80Ie0Lly18LAnw5s8Eb7kt8rfxSUy24AztJZas9f6DPZpVlzDUtFt/ag== + version "6.5.0" + resolved "https://registry.yarnpkg.com/csv-stringify/-/csv-stringify-6.5.0.tgz#7b1491893c917e018a97de9bf9604e23b88647c2" + integrity sha512-edlXFVKcUx7r8Vx5zQucsuMg4wb/xT6qyz+Sr1vnLrdXqlLD1+UKyWNyZ9zn6mUW1ewmGxrpVwAcChGF0HQ/2Q== dargs@^7.0.0: version "7.0.0" @@ -1669,7 +1669,14 @@ dateformat@^4.6.3: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== -debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + +debug@4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -1683,13 +1690,6 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.3.5: - version "4.3.5" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" - integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== - dependencies: - ms "2.1.2" - decamelize-keys@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.1.tgz#04a2d523b2f18d80d0158a43b895d56dff8d19d8" @@ -2244,27 +2244,16 @@ extend@^3.0.2: integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== fast-copy@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.1.tgz#9e89ef498b8c04c1cd76b33b8e14271658a732aa" - integrity sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA== + version "3.0.2" + resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.2.tgz#59c68f59ccbcac82050ba992e0d5c389097c9d35" + integrity sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ== 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" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.11: - version "3.3.1" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" - integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-glob@^3.2.9: +fast-glob@^3.2.11, fast-glob@^3.2.9: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== @@ -2286,9 +2275,9 @@ fast-levenshtein@^2.0.6: integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fast-redact@^3.1.1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.3.0.tgz#7c83ce3a7be4898241a46560d51de10f653f7634" - integrity sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ== + version "3.5.0" + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.5.0.tgz#e9ea02f7e57d0cd8438180083e93077e496285e4" + integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A== fast-safe-stringify@^2.1.1: version "2.1.1" @@ -3895,9 +3884,9 @@ object.values@^1.1.7: es-abstract "^1.22.1" on-exit-leak-free@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz#5c703c968f7e7f851885f6459bf8a8a57edc9cc4" - integrity sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w== + version "2.1.2" + resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8" + integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA== once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" @@ -4081,7 +4070,7 @@ picomatch@^3.0.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-3.0.1.tgz#817033161def55ec9638567a2f3bbc876b3e7516" integrity sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag== -pino-abstract-transport@^1.0.0, pino-abstract-transport@^1.1.0, pino-abstract-transport@^1.2.0: +pino-abstract-transport@^1.0.0, pino-abstract-transport@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz#97f9f2631931e242da531b5c66d3079c12c9d1b5" integrity sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q== @@ -4211,9 +4200,9 @@ pump@^3.0.0: once "^1.3.1" punycode@^2.1.0, punycode@^2.1.1: - version "2.3.0" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== q@^1.5.1: version "1.5.1" @@ -4276,9 +4265,9 @@ readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.4.0: util-deprecate "^1.0.1" readable-stream@^4.0.0: - version "4.4.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.4.2.tgz#e6aced27ad3b9d726d8308515b9a1b98dc1b9d13" - integrity sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA== + version "4.5.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" + integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== dependencies: abort-controller "^3.0.0" buffer "^6.0.3" @@ -4459,9 +4448,9 @@ safe-stable-stringify@^2.3.1: integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g== sax@>=0.6.0: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + version "1.4.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" + integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== secure-json-parse@^2.4.0: version "2.7.0" @@ -4629,9 +4618,9 @@ snake-case@^3.0.4: tslib "^2.0.3" sonic-boom@^3.0.0, sonic-boom@^3.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.7.0.tgz#b4b7b8049a912986f4a92c51d4660b721b11f2f2" - integrity sha512-IudtNvSqA/ObjN97tfgNmOKyDOs4dNcg4cUUsHDebqsgb8wGBBwb31LIgShNO8fye0dFI52X1+tFoKKI6Rq1Gg== + version "3.8.1" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.8.1.tgz#d5ba8c4e26d6176c9a1d14d549d9ff579a163422" + integrity sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg== dependencies: atomic-sleep "^1.0.0" @@ -4918,9 +4907,9 @@ to-regex-range@^5.0.1: is-number "^7.0.0" tough-cookie@*: - version "4.1.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" - integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== + version "4.1.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" + integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag== dependencies: psl "^1.1.33" punycode "^2.1.1" @@ -4961,10 +4950,10 @@ ts-node@^10.8.1, ts-node@^10.9.2: v8-compile-cache-lib "^3.0.1" yn "3.1.1" -ts-retry-promise@^0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/ts-retry-promise/-/ts-retry-promise-0.7.1.tgz#176d6eee6415f07b6c7c286d3657355e284a6906" - integrity sha512-NhHOCZ2AQORvH42hOPO5UZxShlcuiRtm7P2jIq2L2RY3PBxw2mLnUsEdHrIslVBFya1v5aZmrR55lWkzo13LrQ== +ts-retry-promise@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/ts-retry-promise/-/ts-retry-promise-0.8.1.tgz#ba90eb07cb03677fcbf78fe38e94c9183927e154" + integrity sha512-+AHPUmAhr5bSRRK5CurE9kNH8gZlEHnCgusZ0zy2bjfatUBDX0h6vGQjiT0YrGwSDwRZmU+bapeX6mj55FOPvg== tsconfig-paths@^3.15.0: version "3.15.0" @@ -4977,9 +4966,9 @@ tsconfig-paths@^3.15.0: strip-bom "^3.0.0" tslib@^2.0.3, tslib@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" - integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== tunnel-agent@*: version "0.6.0" From 0ea459c750f0a7dd6f477c46eb20bc58a17282a9 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Wed, 5 Jun 2024 13:44:22 -0500 Subject: [PATCH 28/28] test: re-skip --- test/integration/ux/table.integration.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/integration/ux/table.integration.ts b/test/integration/ux/table.integration.ts index 887c7c36f..ce342efe8 100644 --- a/test/integration/ux/table.integration.ts +++ b/test/integration/ux/table.integration.ts @@ -32,7 +32,8 @@ describe('table', () => { expect(output).to.include('ID'); }); - it('does not exceed stack depth on very tall, wide tables', () => { + // skip because it's too slow + it.skip('does not exceed stack depth on very tall, wide tables', () => { const columnsLength = 100; const row = Object.fromEntries(Array.from({ length: columnsLength }).map((_, i) => [`col${i}`, 'foo'])); const data = Array.from({ length: 150_000 }).fill(row) as Array>;