Node-cli A tool that uses nodeJS to interact with the shell to do a specified job. They usually look like this:

sass xx.scss:xx.css
webpack ....
Copy the code

Wait, we implemented this tool to pull the CavinHuang/webpack-multi-skeleton webpack multi-page skeleton scaffolding tool for local rapid build projects, imagine doing this with the following command:

Webpack-template init # With some options, initialize the entire projectCopy the code

That’s about the whole idea, so let’s start with the simplest and implement it step by step.

Implement your first node command

We initialize a project directly with NPM init

npm init node-cli-demo
Copy the code

Go to the project and add the following code to package.json

"bin": {
  "hi": "./bin/hi.js"
},
Copy the code

Create the bin directory and hi.js and write the following code in hi.js

#! /usr/bin/env node console.log('Hi Welcome node cli')Copy the code

Use the command line to enter the current project directory and type

hi
Copy the code

If the command is not prompted, enter

npm link
Copy the code

Refresh command.

A simple Node-CLI is done. Let’s explain:

  • ! What does /usr/bin/env node do here?

When the system calls a command, it will look for the PATH registered in the variable. If there is a PATH registered in the variable, it will call. Otherwise, the command is not found. Env can be used to retrieve all of the environment variables in the native system, so this is mainly to help the script find node’s script interpreter.

Process command line arguments

Node Process object A global object that provides information and control about the current Node.js process and can be called in the Node environment without requiring require().

The process.argv property returns an array containing the command-line arguments passed when the Node.js process is started. The first element is process.execPath. If you need to access the raw value of argv[0], use process.argv0, the second element will be the path to the JavaScript file to execute, and the rest will be any other command line arguments.

#! /usr/bin/env node console.log('call %s', process.argv[2]);Copy the code

Then type test Hello to print call Hello.

For command line parameter processing, we use the off-the-shelf module Commander to handle, commander provides the user command line input and parameter parsing power. Here we use the lightweight and expressive Commander.

Official website: The official website of Commander

Look at an example from the official website

#! /usr/bin/env node var program = require('commander'); Program.version ('0.1.0').option('-c, --chdir <path>', 'change the working directory'). Option ('-c, --config <path>', 'set config path. defaults to ./deploy.conf') .option('-T, --no-tests', 'ignore test hook'); program .command('setup [env]') .description('run setup commands for all envs') .option("-s, --setup_mode [mode]", "Which setup mode to use") .action(function(env, options){ var mode = options.setup_mode || "normal"; env = env || 'all'; console.log('setup for %s env(s) with %s mode', env, mode); }); program .command('exec <cmd>') .alias('ex') .description('execute the given remote cmd') .option("-e, --exec_mode <mode>", "Which exec mode to use") .action(function(cmd, options){ console.log('exec "%s" using %s mode', cmd, options.exec_mode); }).on('--help', function() { console.log(' Examples:'); console.log(); console.log(' $ deploy exec sequential'); console.log(' $ deploy exec async'); console.log(); }); program .command('*') .action(function(env){ console.log('deploying "%s"', env); }); program.parse(process.argv);Copy the code

First install Commander

yarn add commander # OR npm install commander
Copy the code

Take a look at the effect:

hi -V

hi setup

hi exec
Copy the code

commander.js API

  • Option() — > Initializes the custom parameter object, setting keyword and Description
  • Command() — > initializes the Command line argument object, takes Command line input directly, and returns an array or string
  • Command#command() — > defines the command name
  • Command#arguments() — > defines the arguments to the initial command
  • Command#parseExpectedArgs() — > Parse the expected parameters
  • Command#action() — > Registers the command’s callback function
  • Command# option () – > define the parameters, you need to set up “key words” and “description”, key words including “short” and “write” two parts, with “, “, “|”, “blank space” as separators
  • Command#allowUnknownOption() — > allows unknown arguments on the command line
  • Command#parse() — > parses process.argv, sets the options and calls the command when defining
  • Command#parseOptions() — > parse the parameters
  • Command#opts() — > Sets the parameters
  • Command#description() — > Adds a command description
  • Command#alias() — > Sets the command alias
  • Command#usage() — > Sets/gets the usage
  • Command#name()
  • Command#outputHelp() — > sets the help information displayed
  • Command#help()

With the API above we implement a command that lists all the files and folders in the current folder:

Description ('list files in current working directory') // give a description of the list command .option( '-a, --all', Action (function (options) {var fs = require('fs') var fs = require('fs') ); Fs.readdir (process.cwd(), function (err, files) {var list = files; if ( ! Options. all) {// Check whether the user has given --all or -a arguments, if not, filter out those with. List = files.filter(function (file) {return file.indexof ('.')! = = 0; }); } console.log( list.join( '\n\r' ) ); // Console prints all file names}); });Copy the code

run

Hi list # hi list -a or --all to see the effectCopy the code

Github address: Github portal, 0.0.1 branch for the first version of the code

Build the official version of the development environment to support ES6 syntax and SUPPORT ESLint

yarn add -D babel-cli babel-eslint babel-plugin-transform-es2015-modules-commonjs babel-preset-latest-node
Copy the code

Create a new. Babelrc file in the project root directory.

{
  "presets": [
    ["env", {
      "targets": {
        "node": "current"
      }
    }]
  ],
  "plugins": [
    "transform-es2015-modules-commonjs"
  ]
}
Copy the code

Create a new SRC directory for development, and a new SRC /command and SRC /utils directory for development. The directory structure is as follows:

├─ ├─ ├─ class ├─ class ├─ class ├─ class ├─ class ├─ class ├─ class ├─ class ├─ class ├─ class ├─ class ├─ class ├─ class ├─ class ├─ class ├─ class ├─ class ├─ class ├─ class Each command corresponds to a file, ├ ─utils # tools directoryCopy the code

Next, we implement an entry that transfers the functionality to the corresponding command implementation file to implement it. Create index.js to handle the entry, and create SRC /index.js for the actual function to forward index.js as follows

// Babel require("babel-register") require("babel-core"). Transform ("code", {presets: [ [ require( 'babel-preset-latest-node' ), { target: 'current' } ] ] } ); require( 'babel-polyfill' ) require('./src')Copy the code

SRC /index.js contains the following contents:

var program = require( 'commander' ); program.parse( process.argv ); // Start parsing the user input command require('./command/' + program.args + '.js') // switch to different command processing files according to different commandsCopy the code

Explain why I want to do this:

  • In order to ensure the single responsibility of the document, convenient maintenance;
  • Easy to load dev and product. SRC /command/init.js SRC /command/install.js

src/command/list.js:

var program = require( 'commander' ); Program.command ('init').description('init project for local').action(function (options) {// List implementation body // to do console.log( 'init command' ); }); program.parse( process.argv ); // Start parsing the commands entered by the userCopy the code

src/command/install.js:

var program = require( 'commander' ); program .command( 'install' ) .description( 'install github project to local' ) .action( function ( options ) { // To do console.log('install command'); }); program.parse( process.argv ); // Start parsing the commands entered by the userCopy the code

Type the following command on the command line to test:

webpack-template install

webpack-template init
Copy the code

The second version of github address can be clone down to try.

Next we implement the install and init functions respectively. First, the Install step assumes the following:

  • Pull the template project from the repository through the Github API
  • Download by selecting a template
  • Cached to a local temporary directory for direct use next time

First of all, I went to Github API V3 to find the REQUIRED API interface. In order to facilitate the independent management of template projects, I created an organization to manage them. So, I’m mainly through

/orgs/:org/repos # get the project and /repos/:owner/:repo # get the versionCopy the code

The project has been built, you can view the details of the warehouse through the following API

url -i https://api.github.com/orgs/cavinHuangORG/repos
Copy the code

2. Project version

curl -i https://api.github.com/repos/cavinHuangORG/webpack-multipage-template/tags
Copy the code

If you select an option from the command line, the result is as follows:




inquirer.gif

Here we use another command line interaction library, Inquirer. Js, which is used for command line selection and input; We will implement a simple code in insatll.js to complete the following code:

var inquirer = require( 'inquirer' ); program .command( 'install' ) .description( 'install github project to local' ) .action( function ( options ) { // To do console.log('install command'); let choices = [ 'aaa', 'bbb', 'ccc', 'dddd' ]; let questions = [ { type: 'list', name: 'repo', message: 'which repo do you want to install?', choices } ]; // Call the inquirer. Prompt (questions). Then (answers => {console.log(answers); // Output the final answer})}); program.parse( process.argv ); // Start parsing the commands entered by the userCopy the code

The final result is as follows:




install-2.gif

At this point we’re almost done with what we’re looking for. Next, I want to be able to initialize the entire project with the user entering some specific parameters.

download-git-repo

Here we want to use a library, making library to download code, download git – repo, usage is as follows:

download(repository, destination, options, callback)
Copy the code

Download a git repository to a destination folder with options, and callback. Download the Git repository to the target folder and callback functions with options

  • Repository Github repository address

    • GitHub – GitHub :owner/name or abbreviated owner/name
    • GitLab – gitlab:owner/name
    • Bitbucket – bitbucket:owner/name
  • Destination Destination folder

  • Options Parameters carried during download

    • The default false clone
  • The callback after the callback completes

Download-git-repo usage example

Const downloadGitRepo = require('download-git repo') // Download gitrepo ('CavinHuang/node-cli-demo'), './test', false, err => { console.log(err ? 'SUCCESS' : "FAIL"); })Copy the code

Complete git action classes

Git repository 0.0.3 branch. Git repository 0.0.3 branch. Git repository 0.0.3 branch

Async fetchRepoList() {} @param {[string]} repo [string] * @return {[type]} [description] */ async fetchRepoTagList(repo) {} @param {[string]} repo [repo] * @return {[type]} [description] */ async fetchGitInfo(repo) {} /** * download git repository code to the specified folder * @param {[string]} repo [repository name] * @return {[type]} [description] */ async downloadGitRepo( repo ) {}Copy the code

In install.js, the first thing we need to do is pull out all the templates in the repository for selection. Just change Choices to the Git pants list that we get from the API

import gitCtrl from '.. /utils/gitCtrl' import config from '.. Git = new gitCtrl.gitCtrl(config.repotype, config.registry)Copy the code

Change the action to:

Let choices = await git.fetchrepolist ();Copy the code

Create a config folder to store some configuration, define some common variables, such as cache directory, version, etc. Create constant.js

const os = require( 'os' ); import { name, version, engines } from '.. /.. /package.json'; // system user folder const home = process.env[(process.platform === 'win32')? 'USERPROFILE' : 'home']; // user agent export const ua = `${name}-${version}`; @type {Object} */ export const dirs = {home, download: '${home}/. Webpack-project', rc: `${home}/.webpack-project`, tmp: os.tmpdir(), metalsmith: 'metalsmith' }; /** * version * @type {Object} */ export const versions = {node: process.version.substr(1), nodeEngine: engines.node, [ name ]: version };Copy the code

index.js

RepoType: 'org', // ['org', 'user'] metalsmith: true}Copy the code

With that in mind, let’s download the code below:

DownloadGitRepo (answers.repo) console.log(result? 'SUCCESS' : result )Copy the code

And then we run

webpack-template install
Copy the code

The results are as follows:




install-2.gif

Now let’s add version selection. We’ll modify the code in install.js slightly and add version selection:

// Retrieve the selected git repository const repo = answers.repo; Const tags = await git.fetchrepotagList (repo); const tags = await git.fetchrepotagList (repo); if ( tags.length === 0 ) { version = ''; } else { choices = tags.map( ( { name } ) => name ); answers = await inquirer.prompt( [ { type: 'list', name: 'version', message: 'which version do you want to install?', choices } ] ); version = answers.version; } console.log( answers ); Let result = await git.downloadGitRepo([repo, version].join('@')); console.log( result ? 'SUCCESS' : result )Copy the code


install-3.gif

At this time we go to the system under the user folder. Webpack-project, we will find the project we changed. At this point, our install code is complete, github address

Complete the init command

The init command initializes a local project by cataloging some user-filled information. The idea is to replace the cataloging parameters and copy the downloaded project to the current command line execution directory. First, let’s do the simplest command line user input revenue, again using inquirer:

[{type: 'list', name: 'template', message: 'which template do you want to init?', choices: list }, { type: 'input', name: 'dir', message: 'project name', async validate(input) {const done = this.async();  if ( input.length === 0 ) { done( 'You must input project name' ); return;  } const dir = resolve( process.cwd(), input );  if ( await exists( dir ) ) { done( 'The project name is already existed. Please change another name' );  } done( null, true ); } } ]; const answers = await inquirer.prompt( questions )Copy the code

NCP usage help

The next step is to collect more detailed information and copy the downloaded file to a temporary directory for processing, using the mature NCP library, which is consistent with the Linux CP command interface. NCP [source] [dest] [–limit=concurrency limit] [–filter=filter] –stop Err

var ncp = require('ncp').ncp; ncp.limit = 16; ncp(source, destination, function (err) { if (err) { return console.error(err); } console.log('done! '); });Copy the code

Mkdirp use help

The main function is the same as Linux mkdir -p, except that it runs in Node, creating directories recursively. Main usage:

var mkdirp = require('mkdirp');

mkdirp('/tmp/foo/bar/baz', function (err) {
    if (err) console.error(err)
    else console.log('pow!')
});
Copy the code

Based on these two libraries, we have a separate utility function dedicated to copy our projects

import { ncp } from 'ncp'; import mkdirp from 'mkdirp' import { exists } from 'mz/fs' export default function copyFile( src, dest ) { return new Promise( async ( resolve, reject ) => { if ( ! ( await exists( src ) ) ) { mkdirp.sync( src ); } NCP (SRC, dest, (err) => {if (err) {reject(err); return; } resolve(); }); }); }Copy the code

Copy to a temporary folder and generate projects through a data filling process using a static site generator (Metalsmith) and swig and consolidate a template engine repository

Add copy action and compile action to init.js

const answers = await inquirer.prompt( questions ) const metalsmith = config.metalsmith; if ( metalsmith ) { const tmp = `${dirs.tmp}/${answers.template}`; // Copy a copy to temporary directory and compile in temporary directory to generate await copyFile(' ${dirs.download}/${answers. Template} ', TMP); await metalsmithACtion( answers.template ); // Compile await copyFile(' ${TMP}/${dirs.metalsmith} ', answers.dir); await rmfr( tmp ); } else {await copyFile(' ${dirs.download}/${answers. Template} ', answers.dir); }Copy the code

The final directory structure is as follows:

│. Babelrc │. Gitignore │ index. Js │ package. The json │ ├ ─ bin. │ hi js │ └ ─ SRC │ index. The js │ ├ ─ command │ init. Js │ the js │ ├ ─ config │ constant. Js │ index. The js │ └ ─ utils used by copyFile. Js gitCtrl. Js initProjectQuestion. Js # initialization problem of the project Metalsmithaction.js # temporary folder to compile action render.js # Plugin to compile templatesCopy the code

To this all the functions have been realized, in order to make the whole command use more humanized, more process, we introduce ora this library, project address: ORA, the main effect is as follows:

Create oraloading.js in utils

import ora from 'ora';

export default function OraLoading( action = 'getting', repo = '' ) {
    const l = ora( `${action} ${repo}` );
    return l.start();
}
Copy the code

Ok, now that everything is done, let’s try it out: First, install




install-last.gif

Init is the same thing and I’m not going to show you

NPM release

  • Go to npm.com to register your account, then switch to the command line folder in the current directory, run the NPM login command, enter your account password, and login.

perform

npm publish .
Copy the code

If you are using Taobao source, you need to switch back to NPM source.

npm config set registry http://registry.npmjs.org
Copy the code

Otherwise, the authentication fails.

Github complete code address: Github portal

Please don’t be stingy with your own start, thanks for your encouragement!