[cli] Unify table formatting output (#11387)

This removes 'text-table' as a dependency in favor of using 'cli-table3', and simplifies table formatting logic.
This commit is contained in:
Austin Merrick
2024-04-09 00:33:04 -07:00
committed by GitHub
parent 3e57c4a2de
commit 2e6aab01cb
20 changed files with 104 additions and 150 deletions

View File

@@ -83,7 +83,6 @@
"@types/qs": "6.9.7",
"@types/semver": "6.0.1",
"@types/tar-fs": "1.16.1",
"@types/text-table": "0.2.0",
"@types/title": "3.4.1",
"@types/universal-analytics": "0.4.2",
"@types/update-notifier": "5.1.0",
@@ -162,7 +161,6 @@
"strip-ansi": "6.0.1",
"supports-hyperlinks": "3.0.0",
"tar-fs": "1.16.3",
"text-table": "0.2.0",
"title": "3.4.1",
"tmp-promise": "1.0.3",
"tree-kill": "1.2.2",

View File

@@ -1,6 +1,6 @@
import chalk from 'chalk';
import ms from 'ms';
import table from 'text-table';
import table from '../../util/output/table';
import Client from '../../util/client';
import getAliases from '../../util/alias/get-aliases';
import getScope from '../../util/get-scope';
@@ -9,7 +9,6 @@ import {
getPaginationOpts,
} from '../../util/get-pagination-opts';
import stamp from '../../util/output/stamp';
import strlen from '../../util/strlen';
import getCommandFlags from '../../util/get-command-flags';
import { getCommandName } from '../../util/pkg-name';
import type { Alias } from '@vercel-internals/types';
@@ -78,10 +77,6 @@ function printAliasTable(aliases: Alias[]) {
ms(Date.now() - a.createdAt),
]),
],
{
align: ['l', 'l', 'r'],
hsep: ' '.repeat(4),
stringLength: strlen,
}
{ align: ['l', 'l', 'r'], hsep: 4 }
).replace(/^/gm, ' ')}\n\n`;
}

View File

@@ -1,11 +1,10 @@
import chalk from 'chalk';
import ms from 'ms';
import table from 'text-table';
import table from '../../util/output/table';
import Client from '../../util/client';
import getScope from '../../util/get-scope';
import removeAliasById from '../../util/alias/remove-alias-by-id';
import stamp from '../../util/output/stamp';
import strlen from '../../util/strlen';
import confirm from '../../util/input/confirm';
import findAliasByAliasOrId from '../../util/alias/find-alias-by-alias-or-id';
@@ -84,11 +83,7 @@ async function confirmAliasRemove(client: Client, alias: Alias) {
chalk.gray(`${ms(Date.now() - alias.createdAt)} ago`),
],
],
{
align: ['l', 'l', 'r'],
hsep: ' '.repeat(4),
stringLength: strlen,
}
{ hsep: 4 }
);
client.output.log(`The following alias will be removed permanently`);

View File

@@ -1,6 +1,6 @@
import chalk from 'chalk';
import ms from 'ms';
import table from 'text-table';
import table from '../../util/output/table';
import Client from '../../util/client';
import getScope from '../../util/get-scope';
import {
@@ -9,7 +9,6 @@ import {
} from '../../util/get-pagination-opts';
import stamp from '../../util/output/stamp';
import getCerts from '../../util/certs/get-certs';
import strlen from '../../util/strlen';
import type { Cert } from '@vercel-internals/types';
import getCommandFlags from '../../util/get-command-flags';
import { getCommandName } from '../../util/pkg-name';
@@ -70,11 +69,7 @@ async function ls(
function formatCertsTable(certsList: Cert[]) {
return `${table(
[formatCertsTableHead(), ...formatCertsTableBody(certsList)],
{
align: ['l', 'l', 'r', 'c', 'r'],
hsep: ' '.repeat(2),
stringLength: strlen,
}
{ align: ['l', 'l', 'r', 'c', 'r'], hsep: 2 }
).replace(/^(.*)/gm, ' $1')}\n`;
}

View File

@@ -1,7 +1,7 @@
import chalk from 'chalk';
import ms from 'ms';
import plural from 'pluralize';
import table from 'text-table';
import table from '../../util/output/table';
import type { Cert } from '@vercel-internals/types';
import * as ERRORS from '../../util/errors-ts';
import { Output } from '../../util/output';
@@ -98,7 +98,7 @@ function readConfirmation(output: Output, msg: string, certs: Cert[]) {
output.print(
`${table(certs.map(formatCertRow), {
align: ['l', 'r', 'l'],
hsep: ' '.repeat(6),
hsep: 6,
}).replace(/^(.*)/gm, ' $1')}\n`
);
output.print(

View File

@@ -1,6 +1,6 @@
import chalk from 'chalk';
import ms from 'ms';
import table from 'text-table';
import table from '../../util/output/table';
import type { DNSRecord } from '@vercel-internals/types';
import { Output } from '../../util/output';
import Client from '../../util/client';
@@ -71,7 +71,7 @@ function readConfirmation(
output.print(
`${table([getDeleteTableRow(domainName, record)], {
align: ['l', 'r', 'l'],
hsep: ' '.repeat(6),
hsep: 6,
}).replace(/^(.*)/gm, ' $1')}\n`
);
output.print(

View File

@@ -1,6 +1,7 @@
import chalk from 'chalk';
import { LOGO, NAME } from '@vercel-internals/constants';
import Table, { CellOptions } from 'cli-table3';
import { noBorderChars } from '../util/output/table';
const INDENT = ' '.repeat(2);
const NEWLINE = '\n';
@@ -39,23 +40,7 @@ type _CellOptions = CellOptions & {
};
const tableOptions = {
chars: {
top: '',
'top-mid': '',
'top-left': '',
'top-right': '',
bottom: '',
'bottom-mid': '',
'bottom-left': '',
'bottom-right': '',
left: '',
'left-mid': '',
mid: '',
'mid-mid': '',
right: '',
'right-mid': '',
middle: '',
},
chars: noBorderChars,
style: {
'padding-left': 0,
'padding-right': 0,

View File

@@ -1,12 +1,11 @@
import chalk from 'chalk';
import ms from 'ms';
import table from 'text-table';
import table from '../../util/output/table';
import title from 'title';
import Now from '../../util';
import getArgs from '../../util/get-args';
import { handleError } from '../../util/error';
import elapsed from '../../util/output/elapsed';
import strlen from '../../util/strlen';
import toHost from '../../util/to-host';
import parseMeta from '../../util/parse-meta';
import { isValidName } from '../../util/is-valid-name';
@@ -275,11 +274,7 @@ export default async function list(client: Client) {
app === null ? filterUniqueApps() : () => true
),
],
{
align: ['l', 'l', 'l', 'l', 'l'],
hsep: ' '.repeat(5),
stringLength: strlen,
}
{ hsep: 5 }
).replace(/^/gm, ' ')}\n\n`
);

