Project setup
Create a new folder (my-cli-demo
for example) and use npm init
command. Answer the questions with default options, except the entry point, which should be changed to bin/index.js
package name: (cli-demo)
version: (1.0.0)
description:
entry point: (index.js) bin/index.js
test command:
keywords:
author:
license: (ISC)
After that, we can use npm to install the project dependencies for our CLI tool:
npm i yargs chalk@~4 && npm i typescript @types/yargs @types/node --save-dev
Lastly we need to tell npm that our package has an executable file and since we are using typescript, we should also add a watch
script, to recompile our code when we make changes. This will allow us to test our CLI tool locally, without having to run a compile command manually every time:
{
"name": "my-cli-demo",
"version": "1.0.0",
"bin": {
"my-cli-demo": "bin/index.js" // <- command name of our cli script and the executable file it points to
},
"main": "bin/index.js",
"scripts": {
"watch": "tsc -w" // <- watch script
},
"license": "ISC",
"dependencies": {
"chalk": "^4.1.2",
"yargs": "^17.7.2"
},
"devDependencies": {
"@types/node": "^20.1.0",
"@types/yargs": "^17.0.24",
"typescript": "^5.0.4"
}
}
Typescript configuration
We should also add a minimal tsconfig.json
:
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"declaration": true, // creates d.ts type declarations
"declarationMap": true, // creates map files for our d.ts files
"sourceMap": true, // creates map files for our source code
"outDir": "bin", // compile source code into "bin" folder
"strict": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": true,
"esModuleInterop": true
},
"include": [
"src/index.ts"
],
"exclude": [
"node_modules"
]
}
Parsing command line arguments
It’s time to start writing our CLI script! We’re going to use yargs
to parse the command line arguments, that can be passed to our CLI tool. Yargs makes it easier to define which command line arguments can be used and it can automatically add documentation for our CLI tool that can be show with the --help
flag.
Start by creating src/index.ts
and adding the following contents:
#! /usr/bin/env node
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'
yargs(hideBin(process.argv))
.help()
.argv
Let’s start by getting our watch script up and running by typing npm run watch
in a terminal. Now everytime we make a change, our code will be recompiled into the bin
folder, which is configured as our main entry point. If we open another terminal and run node . --help
, we should see the following output from our CLI script:
Options:
--version Show version number [boolean]
--help Show help [boolean]
Adding a command module
Yargs allows us to define commands for our CLI tool, for example, we could define an init
command that gets executed like this:
node . init
To add a command module, let’s start by creating src/commands/init.ts
and adding the following content:
import { CommandModule, Argv, ArgumentsCamelCase } from 'yargs'
import chalk from 'chalk'
// the builder function can be used to define additional
// command line arguments for our command
function builder(yargs: Argv) {
return yargs.option('name', {
alias: 'n',
string: true
})
}
// the handler function will be called when our command is executed
// it will receive the command line arguments parsed by yargs
function handler(args: ArgumentsCamelCase) {
console.log(chalk.green('Hello world!'), args)
}
// name and description for our command module
const init: CommandModule = {
command: 'init',
describe: 'Init command',
builder,
handler
}
export default init
We should also change our src/index.ts
to let yargs know about our new command module:
#! /usr/bin/env node
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'
import init from './commands/init'
yargs(hideBin(process.argv))
.command(init) // registers the init command module
// or to register everything in the commands dir: .commandDir('./commands')
.demandCommand()
.help()
.argv
Now if we run node . init --name Rense
in our terminal, we will see the following output:
Hello world! Rense
Publishing to NPM
To publish our new CLI tool to NPM, we first need to add a git repository. Run the git init
command in our project root directory and then create a repository on github or any other git host and add the remote url to our project with: git remote add origin git@github.com:[username]/[repository_name].git
now add our files to the registry with git add .
and push our initial commit: git commit -m "init" && git push
.
We will use np
for publishing, let’s install some additional dependencies to make this happen:
npm i np cross-env --save-dev
We’re going to add a release
script to our package.json
, but we need a way to prevent someone from accidentally running npm publish
in the root of our project. To achieve this we can create this prepublish.js
file with the following content:
const RELEASE_MODE = !!(process.env.RELEASE_MODE)
if (!RELEASE_MODE) {
console.log('Run `npm run release` to publish the package')
process.exit(1)
}
Now we can add the following scripts to our package.json
:
[
"prepublishOnly": "node prepublish.js && tsc",
"release": "cross-env RELEASE_MODE=true np --no-tests"
]
And then run npm run release
and answer the questions that np
asks us. The prePublishOnly
script will prevent anyone from manually running npm publish
.
Using our CLI tool
After the CLI tool is published to NPM, we can install it anywhere we want to use it:
npm i my-cli-demo
my-cli-demo init --name Rense
Top comments (0)