Create custom scaffolding

Description of Requirement

Every time you start a new project, you need to build the architecture from scratch, or just copy and paste the previous project and modify it.
Of course, sometimes those who are a little more diligent will make a basic template and put it locally or on github, and clone it directly when needed.
But its convenience and versatility are extremely poor, so I started thinking why not make a command line tool similar to vue-cli (I just have time now~)

Basic functions of scaffolding

1. Interactively ask users questions through the command line.
2. Select different templates or generate different files based on the user's answers.

Basic tools for scaffolding construction

commander 可以自定义一些命令行指令,在输入自定义的命令行的时候,会去执行相应的操作
npm install commander


inquirer 可以在命令行询问用户问题,并且可以记录用户回答选择的结果
npm install inquirer


fs-extra 是fs的一个扩展,提供了非常多的便利API,并且继承了fs所有方法和为fs方法添加了promise的支持。
npm install fs-extra


chalk 可以美化终端的输出
npm install [email protected]


figlet 可以在终端输出logo
npm install figlet


ora 控制台的loading样式
npm install ora


download-git-repo 下载远程模板
npm install download-git-repo

Build process

1. First create a folder and initialize the package.json file

mkdir zyq_fronted_cli

cd zyq_fronted_cli

npm init

2. Create the folder bin to place the entry file of the program

zyq_fronted_cli      
├─ bin        
└─ package.json 

3. Create the folder lib to put some tool functions

zyq_fronted_cli      
├─ bin
├─ lib        
└─ package.json 

4. Create the cli.js file in the bin folder

zyq_fronted_cli      
├─ bin
│  ├─ cli.js   
├─ lib        
└─ package.json 

5. Specify the entry file of the program in the package.json file as the cli.js file in the bin folder.

package.json 文件

{
    
    
  "name": "zyq_fronted_cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "bin": {
    
    
    "zyq_fronted": "./bin/cli.js"
  },
  "scripts": {
    
    
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    
    
    "chalk": "^4.1.0",
    "commander": "^10.0.1",
    "figlet": "^1.6.0",
    "fs-extra": "^11.1.1"
  }
}

6. Download and install the commander custom command line command package

npm install commander

