background

Some time ago, I received a demand to develop a business component package to provide business group partners to use. Similar packages have been developed before, and aside from the difference in what the packages do, some of the content is pretty much the same. Git commit specifications, mandatory files such as SRC /index.js, etc.

If you want to create a new package project, most of the files mentioned above will be copied from the existing project to modify, and then based on the following development. And the next time there’s a new need like this, the cycle starts… Orz

In that case, why don’t I get a scaffolding tool to automate all this manual work

As the saying goes: the circle is better than... Go back and make your own web. Fuck!Copy the code

The preparatory work

Three simple steps to prepare

Step 1: Register an NPM account for sending packages step 2: mkdir cli-tool && CD cli-tool && NPM init -y Step 3: Then fill in the 'package.json' file with some basic informationCopy the code
{
  "name": "cli-tool"."version": "1.0.0"."description": "A tool for cli creation"."main": "index.js"."scripts": {
    "pub:beta": "npm publish --tag=beta"."pub": "npm publish"
  },
  "publishConfig": {
    "registry": "https://registry.npmjs.org/"
  },
  "keywords": [
    "create"."cli"]."author": "your name"."license": "ISC"
}
Copy the code

Start building the wheel

Step 1: Define the command

I’m sure you’ve all used scaffolding, and scaffolding tools often provide script commands to perform some operations. For example, –version for version number, –help for help information, and so on. In this step, we also define the commands for our own scaffolding.

Creating a script File

First, we create the bin directory under the root of the project, and then create the file ct.js that defines the script in that folder. Once created, define the file corresponding to the command in package.json:

{
  // ...
  "bin": {
    "ct": "bin/ct.js"}}Copy the code

Where, ct specifies the command we need to input when using scaffolding in the future, bin/ct.js specifies the script file path to execute after the command is input.

Write a script

We define the following two basic commands in the script:

Run the --version/-v command to view the version information. 2. The command used to create the project: --create/-cCopy the code
#! /usr/bin/env node

const process = require('process')
const packageJson = require('.. /package.json')

/* * the first and second parameters of the command are path information, so we do not need to use the third parameter */
const argvs = process.argv.slice(2)

switch (argvs[0] | |'-c') {
    /** View the current plug-in version */
    case '-v':
    case '--version':
      console.log(`v${packageJson.version}`)
      break
    /** Create project */
    case '-c':
    case '--create':
      console.log('Create project')
      break
}
Copy the code

After writing, if you want to verify on the command line, you can type the following command:

npm link
Copy the code

Then enter it on the command line

ct -v
Copy the code

We can then see the printed version information in the command line tool:

This completes our basic command definition.

Step 2: Implement the project creation logic

Next we will start creating the project. Before we start implementing the concrete logic, we can outline the steps we might take:

Step 1: Create the project Step 2: Create the basic files and folders required for the project. For example, package.json file or SRC directory. Step 3: After the package is created, go to the corresponding directory to install the packageCopy the code

Based on the above steps, let’s implement the concrete logic.

Create a project

First, we create an index.js file in the SRC /create path to write the specific creation logic.

module.exports.run = () = > {}
Copy the code

We then introduce this method in the bin/ct.js file and call it when -c/–create is executed:

const { run } = require('.. /src/create/index')

/ /... Omit a lot of code

/** Create package project */
case '-c':
case '--create':
  run()
  break

/ /... Omit a lot of code
Copy the code

Following the vue-CLI model, when creating a project, we can use interactive commands to make the creation process more user-friendly. Here we will use one package: inquirer. It allows us to define interactive commands.

First, we install it locally

yarn add -D inquirer
Copy the code

After that, in the SRC/Constants directory, we create questions.js to define specific steps:

/** * Defines an interactive command line problem */
const DEFAULT_QUESTIONS = [{
  type: 'input'.name: 'PROJECT_NAME'.message: 'Project name:'.validate: function (name) {
    const done = this.async()
    // If the directory already exists, prompt you to change the directory name
    if ([' '.null.undefined].includes(name)) {
      done('Please enter the project name! '.true)
      return
    }
    if (fs.existsSync(name)) {
      done(`The directory "${name}" is exist!! Please reset the dirname.`.true)
      return
    }
    done(null.true)}}, {type: 'input'.name: 'PROJECT_DESCRIPTION'.message: 'Project description:'
}, {
  type: 'input'.name: 'PROJECT_AUTHOR'.message: 'Project author:'
}]

