You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
236 lines
6.1 KiB
236 lines
6.1 KiB
'use strict'; |
|
const path = require('path'); |
|
const buildParserOptions = require('minimist-options'); |
|
const parseArguments = require('yargs-parser'); |
|
const camelCaseKeys = require('camelcase-keys'); |
|
const decamelize = require('decamelize'); |
|
const decamelizeKeys = require('decamelize-keys'); |
|
const trimNewlines = require('trim-newlines'); |
|
const redent = require('redent'); |
|
const readPkgUp = require('read-pkg-up'); |
|
const hardRejection = require('hard-rejection'); |
|
const normalizePackageData = require('normalize-package-data'); |
|
|
|
// Prevent caching of this module so module.parent is always accurate |
|
delete require.cache[__filename]; |
|
const parentDir = path.dirname(module.parent && module.parent.filename ? module.parent.filename : '.'); |
|
|
|
const isFlagMissing = (flagName, definedFlags, receivedFlags, input) => { |
|
const flag = definedFlags[flagName]; |
|
let isFlagRequired = true; |
|
|
|
if (typeof flag.isRequired === 'function') { |
|
isFlagRequired = flag.isRequired(receivedFlags, input); |
|
if (typeof isFlagRequired !== 'boolean') { |
|
throw new TypeError(`Return value for isRequired callback should be of type boolean, but ${typeof isFlagRequired} was returned.`); |
|
} |
|
} |
|
|
|
if (typeof receivedFlags[flagName] === 'undefined') { |
|
return isFlagRequired; |
|
} |
|
|
|
return flag.isMultiple && receivedFlags[flagName].length === 0; |
|
}; |
|
|
|
const getMissingRequiredFlags = (flags, receivedFlags, input) => { |
|
const missingRequiredFlags = []; |
|
if (typeof flags === 'undefined') { |
|
return []; |
|
} |
|
|
|
for (const flagName of Object.keys(flags)) { |
|
if (flags[flagName].isRequired && isFlagMissing(flagName, flags, receivedFlags, input)) { |
|
missingRequiredFlags.push({key: flagName, ...flags[flagName]}); |
|
} |
|
} |
|
|
|
return missingRequiredFlags; |
|
}; |
|
|
|
const reportMissingRequiredFlags = missingRequiredFlags => { |
|
console.error(`Missing required flag${missingRequiredFlags.length > 1 ? 's' : ''}`); |
|
for (const flag of missingRequiredFlags) { |
|
console.error(`\t--${decamelize(flag.key, '-')}${flag.alias ? `, -${flag.alias}` : ''}`); |
|
} |
|
}; |
|
|
|
const validateOptions = ({flags}) => { |
|
const invalidFlags = Object.keys(flags).filter(flagKey => flagKey.includes('-') && flagKey !== '--'); |
|
if (invalidFlags.length > 0) { |
|
throw new Error(`Flag keys may not contain '-': ${invalidFlags.join(', ')}`); |
|
} |
|
}; |
|
|
|
const reportUnknownFlags = unknownFlags => { |
|
console.error([ |
|
`Unknown flag${unknownFlags.length > 1 ? 's' : ''}`, |
|
...unknownFlags |
|
].join('\n')); |
|
}; |
|
|
|
const buildParserFlags = ({flags, booleanDefault}) => { |
|
const parserFlags = {}; |
|
|
|
for (const [flagKey, flagValue] of Object.entries(flags)) { |
|
const flag = {...flagValue}; |
|
|
|
if ( |
|
typeof booleanDefault !== 'undefined' && |
|
flag.type === 'boolean' && |
|
!Object.prototype.hasOwnProperty.call(flag, 'default') |
|
) { |
|
flag.default = flag.isMultiple ? [booleanDefault] : booleanDefault; |
|
} |
|
|
|
if (flag.isMultiple) { |
|
flag.type = flag.type ? `${flag.type}-array` : 'array'; |
|
flag.default = flag.default || []; |
|
delete flag.isMultiple; |
|
} |
|
|
|
parserFlags[flagKey] = flag; |
|
} |
|
|
|
return parserFlags; |
|
}; |
|
|
|
const validateFlags = (flags, options) => { |
|
for (const [flagKey, flagValue] of Object.entries(options.flags)) { |
|
if (flagKey !== '--' && !flagValue.isMultiple && Array.isArray(flags[flagKey])) { |
|
throw new Error(`The flag --${flagKey} can only be set once.`); |
|
} |
|
} |
|
}; |
|
|
|
const meow = (helpText, options) => { |
|
if (typeof helpText !== 'string') { |
|
options = helpText; |
|
helpText = ''; |
|
} |
|
|
|
const foundPkg = readPkgUp.sync({ |
|
cwd: parentDir, |
|
normalize: false |
|
}); |
|
|
|
options = { |
|
pkg: foundPkg ? foundPkg.packageJson : {}, |
|
argv: process.argv.slice(2), |
|
flags: {}, |
|
inferType: false, |
|
input: 'string', |
|
help: helpText, |
|
autoHelp: true, |
|
autoVersion: true, |
|
booleanDefault: false, |
|
hardRejection: true, |
|
allowUnknownFlags: true, |
|
...options |
|
}; |
|
|
|
if (options.hardRejection) { |
|
hardRejection(); |
|
} |
|
|
|
validateOptions(options); |
|
let parserOptions = { |
|
arguments: options.input, |
|
...buildParserFlags(options) |
|
}; |
|
|
|
parserOptions = decamelizeKeys(parserOptions, '-', {exclude: ['stopEarly', '--']}); |
|
|
|
if (options.inferType) { |
|
delete parserOptions.arguments; |
|
} |
|
|
|
parserOptions = buildParserOptions(parserOptions); |
|
|
|
parserOptions.configuration = { |
|
...parserOptions.configuration, |
|
'greedy-arrays': false |
|
}; |
|
|
|
if (parserOptions['--']) { |
|
parserOptions.configuration['populate--'] = true; |
|
} |
|
|
|
if (!options.allowUnknownFlags) { |
|
// Collect unknown options in `argv._` to be checked later. |
|
parserOptions.configuration['unknown-options-as-args'] = true; |
|
} |
|
|
|
const {pkg} = options; |
|
const argv = parseArguments(options.argv, parserOptions); |
|
let help = redent(trimNewlines((options.help || '').replace(/\t+\n*$/, '')), 2); |
|
|
|
normalizePackageData(pkg); |
|
|
|
process.title = pkg.bin ? Object.keys(pkg.bin)[0] : pkg.name; |
|
|
|
let {description} = options; |
|
if (!description && description !== false) { |
|
({description} = pkg); |
|
} |
|
|
|
help = (description ? `\n ${description}\n` : '') + (help ? `\n${help}\n` : '\n'); |
|
|
|
const showHelp = code => { |
|
console.log(help); |
|
process.exit(typeof code === 'number' ? code : 2); |
|
}; |
|
|
|
const showVersion = () => { |
|
console.log(typeof options.version === 'string' ? options.version : pkg.version); |
|
process.exit(0); |
|
}; |
|
|
|
if (argv._.length === 0 && options.argv.length === 1) { |
|
if (argv.version === true && options.autoVersion) { |
|
showVersion(); |
|
} |
|
|
|
if (argv.help === true && options.autoHelp) { |
|
showHelp(0); |
|
} |
|
} |
|
|
|
const input = argv._; |
|
delete argv._; |
|
|
|
if (!options.allowUnknownFlags) { |
|
const unknownFlags = input.filter(item => typeof item === 'string' && item.startsWith('-')); |
|
if (unknownFlags.length > 0) { |
|
reportUnknownFlags(unknownFlags); |
|
process.exit(2); |
|
} |
|
} |
|
|
|
const flags = camelCaseKeys(argv, {exclude: ['--', /^\w$/]}); |
|
const unnormalizedFlags = {...flags}; |
|
|
|
validateFlags(flags, options); |
|
|
|
for (const flagValue of Object.values(options.flags)) { |
|
delete flags[flagValue.alias]; |
|
} |
|
|
|
const missingRequiredFlags = getMissingRequiredFlags(options.flags, flags, input); |
|
if (missingRequiredFlags.length > 0) { |
|
reportMissingRequiredFlags(missingRequiredFlags); |
|
process.exit(2); |
|
} |
|
|
|
return { |
|
input, |
|
flags, |
|
unnormalizedFlags, |
|
pkg, |
|
help, |
|
showHelp, |
|
showVersion |
|
}; |
|
}; |
|
|
|
module.exports = meow;
|
|
|