Skip to content

Commit d531f8b

Browse files
committed
fix: Remove table output from search and tar summary
This removes table output from `npm search` and from all commands that log a summary of tarball content (`npm publish` and `npm pack`). Table output is discouraged in a cli for accessibility reasons.
1 parent dfa4cab commit d531f8b

File tree

9 files changed

+1283
-432
lines changed

9 files changed

+1283
-432
lines changed

lib/commands/search.js

+3-45
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,9 @@
1-
const { Minipass } = require('minipass')
21
const Pipeline = require('minipass-pipeline')
32
const libSearch = require('libnpmsearch')
43
const { log, output } = require('proc-log')
54

65
const formatSearchStream = require('../utils/format-search-stream.js')
76

8-
function filter (data, include, exclude) {
9-
const words = [data.name]
10-
.concat(data.maintainers.map(m => `=${m.username}`))
11-
.concat(data.keywords || [])
12-
.map(f => f && f.trim && f.trim())
13-
.filter(f => f)
14-
.join(' ')
15-
.toLowerCase()
16-
17-
if (exclude.find(e => match(words, e))) {
18-
return false
19-
}
20-
21-
return true
22-
}
23-
24-
function match (words, pattern) {
25-
if (pattern.startsWith('/')) {
26-
if (pattern.endsWith('/')) {
27-
pattern = pattern.slice(0, -1)
28-
}
29-
pattern = new RegExp(pattern.slice(1))
30-
return words.match(pattern)
31-
}
32-
return words.indexOf(pattern) !== -1
33-
}
34-
357
const BaseCommand = require('../base-command.js')
368
class Search extends BaseCommand {
379
static description = 'Search for packages'
@@ -57,7 +29,7 @@ class Search extends BaseCommand {
5729
const opts = {
5830
...this.npm.flatOptions,
5931
...this.npm.flatOptions.search,
60-
include: args.map(s => s.toLowerCase()).filter(s => s),
32+
include: args.map(s => s.toLowerCase()).filter(Boolean),
6133
exclude: this.npm.flatOptions.search.exclude.split(/\s+/),
6234
}
6335

@@ -68,30 +40,16 @@ class Search extends BaseCommand {
6840
// Used later to figure out whether we had any packages go out
6941
let anyOutput = false
7042

71-
class FilterStream extends Minipass {
72-
constructor () {
73-
super({ objectMode: true })
74-
}
75-
76-
write (pkg) {
77-
if (filter(pkg, opts.include, opts.exclude)) {
78-
super.write(pkg)
79-
}
80-
}
81-
}
82-
83-
const filterStream = new FilterStream()
84-
8543
// Grab a configured output stream that will spit out packages in the desired format.
86-
const outputStream = await formatSearchStream({
44+
const outputStream = formatSearchStream({
8745
args, // --searchinclude options are not highlighted
8846
...opts,
47+
npm: this.npm,
8948
})
9049

9150
log.silly('search', 'searching packages')
9251
const p = new Pipeline(
9352
libSearch.stream(opts.include, opts),
94-
filterStream,
9553
outputStream
9654
)
9755

lib/utils/format-search-stream.js

+109-73
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
const { stripVTControlCharacters } = require('node:util')
1+
/* eslint-disable max-len */
2+
const { stripVTControlCharacters: strip } = require('node:util')
23
const { Minipass } = require('minipass')
3-
const columnify = require('columnify')
44

55
// This module consumes package data in the following format:
66
//
@@ -16,14 +16,48 @@ const columnify = require('columnify')
1616
// The returned stream will format this package data
1717
// into a byte stream of formatted, displayable output.
1818

19-
module.exports = async (opts) => {
20-
return opts.json ? new JSONOutputStream() : new TextOutputStream(opts)
19+
function filter (data, exclude) {
20+
const words = [data.name]
21+
.concat(data.maintainers.map(m => m.username))
22+
.concat(data.keywords || [])
23+
.map(f => f?.trim?.())
24+
.filter(Boolean)
25+
.join(' ')
26+
.toLowerCase()
27+
28+
if (exclude.find(pattern => {
29+
// Treats both /foo and /foo/ as regex searches
30+
if (pattern.startsWith('/')) {
31+
if (pattern.endsWith('/')) {
32+
pattern = pattern.slice(0, -1)
33+
}
34+
return words.match(new RegExp(pattern.slice(1)))
35+
}
36+
return words.includes(pattern)
37+
})) {
38+
return false
39+
}
40+
41+
return true
42+
}
43+
44+
module.exports = (opts) => {
45+
return opts.json ? new JSONOutputStream(opts) : new TextOutputStream(opts)
2146
}
2247

2348
class JSONOutputStream extends Minipass {
2449
#didFirst = false
50+
#exclude
51+
52+
constructor (opts) {
53+
super()
54+
this.#exclude = opts.exclude
55+
}
2556

2657
write (obj) {
58+
if (!filter(obj, this.#exclude)) {
59+
return
60+
}
2761
if (!this.#didFirst) {
2862
super.write('[\n')
2963
this.#didFirst = true
@@ -41,94 +75,96 @@ class JSONOutputStream extends Minipass {
4175
}
4276

4377
class TextOutputStream extends Minipass {
44-
#opts
45-
#line = 0
78+
#args
79+
#chalk
80+
#exclude
81+
#parseable
4682

4783
constructor (opts) {
4884
super()
49-
this.#opts = opts
85+
this.#args = opts.args.map(s => s.toLowerCase()).filter(Boolean)
86+
this.#chalk = opts.npm.chalk
87+
this.#exclude = opts.exclude
88+
this.#parseable = opts.parseable
5089
}
5190

52-
write (pkg) {
53-
return super.write(this.#prettify(pkg))
54-
}
55-
56-
#prettify (data) {
91+
write (data) {
92+
if (!filter(data, this.#exclude)) {
93+
return
94+
}
95+
// Normalize
5796
const pkg = {
58-
author: data.maintainers.map((m) => `=${stripVTControlCharacters(m.username)}`).join(' '),
59-
date: 'prehistoric',
60-
description: stripVTControlCharacters(data.description ?? ''),
61-
keywords: '',
62-
name: stripVTControlCharacters(data.name),
97+
authors: data.maintainers.map((m) => `${strip(m.username)}`).join(' '),
98+
publisher: strip(data.publisher.username),
99+
date: data.date ? data.date.toISOString().slice(0, 10) : 'prehistoric',
100+
description: strip(data.description ?? ''),
101+
keywords: [],
102+
name: strip(data.name),
63103
version: data.version,
64104
}
65105
if (Array.isArray(data.keywords)) {
66-
pkg.keywords = data.keywords.map((k) => stripVTControlCharacters(k)).join(' ')
106+
pkg.keywords = data.keywords.map(strip)
67107
} else if (typeof data.keywords === 'string') {
68-
pkg.keywords = stripVTControlCharacters(data.keywords.replace(/[,\s]+/, ' '))
69-
}
70-
if (data.date) {
71-
pkg.date = data.date.toISOString().split('T')[0] // remove time
108+
pkg.keywords = strip(data.keywords.replace(/[,\s]+/, ' ')).split(' ')
72109
}
73110

74-
const columns = ['name', 'description', 'author', 'date', 'version', 'keywords']
75-
if (this.#opts.parseable) {
76-
return columns.map((col) => pkg[col] && ('' + pkg[col]).replace(/\t/g, ' ')).join('\t')
111+
let output
112+
if (this.#parseable) {
113+
output = [pkg.name, pkg.description, pkg.author, pkg.date, pkg.version, pkg.keywords]
114+
.filter(Boolean)
115+
.map(col => ('' + col).replace(/\t/g, ' ')).join('\t')
116+
return super.write(output)
77117
}
78118

79-
// stdout in tap is never a tty
80-
/* istanbul ignore next */
81-
const maxWidth = process.stdout.isTTY ? process.stdout.getWindowSize()[0] : Infinity
82-
let output = columnify(
83-
[pkg],
84-
{
85-
include: columns,
86-
showHeaders: ++this.#line <= 1,
87-
columnSplitter: ' | ',
88-
truncate: !this.#opts.long,
89-
config: {
90-
name: { minWidth: 25, maxWidth: 25, truncate: false, truncateMarker: '' },
91-
description: { minWidth: 20, maxWidth: 20 },
92-
author: { minWidth: 15, maxWidth: 15 },
93-
date: { maxWidth: 11 },
94-
version: { minWidth: 8, maxWidth: 8 },
95-
keywords: { maxWidth: Infinity },
96-
},
119+
const keywords = pkg.keywords.map(k => {
120+
if (this.#args.includes(k)) {
121+
return this.#chalk.cyan(k)
122+
} else {
123+
return k
124+
}
125+
}).join(' ')
126+
127+
let description = []
128+
for (const arg of this.#args) {
129+
const finder = pkg.description.toLowerCase().split(arg.toLowerCase())
130+
let p = 0
131+
for (const f of finder) {
132+
description.push(pkg.description.slice(p, p + f.length))
133+
const word = pkg.description.slice(p + f.length, p + f.length + arg.length)
134+
description.push(this.#chalk.cyan(word))
135+
p += f.length + arg.length
97136
}
98-
).split('\n').map(line => line.slice(0, maxWidth)).join('\n')
99-
100-
if (!this.#opts.color) {
101-
return output
102137
}
103-
104-
const colors = ['31m', '33m', '32m', '36m', '34m', '35m']
105-
106-
this.#opts.args.forEach((arg, i) => {
107-
const markStart = String.fromCharCode(i % colors.length + 1)
108-
const markEnd = String.fromCharCode(0)
109-
110-
if (arg.charAt(0) === '/') {
111-
output = output.replace(
112-
new RegExp(arg.slice(1, -1), 'gi'),
113-
bit => `${markStart}${bit}${markEnd}`
114-
)
115-
} else {
116-
// just a normal string, do the split/map thing
138+
description = description.filter(Boolean)
139+
let name = pkg.name
140+
if (this.#args.includes(pkg.name)) {
141+
name = this.#chalk.cyan(pkg.name)
142+
} else {
143+
name = []
144+
for (const arg of this.#args) {
145+
const finder = pkg.name.toLowerCase().split(arg.toLowerCase())
117146
let p = 0
118-
119-
output = output.toLowerCase().split(arg.toLowerCase()).map(piece => {
120-
piece = output.slice(p, p + piece.length)
121-
p += piece.length
122-
const mark = `${markStart}${output.slice(p, p + arg.length)}${markEnd}`
123-
p += arg.length
124-
return `${piece}${mark}`
125-
}).join('')
147+
for (const f of finder) {
148+
name.push(pkg.name.slice(p, p + f.length))
149+
const word = pkg.name.slice(p + f.length, p + f.length + arg.length)
150+
name.push(this.#chalk.cyan(word))
151+
p += f.length + arg.length
152+
}
126153
}
127-
})
154+
name = this.#chalk.blue(name.join(''))
155+
}
128156

129-
for (let i = 1; i <= colors.length; i++) {
130-
output = output.split(String.fromCharCode(i)).join(`\u001B[${colors[i - 1]}`)
157+
if (description.length) {
158+
output = `${name}\n${description.join('')}\n`
159+
} else {
160+
output = `${name}\n`
161+
}
162+
output += `Version ${this.#chalk.blue(pkg.version)} published ${this.#chalk.blue(pkg.date)} by ${this.#chalk.blue(pkg.publisher)}\n`
163+
output += `Maintainers: ${pkg.authors}\n`
164+
if (keywords) {
165+
output += `Keywords: ${keywords}\n`
131166
}
132-
return output.split('\u0000').join('\u001B[0m').trim()
167+
output += `${this.#chalk.blue(`https://npm.im/${pkg.name}`)}\n`
168+
return super.write(output)
133169
}
134170
}

lib/utils/tar.js

+22-47
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ const tar = require('tar')
22
const ssri = require('ssri')
33
const { log } = require('proc-log')
44
const formatBytes = require('./format-bytes.js')
5-
const columnify = require('columnify')
65
const localeCompare = require('@isaacs/string-locale-compare')('en', {
76
sensitivity: 'case',
87
numeric: true,
@@ -12,60 +11,36 @@ const logTar = (tarball, opts = {}) => {
1211
const { unicode = false } = opts
1312
log.notice('')
1413
log.notice('', `${unicode ? '📦 ' : 'package:'} ${tarball.name}@${tarball.version}`)
15-
log.notice('=== Tarball Contents ===')
14+
log.notice('Tarball Contents')
1615
if (tarball.files.length) {
1716
log.notice(
1817
'',
19-
columnify(
20-
tarball.files
21-
.map(f => {
22-
const bytes = formatBytes(f.size, false)
23-
return /^node_modules\//.test(f.path) ? null : { path: f.path, size: `${bytes}` }
24-
})
25-
.filter(f => f),
26-
{
27-
include: ['size', 'path'],
28-
showHeaders: false,
29-
}
30-
)
18+
tarball.files.map(f =>
19+
/^node_modules\//.test(f.path) ? null : `${formatBytes(f.size, false)} ${f.path}`
20+
).filter(f => f).join('\n')
3121
)
3222
}
3323
if (tarball.bundled.length) {
34-
log.notice('=== Bundled Dependencies ===')
24+
log.notice('Bundled Dependencies')
3525
tarball.bundled.forEach(name => log.notice('', name))
3626
}
37-
log.notice('=== Tarball Details ===')
38-
log.notice(
39-
'',
40-
columnify(
41-
[
42-
{ name: 'name:', value: tarball.name },
43-
{ name: 'version:', value: tarball.version },
44-
tarball.filename && { name: 'filename:', value: tarball.filename },
45-
{ name: 'package size:', value: formatBytes(tarball.size) },
46-
{ name: 'unpacked size:', value: formatBytes(tarball.unpackedSize) },
47-
{ name: 'shasum:', value: tarball.shasum },
48-
{
49-
name: 'integrity:',
50-
value:
51-
tarball.integrity.toString().slice(0, 20) +
52-
'[...]' +
53-
tarball.integrity.toString().slice(80),
54-
},
55-
tarball.bundled.length && { name: 'bundled deps:', value: tarball.bundled.length },
56-
tarball.bundled.length && {
57-
name: 'bundled files:',
58-
value: tarball.entryCount - tarball.files.length,
59-
},
60-
tarball.bundled.length && { name: 'own files:', value: tarball.files.length },
61-
{ name: 'total files:', value: tarball.entryCount },
62-
].filter(x => x),
63-
{
64-
include: ['name', 'value'],
65-
showHeaders: false,
66-
}
67-
)
68-
)
27+
log.notice('Tarball Details')
28+
log.notice('', `name: ${tarball.name}`)
29+
log.notice('', `version: ${tarball.version}`)
30+
if (tarball.filename) {
31+
log.notice('', `filename: ${tarball.filename}`)
32+
}
33+
log.notice('', `package size: ${formatBytes(tarball.size)}`)
34+
log.notice('', `unpacked size: ${formatBytes(tarball.unpackedSize)}`)
35+
log.notice('', `shasum: ${tarball.shasum}`)
36+
/* eslint-disable-next-line max-len */
37+
log.notice('', `integrity: ${tarball.integrity.toString().slice(0, 20)}[...]${tarball.integrity.toString().slice(80)}`)
38+
if (tarball.bundled.length) {
39+
log.notice('', `bundled deps: ${tarball.bundled.length}`)
40+
log.notice('', `bundled files: ${tarball.entryCount - tarball.files.length}`)
41+
log.notice('', `own files: ${tarball.files.length}`)
42+
}
43+
log.notice('', `total files: ${tarball.entryCount}`)
6944
log.notice('', '')
7045
}
7146

0 commit comments

Comments
 (0)