module.exports = { DEFAULT_QUESTIONS }
Copy the code

Here is a brief explanation of the configuration items used. For more information about other configuration items, you can poke:

Type ${string} Interaction type name ${string} Key message ${string} interaction message validate ${function} verification methodCopy the code

Once created, introduce inquirer and the interaction steps defined above in SRC /create/index.js:

const inquirer = require('inquirer')
const { DEFAULT_QUESTIONS } = require('.. /constants/questions')

module.exports.run = async() = > {try {
    // Get the input information
    const answers = await inquirer.prompt(DEFAULT_QUESTIONS)
  } catch (error) {
    console.log(chalk.red(`Create objet defeat! The error message is: ${error}`))}}Copy the code

At this point, when we execute cT-c in the console, we can see the interactive input interface:

Once the input is complete, we print answers to the console:

The result is what we typed, and the corresponding key is the name property we set when we customized the problem. We can create a project folder for the PROJECT_NAME attribute we entered.

You can create folders using fs.mkdir provided in Nodejs, and I recommend zx, Google’s open source library. This library encapsulates a series of shell commands so that we can use commands as if they were in the console:

yarn add -D zx
Copy the code

Going back to the SRC /create/index.js file, we introduced Zx into it and created our project folder from it. Note: When using Zx, you need to specify #! At the top of the file. The/usr/bin/env zx:

#!/usr/bin/env zx
/ /... Omit a lot of code

const answers = await inquirer.prompt(questions)
// Create a new directory
await $`mkdir ${answers.PROJECT_NAME}`

/ /... Omit a lot of code
Copy the code

Create common files

Before we start this step, we can first think about what files are commonly used when creating a new project. I believe partners have their own answers in their hearts.

Here, I will create including package. Json,. Gitignore, SRC/index. The js file, such as the file content and advance the template definition in the SRC/create/constants/defaultInit in js:

// Define the contents of index.js
const INDEX_CONTENT = {
    filename: 'src/index.js'.content: ' '
}
// Define the package.json content
const PACKAGE_CONTENT = {
    filename: 'package.json'.content: JSON.stringify({
        "version": "1.0.0"."main": "index.js"."scripts": {
            "ca": "git add -A && git-cz -av"."commit": "git-cz"
        },
        "keywords": []."license": "ISC"."devDependencies": {
            "husky": "^ 5.0.9"."commitizen": "^ holdings"."cz-conventional-changelog": "^ 3.3.0"."lint-staged": "^ 10.5.4"}})}// define.czrc content
const CZRC_CONTENT = {
    filename: '.czrc'.content: '{ "path": "cz-conventional-changelog" }'
}
// define. Huskyrc content
const HUSKYRC_CONTENT = {
    filename: '.huskyrc.yml'.content: `hooks: pre-commit: lint-staged commit-msg: 'commitlint -E HUSKY_GIT_PARAMS' `
}
Commitlintrc content
const COMMITLINTRC_CONTENT = {
    filename: '.commitlintrc.yml'.content: `extends: - '@commitlint/config-conventional' `
}
// Define.lintStageDRC content
const LINTSTAGEDRC_CONTENT = {
    filename: '.lintstagedrc.yml'.content: `'**/*.{js, jsx, vue}': - 'eslint --fix' - 'git add' '**/*.{less, md}': - 'prettier --write' - 'git add' `
}

/ / definition. Gitignore
const GIT_IGNORE_CONTENT = {
    filename: '.gitignore'.content: '/node_modules'
}

module.exports = {
    INDEX_CONTENT,
    PACKAGE_CONTENT,
    CZRC_CONTENT,
    HUSKYRC_CONTENT,
    COMMITLINTRC_CONTENT,
    LINTSTAGEDRC_CONTENT,
    GIT_IGNORE_CONTENT
}
Copy the code

