| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175 |
- const { stripVTControlCharacters: strip } = require('node:util')
- const { Minipass } = require('minipass')
- // This module consumes package data in the following format:
- //
- // {
- // name: String,
- // description: String,
- // maintainers: [{ username: String, email: String }],
- // keywords: String | [String],
- // version: String,
- // date: Date // can be null,
- // }
- //
- // The returned stream will format this package data into a byte stream of formatted, displayable output.
- function filter (data, exclude) {
- const words = [data.name]
- .concat(data.maintainers.map(m => m.username))
- .concat(data.keywords || [])
- .map(f => f?.trim?.())
- .filter(Boolean)
- .join(' ')
- .toLowerCase()
- if (exclude.find(pattern => {
- // Treats both /foo and /foo/ as regex searches
- if (pattern.startsWith('/')) {
- if (pattern.endsWith('/')) {
- pattern = pattern.slice(0, -1)
- }
- return words.match(new RegExp(pattern.slice(1)))
- }
- return words.includes(pattern)
- })) {
- return false
- }
- return true
- }
- module.exports = (opts) => {
- return opts.json ? new JSONOutputStream(opts) : new TextOutputStream(opts)
- }
- class JSONOutputStream extends Minipass {
- #didFirst = false
- #exclude
- constructor (opts) {
- super()
- this.#exclude = opts.exclude
- }
- write (obj) {
- if (!filter(obj, this.#exclude)) {
- return
- }
- if (!this.#didFirst) {
- super.write('[\n')
- this.#didFirst = true
- } else {
- super.write('\n,\n')
- }
- return super.write(JSON.stringify(obj))
- }
- end () {
- super.write(this.#didFirst ? ']\n' : '\n[]\n')
- super.end()
- }
- }
- class TextOutputStream extends Minipass {
- #args
- #chalk
- #exclude
- #parseable
- constructor (opts) {
- super()
- // Consider a search for "cowboys" and "boy".
- // If we highlight "boys" first the "cowboys" string will no longer string match because of the ansi highlighting added to "boys".
- // If we highlight "boy" second then the ansi reset at the end will make the highlighting only on "cowboy" with a normal "s".
- // Neither is perfect but at least the first option doesn't do partial highlighting. So, we sort strings smaller to larger
- this.#args = opts.args
- .map(s => s.toLowerCase())
- .filter(Boolean)
- .sort((a, b) => a.length - b.length)
- this.#chalk = opts.npm.chalk
- this.#exclude = opts.exclude
- this.#parseable = opts.parseable
- }
- write (data) {
- if (!filter(data, this.#exclude)) {
- return
- }
- // Normalize
- const pkg = {
- authors: data.maintainers.map((m) => `${strip(m.username)}`).join(' '),
- publisher: strip(data.publisher?.username || ''),
- date: data.date ? data.date.toISOString().slice(0, 10) : 'prehistoric',
- description: strip(data.description ?? ''),
- keywords: [],
- name: strip(data.name),
- version: data.version,
- }
- if (Array.isArray(data.keywords)) {
- pkg.keywords = data.keywords.map(strip)
- } else if (typeof data.keywords === 'string') {
- pkg.keywords = strip(data.keywords.replace(/[,\s]+/, ' ')).split(' ')
- }
- let output
- if (this.#parseable) {
- output = [pkg.name, pkg.description, pkg.author, pkg.date, pkg.version, pkg.keywords]
- .filter(Boolean)
- .map(col => ('' + col).replace(/\t/g, ' ')).join('\t')
- return super.write(output)
- }
- const keywords = pkg.keywords.map(k => {
- if (this.#args.includes(k)) {
- return this.#chalk.cyan(k)
- } else {
- return k
- }
- }).join(' ')
- const description = this.#highlight(pkg.description)
- let name
- if (this.#args.includes(pkg.name)) {
- name = this.#chalk.cyan(pkg.name)
- } else {
- name = this.#highlight(pkg.name)
- name = this.#chalk.blue(name)
- }
- if (description.length) {
- output = `${name}\n${description}\n`
- } else {
- output = `${name}\n`
- }
- if (pkg.publisher) {
- output += `Version ${this.#chalk.blue(pkg.version)} published ${this.#chalk.blue(pkg.date)} by ${this.#chalk.blue(pkg.publisher)}\n`
- } else {
- output += `Version ${this.#chalk.blue(pkg.version)} published ${this.#chalk.blue(pkg.date)} by ${this.#chalk.yellow('???')}\n`
- }
- output += `Maintainers: ${pkg.authors}\n`
- if (keywords) {
- output += `Keywords: ${keywords}\n`
- }
- output += `${this.#chalk.blue(`https://npm.im/${pkg.name}`)}\n`
- return super.write(output)
- }
- #highlight (input) {
- let output = input
- for (const arg of this.#args) {
- let i = output.toLowerCase().indexOf(arg)
- while (i > -1) {
- const highlit = this.#chalk.cyan(output.slice(i, i + arg.length))
- output = [
- output.slice(0, i),
- highlit,
- output.slice(i + arg.length),
- ].join('')
- i = output.toLowerCase().indexOf(arg, i + highlit.length)
- }
- }
- return output
- }
- }
|