1const { stripVTControlCharacters } = require('node:util') 2const { Minipass } = require('minipass') 3const columnify = require('columnify') 4 5// This module consumes package data in the following format: 6// 7// { 8// name: String, 9// description: String, 10// maintainers: [{ username: String, email: String }], 11// keywords: String | [String], 12// version: String, 13// date: Date // can be null, 14// } 15// 16// The returned stream will format this package data 17// into a byte stream of formatted, displayable output. 18 19module.exports = async (opts) => { 20 return opts.json ? new JSONOutputStream() : new TextOutputStream(opts) 21} 22 23class JSONOutputStream extends Minipass { 24 #didFirst = false 25 26 write (obj) { 27 if (!this.#didFirst) { 28 super.write('[\n') 29 this.#didFirst = true 30 } else { 31 super.write('\n,\n') 32 } 33 34 return super.write(JSON.stringify(obj)) 35 } 36 37 end () { 38 super.write(this.#didFirst ? ']\n' : '\n[]\n') 39 super.end() 40 } 41} 42 43class TextOutputStream extends Minipass { 44 #opts 45 #line = 0 46 47 constructor (opts) { 48 super() 49 this.#opts = opts 50 } 51 52 write (pkg) { 53 return super.write(this.#prettify(pkg)) 54 } 55 56 #prettify (data) { 57 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), 63 version: data.version, 64 } 65 if (Array.isArray(data.keywords)) { 66 pkg.keywords = data.keywords.map((k) => stripVTControlCharacters(k)).join(' ') 67 } 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 72 } 73 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') 77 } 78 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 }, 97 } 98 ).split('\n').map(line => line.slice(0, maxWidth)).join('\n') 99 100 if (!this.#opts.color) { 101 return output 102 } 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 117 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('') 126 } 127 }) 128 129 for (let i = 1; i <= colors.length; i++) { 130 output = output.split(String.fromCharCode(i)).join(`\u001B[${colors[i - 1]}`) 131 } 132 return output.split('\u0000').join('\u001B[0m').trim() 133 } 134} 135