From cd291e7aa52e56fc45f8245e67c77e0ed3711b07 Mon Sep 17 00:00:00 2001 From: Gar Date: Tue, 14 Nov 2023 13:36:16 -0800 Subject: [PATCH] fix: refactor search formatting code (#6995) output is the same but the code is more streamlined, and passes in the stripAnsi function as a "clean" function that can be extended or replaced later --- lib/commands/search.js | 6 +- lib/utils/format-search-stream.js | 194 ++++++++---------- .../test/lib/commands/search.js.test.cjs | 28 +-- 3 files changed, 100 insertions(+), 128 deletions(-) diff --git a/lib/commands/search.js b/lib/commands/search.js index 8d6744f591333..2af4daa211dca 100644 --- a/lib/commands/search.js +++ b/lib/commands/search.js @@ -81,12 +81,12 @@ class Search extends BaseCommand { const filterStream = new FilterStream() - // Grab a configured output stream that will spit out packages in the - // desired format. + const { default: stripAnsi } = await import('strip-ansi') + // Grab a configured output stream that will spit out packages in the desired format. const outputStream = await formatSearchStream({ args, // --searchinclude options are not highlighted ...opts, - }) + }, stripAnsi) log.silly('search', 'searching packages') const p = new Pipeline( diff --git a/lib/utils/format-search-stream.js b/lib/utils/format-search-stream.js index 766028bddc797..cb29151e7c2e7 100644 --- a/lib/utils/format-search-stream.js +++ b/lib/utils/format-search-stream.js @@ -15,11 +15,8 @@ const columnify = require('columnify') // The returned stream will format this package data // into a byte stream of formatted, displayable output. -let stripAnsi -module.exports = async (opts) => { - stripAnsi = await import('strip-ansi') - stripAnsi = stripAnsi.default - return opts.json ? new JSONOutputStream() : new TextOutputStream(opts) +module.exports = async (opts, clean) => { + return opts.json ? new JSONOutputStream() : new TextOutputStream(opts, clean) } class JSONOutputStream extends Minipass { @@ -43,121 +40,96 @@ class JSONOutputStream extends Minipass { } class TextOutputStream extends Minipass { - constructor (opts) { + #clean + #opts + #line = 0 + + constructor (opts, clean) { super() - this._opts = opts - this._line = 0 + this.#clean = clean + this.#opts = opts } write (pkg) { - return super.write(prettify(pkg, ++this._line, this._opts)) - } -} - -function prettify (data, num, opts) { - var truncate = !opts.long - - var pkg = normalizePackage(data, opts) - - var columns = ['name', 'description', 'author', 'date', 'version', 'keywords'] - - if (opts.parseable) { - return columns.map(function (col) { - return pkg[col] && ('' + pkg[col]).replace(/\t/g, ' ') - }).join('\t') + return super.write(this.#prettify(pkg)) } - // stdout in tap is never a tty - /* istanbul ignore next */ - const maxWidth = process.stdout.isTTY ? process.stdout.getWindowSize()[0] : Infinity - let output = columnify( - [pkg], - { - include: columns, - showHeaders: num <= 1, - columnSplitter: ' | ', - truncate: truncate, - config: { - name: { minWidth: 25, maxWidth: 25, truncate: false, truncateMarker: '' }, - description: { minWidth: 20, maxWidth: 20 }, - author: { minWidth: 15, maxWidth: 15 }, - date: { maxWidth: 11 }, - version: { minWidth: 8, maxWidth: 8 }, - keywords: { maxWidth: Infinity }, - }, + #prettify (data) { + const pkg = { + author: data.maintainers.map((m) => `=${this.#clean(m.username)}`).join(' '), + date: 'prehistoric', + description: this.#clean(data.description ?? ''), + keywords: '', + name: this.#clean(data.name), + version: data.version, + } + if (Array.isArray(data.keywords)) { + pkg.keywords = data.keywords.map((k) => this.#clean(k)).join(' ') + } else if (typeof data.keywords === 'string') { + pkg.keywords = this.#clean(data.keywords.replace(/[,\s]+/, ' ')) + } + if (data.date) { + pkg.date = data.date.toISOString().split('T')[0] // remove time } - ).split('\n').map(line => line.slice(0, maxWidth)).join('\n') - - if (opts.color) { - output = highlightSearchTerms(output, opts.args) - } - - return output -} - -var colors = [31, 33, 32, 36, 34, 35] -var cl = colors.length - -function addColorMarker (str, arg, i) { - var m = i % cl + 1 - var markStart = String.fromCharCode(m) - var markEnd = String.fromCharCode(0) - - if (arg.charAt(0) === '/') { - return str.replace( - new RegExp(arg.slice(1, -1), 'gi'), - bit => markStart + bit + markEnd - ) - } - - // just a normal string, do the split/map thing - var pieces = str.toLowerCase().split(arg.toLowerCase()) - var p = 0 - - return pieces.map(function (piece) { - piece = str.slice(p, p + piece.length) - var mark = markStart + - str.slice(p + piece.length, p + piece.length + arg.length) + - markEnd - p += piece.length + arg.length - return piece + mark - }).join('') -} - -function colorize (line) { - for (var i = 0; i < cl; i++) { - var m = i + 1 - var color = '\u001B[' + colors[i] + 'm' - line = line.split(String.fromCharCode(m)).join(color) - } - var uncolor = '\u001B[0m' - return line.split('\u0000').join(uncolor) -} -function highlightSearchTerms (str, terms) { - terms.forEach(function (arg, i) { - str = addColorMarker(str, arg, i) - }) + const columns = ['name', 'description', 'author', 'date', 'version', 'keywords'] + if (this.#opts.parseable) { + return columns.map((col) => pkg[col] && ('' + pkg[col]).replace(/\t/g, ' ')).join('\t') + } - return colorize(str).trim() -} + // stdout in tap is never a tty + /* istanbul ignore next */ + const maxWidth = process.stdout.isTTY ? process.stdout.getWindowSize()[0] : Infinity + let output = columnify( + [pkg], + { + include: columns, + showHeaders: ++this.#line <= 1, + columnSplitter: ' | ', + truncate: !this.#opts.long, + config: { + name: { minWidth: 25, maxWidth: 25, truncate: false, truncateMarker: '' }, + description: { minWidth: 20, maxWidth: 20 }, + author: { minWidth: 15, maxWidth: 15 }, + date: { maxWidth: 11 }, + version: { minWidth: 8, maxWidth: 8 }, + keywords: { maxWidth: Infinity }, + }, + } + ).split('\n').map(line => line.slice(0, maxWidth)).join('\n') + + if (!this.#opts.color) { + return output + } -function normalizePackage (data, opts) { - return { - name: stripAnsi(data.name), - description: stripAnsi(data.description ?? ''), - author: data.maintainers.map((m) => `=${stripAnsi(m.username)}`).join(' '), - keywords: Array.isArray(data.keywords) - ? data.keywords.map(stripAnsi).join(' ') - : typeof data.keywords === 'string' - ? stripAnsi(data.keywords.replace(/[,\s]+/, ' ')) - : '', - version: data.version, - date: (data.date && - (data.date.toISOString() // remove time - .split('T').join(' ') - .replace(/:[0-9]{2}\.[0-9]{3}Z$/, '')) - .slice(0, -5)) || - 'prehistoric', + const colors = ['31m', '33m', '32m', '36m', '34m', '35m'] + + this.#opts.args.forEach((arg, i) => { + const markStart = String.fromCharCode(i % colors.length + 1) + const markEnd = String.fromCharCode(0) + + if (arg.charAt(0) === '/') { + output = output.replace( + new RegExp(arg.slice(1, -1), 'gi'), + bit => `${markStart}${bit}${markEnd}` + ) + } else { + // just a normal string, do the split/map thing + let p = 0 + + output = output.toLowerCase().split(arg.toLowerCase()).map(piece => { + piece = output.slice(p, p + piece.length) + p += piece.length + const mark = `${markStart}${output.slice(p, p + arg.length)}${markEnd}` + p += arg.length + return `${piece}${mark}` + }).join('') + } + }) + + for (let i = 1; i <= colors.length; i++) { + output = output.split(String.fromCharCode(i)).join(`\u001B[${colors[i - 1]}`) + } + return output.split('\u0000').join('\u001B[0m').trim() } } diff --git a/tap-snapshots/test/lib/commands/search.js.test.cjs b/tap-snapshots/test/lib/commands/search.js.test.cjs index 8b2796c1f92c3..33d4a0533b93c 100644 --- a/tap-snapshots/test/lib/commands/search.js.test.cjs +++ b/tap-snapshots/test/lib/commands/search.js.test.cjs @@ -46,20 +46,20 @@ pkg-no-desc | | =lukekarrys | 2019-09-26 ` exports[`test/lib/commands/search.js TAP search --parseable > should have expected search results as parseable 1`] = ` -libnpm Collection of programmatic APIs for the npm CLI =nlf =ruyadorno =darcyclarke =isaacs 2019-07-16 3.0.1 npm api package manager lib -libnpmaccess programmatic library for \`npm access\` commands =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 4.0.1 libnpmaccess -@evocateur/libnpmaccess programmatic library for \`npm access\` commands =evocateur 2019-07-16 3.1.2 -@evocateur/libnpmpublish Programmatic API for the bits behind npm publish and unpublish =evocateur 2019-07-16 1.2.2 -libnpmorg Programmatic api for \`npm org\` commands =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 2.0.1 libnpm npm package manager api orgs teams -libnpmsearch Programmatic API for searching in npm and compatible registries. =nlf =ruyadorno =darcyclarke =isaacs 2020-12-08 3.1.0 npm search api libnpm -libnpmteam npm Team management APIs =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 2.0.2 -libnpmhook programmatic API for managing npm registry hooks =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 6.0.1 npm hooks registry npm api -libnpmpublish Programmatic API for the bits behind npm publish and unpublish =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 4.0.0 -libnpmfund Programmatic API for npm fund =nlf =ruyadorno =darcyclarke =isaacs 2020-12-08 1.0.2 npm npmcli libnpm cli git fund gitfund -@npmcli/map-workspaces Retrieves a name:pathname Map for a given workspaces config =nlf =ruyadorno =darcyclarke =isaacs 2020-09-30 1.0.1 npm bad map npmcli libnpm cli workspaces map-workspaces -libnpmversion library to do the things that 'npm version' does =nlf =ruyadorno =darcyclarke =isaacs 2020-11-04 1.0.7 -@types/libnpmsearch TypeScript definitions for libnpmsearch =types 2019-09-26 2.0.1 -pkg-no-desc =lukekarrys 2019-09-26 1.0.0 +libnpm Collection of programmatic APIs for the npm CLI =nlf =ruyadorno =darcyclarke =isaacs 2019-07-16 3.0.1 npm api package manager lib +libnpmaccess programmatic library for \`npm access\` commands =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 4.0.1 libnpmaccess +@evocateur/libnpmaccess programmatic library for \`npm access\` commands =evocateur 2019-07-16 3.1.2 +@evocateur/libnpmpublish Programmatic API for the bits behind npm publish and unpublish =evocateur 2019-07-16 1.2.2 +libnpmorg Programmatic api for \`npm org\` commands =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 2.0.1 libnpm npm package manager api orgs teams +libnpmsearch Programmatic API for searching in npm and compatible registries. =nlf =ruyadorno =darcyclarke =isaacs 2020-12-08 3.1.0 npm search api libnpm +libnpmteam npm Team management APIs =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 2.0.2 +libnpmhook programmatic API for managing npm registry hooks =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 6.0.1 npm hooks registry npm api +libnpmpublish Programmatic API for the bits behind npm publish and unpublish =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 4.0.0 +libnpmfund Programmatic API for npm fund =nlf =ruyadorno =darcyclarke =isaacs 2020-12-08 1.0.2 npm npmcli libnpm cli git fund gitfund +@npmcli/map-workspaces Retrieves a name:pathname Map for a given workspaces config =nlf =ruyadorno =darcyclarke =isaacs 2020-09-30 1.0.1 npm bad map npmcli libnpm cli workspaces map-workspaces +libnpmversion library to do the things that 'npm version' does =nlf =ruyadorno =darcyclarke =isaacs 2020-11-04 1.0.7 +@types/libnpmsearch TypeScript definitions for libnpmsearch =types 2019-09-26 2.0.1 +pkg-no-desc =lukekarrys 2019-09-26 1.0.0 ` exports[`test/lib/commands/search.js TAP search > should have filtered expected search results 1`] = `