Boost your productivity by creating your own CLI command with typescript (Part 1)

nodejs, typescript, npm, npm, cli
Saturday, January 9, 2021

Context

Your daily productivity can be greatly improved ๐Ÿš€ if you can automate all the tasks you used to do.

Thanks to node, npm, npx and typescript: creating a CLI command and making it available on your system or globally has never been easier.

As an example, we will create a CLI command to get a stock value for a symbol. This command will be called pique-sous (as a reference to the Disney character "Uncle \$crooge" in french ๐Ÿ˜„ ๐Ÿฆ†)

1$ pique-sous MSFT SFIX GOOG

Result:

1Retrieving stock information for MSFT at date 2021-01-10T01:37:57.574Z
2{
3 language: 'en-US',
4 region: 'US',
5 quoteType: 'EQUITY',
6 quoteSourceName: 'Delayed Quote',
7 regularMarketOpen: 218.68,
8 exchange: 'NMS',
9 shortName: 'Microsoft Corporation',
10 longName: 'Microsoft Corporation',
11 messageBoardId: 'finmb_21835',
12 exchangeTimezoneName: 'America/New_York',
13 exchangeTimezoneShortName: 'EST',
14 gmtOffSetMilliseconds: -18000000,
15 market: 'us_market',
16 esgPopulated: false,
17 displayName: 'Microsoft',
18 symbol: 'MSFT'
19}

The final results in available at https://github.com/raphaelmansuy/pique-sous and published at https://www.npmjs.com/package/pique-sous.

This article was originally published at https://www.elitizon.com/

๐Ÿ— 6 Easy steps to make it happen !

Step1 : creating a basic typescript project

โœ… Create a directory called pique-sous

1$ mkdir ./pique-sous

โœ… create a file index.ts under pique-sous

1$ cd ./pique-sous
2$ touch ./index.ts

As a result, you should have:

1pique-sous
2โ””โ”€โ”€ index.ts

โœ… Edit the index.ts and add a simple command for testing such as:

1const currentDateAndTime = new Date().toIsoString()
2
3console.log(currentDateTime)

โœ… Execute and test the file with ts-node

1npx ts-node index.ts

npx is a tool from the NPM registry allowing executing commands without installation ts-node is a node version supporting typescript directly

As a result you should have something like this:

12021-01-10T02:37:49.683Z

Step2 : make the file executable

โœ… Modify the index.ts file such as

1#!/usr/bin/env npx ts-node
2
3const currentDateAndTime = new Date().toIsoString()
4
5console.log(currentDateTime)

The first line #!/usr/bin/env npx ts-node specifies that the file must be executed by npx and ts-node

โœ… Add the executable permission to the index.ts file

1$ chmod u+x ./index.ts

โœ… Test the file

1$ ./index.ts

Results:

1$ ./index.ts
2$ 2021-01-10T03:24:43.190Z

Step 3: package the project

โœ… Add package.json file

Inside the directory use the npm command to create a package.json file

1$ npm init

Answer the questions:

1package name: (pique-sous)
2version: (1.0.0)
3description: A simple package
4entry point: (index.js) bin/index.js
5test command:
6git repository:
7keywords:
8author: raphael mansuy
9license: (ISC) MIT
10About to write to /Users/raphaelmansuy/Projects/Github/raphaelmansuy/ElitizonWeb/data/blog/2021/01-09-how-to-create-a-cli-command-with-typescript/steps/step01/pique-sous/package.json:
11
12{
13 "name": "pique-sous",
14 "version": "1.0.0",
15 "description": "A simple package",
16 "main": "bin/index.js",
17 "scripts": {
18 "test": "echo \"Error: no test specified\" && exit 1"
19 },
20 "author": "raphael mansuy",
21 "license": "MIT"
22}

โœ… Configure compilation from typescript to javascript

Create a file called tsconfig.json as follow:

1{
2 "compilerOptions": {
3 "module": "commonjs",
4 "target": "es2017",
5 "lib": ["es2015"],
6 "moduleResolution": "node",
7 "sourceMap": true,
8 "outDir": "bin",
9 "baseUrl": ".",
10 "paths": {
11 "*": ["node_modules/*", "src/types/*"]
12 },
13 "emitDecoratorMetadata": true,
14 "experimentalDecorators": true
15 },
16 "include": ["src/**/*"]
17}

โœ… Create a src directory and move the index.ts in the ./src directory

1$ mkdir ./src
2$ mv ./index.ts ./src

Results:

1.
2โ”œโ”€โ”€ package.json
3โ”œโ”€โ”€ src
4โ”‚ย ย  โ””โ”€โ”€ index.ts
5โ””โ”€โ”€ tsconfig.json
6
71 directory, 3 files

