From 71fa21fd7e4115cabadb73c901111c792547e16d Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Mon, 23 Sep 2024 14:19:33 -0600 Subject: [PATCH 1/7] feat!: use @oclif/table BREAKING CHANGE: new table API --- .github/workflows/test.yml | 21 -- package.json | 3 +- src/exported.ts | 1 + src/sfCommand.ts | 5 +- src/ux/table.ts | 331 +++-------------------- src/ux/ux.ts | 30 +- test/integration/ux/table.integration.ts | 47 ---- test/unit/stubUx.test.ts | 13 +- test/unit/ux/table.test.ts | 186 ------------- test/unit/ux/ux.test.ts | 53 +++- yarn.lock | 316 +++++++++++++++++++--- 11 files changed, 384 insertions(+), 622 deletions(-) delete mode 100644 test/integration/ux/table.integration.ts delete mode 100644 test/unit/ux/table.test.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 32eb1bfe..0ce84ee5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,27 +17,6 @@ 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 f983c778..a4a6f72d 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "prepack": "sf-prepack", "prepare": "sf-install", "test": "wireit", - "test:integration": "mocha test/**/*.integration.ts --timeout 30000", "test:only": "wireit" }, "exports": { @@ -47,6 +46,7 @@ "@inquirer/confirm": "^3.1.22", "@inquirer/password": "^2.2.0", "@oclif/core": "^4.0.19", + "@oclif/table": "^0.1.2", "@salesforce/core": "^8.5.1", "@salesforce/kit": "^3.2.2", "@salesforce/ts-types": "^2.0.12", @@ -59,6 +59,7 @@ }, "devDependencies": { "@inquirer/type": "^1.5.2", + "@oclif/test": "^4.0.9", "@salesforce/dev-scripts": "^10.2.10", "@types/cli-progress": "^3.11.6", "eslint-plugin-sf-plugin": "^1.20.7", diff --git a/src/exported.ts b/src/exported.ts index b2343008..cb711af0 100644 --- a/src/exported.ts +++ b/src/exported.ts @@ -9,6 +9,7 @@ export { toHelpSection, parseVarArgs } from './util.js'; export { Progress } from './ux/progress.js'; export { Spinner } from './ux/spinner.js'; export { Ux } from './ux/ux.js'; +export { convertToNewTableAPI } from './ux/table.js'; export { StandardColors } from './ux/standardColors.js'; export { SfCommand, SfCommandInterface } from './sfCommand.js'; diff --git a/src/sfCommand.ts b/src/sfCommand.ts index b57056ee..1ffa1137 100644 --- a/src/sfCommand.ts +++ b/src/sfCommand.ts @@ -17,6 +17,7 @@ import { StructuredMessage, } from '@salesforce/core'; import type { AnyJson } from '@salesforce/ts-types'; +import { TableOptions } from '@oclif/table'; import { Progress } from './ux/progress.js'; import { Spinner } from './ux/spinner.js'; import { Ux } from './ux/ux.js'; @@ -235,8 +236,8 @@ export abstract class SfCommand extends Command { /** * Display a table on the console. Will automatically be suppressed when --json flag is present. */ - public table(data: R[], columns: Ux.Table.Columns, options?: Ux.Table.Options): void { - this.ux.table(data, columns, options); + public table>(options: TableOptions): void { + this.ux.table(options); } /** diff --git a/src/ux/table.ts b/src/ux/table.ts index d30a5995..c7790819 100644 --- a/src/ux/table.ts +++ b/src/ux/table.ts @@ -4,303 +4,18 @@ * 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 ansis from 'ansis'; -import { orderBy } from 'natural-orderby'; -import sliceAnsi from 'slice-ansi'; -import sw from 'string-width'; -import { ux } from '@oclif/core'; +import { TableOptions } from '@oclif/table'; -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(); - if (width < 1) { - return 80; - } - - if (width < 40) { - return 40; - } - - return width; -} - -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 }>; - - private options: Options & { printLine: (s: unknown) => void }; - - private data: Array>; - - public constructor(data: T[], columns: Columns, options: Options = {}) { - // assign columns - 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] ?? ''); - 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, extended, filter, printLine, sort, title } = options; - this.options = { - columns: cols, - extended, - filter, - 'no-header': options['no-header'] ?? false, - 'no-truncate': options['no-truncate'] ?? false, - printLine: printLine ?? ((s: string): void => ux.stdout(s)), - rowStart: ' ', - sort, - title, - }; - - // build table rows from input array data - 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) { - // 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) => { - 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: Record): unknown => - 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; - } - - public display(): void { - 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 maybeShorten = (): void => { - // 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; - } - } - }; - - maybeShorten(); - - // print table title - if (options.title) { - options.printLine(options.title); - // print title divider - options.printLine( - ''.padEnd( - columns.reduce((sum, col) => sum + col.width, 1), - '=' - ) - ); - // TODO: avoid mutating the passed in options to prevent sideeffects where this table changes the options to other tables - 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); - } - - if (headers) options.printLine(ansis.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); - } - - if (dividers) options.printLine(ansis.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[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[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 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()); - } -} - -export function table>(data: T[], columns: Columns, options: Options = {}): void { - new Table(data, columns, options).display(); -} - -export type Column> = { +type Column> = { extended: boolean; header: string; minWidth: number; get(row: T): unknown; }; -export type Columns> = { [key: string]: Partial> }; +type Columns> = { [key: string]: Partial> }; -export type Options = { +type Options = { columns?: string; extended?: boolean; filter?: string; @@ -312,12 +27,32 @@ export type Options = { 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.split('\n'); - return Math.max(previous, manyLines.length > 1 ? Math.max(...manyLines.map((r: string) => sw(r))) : sw(d)); - }, 0); +/** + * Converts inputs to previous table API to the new table API. + * + * Note that the following options will not be converted: + * - 'extended' + * - 'filter' + * - 'sort' + * - 'no-header' + * - 'no-truncate' + * - 'row-start' + * - 'print-line' + * + * @deprecated Please use the new table API directly. + */ +export function convertToNewTableAPI>( + data: T[], + columns: Columns, + options?: Options +): TableOptions> { + const cols = Object.entries(columns).map(([key, opts]) => { + if (opts.header) return { key, name: opts.header }; + return key; + }); + const d = data.map((row) => + Object.fromEntries(Object.entries(columns).map(([key, { get }]) => [key, get ? get(row) : row[key]])) + ) as Array>; + + return { data: d, title: options?.title, borderStyle: 'headers-only-with-underline', columns: cols }; +} diff --git a/src/ux/ux.ts b/src/ux/ux.ts index 18dca523..871f6514 100644 --- a/src/ux/ux.ts +++ b/src/ux/ux.ts @@ -9,9 +9,10 @@ import ansis from 'ansis'; import { ux } from '@oclif/core'; import { AnyJson } from '@salesforce/ts-types'; import terminalLink from 'terminal-link'; +import { printTable, TableOptions } from '@oclif/table'; +import { env } from '@salesforce/kit'; 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'; /** @@ -72,12 +73,21 @@ export class Ux extends UxBase { /** * Display a table to the console. This will be automatically suppressed if output is disabled. * - * @param data Data to be displayed - * @param columns Columns to display the data in - * @param options Options for how the table should be displayed + * @param options Table properties */ - public table(data: T[], columns: Ux.Table.Columns, options?: Ux.Table.Options): void { - this.maybeNoop(() => table(data, columns, { 'no-truncate': true, ...options })); + public table>(options: TableOptions): void { + this.maybeNoop(() => + printTable({ + ...options, + // Don't allow anyone to override these properties + borderStyle: 'headers-only-with-underline', + noStyle: env.getBoolean('SF_NO_TABLE_STYLE', false), + headerOptions: { + ...options.headerOptions, + formatter: 'capitalCase', + }, + }) + ); } /** @@ -131,11 +141,3 @@ export class Ux extends UxBase { this.maybeNoop(() => ux.stdout(ansis.dim('=== ') + ansis.bold(text) + '\n')); } } - -export namespace Ux { - export namespace Table { - export type Data = Record; - export type Columns = TableColumns; - export type Options = TableOptions; - } -} diff --git a/test/integration/ux/table.integration.ts b/test/integration/ux/table.integration.ts deleted file mode 100644 index ce342efe..00000000 --- a/test/integration/ux/table.integration.ts +++ /dev/null @@ -1,47 +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 { expect } from 'chai'; -import stripAnsi from 'strip-ansi'; -import { table } from '../../../src/ux/table.js'; - -describe('table', () => { - let output = ''; - function printLine(line: string) { - output += stripAnsi(line) + '\n'; - } - - afterEach(() => { - output = ''; - }); - - 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/unit/stubUx.test.ts b/test/unit/stubUx.test.ts index 73c10d54..9bf085bd 100644 --- a/test/unit/stubUx.test.ts +++ b/test/unit/stubUx.test.ts @@ -13,11 +13,6 @@ import { stubUx, stubSfCommandUx, SfCommand, Ux, stubSpinner, Flags } from '../. const TABLE_DATA = Array.from({ length: 10 }).fill({ id: '123', name: 'foo', value: 'bar' }) as Array< Record >; -const TABLE_COLUMNS = { - id: { header: 'ID' }, - name: {}, - value: { header: 'TEST' }, -}; class Cmd extends SfCommand { public static flags = { @@ -138,10 +133,10 @@ class Cmd extends SfCommand { private runTable(): void { switch (this.flags.method) { case 'SfCommand': - this.table(TABLE_DATA, TABLE_COLUMNS); + this.table({ data: TABLE_DATA }); break; case 'Ux': - new Ux().table(TABLE_DATA, TABLE_COLUMNS); + new Ux().table({ data: TABLE_DATA }); break; default: throw new Error(`Invalid method: ${this.flags.method}`); @@ -263,7 +258,7 @@ describe('Ux Stubs', () => { it('should stub table', async () => { await Cmd.run(['--table', '--method=SfCommand']); - expect(sfCommandUxStubs.table.firstCall.args).to.deep.equal([TABLE_DATA, TABLE_COLUMNS]); + expect(sfCommandUxStubs.table.firstCall.args).to.deep.equal([{ data: TABLE_DATA }]); }); it('should stub url', async () => { @@ -307,7 +302,7 @@ describe('Ux Stubs', () => { it('should stub table', async () => { await Cmd.run(['--table', '--method=Ux']); - expect(uxStubs.table.firstCall.args).to.deep.equal([TABLE_DATA, TABLE_COLUMNS]); + expect(uxStubs.table.firstCall.args).to.deep.equal([{ data: TABLE_DATA }]); }); it('should stub url', async () => { diff --git a/test/unit/ux/table.test.ts b/test/unit/ux/table.test.ts deleted file mode 100644 index 15adaa20..00000000 --- a/test/unit/ux/table.test.ts +++ /dev/null @@ -1,186 +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 { 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 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`); - }); - }); - - 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`); - }); - }); -}); diff --git a/test/unit/ux/ux.test.ts b/test/unit/ux/ux.test.ts index 2b592ea9..f0aa45db 100644 --- a/test/unit/ux/ux.test.ts +++ b/test/unit/ux/ux.test.ts @@ -8,7 +8,9 @@ import { expect } from 'chai'; import sinon from 'sinon'; import { ux as coreUx } from '@oclif/core'; +import { captureOutput } from '@oclif/test'; import { Ux } from '../../../src/ux/ux.js'; +import { convertToNewTableAPI } from '../../../src/ux/table.js'; describe('Ux', () => { let sandbox: sinon.SinonSandbox; @@ -24,20 +26,47 @@ describe('Ux', () => { }); describe('table', () => { - it('should log a table', () => { - const ux = new Ux(); - 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 '], - ]); + it('should log a table', async () => { + const { stdout } = await captureOutput(async () => { + const ux = new Ux(); + ux.table({ data: [{ key: 'foo', value: 'bar' }], title: 'Title' }); + }); + expect(stdout.replaceAll(' \n', '\n')).to.equal(`Title + Key Value + ───────────── + foo bar +`); }); - 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(stdoutStub.callCount).to.equal(0); + it('should not log anything if output is not enabled', async () => { + const { stdout } = await captureOutput(async () => { + const ux = new Ux({ jsonEnabled: true }); + ux.table({ data: [{ key: 'foo', value: 'bar' }], title: 'Title' }); + }); + expect(stdout).to.equal(''); + }); + }); + + describe('table (with convertToNewTableAPI)', () => { + it('should log a table', async () => { + const { stdout } = await captureOutput(async () => { + const ux = new Ux(); + const opts = convertToNewTableAPI([{ key: 'foo', value: 'bar' }], { key: {}, value: {} }, { title: 'Title' }); + ux.table(opts); + }); + expect(stdout.replaceAll(' \n', '\n')).to.equal(`Title + Key Value + ───────────── + foo bar +`); + }); + + it('should not log anything if output is not enabled', async () => { + const { stdout } = await captureOutput(async () => { + const ux = new Ux({ jsonEnabled: true }); + ux.table(convertToNewTableAPI([{ key: 'foo', value: 'bar' }], { key: {}, value: {} })); + }); + expect(stdout).to.equal(''); }); }); diff --git a/yarn.lock b/yarn.lock index 8c6f5d4a..cba50070 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,14 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== +"@alcalzone/ansi-tokenize@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz#9f89839561325a8e9a0c32360b8d17e48489993f" + integrity sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw== + dependencies: + ansi-styles "^6.2.1" + is-fullwidth-code-point "^4.0.0" + "@ampproject/remapping@^2.2.0": version "2.2.1" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" @@ -550,6 +558,29 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@oclif/core@^4": + version "4.0.22" + resolved "https://registry.yarnpkg.com/@oclif/core/-/core-4.0.22.tgz#acb233c0c18ff4f365caca15e29e2807a4325709" + integrity sha512-aXM2O4g7f+kPNzhhOfqGOVRVYDxTVrH7Y720MuH0Twq5WHMxI4XwntnyBaRscoCPG6FWhItZLtiZxsvaUdupGg== + dependencies: + ansi-escapes "^4.3.2" + ansis "^3.3.2" + clean-stack "^3.0.1" + cli-spinners "^2.9.2" + debug "^4.3.7" + ejs "^3.1.10" + get-package-type "^0.1.0" + globby "^11.1.0" + indent-string "^4.0.0" + is-wsl "^2.2.0" + lilconfig "^3.1.2" + minimatch "^9.0.5" + string-width "^4.2.3" + supports-color "^8" + widest-line "^3.1.0" + wordwrap "^1.0.0" + wrap-ansi "^7.0.0" + "@oclif/core@^4.0.19": version "4.0.19" resolved "https://registry.yarnpkg.com/@oclif/core/-/core-4.0.19.tgz#e52ba83c3eaa75c580c567067ed86b5bd4cadbe6" @@ -573,6 +604,30 @@ wordwrap "^1.0.0" wrap-ansi "^7.0.0" +"@oclif/table@^0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@oclif/table/-/table-0.1.2.tgz#de0f0edaa5d68ce8310117c8ad262688cb9e233a" + integrity sha512-IfqGP4tkwbM4KoplwiMzUEr5T7Vh8Mda/EHrGK0YBjFIqCjJzj52lQ4pHAGoNbXN64IAS82RUKr9FxDH3wAOJg== + dependencies: + "@oclif/core" "^4" + "@types/react" "^18.3.3" + change-case "^5.4.4" + cli-truncate "^4.0.0" + ink "^5.0.1" + natural-orderby "^3.0.2" + object-hash "^3.0.0" + react "^18.3.1" + strip-ansi "^7.1.0" + wrap-ansi "^9.0.0" + +"@oclif/test@^4.0.9": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@oclif/test/-/test-4.0.9.tgz#c4b4b4878911489a79f296a15448e76d860b39d2" + integrity sha512-xDGBFBNE6ckoBT9EhMi6ZvwAaEeJRGvRmn2qZWujJl9EJ56a72KHZsvTJVgl2p/AQ2vZ1UH06YZ440GOnjExzQ== + dependencies: + ansis "^3.3.2" + debug "^4.3.6" + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -786,6 +841,19 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== +"@types/prop-types@*": + version "15.7.13" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451" + integrity sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA== + +"@types/react@^18.3.3": + version "18.3.8" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.8.tgz#1672ab19993f8aca7c7dc844c07d5d9e467d5a79" + integrity sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + "@types/semver@^7.5.0": version "7.5.8" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" @@ -1040,6 +1108,13 @@ ansi-escapes@^5.0.0: dependencies: type-fest "^1.0.2" +ansi-escapes@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-7.0.0.tgz#00fc19f491bbb18e1d481b97868204f92109bfe7" + integrity sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw== + dependencies: + environment "^1.0.0" + ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -1064,7 +1139,7 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-styles@^6.1.0, ansi-styles@^6.2.1: +ansi-styles@^6.0.0, ansi-styles@^6.1.0, ansi-styles@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== @@ -1219,6 +1294,11 @@ atomic-sleep@^1.0.0: resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== +auto-bind@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/auto-bind/-/auto-bind-5.0.1.tgz#50d8e63ea5a1dddcb5e5e36451c1a8266ffbb2ae" + integrity sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg== + available-typed-arrays@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" @@ -1411,7 +1491,7 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^5.0.0: +chalk@^5.0.0, chalk@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== @@ -1434,6 +1514,11 @@ change-case@^4.1.2: snake-case "^3.0.4" tslib "^2.0.3" +change-case@^5.4.4: + version "5.4.4" + resolved "https://registry.yarnpkg.com/change-case/-/change-case-5.4.4.tgz#0d52b507d8fb8f204343432381d1a6d7bff97a02" + integrity sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w== + check-error@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" @@ -1480,6 +1565,18 @@ clean-stack@^3.0.1: dependencies: escape-string-regexp "4.0.0" +cli-boxes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-3.0.0.tgz#71a10c716feeba005e4504f36329ef0b17cf3145" + integrity sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g== + +cli-cursor@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea" + integrity sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg== + dependencies: + restore-cursor "^4.0.0" + cli-progress@^3.12.0: version "3.12.0" resolved "https://registry.yarnpkg.com/cli-progress/-/cli-progress-3.12.0.tgz#807ee14b66bcc086258e444ad0f19e7d42577942" @@ -1492,6 +1589,14 @@ cli-spinners@^2.9.2: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== +cli-truncate@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-4.0.0.tgz#6cc28a2924fee9e25ce91e973db56c7066e6172a" + integrity sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA== + dependencies: + slice-ansi "^5.0.0" + string-width "^7.0.0" + cli-width@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-4.1.0.tgz#42daac41d3c254ef38ad8ac037672130173691c5" @@ -1524,6 +1629,13 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" +code-excerpt@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/code-excerpt/-/code-excerpt-4.0.0.tgz#2de7d46e98514385cb01f7b3b741320115f4c95e" + integrity sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA== + dependencies: + convert-to-spaces "^2.0.1" + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -1629,6 +1741,11 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +convert-to-spaces@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz#61a6c98f8aa626c16b296b862a91412a33bceb6b" + integrity sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ== + core-js-compat@^3.34.0: version "3.36.1" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.36.1.tgz#1818695d72c99c25d621dca94e6883e190cea3c8" @@ -1677,6 +1794,11 @@ csprng@*: dependencies: sequin "*" +csstype@^3.0.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + csv-parse@^5.5.2: version "5.5.6" resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-5.5.6.tgz#0d726d58a60416361358eec291a9f93abe0b6b1a" @@ -1711,6 +1833,13 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.3.6, debug@^4.3.7: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + decamelize-keys@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.1.tgz#04a2d523b2f18d80d0158a43b895d56dff8d19d8" @@ -1897,6 +2026,11 @@ entities@^4.2.0, entities@^4.4.0, entities@^4.5.0: resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== +environment@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/environment/-/environment-1.1.0.tgz#8e86c66b180f363c7ab311787e0259665f45a9f1" + integrity sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -1999,6 +2133,11 @@ escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + eslint-config-prettier@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f" @@ -2842,6 +2981,11 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== +indent-string@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-5.0.0.tgz#4fd2980fccaf8622d14c64d694f4cf33c81951a5" + integrity sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -2860,6 +3004,36 @@ ini@^1.3.4: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +ink@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ink/-/ink-5.0.1.tgz#f2ef9796a3911830c3995dedd227ec84ae27de4b" + integrity sha512-ae4AW/t8jlkj/6Ou21H2av0wxTk8vrGzXv+v2v7j4in+bl1M5XRMVbfNghzhBokV++FjF8RBDJvYo+ttR9YVRg== + dependencies: + "@alcalzone/ansi-tokenize" "^0.1.3" + ansi-escapes "^7.0.0" + ansi-styles "^6.2.1" + auto-bind "^5.0.1" + chalk "^5.3.0" + cli-boxes "^3.0.0" + cli-cursor "^4.0.0" + cli-truncate "^4.0.0" + code-excerpt "^4.0.0" + indent-string "^5.0.0" + is-in-ci "^0.1.0" + lodash "^4.17.21" + patch-console "^2.0.0" + react-reconciler "^0.29.0" + scheduler "^0.23.0" + signal-exit "^3.0.7" + slice-ansi "^7.1.0" + stack-utils "^2.0.6" + string-width "^7.0.0" + type-fest "^4.8.3" + widest-line "^5.0.0" + wrap-ansi "^9.0.0" + ws "^8.15.0" + yoga-wasm-web "~0.3.3" + internal-slot@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" @@ -2951,6 +3125,11 @@ 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@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz#fae3167c729e7463f8461ce512b080a49268aa88" + integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== + is-fullwidth-code-point@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz#9609efced7c2f97da7b60145ef481c787c7ba704" @@ -2965,6 +3144,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-in-ci@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-in-ci/-/is-in-ci-0.1.0.tgz#5e07d6a02ec3a8292d3f590973357efa3fceb0d3" + integrity sha512-d9PXLEY0v1iJ64xLiQMJ51J128EYHAaOR4yZqQi8aHGfw6KgifM3/Viw1oZZ1GCVmb3gBuyhLyHj0HgR2DhSXQ== + is-negative-zero@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" @@ -3184,7 +3368,7 @@ joycon@^3.1.1: resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== -js-tokens@^4.0.0: +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== @@ -3487,7 +3671,7 @@ lodash.upperfirst@^4.3.1: resolved "https://registry.yarnpkg.com/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz#1365edf431480481ef0d1c68957a5ed99d49f7ce" integrity sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg== -lodash@^4.17.15: +lodash@^4.17.15, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -3500,6 +3684,13 @@ log-symbols@^4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" +loose-envify@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + loupe@^2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" @@ -3870,6 +4061,11 @@ nyc@^17.0.0: test-exclude "^6.0.0" yargs "^15.0.2" +object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + object-inspect@^1.13.1, object-inspect@^1.9.0: version "1.13.1" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" @@ -4037,6 +4233,11 @@ pascal-case@^3.1.2: no-case "^3.0.4" tslib "^2.0.3" +patch-console@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/patch-console/-/patch-console-2.0.0.tgz#9023f4665840e66f40e9ce774f904a63167433bb" + integrity sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA== + path-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/path-case/-/path-case-3.0.4.tgz#9168645334eb942658375c56f80b4c0cb5f82c6f" @@ -4276,6 +4477,21 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" +react-reconciler@^0.29.0: + version "0.29.2" + resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.29.2.tgz#8ecfafca63549a4f4f3e4c1e049dd5ad9ac3a54f" + integrity sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.2" + +react@^18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" + integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== + dependencies: + loose-envify "^1.1.0" + read-pkg-up@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" @@ -4429,6 +4645,14 @@ resolve@^1.1.6, resolve@^1.10.0, resolve@^1.22.4: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +restore-cursor@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-4.0.0.tgz#519560a4318975096def6e609d44100edaa4ccb9" + integrity sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + retry@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" @@ -4492,6 +4716,13 @@ sax@>=0.6.0: resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== +scheduler@^0.23.0, scheduler@^0.23.2: + version "0.23.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== + dependencies: + loose-envify "^1.1.0" + secure-json-parse@^2.4.0: version "2.7.0" resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862" @@ -4612,7 +4843,7 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" -signal-exit@^3.0.2, signal-exit@^3.0.3: +signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== @@ -4639,6 +4870,14 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slice-ansi@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-5.0.0.tgz#b73063c57aa96f9cd881654b15294d95d285c42a" + integrity sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ== + dependencies: + ansi-styles "^6.0.0" + is-fullwidth-code-point "^4.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" @@ -4743,16 +4982,14 @@ srcset@^5.0.0: resolved "https://registry.yarnpkg.com/srcset/-/srcset-5.0.0.tgz#9df6c3961b5b44a02532ce6ae4544832609e2e3f" integrity sha512-SqEZaAEhe0A6ETEa9O1IhSPC7MdvehZtCnTR0AftXk3QhY2UNgb+NApFOUPZILXk/YTDfFxMTNJOBpzrJsEdIA== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== +stack-utils@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" + escape-string-regexp "^2.0.0" -string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -4770,7 +5007,7 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" -string-width@^7.2.0: +string-width@^7.0.0, string-width@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.2.0.tgz#b5bb8e2165ce275d4d43476dd2700ad9091db6dc" integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ== @@ -4820,14 +5057,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -5073,6 +5303,11 @@ type-fest@^1.0.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== +type-fest@^4.8.3: + version "4.26.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.26.1.tgz#a4a17fa314f976dd3e6d6675ef6c775c16d7955e" + integrity sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg== + 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" @@ -5303,6 +5538,13 @@ widest-line@^3.1.0: dependencies: string-width "^4.0.0" +widest-line@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-5.0.0.tgz#b74826a1e480783345f0cd9061b49753c9da70d0" + integrity sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA== + dependencies: + string-width "^7.0.0" + wireit@^0.14.5: version "0.14.5" resolved "https://registry.yarnpkg.com/wireit/-/wireit-0.14.5.tgz#cd1c4136444c8dbe655f34f60fe2454a9e69d430" @@ -5324,7 +5566,7 @@ workerpool@^6.5.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -5342,15 +5584,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -5360,6 +5593,15 @@ wrap-ansi@^8.1.0: string-width "^5.0.1" strip-ansi "^7.0.1" +wrap-ansi@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-9.0.0.tgz#1a3dc8b70d85eeb8398ddfb1e4a02cd186e58b3e" + integrity sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q== + dependencies: + ansi-styles "^6.2.1" + string-width "^7.0.0" + strip-ansi "^7.1.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -5375,6 +5617,11 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" +ws@^8.15.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + xml2js@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.2.tgz#dd0b630083aa09c161e25a4d0901e2b2a929b499" @@ -5503,3 +5750,8 @@ yoctocolors-cjs@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz#f4b905a840a37506813a7acaa28febe97767a242" integrity sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA== + +yoga-wasm-web@~0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz#eb8e9fcb18e5e651994732f19a220cb885d932ba" + integrity sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA== From 47ab12d0d2f91d16017db8d44ef37f6ad7dbd182 Mon Sep 17 00:00:00 2001 From: Eric Willhoit Date: Tue, 8 Oct 2024 13:51:22 -0500 Subject: [PATCH 2/7] chore: fix test --- test/unit/ux/ux.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/unit/ux/ux.test.ts b/test/unit/ux/ux.test.ts index f0aa45db..b2dad615 100644 --- a/test/unit/ux/ux.test.ts +++ b/test/unit/ux/ux.test.ts @@ -35,6 +35,7 @@ describe('Ux', () => { Key Value ───────────── foo bar + `); }); @@ -58,6 +59,7 @@ describe('Ux', () => { Key Value ───────────── foo bar + `); }); From 537f4f6cf661603dc5296aab6dde48301ef5026d Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Thu, 10 Oct 2024 09:08:21 -0600 Subject: [PATCH 3/7] feat: allow borderStyle to be configured via env var --- src/ux/ux.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/ux/ux.ts b/src/ux/ux.ts index 871f6514..3891df0f 100644 --- a/src/ux/ux.ts +++ b/src/ux/ux.ts @@ -76,11 +76,32 @@ export class Ux extends UxBase { * @param options Table properties */ public table>(options: TableOptions): void { + const borderStyles = [ + 'all', + 'headers-only-with-outline', + 'headers-only-with-underline', + 'headers-only', + 'horizontal-with-outline', + 'horizontal', + 'none', + 'outline', + 'vertical-with-outline', + 'vertical', + ]; + const determineBorderStyle = (): TableOptions['borderStyle'] => { + const envVar = env.getString('SF_TABLE_BORDER_STYLE', 'headers-only-with-underline'); + if (borderStyles.includes(envVar)) { + return envVar as TableOptions['borderStyle']; + } + + return 'headers-only-with-underline'; + }; + this.maybeNoop(() => printTable({ ...options, // Don't allow anyone to override these properties - borderStyle: 'headers-only-with-underline', + borderStyle: determineBorderStyle(), noStyle: env.getBoolean('SF_NO_TABLE_STYLE', false), headerOptions: { ...options.headerOptions, From 2d5169d79a881048c3932bc9199929d7d87c79fb Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Thu, 10 Oct 2024 09:09:52 -0600 Subject: [PATCH 4/7] test: adjust number of newlines --- test/unit/ux/ux.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/unit/ux/ux.test.ts b/test/unit/ux/ux.test.ts index b2dad615..f0aa45db 100644 --- a/test/unit/ux/ux.test.ts +++ b/test/unit/ux/ux.test.ts @@ -35,7 +35,6 @@ describe('Ux', () => { Key Value ───────────── foo bar - `); }); @@ -59,7 +58,6 @@ describe('Ux', () => { Key Value ───────────── foo bar - `); }); From 2fe71ca12871e884252ddbd63c87d4afb4a20eb4 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Thu, 10 Oct 2024 09:20:10 -0600 Subject: [PATCH 5/7] test: trim tables for testing output --- test/unit/ux/ux.test.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/unit/ux/ux.test.ts b/test/unit/ux/ux.test.ts index f0aa45db..6dbf0732 100644 --- a/test/unit/ux/ux.test.ts +++ b/test/unit/ux/ux.test.ts @@ -31,11 +31,10 @@ describe('Ux', () => { const ux = new Ux(); ux.table({ data: [{ key: 'foo', value: 'bar' }], title: 'Title' }); }); - expect(stdout.replaceAll(' \n', '\n')).to.equal(`Title + expect(stdout.replaceAll(' \n', '\n').trimEnd()).to.equal(`Title Key Value ───────────── - foo bar -`); + foo bar`); }); it('should not log anything if output is not enabled', async () => { @@ -54,11 +53,10 @@ describe('Ux', () => { const opts = convertToNewTableAPI([{ key: 'foo', value: 'bar' }], { key: {}, value: {} }, { title: 'Title' }); ux.table(opts); }); - expect(stdout.replaceAll(' \n', '\n')).to.equal(`Title + expect(stdout.replaceAll(' \n', '\n').trimEnd()).to.equal(`Title Key Value ───────────── - foo bar -`); + foo bar`); }); it('should not log anything if output is not enabled', async () => { From c0da503c0f453ca35ebdb02d9b00b03f0cbbb0a7 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Thu, 10 Oct 2024 10:25:10 -0600 Subject: [PATCH 6/7] fix: change default table style --- src/ux/ux.ts | 6 ++++-- test/unit/ux/ux.test.ts | 16 ++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/ux/ux.ts b/src/ux/ux.ts index 3891df0f..7abca9f7 100644 --- a/src/ux/ux.ts +++ b/src/ux/ux.ts @@ -88,13 +88,15 @@ export class Ux extends UxBase { 'vertical-with-outline', 'vertical', ]; + + const defaultStyle = 'vertical-with-outline'; const determineBorderStyle = (): TableOptions['borderStyle'] => { - const envVar = env.getString('SF_TABLE_BORDER_STYLE', 'headers-only-with-underline'); + const envVar = env.getString('SF_TABLE_BORDER_STYLE', defaultStyle); if (borderStyles.includes(envVar)) { return envVar as TableOptions['borderStyle']; } - return 'headers-only-with-underline'; + return defaultStyle; }; this.maybeNoop(() => diff --git a/test/unit/ux/ux.test.ts b/test/unit/ux/ux.test.ts index 6dbf0732..0bcddffc 100644 --- a/test/unit/ux/ux.test.ts +++ b/test/unit/ux/ux.test.ts @@ -32,9 +32,11 @@ describe('Ux', () => { ux.table({ data: [{ key: 'foo', value: 'bar' }], title: 'Title' }); }); expect(stdout.replaceAll(' \n', '\n').trimEnd()).to.equal(`Title - Key Value - ───────────── - foo bar`); +┌─────┬───────┐ +│ Key │ Value │ +├─────┼───────┤ +│ foo │ bar │ +└─────┴───────┘`); }); it('should not log anything if output is not enabled', async () => { @@ -54,9 +56,11 @@ describe('Ux', () => { ux.table(opts); }); expect(stdout.replaceAll(' \n', '\n').trimEnd()).to.equal(`Title - Key Value - ───────────── - foo bar`); +┌─────┬───────┐ +│ Key │ Value │ +├─────┼───────┤ +│ foo │ bar │ +└─────┴───────┘`); }); it('should not log anything if output is not enabled', async () => { From 142abd2a4a9084a5ca28f13269ae28e21edbb252 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Thu, 17 Oct 2024 09:20:29 -0600 Subject: [PATCH 7/7] chore: bump oclif/table --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 420d5ad7..5c0595b1 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "dependencies": { "@inquirer/confirm": "^3.1.22", "@inquirer/password": "^2.2.0", - "@oclif/table": "^0.1.17", + "@oclif/table": "^0.1.19", "@oclif/core": "^4.0.27", "@salesforce/core": "^8.5.1", "@salesforce/kit": "^3.2.3", diff --git a/yarn.lock b/yarn.lock index d60598d9..c4f26170 100644 --- a/yarn.lock +++ b/yarn.lock @@ -605,10 +605,10 @@ wordwrap "^1.0.0" wrap-ansi "^7.0.0" -"@oclif/table@^0.1.17": - version "0.1.17" - resolved "https://registry.yarnpkg.com/@oclif/table/-/table-0.1.17.tgz#8877663f7354ac96a6fc167448934572440c7634" - integrity sha512-x7ffsCMeqTM6oECg2cd3MDTGTWx19oOxIMEVOVk0qvsfrQfsecB4m7dQh9W6+VLvGg/bkxJKHB14Ytf8ieiqmw== +"@oclif/table@^0.1.19": + version "0.1.19" + resolved "https://registry.yarnpkg.com/@oclif/table/-/table-0.1.19.tgz#cf279e0d6ec2f3df4cdd2f8e55d7f6c4069f43d5" + integrity sha512-a659MZzfWcRa0IvrxJWkEhh3bU3JV1QQzExrj98A4SLZJ1NVuWsnkyMFc9IZ8iQsz1j3sLdiQ365xxrl1eJi8A== dependencies: "@oclif/core" "^4" "@types/react" "^18.3.11"