View File

@@ -1,11 +1,10 @@
import chalk from 'chalk';
import ms from 'ms';
import table from 'text-table';
import table from '../../util/output/table';
import type { Project } from '@vercel-internals/types';
import Client from '../../util/client';
import getCommandFlags from '../../util/get-command-flags';
import { getCommandName } from '../../util/pkg-name';
import strlen from '../../util/strlen';
import { NODE_VERSIONS } from '@vercel/build-utils';
export default async function list(
@@ -100,11 +99,7 @@ export default async function list(
])
.flat(),
],
{
align: ['l', 'l', 'l'],
hsep: ' '.repeat(3),
stringLength: strlen,
}
{ hsep: 3 }
).replace(/^/gm, ' ');
output.print(`\n${tablePrint}\n\n`);

View File

@@ -1,7 +1,7 @@
import chalk from 'chalk';
import ms from 'ms';
import plural from 'pluralize';
import table from 'text-table';
import table from '../../util/output/table';
import Now from '../../util';
import getAliases from '../../util/alias/get-aliases';
import elapsed from '../../util/output/elapsed';
@@ -245,7 +245,7 @@ function readConfirmation(
const url = depl.url ? chalk.underline(`https://${depl.url}`) : '';
return [` ${depl.id}`, url, time];
}),
{ align: ['l', 'r', 'l'], hsep: ' '.repeat(6) }
{ align: ['l', 'r', 'l'], hsep: 6 }
);
output.print(`${deploymentTable}\n`);
}

