11cb0ef41Sopenharmony_ciconst { readFile } = require('fs/promises') 21cb0ef41Sopenharmony_ciconst path = require('path') 31cb0ef41Sopenharmony_ciconst { glob } = require('glob') 41cb0ef41Sopenharmony_ciconst BaseCommand = require('../base-command.js') 51cb0ef41Sopenharmony_ci 61cb0ef41Sopenharmony_ciconst globify = pattern => pattern.split('\\').join('/') 71cb0ef41Sopenharmony_ci 81cb0ef41Sopenharmony_ciclass HelpSearch extends BaseCommand { 91cb0ef41Sopenharmony_ci static description = 'Search npm help documentation' 101cb0ef41Sopenharmony_ci static name = 'help-search' 111cb0ef41Sopenharmony_ci static usage = ['<text>'] 121cb0ef41Sopenharmony_ci static params = ['long'] 131cb0ef41Sopenharmony_ci 141cb0ef41Sopenharmony_ci async exec (args) { 151cb0ef41Sopenharmony_ci if (!args.length) { 161cb0ef41Sopenharmony_ci throw this.usageError() 171cb0ef41Sopenharmony_ci } 181cb0ef41Sopenharmony_ci 191cb0ef41Sopenharmony_ci const docPath = path.resolve(this.npm.npmRoot, 'docs/content') 201cb0ef41Sopenharmony_ci let files = await glob(`${globify(docPath)}/*/*.md`) 211cb0ef41Sopenharmony_ci // preserve glob@8 behavior 221cb0ef41Sopenharmony_ci files = files.sort((a, b) => a.localeCompare(b, 'en')) 231cb0ef41Sopenharmony_ci const data = await this.readFiles(files) 241cb0ef41Sopenharmony_ci const results = await this.searchFiles(args, data, files) 251cb0ef41Sopenharmony_ci const formatted = this.formatResults(args, results) 261cb0ef41Sopenharmony_ci if (!formatted.trim()) { 271cb0ef41Sopenharmony_ci this.npm.output(`No matches in help for: ${args.join(' ')}\n`) 281cb0ef41Sopenharmony_ci } else { 291cb0ef41Sopenharmony_ci this.npm.output(formatted) 301cb0ef41Sopenharmony_ci } 311cb0ef41Sopenharmony_ci } 321cb0ef41Sopenharmony_ci 331cb0ef41Sopenharmony_ci async readFiles (files) { 341cb0ef41Sopenharmony_ci const res = {} 351cb0ef41Sopenharmony_ci await Promise.all(files.map(async file => { 361cb0ef41Sopenharmony_ci res[file] = (await readFile(file, 'utf8')) 371cb0ef41Sopenharmony_ci .replace(/^---\n(.*\n)*?---\n/, '').trim() 381cb0ef41Sopenharmony_ci })) 391cb0ef41Sopenharmony_ci return res 401cb0ef41Sopenharmony_ci } 411cb0ef41Sopenharmony_ci 421cb0ef41Sopenharmony_ci async searchFiles (args, data, files) { 431cb0ef41Sopenharmony_ci const results = [] 441cb0ef41Sopenharmony_ci for (const [file, content] of Object.entries(data)) { 451cb0ef41Sopenharmony_ci const lowerCase = content.toLowerCase() 461cb0ef41Sopenharmony_ci // skip if no matches at all 471cb0ef41Sopenharmony_ci if (!args.some(a => lowerCase.includes(a.toLowerCase()))) { 481cb0ef41Sopenharmony_ci continue 491cb0ef41Sopenharmony_ci } 501cb0ef41Sopenharmony_ci 511cb0ef41Sopenharmony_ci const lines = content.split(/\n+/) 521cb0ef41Sopenharmony_ci 531cb0ef41Sopenharmony_ci // if a line has a search term, then skip it and the next line. 541cb0ef41Sopenharmony_ci // if the next line has a search term, then skip all 3 551cb0ef41Sopenharmony_ci // otherwise, set the line to null. then remove the nulls. 561cb0ef41Sopenharmony_ci for (let i = 0; i < lines.length; i++) { 571cb0ef41Sopenharmony_ci const line = lines[i] 581cb0ef41Sopenharmony_ci const nextLine = lines[i + 1] 591cb0ef41Sopenharmony_ci let match = false 601cb0ef41Sopenharmony_ci if (nextLine) { 611cb0ef41Sopenharmony_ci match = args.some(a => 621cb0ef41Sopenharmony_ci nextLine.toLowerCase().includes(a.toLowerCase())) 631cb0ef41Sopenharmony_ci if (match) { 641cb0ef41Sopenharmony_ci // skip over the next line, and the line after it. 651cb0ef41Sopenharmony_ci i += 2 661cb0ef41Sopenharmony_ci continue 671cb0ef41Sopenharmony_ci } 681cb0ef41Sopenharmony_ci } 691cb0ef41Sopenharmony_ci 701cb0ef41Sopenharmony_ci match = args.some(a => line.toLowerCase().includes(a.toLowerCase())) 711cb0ef41Sopenharmony_ci 721cb0ef41Sopenharmony_ci if (match) { 731cb0ef41Sopenharmony_ci // skip over the next line 741cb0ef41Sopenharmony_ci i++ 751cb0ef41Sopenharmony_ci continue 761cb0ef41Sopenharmony_ci } 771cb0ef41Sopenharmony_ci 781cb0ef41Sopenharmony_ci lines[i] = null 791cb0ef41Sopenharmony_ci } 801cb0ef41Sopenharmony_ci 811cb0ef41Sopenharmony_ci // now squish any string of nulls into a single null 821cb0ef41Sopenharmony_ci const pruned = lines.reduce((l, r) => { 831cb0ef41Sopenharmony_ci if (!(r === null && l[l.length - 1] === null)) { 841cb0ef41Sopenharmony_ci l.push(r) 851cb0ef41Sopenharmony_ci } 861cb0ef41Sopenharmony_ci 871cb0ef41Sopenharmony_ci return l 881cb0ef41Sopenharmony_ci }, []) 891cb0ef41Sopenharmony_ci 901cb0ef41Sopenharmony_ci if (pruned[pruned.length - 1] === null) { 911cb0ef41Sopenharmony_ci pruned.pop() 921cb0ef41Sopenharmony_ci } 931cb0ef41Sopenharmony_ci 941cb0ef41Sopenharmony_ci if (pruned[0] === null) { 951cb0ef41Sopenharmony_ci pruned.shift() 961cb0ef41Sopenharmony_ci } 971cb0ef41Sopenharmony_ci 981cb0ef41Sopenharmony_ci // now count how many args were found 991cb0ef41Sopenharmony_ci const found = {} 1001cb0ef41Sopenharmony_ci let totalHits = 0 1011cb0ef41Sopenharmony_ci for (const line of pruned) { 1021cb0ef41Sopenharmony_ci for (const arg of args) { 1031cb0ef41Sopenharmony_ci const hit = (line || '').toLowerCase() 1041cb0ef41Sopenharmony_ci .split(arg.toLowerCase()).length - 1 1051cb0ef41Sopenharmony_ci 1061cb0ef41Sopenharmony_ci if (hit > 0) { 1071cb0ef41Sopenharmony_ci found[arg] = (found[arg] || 0) + hit 1081cb0ef41Sopenharmony_ci totalHits += hit 1091cb0ef41Sopenharmony_ci } 1101cb0ef41Sopenharmony_ci } 1111cb0ef41Sopenharmony_ci } 1121cb0ef41Sopenharmony_ci 1131cb0ef41Sopenharmony_ci const cmd = 'npm help ' + 1141cb0ef41Sopenharmony_ci path.basename(file, '.md').replace(/^npm-/, '') 1151cb0ef41Sopenharmony_ci results.push({ 1161cb0ef41Sopenharmony_ci file, 1171cb0ef41Sopenharmony_ci cmd, 1181cb0ef41Sopenharmony_ci lines: pruned, 1191cb0ef41Sopenharmony_ci found: Object.keys(found), 1201cb0ef41Sopenharmony_ci hits: found, 1211cb0ef41Sopenharmony_ci totalHits, 1221cb0ef41Sopenharmony_ci }) 1231cb0ef41Sopenharmony_ci } 1241cb0ef41Sopenharmony_ci 1251cb0ef41Sopenharmony_ci // sort results by number of results found, then by number of hits 1261cb0ef41Sopenharmony_ci // then by number of matching lines 1271cb0ef41Sopenharmony_ci 1281cb0ef41Sopenharmony_ci // coverage is ignored here because the contents of results are 1291cb0ef41Sopenharmony_ci // nondeterministic due to either glob or readFiles or Object.entries 1301cb0ef41Sopenharmony_ci return results.sort(/* istanbul ignore next */ (a, b) => 1311cb0ef41Sopenharmony_ci a.found.length > b.found.length ? -1 1321cb0ef41Sopenharmony_ci : a.found.length < b.found.length ? 1 1331cb0ef41Sopenharmony_ci : a.totalHits > b.totalHits ? -1 1341cb0ef41Sopenharmony_ci : a.totalHits < b.totalHits ? 1 1351cb0ef41Sopenharmony_ci : a.lines.length > b.lines.length ? -1 1361cb0ef41Sopenharmony_ci : a.lines.length < b.lines.length ? 1 1371cb0ef41Sopenharmony_ci : 0).slice(0, 10) 1381cb0ef41Sopenharmony_ci } 1391cb0ef41Sopenharmony_ci 1401cb0ef41Sopenharmony_ci formatResults (args, results) { 1411cb0ef41Sopenharmony_ci const cols = Math.min(process.stdout.columns || Infinity, 80) + 1 1421cb0ef41Sopenharmony_ci 1431cb0ef41Sopenharmony_ci const output = results.map(res => { 1441cb0ef41Sopenharmony_ci const out = [res.cmd] 1451cb0ef41Sopenharmony_ci const r = Object.keys(res.hits) 1461cb0ef41Sopenharmony_ci .map(k => `${k}:${res.hits[k]}`) 1471cb0ef41Sopenharmony_ci .sort((a, b) => a > b ? 1 : -1) 1481cb0ef41Sopenharmony_ci .join(' ') 1491cb0ef41Sopenharmony_ci 1501cb0ef41Sopenharmony_ci out.push(' '.repeat((Math.max(1, cols - out.join(' ').length - r.length - 1)))) 1511cb0ef41Sopenharmony_ci out.push(r) 1521cb0ef41Sopenharmony_ci 1531cb0ef41Sopenharmony_ci if (!this.npm.config.get('long')) { 1541cb0ef41Sopenharmony_ci return out.join('') 1551cb0ef41Sopenharmony_ci } 1561cb0ef41Sopenharmony_ci 1571cb0ef41Sopenharmony_ci out.unshift('\n\n') 1581cb0ef41Sopenharmony_ci out.push('\n') 1591cb0ef41Sopenharmony_ci out.push('-'.repeat(cols - 1) + '\n') 1601cb0ef41Sopenharmony_ci res.lines.forEach((line, i) => { 1611cb0ef41Sopenharmony_ci if (line === null || i > 3) { 1621cb0ef41Sopenharmony_ci return 1631cb0ef41Sopenharmony_ci } 1641cb0ef41Sopenharmony_ci 1651cb0ef41Sopenharmony_ci const hilitLine = [] 1661cb0ef41Sopenharmony_ci for (const arg of args) { 1671cb0ef41Sopenharmony_ci const finder = line.toLowerCase().split(arg.toLowerCase()) 1681cb0ef41Sopenharmony_ci let p = 0 1691cb0ef41Sopenharmony_ci for (const f of finder) { 1701cb0ef41Sopenharmony_ci hilitLine.push(line.slice(p, p + f.length)) 1711cb0ef41Sopenharmony_ci const word = line.slice(p + f.length, p + f.length + arg.length) 1721cb0ef41Sopenharmony_ci const hilit = this.npm.chalk.bgBlack.red(word) 1731cb0ef41Sopenharmony_ci hilitLine.push(hilit) 1741cb0ef41Sopenharmony_ci p += f.length + arg.length 1751cb0ef41Sopenharmony_ci } 1761cb0ef41Sopenharmony_ci } 1771cb0ef41Sopenharmony_ci out.push(hilitLine.join('') + '\n') 1781cb0ef41Sopenharmony_ci }) 1791cb0ef41Sopenharmony_ci 1801cb0ef41Sopenharmony_ci return out.join('') 1811cb0ef41Sopenharmony_ci }).join('\n') 1821cb0ef41Sopenharmony_ci 1831cb0ef41Sopenharmony_ci const finalOut = results.length && !this.npm.config.get('long') 1841cb0ef41Sopenharmony_ci ? 'Top hits for ' + (args.map(JSON.stringify).join(' ')) + '\n' + 1851cb0ef41Sopenharmony_ci '—'.repeat(cols - 1) + '\n' + 1861cb0ef41Sopenharmony_ci output + '\n' + 1871cb0ef41Sopenharmony_ci '—'.repeat(cols - 1) + '\n' + 1881cb0ef41Sopenharmony_ci '(run with -l or --long to see more context)' 1891cb0ef41Sopenharmony_ci : output 1901cb0ef41Sopenharmony_ci 1911cb0ef41Sopenharmony_ci return finalOut.trim() 1921cb0ef41Sopenharmony_ci } 1931cb0ef41Sopenharmony_ci} 1941cb0ef41Sopenharmony_cimodule.exports = HelpSearch 195