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: []
 	});
 });