preface

Under article is simply that the front-end scaffolding code, and the implementation mentality, has recently increased the function of the part, this is the last article, plus the are described in a unified, recently saw the nuggets articles of others, found that only vernacular is the best, can let a person quickly understand the language, so this time, Just write a front end scaffold in plain English.

start

What is front-end scaffolding

1. What is

  • Scaffolding is to ensure the smooth construction process and set up the work platform. (Baidu’s definition of scaffolding)
  • A directory template that helps me quickly generate new projects
  • Can improve my development efficiency and development comfort

2. What are the benefits

  • It is to unify the technology stack of each line of business and formulate specifications, so that problems can be solved uniformly and upgrades and iterations can be carried out synchronously
  • To improve efficiency, on the basis of a unified basis, provide more tools to improve efficiency. This efficiency is not only the development efficiency, but also the efficiency of the whole process from development to online, such as the encapsulation of some business components, the release system to improve online efficiency, various utils and so on

Start coding

In this paper, the test branch of code at https://github.com/amazingliyuzhao/cli-demo

1. Project Catalog

Look at the macaw – hellow. Js

console.log('hello, cli')
Copy the code

And then I’m going to run it from the command line

node ./bin/macaw-hellow.js
Copy the code

You can see the print statement

So far we have written the execution order. And then we’re going to add a little bit of functionality, first of all we’re going to look at other scaffolding like vue-cli when you create a file, vue create projectName, which is basically a custom command and then + projectName

We noticed that they didn’t have the +node command in front of them, but they still executed the statement. This is because the github official documentation for commander. Js can be accessed directly using the commander. Js tool written by TJ.

2. commander.js

Get project name

Step one, introduce

const program = require('commander')
Copy the code

Let’s change our Hellow file

program.usage('<project-name>')
        .parse(process.argv) // Add this to get the project name
// Get the project name based on the input
let projectName = program.rawArgs[2] // Get the project name
console.log(projectName)
Copy the code

Execute the hellow file

node ./bin/macaw-hello.js testDemo
Copy the code

You can see the print

What to do next with the name, of course, is to create our project based on the name.

Project name tolerance

But for fault tolerance, what if there’s no name

Help () is equivalent to executing the –help option of the command, displaying the help message, which is a command option built into COMMANDER

if(! projectName) {// project-name Mandatory If no name is entered, run helphelp
  program.help()// This is equivalent to executing the --help option of the command to display the help message, which is a built-in commander command option
  return
}
Copy the code

Creating a folder

There is a lot of logic for creating folders, so we can sort it out first

Logic for creating files

  1. If the project name is the same as the root directory

    Generate files directly in the root directory

  2. If create project name and root directory are inconsistent

    Create the project directory directly under the root directory and generate the widgets

  3. If a file with the same project name already exists in the root directory

    Ask the user whether to overwrite

Take a look at the processing code

#! /usr/bin/env node

const program = require('commander')
const path = require('path')
const fs = require('fs')
const glob = require('glob') // npm i glob -D
const download = require('.. /lib/download') // Download the configuration
const inquirer = require('inquirer') // Import on demand
const logSymbols = require("log-symbols");
const chalk = require('chalk')
const remove = require('.. /lib/remove') // Delete file js
const generator = require('.. /lib/generator')// Template insertion
const CFonts = require('cfonts');

program.usage('<project-name>')
            .parse(process.argv) // Add this to get the project name

// Get the project name based on the input
// console.log(program)
let projectName = program.rawArgs[2] // Get the project name

if(! projectName) {// project-name Mandatory If no name is entered, run helphelp
  program.help()// This is equivalent to executing the --help option of the command to display the help message, which is a built-in commander command option
  return
}

