The purpose of scaffolding is to quickly build the basic structure of the project and provide project specifications and conventions. The scaffolding commonly used in daily work includes vue-CLI, create-react-app, angular-cli, etc., all of which are quick to build content through simple initialization commands.

Scaffolding is a tool we use a lot, and it’s an important way to improve team performance. So a systematic grasp of scaffolding related knowledge, for front-end developers is very important, even if many people will not necessarily participate in their departments or companies in the future infrastructure work, but a systematic grasp of this skill can also facilitate our later source code reading. Let’s take a look at 😉

A simple prototype of scaffolding 🐣

Scaffolding is to ask some simple questions at startup, and render corresponding template files based on the results of user answers. The basic workflow is as follows:

  1. Ask the user questions through command line interaction
  2. Generate files based on the results of user answers

For example, when we create a VUE project using vue-CLI 👇

Step1: run the create command

$ vue create hello-world
Copy the code

Step2: Ask the user questions

Step3: generate project files that meet user requirements

# ignore some folders├─ │ ├─ ├─ vue- Project ├─ index.html ├─ SRC │ ├─ vue │ ├─ │ ├─ ├─ vue │ ├─ │ ├─ vue │ ├─ │ ├─ vue │ ├─ ├─ vue │ ├─ ├─ vue │ ├─ ├─ vue │ ├─ ├ ─ garbage, ├ ─ garbage, ├ ─ garbageCopy the code

Refer to the above process we can build a simple scaffolding prototype

1. Start the CLI

Goal: Implement my-node-cli on the command line to start our scaffolding

1.1 Creating a Project Directory My-node-CLI

$ mkdir my-node-cli 
$ cd my-node-cli 
$ npm init Generate package.json file
Copy the code

1.2 Creating the program entry file cli.js

$ touch cli.js # Create a cli.js file
Copy the code

Specify the entry file in package.json as cli.js 👇

{
  "name": "my-node-cli"."version": "1.0.0"."description": ""."main": "cli.js"."bin": "cli.js".// Manually add the entry file as cli.js
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": ""."license": "ISC"
}
Copy the code

Project directory structure:

My-node ├─ cli.js ├─ package.jsonCopy the code

Open cli.js to edit

#! /usr/bin/env node

/ / #! The name of the symbol is Shebang and is used to specify the interpreter of the script
// The Node CLI application entry file must have such a header
// In Linux or macOS, change the read/write permission to 755
// Use chmod 755 cli.js to implement the modification

// Check whether the entry file is properly executed
console.log('my-node-cli working~')
Copy the code

1.3 NPM Link Link to global

$ npm link # or yarn link
Copy the code

Execution completed ✅

To test this, type my-node-cli on the command line

$ my-node-cli
Copy the code

Here we see it printed on the command line

my-node-cli working~
Copy the code

➤ Finish your ➤

2. Ask for user information

The function to implement and query user information needs to be introduced in the inquirer. Js 👉 documentation see here

$ npm install inquirer --dev # yarn add inquirer --dev
Copy the code

Then we set up our problem in cli.js

#! /usr/bin/env node

const inquirer = require('inquirer')

inquirer.prompt([
  {
    type: 'input'.//type: input, number, confirm, list, checkbox...
    name: 'name'./ / the key name
    message: 'Your name'.// Prompt message
    default: 'my-node-cli' / / the default value
  }
]).then(answers= > {
  // Prints the interoperable input results
  console.log(answers)
})

Copy the code

Type my-node-cli on the command line to see the result

Here we have the project name {name: ‘my-app’} entered by the user, 👌

3. Generate corresponding files

3.1 Creating a Template folder

$ mkdir templates Create template folder
Copy the code

3.2 Create two simple example files index. HTML and common. CSS

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>
    <! -- EJS syntax -->
    <%= name %>
  </title>
</head>
<body>
  <h1><%= name %></h1>
</body>

</html>
Copy the code
/* common.css */
body {
  margin: 20px auto;
  background-color: azure;
}
Copy the code

The directory structure at this point

My-node -cli ├─ templates │ ├─ common.css │ ├─ cli.js ├─ package-lock.json ├─ ├.jsonCopy the code

3.3 Then improve the file generation logic

Here the ejS template engine is used to render the user-input data to the template file

npm install ejs --save # yarn add ejs --save
Copy the code

Go to cli.js 👇

#! /usr/bin/env node

const inquirer = require('inquirer')
const path = require('path')
const fs = require('fs')
const ejs = require('ejs')

inquirer.prompt([
  {
    type: 'input'./ / type: input, confirm the list, rawlist, checkbox, password...
    name: 'name'./ / the key name
    message: 'Your name'.// Prompt message
    default: 'my-node-cli' / / the default value
  }
]).then(answers= > {
  // Template file directory
  const destUrl = path.join(__dirname, 'templates'); 
  // Generate the file directory
  // process.cwd() corresponds to the console directory
  const cwdUrl = process.cwd();
  // Read files from the template directory
  fs.readdir(destUrl, (err, files) = > {
    if (err) throw err;
    files.forEach((file) = > {
      // Use EJS to render the corresponding template file
      // renderFile (template file address, passed rendering data)
      ejs.renderFile(path.join(destUrl, file), answers).then(data= > {
        // Generate template file after EJS processing
        fs.writeFileSync(path.join(cwdUrl, file) , data)
      })
    })
  })
})
Copy the code

Also, in the console, run my-node-cli. Now index.html and common.css have been successfully created

Let’s print the current directory structure 👇

My - node - cli ├ ─ templates │ ├ ─. Common CSS │ └ ─ index. The HTML ├ ─ cli. Js ├ ─. Common CSS... Generate the corresponding common. CSS file ├ ─ index. The HTML... ├─ package-lock.json ├─ package.jsonCopy the code

