How to increase your coding performance by creating a JavaScript CLI
In our team we have quite a few repetitive tasks when creating new features for our Content Management System. For example when we create new modules for our GraphQL Service, each one follows the same basic structure with a schema, some resolvers and tests. Further there is a config file to let the system know there is a new module. So we always duplicate an older module, delete almost all the content and edit the names to fit our new module.
So what we really wanted was to automate all this boring stuff but couldn’t find any good tutorials on how to create and modify old files with a JavaScript CLI.
What we are going to do?
- Reading user input in the command line
- Reading and parsing a JavaScript file using babel
- Creating a test file using the exports of this file
Setting up a new project
Start with creating a project by typing the following commands:
mkdir cli-tutorial && cd $_
yarn init -y
Now we have a fresh project and can start off with adding a couple of dependencies we are going to use.
- inquirer: for user-interaction in the terminal
- babel: parsing JavaScript-files to get the AST (Abstract Syntax Tree)
yarn add @babel/parser inquirer
Open up the package.json, edit anything you like to fit your needs and also add the the start-script.
{
...
"scripts": {
"start": "node index.js"
},
...
}
Furthermore let’s create a simple JavaScript file with some exported variables/functions/classes and let’s call it dummy.js:
const sum1 = 7
const firstname = 'John'export const add = sum2 => sum1 + sum2
export const fullName = firstname + ' Doe'export class Vector {
constructor(x, y) {
this.x = x
this.y = y
}length() {
return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2))
}
}
Reading filename from user input
Now we want the user to choose a file from the current working directory. To do that three steps are necessary:
- Fetch all files that end with .js but not .test.js from the current directory.
- Show these files in a list in the terminal using inquirer.
- Wait for the user to select a file.
So we create an index.js and paste the following lines:
const fs = require('fs')
const inquirer = require('inquirer')const run = async () => {
const directoryFiles = fs
.readdirSync(process.cwd())
.filter(file => file.endsWith('.js') &&
!file.endsWith('.test.js')) const { filename } = await inquirer.prompt({
type: 'list',
name: 'filename',
choices: directoryFiles,
})
}run()
Reading the content of the file
In order to get the file’s content we need to read it using the fs module which is pretty basic. Then we can use babel’s parser module to get the AST of the the code.
...
const { parse } = require('@babel/parser')const run = async () => {
...
const content = fs.readFileSync(filename, 'utf-8')
const ast = parse(content, {
sourceType: 'module',
})
}
Setting the sourceType to module lets the parser know that it reads an ES6 file that uses import and export statements.
You can also visit http://www.astexplorer.com and paste our dummy.js to take a peek at what the AST looks like. There are some properties of type VariableDeclaration and some properties of type ExportNamedDeclaration.

In our use case we just need all the names of those ExportNamedDeclarations, so we add this function to index.js:
function getExports(body) {
const namedExports = body
.filter(item => item.type === 'ExportNamedDeclaration')
.map(item => item.declaration)
.reduce((total, current) => {
if (current.id) {
return [...total, current.id.name]
} else if (current.declarations) {
return [
...total,
...current.declarations.map(declaration => declaration.id.name),
]
}
}, [])
return namedExports
}
The reduce function takes care of the different structure of the first export compared to the other two.
Creating a test file
To generate new files let’s create create-test-file.js which will contain our test-template:
module.exports = {
createTestFile: ({ filename, imports }) => {
return `/* eslint-env jest */
import { ${imports.join(', ')} } from '${filename}'describe('${filename}', () => {
${imports
.map(i => {
return `test('${i}', () => {
expect(true).toBeTruthy()
})`
})
.join('\r\n \r\n \t')}
})
`
},
}
Using a template-literal we can simply fill in our variables. At first we are going to import all exported properties from the original file. Then we create a test function for each of these properties.
Back to the index.js we now are able to use this template:
const { createTestFile } = require('./create-test-file')const run = async () => {
...
const exports = getExports(ast.program.body)
const testContent = createTestFile(
{ imports: exports, filename }
)
}
The last thing we need to do is to generate a filename for the test which in our case will be dummy.test.js. After that we can simply put the content into the file.
const run = async () => {
...
const testFilename = createTestFilename(filename)
fs.writeFileSync(testFilename, testContent)
}function createTestFilename(originalName) {
const parts = originalName.split('.')
return parts.slice(0, -1).join('.')
+ '.test.' + parts[parts.length - 1]
}
Now let’s run our CLI by typing yarn start into the terminal.
Make the CLI globally available
To be able to run our script from anywhere in the terminal we need to add the following to the package.json:
{
...
"bin": {
"cli-tutorial": "./index.js"
},
}
We also have to add the following line to the very top of index.js:
#!/usr/bin/env node
Now let’s globally install the CLI by running npm i -g