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.