Open the generated index.html file and take a look

<! DOCTYPEhtml>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <! -- EJS syntax -->
  <title>
    my-app
  </title>
</head>

<body>
  <h1>my-app</h1>
</body>

</html>
Copy the code

{name: ‘my-app’} entered by the user has been added to the generated file ✌️

Click here to open 👉 my-node-CLI source code address

Two, popular scaffolding tool library 🔧

Build a scaffold in actual production or read other scaffolding source code need to know the following tool library 👇

The name of the Introduction to the
commander Command line custom command
inquirer The cli asks the user questions and records the answers
chalk Console output content styling
ora Console Loading style
figlet Logo Printing on the console
easy-table Console output table
download-git-repo Download the remote template
fs-extra System FS module extension, provides more convenient API, and inherited the FS module API
cross-spawn Supports cross-platform invocation of system commands

Focus on the following, and check out the documentation for other tools

1. Commander Custom command line commands

More usage 👉 Chinese document

Simple case 👇

1.1 Creating a Simple Node Cli project

// package.json
{
  "name": "my-vue"."version": "1.0.0"."description": ""."bin": "./bin/cli.js"."scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "T-Roc"."license": "ISC"."devDependencies": {
    "commander": "^ 7.2.0"}}Copy the code

Directory structure:

├─ ├─ download.json ├─ download.txtCopy the code

1.3 Introduction of COMMANDER Write code

# install dependencies
npm install commander # yarn add commander 
Copy the code

Improve the bin.js code

#! /usr/bin/env node

const program = require('commander')

program
.version('0.1.0 from')
.command('create <name>')
.description('create a new project')
.action(name= > { 
    // Prints the values entered on the command line
    console.log("project name is " + name)
})

program.parse()
Copy the code

1.3 NPM Link Link to global

  • performnpm linkWill be appliedmy-vueLink to global
  • When finished, execute on the command linemy-vue

Take a look at the output from the command line 👇

~/Desktop/cli/npms-demo ->my-vue

Usage: my-vue [options] [command]

Options:
  -V, --version   output the version number
  -h, --help      display help for command

Commands:
  create <name>   create a new project
  help [command]  display help for command
Copy the code

Now you have instructions to use the my-vue command. Under Commands you will see the create command we just created, create

, run it from the command line

~/Desktop/cli/npms-demo ->my-vue create my-app
project name is my-app
Copy the code

At this point, the console prints the

value my-app 👏 after the create command

2. Chalk command line beautification tool

Chalk beautifies the style of our output on the command line, such as adding colors to important information

2.1 Installation Dependencies

npm install chalk # yarn add chalk
Copy the code

2.2 Basic Usage

Open bin/cli.js in the nPMS-demo project

#! /usr/bin/env node

const program = require('commander')
const chalk = require('chalk')

program
.version('0.1.0 from')
.command('create <name>')
.description('create a new project')
.action(name= > { 
    // Prints the values entered on the command line

    // Text style
    console.log("project name is " + chalk.bold(name))

    / / color
    console.log("project name is " + chalk.cyan(name))
    console.log("project name is " + chalk.green(name))

    / / the background color
    console.log("project name is " + chalk.bgRed(name))

    // Use RGB color output
    console.log("project name is " + chalk.rgb(4.156.219).underline(name));
    console.log("project name is " + chalk.hex('#049CDB').bold(name));
    console.log("project name is " + chalk.bgHex('#049CDB').bold(name))
})

program.parse()
Copy the code

To see the effect, run the project my-vue create my-app from the command line

The specific style comparison table is as follows 👇

3. Inquirer command line interaction tool

👉 Document address

Inquirer is used frequently in scaffolding tools. In fact, in the simple prototype of scaffolding above, we have already used it.

4. Ora command line loading

👉 Document address

// Customize text information
const message = 'Loading unicorns'
/ / initialization
const spinner = ora(message);
// Start loading animation
spinner.start();

setTimeout(() = > {
    // Modify the animation style

    // Type: string
    // Default: 'cyan'
    // Values: 'black' | 'red' | 'green' | 'yellow' | 'blue' | 'magenta' | 'cyan' | 'white' | 'gray'
    spinner.color = 'red';    
    spinner.text = 'Loading rainbows';

    setTimeout(() = > {
        // Load status changed
        spinner.stop() / / stop
        spinner.succeed('Loading succeed'); ✔ / / success
        // spinner.fail(text?) ; Failure ✖
        // spinner.warn(text?) ; Prompt ⚠
        // spinner.info(text?); Information ℹ
    }, 2000);
}, 2000);
Copy the code

The command line output is as follows

Cross-spawn cross-platform shell tool

👉 Document address

Inside scaffolding, it can be used to automatically execute shell commands such as:

#! /usr/bin/env node 

const spawn = require('cross-spawn');
const chalk = require('chalk')

// Define the dependencies that need to be followed
const dependencies = ['vue'.'vuex'.'vue-router'];

// Perform the installation
const child = spawn('npm'['install'.'-D'].concat(dependencies), { 
    stdio: 'inherit' 
});

// Listen for execution results
child.on('close'.function(code) {
    // Execution failed
    if(code ! = =0) {
        console.log(chalk.red('Error occurred while installing dependencies! '));
        process.exit(1);
    }
    // The command is successfully executed
    else {
        console.log(chalk.cyan('Install finished'))}})Copy the code

Again, execute my-vue on the command line to see the result

Successful installation 👍

Three, build their own scaffolding 🏗

First give our scaffolding a name, just zhurong landed on Mars, it is better to call: zhurong-cli 😆

