From 3cfe03df468bde563428d2991e72d180aa52cc1c Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 27 May 2023 15:20:18 +1200 Subject: [PATCH 1/8] feat: add proper wrapping for esm and Deno by updating dependencies - add esm test to npm run script - run full tests from esm Co-authored-by: Momtchil Momtchev --- .github/workflows/ci.yaml | 9 - deno.ts | 8 +- index.mjs | 8 +- lib/cjs.ts | 7 +- lib/string-utils.ts | 30 --- package.json | 16 +- rollup.config.js | 10 +- test/cjs/cliui-test.cjs | 4 + test/cliui.cjs | 492 ------------------------------------ test/deno/cliui-test.ts | 9 +- test/esm/cliui-test.mjs | 47 +--- test/shared-tests.cjs | 508 ++++++++++++++++++++++++++++++++++++++ 12 files changed, 549 insertions(+), 599 deletions(-) delete mode 100644 lib/string-utils.ts create mode 100644 test/cjs/cliui-test.cjs delete mode 100644 test/cliui.cjs create mode 100644 test/shared-tests.cjs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0f34c9e..a873196 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -45,15 +45,6 @@ jobs: PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true - run: npm test - run: npm run coverage - esm: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 - with: - node-version: 14 - - run: npm install - - run: npm run test:esm deno: runs-on: ubuntu-latest steps: diff --git a/deno.ts b/deno.ts index a94e49f..3fa550f 100644 --- a/deno.ts +++ b/deno.ts @@ -1,13 +1,13 @@ // Bootstrap cliui with CommonJS dependencies: import { cliui, UI } from './build/lib/index.js' import type { UIOptions } from './build/lib/index.d.ts' -import { wrap, stripAnsi } from './build/lib/string-utils.js' +import wrap from 'wrap-ansi' +import stripAnsi from 'strip-ansi' +import stringWidth from 'string-width' export default function ui (opts: UIOptions): UI { return cliui(opts, { - stringWidth: (str: string) => { - return [...str].length - }, + stringWidth, stripAnsi, wrap }) diff --git a/index.mjs b/index.mjs index bc7a022..720cb52 100644 --- a/index.mjs +++ b/index.mjs @@ -1,12 +1,12 @@ // Bootstrap cliui with CommonJS dependencies: import { cliui } from './build/lib/index.js' -import { wrap, stripAnsi } from './build/lib/string-utils.js' +import wrap from 'wrap-ansi' +import stripAnsi from 'strip-ansi' +import stringWidth from 'string-width' export default function ui (opts) { return cliui(opts, { - stringWidth: (str) => { - return [...str].length - }, + stringWidth, stripAnsi, wrap }) diff --git a/lib/cjs.ts b/lib/cjs.ts index bda4241..7c68d6b 100644 --- a/lib/cjs.ts +++ b/lib/cjs.ts @@ -1,8 +1,9 @@ // Bootstrap cliui with CommonJS dependencies: import { cliui, UIOptions } from './index.js' -const stringWidth = require('string-width') -const stripAnsi = require('strip-ansi') -const wrap = require('wrap-ansi') +import stringWidth from 'string-width' +import wrap from 'wrap-ansi' +import stripAnsi from 'strip-ansi' + export default function ui (opts: UIOptions) { return cliui(opts, { stringWidth, diff --git a/lib/string-utils.ts b/lib/string-utils.ts deleted file mode 100644 index 23d78fd..0000000 --- a/lib/string-utils.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Minimal replacement for ansi string helpers "wrap-ansi" and "strip-ansi". -// to facilitate ESM and Deno modules. -// TODO: look at porting https://www.npmjs.com/package/wrap-ansi to ESM. - -// The npm application -// Copyright (c) npm, Inc. and Contributors -// Licensed on the terms of The Artistic License 2.0 -// See: https://github.com/npm/cli/blob/4c65cd952bc8627811735bea76b9b110cc4fc80e/lib/utils/ansi-trim.js -const ansi = new RegExp('\x1b(?:\\[(?:\\d+[ABCDEFGJKSTm]|\\d+;\\d+[Hfm]|' + -'\\d+;\\d+;\\d+m|6n|s|u|\\?25[lh])|\\w)', 'g') - -export function stripAnsi (str: string) { - return str.replace(ansi, '') -} - -export function wrap (str: string, width: number) { - const [start, end] = str.match(ansi) || ['', ''] - str = stripAnsi(str) - let wrapped = '' - for (let i = 0; i < str.length; i++) { - if (i !== 0 && (i % width) === 0) { - wrapped += '\n' - } - wrapped += str.charAt(i) - } - if (start && end) { - wrapped = `${start}${wrapped}${end}` - } - return wrapped -} diff --git a/package.json b/package.json index eab6bf4..1c52359 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,7 @@ "check": "standardx '**/*.ts' && standardx '**/*.js' && standardx '**/*.cjs'", "fix": "standardx --fix '**/*.ts' && standardx --fix '**/*.js' && standardx --fix '**/*.cjs'", "pretest": "rimraf build && tsc -p tsconfig.test.json && cross-env NODE_ENV=test npm run build:cjs", - "test": "c8 mocha ./test/*.cjs", - "test:esm": "c8 mocha ./test/esm/cliui-test.mjs", + "test": "c8 mocha ./test/cjs/*.cjs ./test/esm/*.mjs", "postest": "check", "coverage": "c8 report --check-coverage", "precompile": "rimraf build", @@ -49,12 +48,15 @@ "author": "Ben Coe ", "license": "ISC", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "string-width": "^5.1.2", + "strip-ansi": "^7.0.1", + "wrap-ansi": "^8.1.0" }, "devDependencies": { + "@rollup/plugin-commonjs": "^25.0.0", + "@rollup/plugin-node-resolve": "^15.0.2", "@types/node": "^14.0.27", + "@types/wrap-ansi": "^8.0.1", "@typescript-eslint/eslint-plugin": "^4.0.0", "@typescript-eslint/parser": "^4.0.0", "c8": "^7.3.0", @@ -67,8 +69,8 @@ "gts": "^3.0.0", "mocha": "^10.0.0", "rimraf": "^3.0.2", - "rollup": "^2.23.1", - "rollup-plugin-ts": "^3.0.2", + "rollup": "^2.79.1", + "rollup-plugin-ts": "^3.2.0", "standardx": "^7.0.0", "typescript": "^4.0.0" }, diff --git a/rollup.config.js b/rollup.config.js index fed3531..f6737cd 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,4 +1,10 @@ import ts from 'rollup-plugin-ts' +import nodeResolve from '@rollup/plugin-node-resolve' +import commonjs from '@rollup/plugin-commonjs' + +// These two transient dependencies are still CommonJS: +// 'node_modules/emoji-regex/index.js', +// 'node_modules/eastasianwidth/eastasianwidth.js' const output = { format: 'cjs', @@ -12,6 +18,8 @@ export default { input: './lib/cjs.ts', output, plugins: [ - ts({ /* options */ }) + ts({ /* options */ }), + nodeResolve(), + commonjs() ] } diff --git a/test/cjs/cliui-test.cjs b/test/cjs/cliui-test.cjs new file mode 100644 index 0000000..8f73d38 --- /dev/null +++ b/test/cjs/cliui-test.cjs @@ -0,0 +1,4 @@ +const { runTests } = require('../shared-tests.cjs') +const cliui = require('../../build/index.cjs') + +runTests(cliui, 'CJS') diff --git a/test/cliui.cjs b/test/cliui.cjs deleted file mode 100644 index 6dd86e7..0000000 --- a/test/cliui.cjs +++ /dev/null @@ -1,492 +0,0 @@ -'use strict' - -/* global describe, it */ - -require('chai').should() - -// Force chalk to enable color, if it's disabled the test fails. -process.env.FORCE_COLOR = 1 - -const chalk = require('chalk') -const cliui = require('../build/index.cjs') -const stripAnsi = require('strip-ansi') - -describe('cliui', () => { - describe('resetOutput', () => { - it('should set lines to empty', () => { - const ui = cliui() - ui.div('i am a value that would be in a line') - ui.resetOutput() - ui.toString().length.should.be.equal(0) - }) - }) - - describe('div', () => { - it("wraps text at 'width' if a single column is given", () => { - const ui = cliui({ - width: 10 - }) - - ui.div('i am a string that should be wrapped') - - ui.toString().split('\n').forEach(row => { - row.length.should.be.lte(10) - }) - }) - - it('evenly divides text across columns if multiple columns are given', () => { - const ui = cliui({ - width: 40 - }) - - ui.div( - { text: 'i am a string that should be wrapped', width: 15 }, - 'i am a second string that should be wrapped', - 'i am a third string that should be wrapped' - ) - - // total width of all columns is <= - // the width cliui is initialized with. - ui.toString().split('\n').forEach(row => { - row.length.should.be.lte(40) - }) - - // it should wrap each column appropriately. - const expected = [ - 'i am a string i am a i am a third', - 'that should be second string that', - 'wrapped string that should be', - ' should be wrapped', - ' wrapped' - ] - - ui.toString().split('\n').should.eql(expected) - }) - - it('allows for a blank row to be appended', () => { - const ui = cliui({ - width: 40 - }) - - ui.div() - - // it should wrap each column appropriately. - const expected = [''] - - ui.toString().split('\n').should.eql(expected) - }) - }) - - describe('_columnWidths', () => { - it('uses same width for each column by default', () => { - const ui = cliui({ - width: 40 - }) - const widths = ui.columnWidths([{}, {}, {}]) - - widths[0].should.equal(13) - widths[1].should.equal(13) - widths[2].should.equal(13) - }) - - it('divides width over remaining columns if first column has width specified', () => { - const ui = cliui({ - width: 40 - }) - const widths = ui.columnWidths([{ width: 20 }, {}, {}]) - - widths[0].should.equal(20) - widths[1].should.equal(10) - widths[2].should.equal(10) - }) - - it('divides width over remaining columns if middle column has width specified', () => { - const ui = cliui({ - width: 40 - }) - const widths = ui.columnWidths([{}, { width: 10 }, {}]) - - widths[0].should.equal(15) - widths[1].should.equal(10) - widths[2].should.equal(15) - }) - - it('keeps track of remaining width if multiple columns have width specified', () => { - const ui = cliui({ - width: 40 - }) - const widths = ui.columnWidths([{ width: 20 }, { width: 12 }, {}]) - - widths[0].should.equal(20) - widths[1].should.equal(12) - widths[2].should.equal(8) - }) - - it('uses a sane default if impossible widths are specified', () => { - const ui = cliui({ - width: 40 - }) - const widths = ui.columnWidths([{ width: 30 }, { width: 30 }, { padding: [0, 2, 0, 1] }]) - - widths[0].should.equal(30) - widths[1].should.equal(30) - widths[2].should.equal(4) - }) - }) - - describe('alignment', () => { - it('allows a column to be right aligned', () => { - const ui = cliui({ - width: 40 - }) - - ui.div( - 'i am a string', - { text: 'i am a second string', align: 'right' }, - 'i am a third string that should be wrapped' - ) - - // it should right-align the second column. - const expected = [ - 'i am a stringi am a secondi am a third', - ' stringstring that', - ' should be', - ' wrapped' - ] - - ui.toString().split('\n').should.eql(expected) - }) - - it('allows a column to be center aligned', () => { - const ui = cliui({ - width: 60 - }) - - ui.div( - 'i am a string', - { text: 'i am a second string', align: 'center', padding: [0, 2, 0, 2] }, - 'i am a third string that should be wrapped' - ) - - // it should right-align the second column. - const expected = [ - 'i am a string i am a second i am a third string', - ' string that should be', - ' wrapped' - ] - - ui.toString().split('\n').should.eql(expected) - }) - }) - - describe('padding', () => { - it('handles left/right padding', () => { - const ui = cliui({ - width: 40 - }) - - ui.div( - { text: 'i have padding on my left', padding: [0, 0, 0, 4] }, - { text: 'i have padding on my right', padding: [0, 2, 0, 0], align: 'center' }, - { text: 'i have no padding', padding: [0, 0, 0, 0] } - ) - - // it should add left/right padding to columns. - const expected = [ - ' i have i have i have no', - ' padding padding on padding', - ' on my my right', - ' left' - ] - - ui.toString().split('\n').should.eql(expected) - }) - - it('handles top/bottom padding', () => { - const ui = cliui({ - width: 40 - }) - - ui.div( - 'i am a string', - { text: 'i am a second string', padding: [2, 0, 0, 0] }, - { text: 'i am a third string that should be wrapped', padding: [0, 0, 1, 0] } - ) - - // it should add top/bottom padding to second - // and third columns. - const expected = [ - 'i am a string i am a third', - ' string that', - ' i am a secondshould be', - ' string wrapped', - '' - ] - - ui.toString().split('\n').should.eql(expected) - }) - - it('preserves leading whitespace as padding', () => { - const ui = cliui() - - ui.div(' LEADING WHITESPACE') - ui.div('\u001b[34m with ansi\u001b[39m') - - const expected = [ - ' LEADING WHITESPACE', - ' with ansi' - ] - - ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected) - }) - }) - - describe('border', () => { - it('allows a border to be placed around a div', () => { - const ui = cliui({ - width: 40 - }) - - ui.div( - { text: 'i am a first string', padding: [0, 0, 0, 0], border: true }, - { text: 'i am a second string', padding: [1, 0, 0, 0], border: true } - ) - - const expected = [ - '.------------------.', - '| i am a first |.------------------.', - '| string || i am a second |', - "'------------------'| string |", - " '------------------'" - ] - - ui.toString().split('\n').should.eql(expected) - }) - }) - - describe('wrap', () => { - it('allows wordwrap to be disabled', () => { - const ui = cliui({ - wrap: false - }) - - ui.div( - { text: 'i am a string', padding: [0, 1, 0, 0] }, - { text: 'i am a second string', padding: [0, 2, 0, 0] }, - { text: 'i am a third string that should not be wrapped', padding: [0, 0, 0, 2] } - ) - - ui.toString().should.equal('i am a string i am a second string i am a third string that should not be wrapped') - }) - }) - - describe('span', () => { - it('appends the next row to the end of the prior row if it fits', () => { - const ui = cliui({ - width: 40 - }) - - ui.span( - { text: 'i am a string that will be wrapped', width: 30 } - ) - - ui.div( - { text: ' [required] [default: 99]', align: 'right' } - ) - - const expected = [ - 'i am a string that will be', - 'wrapped [required] [default: 99]' - ] - - ui.toString().split('\n').should.eql(expected) - }) - - it('does not append the string if it does not fit on the prior row', () => { - const ui = cliui({ - width: 40 - }) - - ui.span( - { text: 'i am a string that will be wrapped', width: 30 } - ) - - ui.div( - { text: 'i am a second row', align: 'left' } - ) - - const expected = [ - 'i am a string that will be', - 'wrapped', - 'i am a second row' - ] - - ui.toString().split('\n').should.eql(expected) - }) - - it('always appends text to prior span if wrap is disabled', () => { - const ui = cliui({ - wrap: false, - width: 40 - }) - - ui.span( - { text: 'i am a string that will be wrapped', width: 30 } - ) - - ui.div( - { text: 'i am a second row', align: 'left', padding: [0, 0, 0, 3] } - ) - - ui.div('a third line') - - const expected = [ - 'i am a string that will be wrapped i am a second row', - 'a third line' - ] - - ui.toString().split('\n').should.eql(expected) - }) - - it('appends to prior line appropriately when strings contain ansi escape codes', () => { - const ui = cliui({ - width: 40 - }) - - ui.span( - { text: chalk.green('i am a string that will be wrapped'), width: 30 } - ) - - ui.div( - { text: chalk.blue(' [required] [default: 99]'), align: 'right' } - ) - - const expected = [ - 'i am a string that will be', - 'wrapped [required] [default: 99]' - ] - - ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected) - }) - }) - - describe('layoutDSL', () => { - it('turns tab into multiple columns', () => { - const ui = cliui({ - width: 60 - }) - - ui.div( - ' \tmy awesome regex\n \tanother row\t a third column' - ) - - const expected = [ - ' my awesome regex', - ' another row a third column' - ] - - ui.toString().split('\n').should.eql(expected) - }) - - it('turns newline into multiple rows', () => { - const ui = cliui({ - width: 40 - }) - - ui.div( - 'Usage: $0\n \t my awesome regex\n \t my awesome glob\t [required]' - ) - const expected = [ - 'Usage: $0', - ' my awesome regex', - ' my awesome [required]', - ' glob' - ] - - ui.toString().split('\n').should.eql(expected) - }) - - it('aligns rows appropriately when they contain ansi escape codes', () => { - const ui = cliui({ - width: 40 - }) - - ui.div( - ' \t ' + chalk.red('my awesome regex') + '\t [regex]\n ' + chalk.blue('') + '\t my awesome glob\t [required]' - ) - - const expected = [ - ' my awesome [regex]', - ' regex', - ' my awesome [required]', - ' glob' - ] - - ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected) - }) - - it('ignores ansi escape codes when measuring padding', () => { - // Forcefully enable color-codes for this test - const { enabled, level } = chalk - chalk.enabled = true - chalk.level = 1 - - const ui = cliui({ - width: 25 - }) - - // using figlet font 'Shadow' rendering of text 'true' here - ui.div( - chalk.blue(' | \n __| __| | | _ \\ \n | | | | __/ \n \\__| _| \\__,_| \\___| \n ') - ) - - // relevant part is first line - leading whitespace should be preserved as left padding - const expected = [ - ' |', - ' __| __| | | _ \\', - ' | | | | __/', - ' \\__| _| \\__,_| \\___|', - ' ' - ] - - ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected) - chalk.enabled = enabled - chalk.level = level - }) - - it('correctly handles lack of ansi escape codes when measuring padding', () => { - const ui = cliui({ - width: 25 - }) - - // using figlet font 'Shadow' rendering of text 'true' here - ui.div( - ' | \n __| __| | | _ \\ \n | | | | __/ \n \\__| _| \\__,_| \\___| \n ' - ) - - // The difference - const expected = [ - ' |', - ' __| __| | | _ \\', - ' | | | | __/', - ' \\__| _| \\__,_| \\___|', - '' - ] - - ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected) - }) - - it('does not apply DSL if wrap is false', () => { - const ui = cliui({ - width: 40, - wrap: false - }) - - ui.div( - 'Usage: $0\ttwo\tthree' - ) - - ui.toString().should.eql('Usage: $0\ttwo\tthree') - }) - }) -}) diff --git a/test/deno/cliui-test.ts b/test/deno/cliui-test.ts index ce24068..de7b6c4 100644 --- a/test/deno/cliui-test.ts +++ b/test/deno/cliui-test.ts @@ -39,10 +39,11 @@ Deno.test('evenly divides text across columns if multiple columns are given', () // TODO: we should flesh out the Deno and ESM implementation // such that it spreads words out over multiple columns appropriately: const expected = [ - 'i am a string ti am a seconi am a third', - 'hat should be wd string tha string that', - 'rapped t should be should be w', - ' wrapped rapped' + 'i am a string i am a i am a third', + 'that should be second string that', + 'wrapped string that should be', + ' should be wrapped', + ' wrapped' ] ui.toString().split('\n').forEach((line: string, i: number) => { diff --git a/test/esm/cliui-test.mjs b/test/esm/cliui-test.mjs index f57d77d..b740be3 100644 --- a/test/esm/cliui-test.mjs +++ b/test/esm/cliui-test.mjs @@ -1,47 +1,4 @@ -import {ok as assert, strictEqual} from 'assert' +import { runTests } from '../shared-tests.cjs' import cliui from '../../index.mjs' -describe('ESM', () => { - it("wraps text at 'width' if a single column is given", () => { - const ui = cliui({ - width: 10 - }) - - ui.div('i am a string that should be wrapped') - - ui.toString().split('\n').forEach((row) => { - assert(row.length <= 10) - }) - }) - - it('evenly divides text across columns if multiple columns are given', () => { - const ui = cliui({ - width: 40 - }) - - ui.div( - { text: 'i am a string that should be wrapped', width: 15 }, - 'i am a second string that should be wrapped', - 'i am a third string that should be wrapped' - ) - - // total width of all columns is <= - // the width cliui is initialized with. - ui.toString().split('\n').forEach((row) => { - assert(row.length <= 40) - }) - - // it should wrap each column appropriately. - // TODO: we should flesh out the Deno and ESM implementation - // such that it spreads words out over multiple columns appropriately: - const expected = [ - 'i am a string ti am a seconi am a third', - 'hat should be wd string tha string that', - 'rapped t should be should be w', - ' wrapped rapped' - ] - ui.toString().split('\n').forEach((line, i) => { - strictEqual(line, expected[i]) - }) - }) -}) \ No newline at end of file +runTests(cliui, 'ESM') diff --git a/test/shared-tests.cjs b/test/shared-tests.cjs new file mode 100644 index 0000000..86ba98c --- /dev/null +++ b/test/shared-tests.cjs @@ -0,0 +1,508 @@ +'use strict' + +/* global describe, it */ + +// These are the full tests originally used only for cjs, now also run from esm. +// (Not attempting to run from deno.) + +require('chai').should() + +// stripAnsi adapted from: +// The npm application +// Copyright (c) npm, Inc. and Contributors +// Licensed on the terms of The Artistic License 2.0 +// See: https://github.com/npm/cli/blob/4c65cd952bc8627811735bea76b9b110cc4fc80e/lib/utils/ansi-trim.js +const ansi = new RegExp('\x1b(?:\\[(?:\\d+[ABCDEFGJKSTm]|\\d+;\\d+[Hfm]|' + +'\\d+;\\d+;\\d+m|6n|s|u|\\?25[lh])|\\w)', 'g') +function stripAnsi (str) { + return str.replace(ansi, '') +} + +// Force chalk to enable color, if it's disabled the test fails. +process.env.FORCE_COLOR = 1 + +const chalk = require('chalk') + +function runTests (cliui, moduleType) { + describe(moduleType, () => { + describe('resetOutput', () => { + it('should set lines to empty', () => { + const ui = cliui() + ui.div('i am a value that would be in a line') + ui.resetOutput() + ui.toString().length.should.be.equal(0) + }) + }) + + describe('div', () => { + it("wraps text at 'width' if a single column is given", () => { + const ui = cliui({ + width: 10 + }) + + ui.div('i am a string that should be wrapped') + + ui.toString().split('\n').forEach(row => { + row.length.should.be.lte(10) + }) + }) + + it('evenly divides text across columns if multiple columns are given', () => { + const ui = cliui({ + width: 40 + }) + + ui.div( + { text: 'i am a string that should be wrapped', width: 15 }, + 'i am a second string that should be wrapped', + 'i am a third string that should be wrapped' + ) + + // total width of all columns is <= + // the width cliui is initialized with. + ui.toString().split('\n').forEach(row => { + row.length.should.be.lte(40) + }) + + // it should wrap each column appropriately. + const expected = [ + 'i am a string i am a i am a third', + 'that should be second string that', + 'wrapped string that should be', + ' should be wrapped', + ' wrapped' + ] + + ui.toString().split('\n').should.eql(expected) + }) + + it('allows for a blank row to be appended', () => { + const ui = cliui({ + width: 40 + }) + + ui.div() + + // it should wrap each column appropriately. + const expected = [''] + + ui.toString().split('\n').should.eql(expected) + }) + }) + + describe('_columnWidths', () => { + it('uses same width for each column by default', () => { + const ui = cliui({ + width: 40 + }) + const widths = ui.columnWidths([{}, {}, {}]) + + widths[0].should.equal(13) + widths[1].should.equal(13) + widths[2].should.equal(13) + }) + + it('divides width over remaining columns if first column has width specified', () => { + const ui = cliui({ + width: 40 + }) + const widths = ui.columnWidths([{ width: 20 }, {}, {}]) + + widths[0].should.equal(20) + widths[1].should.equal(10) + widths[2].should.equal(10) + }) + + it('divides width over remaining columns if middle column has width specified', () => { + const ui = cliui({ + width: 40 + }) + const widths = ui.columnWidths([{}, { width: 10 }, {}]) + + widths[0].should.equal(15) + widths[1].should.equal(10) + widths[2].should.equal(15) + }) + + it('keeps track of remaining width if multiple columns have width specified', () => { + const ui = cliui({ + width: 40 + }) + const widths = ui.columnWidths([{ width: 20 }, { width: 12 }, {}]) + + widths[0].should.equal(20) + widths[1].should.equal(12) + widths[2].should.equal(8) + }) + + it('uses a sane default if impossible widths are specified', () => { + const ui = cliui({ + width: 40 + }) + const widths = ui.columnWidths([{ width: 30 }, { width: 30 }, { padding: [0, 2, 0, 1] }]) + + widths[0].should.equal(30) + widths[1].should.equal(30) + widths[2].should.equal(4) + }) + }) + + describe('alignment', () => { + it('allows a column to be right aligned', () => { + const ui = cliui({ + width: 40 + }) + + ui.div( + 'i am a string', + { text: 'i am a second string', align: 'right' }, + 'i am a third string that should be wrapped' + ) + + // it should right-align the second column. + const expected = [ + 'i am a stringi am a secondi am a third', + ' stringstring that', + ' should be', + ' wrapped' + ] + + ui.toString().split('\n').should.eql(expected) + }) + + it('allows a column to be center aligned', () => { + const ui = cliui({ + width: 60 + }) + + ui.div( + 'i am a string', + { text: 'i am a second string', align: 'center', padding: [0, 2, 0, 2] }, + 'i am a third string that should be wrapped' + ) + + // it should right-align the second column. + const expected = [ + 'i am a string i am a second i am a third string', + ' string that should be', + ' wrapped' + ] + + ui.toString().split('\n').should.eql(expected) + }) + }) + + describe('padding', () => { + it('handles left/right padding', () => { + const ui = cliui({ + width: 40 + }) + + ui.div( + { text: 'i have padding on my left', padding: [0, 0, 0, 4] }, + { text: 'i have padding on my right', padding: [0, 2, 0, 0], align: 'center' }, + { text: 'i have no padding', padding: [0, 0, 0, 0] } + ) + + // it should add left/right padding to columns. + const expected = [ + ' i have i have i have no', + ' padding padding on padding', + ' on my my right', + ' left' + ] + + ui.toString().split('\n').should.eql(expected) + }) + + it('handles top/bottom padding', () => { + const ui = cliui({ + width: 40 + }) + + ui.div( + 'i am a string', + { text: 'i am a second string', padding: [2, 0, 0, 0] }, + { text: 'i am a third string that should be wrapped', padding: [0, 0, 1, 0] } + ) + + // it should add top/bottom padding to second + // and third columns. + const expected = [ + 'i am a string i am a third', + ' string that', + ' i am a secondshould be', + ' string wrapped', + '' + ] + + ui.toString().split('\n').should.eql(expected) + }) + + it('preserves leading whitespace as padding', () => { + const ui = cliui() + + ui.div(' LEADING WHITESPACE') + ui.div('\u001b[34m with ansi\u001b[39m') + + const expected = [ + ' LEADING WHITESPACE', + ' with ansi' + ] + + ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected) + }) + }) + + describe('border', () => { + it('allows a border to be placed around a div', () => { + const ui = cliui({ + width: 40 + }) + + ui.div( + { text: 'i am a first string', padding: [0, 0, 0, 0], border: true }, + { text: 'i am a second string', padding: [1, 0, 0, 0], border: true } + ) + + const expected = [ + '.------------------.', + '| i am a first |.------------------.', + '| string || i am a second |', + "'------------------'| string |", + " '------------------'" + ] + + ui.toString().split('\n').should.eql(expected) + }) + }) + + describe('wrap', () => { + it('allows wordwrap to be disabled', () => { + const ui = cliui({ + wrap: false + }) + + ui.div( + { text: 'i am a string', padding: [0, 1, 0, 0] }, + { text: 'i am a second string', padding: [0, 2, 0, 0] }, + { text: 'i am a third string that should not be wrapped', padding: [0, 0, 0, 2] } + ) + + ui.toString().should.equal('i am a string i am a second string i am a third string that should not be wrapped') + }) + }) + + describe('span', () => { + it('appends the next row to the end of the prior row if it fits', () => { + const ui = cliui({ + width: 40 + }) + + ui.span( + { text: 'i am a string that will be wrapped', width: 30 } + ) + + ui.div( + { text: ' [required] [default: 99]', align: 'right' } + ) + + const expected = [ + 'i am a string that will be', + 'wrapped [required] [default: 99]' + ] + + ui.toString().split('\n').should.eql(expected) + }) + + it('does not append the string if it does not fit on the prior row', () => { + const ui = cliui({ + width: 40 + }) + + ui.span( + { text: 'i am a string that will be wrapped', width: 30 } + ) + + ui.div( + { text: 'i am a second row', align: 'left' } + ) + + const expected = [ + 'i am a string that will be', + 'wrapped', + 'i am a second row' + ] + + ui.toString().split('\n').should.eql(expected) + }) + + it('always appends text to prior span if wrap is disabled', () => { + const ui = cliui({ + wrap: false, + width: 40 + }) + + ui.span( + { text: 'i am a string that will be wrapped', width: 30 } + ) + + ui.div( + { text: 'i am a second row', align: 'left', padding: [0, 0, 0, 3] } + ) + + ui.div('a third line') + + const expected = [ + 'i am a string that will be wrapped i am a second row', + 'a third line' + ] + + ui.toString().split('\n').should.eql(expected) + }) + + it('appends to prior line appropriately when strings contain ansi escape codes', () => { + const ui = cliui({ + width: 40 + }) + + ui.span( + { text: chalk.green('i am a string that will be wrapped'), width: 30 } + ) + + ui.div( + { text: chalk.blue(' [required] [default: 99]'), align: 'right' } + ) + + const expected = [ + 'i am a string that will be', + 'wrapped [required] [default: 99]' + ] + + ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected) + }) + }) + + describe('layoutDSL', () => { + it('turns tab into multiple columns', () => { + const ui = cliui({ + width: 60 + }) + + ui.div( + ' \tmy awesome regex\n \tanother row\t a third column' + ) + + const expected = [ + ' my awesome regex', + ' another row a third column' + ] + + ui.toString().split('\n').should.eql(expected) + }) + + it('turns newline into multiple rows', () => { + const ui = cliui({ + width: 40 + }) + + ui.div( + 'Usage: $0\n \t my awesome regex\n \t my awesome glob\t [required]' + ) + const expected = [ + 'Usage: $0', + ' my awesome regex', + ' my awesome [required]', + ' glob' + ] + + ui.toString().split('\n').should.eql(expected) + }) + + it('aligns rows appropriately when they contain ansi escape codes', () => { + const ui = cliui({ + width: 40 + }) + + ui.div( + ' \t ' + chalk.red('my awesome regex') + '\t [regex]\n ' + chalk.blue('') + '\t my awesome glob\t [required]' + ) + + const expected = [ + ' my awesome [regex]', + ' regex', + ' my awesome [required]', + ' glob' + ] + + ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected) + }) + + it('ignores ansi escape codes when measuring padding', () => { + // Forcefully enable color-codes for this test + const { enabled, level } = chalk + chalk.enabled = true + chalk.level = 1 + + const ui = cliui({ + width: 25 + }) + + // using figlet font 'Shadow' rendering of text 'true' here + ui.div( + chalk.blue(' | \n __| __| | | _ \\ \n | | | | __/ \n \\__| _| \\__,_| \\___| \n ') + ) + + // relevant part is first line - leading whitespace should be preserved as left padding + const expected = [ + ' |', + ' __| __| | | _ \\', + ' | | | | __/', + ' \\__| _| \\__,_| \\___|', + ' ' + ] + + ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected) + chalk.enabled = enabled + chalk.level = level + }) + + it('correctly handles lack of ansi escape codes when measuring padding', () => { + const ui = cliui({ + width: 25 + }) + + // using figlet font 'Shadow' rendering of text 'true' here + ui.div( + ' | \n __| __| | | _ \\ \n | | | | __/ \n \\__| _| \\__,_| \\___| \n ' + ) + + // The difference + const expected = [ + ' |', + ' __| __| | | _ \\', + ' | | | | __/', + ' \\__| _| \\__,_| \\___|', + '' + ] + + ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected) + }) + + it('does not apply DSL if wrap is false', () => { + const ui = cliui({ + width: 40, + wrap: false + }) + + ui.div( + 'Usage: $0\ttwo\tthree' + ) + + ui.toString().should.eql('Usage: $0\ttwo\tthree') + }) + }) + }) +} + +module.exports = { runTests } From 786dbf7d4f339afd9f4e469b3dafcb78b313c8ec Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 22 Nov 2023 22:27:35 +1300 Subject: [PATCH 2/8] Remove stale comment --- test/deno/cliui-test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/deno/cliui-test.ts b/test/deno/cliui-test.ts index de7b6c4..cae566f 100644 --- a/test/deno/cliui-test.ts +++ b/test/deno/cliui-test.ts @@ -36,8 +36,6 @@ Deno.test('evenly divides text across columns if multiple columns are given', () }) // it should wrap each column appropriately. - // TODO: we should flesh out the Deno and ESM implementation - // such that it spreads words out over multiple columns appropriately: const expected = [ 'i am a string i am a i am a third', 'that should be second string that', From 5b1d70b54e4d1572ac261cf26dcccd907a5f693f Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 23 Nov 2023 20:25:11 +1300 Subject: [PATCH 3/8] Downgrade dependencies for node 12 compatibility --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 1c52359..28d3bdd 100644 --- a/package.json +++ b/package.json @@ -53,8 +53,8 @@ "wrap-ansi": "^8.1.0" }, "devDependencies": { - "@rollup/plugin-commonjs": "^25.0.0", - "@rollup/plugin-node-resolve": "^15.0.2", + "@rollup/plugin-commonjs": "^22.0.2", + "@rollup/plugin-node-resolve": "^14.1.0", "@types/node": "^14.0.27", "@types/wrap-ansi": "^8.0.1", "@typescript-eslint/eslint-plugin": "^4.0.0", @@ -67,12 +67,12 @@ "eslint-plugin-import": "^2.22.0", "eslint-plugin-node": "^11.1.0", "gts": "^3.0.0", - "mocha": "^10.0.0", + "mocha": "^9.2.2", "rimraf": "^3.0.2", "rollup": "^2.79.1", - "rollup-plugin-ts": "^3.2.0", + "rollup-plugin-ts": "^2.0.7", "standardx": "^7.0.0", - "typescript": "^4.0.0" + "typescript": "^4.6.4" }, "files": [ "build", From 25fe32623af123fd6d0adf3935f9e56dd8f9b726 Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 23 Nov 2023 20:27:16 +1300 Subject: [PATCH 4/8] Lock TypeScript to avoid issue with rollup@2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 28d3bdd..d5b646e 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "rollup": "^2.79.1", "rollup-plugin-ts": "^2.0.7", "standardx": "^7.0.0", - "typescript": "^4.6.4" + "typescript": "4.6.4" }, "files": [ "build", From 0a0a0e8b004d599ab0131efd1bbe4f60c1f9196f Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 23 Nov 2023 20:51:11 +1300 Subject: [PATCH 5/8] Rework coverage to avoid transpiled inclusions --- .github/workflows/ci.yaml | 2 +- package.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a873196..544eadc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -43,7 +43,7 @@ jobs: - run: npm install env: PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true - - run: npm test + - run: npm test-esm-only - run: npm run coverage deno: runs-on: ubuntu-latest diff --git a/package.json b/package.json index d5b646e..9c620bf 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "fix": "standardx --fix '**/*.ts' && standardx --fix '**/*.js' && standardx --fix '**/*.cjs'", "pretest": "rimraf build && tsc -p tsconfig.test.json && cross-env NODE_ENV=test npm run build:cjs", "test": "c8 mocha ./test/cjs/*.cjs ./test/esm/*.mjs", + "test-esm-only": "c8 mocha ./test/esm/*.mjs", "postest": "check", "coverage": "c8 report --check-coverage", "precompile": "rimraf build", From c558e8248b43a00332e344b47a4d57e328a6abfa Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 23 Nov 2023 20:54:19 +1300 Subject: [PATCH 6/8] Fix coverage CI --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 544eadc..8aa2751 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -43,7 +43,7 @@ jobs: - run: npm install env: PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true - - run: npm test-esm-only + - run: npm run test-esm-only - run: npm run coverage deno: runs-on: ubuntu-latest From efb4f22d125e2ed393dd462cec1b295aef72fc2b Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 23 Nov 2023 21:57:08 +1300 Subject: [PATCH 7/8] Remove stale dependency --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 9c620bf..e687e56 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "@rollup/plugin-commonjs": "^22.0.2", "@rollup/plugin-node-resolve": "^14.1.0", "@types/node": "^14.0.27", - "@types/wrap-ansi": "^8.0.1", "@typescript-eslint/eslint-plugin": "^4.0.0", "@typescript-eslint/parser": "^4.0.0", "c8": "^7.3.0", From 4bb46f504189d995e3532a7a9863c7007210009d Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 25 Nov 2023 09:44:25 +1300 Subject: [PATCH 8/8] Tell c8 about remap to fix coverage after rollup --- .github/workflows/ci.yaml | 2 +- package.json | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8aa2751..869277f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -43,7 +43,7 @@ jobs: - run: npm install env: PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true - - run: npm run test-esm-only + - run: npm run test - run: npm run coverage deno: runs-on: ubuntu-latest diff --git a/package.json b/package.json index e687e56..6dcd1aa 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,9 @@ "check": "standardx '**/*.ts' && standardx '**/*.js' && standardx '**/*.cjs'", "fix": "standardx --fix '**/*.ts' && standardx --fix '**/*.js' && standardx --fix '**/*.cjs'", "pretest": "rimraf build && tsc -p tsconfig.test.json && cross-env NODE_ENV=test npm run build:cjs", - "test": "c8 mocha ./test/cjs/*.cjs ./test/esm/*.mjs", - "test-esm-only": "c8 mocha ./test/esm/*.mjs", + "test": "c8 --exclude-after-remap mocha ./test/cjs/*.cjs ./test/esm/*.mjs", "postest": "check", - "coverage": "c8 report --check-coverage", + "coverage": "c8 --exclude-after-remap report --check-coverage", "precompile": "rimraf build", "compile": "tsc", "postcompile": "npm run build:cjs",