Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/npm/cli.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGar <gar+gh@danger.computer>2021-10-08 04:40:03 +0300
committerGar <gar+gh@danger.computer>2021-11-04 00:04:22 +0300
commit8ffeb71dfb248b4a76744bd06cd4d6100f17c8ae (patch)
treeb44d12a79dff3afe0c92df6f6c0219f6d91ad471 /lib/commands/completion.js
parent85d59191cf681eabd8827ca58f925c1063776f61 (diff)
chore: refactor commands
This is the first phase of refactoring the internal structure of the npm commands to set us up for future changes. This iteration changes the function signature of `exec` for all the commands to be a async (no more callbacks), and also groups all the commands into their own subdirectory. It also removes the Proxy `npm.commands` object, in favor of an `npm.cmd` and `npm.exec` function that breaks up the two things that proxy was doing. Namely, getting to the attributes of a given command (`npm.cmd` now does this), and actually running the command `npm.exec` does this. PR-URL: https://github.com/npm/cli/pull/3959 Credit: @wraithgar Close: #3959 Reviewed-by: @lukekarrys
Diffstat (limited to 'lib/commands/completion.js')
-rw-r--r--lib/commands/completion.js282
1 files changed, 282 insertions, 0 deletions
diff --git a/lib/commands/completion.js b/lib/commands/completion.js
new file mode 100644
index 000000000..fbbde0df7
--- /dev/null
+++ b/lib/commands/completion.js
@@ -0,0 +1,282 @@
+// Each command has a completion function that takes an options object and a cb
+// The callback gets called with an error and an array of possible completions.
+// The options object is built up based on the environment variables set by
+// zsh or bash when calling a function for completion, based on the cursor
+// position and the command line thus far. These are:
+// COMP_CWORD: the index of the "word" in the command line being completed
+// COMP_LINE: the full command line thusfar as a string
+// COMP_POINT: the cursor index at the point of triggering completion
+//
+// We parse the command line with nopt, like npm does, and then create an
+// options object containing:
+// words: array of words in the command line
+// w: the index of the word being completed (ie, COMP_CWORD)
+// word: the word being completed
+// line: the COMP_LINE
+// lineLength
+// point: the COMP_POINT, usually equal to line length, but not always, eg if
+// the user has pressed the left-arrow to complete an earlier word
+// partialLine: the line up to the point
+// partialWord: the word being completed (which might be ''), up to the point
+// conf: a nopt parse of the command line
+//
+// When the implementation completion method returns its list of strings,
+// and arrays of strings, we filter that by any that start with the
+// partialWord, since only those can possibly be valid matches.
+//
+// Matches are wrapped with ' to escape them, if necessary, and then printed
+// one per line for the shell completion method to consume in IFS=$'\n' mode
+// as an array.
+//
+
+const { definitions, shorthands } = require('../utils/config/index.js')
+const deref = require('../utils/deref-command.js')
+const { aliases, cmdList, plumbing } = require('../utils/cmd-list.js')
+const aliasNames = Object.keys(aliases)
+const fullList = cmdList.concat(aliasNames).filter(c => !plumbing.includes(c))
+const nopt = require('nopt')
+const configNames = Object.keys(definitions)
+const shorthandNames = Object.keys(shorthands)
+const allConfs = configNames.concat(shorthandNames)
+const isWindowsShell = require('../utils/is-windows-shell.js')
+const fileExists = require('../utils/file-exists.js')
+
+const { promisify } = require('util')
+const BaseCommand = require('../base-command.js')
+
+class Completion extends BaseCommand {
+ /* istanbul ignore next - see test/lib/load-all-commands.js */
+ static get description () {
+ return 'Tab Completion for npm'
+ }
+
+ /* istanbul ignore next - see test/lib/load-all-commands.js */
+ static get name () {
+ return 'completion'
+ }
+
+ // completion for the completion command
+ async completion (opts) {
+ if (opts.w > 2)
+ return
+
+ const { resolve } = require('path')
+ const [bashExists, zshExists] = await Promise.all([
+ fileExists(resolve(process.env.HOME, '.bashrc')),
+ fileExists(resolve(process.env.HOME, '.zshrc')),
+ ])
+ const out = []
+ if (zshExists)
+ out.push(['>>', '~/.zshrc'])
+
+ if (bashExists)
+ out.push(['>>', '~/.bashrc'])
+
+ return out
+ }
+
+ async exec (args) {
+ if (isWindowsShell) {
+ const msg = 'npm completion supported only in MINGW / Git bash on Windows'
+ throw Object.assign(new Error(msg), {
+ code: 'ENOTSUP',
+ })
+ }
+
+ const { COMP_CWORD, COMP_LINE, COMP_POINT } = process.env
+
+ // if the COMP_* isn't in the env, then just dump the script.
+ if (COMP_CWORD === undefined ||
+ COMP_LINE === undefined ||
+ COMP_POINT === undefined)
+ return dumpScript()
+
+ // ok we're actually looking at the envs and outputting the suggestions
+ // get the partial line and partial word,
+ // if the point isn't at the end.
+ // ie, tabbing at: npm foo b|ar
+ const w = +COMP_CWORD
+ const words = args.map(unescape)
+ const word = words[w]
+ const line = COMP_LINE
+ const point = +COMP_POINT
+ const partialLine = line.substr(0, point)
+ const partialWords = words.slice(0, w)
+
+ // figure out where in that last word the point is.
+ const partialWordRaw = args[w]
+ let i = partialWordRaw.length
+ while (partialWordRaw.substr(0, i) !== partialLine.substr(-1 * i) && i > 0)
+ i--
+
+ const partialWord = unescape(partialWordRaw.substr(0, i))
+ partialWords.push(partialWord)
+
+ const opts = {
+ words,
+ w,
+ word,
+ line,
+ lineLength: line.length,
+ point,
+ partialLine,
+ partialWords,
+ partialWord,
+ raw: args,
+ }
+
+ if (partialWords.slice(0, -1).indexOf('--') === -1) {
+ if (word.charAt(0) === '-')
+ return this.wrap(opts, configCompl(opts))
+
+ if (words[w - 1] &&
+ words[w - 1].charAt(0) === '-' &&
+ !isFlag(words[w - 1])) {
+ // awaiting a value for a non-bool config.
+ // don't even try to do this for now
+ return this.wrap(opts, configValueCompl(opts))
+ }
+ }
+
+ // try to find the npm command.
+ // it's the first thing after all the configs.
+ // take a little shortcut and use npm's arg parsing logic.
+ // don't have to worry about the last arg being implicitly
+ // boolean'ed, since the last block will catch that.
+ const types = Object.entries(definitions).reduce((types, [key, def]) => {
+ types[key] = def.type
+ return types
+ }, {})
+ const parsed = opts.conf =
+ nopt(types, shorthands, partialWords.slice(0, -1), 0)
+ // check if there's a command already.
+ const cmd = parsed.argv.remain[1]
+ if (!cmd)
+ return this.wrap(opts, cmdCompl(opts))
+
+ Object.keys(parsed).forEach(k => this.npm.config.set(k, parsed[k]))
+
+ // at this point, if words[1] is some kind of npm command,
+ // then complete on it.
+ // otherwise, do nothing
+ const impl = this.npm.cmd(cmd)
+ if (impl.completion) {
+ const comps = await impl.completion(opts)
+ return this.wrap(opts, comps)
+ }
+ }
+
+ // The command should respond with an array. Loop over that,
+ // wrapping quotes around any that have spaces, and writing
+ // them to stdout.
+ // If any of the items are arrays, then join them with a space.
+ // Ie, returning ['a', 'b c', ['d', 'e']] would allow it to expand
+ // to: 'a', 'b c', or 'd' 'e'
+ wrap (opts, compls) {
+ if (!Array.isArray(compls))
+ compls = compls ? [compls] : []
+
+ compls = compls.map(c =>
+ Array.isArray(c) ? c.map(escape).join(' ') : escape(c))
+
+ if (opts.partialWord)
+ compls = compls.filter(c => c.startsWith(opts.partialWord))
+
+ if (compls.length > 0)
+ this.npm.output(compls.join('\n'))
+ }
+}
+
+const dumpScript = async () => {
+ const fs = require('fs')
+ const readFile = promisify(fs.readFile)
+ const { resolve } = require('path')
+ const p = resolve(__dirname, '..', 'utils', 'completion.sh')
+
+ const d = (await readFile(p, 'utf8')).replace(/^#!.*?\n/, '')
+ await new Promise((res, rej) => {
+ let done = false
+ process.stdout.on('error', er => {
+ if (done)
+ return
+
+ done = true
+
+ // Darwin is a pain sometimes.
+ //
+ // This is necessary because the "source" or "." program in
+ // bash on OS X closes its file argument before reading
+ // from it, meaning that you get exactly 1 write, which will
+ // work most of the time, and will always raise an EPIPE.
+ //
+ // Really, one should not be tossing away EPIPE errors, or any
+ // errors, so casually. But, without this, `. <(npm completion)`
+ // can never ever work on OS X.
+ // TODO Ignoring coverage, see 'non EPIPE errors cause failures' test.
+ /* istanbul ignore next */
+ if (er.errno === 'EPIPE')
+ res()
+ else
+ rej(er)
+ })
+
+ process.stdout.write(d, () => {
+ if (done)
+ return
+
+ done = true
+ res()
+ })
+ })
+}
+
+const unescape = w => w.charAt(0) === '\'' ? w.replace(/^'|'$/g, '')
+ : w.replace(/\\ /g, ' ')
+
+const escape = w => !/\s+/.test(w) ? w
+ : '\'' + w + '\''
+
+// the current word has a dash. Return the config names,
+// with the same number of dashes as the current word has.
+const configCompl = opts => {
+ const word = opts.word
+ const split = word.match(/^(-+)((?:no-)*)(.*)$/)
+ const dashes = split[1]
+ const no = split[2]
+ const flags = configNames.filter(isFlag)
+ return allConfs.map(c => dashes + c)
+ .concat(flags.map(f => dashes + (no || 'no-') + f))
+}
+
+// expand with the valid values of various config values.
+// not yet implemented.
+const configValueCompl = opts => []
+
+// check if the thing is a flag or not.
+const isFlag = word => {
+ // shorthands never take args.
+ const split = word.match(/^(-*)((?:no-)+)?(.*)$/)
+ const no = split[2]
+ const conf = split[3]
+ const {type} = definitions[conf]
+ return no ||
+ type === Boolean ||
+ (Array.isArray(type) && type.includes(Boolean)) ||
+ shorthands[conf]
+}
+
+// complete against the npm commands
+// if they all resolve to the same thing, just return the thing it already is
+const cmdCompl = opts => {
+ const matches = fullList.filter(c => c.startsWith(opts.partialWord))
+ if (!matches.length)
+ return matches
+
+ const derefs = new Set([...matches.map(c => deref(c))])
+ if (derefs.size === 1)
+ return [...derefs]
+
+ return fullList
+}
+
+module.exports = Completion