โœ… Add typescript support for the compilation

1$ yarn add typescript @types/node -D

Result:

1yarn add v1.22.10
2info No lockfile found.
3[1/4] ๐Ÿ” Resolving packages...
4[2/4] ๐Ÿšš Fetching packages...
5[3/4] ๐Ÿ”— Linking dependencies...
6[4/4] ๐Ÿ”จ Building fresh packages...
7success Saved lockfile.
8success Saved 2 new dependencies.
9info Direct dependencies
10โ”œโ”€ @types/node@14.14.20
11โ””โ”€ typescript@4.1.3
12info All dependencies
13โ”œโ”€ @types/node@14.14.20
14โ””โ”€ typescript@4.1.3
15โœจ Done in 1.44s.

The package.json should look like this:

1{
2 "name": "pique-sous",
3 "version": "1.0.0",
4 "description": "A simple package",
5 "main": "bin/index.js",
6 "scripts": {
7 "test": "echo \"Error: no test specified\" && exit 1"
8 },
9 "author": "raphael mansuy",
10 "license": "MIT",
11 "devDependencies": {
12 "@types/node": "^14.14.20",
13 "typescript": "^4.1.3"
14 }
15}

โœ… Edit the package.json as follow

๐Ÿ‘‰ add "bin" entry with value "bin/index.js" ๐Ÿ‘‰ add "build" command in "scripts"

1{
2 "name": "pique-sous",
3 "version": "1.0.0",
4 "description": "A simple package",
5 "main": "bin/index.js",
6 "bin": "bin/index.js",
7 "scripts": {
8 "build": "tsc",
9 "test": "echo \"Error: no test specified\" && exit 1"
10 },
11 "author": "raphael mansuy",
12 "license": "MIT",
13 "devDependencies": {
14 "@types/node": "^14.14.20",
15 "typescript": "^4.1.3"
16 }
17}

โœ… Edit the index.ts as follow

๐Ÿ‘‰ replace npx ts-node by node because the result of the compilation by typescript compiler will be a javascript file

1#!/usr/bin/env node
2
3const currentDateTime = new Date().toISOString()
4
5console.log(currentDateTime)

โœ… Build

1yarn build

Results:

1yarn run v1.22.10
2$ tsc
3โœจ Done in 1.66s.

The bin directory contains now the outcome of the compilation process:

1$ tree ./bin
2./bin
3โ”œโ”€โ”€ index.js
4โ””โ”€โ”€ index.js.map
5
60 directories, 2 files

โœ… Make ./bin/index.js executable

1chmod u+x ./bin/index.js

โœ… Test the result

1./bin/index.js

Result:

1โฏ pique-sous
22021-01-10T04:33:08.303Z

Step 4: publish the command locally

๐Ÿ”ฅ The command can now be made available for use locally:

1$ yarn link --global

Result:

1yarn link v1.22.10
2success Registered "pique-sous".
3info You can now run `yarn link "pique-sous"` in the projects where you want to use this package and it will be used instead.
4โœจ Done in 0.04s.

๐ŸŽ‰ ๐Ÿ’ช We can now use the command from everywhere

1โฏ pique-sous
22021-01-10T05:45:10.586Z

๐ŸŒˆ ๐Ÿ™ˆ We can unregister the command with:

1$ yarn unlink --global

Step 5: publish the cli command on www.npmjs.org

๐Ÿ‘‰ First, you need to signup and create an account on https://www.npmjs.com/ ๐Ÿ‘‰ ๐Ÿงจ You need to be sure that the name of your package is not taken on npmjs.com, the name of the package in the package.json must be modified it the name already exists on npm.

Type the following command in the base directory:

1$ npm publish

Enter your npm credentials

Result:

1npm notice
2npm notice ๐Ÿ“ฆ pique-sous@1.0.0
3npm notice === Tarball Contents ===
4npm notice 133B bin/index.js
5npm notice 198B bin/index.js.map
6npm notice 372B package.json
7npm notice 100B src/index.ts
8npm notice 364B tsconfig.json
9npm notice === Tarball Details ===
10npm notice name: pique-sous
11npm notice version: 1.0.0
12npm notice filename: pique-sous-1.0.0.tgz
13npm notice package size: 810 B
14npm notice unpacked size: 1.2 kB
15npm notice shasum: 6c8aea7b85c125a2d9dbbeec81d15ef94b07240a
16npm notice integrity: sha512-ozbnViT18DSUI[...]FquBcXBSV8f2g==
17npm notice total files: 5
18npm notice

Your command is now published on npm and be installed or executed from everywhere.

Example:

Execution without formal installation:

1npx pique-sous

Or global installation:

1npm install -g pique-sous

