Skip to content

Commit

Permalink
feat(command): add a command method to convert flags to prompts
Browse files Browse the repository at this point in the history
expose a top level method on command class to convert its flags to an
array of inquirer prompt objects. When you need to delegate to some
other instance of inquirer, this will make it easy to pass off what
would done internally by the the interactive handler.
  • Loading branch information
esatterwhite committed Mar 19, 2021
1 parent eebfd07 commit 5817b34
Show file tree
Hide file tree
Showing 12 changed files with 233 additions and 38 deletions.
19 changes: 19 additions & 0 deletions lib/command/flag-to-prompt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use strict'

const flagType = require('./flag-type')

module.exports = flagToPrompt

function flagToPrompt(name, opt) {
const display = name.replace(':', ' ')
return {
'type': flagType(opt)
, 'name': name
, 'message': display + ': ' + (opt.description || '(no description)')
, 'choices': opt.choices
, 'default': opt.default || null
, 'when': opt.when
, 'validate': opt.validate
, 'filter': opt.filter
}
}
24 changes: 24 additions & 0 deletions lib/command/flag-type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use strict'

module.exports = flagType

function flagType(flag) {
if (Array.isArray(flag.type)) return flagType(unnest(flag))
if (flag.type === Boolean) return 'confirm'
if (flag.type === Number) return 'number'
if (flag.mask) return 'password'
if (flag.choices) {
if (flag.multi) return 'checkbox'
return 'list'
}

return 'input'
}

function unnest(flag) {
const types = flag.type.filter((type) => {
return type !== Array
})

return {type: types[0]}
}
62 changes: 36 additions & 26 deletions lib/command.js → lib/command/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ const ora = require('ora')
const toArray = require('mout/lang/toArray')
const isFunction = require('mout/lang/isFunction')
const hasOwn = require('mout/object/hasOwn')
const Registry = require('./registry')
const conf = require('./conf')
const usage = require('./usage')
const object = require('./lang/object')
const typeOf = require('./usage/type-of')
const exceptions = require('./exceptions')
const toPrompt = require('./flag-to-prompt')
const Registry = require('../registry')
const conf = require('../conf')
const usage = require('../usage')
const object = require('../lang/object')
const typeOf = require('../usage/type-of')
const exceptions = require('../exceptions')
const stop_flags = new Set(['help', 'interactive', 'skip', 'color'])
const ARGV = 'argv'
const on_exp = /^on([A-z])/
Expand Down Expand Up @@ -245,9 +246,7 @@ class Command extends Registry {
**/
get conf() {

if (this._optcache) {
return this._optcache
}
if (this._optcache) return this._optcache

this._optcache = {}
this._required = new Set()
Expand Down Expand Up @@ -350,7 +349,7 @@ class Command extends Registry {
answers[flag] = await ask(flag, cmd, current, this[kPrompt])
continue
}
const arg = toQuestion(flag, cmd, current, answers)
const arg = toQuestion(flag, current, answers)
const res = await this.prompt(arg)
Object.assign(answers, res)
}
Expand Down Expand Up @@ -499,21 +498,44 @@ class Command extends Registry {
return (this.argv.color && chalk[color]) ? chalk[color](text) : text
}

/**
* Registers a new sub command
* @method module:seeli/lib/command#use
* @param {module:seeli/lib/command} command The command to register
**/
use(...args) {
return this.register(...args)
}

/**
* Convert all registered flags to inquierer compatible prompt objects
* @method module:seeli/lib/command#toPrompt
* @returns {Object[]} array of inquirer prompt objects
**/
toPrompt() {
const prompts = []
const flags = this.options.flags
for (const [name, opts] of Object.entries(flags)) {
if (stop_flags.has(name)) continue
if (opts.interactive === false) continue
prompts.push(toPrompt(name, opts))
}
return prompts
}

static run(...args) {
const cmd = new(this)()
return cmd.run(...args)
}

}

module.exports = Command

