diff --git a/lib/help-search.js b/lib/help-search.js index ae4302e8cc2f4..c1814b4e53fc3 100644 --- a/lib/help-search.js +++ b/lib/help-search.js @@ -1,11 +1,11 @@ const fs = require('fs') const path = require('path') const npm = require('./npm.js') -const glob = require('glob') 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') @@ -23,12 +23,17 @@ const helpSearch = async args => { const docPath = path.resolve(__dirname, '..', 'docs/content') - // XXX: make glob return a promise and remove this wrapping - const files = await new Promise((res, rej) => - glob(`${docPath}/*/*.md`, (er, files) => er ? rej(er) : res(files))) - + const files = await glob(`${docPath}/*/*.md`) const data = await readFiles(files) const results = await searchFiles(args, data, files) + // 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) @@ -125,15 +130,6 @@ const searchFiles = async (args, data, files) => { }) } - // if only one result, then just show that help section. - if (results.length === 1) { - npm.commands.help([results[0].file.replace(/\.md$/, '')], er => { - if (er) - throw er - }) - return [] - } - // sort results by number of results found, then by number of hits // then by number of matching lines return results.sort((a, b) => @@ -147,9 +143,6 @@ const searchFiles = async (args, data, files) => { } const formatResults = (args, results) => { - if (!results) - return 'No results for ' + args.map(JSON.stringify).join(' ') - const cols = Math.min(process.stdout.columns || Infinity, 80) + 1 const out = results.map(res => { diff --git a/test/lib/help-search.js b/test/lib/help-search.js new file mode 100644 index 0000000000000..5ecf5db0614ac --- /dev/null +++ b/test/lib/help-search.js @@ -0,0 +1,181 @@ +const { test } = require('tap') +const { join } = require('path') +const requireInject = require('require-inject') +const ansicolors = require('ansicolors') + +const OUTPUT = [] +const output = (msg) => { + OUTPUT.push(msg) +} + +let npmHelpArgs = null +let npmHelpErr = null +const npm = { + color: false, + flatOptions: { + long: false, + }, + commands: { + help: (args, cb) => { + npmHelpArgs = args + return cb(npmHelpErr) + }, + }, +} + +let npmUsageArg = null +const npmUsage = (arg) => { + npmUsageArg = arg +} + +let globRoot = null +const globDir = { + 'npm-exec.md': 'the exec command\nhelp has multiple lines of exec help\none of them references exec', + 'npm-something.md': 'another\ncommand you run\nthat\nreferences exec\nand has multiple lines\nwith no matches\nthat will be ignored\nand another line\nthat does have exec as well', + 'npm-run-script.md': 'the scripted run-script command runs scripts\nand has lines\nsome of which dont match the string run\nor script\nscript', + 'npm-install.md': 'does a thing in a script\nif a thing does not exist in a thing you run\nto install it and run it maybe in a script', + 'npm-help.md': 'will run the `help-search` command if you need to run it to help you search', + 'npm-help-search.md': 'is the help search command\nthat you get if you run help-search', + 'npm-useless.md': 'exec\nexec', + 'npm-more-useless.md': 'exec exec', + 'npm-extra-useless.md': 'exec\nexec\nexec', +} +const glob = (p, cb) => cb(null, Object.keys(globDir).map((file) => join(globRoot, file))) + +const helpSearch = requireInject('../../lib/help-search.js', { + '../../lib/npm.js': npm, + '../../lib/utils/npm-usage.js': npmUsage, + '../../lib/utils/output.js': output, + glob, +}) + +test('npm help-search', t => { + globRoot = t.testdir(globDir) + t.teardown(() => { + OUTPUT.length = 0 + globRoot = null + }) + + return helpSearch(['exec'], (err) => { + if (err) + throw err + + t.match(OUTPUT, /Top hits for/, 'outputs results') + t.match(OUTPUT, /Did you mean this\?\n\s+exec/, 'matched command, so suggest it') + t.end() + }) +}) + +test('npm help-search multiple terms', t => { + globRoot = t.testdir(globDir) + t.teardown(() => { + OUTPUT.length = 0 + globRoot = null + }) + + return helpSearch(['run', 'script'], (err) => { + if (err) + throw err + + t.match(OUTPUT, /Top hits for/, 'outputs results') + t.match(OUTPUT, /run:\d+ script:\d+/, 'shows hit counts for both terms') + t.end() + }) +}) + +test('npm help-search single result prints full section', t => { + globRoot = t.testdir(globDir) + t.teardown(() => { + OUTPUT.length = 0 + npmHelpArgs = null + globRoot = null + }) + + return helpSearch(['does not exist in'], (err) => { + if (err) + throw err + + t.strictSame(npmHelpArgs, ['npm-install'], 'identified the correct man page and called help with it') + t.end() + }) +}) + +test('npm help-search single result propagates error', t => { + globRoot = t.testdir(globDir) + npmHelpErr = new Error('help broke') + t.teardown(() => { + OUTPUT.length = 0 + npmHelpArgs = null + npmHelpErr = null + globRoot = null + }) + + return helpSearch(['does not exist in'], (err) => { + t.strictSame(npmHelpArgs, ['npm-install'], 'identified the correct man page and called help with it') + t.match(err, /help broke/, 'propagated the error from help') + t.end() + }) +}) + +test('npm help-search long output', t => { + globRoot = t.testdir(globDir) + npm.flatOptions.long = true + t.teardown(() => { + OUTPUT.length = 0 + npm.flatOptions.long = false + globRoot = null + }) + + return helpSearch(['exec'], (err) => { + if (err) + throw err + + t.match(OUTPUT, /has multiple lines of exec help/, 'outputs detailed results') + t.end() + }) +}) + +test('npm help-search long output with color', t => { + globRoot = t.testdir(globDir) + npm.flatOptions.long = true + npm.color = true + t.teardown(() => { + OUTPUT.length = 0 + npm.flatOptions.long = false + npm.color = false + globRoot = null + }) + + return helpSearch(['help-search'], (err) => { + if (err) + throw err + + const highlightedText = ansicolors.bgBlack(ansicolors.red('help-search')) + t.equal(OUTPUT.some((line) => line.includes(highlightedText)), true, 'returned highlighted search terms') + t.end() + }) +}) + +test('npm help-search no args', t => { + return helpSearch([], (err) => { + t.match(err, /npm help-search/, 'throws usage') + t.end() + }) +}) + +test('npm help-search no matches', t => { + globRoot = t.testdir(globDir) + t.teardown(() => { + OUTPUT.length = 0 + npmUsageArg = null + globRoot = null + }) + + return helpSearch(['asdfasdf'], (err) => { + if (err) + throw err + + t.equal(npmUsageArg, false, 'called npmUsage for no matches') + t.end() + }) +})