[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

@@ -0,0 +1,2 @@
---
---

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,7 @@
import isErrnoException from '@vercel/error-utils'; import isErrnoException from '@vercel/error-utils';
import chalk from 'chalk'; import chalk from 'chalk';
import table from 'text-table'; import table from '../../util/output/table';
import ms from 'ms'; import ms from 'ms';
import strlen from '../../util/strlen';
import { handleError, error } from '../../util/error'; import { handleError, error } from '../../util/error';
import NowSecrets from '../../util/secrets'; import NowSecrets from '../../util/secrets';
import getScope from '../../util/get-scope'; 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`), chalk.gray(`${ms(cur - new Date(secret.created))} ago`),
]) ])
), ),
{ { hsep: 2 }
align: ['l', 'l', 'l'],
hsep: ' '.repeat(2),
stringLength: strlen,
}
); );
if (out) { 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 time = chalk.gray(`${ms(new Date() - new Date(secret.created))} ago`);
const tbl = table([[chalk.bold(secret.name), time]], { const tbl = table([[chalk.bold(secret.name), time]], {
align: ['r', 'l'], align: ['r', 'l'],
hsep: ' '.repeat(6), hsep: 6,
}); });
output.print( output.print(

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,38 +1,52 @@
import chalk from 'chalk'; import Table from 'cli-table3';
const printLine = (data: string[], sizes: number[]) => const defaultStyle = {
data.reduce((line, col, i) => line + col.padEnd(sizes[i]), ''); '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( export default function table(
fieldNames: string[] = [], rows: string[][],
data: string[][] = [], opts?: { hsep?: number; align?: ('l' | 'c' | 'r')[] }
margins: number[] = [],
print: (str: string) => void
) { ) {
// Compute size of each column const table = new Table({
const sizes = data style: {
.reduce( ...defaultStyle,
(acc, row) => 'padding-right': opts?.hsep ?? defaultStyle['padding-right'],
row.map((col, i) => { },
const currentMaxColSize = acc[i] || 0; chars: noBorderChars,
const colSize = (col && col.length) || 0; });
return Math.max(currentMaxColSize, colSize); table.push(
}), ...rows.map(row =>
fieldNames.map(col => col.length) 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); return table.toString();
// Print header
print(chalk.grey(printLine(fieldNames, sizes)));
print('\n');
// Print content
for (const row of data) {
print(printLine(row, sizes));
print('\n');
}
} }

View File

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

View File

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

18
pnpm-lock.yaml generated
View File

@@ -1,5 +1,9 @@
lockfileVersion: '6.0' lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers: importers:
.: .:
@@ -463,9 +467,6 @@ importers:
'@types/tar-fs': '@types/tar-fs':
specifier: 1.16.1 specifier: 1.16.1
version: 1.16.1 version: 1.16.1
'@types/text-table':
specifier: 0.2.0
version: 0.2.0
'@types/title': '@types/title':
specifier: 3.4.1 specifier: 3.4.1
version: 3.4.1 version: 3.4.1
@@ -700,9 +701,6 @@ importers:
tar-fs: tar-fs:
specifier: 1.16.3 specifier: 1.16.3
version: 1.16.3 version: 1.16.3
text-table:
specifier: 0.2.0
version: 0.2.0
title: title:
specifier: 3.4.1 specifier: 3.4.1
version: 3.4.1 version: 3.4.1
@@ -5377,10 +5375,6 @@ packages:
minipass: 4.2.8 minipass: 4.2.8
dev: true dev: true
/@types/text-table@0.2.0:
resolution: {integrity: sha512-om4hNWnI01IKUFCjGQG33JqFcnmt0W5C3WX0G1FVBaucr7oRnL29aAz2hnxpbZnE2t9f8/BR5VOtgcOtsonpLA==}
dev: true
/@types/text-table@0.2.1: /@types/text-table@0.2.1:
resolution: {integrity: sha512-dchbFCWfVgUSWEvhOkXGS7zjm+K7jCUvGrQkAHPk2Fmslfofp4HQTH2pqnQ3Pw5GPYv0zWa2AQjKtsfZThuemQ==} resolution: {integrity: sha512-dchbFCWfVgUSWEvhOkXGS7zjm+K7jCUvGrQkAHPk2Fmslfofp4HQTH2pqnQ3Pw5GPYv0zWa2AQjKtsfZThuemQ==}
dev: true dev: true
@@ -16561,7 +16555,3 @@ packages:
/zwitch@2.0.4: /zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
dev: true dev: true
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false