background

These years, I have often used various front-end scaffolding tools to build and install projects through the command line, which is convenient and fast. One day, I suddenly thought, why can it use the command line to build projects? How do you build projects from the command line? Can I also implement a scaffold?

The introduction

Scaffolding such as vue-CLI and React-native CLI can quickly generate an initial project by typing a simple command vue init webpack project.

thinking

Under what circumstances was scaffolding born? Why is scaffolding needed?

  • Less repetitive work, no need to copy other projects and delete irrelevant code, or create a project and file from scratch.

  • Dynamically generate project structures, configuration files, etc., based on interactions.

  • It’s easier to collaborate with multiple people, without having to pass files around.

Train of thought

To develop scaffolding, you first have to think clearly, how does scaffolding work? We can learn from vue-CLI basic ideas. Vue-cli is to put the project template on Git, and then download different templates according to user interaction during the operation, which are rendered by the template engine to generate the project.

In this way, the template and scaffold can be maintained separately. Even if the template changes, only the latest template can be uploaded, and the latest project can be generated without requiring the user to update the scaffold.

Following the idea of VUe-CLI, I also independently published the project template to Git, downloaded it through scaffolding tools, obtained the information of the new project through interaction with scaffolding, rendered the project template with the input of interaction as meta information, and finally got the infrastructure of the project.

Third-party libraries

Let’s start by looking at which libraries are used:

  • Commander. Js automatically parses commands and parameters to process user input commands.

  • Download-git-repo, which downloads and extracts a Git repository for downloading project templates.

  • Inquirer. Js, a collection of generic command-line user interfaces for interacting with users.

  • Handlebars.js, a template engine, dynamically populates files with user-submitted information.

  • Ora, if the download process is long, can be used to display the animation effect in the download.

  • Chalk, which adds color to the font on the terminal.

  • Log-symbols can display symbols such as √ or × on the terminal.

Initialize the project

First create an empty project, temporarily named my-cli, enter the folder, execute NPM init to generate a package.json file, then create a new index.js file, and finally install the dependencies required above.

npm install commander download-git-repo inquirer handlebars ora chalk log-symbols -S

Processing command line

Node.js has built-in support for command-line operations, and the bin field in package.json defines the command name and associated execution file. So now add the contents of bin to package.json:

{" name ":" my - cli ", "version" : "1.0.0", "description" : "self-built scaffolding tools", "bin" : {" mys ":" index. Js "},... }Copy the code

Then define the init command in index.js:

#! /usr/bin/env node const program = require('commander'); The program version (1.0.0, '-v - version). The command (' init < name >'). The action ((name) = > {the console. The log (name); }) .parse(process.argv);Copy the code

#! /usr/bin/env node has a proper noun Shebang, which usually appears on the first line of scripts on Unix-like systems, as the first two characters. Shebang can be followed by one or more whitespace characters, followed by the absolute path of the interpreter, which indicates the interpreter executing the script file.

Calling version(‘1.0.0’, ‘-v, –version’) adds -v and -version to the command, and prints the version number with these options.

Call command(‘init ‘) to define the init command, and name is the required parameter, which is the project name.

Action () is what happens when you execute the init command, and this is where the process of building the project takes place, just printing the name for the moment.

Actually, at this point, you’re ready to execute init. Let’s test this by executing it in the sibling of my-cli:

node index.js init Helloword

You can see that the command line tool also prints HelloWorld, so it is clear that the parameter name in action((name) => {}) is the name of the project we typed when we executed init.

Now that the command is complete, it’s time to download the template to generate the project structure.

Download the template

Download-git-repo supports downloading repositories from Github, Gitlab and Bitbucket. Please refer to the official documentation of each repository.

#! /usr/bin/env node const program = require('commander'); The program version (' 1.0.0 ', '- v, --version') .command('init <name>') .action((name) => { download( 'http://xxxxxx:9999:HTML5/H5Template#master', name, { clone: true }, (err) => { console.log(err ? 'Error' : 'Success'); } ) }) .parse(process.argv);Copy the code

The first argument to download() is the repository address.

Actual warehouse address is http://xxxxxx:9999/HTML5/H5Template#master, you can see the port number at the back of the ‘/’ to write in the parameter ‘:’, # master representative is the branch name, different templates can be placed in different branches, You can change the branch to download different template files.

${name} = test/${name}

Command line interaction

After the user runs the init command, the cli interaction can ask the user a question, receive the user’s input, and process the problem accordingly. This is done using inquirer. Js.