View File

@@ -1,8 +1,7 @@
import isErrnoException from '@vercel/error-utils';
import chalk from 'chalk';
import table from 'text-table';
import table from '../../util/output/table';
import ms from 'ms';
import strlen from '../../util/strlen';
import { handleError, error } from '../../util/error';
import NowSecrets from '../../util/secrets';
import getScope from '../../util/get-scope';
@@ -124,11 +123,7 @@ async function run({ output, contextName, currentTeam, client }) {
chalk.gray(`${ms(cur - new Date(secret.created))} ago`),
])
),
{
align: ['l', 'l', 'l'],
hsep: ' '.repeat(2),
stringLength: strlen,
}
{ hsep: 2 }
);
if (out) {
@@ -285,7 +280,7 @@ async function readConfirmation(client, output, secret, contextName) {
const time = chalk.gray(`${ms(new Date() - new Date(secret.created))} ago`);
const tbl = table([[chalk.bold(secret.name), time]], {
align: ['r', 'l'],
hsep: ' '.repeat(6),
hsep: 6,
});
output.print(

View File

@@ -1,5 +1,6 @@
import chars from '../../util/output/chars';
import table from '../../util/output/table';
import { gray } from 'chalk';
import getUser from '../../util/get-user';
import getTeams from '../../util/teams/get-teams';
import { packageName } from '../../util/pkg-name';
@@ -53,7 +54,7 @@ export default async function list(client: Client): Promise<number> {
id,
name,
value: slug,
current: id === currentTeam ? chars.tick : '',
prefix: id === currentTeam ? chars.tick : ' ',
}));
if (user.version !== 'northstar') {
@@ -61,7 +62,7 @@ export default async function list(client: Client): Promise<number> {
id: user.id,
name: user.email,
value: user.username || user.email,
current: accountIsCurrent ? chars.tick : '',
prefix: accountIsCurrent ? chars.tick : ' ',
});
}
@@ -76,14 +77,22 @@ export default async function list(client: Client): Promise<number> {
output.stopSpinner();
client.stdout.write('\n'); // empty line
table(
['', 'id', 'email / name'],
teamList.map(team => [team.current, team.value, team.name]),
[1, 5],
(str: string) => {
client.stdout.write(str);
}
const teamTable = table(
[
['id', 'email / name'].map(str => gray(str)),
...teamList.map(team => [team.value, team.name]),
],
{ hsep: 5 }
);
client.stderr.write(
currentTeam
? teamTable
.split('\n')
.map((line, i) => `${i > 0 ? teamList[i - 1].prefix : ' '} ${line}`)
.join('\n')
: teamTable
);
client.stderr.write('\n');
if (pagination?.count === 20) {
const flags = getCommandFlags(argv, ['_', '--next', '-N', '-d']);

View File

@@ -1,16 +1,8 @@
import chalk from 'chalk';
import table from 'text-table';
import strlen from './strlen';
import table from './output/table';
import { gray } from 'chalk';
const HEADER = ['name', 'type', 'value'].map(v => chalk.gray(v));
const HEADER = ['name', 'type', 'value'].map(v => gray(v));
export default function formatDNSTable(
rows: string[][],
{ extraSpace = '' } = {}
) {
return table([HEADER, ...rows], {
align: ['l', 'l', 'l'],
hsep: ' '.repeat(8),
stringLength: strlen,
}).replace(/^(.*)/gm, `${extraSpace}$1`);
export default function formatDNSTable(rows: string[][]) {
return table([HEADER, ...rows], { hsep: 8 });
}

View File

@@ -1,6 +1,5 @@
import chalk from 'chalk';
import table from 'text-table';
import strlen from './strlen';
import table from './output/table';
import chars from './output/chars';
export default function formatNSTable(
@@ -35,10 +34,6 @@ export default function formatNSTable(
],
...rows,
],
{
align: ['l', 'l', 'l', 'l'],
hsep: ' '.repeat(4),
stringLength: strlen,
}
{ hsep: 4 }
).replace(/^(.*)/gm, `${extraSpace}$1`);
}

View File

@@ -1,5 +1,5 @@
import chalk from 'chalk';
import table from 'text-table';
import table from './output/table';
import strlen from './strlen';
// header:
@@ -19,9 +19,8 @@ import strlen from './strlen';
// ]
export default function formatTable(
header: string[],
align: Array<'l' | 'r' | 'c' | '.'>,
blocks: { name?: string; rows: string[][] }[],
hsep = ' '
align: Array<'l' | 'r' | 'c'>,
blocks: { name?: string; rows: string[][] }[]
) {
const nrCols = header.length;
const padding = [];
@@ -57,7 +56,7 @@ export default function formatTable(
rows[i][j] = al === 'l' ? col + pad : pad + col;
}
}
out += table(rows, { align, hsep, stringLength: strlen });
out += table(rows, { align, hsep: 4 });
}
out += '\n\n';
}

