From a35417b70522bd9b862e2186afd0547ff57a171b Mon Sep 17 00:00:00 2001 From: George Karagkiaouris <gkaragkiaouris2@gmail.com> Date: Sun, 9 Aug 2020 14:31:24 -0400 Subject: [PATCH] TypeScript support, allow missing index routes, and more (#31) --- boot.js | 132 ++++++++++++------ index.js | 22 ++- lib/get-entrypoint-paths.js | 2 +- lib/parse-command.js | 36 ++++- lib/read-commands.js | 25 ++-- readme.md | 45 ++++++ test/commands.js | 34 ++++- .../commands/aliases/commands/index.js | 11 +- .../multi-command/commands/with-index/c.js | 6 + .../commands/with-index/index.js | 6 + .../multi-command/commands/without-index/d.js | 6 + .../positional-args/commands/index.js | 9 +- .../fixtures/commands/typescript/package.json | 2 +- .../parse-command/class-positional-args.js | 3 +- .../parse-command/function-positional-args.js | 5 +- .../wrong-order-positional-args.js | 13 ++ .../parse-command/wrong-variadic-arg.js | 13 ++ test/parse-command.js | 43 +++++- 18 files changed, 336 insertions(+), 77 deletions(-) create mode 100644 test/fixtures/commands/multi-command/commands/with-index/c.js create mode 100644 test/fixtures/commands/multi-command/commands/with-index/index.js create mode 100644 test/fixtures/commands/multi-command/commands/without-index/d.js create mode 100644 test/fixtures/parse-command/wrong-order-positional-args.js create mode 100644 test/fixtures/parse-command/wrong-variadic-arg.js diff --git a/boot.js b/boot.js index 68203e1..e358c8f 100644 --- a/boot.js +++ b/boot.js @@ -4,50 +4,16 @@ const camelcaseKeys = require('camelcase-keys'); const decamelize = require('decamelize'); const yargs = require('yargs'); +const getCommandsMessage = command => { + const usageString = [...command.usage, command.name].join(' '); + return `Commands:\n${command.subCommands.map(subCommand => ` ${usageString} ${subCommand.name}`).join('\n')}\n`; +}; + module.exports = (dirname, React, Ink, commands) => { const addCommand = (command, yargs) => { - // Yargs expects a special format for positional args when defining a command - // in order to correctly parse them, e.g. "<first-arg> <second-arg>" - const positionalArgs = command.args - .filter(arg => arg.positional) - .map(arg => `<${decamelize(arg.key, '-')}>`) - .join(' '); - - const name = command.name === 'index' ? '*' : command.name; - const yargsName = positionalArgs.length > 0 ? `${name} ${positionalArgs}` : name; - const description = command.description || `${name} command`; - + // Don't need to add a description as it'll be handled by the * selector in the builder // eslint-disable-next-line no-use-before-define - yargs.command(yargsName, description, builder.bind(null, command), handler.bind(null, command)); - }; - - const builder = (command, yargs) => { - for (const arg of command.args) { - // `inputArgs` is a reserved prop by Pastel and doesn't require a definition in yargs - if (arg.key === 'inputArgs') { - continue; - } - - if (arg.positional) { - yargs.positional(decamelize(arg.key, '-'), { - type: arg.type, - description: arg.description, - default: arg.defaultValue - }); - } else { - yargs.option(decamelize(arg.key, '-'), { - type: arg.type, - description: arg.description, - default: arg.defaultValue, - demandOption: arg.isRequired, - alias: arg.aliases.map(alias => decamelize(alias, '-')) - }); - } - } - - for (const subCommand of command.subCommands) { - addCommand(subCommand, yargs); - } + yargs.command(command.name, '', builder.bind(null, command), () => yargs.showHelp()); }; const handler = (command, argv) => { @@ -64,9 +30,87 @@ module.exports = (dirname, React, Ink, commands) => { }); }; - for (const command of commands) { - addCommand(command, yargs); - } + const builder = (command, yargs) => { + const { + positionalArgs = [], + args, + description, + name, + subCommands, + isDefaultIndex, + usage + } = command; + + for (const subCommand of subCommands) { + addCommand({ + ...subCommand, + usage: [...usage, name] + }, yargs); + } + + // If there is no index defined, yargs will just list the sub-commands + if (isDefaultIndex) { + return; + } + + const hasPositionalArgs = positionalArgs.length > 0; + const positionalArgsString = positionalArgs.map(key => { + const {isRequired, type, aliases} = args.find(arg => arg.key === key); + const [startTag, endTag] = isRequired ? ['<', '>'] : ['[', type === 'array' ? '..]' : ']']; + const argsNames = [key, ...aliases.slice(0, 1)].map(name => decamelize(name, '-')).join('|'); + + return `${startTag}${argsNames}${endTag}`; + }).join(' '); + + const yargsName = hasPositionalArgs ? `* ${positionalArgsString}` : '*'; + const commandDescription = description || `${name} command`; + + yargs.command(yargsName, commandDescription, scopedYargs => { + for (const arg of command.args) { + // `inputArgs` is a reserved prop by Pastel and doesn't require a definition in yargs + if (arg.key === 'inputArgs') { + continue; + } + + if (arg.positional) { + scopedYargs.positional(decamelize(arg.key, '-'), { + type: arg.type, + description: arg.description, + default: arg.defaultValue, + // This only keeps one for some reason for positional arguments + // The slice ensures we keep the same as the one we add in the command + alias: arg.aliases.slice(0, 1).map(alias => decamelize(alias, '-')) + }); + } else { + scopedYargs.option(decamelize(arg.key, '-'), { + type: arg.type, + description: arg.description, + default: arg.defaultValue, + demandOption: arg.isRequired, + alias: arg.aliases.map(alias => decamelize(alias, '-')) + }); + } + } + + // If the index command takes no arguments and there are available sub-commands, + // list the sub-commands in the help message. + if (!hasPositionalArgs && subCommands.length > 0) { + const usageMessage = getCommandsMessage(command); + scopedYargs.demandCommand(0, 0, usageMessage, usageMessage); + } + }, handler.bind(null, command)); + }; + + yargs.command( + '*', '', + yargs => { + const usage = [path.basename(yargs.$0)]; + for (const command of commands) { + addCommand({...command, usage}, yargs); + } + }, + () => yargs.showHelp() + ); yargs.parse(); }; diff --git a/index.js b/index.js index 249dd6e..f6c94a9 100644 --- a/index.js +++ b/index.js @@ -22,10 +22,28 @@ class Pastel extends EventEmitter { this.commandsPath = path.join(appPath, 'commands'); this.buildPath = path.join(appPath, 'build'); this.cachePath = path.join(appPath, 'node_modules', '.cache', 'parcel'); + this.tsConfigPath = path.join(appPath, 'tsconfig.json'); this.testMode = testMode; } + checkOrCreateTsConfig() { + if (!fs.existsSync(this.tsConfigPath)) { + fs.writeFileSync(this.tsConfigPath, JSON.stringify({compilerOptions: {jsx: 'react'}}, null, 2)); + console.log('We detected TypeScript in your project and created a `tsconfig.json` file for you.'); + } + } + + getEntryPointPaths(commands) { + const paths = getEntrypointPaths(this.commandsPath, commands); + + if (paths.some(filePath => path.extname(filePath) === '.tsx')) { + this.checkOrCreateTsConfig(); + } + + return paths; + } + async createBuildDir() { // `del` refuses to delete "build" directory when running Pastel's tests, // so we use `force` option to override this behavior in test mode @@ -65,7 +83,7 @@ class Pastel extends EventEmitter { await this.createBuildDir(); await this.saveEntrypoint(); const commands = await this.scanCommands(); - const bundler = this.createBundler(getEntrypointPaths(this.commandsPath, commands), {watch: false}); + const bundler = this.createBundler(this.getEntryPointPaths(commands), {watch: false}); return bundler.bundle(); } @@ -107,7 +125,7 @@ class Pastel extends EventEmitter { const commands = await this.scanCommands(); - bundler = this.createBundler(getEntrypointPaths(this.commandsPath, commands), {watch: true}); + bundler = this.createBundler(this.getEntryPointPaths(commands), {watch: true}); bundler.on('buildStart', onStart); bundler.on('bundled', handleAsyncErrors(async () => { diff --git a/lib/get-entrypoint-paths.js b/lib/get-entrypoint-paths.js index feb70a0..bbf12e2 100644 --- a/lib/get-entrypoint-paths.js +++ b/lib/get-entrypoint-paths.js @@ -6,7 +6,7 @@ const getEntrypointPaths = (commandsPath, commands) => { const entrypointPaths = []; for (const command of commands) { - entrypointPaths.push(path.join(commandsPath, command.path)); + entrypointPaths.push(path.join(commandsPath, command.path || '')); entrypointPaths.push(...getEntrypointPaths(commandsPath, command.subCommands)); } diff --git a/lib/parse-command.js b/lib/parse-command.js index 7bc48d9..8c3ad0a 100644 --- a/lib/parse-command.js +++ b/lib/parse-command.js @@ -291,8 +291,9 @@ module.exports = async filePath => { } }); - return { + const command = { description, + positionalArgs: positionalArgs || [], args: args.map(arg => { return { ...arg, @@ -302,4 +303,37 @@ module.exports = async filePath => { }; }) }; + + const argsMap = new Map( + command.args.map(arg => [arg.key, arg]) + ); + + if (positionalArgs && positionalArgs.length > 0) { + const missingArg = positionalArgs.find(key => !argsMap.has(key)); + + if (missingArg) { + throw new Error(`Positional argument \`${missingArg}\` is not declared in \`propTypes\` in \`${componentName}\``); + } + + const firstOptionalArg = positionalArgs.findIndex(key => !argsMap.get(key).isRequired); + + if (firstOptionalArg > -1 && positionalArgs.some((key, index) => index > firstOptionalArg && argsMap.get(key).isRequired)) { + throw new Error(`Optional positional arguments need to come after required positional arguments in \`${componentName}\``); + } + + const variadicArg = positionalArgs.findIndex(key => argsMap.get(key).type === 'array'); + + if (variadicArg > -1 && variadicArg !== positionalArgs.length - 1) { + throw new Error(`Variadic positional argument must be at the end in \`${componentName}\``); + } + + for (const key of positionalArgs) { + const {aliases} = argsMap.get(key); + if (aliases.length > 1) { + console.warn(`Positional arguments can only have up to one alias. The rest will be ignored for \`${key}\` in \`${componentName}\``); + } + } + } + + return command; }; diff --git a/lib/read-commands.js b/lib/read-commands.js index 5b89451..6c793bf 100644 --- a/lib/read-commands.js +++ b/lib/read-commands.js @@ -1,43 +1,46 @@ 'use strict'; const {promisify} = require('util'); -const {join, basename, relative} = require('path'); +const {join, relative, extname, parse} = require('path'); const fs = require('fs'); const parseCommand = require('./parse-command'); const stat = promisify(fs.stat); -const isIndexCommand = command => basename(basename(command.path, '.js'), '.tsx') === 'index'; - // `dirPath` is a path in source `commands` folder // `buildDirPath` is a path to the same folder, but in `build` folder const readCommands = async (commandsPath, dirPath) => { const paths = fs.readdirSync(dirPath); const commands = []; - const promises = paths.filter(path => path.endsWith('.js') || path.endsWith('.tsx')).map(async path => { + const promises = paths.map(async path => { // Since `readdir` returns relative paths, we need to transform them to absolute paths const fullPath = join(dirPath, path); const stats = await stat(fullPath); if (stats.isDirectory()) { const subCommands = await readCommands(commandsPath, fullPath); - const indexCommand = subCommands.find(isIndexCommand); + const indexCommand = subCommands.find(command => command.isIndex) || { + isDefaultIndex: true + }; commands.push({ ...indexCommand, + isIndex: false, name: path, - subCommands: subCommands.filter(command => !isIndexCommand(command)) + subCommands: subCommands.filter(command => !command.isIndex) }); } - if (stats.isFile()) { - const {description, args} = await parseCommand(fullPath); + if (stats.isFile() && ['.js', '.tsx'].includes(extname(fullPath))) { + const command = await parseCommand(fullPath); + const {name} = parse(fullPath); + const isIndex = name === 'index'; commands.push({ + ...command, + isIndex, + name: isIndex ? '*' : name, path: relative(commandsPath, fullPath), - name: basename(basename(fullPath, '.js'), '.tsx'), - description, - args, subCommands: [] }); } diff --git a/readme.md b/readme.md index 988985c..53dab18 100644 --- a/readme.md +++ b/readme.md @@ -422,6 +422,51 @@ $ my-cli Jane Hopper First argument is "Jane" and second is "Hopper" ``` +The order of the fields in `positionalArgs` will be respected. Optional arguments need to appear after required ones. +If you want to collect an arbitrary amount of arguments you can define a variadic argument by giving it the array type. +Variadic arguments need to always be last and will capture all the remaining arguments. + +```jsx +import React from 'react'; +import PropTypes from 'prop-types'; +import {Text} from 'ink'; + +const DownloadCommand = ({urls}) => ( + <Text> + Downloading {urls.length} urls + </Text> +); + +DownloadCommand.propTypes = { + urls: PropTypes.array +}; + +DownloadCommand.positionalArgs = ['urls']; + +export default DownloadCommand; +``` + +```bash +$ my-cli download https://some/url https://some/other/url +Downloading 2 urls +``` + +Positional arguments also support aliases, but only one per argument. The rest will be ignored. + +### TypeScript + +Pastel supports TypeScript by simply renaming a command file and giving it the `.tsx` extension. A `tsconfig.json` will be generated for you. + +If you want to define your own, make sure it contains the following: + +```json +{ + "compilerOptions": { + "jsx": "react" + } +} +``` + ### Distribution Since Pastel compiles your application, the final source code of your CLI is generated in the `build` folder. diff --git a/test/commands.js b/test/commands.js index 3faec72..f2e7ad8 100644 --- a/test/commands.js +++ b/test/commands.js @@ -16,12 +16,12 @@ const build = async fixture => { }); }; -const cli = async (fixture, args = []) => { - const {stdout} = await execa('node', [path.join(__dirname, 'fixtures', 'commands', fixture, 'build', 'cli'), ...args], { +const cli = async (fixture, args = [], {returnStderr} = {}) => { + const {stdout, stderr} = await execa('node', [path.join(__dirname, 'fixtures', 'commands', fixture, 'build', 'cli'), ...args], { cwd: path.join(__dirname, 'fixtures', 'commands', fixture) }); - return stdout; + return returnStderr ? stderr : stdout; }; const cleanup = () => { @@ -44,9 +44,26 @@ test('multiple commands', async t => { await build('multi-command'); const outputA = await cli('multi-command', ['a']); const outputB = await cli('multi-command', ['b']); + const outputC = await cli('multi-command', ['with-index', 'c']); + const outputWithIndex = await cli('multi-command', ['with-index']); + const outputD = await cli('multi-command', ['without-index', 'd']); + const outputWithoutIndex = await cli('multi-command', ['without-index'], {returnStderr: true}); t.is(outputA, 'Command A'); t.is(outputB, 'Command B'); + t.is(outputC, 'Command C'); + t.is(outputWithIndex, 'Command With Index'); + t.is(outputD, 'Command D'); + t.is(outputWithoutIndex, stripIndent(` + cli without-index + + Commands: + cli without-index d + + Options: + --help Show help [boolean] + --version Show version number [boolean] + `).trim()); }); test('flags', async t => { @@ -101,10 +118,13 @@ test('aliases', async t => { const helpOutput = await cli('aliases', ['--help']); t.is(helpOutput, stripIndent(` - cli + cli [positional|other-name] Aliases command + Positionals: + positional, other-name Positional arg [string] + Options: --help Show help [boolean] --version Show version number [boolean] @@ -142,11 +162,12 @@ test('positional args', async t => { const helpOutput = await cli('positional-args', ['--help']); t.is(helpOutput, stripIndent(` - cli <message> <other-message> + cli <message> [other-message] [rest-messages..] Positional args command Positionals: + rest-messages Rest of the messages message Message [string] other-message Other message [string] @@ -160,7 +181,8 @@ test('positional args', async t => { t.is(cmdOutput, stripIndent(` message: hello otherMessage: world - inputArgs: ["something","else"] + restMessages: ["something","else"] + inputArgs: [] `).trim()); }); diff --git a/test/fixtures/commands/aliases/commands/index.js b/test/fixtures/commands/aliases/commands/index.js index 631cc4a..e839046 100644 --- a/test/fixtures/commands/aliases/commands/index.js +++ b/test/fixtures/commands/aliases/commands/index.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import {Box, Text} from 'ink'; /// Aliases command -const Aliases = ({stream, newArg}) => ( +const Aliases = ({stream, newArg, positional}) => ( <Box flexDirection="column"> <Text>stream: {stream}</Text> <Text>newArg: {newArg}</Text> @@ -14,15 +14,20 @@ Aliases.propTypes = { /// Stream arg stream: PropTypes.string, /// New arg - newArg: PropTypes.string + newArg: PropTypes.string, + /// Positional arg + positional: PropTypes.string }; Aliases.shortFlags = { stream: 's' }; +Aliases.positionalArgs = ['positional']; + Aliases.aliases = { - newArg: ['oldArg'] + newArg: ['oldArg'], + positional: 'otherName' }; export default Aliases; diff --git a/test/fixtures/commands/multi-command/commands/with-index/c.js b/test/fixtures/commands/multi-command/commands/with-index/c.js new file mode 100644 index 0000000..b4e7e0b --- /dev/null +++ b/test/fixtures/commands/multi-command/commands/with-index/c.js @@ -0,0 +1,6 @@ +import React from 'react'; +import {Text} from 'ink'; + +const CommandC = () => <Text>Command C</Text>; + +export default CommandC; diff --git a/test/fixtures/commands/multi-command/commands/with-index/index.js b/test/fixtures/commands/multi-command/commands/with-index/index.js new file mode 100644 index 0000000..2b569f9 --- /dev/null +++ b/test/fixtures/commands/multi-command/commands/with-index/index.js @@ -0,0 +1,6 @@ +import React from 'react'; +import {Text} from 'ink'; + +const CommandWithIndex = () => <Text>Command With Index</Text>; + +export default CommandWithIndex; diff --git a/test/fixtures/commands/multi-command/commands/without-index/d.js b/test/fixtures/commands/multi-command/commands/without-index/d.js new file mode 100644 index 0000000..663b22d --- /dev/null +++ b/test/fixtures/commands/multi-command/commands/without-index/d.js @@ -0,0 +1,6 @@ +import React from 'react'; +import {Text} from 'ink'; + +const CommandD = () => <Text>Command D</Text>; + +export default CommandD; diff --git a/test/fixtures/commands/positional-args/commands/index.js b/test/fixtures/commands/positional-args/commands/index.js index 5f1536a..ce395fd 100644 --- a/test/fixtures/commands/positional-args/commands/index.js +++ b/test/fixtures/commands/positional-args/commands/index.js @@ -3,23 +3,26 @@ import PropTypes from 'prop-types'; import {Box, Text} from 'ink'; /// Positional args command -const PositionalArgs = ({message, otherMessage, inputArgs}) => ( +const PositionalArgs = ({message, otherMessage, inputArgs, restMessages}) => ( <Box flexDirection="column"> <Text>message: {message}</Text> <Text>otherMessage: {otherMessage}</Text> + <Text>restMessages: {JSON.stringify(restMessages)}</Text> <Text>inputArgs: {JSON.stringify(inputArgs)}</Text> </Box> ); PositionalArgs.propTypes = { + /// Rest of the messages + restMessages: PropTypes.array, /// Message - message: PropTypes.string, + message: PropTypes.string.isRequired, /// Other message otherMessage: PropTypes.string, /// Input args inputArgs: PropTypes.array }; -PositionalArgs.positionalArgs = ['message', 'otherMessage']; +PositionalArgs.positionalArgs = ['message', 'otherMessage', 'restMessages']; export default PositionalArgs; diff --git a/test/fixtures/commands/typescript/package.json b/test/fixtures/commands/typescript/package.json index 2f5a96c..bb82ca8 100644 --- a/test/fixtures/commands/typescript/package.json +++ b/test/fixtures/commands/typescript/package.json @@ -2,6 +2,6 @@ "name": "test-bin", "bin": "./build/cli", "devDependencies": { - "typescript": "^3.5.2" + "typescript": "^3.9.7" } } diff --git a/test/fixtures/parse-command/class-positional-args.js b/test/fixtures/parse-command/class-positional-args.js index 79502ed..2064473 100644 --- a/test/fixtures/parse-command/class-positional-args.js +++ b/test/fixtures/parse-command/class-positional-args.js @@ -4,10 +4,11 @@ import PropTypes from 'prop-types'; /// Description class Demo extends React.Component { static propTypes = { + variadicArg: PropTypes.array, arg: PropTypes.string } - static positionalArgs = ['arg'] + static positionalArgs = ['arg', 'variadicArg'] render() { return null; diff --git a/test/fixtures/parse-command/function-positional-args.js b/test/fixtures/parse-command/function-positional-args.js index 442fbfe..9795760 100644 --- a/test/fixtures/parse-command/function-positional-args.js +++ b/test/fixtures/parse-command/function-positional-args.js @@ -4,9 +4,10 @@ import PropTypes from 'prop-types'; const Demo = () => null; Demo.propTypes = { - arg: PropTypes.string + optionalArg: PropTypes.string, + arg: PropTypes.string.isRequired }; -Demo.positionalArgs = ['arg']; +Demo.positionalArgs = ['arg', 'optionalArg']; export default Demo; diff --git a/test/fixtures/parse-command/wrong-order-positional-args.js b/test/fixtures/parse-command/wrong-order-positional-args.js new file mode 100644 index 0000000..5625e36 --- /dev/null +++ b/test/fixtures/parse-command/wrong-order-positional-args.js @@ -0,0 +1,13 @@ +import PropTypes from 'prop-types'; + +/// Description +const Demo = () => null; + +Demo.propTypes = { + arg: PropTypes.string.isRequired, + optionalArg: PropTypes.string +}; + +Demo.positionalArgs = ['optionalArg', 'arg']; + +export default Demo; diff --git a/test/fixtures/parse-command/wrong-variadic-arg.js b/test/fixtures/parse-command/wrong-variadic-arg.js new file mode 100644 index 0000000..71dd8d9 --- /dev/null +++ b/test/fixtures/parse-command/wrong-variadic-arg.js @@ -0,0 +1,13 @@ +import PropTypes from 'prop-types'; + +/// Description +const Demo = () => null; + +Demo.propTypes = { + arg: PropTypes.string, + variadicArg: PropTypes.array +}; + +Demo.positionalArgs = ['variadicArg', 'arg']; + +export default Demo; diff --git a/test/parse-command.js b/test/parse-command.js index 464e00d..8056ba5 100644 --- a/test/parse-command.js +++ b/test/parse-command.js @@ -9,6 +9,7 @@ test('parse arrow function', async t => { t.deepEqual(command, { description: 'Description', + positionalArgs: [], args: [ { key: 'arg', @@ -28,7 +29,8 @@ test('parse arrow function with default export', async t => { t.deepEqual(command, { description: 'Description', - args: [] + args: [], + positionalArgs: [] }); }); @@ -37,6 +39,7 @@ test('parse named function', async t => { t.deepEqual(command, { description: 'Description', + positionalArgs: [], args: [ { key: 'arg', @@ -56,6 +59,7 @@ test('parse named function with default export', async t => { t.deepEqual(command, { description: 'Description', + positionalArgs: [], args: [ { key: 'arg', @@ -75,6 +79,7 @@ test('parse class', async t => { t.deepEqual(command, { description: 'Description', + positionalArgs: [], args: [ { key: 'arg', @@ -94,6 +99,7 @@ test('parse class with static prop types', async t => { t.deepEqual(command, { description: 'Description', + positionalArgs: [], args: [ { key: 'arg', @@ -113,6 +119,7 @@ test('parse class with default export', async t => { t.deepEqual(command, { description: 'Description', + positionalArgs: [], args: [ { key: 'arg', @@ -132,6 +139,7 @@ test('parse prop types', async t => { t.deepEqual(command, { description: 'Description', + positionalArgs: [], args: [ { key: 'stringArg', @@ -178,6 +186,7 @@ test('parse function aliases and short flags', async t => { t.deepEqual(command, { description: 'Description', + positionalArgs: [], args: [ { key: 'arg', @@ -197,6 +206,7 @@ test('parse class aliases and short flags', async t => { t.deepEqual(command, { description: 'Description', + positionalArgs: [], args: [ { key: 'arg', @@ -216,15 +226,25 @@ test('parse function positional args', async t => { t.deepEqual(command, { description: 'Description', + positionalArgs: ['arg', 'optionalArg'], args: [ { - key: 'arg', + key: 'optionalArg', type: 'string', description: '', isRequired: false, defaultValue: undefined, aliases: [], positional: true + }, + { + key: 'arg', + type: 'string', + description: '', + isRequired: true, + defaultValue: undefined, + aliases: [], + positional: true } ] }); @@ -235,7 +255,17 @@ test('parse class positional args', async t => { t.deepEqual(command, { description: 'Description', + positionalArgs: ['arg', 'variadicArg'], args: [ + { + key: 'variadicArg', + type: 'array', + description: '', + isRequired: false, + defaultValue: undefined, + aliases: [], + positional: true + }, { key: 'arg', type: 'string', @@ -249,10 +279,19 @@ test('parse class positional args', async t => { }); }); +test('throw error when positional args are in wrong order', async t => { + await t.throws(parseCommand(fixture('wrong-order-positional-args'))); +}); + +test('throw error when variadic arg is not last', async t => { + await t.throws(parseCommand(fixture('wrong-variadic-arg'))); +}); + test('do not error on let reassignment', async t => { const command = await parseCommand(fixture('let')); t.deepEqual(command, { description: 'Description', + positionalArgs: [], args: [] }); });