Introduction
In this article I will show you how I created my own command lite interface with NodeJs. In my case I’m doing a CLI for Stencil with the following features to begin with,
- Create a new Stencil app from an app starter template.
- Create a new Stencil component or library with the standalone component template.
- Generate a new Stencil component in an existing project.
- Shortcuts for npm commands.
First I created a new GitHub repository for the CLI (Command Line Interface).
Available over here: https://github.com/nerdic-coder/stencil-cli
Setup the NodeJS environment
Now when you have a git repository, git clone it down to your computer,
git clone https://github.com/nerdic-coder/stencil-cli.git stencil-cli
Since our project is basically empty at the moment let’s create a npm package project with the command ‘npm init‘. It will ask you a few questions about your project.
We will only need one dependency in our project and it’s called ‘shelljs‘ it’s a great npm package that makes it very easy to run shell commands in our NodeJS application.
Install it with command,
npm install --save shelljs
Now we will create the main JavaScript file that will do all the magic for us later on. Create a folder in your project named ‘bin’ and in the folder create the main js file, in my case I named it ‘stencil-cli.js’.
Now we need to update the package.json file with the information where NodeJS can find the main script file. Add the following to the package.json,
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"main": "bin/stencil-cli.js", | |
"bin": { | |
"stencil": "bin/stencil-cli.js" | |
} |
Replace the path to wherever you created your main script file.
Building the commands
The next step is to write the code for the commands that we want the CLI to be able to do.
First the script needs to initialize some dependencies and check that all required parameters is given.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/env node | |
var shell = require("shelljs"); | |
const path = require('path'); | |
// The path to the installation directory if this tool | |
var cliPath = path.join(path.dirname(__filename), '..'); | |
// Check that a command is given | |
if (!process.argv[2]) { | |
shell.echo('Please tell me what you want me todo!'); | |
shell.exit(1); | |
} |
The first line is to define that this is a node JavaScript file. The second line loads the shelljs library into the ‘shell’ variable. Then we also need to load the path library into a ‘path’ variable, it’s used to locate files on the filesystem among other files related tasks.
Then we define a ‘cliPath’ variable that will be used later to find the component template files. So the path.join takes the directory of the current script file and one folder above that to get the projects root directory.
The last check we need to do before we can start building any commands is a check if any command was given, otherwise the CLI have nothing to do and will quit itself. This is if you run the command ‘stencil’ without any arguments it will print out “Please tell me what you want me todo!”, an improvement here could be printing some kind of help page instead.
Create new app command
The first command we want is one that creates a new Stencil app project, the command for that will be ‘stencil start-app my-app’ where my-app is the name of your app.
It will look something like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Command for starting a new stencil starter app, example 'stencil start-app my-app' | |
if (process.argv[2] === 'start-app') { | |
if (!shell.which('git')) { | |
shell.echo('Sorry, this script requires git'); | |
shell.exit(1); | |
} | |
var projectName = process.argv[3]; | |
if (!projectName) { | |
shell.echo('Please state the project name after the "start-app" command.'); | |
shell.exit(1); | |
} | |
shell.exec('git clone https://github.com/ionic-team/stencil-app-starter ' + projectName); | |
shell.cd(projectName); | |
shell.echo('Running: git remote rm origin'); | |
shell.exec('git remote rm origin'); | |
shell.echo('Updating npm package names to ' + projectName + '.'); | |
shell.ls('package*.json').forEach(function (file) { | |
shell.sed('-i', '@stencil/starter', projectName, file); | |
}); | |
shell.echo('Running: npm install'); | |
shell.exec('npm install'); | |
shell.exit(0); | |
} |
The first thing we check is that the second argument of the command is ‘start-app’, then we know (assumes?) that the user want to create a new Stencil app.
Then we check the first requirement that the user needs to have git installed or else we will not be able to download the app starter project from GitHub.
Then we set the ‘projectName’ variable to the third argument of the command, if that argument is missing the script will quit with an error.
Now we have everything we need to start creating the project, the ‘shell.exec’ command runs a command in the same way you do in a normal terminal. So the first command we run is ‘git clone https://github.com/ionic-team/stencil-app-starter ‘ + projectName’, that clones down the stencil starter app to a folder with your project name.
The next command ‘shell.cd’ we use to change directory to the newly created project folder. Then we remove the connection from the origin GitHub project, since we probably don’ want to update the template.
The ‘shell.echo’ by the way is just a way to print information to the end users shell window and ‘shell.exit’ is how we end the application, if we end it with 0 we tell the user and whatever is listening (Jenkins perhaps) that the process ended successfully, if we end it with a 1 it means that the command ended badly. When we end it with a failure it’s always a good idea to be as clear as possible to the end user about what went wrong and it’s better to share to many details than to share to few details.
So the next thing we do is finding all the ‘package.json’ files with ‘shell.ls’, hopefully just one in this case. For each file we find we want to change the project name from ‘@stencil/starter’ to your given ‘projectName’.
At last we do an ‘npm install’ and we are ready to start working on that new app and that we won’t do in this article.
Create new standalone component command
The second command we want is one that creates a new Stencil component/library project, the command for that will be ‘stencil start-component my-component’ where my-component is the name of your component or library.
It will look something like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Command for starting a new stencil component project, example 'stencil start-component my-component' | |
if (process.argv[2] === 'start-component') { | |
if (!shell.which('git')) { | |
shell.echo('Sorry, this script requires git'); | |
shell.exit(1); | |
} | |
var projectName = process.argv[3]; | |
if (!projectName) { | |
shell.echo('Please state the project name after the "start-component" command.'); | |
shell.exit(1); | |
} | |
shell.exec('git clone https://github.com/ionic-team/stencil-component-starter ' + projectName); | |
shell.cd(projectName); | |
shell.echo('Running: git remote rm origin'); | |
shell.exec('git remote rm origin'); | |
shell.echo('Updating npm package names to ' + projectName + '.'); | |
shell.ls('package*.json').forEach(function (file) { | |
shell.sed('-i', 'my-component', projectName, file); | |
}); | |
shell.echo('Updating namespace in stencil.config.js to ' + projectName + '.'); | |
shell.ls('stencil.config.js').forEach(function (file) { | |
shell.sed('-i', 'mycomponent', projectName, file); | |
}); | |
shell.echo('Updating script tag in index.html to ' + projectName + '.'); | |
shell.ls('src/index.html').forEach(function (file) { | |
shell.sed('-i', 'mycomponent', projectName, file); | |
}); | |
shell.echo('Running: npm install'); | |
shell.exec('npm install'); | |
shell.exit(0); | |
} |
It’s pretty much the same logic here, just that we download another project template this time and have a few more files to replace our project name in.
Generate Stencil component
The third command we want is one that creates a new Stencil component inside an existing project, the command for that will be ‘stencil generate my-component’ where my-component is the name of your component.
It will look something like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Command for generating a new stencil component within any stencil project, example 'stencil generate my-component' | |
if (process.argv[2] === 'generate') { | |
if (!process.argv[3]) { | |
shell.echo('Please state the component name after the "generate" command.'); | |
shell.exit(1); | |
} | |
// The uppercase version of the component alias | |
var componentName = capitalizeFirstLetter(process.argv[3]); | |
// The tag name of the component alias | |
var componentTag = process.argv[3].toLowerCase(); | |
var templatePath = path.join(cliPath, 'templates', 'component'); | |
// Create the right formats for the given component name | |
if (componentName.includes('-')) { | |
var componentParts = componentName.split("-"); | |
componentName = ''; | |
for(var part in componentParts) { | |
componentName += capitalizeFirstLetter(componentParts[part]); | |
} | |
} else { | |
var componentParts = componentName.split(/(?=[A-Z])/); | |
componentTag = ''; | |
var first = true; | |
for(var part in componentParts) { | |
if (first) { | |
componentTag += componentParts[part].toLowerCase(); | |
} else { | |
componentTag += '-' + componentParts[part].toLowerCase(); | |
} | |
first = false; | |
} | |
} | |
// Create component folder | |
shell.mkdir('src/components/' + componentTag); | |
// Copy template files to the new component folder | |
shell.cp(path.join(templatePath, 'component.css'), 'src/components/' + componentTag + '/' + componentTag + '.css'); | |
shell.cp(path.join(templatePath, 'component.spec.ts'), 'src/components/' + componentTag + '/' + componentTag + '.spec.ts'); | |
shell.cp(path.join(templatePath, 'component.tsx'), 'src/components/' + componentTag + '/' + componentTag + '.tsx'); | |
// Replace the placeholders with the component name and tag name | |
shell.ls('src/components/' + componentTag + '/' + componentTag + '.*').forEach(function (file) { | |
shell.sed('-i', 'COMPONENT_NAME', componentName, file); | |
shell.sed('-i', 'COMPONENT_TAG', componentTag, file); | |
}); | |
shell.echo('Generated stencil component "' + componentName + '".'); | |
shell.exit(0); | |
} |
Here we have a little more complex logic to figure out. Since a component have two different formats, we always have to convert the given component name to the formats, MyComponent and my-component. So we add the given name to two variables, ‘componentName’ setting the first character to uppercase, ‘componentTag’, making it all lowercase. Then we check if the given component have any dash in it, if it do we split it up by the dash sign and remove the dash and make every parts first character uppercase and then put it together again and with that we get the proper ‘componentName’. If there is no dash we instead assumes the user entered the component name in the format ‘MyComponent’, then we instead split the string at every uppercase character with the regexp ‘/(?=[A-Z])/’ and make them lowercase and add a dash infront of each part, so in my example we will get ‘my-component’.
Once we have figured out that detail we move on to creating the directory for the component in the projects ‘src/components/’ folder with the command ‘shell.mkdir’. Then we copy the template files with command ‘shell.cp’ to the correct locations. Then we update all the component files with the right class name and tag name and that’s it you can start modifying the empty component anyway you like (as long as it compiles)!
Command shortcuts
The last part I did was making shortcuts to some standard npm commands.
Command ‘stencil start’ runs a regular ‘npm start’ that starts the Stencil development server.
I planned to have a ‘stencil test’ command that would be running the Jest unit tests, but Jest did not like being run in a NodeJS sub-process apparently. So instead I added a warning there. If you have any suggestions on how to make it work, just let me know or make a pull request with the solution. 🙂
Then the last part is just a wildcard were you can run ‘stencil anything’ and it will run ‘npm run anything’.
Conclusion
That ends this article about creating command line interfaces using NodeJs, I hope you found some value in it. You can use my stencil-cli from npmjs.org right now over here: https://www.npmjs.com/package/@nerdic-coder/stencil-cli
You can take a look at the projects source over at GitHub on this URL: https://github.com/nerdic-coder/stencil-cli
If you now want to publish your new CLI to the nmpjs repository you can read my article about that here: How I created and published my first Stencil component
Leave a Reply