@elricb/pastel
v1.1.2
Published
Framework for effortlessly building Ink apps
Downloads
4
Readme
Framework for effortlessly building Ink apps inspired by Ronin and ZEIT's Next
Install
$ npm install pastel ink react prop-types
Get Started
Pastel's API is a filesystem and React's propTypes
.
Each file in commands
folder is a separate command.
If you need to set up nested commands, you can create sub-folders and put these commands inside.
Tip: Want to skip the boring stuff and get straight to building a cool CLI? Use create-pastel-app to quickly scaffold out a Pastel app.
First, create a package.json
with the following contents:
{
"name": "hello-person",
"bin": "./build/cli.js",
"scripts": {
"build": "pastel build",
"dev": "pastel dev",
"prepare": "pastel build"
}
}
After that, install Pastel and its dependencies:
$ npm install pastel ink react prop-types
Then create a commands
folder:
$ mkdir commands
Then you can start creating commands in that folder. Let's create a main command, which has to be named index.js
at commands/index.js
:
import React from 'react';
import PropTypes from 'prop-types';
import {Text} from 'ink';
/// This is my command description
const HelloPerson = ({name}) => <Text>Hello, {name}</Text>;
HelloPerson.propTypes = {
/// This is "name" option description
name: PropTypes.string.isRequired
};
export default HelloPerson;
Note that React's propTypes
are translated into options that are accepted by your CLI.
Finally, last step is to build your CLI:
$ npm run dev
This command will link your CLI, which will make hello-person
(which is the name we picked in package.json
) executable available globally.
It will also watch for any changes and rebuild your application.
Now is the time to test your CLI!
$ hello-person --name=Millie
Hello, Millie
Pastel also generates a help message automatically:
$ hello-person --help
hello-person
This is my command description
Options:
--name This is "name" option description [boolean]
Did you notice that command and option descriptions are parsed from JavaScript comments as well? Neat! If you found Pastel interesting, keep reading to see what else it can do!
Documentation
Commands
Each file in commands
folder is a command. For example, if you need a main (or index), create
and delete
commands, simply create index.js
, create.js
and delete.js
files like that:
commands/
- index.js
- create.js
- delete.js
If you need nested commands, simply create sub-folders and put the files inside:
commands/
- index.js
- posts/
- index.js
- create.js
- update.js
This will generate 4 commands:
my-cli
(index.js
)my-cli posts
(posts/index.js
)my-cli posts create
(posts/create.js
)my-cli posts update
(posts/update.js
)
Each command must export a React command for rendering that command. For example:
import React from 'react';
import {Text} from 'ink';
const HelloWorld = () => <Text>Hello World</Text>;
export default HelloWorld;
Options
To accept options in your command, simply define the propTypes
of your command's React component.
Pastel scans each command and determines which propTypes
are set.
Then it adds them to the list of accepted options of your command and to help message of your CLI (--help
).
For example, here's how to accept a name
option:
import React from 'react';
import PropTypes from 'prop-types';
import {Text} from 'ink';
const Hello = ({name}) => <Text>Hello, {name}</Text>;
Hello.propTypes = {
name: PropTypes.string.isRequired
};
export default Hello;
Assuming this is an index.js
command and your CLI is named hello
, you can execute this command like this:
$ hello --name=Millie
Hello, Millie
Beautiful, isn't it?
Pastel supports the following propTypes
:
string
bool
number
array
Naming
Options that are named with a single word (e.g. name
, force
, verbose
) aren't modified in any way.
However, there are cases where you need longer names like --project-id
and variables that have dashes in their name aren't supported in JavaScript.
Pastel has an elegant solution for this!
Just name your option projectId
(camelCase) and Pastel will define it as --project-id
option in your CLI.
import React from 'react';
import PropTypes from 'prop-types';
import {Text} from 'ink';
const ListMembers = ({projectId}) => /* JSX */;
ListMembers.propTypes = {
projectId: PropTypes.string.isRequired
};
export default ListMembers;
$ list-members --project-id=abc
Descriptions
Pastel also offers a zero-API way of adding description to your commands and options.
Simply add a comment that starts with 3 slashes (///
) above the command or option you want to describe and Pastel will automatically parse it.
For example, here's how to add a description to your command:
/// List all members in the project
const ListMembers = ({projectId}) => <JSX/>;
And here's how to document your options:
ListMembers.propTypes = {
/// ID of the project
projectId: PropTypes.string
};
When you run that command with a --help
flag, here's the help message that will be generated:
$ list-members --help
List all members in the project
Options:
--project-id ID of the project [string]
Default values
There's no API for defining default values either.
Just set the desired default values in defaultProps
property for each option and Pastel will take care of the rest.
import React from 'react';
import PropTypes from 'prop-types';
import {Text} from 'ink';
const Hello = ({name}) => <Text>Hello, {name}</Text>;
Hello.propTypes = {
name: PropTypes.string
};
Hello.defaultProps = {
name: 'Katy'
};
export default Hello;
$ hello
Hello, Katy
Short flags
Options can often be set with a shorter version of their name, using short flags.
Most popular example is --force
option. Most CLIs also accept -f
as a shorter version of the same option.
To achieve the same functionality in Pastel, you can set shortFlags
property and define short equivalents of option names:
ImportantCommand.propTypes = {
force: PropTypes.bool
};
ImportantCommand.shortFlags = {
force: 'f'
};
Then you will be able to pass -f
instead of --force
:
$ important-command -f
Aliases
Aliases work the same way as short flags, but they have a different purpose.
Most likely you will need aliases to rename some option you want to deprecate.
For example, if your CLI previously accepted --group
, but you've decided to rename it to --team
, aliases will come handy:
import React from 'react';
import PropTypes from 'prop-types';
import {Text} from 'ink';
const ListMembers = ({team}) => /* JSX */;
ListMembers.propTypes = {
team: PropTypes.string.isRequired
};
ListMembers.aliases = {
team: 'group'
};
export default ListMembers;
Both of these commands will produce the same output:
$ list-members --team=rockstars
$ list-members --group=rockstars
You can also set multiple aliases per option by passing an array:
ListMembers.aliases = {
team: ['group', 'squad']
};
Arguments
First of all, let's clarify that arguments are different than options.
They're not prefixed with --
or -
and passed to your CLI like this:
$ my-beautiful-cli first second third
There can be any amount of such arguments, so we can't and shouldn't define a prop type for each of them.
Instead, Pastel reserves inputArgs
prop to pass these arguments to your command:
import React from 'react';
import PropTypes from 'prop-types';
import {Text} from 'ink';
const MyCli = ({inputArgs}) => (
<Text>
First argument is "{inputArgs[0]}" and second is "{inputArgs[1]}"
</Text>
);
MyCli.propTypes = {
inputArgs: PropTypes.array
};
export default MyCli;
If you run this command like this:
$ my-cli Jane Hopper
You will see the following output:
First argument is "Jane" and second is "Hopper"
Positional arguments
If you check out the example from the section above, you'll see that accessing arguments via index in inputArgs
may not be convenient.
inputArgs
works great for cases where you can't predict the amount of arguments.
But if you do know that user can pass 2 arguments, for example, then you can take advantage of positional arguments.
Positional arguments are specified the same way as regular arguments, only each of them can be assigned to a different prop.
So if you take a look at the example above, we know that first argument is the first name and second argument is last name.
Here's how to define that in Pastel:
import React from 'react';
import PropTypes from 'prop-types';
import {Text} from 'ink';
const MyCli = ({firstName, lastName}) => (
<Text>
First argument is "{firstName}" and second is "{lastName}"
</Text>
);
MyCli.propTypes = {
firstName: PropTypes.string,
lastName: PropTypes.string
};
MyCli.positionalArgs = ['firstName', 'lastName'];
export default MyCli;
Nothing changes the way you execute this command:
$ 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.
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;
$ 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:
{
"compilerOptions": {
"jsx": "react"
}
}
Distribution
Since Pastel compiles your application, the final source code of your CLI is generated in the build
folder.
I recommend adding this folder to .gitignore
to prevent committing it to your repository.
Also, to make sure you're shipping the latest and working version of the CLI when publishing your package on npm, add prepare
script to package.json
, which runs pastel build
.
{
"scripts": {
"prepare": "pastel build"
}
}
This will always build your application before publishing.
Another important part is including build
folder in the npm package by adding it to files
field (if you're using it):
{
"files": [
"build"
]
}
And last but not least, bin
field.
This is the field which tells npm that your package contains a CLI and to ensure Pastel is working correctly, you must set it to ./build/cli.js
:
{
"bin": "./build/cli.js"
}
To sum up, here's the required fields together:
{
"name": "my-cli",
"bin": "./build/cli.js",
"scripts": {
"prepare": "pastel build"
},
"files": [
"build"
]
}
License
MIT © Vadim Demedes