Then, create default.js in SRC /create, import the file template, and write the logic to create the file. The file throws a method that accepts the answer to the question in the previous step and fills the file into the corresponding path accordingly. Without further ado, on the code:

#!/usr/bin/env zx

require('zx/globals')
const {
  INDEX_CONTENT,
  PACKAGE_CONTENT,
  CZRC_CONTENT,
  HUSKYRC_CONTENT,
  COMMITLINTRC_CONTENT,
  LINTSTAGEDRC_CONTENT,
  GIT_IGNORE_CONTENT
} = require('.. /.. /constants/defaultInit')

/** * copy all files from the template directory to the specified directory *@param {Object} Answers Input information */
module.exports = (answers) = > {
    const { PROJECT_NAME } = answers
    const baseDir = `${process.cwd()}/${PROJECT_NAME}`
    // Create the SRC directory and create the index.js file
    fs.mkdir(`${baseDir}/src`, { recursive: true }, err= > {
      if (err) {
        console.log({err})
        return
      }
      // Create a file
      const files = [
        INDEX_CONTENT,
        PACKAGE_CONTENT,
        CZRC_CONTENT,
        HUSKYRC_CONTENT,
        COMMITLINTRC_CONTENT,
        LINTSTAGEDRC_CONTENT,
        GIT_IGNORE_CONTENT
      ]
      files.forEach(file= > {
        const { filename, content } = file
        let fileContent = content
        // If it is package.json, fill in the corresponding information
        if (filename === 'package.json') {
          const { PROJECT_NAME, PROJECT_DESCRIPTION, PROJECT_AUTHOR } = answers
          const packageContent = {
            name: PROJECT_NAME,
            author: PROJECT_AUTHOR,
            description: PROJECT_DESCRIPTION, ... JSON.parse(content) } fileContent =JSON.stringify(packageContent, null.'\t')
        }
        fs.writeFile(`${baseDir}/${file.filename}`, fileContent, {
          encoding: 'utf-8'
        }, err= > {
          err && console.log({ type: 'Create index.js failed: ', err })
        })
      })
    })
}
Copy the code

Go back to the SCR /create/index.js file again and introduce the method and execute:

const initObject = require('.. /utils/default')

/ /... Omit a lot of code

// Get the input information
const answers = await inquirer.prompt(questions)
// Create a new directory
await $`mkdir ${answers.PROJECT_NAME}`
// Create a file
await initObject(answers)

/ /... Omit a lot of code
Copy the code

Go to the corresponding directory to perform the installation

This step is easy, just jump to the directory we created and install the required packages:

// src/create/index.js

/ /... Omit a lot of code

// Get the input information
const answers = await inquirer.prompt(questions)
// Create a new directory
await $`mkdir ${answers.PROJECT_NAME}`
// Create a file
await initObject(answers)
// Jump to the directory
cd(dirName)
// Run the installation command
await $`yarn`

/ /... Omit a lot of code
Copy the code

At this point, all the basic logic is complete

Step 3: Publish the tool to NPM

After writing, we can directly execute the pre-set pub command to publish.

Further reading

About Command parsing

In this article, we used process.argv to parse the input commands. If it was a simple project, it would be fine. Commander makes this process much easier and can be configured with a help message. If you are interested, go and try it

About the color of the printed message

Different types of console messages can be printed in different colors to make them more intuitive and aesthetically pleasing. In the zX mentioned above, Chalk is also packaged and can be used directly once zX is introduced. It allows you to “color” console output information. An 🌰 :

console.log(chalk.red(`Create objet defeat! The error message is: ${error}`))
Copy the code

More colors and use methods, friends can also go to study, let the console output colorful ~

conclusion

At this point, we’ve created a simple scaffolding tool of our own. We can continue to optimize it according to our own needs in the later stage. We can also think about what other partners need when using your scaffold, and how to help more partners improve their human efficiency.

About CLI tools, Cui da in B station also has a detailed explanation of the video, we can also go to reference: portal

The scaffolding tools used in this article have been uploaded to git and are welcome to ⭐️

At the same time, it has been uploaded to NPM, interested partners can try the global installation ~

See here, quickly start to weave a net cafe ~