View File

@@ -1,38 +1,52 @@
import chalk from 'chalk';
import Table from 'cli-table3';
const printLine = (data: string[], sizes: number[]) =>
data.reduce((line, col, i) => line + col.padEnd(sizes[i]), '');
const defaultStyle = {
'padding-left': 0,
'padding-right': 2,
};
export const noBorderChars = {
top: '',
'top-mid': '',
'top-left': '',
'top-right': '',
bottom: '',
'bottom-mid': '',
'bottom-left': '',
'bottom-right': '',
left: '',
'left-mid': '',
mid: '',
'mid-mid': '',
right: '',
'right-mid': '',
middle: '',
};
const alignMap = {
l: 'left',
c: 'center',
r: 'right',
} as const;
/**
* Print a table.
*/
export default function table(
fieldNames: string[] = [],
data: string[][] = [],
margins: number[] = [],
print: (str: string) => void
rows: string[][],
opts?: { hsep?: number; align?: ('l' | 'c' | 'r')[] }
) {
// Compute size of each column
const sizes = data
.reduce(
(acc, row) =>
row.map((col, i) => {
const currentMaxColSize = acc[i] || 0;
const colSize = (col && col.length) || 0;
return Math.max(currentMaxColSize, colSize);
}),
fieldNames.map(col => col.length)
const table = new Table({
style: {
...defaultStyle,
'padding-right': opts?.hsep ?? defaultStyle['padding-right'],
},
chars: noBorderChars,
});
table.push(
...rows.map(row =>
row.map((cell, i) => ({
content: cell,
hAlign: alignMap[opts?.align?.[i] ?? 'l'],
}))
)
// Add margin to all columns except the last
.map((size, i) => (i < margins.length && size + margins[i]) || size);
// Print header
print(chalk.grey(printLine(fieldNames, sizes)));
print('\n');
// Print content
for (const row of data) {
print(printLine(row, sizes));
print('\n');
}
);
return table.toString();
}

View File

@@ -369,7 +369,7 @@ test('list the scopes', async () => {
expect(exitCode, formatOutput({ stdout, stderr })).toBe(0);
const include = new RegExp(`${contextName}\\s+${email}`);
expect(stdout).toMatch(include);
expect(stderr).toMatch(include);
});
test('domains inspect', async () => {

View File

@@ -11,7 +11,7 @@ describe('teams', () => {
const user = useUser();
useTeams(undefined, { apiVersion: 2 });
const exitCodePromise = teamsList(client);
await expect(client.stdout).toOutput(user.username);
await expect(client.stderr).toOutput(user.username);
await expect(exitCodePromise).resolves.toEqual(0);
});
});