Step 6: Add Yahoo finance get stocks information

โœ… Install axios library

1yarn add axios

โœ… Add file ./src/getStock.ts

1import axios from "axios"
2
3export const getSingleStockInfo = async (stock: string) => {
4 if (!stock) {
5 throw new Error("Stock symbol argument required")
6 }
7
8 if (typeof stock !== "string") {
9 throw new Error(
10 `Invalid argument type for stock argument. Required: string. Found: ${typeof stock}`
11 )
12 }
13
14 const url = `https://query1.finance.yahoo.com/v7/finance/quote?symbols=${stock}`
15
16 const res = await axios.get(url)
17
18 const { data } = res
19 if (
20 !data ||
21 !data.quoteResponse ||
22 !data.quoteResponse.result ||
23 data.quoteResponse.result.length === 0
24 ) {
25 throw new Error(`Error retrieving info for symbol ${stock}`)
26 }
27
28 const quoteResponse = data.quoteResponse.result[0]
29
30 return quoteResponse
31}

โœ… Add file "./src/getVersion.ts"

1import * as fs from "fs"
2import * as Path from "path"
3
4export const getVersion = () => {
5 const packageJSONPath = Path.resolve(__dirname, "../package.json")
6 const content = fs.readFileSync(packageJSONPath, { encoding: "utf8" })
7 const config = JSON.parse(content)
8 return config.version
9}

โœ… Modify ./src/index.ts

1#!/usr/bin/env node
2
3import { getSingleStockInfo } from "./getStock"
4import { getVersion } from "./getVersion"
5
6/**
7 * return the arguments of the command except node and index.ts
8 */
9const getArgs = () => {
10 // We retrieve all the command argumnts except the first 2
11 const args = process.argv.slice(2)
12 return args
13}
14
15/**
16 * Command Help
17 */
18const printCommandHelp = () => {
19 const version = getVersion()
20 const help = `
21pique-sous (version: ${version})
22
23A simple command to retrieve stock information.
24
25Example:
26
27$ pique-sous MSFT SFIX GOOG
28
29`
30 console.log(help)
31}
32
33const symbols = getArgs()
34
35// Print help if no arguments
36if (symbols.length === 0) {
37 printCommandHelp()
38 getVersion()
39 process.exit(0)
40}
41
42const now = new Date().toISOString()
43
44// Call the yahoo API for each symbol and display the result on the console
45symbols.forEach((symbol) => {
46 console.log(`Retrieving stock information for ${symbol} at date ${now}`)
47 getSingleStockInfo(symbol).then(console.log)
48})

โœ… Increment the version number in package.json to 1.1.0 ("version")

1{
2 "devDependencies": {
3 "@types/axios": "^0.14.0",
4 "@types/node": "^14.14.20"
5 },
6 "name": "pique-sous",
7 "version": "1.1.0",
8 "description": "A simple command to retrieve stock information",
9 "main": "./bin/index.js",
10 "dependencies": {
11 "axios": "^0.21.1",
12 "typescript": "^4.1.3"
13 },
14 "bin": "bin/index.js",
15 "scripts": {
16 "build": "tsc",
17 "test": "echo \"Error: no test specified\" && exit 1"
18 },
19 "keywords": ["cli", "node", "typescript", "npm", "stock", "yahoo finance"],
20 "contributors": ["raphaelmansuy"],
21 "repository": {
22 "url": "https://github.com/raphaelmansuy/pique-sous.git",
23 "type": ""
24 },
25 "author": {
26 "email": "raphael.mansuy@gmail.com",
27 "name": "raphaelmansuy"
28 },
29 "license": "MIT"
30}

โœ… Build a new version

1$ yarn build

โœ… Test locally

Publish the component:

1$ yarn link --global

โœ… Execute

1$ pique-sous MSFT

Result:

1Retrieving stock information for MSFT at date 2021-01-10T06:11:41.305Z
2{
3 language: 'en-US',
4 region: 'US',
5 quoteType: 'EQUITY',
6 quoteSourceName: 'Delayed Quote',
7 triggerable: true,
8 currency: 'USD',
9 exchange: 'NMS',
10 shortName: 'Microsoft Corporation',
11 longName: 'Microsoft Corporation',
12 messageBoardId: 'finmb_21835',
13 exchangeTimezoneName: 'America/New_York',
14 exchangeTimezoneShortName: 'EST',
15 gmtOffSetMilliseconds: -18000000,
16
17 ...

๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ The package can now be republished on npm ๐Ÿ’ช.

Subscribe to our Newsletter

We deliver high quality blog posts written by professionals monthly. And we promise no spam.

elitizon ltd.

ยฉ 2020 elitizon ltd. All Rights Reserved.