Cli (scaffolding) tools are now used frequently in our daily development. For example, vue-CLI, create-react-app, webpack-CLI, and so on. Using these scaffolds can help us build projects quickly. But sometimes we want to customize the scaffolding for our own projects to avoid a lot of copy work. So let’s implement a clean version of the CLI of our own.

Full project address: Github: UC-CLI

Prepare before you start

You may be wondering: how do you get a custom CLI tool installed globally to start scaffolding with instructions?

In the package.json file, there is a bin field that can be set to allow our scaffolding to build content as instructions once installed.

First, we create a new project and initialize the package.json file

mkdir uc-cli && cd uc-cli

npm init

Then set your named directive in the bin field:

"bin": {
   "uc": "bin/index.js"
 }
Copy the code

The instructions I set here are UC instructions, you can also set your own instructions. The index.js file in the bin directory is started. So we’ll create a new bin directory under the project and test it by writing a little code in the index.js file

console.log('test uc-cli')
Copy the code

You can then print the results using the node bin/index.js test. Run NPM install -g under the project if you want to test in the form of instructions. If the project file is updated, you can run NPM link to update it

Preparation before setting up

Since you are developing your own CLI tools, you must prepare templates before developing them. A template is a project file that you want to automatically generate, such as an HTML file or a JS file, or a project template. Like vue-CLI generated project files.

First, we create a new lib folder under the project to store the core logic code, and a new template folder to store the template files. We put the files that do not need to be changed according to the user configuration in the base folder, and the files that need to be changed in the template folder.

I wrote about building a CLI a year ago: WebPack – Customize a VUE-CLI from scratch (first small step). At that time, the Vue development environment was only set up, and there was no similar function of CLI tool, so it was just a template.

Now that you have the template, you need to consider what the CLI needs. As far as my CLI tool is concerned, the following options are designed for user customization:

  • Create project type:
    • vue
    • React (not supported yet)
  • How to create a project: (1: manual configuration, 2: default configuration)
  • When manual configuration is selected
    • Whether to add a CSS preprocessor
    • Whether esLint needs to be added
    • Whether to add vue-router
    • Whether to add vuex
    • Whether you need to add AXIOS (with interceptors configured, wrapped AXIOS)
    • Select the package manager for the installation package (auto: automatic selection, YARN, NPM, CNPM)

You can also figure out what your CLI tools will need before you start developing them.

If your CLI tool needs to be published to NPM, it is a good idea to check with NPM before you start. If the package name you created has been published to NPM, you cannot publish it again

Write core code

The core logic of the CLI tool is to obtain user-defined options and then copy the prepared template or template file in the remote repository to the local PC based on the configuration options.

Before you start writing code you need to know some of the toolkit’s features and usage:

inquirer

To interact with the user, get the user’s custom options using the address: inquirer package, used as follows:

// Interact with the user to get the user configuration
function getUserOption () {
  let qustions = []
  qustions.push(cssPreName: {
    name: 'cssPreName'.type: 'list'.message: Please select the CSS preprocessor you want to add:.choices: ['none'.'sass'.'less'.'stylus'] }) qustions.push({... })return inquirer.prompt(qustions)
}
// Inquirer. Prompt returns a Promise object.inquirer.prompt([...] ).then(optins= > {
  // 
})
Copy the code

The running effect is as follows:

chalk: Can output fonts in different colors on the console

cross-spawnSpawn: cross-terminal command pack. Using spawn helps us execute some commands. Such as NPM I. The usage is similar to child_process.spawn

which: helps us detect which commands are available. Bitmap: Checks whether the NPM or YARN command is available

MemFs and memEditorThese packages can be used when we have template files that need to be dynamically changed based on the user’s configuration items. Using EJS template syntax to dynamically change the template content and inject data into the template

this.store = memFs.create()
this.fs = memEditor.create(this.store)

this.fs.copyTpl('template/vue/App.vue'.'App.vue', {
  addRouter: true
})
Copy the code

For example, the app. vue file exports different templates based on whether vue-router is used

<template>
  <div>
  <%_ if (addRouter) { _%>
    <router-view/>
  <%_ } else { _%>
    <Home/>The < % _} _ % ></div>
</template>

<script>The < % _if(! addRouter) { _%>import Home from '@/views/Home.vue'The < % _} _ % >export default {
  name: 'App'The < % _if(! addRouter) { _%> components: { Home }, <%_ } _%> data () {return{}}}</script>
Copy the code

commander: A complete Node.js command-line solution inspired by Ruby commander.