// The current directory is empty. If the name of the current directory is the same as project-name, the project is directly created in the current directory. Otherwise, the project root directory is created in the current directory with project-name as the name
// The current directory is not empty. If there is no directory with the same name as project-name, create a directory with project-name as the root directory of the project. Otherwise, a message is displayed indicating that the project already exists.
// process.cwd() is the directory where the node command is currently executed
//__dirname is the address of the js file being executed -- the directory in which the file resides
const list = glob.sync(The '*')  // Traverses the current directory, array type
let next = undefined;
let rootName = path.basename(process.cwd());
if (list.length) {  // If the current directory is not empty
  if (list.some(n= > {
    const fileName = path.resolve(process.cwd(), n);
    const isDir = fs.statSync(fileName).isDirectory();
    return projectName === n && isDir // Find a file with the same name as the current directory file{}))// If the file already exists
    next = inquirer.prompt([
      {
        name:"isRemovePro".message:` project${projectName}Already exists, whether to overwrite file '.type: 'confirm'.default: true
      }
    ]).then(answer= >{
        if(answer.isRemovePro){
          remove(path.resolve(process.cwd(), projectName))
          rootName = projectName;
          return Promise.resolve(projectName);
        }else{
          console.log("Stop creating")
          next = undefined
          // return;}}}})else if (rootName === projectName) {  // If the file name is the same as the root directory name
  rootName = '. ';
  next = inquirer.prompt([
    {
      name: 'buildInCurrent'.message: 'The current directory is empty and the directory name is the same as the project name. Do you want to create a new project directly under the current directory? '.type: 'confirm'.default: true
    }
  ]).then(answer= > {
    console.log(answer.buildInCurrent)
    return Promise.resolve(answer.buildInCurrent ? '. ' : projectName)
  })
} else {
  rootName = projectName;
  next = Promise.resolve(projectName) // Return the resole function, passing projectName
}

next && go()

function go () {
  // reserved, processing subcommands
}

Copy the code

We talked about asking the user, so how do we interact with the user at the command line

3. Inquirer. Js — a tool for users to interact with the command line

This library can be used to interact with the user at the command line. Let’s look at the scenario

  • Type: Indicates the type of the query. The options are input, confirm, list, rawlist, expand, checkbox, password, and editor.
  • Message: a description of the problem;
  • Default: the default value.
  • Choices: list options, available under certain types, and containing a separator;
  • Validate: verifies the user’s answer;
  • Filter: The user answers are filtered and the processed value is returned.
  • Transformer: Process the display of the user’s answers (e.g. change the font or background color) without affecting the content of the final answer;
  • When: Judge whether the current question needs to be answered based on the answers to the previous questions.
  • PageSize: Changes the number of rendered lines for certain types;
  • Prefix: Modify the default message prefix.
  • Suffix: Change the default message suffix.

The general framework is

const inquirer = require('inquirer');/ / introduction

const promptList = [];// User interaction

inquirer.prompt(promptList).then(answers= > {
    console.log("Last output")
    console.log(answers); // The result returned
})

Copy the code

Let’s look at them one by one

  1. The first is input in type

Copy the code
const promptList = [{
  type: 'input'.message: 'Set a user name :'.name: 'name'.default: "test_user" / / the default value}, {type: 'input'.message: 'Please enter mobile phone number :'.name: 'phone'.validate: function(val) {
      if(! val.match(/\d{11}/g)) { // Check digit
          return "Please enter 11 digits.";
      }
      return true}}];Copy the code

Effect of

  1. Type – > confirm
const promptList = [{
  type: "confirm".message: "Is the first condition true?".name: "firstChange".prefix: "The prefix of the first condition"}, {type: "confirm".message: "Is the second condition true (dependent on the first condition)?".name: "secondChange".suffix: "Suffixes for the second condition.".when: function(answers) { // The current question will only be asked if firstChange is true
      return answers.firstChange
  }
}];
Copy the code

Effect of

const promptList = [{
  type: 'list'.message: 'Please choose a fruit :'.name: 'fruit'.choices: [
      "🍎 Apple"."🍐 Pear"."🍌 Banana"].filter: function (val) { // Use filter to change the answer to lowercase
      returnval.toLowerCase(); }}];Copy the code

Effect of

  1. type–>list
const promptList = [{
  type: "expand".message: "Please choose a fruit:".name: "fruit".choices: [{key: "a".name: "Apple".value: "apple"
      },
      {
          key: "O".name: "Orange".value: "orange"
      },
      {
          key: "p".name: "Pear".value: "pear"}}]];Copy the code

Key is a prompt, and key can only be a simple letter. H defaults to help, and selecting H shows all the options

Effect of

const promptList = [{
  type: "checkbox".message: "Type.".name: "color".choices: [
    new inquirer.Separator("Brand - - -"), // Add a delimiter
      {
        name: "Audi"}, {name: "Mercedes"}, {name: "Red flag",},new inquirer.Separator("- color -"), // Add a delimiter
      {
          name: "blur".checked: true // It is selected by default
      },
      {
        name: "red"}, {name: "green"}}]];Copy the code

  1. Enter the ciphertext value in type –>password
const promptList = [{
  type: "password".// Enter the password in ciphertext
  message: "Please enter your password:".name: "pwd"
}];
Copy the code

  1. Enter ciphertext in type –> Editor
const promptList = [{
  type: "editor".message: "Please enter remarks:".name: "editor"
}];
Copy the code

Press Enter to write the remarks, esc+:wq to save the configuration and exit

That leaves us with the types of inquire commonly used, and let’s move on to our project

So going back to the previous flow, we’ve gone to create a file and after we’ve done the logic of creating a file we execute next and go,

 next = Promise.resolve(projectName) // Return the resole function, passing projectName
Copy the code

Create our directory in the go function

function go () {
    next.then(projectRoot= > { //
        if(projectRoot ! = ='. ') {
          fs.mkdirSync(projectRoot)// Create a directory file}}}Copy the code

Let’s take a look at the details. I’m executing amaz-test + project name, where amaz is my custom command. Let’s look at the configuration of our package.json file

{
  "name": "liyuzhaocli-2"."version": "1.0.0"."description": "Amz Scaffolding 1.0"."bin": { 
    "macaw": "./bin/macaw.js"."amaz": "./bin/macaw-init.js"."amaz-test": "./bin/macaw-test.js"
  },
  "main": "./bin/macaw-hellow.js"."scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "cli"]."author": "amaizngli"."license": "ISC"."dependencies": {
    "cfonts": "^ 2.4.5"."commander": "^ 4.0.0"."download-git-repo": "^ 3.0.2." "."glob": "^ 7.1.5." "."handlebars": "^ 4.5.1." "."inquirer": "^ 7.0.0." "."metalsmith": "^ 2.3.0." "."ora": "^ 4.0.2." "}}Copy the code

You can see below bin is our custom command

If you want to simulate the effect of publishing NPM, you can link to global directly from the local NPM link so that you can use our commands on your computer.

But now that we’ve just created an empty directory and need to fill it with our templates, we need another tool

4. Download-git-repo — a tool to download remote Git files

First we will create a download.js file in the lib directory

www.npmjs.com/package/dow…

Let’s take a look at the code

const download = require('download-git-repo')
const path = require("path")
const ora = require('ora')

module.exports = function (target) {
  target = path.join(target || '. '.'.download-temp');
  return new Promise(function (res, rej) {
    // You can set the url for downloading according to the template address. Note that if git is used, the branch behind the URL cannot be ignored
    // The format is the name/address without.git but with the # branch
    // let url = 'amazingliyuzhao/CliTestGit#test'
    let url = 'amazingliyuzhao/cli-template#test'
    const spinner = ora('Downloading project template, source address:${url}`)
    spinner.start();

    download(url, target, { clone: false }, function (err) { // clone false to false
      if (err) {
        spinner.fail()
        rej(err)
      }
      else {
        // The downloaded template is stored in a temporary path. After the download is complete, you can notify this temporary path down for subsequent processing
        spinner.succeed()
        res(target)
      }
    })
  })
}
Copy the code

Ora is a code beautification library

Note that a big hole is the url format, our normal git address is github.com/amaz/cli-de…

Use amaz/cli-demo#test when using this tool. # is followed by the corresponding branch name. Master must also write without default

If you want to update the template, you just need to update the remote Git repository. Each time you pull the template, it will be the latest code.

Finally, it is introduced in our macaw-init file

const download = require('.. /lib/download') // Download the configuration
Copy the code

And then I rewrite the go function

function go () {
      next.then(projectRoot= > { 
        if(projectRoot ! = ='. ') {
          fs.mkdirSync(projectRoot) // Create a file
        }
        return download(projectRoot).then(target= > {// Download the template
          return {
            projectRoot,
            downloadTemp: target
          }
        })
      })
  }
Copy the code

Download the remote template

Well, the problem came again. After downloading the template, all the information was written in the template. The version information, project name and version number need to be modified by ourselves in the project, which seems not as customizable as we expected

So our expectation is to customize our information through user input. When it comes to user interaction we first think of inquire.js tools mentioned above, but how do we populate our templates with what we get from users?

Now we’re going to use a new tool

Metalsmith – a tool for downloading remote Git files

Metalsmith. IO/but to be honest, it’s a little hard to read.

Recommend a site www.kutu66.com//GitHub/art…

Create a new generator file under lib,

The installation

npm i handlebars metalsmith -D
Copy the code
// // npm i handlebars metalsmith -D
const rm = require('rimraf').sync // Package the rm -rf command as a package to delete files and folders, regardless of whether the folder is empty
const Metalsmith = require('metalsmith') / / the interpolation
const Handlebars = require('handlebars') / / template
const remove = require(".. /lib/remove") / / delete
const fs = require("fs")
const path = require("path")

module.exports = function (context) {
  let metadata = context.metadata; // User-defined information
  let src = context.downloadTemp; // Temporarily store the file directory
  let dest = '/' + context.projectRoot; // The root of the project

  if(! src) {return Promise.reject(new Error('invalid source:${src}`))}return new Promise((resolve, reject) = > {
    const metalsmith = Metalsmith(process.cwd())
      .metadata(metadata) // Put the user input information into
      .clean(false)
      .source(src)
      .destination(dest);
    
    metalsmith.use((files, metalsmith, done) = > {
      const meta = metalsmith.metadata()
      Object.keys(files).forEach(fileName= > {
        if(fileName.split(".").pop() ! ="png") {const t = files[fileName].contents.toString()
          files[fileName].contents = new Buffer.from(Handlebars.compile(t)(meta),'UTF-8')
        }
      })
      done()
    }).build(err= >{ remove(src); err ? reject(err) : resolve(context); })})}Copy the code

For specific usage methods, you can see the website (too long did not see, I also forgot ~, later to add details).

Basically, this is a file to achieve interpolation, use this tool at the same time also need a template library, here I choose Handlebars, specific syntax can be searched, and EJS and other template JS files, but the syntax may be different.

The idea is to go through all the files in the template, and then convert the contents of the file to a string, and then find the content of the template string and process it, but there is a catch, the tool doesn’t seem to recognize the image, so I put the code above to filter the image.

Then add our code to macaw-init.js

const generator = require('.. /lib/generator')// Template insertion
Copy the code

So let’s rewrite our go function

function go () {
  next.then(projectRoot= > { //
    if(projectRoot ! = ='. ') {
      fs.mkdirSync(projectRoot)
    }
    CFonts.say('amazing', {
      font: 'block'.// define the font face
      align: 'left'.// define text alignment
      colors: ['#f80'].// define all colors
      background: 'transparent'.// define the background color, you can also use `backgroundColor` here as key
      letterSpacing: 1.// define letter spacing
      lineHeight: 1.// define the line height
      space: true.// define if the output text should have empty lines on top and on the bottom
      maxLength: '0'.// define how many character can be on one line
    });
    return download(projectRoot).then(target= > {
      return {
        projectRoot,
        downloadTemp: target
      }
    })
  })
  .then(context= > {
    // console.log(context)
    return inquirer.prompt([
      {
        name: 'projectName'.message: 'Project name'.default: context.name
      }, {
        name: 'projectVersion'.message: 'Project Version number'.default: '1.0.0'
      }, {
        name: 'projectDescription'.message: 'Introduction to the Project'.default: `A project named ${context.projectRoot}`}, {name: 'isElement'.message: 'Use element or not'.default: "No"}, {name: 'isEslint'.message: 'Whether to use isEslint'.default: "No",
      }
    ]).then(answers= > { // Optional callback function
      let v = answers.isElement.toUpperCase();
      answers.isElement = v === "YES" || v === "Y";
      let iseslint = answers.isEslint.toUpperCase();
      answers.isEslint = iseslint === "YES" || iseslint === "Y";
      return {
        ...context,
        metadata: {
          ...answers
        }
      }
    })
  }).then(context= > {
    console.log("Generate file")
    console.log(context)
     // Delete the temporary folder and move the file to the target directory
     return generator(context); // Interpolate
  }).then(context= > {
    // Success is shown in green to give positive feedback
    console.log(logSymbols.success, chalk.green('created successfully :)'))
    console.log(chalk.green('cd ' + context.projectRoot + '\nnpm install\nnpm start'))
  }).catch(err= > {
    console.error(err)
     // Failed with red, enhanced hint
     console.log(err);
     console.error(logSymbols.error, chalk.red('failed to create:${err.message}`))})}Copy the code

You can see our user interaction with inquire.js and then interpolate the user responses after processing them

Website www.npmjs.com/package/cfo…

There are all kinds of fancy effects to explore

  CFonts.say('amazing', {
      font: 'block'.// define the font face
      align: 'left'.// define text alignment
      colors: ['#f80'].// define all colors
      background: 'transparent'.// define the background color, you can also use `backgroundColor` here as key
      letterSpacing: 1.// define letter spacing
      lineHeight: 1.// define the line height
      space: true.// define if the output text should have empty lines on top and on the bottom
      maxLength: '0'.// define how many character can be on one line
    });
Copy the code

Now that we have pulled the template from the remote end and implemented the interpolation, what do we do with the template?

6. Interpolate in the template

Let’s take a look at the processing code

{ "name": "{{projectName}}", "version": "{{projectVersion}}", "description": "{{projectDescription}}", "author": "lyz", "private": true, "scripts": { "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", "start": "npm run dev", "lint": "eslint --ext .js,.vue src", "build": "node build/build.js" }, "dependencies": { {{#if isElement}} "element-ui": "^ 2.13.0", {{/ if}} "vue" : "^ 2.5.2", "vue - the router" : "^ 3.0.1", "node - sass" : "^ 4.13.0"}, "devDependencies" : {" autoprefixer ":" ^ 7.1.2 ", "Babel - core" : "^ 6.22.1", "Babel - eslint" : "^ 8.2.1", "Babel - helper - vue - JSX - merge - props" : "^ 2.0.3", "Babel - loader" : "^ 7.1.1", "Babel - plugin - syntax - JSX" : "^ 6.18.0", "Babel - plugin - transform - runtime" : "^ 6.22.0", "Babel - plugin - transform - vue - JSX" : "^ 3.5.0", "Babel - preset - env" : "^ 1.3.2", "Babel - preset - stage - 2" : "^ 6.22.0", "chalk" : "^ 2.0.1", "copy - webpack - plugin" : "^ 4.0.1", "CSS - loader" : "^ 0.28.0", "eslint" : "^ 4.15.0 eslint - config -", "standard" : "^ 10.2.1", "eslint - friendly - the formatter" : "^ 3.0.0", "eslint - loader" : "^ 1.7.1 eslint", "- the plugin - import" : "^ 2.7.0", "eslint - plugin - node" : "^ 5.2.0", "eslint - plugin - promise" : "^ 3.4.0 eslint - plugin -", "standard" : "^ 3.0.1", "eslint - plugin - vue" : "^ 4.0.0", "extract - text - webpack - plugin" : "^ 3.0.0 file -", "loader" : "^ 1.1.4", "friendly - errors - webpack - plugin" : "^ 1.6.1", "HTML - webpack - plugin" : "^ 2.30.1 node", "- the notifier" : "^ 5.1.2", "optimize - CSS - assets - webpack - plugin" : "^ 3.2.0", "ora" : "^ 1.2.0", "portfinder" : "^ 1.0.13 postcss -", "import" : "^ 11.0.0", "postcss - loader" : "^ mid-atlantic moved", "postcss - url" : "^ 7.2.1", "rimraf" : "^" server, "sass - loader" : "^ 7.3.1," "semver" : "^ 5.3.0", "shelljs" : "^ 0.7.6", "uglifyjs - webpack - plugin" : "^ 1.1.1", "url - loader" : "^ 0.5.8", "vue - loader" : "^ 13.3.0", "vue - style - loader" : "^ 3.0.1", "vue - the template - the compiler" : "^ 2.5.2 webpack", ""," ^ 3.6.0 ", "webpack - bundle - analyzer" : "^ 2.9.0", "webpack - dev - server" : "^ 2.9.1", "webpack - merge" : "^ 4.1.0"}, "engines" : {" node ":" > = 6.0.0 ", "NPM" : "> = 3.0.0"}, "browserslist" : [ "> 1%", "last 2 versions", "not ie <= 8" ] }Copy the code

As you can see, our template syntax {{}} and the following {{#if isElement}}{{/if}} are handlebus syntax that will be automatically parsed when encountered in the traversal file by the Metalsmith tool. Insert user input processing into our template. Here, I choose whether to configure Element and whether to configure ESLint by selecting whether to load the corresponding dependency packages in Packagejson

7. More extensions

In fact, so far, we can write a micro-customization template, but our interpolation is done in the code, but if we design to the file level of the configuration of how to add and delete, the current idea is to use the.gitignore file, add or remove files that are not needed from the file. Again, using our interpolation tool. This module will be supplemented later,

When it comes to changing the entire framework, such as a Vue and a React, it is recommended to add user queries when downloading templates, and then maintain multiple Git repositories on our remote site, downloading templates for different repositories according to the user’s choice.

conclusion

It may take more than a week to finish this article. Every time I open my mind again, THERE may be some omissions. I hope to correct any mistakes I find in the comments section.