preface
If you want to learn about command line scaffolding, this article will take you inside the commander and Inquirer libraries. A Demo example is then used to demonstrate how to create a scaffold.
commander
Commander is the NPM library that encapsulates the ability to parse the command line. The scaffolding we’re going to do is based on Commander.
Let’s start with a few simple ways to use it
Displays help & version information
const program = require('commander');
// Define the program name, version
program.name(cliName).version(pkg.version);
// Parse the command line
program.parse(process.argv);
Copy the code
Run node./bin/fl-cli –help to see the following:
D:\project\frontend-learn-cli>node ./bin/fl-cli --help
Usage: fl [options]
Options:
-V, --version output the version number
-h, --help display help for command
Copy the code
The program.parse method must be called otherwise the command line tool will not “work properly.”
In the output, we can see the Usage entry. By default, it shows us how to use the command, but we can also change it:
program.usage('<command> [options]');
Copy the code
Usage: fl <command> [options]
Copy the code
How to contract the Command?
Name a create command, convention used to create an application (similar to vue create)
program
.command('create')
.description('Create application')
.action(() = > {
console.log('hello command');
});
Copy the code
With the help command, you can see the new command information added to the Commands entry:
D:\project\frontend-learn-cli>node ./bin/fl-cli --help Usage: fl <command> [options] Options: -V, --version output the version number -h, --help display help for command Commands: Help [command] display help for commandCopy the code
Execute the create command:
D:\project\frontend-learn-cli>node ./bin/fl-cli create
hello command
Copy the code
How do I define the command parameters Options?
We need to add additional parameters to make the command handle multiple situations. For example, we can set -f to force the created application to overwrite the current path:
program
.command('create')
.description('Create application')
.option('-f,'.'Force create or not')
.action((options, command) = > {
console.log(options);
});
Copy the code
D:\project\frontend-learn-cli>node ./bin/fl-cli create -f
{ f: true }
Copy the code
We can also customize parameter values:
.option('-f,--force <path>'.'Force create or not')
Copy the code
D:\project\frontend-learn-cli>node ./bin/fl-cli create --force /use/local
{ force: '/use/local' }
Copy the code
Note that an error occurs if the user enters an unexpected parameter:
D:\project\frontend-learn-cli>node ./bin/fl-cli create -abc
error: unknown option '-abc'
Copy the code
We can add the allowUnknownOption() method to prevent unexpected arguments from affecting command execution:
program
.command('create')
.description('Create application')
.option('-f,--force <path>'.'Force create or not')
.allowUnknownOption()
.action((options, command) = > {
console.log(options);
});
Copy the code
D:\project\frontend-learn-cli>node ./bin/fl-cli create -abc
{}
Copy the code
Advanced skills
As above, we can develop simple command-line tools. But it’s still a bit far from being a user-friendly command-line tool, so here are a few tips:
Color of the console output statement
There is a repository called Chalk that allows us to customize the colors of console.log output. This is a great visualization for the command line.
We wrap the –help command by adding a blue help statement at the end of the help message:
const chalk = require('chalk');
exports.outputHelp = (cliName) = > {
console.log();
console.log(` Run ${chalk.cyan(`${cliName} <command> --help`)} for detailed usage of given command.`);
console.log();
};
Copy the code
program.on('--help'.() = > {
outputHelp(cliName);
});
Copy the code
Also, iterate over each Command and add this functionality to it as well:
program.commands.forEach((c) = > c.on('--help'.() = > outputHelp(cliName)));
Copy the code
Command/Parameter error encapsulation
Command line errors may occur due to command errors, parameter errors or missing values. For such unexpected errors, it is necessary to prompt the user in time, instead of only displaying information such as: error: option ‘-f,–force ‘argument missing.
Referring to Vue’s error handling, we can override the method of Commander and combine chalk to enhance the output of error messages:
var enhanceErrorMessages = (methodName, log) = > {
program.Command.prototype[methodName] = function (. args) {
if (methodName === 'unknownOption' && this._allowUnknownOption) {
return;
}
this.outputHelp();
console.log(` `+ chalk.red(log(... args)));console.log();
process.exit(1);
};
};
// There are such methods as missingArgument, unknownOption, optionMissingArgument
enhanceErrorMessages('missingArgument'.(argName) = > {
return `Missing required argument ${chalk.yellow(` <${argName}> `)}. `;
});
Copy the code
Interactive command line Inquirer
With Command, Option can define all the functions required on the command line, but can be quite unfriendly to the user (facing a lot of command instructions).
Vue-cli knows that when creating a project, you can follow the command line prompts to select the desired functionality.
This interactive command line can be done by inquirer.
Here’s how it looks with this approach:
Define a batch of questions:
const defaultQuestions = [
// List selection
{
type: 'list'.name: 'template'.message: 'Please select template'.choices: [{ name: 'express' }, { name: 'vue2'}],},/ / input
{
type: 'input'.name: 'appName'.message: 'Please enter application name'.default() {
return 'app'; }},];const expressQuestions = [
{
type: 'checkbox'.name: 'express.middleware'.message: 'Please select middleware'.choices: [{name: 'express.json'.checked: true },
{ name: 'express.urlencoded'.checked: false},].when(answers) {
return answers.template == 'express'; }},];const questions = [...defaultQuestions, ...expressQuestions];
Copy the code
Then call the inquirer. Prompt method in the action callback of the command:
program
.command('create')
.description('Create application')
.option('-f,--force <path>'.'Force create or not')
// .allowUnknownOption()
.action((options, command) = > {
inquirer.prompt(questions).then(async (answers) => {
console.log(answers);
});
});
Copy the code
When the interaction ends, the answers will be our custom value:
{
template: 'express'.appName: 'app'.express: { middlewares: [ 'json']}} {template: 'express'.appName: 'app'.express: { middlewares: [ 'json']}}Copy the code
Demo
Now that you’ve learned more about command-line tool operations above, you can actually “build” a scaffold tool.
A scaffold to create the Express code template is provided below. Used to quickly generate directories of relevant code without having to go to the technology stack site to copy a lot of code. (For specific code logic, search frontend-learn-cli in NPM)
Simple implementation idea:
throughcommander, to achieve the creation ofcreateCommand line parsing of instructions
program
.command('create')
.description('Create application')
.action((options, command) = > {
//inquirer...
});
Copy the code
addinquirerDefine relevant optional questions
inquirer.prompt(questions).then(async (answers) => {
// Template logic...
// Template type (Express, vue...)
// Project name
// Middleware selection
// ...
});
Copy the code
Define template fixation code
Define the code structure based on your express habits:
template
|- express
|- bin
| www.ejs
|- routes
| health.js.ejs
| app.js.ejs
| package.json.ejs
Copy the code
Note: the files above are suffixed with EJS. The reason: Template variables inject custom values dynamically based on the ANSWERS variable.
Ejs file templates will look like this:
# app.js.ejs
<% if (express.middlewares.includes('json')) { -%>
app.use(express.json())
<% } -%>
<% if (express.middlewares.includes('urlencoded')) { -%>
app.use(express.urlencoded())
<% } -%>
Copy the code
Why ejS?
Express-generate is express-generate, but you can also choose your own template engine. This template approach makes it very easy for me to do this kind of scaffolding, just copy the historical project demo in and change the suffix.
Read the template to the specified path through fs
Write the files in the template to the current path of the command execution via fs-extra (the details of the file operation are not detailed here)
inquirer.prompt(questions).then(async (answers) => {
try {
// Create directory
_mkdirProjectDir(answers.appName, options.f);
_writeFiles([
{ dirtory: 'bin'.fileName: 'www', answers },
{ dirtory: 'routes'.fileName: 'health.js', answers },
{ dirtory: 'util'.fileName: 'index.js', answers },
{ fileName: 'app.js', answers },
{ fileName: 'package.json', answers },
]);
} catch (err) {
console.log(chalk.red(err.message)); }});Copy the code
Release scaffold
In the package.json file, specify the bin property and point to the file where we implemented Commander:
"bin": {
"fel": "bin/fel-cli.js"
},
Copy the code
Then publish our scaffolding package via NPM publish, and the user can download it via NPM I xxx-g.