7. Introduce commander into cli.js in the bin folder
. Note: If there is a # at the beginning of the file! If so, then this file will be executed as an executable file, which is a file that can be loaded and executed by the operating system. If it contains this symbol, it means that the file can be run as a script.
/usr/bin/env node means that this file is executed using node (it will go to the env environment variable in the user's installation root directory to find node (/usr/bin/env node) and then use node to execute the entire script file)

#! /usr/bin/env node

const commander = require('commander')

commander
.version('0.1.0')
.command('create <project name>')
.discription('create a new project')
.action(res => {
    
    
  console.log(res)
})

commander.parse()

8. Link the current project (zyq_fronted_cli) to the global

npm link

9. Install chalk and figlet and introduce them in cli.js in the bin folder for customizing fonts and colors.

npm install [email protected]
npm install figlet
#! /usr/bin/env node

const commander = require('commander') // 自定义指令

// 自定义指令
commander
  .version('0.1.0')
  .command('create <project_name>')
  .description('create a new project')
  .action(res => {
    
    
    console.log(res)
  })




const chalk = require('chalk') // chalk 改变颜色
const figlet = require('figlet') // figlet 改变字体

commander.on('--help', () => {
    
     // 监听 --help 执行
    
    console.log('\r\n' + figlet.textSync('ZYQ_FRONTED_CLI', {
    
    
      font: 'Ghost',
      horizontalLayout: 'default',
      verticalLayout: 'default',
      width: 500,
      whitespaceBreak: true
    }))

    // 新增说明信息
    console.log(`\r\nRun ${
     
     chalk.cyan(`zyq_fronted <command> --help`)} for detailed usage of given command\r\n`)
  })

commander.parse()

10. Create a create.js file in the lib folder to write the logic required to create the file.

zyq_fronted_cli      
├─ bin
│  ├─ cli.js   
├─ lib        
│  ├─ create.js  
└─ package.json 
create.js 文件
module.exports =  async function (name, option){
    
    
  console.log('项目名称以及配置项:', name, option)
}

11. When creating a project, ask the user whether they need to force overwrite existing files (add the --force option to the cli.js file to implement this requirement, and modify the logic of the create.js file). When creating a project (zyq_fronted create myproject
-- force or zyq_fronted create myproject -f), ask the user whether they need to force overwrite existing files

cli.js 文件


#! /usr/bin/env node


const commander = require('commander') // 自定义指令
const create = require('../lib/create.js')

commander
  .version('0.1.0')
  .command('create <project_name>')
  .description('create a new project')
  .option('-f --force', 'overwrite target directory if it exist')
  .action((name, option) => {
    console.log(name, option)
    create(name, option)
  })



  const chalk = require('chalk') // chalk 改变颜色
  const figlet = require('figlet') // figlet 改变字体
  
  commander.on('--help', () => { // 监听 --help 执行
    
    console.log('\r\n' + figlet.textSync('ZYQ_FRONTED_CLI', {
      font: 'Ghost',
      horizontalLayout: 'default',
      verticalLayout: 'default',
      width: 500,
      whitespaceBreak: true
    }))

    // 新增说明信息
    console.log(`\r\nRun ${
     
     chalk.cyan(`zyq_fronted <command> --help`)} for detailed usage of given command\r\n`)
  })

commander.parse()
create.js 文件


module.exports = async function (name, option){
    
    
  const path = require('path')
  const fs = require('fs-extra') // npm install fs-extra
  const cwd = process.cwd() // 当前命令行选择的目录
  const targeCwd = path.join(cwd, name) // 需要创建的目录地址
    
  // 判断是否存在该目录
  if (fs.existsSync(targetCwd)) {
    
     // 目录存在

    if (options.force) {
    
     // 是否强制创建
      console.log('进行强制创建')
    } else {
    
    
      console.log('询问用户是否强制创建')
    }
  } else {
    
     // 目录不存在
    console.log('目录不存在,进行强制创建')
  }


  console.log('项目名称以及配置项:', name, options)
}

12. Install the inquirer and use the inquirer to obtain the interaction information between the terminal and the user.
The inquirer can ask the user questions on the command line, and can also remember the user's choices on the command line.

npm install --save inquirer@^8.0.0

13. Modify the logic of the create.js file (when creating a project is not mandatory, if the project exists, ask the user whether to overwrite the project; when creating a project is not mandatory, create the project directly if the project does not exist; when it is mandatory When creating a project, the project is forced to be created whether it exists or not;)

create.js 文件

module.exports =  async function (name, options){
    
    
  const path = require('path')
  const fs = require('fs-extra')
  const inquirer = require('inquirer')
  
  const cwd  = process.cwd() // 当前命令行选择的目录
  const targetCwd = path.join(cwd, name) // 需要创建的目录地址
  console.log(cwd,targetCwd)
  
  // 判断是否存在该目录
  if (fs.existsSync(targetCwd)) {
    
     // 目录存在

    if (options.force) {
    
     // 是否强制创建

      console.log('进行强制创建')
      // 移除原来存在的项目
      await fs.remove(targetCwd)

    } else {
    
    

      console.log('询问用户是否强制创建')

      // 询问用户是否强制创建项目
      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 {
    
    
        console.log('移除存在的文件')
        await fs.remove(targetCwd)
      }

    }
  } else {
    
     // 目录不存在
    console.log('目录不存在,进行强制创建')
  }


  console.log('项目名称以及配置项:', name, options)
}

14. Create a factory.js file in the lib folder, which is responsible for creating directories, pulling templates and other logic

zyq_fronted_cli      
├─ bin
│  ├─ cli.js   
├─ lib
│  ├─ create.js
│  ├─ factory.js 
└─ package.json

15. Edit the content of the factory.js file and introduce it into the create.js file.

factory.js 文件


module.exports = class Factory{
    
    
  constructor(name, targetCwd){
    
    
    this.name = name // 目录名称
    this.targetCwd = targetCwd // 目录所在地址

    console.log(this.name, this.targetCwd)
  }

  // 创建
  create() {
    
    

  }
}
create.js 文件


module.exports =  async function (name, options){
    
    
  const path = require('path')
  const fs = require('fs-extra')
  const inquirer = require('inquirer')

  const cwd  = process.cwd() // 当前命令行选择的目录
  const targetCwd = path.join(cwd, name) // 需要创建的目录地址
  console.log(cwd,targetCwd)


  
  // 判断是否存在该目录
  if (fs.existsSync(targetCwd)) {
    
     // 目录存在

    if (options.force) {
    
     // 是否强制创建

      console.log('进行强制创建')
      // 移除原来存在的项目
      await fs.remove(targetCwd)

    } else {
    
    

      console.log('询问用户是否强制创建')

      // 询问用户是否强制创建项目
      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 {
    
    
        console.log('移除存在的文件')
        await fs.remove(targetCwd)
      }

    }
  } else {
    
     // 目录不存在
    console.log('目录不存在,进行强制创建')
  }

  // 创建项目  
  const Factory = require('./factory')
  const factory = new Factory(name, targetCwd)
  factory.create()
  console.log('项目名称以及配置项:', name, options)
}

16. Next, write the logic of asking the user to choose a template.
Github provides an interface to obtain templates. You can prepare two templates in advance and publish them to github.
Create an http.js file in the lib folder, which is specially used to manage the interface. Create After that, the entire directory structure is as follows:

zyq_fronted_cli      
├─ bin
│  ├─ cli.js   
├─ lib
│  ├─ create.js
│  ├─ factory.js
│  ├─ http.js 
└─ package.json 

17. Install axios and write the content of http.js file

npm install axios
http.js 文件


const axios = require('axios')

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

// 获取模版列表
async function getRepoList(myGithub = 'vue3-0-cli-yd'){
    
    
  return axios.get('https://api.github.com/orgs/vue3-0-cli-yd/repos') // 更换自己的 github 项目 `https://api.github.com/orgs/wangml-gitbub/repos`
}

// 获取版本信息
async function getTagList(repo) {
    
    
  return axios.get(`https://api.github.com/repos/vue3-0-cli-yd/${
     
     repo}/tags`) // https://api.github.com/orgs/wangml-gitbub/repos
}

module.exports = {
    
    
  getRepoList,
  getTagList
}

18. Install ora, used to display the loading effect;
install util, util allows hosts without node environments (such as browsers) to have the node's util module;
install download-git-repo, used to download git repositories

npm install [email protected]
npm install util
npm install download-git-repo

19. Write the content of the factory.js file, add loading animation, obtain the template selected by the user, obtain the tag list of the template, download the remote template, and create the logic of the project

factory.js 文件


const {
    
     getRepoList, getTagList } = require('./http')
const ora = require('ora') // 显示加载中的效果
const util = require('util') // 让没有 node 环境的宿主拥有 node 的 util 模块
const downloadGitRepo = require('download-git-repo') // 下载 git 存储库
const inquirer = require('inquirer')
const path = require('path')
const chalk = require('chalk')

module.exports = class Factory{
    
    
  constructor(name, targetCwd){
    
    
    this.name = name // 目录名称
    this.targetCwd = targetCwd // 目录所在地址
    this.downloadGitRepo = util.promisify(downloadGitRepo) // 对 download-git-repo 进行 promise 化改造

    console.log(this.name, this.targetCwd)
  }

  // 加载动画
  async loading(fn, message, ...args) {
    
    
   
    const spinning = ora(message) // 初始化 ora,传入提示信息 message 
    spinning.start() // 开始加载动画


    try {
    
    
      const result = await fn(...args) // 执行 fn 方法
      spinning.succeed() // 将状态改为成功

      return result

    } catch (err){
    
    
      spinning.fail('Request failed, refetch ...')
    }
  }

  // 获取用户选择的模版
  async getRepo(){
    
    

    // 从远程拉取模板数据
    const repoList = await this.loading(getRepoList, 'waiting fetch template')
    if(!repoList) return

    // 过滤需要的模板名称
    const repos = repoList.map(item => item.name) 
    console.log(repos)
    
    // 让用户选择模版
    const {
    
     repo } = await inquirer.prompt({
    
    
      name: 'repo',
      type: 'list',
      choices: repos,
      message: 'Please choose a template to create project'
    })

    // 返回用户选择的名称
    return repo 
  }

  // 获取模版的 tag 列表
  async getTag(repo){
    
    
    // 从远程拉取模板 tag 列表
    const tags = await this.loading(getTagList, 'waiting fetch tag', repo)
    if(!tags) return

    // 过滤需要的 tag 名称
    const tagList = tags.map(item => item.name)
    console.log(tagList)

    // 让用户选择 tag
    const {
    
     tag } = await inquirer.prompt({
    
    
      name: 'tag',
      type: 'list',
      choices: tagList,
      message: 'Place choose a tag to create project'
    })

    // 返回用户选择的 tag
    return tag
  }

  // 下载远程模版
  async download(repo, tag){
    
    
    
    const requestUrl = `vue3-0-cli-yd/${
     
     repo}${
     
     tag ? '#' + tag : ''}` // 拉取模版的地址
    const createUrl =  path.resolve(process.cwd(), this.targetCwd) // 创建项目的地址

    // 下载方法调用
    await this.loading(this.downloadGitRepo, 'waiting download template', requestUrl, createUrl)
  }

  // 创建项目
  async create() {
    
    
    console.log('创建项目---', this.name, this.targetCwd)
    try {
    
    
      // 获取用户选择的模版名称
      const repo = await this.getRepo()

      // 获取用户选择的 tag
      const tag = await this.getTag(repo)

      await this.download(repo, tag)


      // 4)模板使用提示
      console.log(`\r\nSuccessfully created project ${
     
     chalk.cyan(this.name)}`)
      console.log(`\r\n  cd ${
     
     chalk.cyan(this.name)}`)
      console.log(`\r\n  npm install`)
      console.log("\r\n  npm run dev\r\n")
    } catch (error) {
    
    
      console.log(error);
    }
  }
}

20. That’s it. You can use this customized scaffolding to pull the corresponding template.

zyq_fronted create my_project
选择模版及 tag
cd my_project
npm install
npm run dev

21. Code address
cli.js file code

#! /usr/bin/env node


const commander = require('commander') // 自定义指令
const create = require('../lib/create.js')

commander
  .version('0.1.0')
  .command('create <project_name>')
  .description('create a new project')
  .option('-f --force', 'overwrite target directory if it exist')
  .action((name, option) => {
    console.log(name, option)
    create(name, option)
  })



  const chalk = require('chalk') // chalk 改变颜色
  const figlet = require('figlet') // figlet 改变字体

  commander.on('--help', () => { // 监听 --help 执行
    
    console.log('\r\n' + figlet.textSync('ZYQ_FRONTED_CLI', {
      font: 'Ghost',
      horizontalLayout: 'default',
      verticalLayout: 'default',
      width: 500,
      whitespaceBreak: true
    }))

    // 新增说明信息
    console.log(`\r\nRun ${
     
     chalk.cyan(`zyq_fronted <command> --help`)} for detailed usage of given command\r\n`)
  })

commander.parse()

create.js file code

module.exports =  async function (name, options){
    
    
  const path = require('path')
  const fs = require('fs-extra')
  const inquirer = require('inquirer')

  const cwd  = process.cwd() // 当前命令行选择的目录
  const targetCwd = path.join(cwd, name) // 需要创建的目录地址
  console.log(cwd,targetCwd)


  
  // 判断是否存在该目录
  if (fs.existsSync(targetCwd)) {
    
     // 目录存在

    if (options.force) {
    
     // 是否强制创建

      console.log('进行强制创建')
      // 移除原来存在的项目
      await fs.remove(targetCwd)

    } else {
    
    

      console.log('询问用户是否强制创建')

      // 询问用户是否强制创建项目
      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 {
    
    
        console.log('移除存在的文件')
        await fs.remove(targetCwd)
      }

    }
  } else {
    
     // 目录不存在
    console.log('目录不存在,进行强制创建')
  }

  // 创建项目  
  const Factory = require('./factory')
  const factory = new Factory(name, targetCwd)
  factory.create()
  console.log('项目名称以及配置项:', name, options)
}

http.js file code

const axios = require('axios')

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

// 获取模版列表
async function getRepoList(){
    
    
  return axios.get('https://api.github.com/orgs/vue3-0-cli-yd/repos') // https://api.github.com/orgs/wangml-gitbub/repos
}

// 获取版本信息
async function getTagList(repo) {
    
    
  return axios.get(`https://api.github.com/repos/vue3-0-cli-yd/${
     
     repo}/tags`) // https://api.github.com/orgs/wangml-gitbub/repos
}

module.exports = {
    
    
  getRepoList,
  getTagList
}

factory.js file code

const {
    
     getRepoList, getTagList } = require('./http')
const ora = require('ora') // 显示加载中的效果
const util = require('util') // 让没有 node 环境的宿主拥有 node 的 util 模块
const downloadGitRepo = require('download-git-repo') // 下载 git 存储库
const inquirer = require('inquirer')
const path = require('path')
const chalk = require('chalk')

module.exports = class Factory{
    
    
  constructor(name, targetCwd){
    
    
    this.name = name // 目录名称
    this.targetCwd = targetCwd // 目录所在地址
    this.downloadGitRepo = util.promisify(downloadGitRepo) // 对 download-git-repo 进行 promise 化改造

    console.log(this.name, this.targetCwd)
  }

  // 加载动画
  async loading(fn, message, ...args) {
    
    
   
    const spinning = ora(message) // 初始化 ora,传入提示信息 message 
    spinning.start() // 开始加载动画


    try {
    
    
      const result = await fn(...args) // 执行 fn 方法
      spinning.succeed() // 将状态改为成功

      return result

    } catch (err){
    
    
      spinning.fail('Request failed, refetch ...')
    }
  }

  // 获取用户选择的模版
  async getRepo(){
    
    

    // 从远程拉取模板数据
    const repoList = await this.loading(getRepoList, 'waiting fetch template')
    if(!repoList) return

    // 过滤需要的模板名称
    const repos = repoList.map(item => item.name) 
    console.log(repos)
    
    // 让用户选择模版
    const {
    
     repo } = await inquirer.prompt({
    
    
      name: 'repo',
      type: 'list',
      choices: repos,
      message: 'Please choose a template to create project'
    })

    // 返回用户选择的名称
    return repo 
  }

  // 获取模版的 tag 列表
  async getTag(repo){
    
    
    // 从远程拉取模板 tag 列表
    const tags = await this.loading(getTagList, 'waiting fetch tag', repo)
    if(!tags) return

    // 过滤需要的 tag 名称
    const tagList = tags.map(item => item.name)
    console.log(tagList)

    // 让用户选择 tag
    const {
    
     tag } = await inquirer.prompt({
    
    
      name: 'tag',
      type: 'list',
      choices: tagList,
      message: 'Place choose a tag to create project'
    })

    // 返回用户选择的 tag
    return tag
  }

  // 下载远程模版
  async download(repo, tag){
    
    
    
    const requestUrl = `vue3-0-cli-yd/${
     
     repo}${
     
     tag ? '#' + tag : ''}` // 拉取模版的地址
    const createUrl =  path.resolve(process.cwd(), this.targetCwd) // 创建项目的地址

    // 下载方法调用
    await this.loading(this.downloadGitRepo, 'waiting download template', requestUrl, createUrl)
  }

  // 创建项目
  async create() {
    
    
    console.log('创建项目---', this.name, this.targetCwd)
    try {
    
    
      // 获取用户选择的模版名称
      const repo = await this.getRepo()

      // 获取用户选择的 tag
      const tag = await this.getTag(repo)

      await this.download(repo, tag)


      // 4)模板使用提示
      console.log(`\r\nSuccessfully created project ${
     
     chalk.cyan(this.name)}`)
      console.log(`\r\n  cd ${
     
     chalk.cyan(this.name)}`)
      console.log(`\r\n  npm install`)
      console.log("\r\n  npm run dev\r\n")
    } catch (error) {
    
    
      console.log(error);
    }
  }
}

package.json contents

{
    
    
  "name": "zyq_fronted_cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "bin": {
    
    
    "zyq_fronted": "./bin/cli.js"
  },
  "scripts": {
    
    
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    
    
    "axios": "^1.3.5",
    "chalk": "^4.1.0",
    "commander": "^10.0.1",
    "download-git-repo": "^3.0.2",
    "figlet": "^1.6.0",
    "fs-extra": "^11.1.1",
    "inquirer": "^8.2.5",
    "ora": "^5.4.1",
    "util": "^0.12.5"
  }
}

Guess you like

Origin blog.csdn.net/qq_37600506/article/details/130583803