help.js 3.6 KB

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