. -') _ ('-... -. _. -'). -) _             
  (  OO) )( OO )  /            ( \( -O )                  ( OO ) )            
,(_)----. ,--. ,--. ,--. ,--.   ,------.  .-'), -- -- -- -- -.,,. /,,, ', -. | | | | | | | | | | | / `.'( OO'.....'| \ | | \'. -. / -') '-. / | | | | | |. -') | / | |/ | | | || \| | )| |_( O- ) (_/ / | | | |_|( OO )| |_.'| \ _) | | the \ | |. | | / | |. - \ / / ___ | - | | | | ` -'/ |..'\ | | | | | | \ | (| |'. (_ / | | | | | | -' '-'(_.| | \ \ `' '-' '|  | \   |  |  The '-'| ` -- -- -- -- -- -- -- --'`--' `--'` -- -- -- -- --'    `--' '--'` -- -- -- -- --' `--'` -'` -- -- -- -- -- -' 
Copy the code

What basic functions need to be implemented:

  1. throughzr create <name>Command to start the project
  2. Ask the user to select the template to download
  3. Remotely pull a template file

Construction steps Disassembling:

  1. Create a project
  2. Create scaffold start command (using Commander)
  3. Ask the user questions to get the information needed for creation (using Inquirer)
  4. Download a remote template (use download-git-repo)
  5. Publish the project

1. Create projects

Follow the previous example to create a simple Node-CLI structure

├─ bin │ ├─ cli.jsStart file├ ─ README. Md └ ─ package. The jsonCopy the code

Configure the scaffold startup file

{
  "name": "zhurong-cli"."version": "1.0.0"."description": "simple vue cli"."main": "index.js"."bin": {
    "zr": "./bin/cli.js" // Configure the boot file path with zr as the alias
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": {
    "name": "T-Roc"."email": "[email protected]"
  },
  "license": "MIT"
}
Copy the code

A quick edit of our cli.js

#! /usr/bin/env node

console.log('zhurong-cli working ~')
Copy the code

To facilitate development and debugging, use the NPM link to link globally

~/Desktop/cli/zhurong-cli -> NPM link NPM WARN [email protected] No repository field. Up to dateinNone 1.611 s found 0 December /usr/for1local/bin/zr -> /usr/local/lib/node_modules/zhurong-cli/bin/cli.js
/usr/local/lib/node_modules/zhurong-cli -> /Users/Desktop/cli/zhurong-cli
Copy the code

Once you’re done, test it out

~/Desktop/cli/zhurong-cli ->zr
zhurong-cli working ~ # Print content
Copy the code

OK, so we’ve got what we want to print, and then

2. Create a scaffold startup command

A quick analysis of what we’re going to do?

  1. The first step is to use the COMMANDER dependency to implement this requirement
  2. Reference vuE-CLI commonly used commands are create, config, etc. In the latest version, you can use vue UI for visual creation
  3. If the created one exists, you need to prompt whether to overwrite it

Start now 😉

2.1 Installation Dependencies

$ npm install commander --save
Copy the code

After the installation is complete 👇

2.2 Creating Commands

Open cli.js to edit

#! /usr/bin/env node

const program = require('commander')

program
  // Define commands and parameters
  .command('create <app-name>')
  .description('create a new project')
  -for --force Forcibly creates a directory. If the created directory exists, the directory is overwritten directly
  .option('-f, --force'.'overwrite target directory if it exist')
  .action((name, options) = > {
    // Prints the execution result
    console.log('name:',name,'options:',options)
  })
  
program
   // Set the version number
  .version(`vThe ${require('.. /package.json').version}`)
  .usage('<command> [option]')
  
// Parses the parameters passed by the user to execute the command
program.parse(process.argv);
Copy the code

Type zr on the command line to check that the command was created successfully

~/Desktop/cli/zhurong-cli ->zr
Usage: zr <command> [option]

Options:
  -V, --version                output the version number
  -h, --help                   display help for command

Commands:
  create [options] <app-name>  create a new project
  help [command]               display help for command
Copy the code

Create [options]

~/Desktop/cli/zhurong-cli ->zr create
error: missing required argument 'app-name'

~/Desktop/cli/zhurong-cli ->zr create my-project
执行结果 >>> name: my-project options: {}

~/Desktop/cli/zhurong-cli ->zr create my-project -f
执行结果 >>> name: my-project options: { force: true} ~/Desktop/cli/zhurong-cli ->zr create my-project --force >>> name: my-project options: {force:true }
Copy the code

Successfully obtain the command line input information 👍

2.3 Running Commands

Create the lib folder and create create.js under the folder

// lib/create.js

module.exports = async function (name, options) {
  // Verify that the value is correctly fetched
  console.log('>>> create.js', name, options)
}
Copy the code

Use create.js in cli.js

// bin/cli.js. program .command('create <app-name>')
  .description('create a new project')
  .option('-f, --force'.'overwrite target directory if it exist') // Whether to force the folder to be created when it already exists
  .action((name, options) = > {
    // Perform the creation task in create.js
    require('.. /lib/create.js')(name, options)
  })
......
Copy the code

Execute zr create my-project, and create. Js normally prints our entry and exit information

~/Desktop/cli/zhurong-cli ->zr create my-project
>>> create.js
my-project {}
Copy the code

When creating a directory, you need to ask: Does the directory already exist?

  1. If there is
    • when{ force: true }“, remove the original directory and create it directly
    • when{ force: false }Is asked if the user needs to be overridden
  2. If no, create one directly

Here is the fs extension tool Fs-extra

# fs-extra is an extension to the FS module that supports Promise
$ npm install fs-extra --save
Copy the code

Let’s next refine the implementation logic inside create.js

// lib/create.js

const path = require('path')
const fs = require('fs-extra')

module.exports = async function (name, options) {
  // Execute the create command

  // Directory selected by the current command line
  const cwd  = process.cwd();
  // Directory address to be created
  const targetAir  = path.join(cwd, name)

  // Does the directory already exist?
  if (fs.existsSync(targetAir)) {

    // Is it mandatory?
    if (options.force) {
      await fs.remove(targetAir)
    } else {
      // TODO: asks the user if they are sure to overwrite}}}Copy the code

Ask for some logic, which we will refine below

2.3 Creating More Commands

If you want to add other commands in the same way, I will not extend the description here, as shown in 👇

// bin/cli.js

// Configure the config command
program
  .command('config [value]')
  .description('inspect and modify the config')
  .option('-g, --get <path>'.'get value from option')
  .option('-s, --set <path> <value>')
  .option('-d, --delete <path>'.'delete option from config')
  .action((value, options) = > {
    console.log(value, options)
  })

// Configure the UI command
program
  .command('ui')
  .description('start add open roc-cli ui')
  .option('-p, --port <port>'.'Port used for the UI Server')
  .action((option) = > {
    console.log(option)
  })
Copy the code

2.4 Improve help information

Let’s first look at the information printed by vue-CLI execution –help

Compared with the printed result of ZR –help, an explanatory message is missing at the end of the page. Here we make a supplement. It is important to note that the explanatory message is colored, so we need to use chalk in our tool library to process it

// bin/cli.js

program
  // Listen --help execute
  .on('--help'.() = > {
    // Add description information
    console.log(`\r\nRun ${chalk.cyan(`zr <command> --help`)} for detailed usage of given command\r\n`)})Copy the code

2.5 Print a Logo

If we want to give the scaffold a full Logo at this point, the figlet in the toolkit does this 😎

// bin/cli.js

program
  .on('--help'.() = > {
    // Use figlet to draw the Logo
    console.log('\r\n' + figlet.textSync('zhurong', {
      font: 'Ghost'.horizontalLayout: 'default'.verticalLayout: 'default'.width: 80.whitespaceBreak: true
    }));
    // Add description information
    console.log(`\r\nRun ${chalk.cyan(`roc <command> --help`)} show details\r\n`)})Copy the code

Let’s see what the zr –help looks like when it’s printed

Looks pretty good, haha 😄

3. Ask the user questions to obtain the required information

Here we summon our old friend Inquirer to help us solve the problem of command line interaction

What we are going to do next:

  1. Legacy from the previous step: Ask the user whether to overwrite an existing directory
  2. User selection template
  3. User selected version
  4. Get a link to download the template

3.1 Asking whether to Overwrite an Existing Directory

Here is a solution to the problem left over from the previous step:

  1. If the directory already exists
    • when{ force: false }Is asked if the user needs to be overridden

The logic is actually done, so here’s a sidebar to the query

The first choice is to install the Inquirer

$ npm install inquirer --save
Copy the code

The user is then asked whether to Overwrite

// lib/create.js

const path = require('path')

// fs-extra is an extension of the FS module that supports the Promise syntax
const fs = require('fs-extra')
const inquirer = require('inquirer')

module.exports = async function (name, options) {
  // Execute the create command

  // Directory selected by the current command line
  const cwd  = process.cwd();
  // Directory address to be created
  const targetAir  = path.join(cwd, name)

  // Does the directory already exist?
  if (fs.existsSync(targetAir)) {

    // Is it mandatory?
    if (options.force) {
      await fs.remove(targetAir)
    } else {

      // Ask the user if they are sure to overwrite
      let { action } = await inquirer.prompt([
        {
          name: 'action'.type: 'list'.message: 'Target directory already exists Pick an action:'.choices: [{name: 'Overwrite'.value: 'overwrite'}, {name: 'Cancel'.value: false}}]])if(! action) {return;
      } else if (action === 'overwrite') {
        // Remove an existing directory
        console.log(`\r\nRemoving... `)
        await fs.remove(targetAir)
      }
    }
  }
}
Copy the code

Let’s test it out:

  1. Manually create two directories in the current directory, the directory shown on the command line, and call them my-project and my-project2
  2. performzr create my-project, the effect is as follows

  1. performzr create my-project2 --f, you can immediately see my-Project2 removed

⚠️ Note: Why only remove here? After obtaining the template address, the project directory will be created directly when downloading

3.2 How to Obtain Template Information

I have uploaded the template to the remote repository: github.com/zhurong-cli

Vue3.0-template version information 👇

Vue-template Version information 👇

Making offers

  • Api.github.com/orgs/zhuron… Interface to obtain template information
  • Api.github.com/repos/zhuro… Interface to obtain version information

We create an http.js file in the lib directory to handle template and version retrieval

// lib/http.js

// Process the request through AXIos
const axios = require('axios')

axios.interceptors.response.use(res= > {
  return res.data;
})


/** * get the template list *@returns Promise* /
async function getRepoList() {
  return axios.get('https://api.github.com/orgs/zhurong-cli/repos')}/** * Obtain version information *@param {string} Repo template name *@returns Promise* /
async function  getTagList(repo) {
  return axios.get(`https://api.github.com/repos/zhurong-cli/${repo}/tags`)}module.exports = {
  getRepoList,
  getTagList
}
Copy the code

3.3 Selecting a Template

We specifically create a new generator.js to handle the project creation logic

// lib/Generator.js

class Generator {
  constructor (name, targetDir) {// Directory name
    this.name = name;
    // Create the location
    this.targetDir = targetDir;
  }

  // Core creation logic
  create(){}}module.exports = Generator;
Copy the code

Introduce a Generator class in create.js

// lib/create.js.const Generator = require('./Generator')

module.exports = async function (name, options) {
  // Execute the create command

  // Directory selected by the current command line
  const cwd  = process.cwd();
  // Directory address to be created
  const targetAir  = path.join(cwd, name)

  // Does the directory already exist?
  if (fs.existsSync(targetAir)) {
    ...
  }

  // Create the project
  const generator = new Generator(name, targetAir);

  // Start creating the project
  generator.create()
}

Copy the code

Next, write the logic that asks the user to select a template

// lib/Generator.js

const { getRepoList } = require('./http')
const ora = require('ora')
const inquirer = require('inquirer')

// Add loading animation
async function wrapLoading(fn, message, ... args) {
  // Use ORA to initialize, passing in the prompt message message
  const spinner = ora(message);
  // Start loading animation
  spinner.start();

  try {
    // execute the passed method fn
    const result = awaitfn(... args);// The status is changed to successful
    spinner.succeed();
    return result; 
  } catch (error) {
    // The status is changed to failed
    spinner.fail('Request failed, refetch ... ')}}class Generator {
  constructor (name, targetDir) {// Directory name
    this.name = name;
    // Create the location
    this.targetDir = targetDir;
  }

  // Get the template selected by the user
  // 1) Pull template data from remote
  // 2) The user selects the name of his newly downloaded template
  // 3) return The name selected by the user

  async getRepo() {
    // 1) Pull template data from remote
    const repoList = await wrapLoading(getRepoList, 'waiting fetch template');
    if(! repoList)return;

    // Filter the name of the template we need
    const repos = repoList.map(item= > item.name);

    // 2) The user selects the name of his newly downloaded template
    const { repo } = await inquirer.prompt({
      name: 'repo'.type: 'list'.choices: repos,
      message: 'Please choose a template to create project'
    })

    // 3) return The name selected by the user
    return repo;
  }

  // Core creation logic
  // 1) Get the template name
  // 2) Get the tag name
  // 3) Download the template to the template directory
  async create(){

    // 1) Get the template name
    const repo = await this.getRepo()
    
    console.log('User selected, repo=' + repo)
  }
}

module.exports = Generator;
Copy the code

Let’s test it out and see what it looks like, okay

I chose the default vue-template, at this point

The result of successfully getting the template name repo ✌️

3.4 Selecting a Version

The process is the same as 3.3

// lib/generator.js

const { getRepoList, getTagList } = require('./http')...// Add loading animation
async function wrapLoading(fn, message, ... args) {... }class Generator {
  constructor (name, targetDir) {// Directory name
    this.name = name;
    // Create the location
    this.targetDir = targetDir;
  }

  // Get the template selected by the user
  // 1) Pull template data from remote
  // 2) The user selects the name of his newly downloaded template
  // 3) return The name selected by the user

  async getRepo(){... }// Get the version selected by the user
  // 1) Based on the repO result, pull the corresponding tag list remotely
  // 2) The user selects the tag to download
  // 3) return The tag selected by the user

  async getTag(repo) {
    // 1) Based on the repO result, pull the corresponding tag list remotely
    const tags = await wrapLoading(getTagList, 'waiting fetch tag', repo);
    if(! tags)return;
    
    // Filter the tag name we need
    const tagsList = tags.map(item= > item.name);

    // 2) The user selects the tag to download
    const { tag } = await inquirer.prompt({
      name: 'tag'.type: 'list'.choices: tagsList,
      message: 'Place choose a tag to create project'
    })

    // 3) return The tag selected by the user
    return tag
  }

  // Core creation logic
  // 1) Get the template name
  // 2) Get the tag name
  // 3) Download the template to the template directory
  async create(){

    // 1) Get the template name
    const repo = await this.getRepo()

    // 2) Get the tag name
    const tag = await this.getTag(repo)
     
    console.log('User selected, repo=' + repo + ', tag = '+ tag)
  }
}

module.exports = Generator;
Copy the code

To test this, execute zr Create my-project

Once you’ve made your selection, look at the print

This completes the inquiry, and you are ready to download the template

4. Download the remote template

Download the remote template using the Download-Git-repo toolkit, which is actually on the tools menu listed above, but one thing to note when using it is that it does not support Promises. So we need to promise it using the promisify method in the Util module

4.1 Installing Dependencies and Promising

$ npm install download-git-repo --save
Copy the code

Promise processing

// lib/Generator.js.const util = require('util')
const downloadGitRepo = require('download-git-repo') // Promise is not supported

class Generator {
  constructor (name, targetDir){
    ...

    // Make a promise to download-git-repo
    this.downloadGitRepo = util.promisify(downloadGitRepo); }... }Copy the code

4.2 Core Download Function

Next, the logic for the template download section

// lib/Generator.js.const util = require('util')
const path = require('path')
const downloadGitRepo = require('download-git-repo') // Promise is not supported

// Add loading animation
async function wrapLoading(fn, message, ... args) {... }class Generator {
  constructor (name, targetDir){
    ...

    // Make a promise to download-git-repo
    this.downloadGitRepo = util.promisify(downloadGitRepo); }...// Download the remote template
  // 1) Add the download address
  // 2) Call the download method
  async download(repo, tag){

    // 1) Add the download address
    const requestUrl = `zhurong-cli/${repo}${tag?The '#'+tag:' '}`;

    // 2) Call the download method
    await wrapLoading(
      this.downloadGitRepo, // Remote download method
      'waiting download template'.// Load the prompt
      requestUrl, // Parameter 1: download address
      path.resolve(process.cwd(), this.targetDir)) // Parameter 2: creation location
  }

  // Core creation logic
  // 1) Get the template name
  // 2) Get the tag name
  // 3) Download the template to the template directory
  // 4) Template usage tips
  async create(){

    // 1) Get the template name
    const repo = await this.getRepo()

    // 2) Get the tag name
    const tag = await this.getTag(repo)

    // 3) Download the template to the template directory
    await this.download(repo, tag)
    
    // 4) Template usage tips
    console.log(`\r\nSuccessfully created project ${chalk.cyan(this.name)}`)
    console.log(`\r\n  cd ${chalk.cyan(this.name)}`)
    console.log(' npm run dev\r\n')}}module.exports = Generator;
Copy the code

Complete this and a simple scaffold is complete ✅

To test the effect, run zr create my-project

At this point, we can see that the template has been created 👏👏👏

Zhurong - cli ├ ─ bin │ └ ─ cli. Js ├ ─ lib │ ├ ─ the Generator. The js │ ├ ─ the create. Js │ └ ─ HTTP. Js ├ ─ my - project... │ ├─ public │ ├─ favicon. Ico │ ├─ SRC │ ├─ assets │ ├─ ├─ ├─ │ ├─ logo │ └ ─ the HelloWorld. Vue │ │ ├ ─ App. Vue │ │ └ ─ main. Js │ ├ ─ README. Md │ ├ ─ Babel. Config. Js │ └ ─ package. The json ├ ─ README. Md ├ ─ Package - lock. Json └ ─ package. The jsonCopy the code

5. Publish projects

The above are all in the local test, in the actual use of time, may need to publish to the NPM repository, through the NPM global installation, directly to the target directory below to create the project, how to publish?

  1. The first step is to set up a repository on Git
  2. The second step is to improve the configuration in package.json
{
  "name": "zhurong-cli"."version": "1.0.4"."description": ""."main": "index.js"."bin": {
    "zr": "./bin/cli.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "files": [
    "bin"."lib"]."author": {
    "name": "T-Roc"."email": "[email protected]"
  },
  "keywords": [
    "zhurong-cli"."zr".scaffolding]."license": "MIT"."dependencies": {
    "axios": "^ 0.21.1"."chalk": "^ 4.4.1"."commander": "^ 7.2.0"."download-git-repo": "^ 3.0.2." "."figlet": "^ 1.5.0." "."fs-extra": "^ 10.0.0"."inquirer": "^ 8.0.0." "."ora": "^ 5.4.0"}}Copy the code
  1. Step 3: Usenpm publishRelease, update, be careful to change the version number

With that done, let’s go to the NPM website and search 🔍

It is already available so that we can use it through NPM or YARN global installation

Click here to open 👉 zhurong- CLI source code address

Yeoman: A universal scaffolding system

Yeoman, which was first released in 2012, is an efficient, open source Web application scaffolding software meant to streamline the software development process. Scaffolding software is used to coordinate the use of various tools and interfaces in a project to optimize the project generation process. Allows you to create any type of application (Web, Java, Python, C#, etc.).

Yeoman is actually the sum of three tools:

  • Yo — Scaffolding, automatic generation tool
  • Grunt, Gulp — Build tools
  • Bower, NPM — Package management tool

Using Yeoman to build scaffolding is very simple, Yeoman provides yeoman-Generator to quickly generate a scaffold template, we can use various generators to build any type of project, let’s try 🤓

1. Basic use of Yeoman

Yeoman is a build system where the scaffolding we use is Yo 👇

1.1 Installing Yo globally

$ npm install yo --global # or yarn global add yo
Copy the code

1.2 Installing the Corresponding Generator

Yo can be used with different generator-xxx to create corresponding projects, such as generator-webApp, generator-Node, generator-vue, etc. Here we use generator-Node to demonstrate the operation.

$ npm install generator-node --global # or yarn global add generator-node
Copy the code

1.3 Running the Generator through YO

$ mkdir yo-project
$ cd yo-project
$ yo node
Copy the code

So we can quickly set up a Node project with yo + generator-Node, the directory structure is 👇

Yo - Project ├─.EditorConfig ├─.Eslintignore ├─.travis. Yml ├─.yo-rc.json ├─ Readme.md ├─ lib │ ├─ __tests__ │ ├─ ├─ ├─ ├─ ├─ ├─ ├.txtCopy the code

How do I find the generator I need? Generators can be accessed here at 👉

This approach is very simple and convenient, but the problem with it is obvious — it is not flexible enough. After all, different teams use different technology stacks. What if we want to build the project structure we want? Go on to 👇

2. Customize the Generator

A custom Generator is essentially an NPM package that creates a specific structure, such as 👇

generator-xxx ............ Custom project directory ├─ generators............ │ ├ ─ app................ │ ├ ─ index.js........ ├ ─ package.json.......... Module package configuration fileCopy the code

Or 👇

├─ ├─ ├─ download.json generator-xxx ├─ app │ ├─ ├─ download.jsCopy the code

It is important to note that the name of the project must be in the format generator-

for it to be recognized by YO, such as the generator-node example used above.

2.1 Creating a Project

$ mkdir generator-simple # create project
$ cd generator-simple    Enter the project directory
Copy the code

2.2 Initializing NPM

$ npm init # or yarn init
Copy the code

After entering all the way, we have generated package.json, but we need to do a little extra checking:

  • nameProperty value must be “generator-<name>”
  • keywordMust contain yeoman-Generator
  • filesProperty to point to the project’s template directory.

What does package.json look like after we’ve done that

{
  "name": "generator-simple"."version": "1.0.0"."description": ""."main": "index.js"."scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [ 
    "yeoman-generator"]."files": [
    "generators"]."author": "ITEM"."license": "MIT"
}
Copy the code

⚠️ Note: if you are using the second directory structure here, you need to make some changes to 🔧 in package.json

{
  "files": [
    "app"."router"]}Copy the code

2.3 installationyeoman-generator

Yeoman-generator is a generator base class provided by Yeoman to make it easier to create custom generators.

$ npm install yeoman-generator --save # or yarn add yeoman-generator 
Copy the code

2.4 Instructions for using Generator base Classes

Before introducing Generator base classes, let’s implement a simple 🌰

First open the core entry file and edit the following contents: 👇

/ / ~ / generators/app/index, js

// This file acts as a core entry for the Generator
// You need to export a type inherited from the Yeoman Generator
// The Yeoman Generator will automatically invoke some of the lifecycle methods we defined in this type when working
// We can implement some functions in these methods by calling some utility methods provided by the parent class, such as file writing

const Generator = require('yeoman-generator');

module.exports = class extends Generator {
  // add your own methods
  method1() {
    console.log('I am a custom method');
  }
  method2() {
    console.log('I am a custom method2'); }};Copy the code

Once done, we link the project globally via NPM link

$ npm link # or yarn link
Copy the code

Now that we have global access to the Generator-Simple project, let’s give it a try

$ yo simple
Copy the code

Take a look at the console output

I am a custom method1
I am a custom method2
Copy the code

OK, is the result we want 😎

⚠️ Note that the following error occurs if you run Yo Simple

This generator (simple:app) 
requires yeoman-environment at least 3.0.0, current version is 2.10.3,
try reinstalling latest version of 'yo' or use '--ignore-version-check' option
Copy the code

It can be handled like this:

Plan a

# Uninstall the current version
npm uninstall yeoman-generator

Install a lower version of the packageNPM I [email protected]# to perform
yo simple
Copy the code

Scheme 2

# Global install module
npm i -g yeoman-environment

# New execution mode (yoe correct typo)
yoe run simple
Copy the code

From the little 🌰 above we can see that our custom methods are automatically executed sequentially. The Generator base class also provides some sequentially executed methods, similar to the lifecycle. Let’s see what 👇 is

  1. initializing— Initialization method (check status, get configuration, etc.)

  2. prompting— Get user interaction data (this.prompt())

  3. configuringEdit and configure the project configuration file

  4. defaultIf there is any method inside the Generator that does not match the task name of any of the task queues, it will run under the task default

  5. writingFill the preset template

  6. conflicts— Handling conflicts (internal use only)

  7. install— Install dependencies (eg: NPM, Bower)

  8. endLast call, do some clean work

2.5 Start our custom Generator

Using the method provided by Generator, we modify the entry file

/ / ~ / generators/app/index, js

const Generator = require('yeoman-generator');

module.exports = class extends Generator {
  // Yo calls this method automatically
  writing () {
    // We use the FS module provided by Generator to try to write a file to the directory
    this.fs.write(
      // destinationPath() based on the project address
      this.destinationPath('temp.txt'), // Write the address
      Math.random().toString() // Write the content)}};Copy the code

Let’s run it

$ yo simple
Copy the code

At this point, the console prints create Temp. TXT, and let’s print out the directory structure

Gene-simple ├─ ├─ ├─ ├─ download.txt.............. Files created in WritingCopy the code

Open the newly created temp. TXT and take a look

0.8115477932475306
Copy the code

You can see that a random number is written to the file.

In practice, we need to create multiple files from the template, which is what we need to do 👇

First, create a template file directory/generators/app/templates /, and a new template files in the folder temp. HTML

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <! Yo supports EJS syntax -->
  <title><%= title %></title>
</head>
<body>
  <% if (success) { %>
    <h1>Here is the template file <%= title %></h1>The < %} % ></body>
</html>
Copy the code

Then, modify the entry file 👇

/ / ~ / generators/app/index, js

const Generator = require('yeoman-generator');

module.exports = class extends Generator {
  // Yo calls this method automatically
  writing () {
    // We use the FS module provided by Generator to try to write a file to the directory
    // this.fs.write(
    // this.destinationPath('temp.txt'),
    // Math.random().toString()
    // )

    // The template file path, which by default points to templates
    const tempPath = this.templatePath('temp.html')
    // Outputs the destination path
    const output = this.destinationPath('index.html')
    // Template data context
    const context = { title: 'Hello ITEM ~'.success: true}

    this.fs.copyTpl(tempPath, output, context)
  }
};
Copy the code

When you’re done, yo Simple runs it so that we have index.html in the root directory. Open it up and take a look at 🤓

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <! -- Support EJS syntax -->
  <title>Hello ITEM ~</title>
</head>
<body>
  
    <h1>Here is the template file Hello ITEM ~</h1>

</body>
</html>
Copy the code

Variables written by ejS have been successfully replaced by data ✌️

Next, we will learn how to get some user-defined data through command line interaction, such as: project name, version number, and so on.

This requires the Promting provided by the Generator to handle some interaction on the command line

/ / ~ / generators/app/index, js

const Generator = require('yeoman-generator');

module.exports = class extends Generator {
  // In this method, you can invoke the parent's prompt() method to make command line queries with the user
  prompting(){
    return this.prompt([
      {
        type: 'input'.// Interaction type
        name: 'name'.message: 'Your project name'.// Ask for information
        default: this.appname // Project directory name, here is generator-simple
      }
    ])
    .then(answers= > {
      console.log(answers) // Prints the input
      this.answers = answers // Save the result to be used later})}// Yo calls this method automaticallywriting () { ...... }};Copy the code

After saving, run Yo Simple again

We see the command line asking for Your Project name? After user input is completed, we get anwsers so that we can use this result in the following process.

/ / ~ / generators/app/index, js.// Template data context
 writing () {
    ...
    // Template data context
    const context = { title: this.answers.name, success: true}

    this.fs.copyTpl(tempPath, output, context)
  }
...
Copy the code

Run Yo Simple again to see the index.html output

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <! -- Support EJS syntax -->
  <title>my-project</title>
</head>
<body>
  
    <h1>Here is the template file my-project</h1>
  
</body>
</html>
Copy the code

We can see that the user input {name: ‘my-project’} is already displayed in our index.html 👌

Click here to open 👉 generator-simple source code address

That’s Yeoman, but let’s take a look at another scaffolding tool — Plop 👇

Plop: A small and beautiful scaffold tool

Plop is small in size and lightweight, and the United States is easy to use

For more information 👉 plop usage documentation

We can integrate it directly into the project to solve the repetitive task of standardizing the creation. Here is a small example

We have agreed on a component creation specification:

  • Component names use large humps
  • Styles need to be wrung out separately
  • Accompanying documentation is required

The use process of PLOP can be roughly broken down into:

  1. Add configuration file plopfile.js in plop installation
  2. Edit a plop configuration file
  3. Creating a template File
  4. Executing a Creation Task

Let’s go to coding

1. Install the plop

We first initialize a Vue project with our Zhurong – CLI

#Global installation
$ npm install zhurong-cli -g 
#Create a VUE project
$ zr create plop-demo
Copy the code

Here, ploP is integrated directly into the project for unified use by the team

$ npm install plop --save-dev
Copy the code

Create the plop configuration file plopfile.js in the project directory

2. Edit the plop configuration file

// ./plopfile.js

module.exports = plop= > {
  plop.setGenerator('component', {
    / / description
    description: 'create a component'.// Ask for the component name
    prompts: [{type: 'input'.name: 'name'.message: 'Your component name'.default: 'MyComponent'}].// Get the action that follows the answer
    actions: [
      // Each object is an action
      {
        type: 'add'.// adds the file
        // The path and name of the created file
        // name is the result entered by the user, using the variable {{}}
        // properCase: Plop has a built-in method that converts name to a large hump
        path: 'src/components/{{ properCase name }}/index.vue'.// Template file address
        templateFile: 'plop-templates/component.vue.hbs'
      },
      {
        type: 'add'.path: 'src/components/{{ properCase name }}/index.scss'.templateFile: 'plop-templates/component.scss.hbs'
      },
      {
        type: 'add'.path: 'src/components/{{ properCase name }}/README.md'.templateFile: 'plop-templates/README.md.hbs'})}}]Copy the code

The properCase method is used to convert the name to a large hump. Other formats include 👇

  • camelCase: changeFormatToThis
  • snakeCase: change_format_to_this
  • dashCase/kebabCase: change-format-to-this
  • dotCase: change.format.to.this
  • pathCase: change/format/to/this
  • properCase/pascalCase: ChangeFormatToThis
  • lowerCase: change format to this
  • sentenceCase: Change format to this,
  • constantCase: CHANGE_FORMAT_TO_THIS
  • titleCase: Change Format To This

We see that the template file is referenced above, but we haven’t actually created it yet, so let’s create it

3. Create a template file

Create the Plop-Templates folder under the project folder to create the corresponding template files

The plop - templates ├ ─ README. Md. HBS... Document Template ├─ Component.scs.hbs.......... ├ ─ component.vue. HBS........... Component templatesCopy the code

Template engine we use is Handlebars, more syntax description 👉 Handlebars Chinese

Edit component. SCSS. HBS

{{! -- ./plop-templates/component.scss.hbs --}}
{{! -- dashCase/kebabCase: change-format-to-this --}}
{{! -- name: Input template name --}}

.{{ dashCase name }}{}Copy the code

Edit component. Vue. HBS

{{! -- ./plop-templates/component.vue.hbs --}}

<template>
  <div class="{{ dashCase name }}">{{ name }}</div>
</template>

<script>
  export default {
    name: '{{ properCase name }}'}</script>

<style lang="scss">
@import "./index.scss";

</style>
Copy the code

Edit the README. Md. HBS

{{! -- ./plop-templates/README.md.hbs --}}Here are the components{{ name }}Instructions for use ofCopy the code

Supplementary notes:

  • Here the template is the simplest implementation, the actual production can be based on the need to enrich the template content
  • DashCase and properCase in the template are the display rules for changing the name command, which have been listed above
    • dashCase: becomes a horizontal link aA-bb-cc
    • properCase: Becomes large hump AaBbCc
    • .
  • Variables are used in Handlebars{{}}The parcel

4. Perform the creation task

Open the package. The json

// add a command to scripts."scripts": {..."plop": "plop"},...Copy the code

At this point we can use NPM Run plop to create the component

Soon the component is created ✅

Now look under the Components folder

The components ├ ─ MyApp │ ├ ─ README. Md │ ├ ─ index. The SCSS │ └ ─ index. The vue └ ─ the HelloWorld. VueCopy the code

MyApp component has been created. Let’s open the file inside

Open the MyApp/index. Vue

<template>
  <div class="my-app">my-app</div>
</template>

<script>
  export default {
    name: 'MyApp',}</script>

<style lang="scss">
@import "./index.scss";

</style>
Copy the code

Open the MyApp/index. SCSS

.my-app{}Copy the code

Open the MyApp/README. Md

Here are the instructions for the component my-appCopy the code

Click here to open 👉 Plop-demo source code address

Write at the end

Do not know everyone read this article, learn to waste 😂

This article sorted out for a long time, I hope to help you learn 😁

In addition, WE also hope that you can like the comment attention to support, your support is the power of writing 😘

Be warned, the next article will cover 👉 packaging knowledge related to build tools


Reference article:

Github.com/CodeLittleP… Cli.vuejs.org/zh/guide/cr… Yeoman. IO/authoring/I… www.jianshu.com/p/93211004c…