Automate a repetitive task with your own node js CLI — Part 2

Anik Dutta
3 min readAug 19, 2019

--

Let’s recap what we have done so far.

  • Created a NodeJS command-line script named my-project
  • Added myCLI.js which contains two functions parseArgumentsIntoOptions (for argument parsing) and promptForMissingOptions(prompting for the missing information)

Logic that will create our projects

Create a templates directory in the root of our project and place four directories with the names typescript, javascript, React and Angularinto it. Inside those place a package.json and a /src directory. Feel free to put anything you wish to. For the sake of simplicity, I’m keeping only this two. When we run my-project in CLI our code will later simply copy those files into the new project in your target directory or current directory.

Again we’ll be using few third-party packages for file/directory copying, executing commands, trigger npm/yarn install and view the progress overview of the task.

ncp - Asynchronous recursive file & directory copying
execa - run external commands
listr - specify a list of tasks and gives the user a neat progress overview
pkg-install - trigger yarn install or npm install

This is how our my-project structure looks like so far:

Add a main.js inside src to place the main logic for copying files, initializing git and finally running task.

import {promisify} from 'util'
import ncp from 'ncp'
const copy = promisify(ncp)
async function copyTemplateFiles(options) {
return copy(options.templateDir, options.targetDir, {
clobber: false
})

copyTemplateFiles will copy template you wished to bootstrap from my-project to you current directory or any other directory you have passed from the CLI.

import execa from 'execa'
async function initGit(options) {
const res = await execa('git', ['init'], {
cwd: options.targetDir
})

if (res.failed){
return Promise.reject('Git init failed')
}
}

execa package allows us to easily run external commands like git. initGit is using execa to run npm init.

import fs from 'fs'
import path from 'path'
import {promisify} from 'util'
import { URL} from 'url'
import { projectInstall} from 'pkg-install'
const access = promisify(fs.access)
export default async function createProject(options) {
options = {
...options,
targetDir: options.targetDir || process.cwd()
}
const curFileUrl = import.meta.url
const templateDir = path.resolve(
new URL(curFileUrl).pathname,
'../../template',
options.template.toLowerCase()
)
options.templateDir = templateDir

try{
await access(templateDir, fs.constants.R_OK)
}catch (e) {
console.error('Invalid template')
process.exit(1)
}

const task = new Listr([
{
title: 'Copy project files',
task: () => copyTemplateFiles(options)
},
{
title: 'Init git',
task: () => initGit(options),
enable: () => options.git
},
{
title: 'Install node modules',
task: () => projectInstall({
cwd: options.targetDir
}),
skip: () => !options.runInstall ? 'Pass --install to automatically install packages': undefined
}
])
await task.run()
}
export async function myCli(args) {
const options = parseArgumentsIntoOptions(args)
const prompt = await promptForMissingOptions(options)
await createProject(prompt)
}

Setting the target directory from the option you have passed or current directory. Then resolving the path for the template directory and setting it in the option. Check if you have access to the template directory, if not throw an error and emit an exit event to terminate the event loop.

Finally, instantiate the Listr for progress list. Start with copying the files, initializing the git and installing the node modules. enable inside init git allows you to handle if the user wants to do so based on the options passed from the CLI. Similarly, skip in install node module task will skip the process if runInstall from CLI is passed as false. Now run it my-project — install.

You can do lots more to this set up now as per your need. Share and comment if you like and how it could be improved.

--

--

Anik Dutta

It’s hard enough to find an error in your code when you’re looking for it; its even harder when you’ve ASSUMED your code is ERROR-FREE. — Steve McConnell