Aleš Dostál
It is probably useless to write that JavaScript is a language that is used in other platforms than just in a web browser today. We often come across the fact that we would like our own console application, which performs our necessary activities. Bash, PowerShell or languages such as C, Python, etc. can be used for this purpose. However, why not use something we know. JavaScript can serve us more than well for this purpose.
To make the examples you find here easier to understand, you can take a look at the repository I created for this purpose: apitree-cli-example.
The first step is to install Node.js. At the same time, I recommend using Yarn to run scripts during development. In any case, the choice of whether to use Yarn or NPM is up to you.
After successfully installing Node.js, we can get to work :)
You can set up the project easily using the npm command:
npm init -y
Then we will install the libraries that we will use in our project:
npm i chalk figlet yargs
We will install type definitions of libraries, ts-node and TypeScript into the dev dependencies:
npm i -D @types/node @types/figlet @types/yargs ts-node typescript
Now we will configure TypeScript using the tsconfig.json file (located in the root of the project):
{ "compilerOptions": { "target": "es5", "module": "commonjs", "lib": ["es6", "es2015", "dom"], "declaration": true, "outDir": "./lib", "rootDir": "./src", "strict": true, "esModuleInterop": true, "resolveJsonModule": true }, "include": ["src/**/*"] }
Nothing is stopping us now and we can start writing our dream code.
What is better to start with than Hello World.
In the src directory we create an index.ts file with the following code:
console.log('Hello World');
To run our amazing code now, we'll modify the package.json, more precisely blok scripts, as follows:
"scripts": { "dev": "ts-node src/index.ts" }
Since we have installed the ts-node library, we can now run TypeScript files directly without having to compile. So here we go. We will run this in the console:
npm run dev
If you did everything right, the console should list Hello World.
We will probably agree that so far we have not succeeded in anything very amazing. Although we have our first program but writing Hello World in the console is not very useful. That's why we will enrich our project a bit.
You must have noticed that we initially installed different libraries and one of them is called yargs. This library is used to allow us to easily write a program that accepts and validates arguments from the console.
To make our project senseful, let's say we want to write a console application that has two commands: create and update. And the --name parameter is required for the create command. Such an application could serve as a generator of your own applications, such as create-react-app, which many of you certainly know well.
So let's modify index.ts to look like this:
#!/usr/bin/env node import yargs from 'yargs'; import { ConsoleService } from './service'; const MAIN_CMD = 'at-example'; const TITLE = 'ApiTree CLI'; const log = ConsoleService.getLogger(); enum Command { CREATE = 'create', UPDATE = 'update', } const run = async (command: Command, args: { [appArgs: string]: unknown }) => { log.title(await ConsoleService.getTitle(TITLE)); switch (command) { case Command.CREATE: log.info(`Create awesome application with name: ${args.name}`); return; case Command.UPDATE: log.info('Update awesome application'); return; default: throw new Error(`Command not implement yet`); } }; yargs .usage(TITLE) .command({ command: Command.CREATE, describe: 'Create awesome application', aliases: 'c', builder: (builder) => builder.options({ name: { type: 'string', demandOption: true, alias: 'n' } }), handler: async (args) => { await run(Command.CREATE, args); }, }) .command({ command: Command.UPDATE, describe: 'Update awesome application', aliases: 'u', handler: async (args) => { await run(Command.UPDATE, args); }, }) .example(Command.CREATE, `${MAIN_CMD} ${Command.CREATE} --name awesome-app`) .example(Command.UPDATE, `${MAIN_CMD} ${Command.UPDATE}`) .demandCommand(1, 1) .showHelpOnFail(true) .epilog('ApiTree software') .strict().argv;
That certainly looks more interesting. Now let's go through the individual parts that we see here.
On the first line you may notice (#!/usr/bin/env node). We don't need it at the moment, but when we distribute our console application, it's exactly this line that makes our script a self-running script.
Then we defined all commands that our application will support with Enum type. So, in our case it is create and update.
Then there is the run function, which executes the given code based on the command and args parameter. Here is a link to the method from ConsoleService, which can be found in the mentioned repository.
Then comes the configuration of the yargs library itself. Here are the most interesting commands themselves in the command block. So we need to specify the name of the command, its description, alias, and then the function that executes the code if it is a given command. For the create command, there is also a builder method that returns the option parameter name, which is of type string and is also mandatory. In addition to defining commands, it is good to set other parameters such as demandCommand(min, max). This method tells you how many commands you can enter in a single run. We will stick to the fact that min and max are 1, and therefore it is possible to run only one command.
Now let's test if our application works. We will use the already defined script in package.json for this. We will run the script using Yarn:
yarn dev
If you did everything correctly, the script should end with an error message saying that you must enter a valid argument:
Not enough non-option arguments: got 0, need at least 1
Therefore, let's run the program with the given parameters:
yarn dev create --name awesome-app
We should now get a successful message:
Create awesome application with name: awesome-app
We can also use our aliases:
yarn run dev c -n test-app
That looks a little better. In addition, the yargs library implements help and version arguments. Let's try it:
yarn run dev --help
In that case, we should get a complete listing of the options we have:
ApiTree CLI Commands: index.ts create Create awesome application [aliases: c] index.ts update Update awesome application [aliases: u] Options: --help Show help [boolean] --version Show version number [boolean] Examples: create at-example create --name awesome-app update at-example update ApiTree software
It is also possible to use help on the commands themselves. See:
yarn run dev create --help
Here we can notice that we have added another option, which is marked as required:
--name, -n [string] [required]
I think we have our application ready. What exactly and how the application will do is up to you. As a last thing, we will now show you how to actually distribute such an application.
In order to distribute our application, we need to modify our package.json:
"files": [ "lib/**/*" ], "main": "./lib/index.js", "bin": { "at-example": "./lib/index.js" }, "scripts": { "dev": "ts-node src/index.ts", "build": "tsc" }
The files block we say which files or directories we will distribute. In our case, it is the lib directory into which the resulting TypeScript file will be compiled.
Next we say which file is the default (main). In this case, it's index.js in the lib directory. This is created by the compilation itself.
Another important setting is the name of the script itself. Here it is good to define a name that is sufficiently descriptive and at the same time does not conflict with other commands in the operating system. In our case, the command will be named at-example.
The last part is the build, which is defined in the scripts block.
Before we distribute our amazing program to the NPM repository, it's a good idea to test it locally. We will use npm for this purpose.
First we compile our project:
npm run build
After compilation, the lib directory is created, in which our project is compiled into JavaScript with a TypeScript definition.
We will now pack our project using NPM to reinstall it globally:
npm pack
After this command, a tgz file will be created in the root of the project, which is named after the name and version in package.json. In my case it is: apitree-apitree-cli-example-0.1.0-alpha.1.tgz, because I have the project name: @ apitree / apitree-cli-example and version: 0.1.0-alpha.1.
We will now install this file globally:
npm i -g apitree-apitree-cli-example-0.1.0-alpha.1.tgz
After a successful installation, we can start our project using the at-example command:
at-example --help
The result is the same as when running via ts-node.
It is necessary to have an NPM repository for remote distribution. Either on npmjs.com or your own. It is also necessary to log in to this repository. Either using an npm login or via .npmrc file in which an access token is defined.
For remote distribution, just run:
npm publish
And for the installation itself then:
npm i -g @company/my-name
Writing console (CLI) applications in JavaScript is a simple matter. The biggest obstacle here is rather the initial configuration itself, which can discourage anyone. However, if you try this start, you have a plenty of variations on what such an application can do. Rather, you may encounter differences between operating systems. For example, this project was created on MacOS, so to ensure compatibility, the project itself should be tested on other systems as well.
Just leave us a message using the contact form or contact our sales department directly. We will arrange a meeting and discuss your business needs. Together we will discuss the options and propose the most suitable solution.