• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1const spawn = require('@npmcli/promise-spawn')
2const path = require('path')
3const openUrl = require('../utils/open-url.js')
4const { glob } = require('glob')
5const localeCompare = require('@isaacs/string-locale-compare')('en')
6const { deref } = require('../utils/cmd-list.js')
7
8const globify = pattern => pattern.split('\\').join('/')
9const BaseCommand = require('../base-command.js')
10
11// Strips out the number from foo.7 or foo.7. or foo.7.tgz
12// We don't currently compress our man pages but if we ever did this would
13// seamlessly continue supporting it
14const manNumberRegex = /\.(\d+)(\.[^/\\]*)?$/
15// hardcoded names for mansections
16// XXX: these are used in the docs workspace and should be exported
17// from npm so section names can changed more easily
18const manSectionNames = {
19  1: 'commands',
20  5: 'configuring-npm',
21  7: 'using-npm',
22}
23
24class Help extends BaseCommand {
25  static description = 'Get help on npm'
26  static name = 'help'
27  static usage = ['<term> [<terms..>]']
28  static params = ['viewer']
29
30  static async completion (opts, npm) {
31    if (opts.conf.argv.remain.length > 2) {
32      return []
33    }
34    const g = path.resolve(npm.npmRoot, 'man/man[0-9]/*.[0-9]')
35    let files = await glob(globify(g))
36    // preserve glob@8 behavior
37    files = files.sort((a, b) => a.localeCompare(b, 'en'))
38
39    return Object.keys(files.reduce(function (acc, file) {
40      file = path.basename(file).replace(/\.[0-9]+$/, '')
41      file = file.replace(/^npm-/, '')
42      acc[file] = true
43      return acc
44    }, { help: true }))
45  }
46
47  async exec (args) {
48    // By default we search all of our man subdirectories, but if the user has
49    // asked for a specific one we limit the search to just there
50    const manSearch = /^\d+$/.test(args[0]) ? `man${args.shift()}` : 'man*'
51
52    if (!args.length) {
53      return this.npm.output(this.npm.usage)
54    }
55
56    // npm help foo bar baz: search topics
57    if (args.length > 1) {
58      return this.helpSearch(args)
59    }
60
61    // `npm help package.json`
62    const arg = (deref(args[0]) || args[0]).replace('.json', '-json')
63
64    // find either section.n or npm-section.n
65    const f = globify(path.resolve(this.npm.npmRoot, `man/${manSearch}/?(npm-)${arg}.[0-9]*`))
66
67    const [man] = await glob(f).then(r => r.sort((a, b) => {
68      // Because the glob is (subtly) different from manNumberRegex,
69      // we can't rely on it passing.
70      const aManNumberMatch = a.match(manNumberRegex)?.[1] || 999
71      const bManNumberMatch = b.match(manNumberRegex)?.[1] || 999
72      if (aManNumberMatch !== bManNumberMatch) {
73        return aManNumberMatch - bManNumberMatch
74      }
75      return localeCompare(a, b)
76    }))
77
78    return man ? this.viewMan(man) : this.helpSearch(args)
79  }
80
81  helpSearch (args) {
82    return this.npm.exec('help-search', args)
83  }
84
85  async viewMan (man) {
86    const viewer = this.npm.config.get('viewer')
87
88    if (viewer === 'browser') {
89      return openUrl(this.npm, this.htmlMan(man), 'help available at the following URL', true)
90    }
91
92    let args = ['man', [man]]
93    if (viewer === 'woman') {
94      args = ['emacsclient', ['-e', `(woman-find-file '${man}')`]]
95    }
96
97    return spawn(...args, { stdio: 'inherit' }).catch(err => {
98      if (err.code) {
99        throw new Error(`help process exited with code: ${err.code}`)
100      } else {
101        throw err
102      }
103    })
104  }
105
106  // Returns the path to the html version of the man page
107  htmlMan (man) {
108    const sect = manSectionNames[man.match(manNumberRegex)[1]]
109    const f = path.basename(man).replace(manNumberRegex, '')
110    return 'file:///' + path.resolve(this.npm.npmRoot, `docs/output/${sect}/${f}.html`)
111  }
112}
113module.exports = Help
114