#! /usr/bin/env node const program = require('commander'); Program.version ('1.0.0', '-v, --version').command('init <name>').action((name) => {inquirer. Prompt ([{name: 'description', message: 'Please enter project description'}]). Then ((answers) => {console.log(answers.description); }) }) .parse(process.argv);Copy the code

As you can see from this example, the question is in prompt(), name is the key in the answer object, message is the question, and the user’s input is in answers. You can refer to the official documentation for more parameter Settings.

The user’s input is obtained through command line interaction so that answers can be rendered into templates.

Apply colours to a drawing template

Here are some changes to the package.json file of the project template repository that you want to download using handlebars syntax:

{" name ":" {{name}} ", "version" : "1.0.0", "description" : "{{description}}", "scripts" : {" test ", "echo \" Error: no test specified\" && exit 1" }, "author": "{{author}}", "license": "ISC", ... }Copy the code

After downloading the template, render the user’s input into package.json

#! /usr/bin/env node const program = require('commander'); Program.version ('1.0.0', '-v, --version').command('init <name>').action((name) => {inquirer. Prompt ([{name: 'description', message: 'Please enter project description'}, {name: 'author', message: 'please enter the author name}]). Then ((answers) = > {download (' http://xxxxxx:9999:HTML5/H5Template#master', the name, {clone: true }, (err) => { const meta = { name, description: answers.description, author: answers.author } const fileName = `${name}/package.json`; const content = fs.readFileSync(fileName).toString(); const result = handlebars.compile(content)(meta); fs.writeFileSync(fileName, result); } ) }) }) .parse(process.argv);Copy the code

Here we use the Node.js file module FS to write the handlebars rendered template back into the file.

Visual beautification

After the user enters an answer, the template is downloaded, and ora is used to indicate that the user is downloading.

const ora = require('ora'); // start downloading const spinner = ora(' downloading template... '); spinner.start(); // Call spinner.fail(); // Call spinner.fail(); Spinner. Success ();Copy the code

Then, the chalk will add styles for the printed information, such as green for the success information and red for the failure information, which will make it easier for users to distinguish and make the display of the terminal more beautiful.

const chalk = require('chalk'); Console. log(chalk. Green (' project initialization completed ')); Console. log(chalk. Red (' project initialization failed '));Copy the code

In addition to coloring printed messages, log-symbols can be used to prefix messages with symbols such as √ or ×

const chalk = require('chalk'); const symbols = require('symbols'); Console. log(symbols.success, chalk. Green (' project created complete ')); Console. log(symbols.error, chalk. Red (' project creation failed '));Copy the code

Complete sample

#! /usr/bin/env nodeconst fs = require('fs'); const program = require('commander'); const download = require('download-git-repo'); const handlebars = require('handlebars'); const inquirer = require('inquirer'); const ora = require('ora'); const chalk = require('chalk'); const symbols = require('symbols'); The program version (1.0.0, '-v - version). The command (' init < name >'). The action (= > {if ((name)! Fs.existssync (name)) {inquirer. Prompt ([{name: 'description', message: 'Please enter project description'}, {name: 'author', message: 'Please enter author name'}, {type: 'confirm', name: 'toBeDelivered', message: 'Is this for delivery (NO)?', default: false}, {type: 'list', name: 'size', message: 'What size do you need?', choices: [ 'Jumbo', 'Large', { name: 'Small', value: 3}, {name: 'scrub ', disabled: 'why disabled'} // Optional]}, {type: 'checkbox', message: 'Select toppings', name: 'toppings', choices: [ { name: 'Ham', checked: true }, { name: 'Mozzarella'},],}]). Then ((answers) = > {const spinner = ora (' is to download the template... '); spinner.start(); download( 'http://xxxxxx:9999:HTML5/H5Template#master', name, { clone: true }, (err) => { if (err) { spinner.fail(); console.log(symbols.error, chalk.red(err)); } else { spinner.succeed(); const meta = { name, description: answers.description, author: answers.author } const fileName = `${name}/package.json`; if (fs.existsSync(fileName)) { const content = fs.readFileSync(fileName).toString(); const result = handlebars.compile(content)(meta); fs.writeFileSync(fileName, result); } console.log(symbols.success, chalk. Green (' initializing project completed ')); }})})} else {console.log(symbol. error, chalk. Red (' symbols ')); } }) .parse(process.argv);Copy the code

The effect is as follows: