Number of words: 3621. Reading Time: 14 minutes

I have been meaning to write this article for a long time, but I have been delayed by other things (perhaps because I am too lazy). If I don’t write it down, I will probably forget it.

NodeJs makes the concept of front-end engineering deeper and closer to the regular army. First came powerful build tools such as Gulp and Webpack, followed by sophisticated scaffolding such as VUe-CLI and creation-React-app, which provided a complete project architecture that allowed us to focus more on the business without spending a lot of time on the project infrastructure.

However, if the off-the-shelf scaffolding doesn’t necessarily meet our business needs or best practices, we can develop one ourselves. Of course, this is actually very simple, using the wheel on the NPM can be done, here is a note, just as a reminder, to throw off a brick.

origin

In a project in the first half of the year, I needed to define a scaffold to help my friends improve the development efficiency, unify the quality of code output and solve some problems in use. Of course, it was also for installation. Before using scaffolding, we ran into these issues:

  • Every time a project is created, you need to go to a Git repository and pull the project template or copy the previous project. There are two problems with doing this
    • After pulling the project from Git, some people have push permission. If they push the modification in the private project to the template repository by mistake, the project template on Git may be damaged
    • The latest project template cannot be retrieved from the previous project copy, resulting in problems that are fixed in the latest template but still exist in the new project
  • Project templates require configuration information that developers can easily forget to fill out

So let’s solve these problems, along the following lines:

  • Pull the latest template from Git, and finally remove the Git repository information to disconnect the remote repository
  • During the initialization, users are forced to enter configuration information through the question and answer mode, and then a configuration file is generated based on the configuration information, similar to the VueCli initialization project.

Of course, in addition to the above requirements, we can also do some additional work:

  • When the pull is complete, the project dependencies are automatically installed and the editor opens
  • View help information and common commands
  • Published to NPM, all personnel can directly global installation use
  • .

Quickly quickly your mother call you.

Implement executable modules

First, we need to create a project called yncms-template-cli. The structure of the project is as follows:

- commands  // This folder is used to place custom commands
- utils
- index.js  // Project entry
- readme.md
Copy the code

To test this, let’s put something in index.js:

#! /usr/bin/env node
// You must add the above content to the file header to specify the runtime environment as Node
console.log('hello cli');
Copy the code

For a normal NodeJS project, we can just use Node index.js, but with scaffolding, this is definitely not the case. We need to publish the project to NPM, users install it globally, and then we can use our own custom commands like yncms-template.

So, we need to make some changes to our project, first adding the following content to packge.json:

 "bin": {
    "yncms-template": "index.js"
  },
Copy the code

Yncms-template can be defined as a command, but it can only be used in the project, not as a global command. Here we need to use the NPM link to link to the global command, which can be found in your global node_modules directory. Then enter the command to test, if the following content appears, the first step has been more than half successful:

PS E:\WorkSpace\yncms-template-cli> yncms-template
hello cli
Copy the code

However, this command can only be used by our own computer at present. In order for others to install and use it, it needs to be released to NPM, and the general process is as follows:

  1. Sign up for an NPM account, skip this step if you already have one
  2. usenpm loginLogin, you need to enterusername,password,email
  3. usenpm publishrelease

This step is relatively simple and I won’t go into details, but please note the following points:

  • Using thenrmYou need to switch the source tonpmThe official source
  • package.jsonThere are several fields that need to be refined in:
    • nameThe published package name cannot be the same as an existing NPM package name
    • versionFor version information, each release must be higher than the online version
    • homepage,bugs,repositoryYou can also add the following page

  • Readme. Md adds scaffolding instructions and instructions for others to use. If you need to add a logo to the document, showing the number of scaffolding downloads and so on, you can generate it here.

After a successful publication, it takes a while to search for it in the NPM repository.

Create a command

Since it is a scaffold, we can’t just make it output a paragraph of text, we need to define some commands, the user input these commands and parameters on the command line, the scaffold will do the corresponding operation. There is no need for us to parse the input commands and parameters ourselves. There is a ready-made wheel (Commander) that is perfectly suited to our needs.

Help (–help)

With Commander installed, we changed the contents of index.js to the following:

#! /usr/bin/env node
const commander = require('commander');
// Use COMMANDER to parse command line input and must be written at the end of everything
commander.parse(process.argv);
Copy the code

At this point, although we haven’t defined any commands, commander internally defines a help command –help(-h):

PS E:\WorkSpace\yncms-template-cli> yncms-template -h
Usage: index [options]

Options:
  -h, --help  output usage information
Copy the code

Version (–version)

Next, we create a query version of the command argument, add the following content to index.js:

// Check the version number
commander.version(require('./package.json').version);
Copy the code

This way, we can check the version number on the command line:

PS E:\WorkSpace\yncms-template-cli> yncms-template -V
1.0.10
PS E:\WorkSpace\yncms-template-cli> yncms-template --version
1.0.10
Copy the code

The default parameter is uppercase V. If you need to change it to lowercase, do the following:

Commander. Version (require('./package.json').version). Option ('-v,--version', 'view version');Copy the code
PS E:\WorkSpace\yncms-template-cli> yncms-template -h
Usage: index [options]
Options:
  -V, --version  output the version number
  -h, --help     output usage information
Copy the code

The init command son

Next, let’s define an init command, such as yncms-template init test. Add the following to index.js:

commander
    .command('init <name>') // define init subcommand, 
      
        is required parameters can be received in action function, if you want to set non-required parameters, use brackets
      
    .option('-d, --dev'.'Get dev') // Configure parameters, used in shorthand and full write, split
    .description('Create project') // Command description
    .action(function (name, option) { // Command to execute the operation. The parameter corresponds to the parameter set above
        // Everything we need to do is done here
        console.log(name);
        console.log(option.dev);
    });
Copy the code

Now test it out:

PS E:\WorkSpace\yncms-template-cli> yncms-template init test -d
test
true
Copy the code

Commander For details, please check the official documentation.

Thus, a prototype custom command is complete, but there are still a few things to do:

  • implementationinitThe operations performed by the command are described in a separate section below.
  • For easy maintenance, will commandactionBreak up tocommandsfolder

Pull the project

Above, we defined the init command, but did not achieve the purpose of initializing the project, so let’s implement it.

In general, project templates can be handled in two ways:

  • The project template and this scaffold together, the advantage is that after the user installs the scaffold, the template in the local, the initialization will be faster; The downside is that the project template is cumbersome to update because it is coupled to the scaffolding
  • Putting your project in a separate GIT repository makes it easier to update the template because it is independent of each other and you only need to maintain your own repository. In addition, you can control the pull permission because if it is a private project, people without permission will not be able to pull successfully. The disadvantage is that every initialization needs to go to GIT pull, which may be slower, but the impact is not big, so it is recommended to choose this method

First, we package a Clone method with download-git-repo to pull projects from Git.

// utils/clone.js
const download = require('download-git-repo');
const symbols = require('log-symbols');  // To output ICONS
const ora = require('ora'); // For output loading
const chalk = require('chalk'); // Used to change the text color
module.exports = function (remote, name, option) {
    const downSpinner = ora('Downloading templates... ').start();
    return new Promise((resolve, reject) = > {
        download(remote, name, option, err= > {
            if (err) {
                downSpinner.fail();
                console.log(symbols.error, chalk.red(err));
                reject(err);
                return;
            };
            downSpinner.succeed(chalk.green('Template downloaded successfully! '));
            resolve();
        });
    });
  };
Copy the code
// commands/init.js
const shell = require('shelljs');
const symbols = require('log-symbols');
const clone = require('.. /utils/clone.js');
const remote = 'https://gitee.com/letwrong/cli-demo.git';
let branch = 'master';

const initAction = async (name, option) => {
    // 0. Check whether the console can run 'git',
    if(! shell.which('git')) {
        console.log(symbols.error, 'Sorry, git command not available! ');
        shell.exit(1);
    }
    // 1. Verify that the input name is valid
    if (fs.existsSync(name)) {
        console.log(symbols.warning,'The project folder already exists${name}! `);
        return;
    }
    if (name.match(/[^A-Za-z0-9\u4e00-\u9fa5_-]/g)) {
        console.log(symbols.error, 'Invalid characters exist in project name! ');
        return;
    }
    // 2. Get option, determine template type (branch)
    if (option.dev) branch = 'develop';
    // 4. Download the template
    await clone(`direct:${remote}#${branch}`, name, { clone: true });
};

module.exports = initAction;
Copy the code

Test it and pull the item successfully without accident.

The project pulled here is associated with the remote repository, we need to delete it (because our project is managed by SVN, so we directly delete the.git folder, if you use git, you can initialize it with git init), and clean up some redundant files:

// commands/init.js
// 5. Clean up files
const deleteDir = ['.git'.'.gitignore'.'README.md'.'docs']; // Files that need to be cleaned
const pwd = shell.pwd();
deleteDir.map(item= > shell.rm('-rf', pwd + ` /${name}/${item}`));
Copy the code

A little personalization

In the above process, we realized the basic functions of a scaffold, which can be roughly divided into three processes (drawing template -> creating project -> finishing cleaning), and also solved the first problem I encountered in the above project. Next, let’s look at how to solve the second problem.

The solution is to force the developer to enter the corresponding configuration through the command line when creating the project, and then automatically write the configuration file, so that you can effectively avoid the embarrassment of forgetting to fill in. Of course, in this way, the project can also be dynamically initialized according to the user’s input to achieve the purpose of personalized.

Here we can use the wheel inquirer directly, the effect is the same as VueCli create project, support many types, more powerful, but also relatively simple, specific use of the official documentation can be. Here I’ll go straight to the code and add the following before step 4 (download template) :

// init.js
const inquirer = require('inquirer');
// Define the questions to be asked
const questions = [
  {
    type: 'input'.message: 'Please enter template name :'.name: 'name'.validate(val) {
      if(! val)return 'Template name cannot be empty! ';
      if (val.match(/[^A-Za-z0-9\u4e00-\u9fa5_-]/g)) return 'Template name contains illegal characters, please retype';
      return true; }}, {type: 'input'.message: 'Please enter template keywords (; Segmentation) : '.name: 'keywords'
  },
  {
    type: 'input'.message: 'Please enter template introduction :'.name: 'description'
  },
  {
    type: 'list'.message: Please select template type:.choices: ['responsive'.'Desktop'.'Mobile end'].name: 'type'
  },
  {
    type: 'list'.message: 'Please select template category :'.choices: ['site'.'single page'.'special'].name: 'category'
  },
  {
    type: 'input'.message: 'Please enter template style :'.name: 'style'
  },
  {
    type: 'input'.message: 'Please enter template color :'.name: 'color'
  },
  {
    type: 'input'.message: 'Please enter your name :'.name: 'author'}];// Get user input from inquirer
const answers = await inquirer.prompt(questions);
// Print the user's configuration to make sure it is correct
console.log('-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --');
console.log(answers);
let confirm = await inquirer.prompt([
    {
        type: 'confirm'.message: 'Confirm creation? '.default: 'Y'.name: 'isConfirm'}]);if(! confirm.isConfirm)return false;
Copy the code

Once you get the configuration input from the user, you can write it to the configuration file or personalize it. This is too simple to go into here.

The icing on the cake

At this point, a fully satisfying scaffolding is complete, but as aspiring programmers, we can do a little more with interface and ease of use:

  • Ora can be used directly to animate asynchronous operations with loding
Const installSpinner = ora(' installing dependencies... ').start(); if (shell.exec('npm install').code ! == 0) {console.log(symbol. warning, chalk. Yellow (' Automatic installation failed, manual installation please! ')); installSpinner.fail(); // Install failed shell.exit(1); } installSpinner. Succeed (chalk. Green (' installSpinner succeeded! '));Copy the code
  • Use log-symbols to display ICONS for success or failure

  • You can add color to the text, using the ready-made wheel Chalk

  • When installing dependencies or taking a long time, users may switch terminals to the background. In this case, you can use Node-Notifier to send system notifications to users after the operation is complete.
Notifier. Notify ({title: 'yncms-template-cli ', icon: path.join(__dirname, 'coulson.png'), message:' (^ ∀ ^ ●) Blue congratulations, the project is created successfully! '});Copy the code
  • When creating a project, you may need to execute some shell commands that you can useshelljsFor example, we want to open it after the project is createdvscodeAnd exit the terminal
The if / / 8. The editor (shell) which (' code ')) shell. The exec (' code. / '); shell.exit(1);Copy the code

conclusion

It’s easy to develop a scaffold using off-the-shelf wheels. I don’t know who said playing with NodeJS is playing with wheels.

In addition to the above methods, we could have created it directly from the famous Yeoman, but I don’t think it’s necessary, because it’s not that hard.

A good scaffold should be able to solve problems encountered in the work and improve development efficiency.