Skip to content

Commit cd291e7

Browse files
authored
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
1 parent e9ec2f7 commit cd291e7

File tree

3 files changed

+100
-128
lines changed

3 files changed

+100
-128
lines changed

lib/commands/search.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,12 @@ class Search extends BaseCommand {
8181

8282
const filterStream = new FilterStream()
8383

84-
// Grab a configured output stream that will spit out packages in the
85-
// desired format.
84+
const { default: stripAnsi } = await import('strip-ansi')
85+
// Grab a configured output stream that will spit out packages in the desired format.
8686
const outputStream = await formatSearchStream({
8787
args, // --searchinclude options are not highlighted
8888
...opts,
89-
})
89+
}, stripAnsi)
9090

9191
log.silly('search', 'searching packages')
9292
const p = new Pipeline(

lib/utils/format-search-stream.js

+83-111
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,8 @@ const columnify = require('columnify')
1515
// The returned stream will format this package data
1616
// into a byte stream of formatted, displayable output.
1717

18-
let stripAnsi
19-
module.exports = async (opts) => {
20-
stripAnsi = await import('strip-ansi')
21-
stripAnsi = stripAnsi.default
22-
return opts.json ? new JSONOutputStream() : new TextOutputStream(opts)
18+
module.exports = async (opts, clean) => {
19+
return opts.json ? new JSONOutputStream() : new TextOutputStream(opts, clean)
2320
}
2421

2522
class JSONOutputStream extends Minipass {
@@ -43,121 +40,96 @@ class JSONOutputStream extends Minipass {
4340
}
4441

4542
class TextOutputStream extends Minipass {
46-
constructor (opts) {
43+
#clean
44+
#opts
45+
#line = 0
46+
47+
constructor (opts, clean) {
4748
super()
48-
this._opts = opts
49-
this._line = 0
49+
this.#clean = clean
50+
this.#opts = opts
5051
}
5152

5253
write (pkg) {
53-
return super.write(prettify(pkg, ++this._line, this._opts))
54-
}
55-
}
56-
57-
function prettify (data, num, opts) {
58-
var truncate = !opts.long
59-
60-
var pkg = normalizePackage(data, opts)
61-
62-
var columns = ['name', 'description', 'author', 'date', 'version', 'keywords']
63-
64-
if (opts.parseable) {
65-
return columns.map(function (col) {
66-
return pkg[col] && ('' + pkg[col]).replace(/\t/g, ' ')
67-
}).join('\t')
54+
return super.write(this.#prettify(pkg))
6855
}
6956

70-
// stdout in tap is never a tty
71-
/* istanbul ignore next */
72-
const maxWidth = process.stdout.isTTY ? process.stdout.getWindowSize()[0] : Infinity
73-
let output = columnify(
74-
[pkg],
75-
{
76-
include: columns,
77-
showHeaders: num <= 1,
78-
columnSplitter: ' | ',
79-
truncate: truncate,
80-
config: {
81-
name: { minWidth: 25, maxWidth: 25, truncate: false, truncateMarker: '' },
82-
description: { minWidth: 20, maxWidth: 20 },
83-
author: { minWidth: 15, maxWidth: 15 },
84-
date: { maxWidth: 11 },
85-
version: { minWidth: 8, maxWidth: 8 },
86-
keywords: { maxWidth: Infinity },
87-
},
57+
#prettify (data) {
58+
const pkg = {
59+
author: data.maintainers.map((m) => `=${this.#clean(m.username)}`).join(' '),
60+
date: 'prehistoric',
61+
description: this.#clean(data.description ?? ''),
62+
keywords: '',
63+
name: this.#clean(data.name),
64+
version: data.version,
65+
}
66+
if (Array.isArray(data.keywords)) {
67+
pkg.keywords = data.keywords.map((k) => this.#clean(k)).join(' ')
68+
} else if (typeof data.keywords === 'string') {
69+
pkg.keywords = this.#clean(data.keywords.replace(/[,\s]+/, ' '))
70+
}
71+
if (data.date) {
72+
pkg.date = data.date.toISOString().split('T')[0] // remove time
8873
}
89-
).split('\n').map(line => line.slice(0, maxWidth)).join('\n')
90-
91-
if (opts.color) {
92-
output = highlightSearchTerms(output, opts.args)
93-
}
94-
95-
return output
96-
}
97-
98-
var colors = [31, 33, 32, 36, 34, 35]
99-
var cl = colors.length
100-
101-
function addColorMarker (str, arg, i) {
102-
var m = i % cl + 1
103-
var markStart = String.fromCharCode(m)
104-
var markEnd = String.fromCharCode(0)
105-
106-
if (arg.charAt(0) === '/') {
107-
return str.replace(
108-
new RegExp(arg.slice(1, -1), 'gi'),
109-
bit => markStart + bit + markEnd
110-
)
111-
}
112-
113-
// just a normal string, do the split/map thing
114-
var pieces = str.toLowerCase().split(arg.toLowerCase())
115-
var p = 0
116-
117-
return pieces.map(function (piece) {
118-
piece = str.slice(p, p + piece.length)
119-
var mark = markStart +
120-
str.slice(p + piece.length, p + piece.length + arg.length) +
121-
markEnd
122-
p += piece.length + arg.length
123-
return piece + mark
124-
}).join('')
125-
}
126-
127-
function colorize (line) {
128-
for (var i = 0; i < cl; i++) {
129-
var m = i + 1
130-
var color = '\u001B[' + colors[i] + 'm'
131-
line = line.split(String.fromCharCode(m)).join(color)
132-
}
133-
var uncolor = '\u001B[0m'
134-
return line.split('\u0000').join(uncolor)
135-
}
13674

137-
function highlightSearchTerms (str, terms) {
138-
terms.forEach(function (arg, i) {
139-
str = addColorMarker(str, arg, i)
140-
})
75+
const columns = ['name', 'description', 'author', 'date', 'version', 'keywords']
76+
if (this.#opts.parseable) {
77+
return columns.map((col) => pkg[col] && ('' + pkg[col]).replace(/\t/g, ' ')).join('\t')
78+
}
14179

142-
return colorize(str).trim()
143-
}
80+
// stdout in tap is never a tty
81+
/* istanbul ignore next */
82+
const maxWidth = process.stdout.isTTY ? process.stdout.getWindowSize()[0] : Infinity
83+
let output = columnify(
84+
[pkg],
85+
{
86+
include: columns,
87+
showHeaders: ++this.#line <= 1,
88+
columnSplitter: ' | ',
89+
truncate: !this.#opts.long,
90+
config: {
91+
name: { minWidth: 25, maxWidth: 25, truncate: false, truncateMarker: '' },
92+
description: { minWidth: 20, maxWidth: 20 },
93+
author: { minWidth: 15, maxWidth: 15 },
94+
date: { maxWidth: 11 },
95+
version: { minWidth: 8, maxWidth: 8 },
96+
keywords: { maxWidth: Infinity },
97+
},
98+
}
99+
).split('\n').map(line => line.slice(0, maxWidth)).join('\n')
100+
101+
if (!this.#opts.color) {
102+
return output
103+
}
144104

145-
function normalizePackage (data, opts) {
146-
return {
147-
name: stripAnsi(data.name),
148-
description: stripAnsi(data.description ?? ''),
149-
author: data.maintainers.map((m) => `=${stripAnsi(m.username)}`).join(' '),
150-
keywords: Array.isArray(data.keywords)
151-
? data.keywords.map(stripAnsi).join(' ')
152-
: typeof data.keywords === 'string'
153-
? stripAnsi(data.keywords.replace(/[,\s]+/, ' '))
154-
: '',
155-
version: data.version,
156-
date: (data.date &&
157-
(data.date.toISOString() // remove time
158-
.split('T').join(' ')
159-
.replace(/:[0-9]{2}\.[0-9]{3}Z$/, ''))
160-
.slice(0, -5)) ||
161-
'prehistoric',
105+
const colors = ['31m', '33m', '32m', '36m', '34m', '35m']
106+
107+
this.#opts.args.forEach((arg, i) => {
108+
const markStart = String.fromCharCode(i % colors.length + 1)
109+
const markEnd = String.fromCharCode(0)
110+
111+
if (arg.charAt(0) === '/') {
112+
output = output.replace(
113+
new RegExp(arg.slice(1, -1), 'gi'),
114+
bit => `${markStart}${bit}${markEnd}`
115+
)
116+
} else {
117+
// just a normal string, do the split/map thing
118+
let p = 0
119+
120+
output = output.toLowerCase().split(arg.toLowerCase()).map(piece => {
121+
piece = output.slice(p, p + piece.length)
122+
p += piece.length
123+
const mark = `${markStart}${output.slice(p, p + arg.length)}${markEnd}`
124+
p += arg.length
125+
return `${piece}${mark}`
126+
}).join('')
127+
}
128+
})
129+
130+
for (let i = 1; i <= colors.length; i++) {
131+
output = output.split(String.fromCharCode(i)).join(`\u001B[${colors[i - 1]}`)
132+
}
133+
return output.split('\u0000').join('\u001B[0m').trim()
162134
}
163135
}

tap-snapshots/test/lib/commands/search.js.test.cjs

+14-14
Original file line numberDiff line numberDiff line change
@@ -46,20 +46,20 @@ pkg-no-desc | | =lukekarrys | 2019-09-26
4646
`
4747

4848
exports[`test/lib/commands/search.js TAP search <name> --parseable > should have expected search results as parseable 1`] = `
49-
libnpm Collection of programmatic APIs for the npm CLI =nlf =ruyadorno =darcyclarke =isaacs 2019-07-16 3.0.1 npm api package manager lib
50-
libnpmaccess programmatic library for \`npm access\` commands =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 4.0.1 libnpmaccess
51-
@evocateur/libnpmaccess programmatic library for \`npm access\` commands =evocateur 2019-07-16 3.1.2
52-
@evocateur/libnpmpublish Programmatic API for the bits behind npm publish and unpublish =evocateur 2019-07-16 1.2.2
53-
libnpmorg Programmatic api for \`npm org\` commands =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 2.0.1 libnpm npm package manager api orgs teams
54-
libnpmsearch Programmatic API for searching in npm and compatible registries. =nlf =ruyadorno =darcyclarke =isaacs 2020-12-08 3.1.0 npm search api libnpm
55-
libnpmteam npm Team management APIs =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 2.0.2
56-
libnpmhook programmatic API for managing npm registry hooks =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 6.0.1 npm hooks registry npm api
57-
libnpmpublish Programmatic API for the bits behind npm publish and unpublish =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 4.0.0
58-
libnpmfund Programmatic API for npm fund =nlf =ruyadorno =darcyclarke =isaacs 2020-12-08 1.0.2 npm npmcli libnpm cli git fund gitfund
59-
@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
60-
libnpmversion library to do the things that 'npm version' does =nlf =ruyadorno =darcyclarke =isaacs 2020-11-04 1.0.7
61-
@types/libnpmsearch TypeScript definitions for libnpmsearch =types 2019-09-26 2.0.1
62-
pkg-no-desc =lukekarrys 2019-09-26 1.0.0
49+
libnpm Collection of programmatic APIs for the npm CLI =nlf =ruyadorno =darcyclarke =isaacs 2019-07-16 3.0.1 npm api package manager lib
50+
libnpmaccess programmatic library for \`npm access\` commands =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 4.0.1 libnpmaccess
51+
@evocateur/libnpmaccess programmatic library for \`npm access\` commands =evocateur 2019-07-16 3.1.2
52+
@evocateur/libnpmpublish Programmatic API for the bits behind npm publish and unpublish =evocateur 2019-07-16 1.2.2
53+
libnpmorg Programmatic api for \`npm org\` commands =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 2.0.1 libnpm npm package manager api orgs teams
54+
libnpmsearch Programmatic API for searching in npm and compatible registries. =nlf =ruyadorno =darcyclarke =isaacs 2020-12-08 3.1.0 npm search api libnpm
55+
libnpmteam npm Team management APIs =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 2.0.2
56+
libnpmhook programmatic API for managing npm registry hooks =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 6.0.1 npm hooks registry npm api
57+
libnpmpublish Programmatic API for the bits behind npm publish and unpublish =nlf =ruyadorno =darcyclarke =isaacs 2020-11-03 4.0.0
58+
libnpmfund Programmatic API for npm fund =nlf =ruyadorno =darcyclarke =isaacs 2020-12-08 1.0.2 npm npmcli libnpm cli git fund gitfund
59+
@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
60+
libnpmversion library to do the things that 'npm version' does =nlf =ruyadorno =darcyclarke =isaacs 2020-11-04 1.0.7
61+
@types/libnpmsearch TypeScript definitions for libnpmsearch =types 2019-09-26 2.0.1
62+
pkg-no-desc =lukekarrys 2019-09-26 1.0.0
6363
`
6464

6565
exports[`test/lib/commands/search.js TAP search <name> > should have filtered expected search results 1`] = `

0 commit comments

Comments
 (0)