Press n or j to go to the next uncovered block, b, p or k for the previous block.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 8x 1x 8x 1x 7x 7x 7x 7x 7x 7x 2x 2x 1x 5x 5x 1x 4x 4x 1x 7x 7x 63x 7x 1x 7x 7x 63x 67x 44x 19x 19x 36x 36x 36x 36x 38x 31x 17x 17x 25x 19x 5x 5x 14x 19x 65x 56x 65x 19x 1x 19x 2x 19x 19x 19x 53x 67x 67x 38x 38x 19x 19x 7x 24x 1x 5x 5x 17x 17x 19x 2x 17x 17x 17x 10x 7x 7x 7x 7x 18x 3x 15x 12x 12x 3x 3x 3x 3x 3x 5x 5x 5x 5x 5x 3x 7x 5x 5x 1x | const fs = require('fs') const path = require('path') const npm = require('./npm.js') const color = require('ansicolors') const output = require('./utils/output.js') const usageUtil = require('./utils/usage.js') const { promisify } = require('util') const glob = promisify(require('glob')) const readFile = promisify(fs.readFile) const didYouMean = require('./utils/did-you-mean.js') const { cmdList } = require('./utils/cmd-list.js') const usage = usageUtil('help-search', 'npm help-search <text>') const completion = require('./utils/completion/none.js') const npmUsage = require('./utils/npm-usage.js') const cmd = (args, cb) => helpSearch(args).then(() => cb()).catch(cb) const helpSearch = async args => { if (!args.length) throw usage const docPath = path.resolve(__dirname, '..', 'docs/content') const files = await glob(`${docPath}/*/*.md`) const data = await readFiles(files) const results = await searchFiles(args, data, files) console.error(args, results) // if only one result, then just show that help section. if (results.length === 1) { return npm.commands.help([path.basename(results[0].file, '.md')], er => { if (er) throw er }) } const formatted = formatResults(args, results) if (!formatted.trim()) npmUsage(false) else { output(formatted) output(didYouMean(args[0], cmdList)) } } const readFiles = async files => { const res = {} await Promise.all(files.map(async file => { res[file] = (await readFile(file, 'utf8')) .replace(/^---\n(.*\n)*?---\n/, '').trim() })) return res } const searchFiles = async (args, data, files) => { const results = [] for (const [file, content] of Object.entries(data)) { const lowerCase = content.toLowerCase() // skip if no matches at all if (!args.some(a => lowerCase.includes(a.toLowerCase()))) continue const lines = content.split(/\n+/) // if a line has a search term, then skip it and the next line. // if the next line has a search term, then skip all 3 // otherwise, set the line to null. then remove the nulls. for (let i = 0; i < lines.length; i++) { const line = lines[i] const nextLine = lines[i + 1] let match = false if (nextLine) { match = args.some(a => nextLine.toLowerCase().includes(a.toLowerCase())) if (match) { // skip over the next line, and the line after it. i += 2 continue } } match = args.some(a => line.toLowerCase().includes(a.toLowerCase())) if (match) { // skip over the next line i++ continue } lines[i] = null } // now squish any string of nulls into a single null const pruned = lines.reduce((l, r) => { if (!(r === null && l[l.length - 1] === null)) l.push(r) return l }, []) if (pruned[pruned.length - 1] === null) pruned.pop() if (pruned[0] === null) pruned.shift() // now count how many args were found const found = {} let totalHits = 0 for (const line of pruned) { for (const arg of args) { const hit = (line || '').toLowerCase() .split(arg.toLowerCase()).length - 1 if (hit > 0) { found[arg] = (found[arg] || 0) + hit totalHits += hit } } } const cmd = 'npm help ' + path.basename(file, '.md').replace(/^npm-/, '') results.push({ file, cmd, lines: pruned, found: Object.keys(found), hits: found, totalHits, }) } // sort results by number of results found, then by number of hits // then by number of matching lines return results.sort((a, b) => a.found.length > b.found.length ? -1 : a.found.length < b.found.length ? 1 : a.totalHits > b.totalHits ? -1 : a.totalHits < b.totalHits ? 1 : a.lines.length > b.lines.length ? -1 : a.lines.length < b.lines.length ? 1 : 0).slice(0, 10) } const formatResults = (args, results) => { const cols = Math.min(process.stdout.columns || Infinity, 80) + 1 const out = results.map(res => { const out = [res.cmd] const r = Object.keys(res.hits) .map(k => `${k}:${res.hits[k]}`) .sort((a, b) => a > b ? 1 : -1) .join(' ') out.push(' '.repeat((Math.max(1, cols - out.join(' ').length - r.length - 1)))) out.push(r) if (!npm.flatOptions.long) return out.join('') out.unshift('\n\n') out.push('\n') out.push('-'.repeat(cols - 1) + '\n') res.lines.forEach((line, i) => { if (line === null || i > 3) return if (!npm.color) { out.push(line + '\n') return } const hilitLine = [] for (const arg of args) { const finder = line.toLowerCase().split(arg.toLowerCase()) let p = 0 for (const f of finder) { hilitLine.push(line.substr(p, f.length)) const word = line.substr(p + f.length, arg.length) const hilit = color.bgBlack(color.red(word)) hilitLine.push(hilit) p += f.length + arg.length } } out.push(hilitLine.join('') + '\n') }) return out.join('') }).join('\n') const finalOut = results.length && !npm.flatOptions.long ? 'Top hits for ' + (args.map(JSON.stringify).join(' ')) + '\n' + '—'.repeat(cols - 1) + '\n' + out + '\n' + '—'.repeat(cols - 1) + '\n' + '(run with -l or --long to see more context)' : out return finalOut.trim() } module.exports = Object.assign(cmd, { usage, completion }) |