Using this package allows me to output configuration instructions, cli tool version numbers, and run commands. Such as:uc -V uc --helpAnd so on. Address:commander.js

Now let’s start developing cli tools

Create a new init method in lib/index.js and reference it in bin/index.js:

// lib/index.js
function init () {}module.exports = init

// bin/index.js
#!/usr/bin/env node

'use strict';

const init = require('.. /lib/index')

init()
Copy the code

Init is the logic for initializing the CLI. In init, print the location where the project was created, the CLI version, and write the user configuration items. The two packages Chalk and Inquirer are needed here.

function init () {
  console.log()
  console.log(chalk.hex('#f3ec00') ('Welcome to UC-CLI version:${packageJson.version}`))
  console.log()
  console.log(chalk.white('project created in:${process.cwd()}`))
  console.log()
  getUserOption().then(option= > {
    console.log(option)
  })
}

function getUserOption () {
  let qustions = []
  qustions.push({
    name: 'cssPreName'.type: 'list'.message: Please select the CSS preprocessor you want to add:.choices: ['none'.'sass'.'less'.'stylus']
  })
  qustions.push({
    name: 'eslint'.type: 'list'.message: 'ESLint configuration:'.choices: ['none'.'standard'.'prettier'.'airbnb']
  })
  qustions.push({
    name: 'router'.type: 'input'.message: 'Do you want to add router (y/n)? '.validate: (val) = > {
      val = val && val.toLowerCase()
      let v = ['y'.'yes'.'n'.'no']
      if (v.indexOf(val) === -1) {
        console.log(chalk.red('Please enter y or n'))
        return false
      }
      return true}})return inquirer.prompt(qustions)
}
Copy the code

The print result is as follows:

After interacting with the user, we need to copy our template file locally according to the user’s configuration items. For the copy of the file we will of course use the command to make it automatically executed.

First, Create a Create class that builds your project, and Create a copyTemplate method that makes a copy of the file. Before copying files, obtain the project name and create folders based on the project name. Also determine if the file already exists before creating it. Process.cwd () is used to get the path where the instruction is running. Argv can be used to retrieve the parameters following the directive, and can be designed so that the first parameter after the directive is the project name. Once the file is created, the copy command can be executed using the cross-spawn package. For some files to be copied according to the user configuration, you need to use the previously mentioned mem-fs and mem-fs-editor. Finally, install and build is complete.

const chalk = require('chalk')
const spawn = require('cross-spawn')
const fs = require('fs')
const path = require('path')
const memFs = require('mem-fs')
const memEditor = require('mem-fs-editor')

class Create {
  constructor (options) {
  	// User configuration item
    this.options = options

    this.store = memFs.create()
    this.fs = memEditor.create(this.store)
  }

  copyTemplate () {
    // Process. argv can be used to obtain the parameters entered after the instruction.
    const projectPath = path.join(process.cwd(), process.argv[2])
    if (fs.existsSync(process.argv[2]) {console.log(chalk.red('Project already exists, please change project name'))
      process.exit(1)}// Create the project file and copy it
    fs.mkdirSync(projectPath)
    // Here is the path usage is self-written, for different platforms can have problems,
    // If you want to publish packages, you need to use node's path to handle it
    spawn('cp'['-r'.'template/base/.', projectPath], { stdio: 'inherit' })

    if (this.options.router === 'y') {
      this.fs.copyTpl('template/App.vue', path.join(projectPath, 'App.vue'), {
        addRouter: true})}this.fs.commit(() = > {
      console.log('Construction, please wait... ')
      // Automatic installation package, where you can use the which package to get installation instructions
      let sp = spawn(
        'cnpm'['install'.'vue-router'.'-D'] and {stdio: 'inherit'.cwd: projectPath }
      )
      sp.on('close'.(code) = > {
        console.log()
        console.log(chalk.green('Build completed √'))})})}}Copy the code

And then use it in the init method

getUserOption().then(option= > {
  let create = new Create(option)
  create.copyTemplate()
})
Copy the code

Execute the command to create the projectYou can use it directly if you publish it to NPM or install it locallyucCommand to build the project. Such as:uc my_project

This is a simple CLI tool. Although it looks very simple, but if you want to do a good job, do rich or very difficult, there are a lot of knowledge points to understand. If you haven’t built your own CLI tool, go ahead and build your own CLI tool. You will be rewarded. You can refer to it during the construction processvue-cliandcreate-react-appThese two excellent CLI tools

Reference links:

Take you hand in hand with a CLI tool