Background
I crated a CLI tool which executes an npm
command (to create a react app) and modifies the contents of the generated files. It was a practice attempt at creating CLI tools for NodeJS and a chance for me to experience publishing NPM packages.
What I set out to do
My plan was to create something which follows the below workflow (in order):
- user invokes the cli command
- user inputs the name of their project
- program calls
create-react-app
to create an app based on project name - program deletes the existing
.gitignore
file when project has been created - program downloads the latest
.gitignore
file for nodejs
I got used to prototyping a lot with create-react-app
and got tired of modifying the .gitignore
file every time I wanted to add .env
or code coverage files. So I created this tool to do this for me. In the future I hope to also modify the README.md
file and allow the users to open the project in vscode when everything has been generated and all files have been modified etc.
The code
The tool, in it's entirety is here:
#!/usr/bin/env node
const chalk = require("chalk");
const figlet = require("figlet");
const exec = require("child_process").exec;
const Ora = require("ora");
const fs = require("fs");
const https = require("https");
// create an input interface
const readline = require("readline").createInterface({
input: process.stdin,
output: process.stdout
});
function execShellCommand(cmd) {
return new Promise((resolve, reject) => {
exec(cmd, (error, stdout, stderr) => {
if (error) {
console.warn(error);
console.warn(stdout);
console.warn(stderr);
}
resolve(stdout ? stdout : stderr);
});
});
}
// 1. clear the console
process.stdout.write("\x1Bc");
// 2. print some fancy text
console.log(
chalk.green(figlet.textSync("My CLI Tool", { horizontalLayout: "full" }))
);
// 3. ask user for a project name
readline.question(`What is your project name? `, name => {
console.log(`\n`); // add a space
let firstSpinner = new Ora(
`Creating a new React project named: ${name} ... this may take a while\n`
);
firstSpinner.start();
// 4. Create a create-react-app project
execShellCommand(`npx create-react-app ${name}`).then(() => {
firstSpinner.succeed();
console.log(`Project ${name} created!\n\n`);
// 5. Remove the existing .gitignore file from that project folder
let secondSpinner = new Ora(
`Removing the original ${name}/.gitignore file...\n`
);
secondSpinner.start();
fs.unlink(`./${name}/.gitignore`, err => {
if (err) {
console.error(err);
return;
}
//file removed
secondSpinner.succeed();
console.log(`Original ${name}/.gitignore file removed!\n\n`);
// 6. Place new .gitignore file in that project directory
let thirdSpinner = new Ora(`Placing new .gitignore file...\n`);
thirdSpinner.start();
let newGitignore = fs.createWriteStream(`./${name}/.gitignore`); // cannot declare anywhere else as the folder has not been created before this point
https
.get(
`https://raw.githubusercontent.com/github/gitignore/master/Node.gitignore`,
res => {
res.pipe(newGitignore);
newGitignore.on("finish", () => {
newGitignore.close();
thirdSpinner.succeed();
console.log(
`New ${name}/.gitignore created!\n\n\nClosing CLI tool\n`
);
readline.close();
process.exit();
});
}
)
.on("error", err => {
fs.unlink(newGitignore);
console.error(err);
process.exit();
});
});
});
readline.close();
});
Reflection
After I created the tool, I realised there are several ways in which I can enhance the user's experience - e.g. adding yargs
to implement a help
function or parsing of arguments into my CLI tool.
Questions
I was wondering how I could improve my code to be more efficient. Perhaps I could learn some best practices before I publish this as an npm
package. In particular, I'd like to know if there is any other way for me to create, start and stop my spinners which doesn't involve creating 3 separate ones for various 'checkpoint' sections of my code. And whether my use of exec()
is efficient/ best suited for this kind of CLI app.