async function ask(name, cmd, opts, inquirer) {
/* istanbul ignore next */
async function ask(name, opts, inquirer) {
const results = []
const question = toQuestion(name, cmd, opts)
const question = toQuestion(name, opts)
while (true) {
const answer = await inquirer(question)
if (answer[name] === '') break
Expand All @@ -522,27 +544,15 @@ async function ask(name, cmd, opts, inquirer) {
return results
}

function toQuestion(flag, cmd, opts, answers) {
const flag_display = flag.replace(':', ' ')
const arg = {
'type': opts.type === Boolean ? 'confirm' : opts.mask ? 'password' : 'input'
, 'name': flag
, 'message': flag_display + ': ' + (opts.description || '(no description)')
, 'default': opts.default || null
}
function toQuestion(flag, opts, answers) {
const arg = toPrompt(flag, opts)

// TODO(esatterwhite)
// wrap validate to throw returned errors so `ask`
// can return them
arg.when = opts.when ? opts.when.bind(null, answers) : undefined
arg.validate = opts.validate ? opts.validate.bind(null, answers) : undefined
arg.filter = opts.filter ? opts.filter.bind(null, answers) : undefined

if (opts.choices) {
arg.type = opts.multi ? 'checkbox' : 'list'
arg.choices = opts.choices
}

return arg
}

Expand Down
2 changes: 0 additions & 2 deletions lib/conf.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
'use strict'

const os = require('os')
const path = require('path')
const chalk = require('chalk')
const pkgup = require('pkg-up')
const set = require('mout/object/set')
const get = require('mout/object/get')
Expand Down
5 changes: 4 additions & 1 deletion lib/usage/type-of.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ function typeOf(thing) {
} else if (typeof thing === 'number' && isNaN(thing)) {
return 'NaN'
} else if (Array.isArray(thing)) {
return typeOf(thing[0]).toLowerCase() + '*'
const clean = thing.filter((item) => {
return item !== Array
})
return typeOf(clean[0]).toLowerCase() + '*'
} else if (isFunction(thing)) {
return typeOf(thing()).toLowerCase()
} else {
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
],
"eslintConfig": {
"root": true,
"extends": "eslint-config-codedependant",
"extends": "codedependant",
"rules": {
"no-eq-null": 0,
"no-var": 2,
Expand Down Expand Up @@ -115,14 +115,16 @@
"coverage-report": [
"text",
"text-summary",
"json"
"json",
"html"
],
"files": [
"test/**/*.js"
],
"nyc-arg": [
"--exclude=test/",
"--exclude=examples/",
"--exclude=release.config.js",
"--all"
]
}
Expand Down
62 changes: 59 additions & 3 deletions test/command.spec.js → test/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,17 @@ test('command', async (t) => {
const a = new Command({
name: 'a'
, usage: ['get a']
, run: async function() {}
, run: async function() {
return null
}
})

const b = new Command({
name: 'b'
, usage: ['get b']
, run: async function() {}
, run: async function() {
return null
}
})

const CmdA = new Command({
Expand Down Expand Up @@ -168,7 +172,7 @@ test('command', async (t) => {
{
const content = await Help.run()
t.match(content.trim(), /Invalid command:/)
t.match(content, /\$ command\.spec get c/)
t.match(content, /\$ command get c/)
}
})
})
Expand Down Expand Up @@ -386,6 +390,20 @@ test('command', async (t) => {
})
})

t.test('Command static run function', async (t) => {
async function run() {
return 'static call'
}
class StaticCommand extends Command {
constructor(opts = {}) {
super(opts, {run})
}
}

const out = await StaticCommand.run()
t.equal(out, 'static call', 'static run function output')
})

t.test('Subclassing', async (tt) => {
test('should allow for subclassing', async (t) => {
const defaults = {
Expand Down Expand Up @@ -565,6 +583,44 @@ test('command', async (t) => {
})
})

t.test('command#toPrompt', async (t) => {
const cmd = new Command({
flags: {
one: {
type: Boolean
, description: 'boolean flag'
, when: () => {
return true
}
}
, two: {
type: [Number, Array]
, filter: (val) => {
return val
}
}
}
})

const out = cmd.toPrompt()
t.ok(Array.isArray(out), 'output is an array')
t.equal(out.length, 2, 'expected prompt count')

t.match(out, [{
type: 'confirm'
, message: 'one: boolean flag'
, when: Function
, validate: undefined
, filter: undefined
}, {
type: 'number'
, message: /no description/ig
, when: undefined
, validate: undefined
, filter: Function
}])
})

t.test('sub command execution', async (t) => {
const bub = new Command({
name: 'bub'
Expand Down
44 changes: 44 additions & 0 deletions test/flag-to-prompt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use strict'

const {test, threw} = require('tap')
const flagToPrompt = require('../lib/command/flag-to-prompt.js')

test('flagToPrompt', async (t) => {

t.test('no description', async (t) => {
const out = flagToPrompt('test:flag', {
type: Boolean
})

t.match(out, {
name: 'test:flag'
, type: 'confirm'
, message: 'test flag: (no description)'
, when: undefined
, validate: undefined
, filter: undefined
})
})

t.test('description w/ input functions', async (t) => {
const out = flagToPrompt('foobar', {
type: String
, description: 'hello world'
, multi: true
, choices: ['one']
, when: () => {}
, validate: () => {}
, filter: () => {}
})

t.match(out, {
name: 'foobar'
, type: 'checkbox'
, message: 'foobar: hello world'
, choices: ['one']
, when: Function
, validate: Function
, filter: Function
})
})
}).catch(threw)
27 changes: 27 additions & 0 deletions test/flag-type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use strict'

const path = require('path')
const url = require('url')
const {test, threw} = require('tap')
const flagType = require('../lib/command/flag-type')

test('flagType', async (t) => {

const cases = [
[{type: Boolean}, 'confirm', 'Boolean === confirm']
, [{type: Number}, 'number', 'Number === number']
, [{type: String}, 'input', 'String === input']
, [{type: path}, 'input', 'path === input']
, [{type: url}, 'input', 'url === input']
, [{type: [Number, Array]}, 'number', '[Number, Array] === number']
, [{type: String, mask: true}, 'password', 'mask=true === password']
, [{type: String, choices: []}, 'list', 'choices === list']
, [{type: String, choices: [], multi: true}, 'checkbox', 'choices + multi === checkbox']
, [{type: Function}, 'input', 'unexpected type === input']
]

for (const [flag, expected, msg] of cases) {
const actual = flagType(flag)
t.equal(actual, expected, msg)
}
}).catch(threw)
Loading

0 comments on commit 5817b34

Please sign in to comment.