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-spawn
Spawn: 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 memEditor
These 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 --help
And 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 locallyuc
Command 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-cli
andcreate-react-app
These two excellent CLI tools
Reference links:
Take you hand in hand with a CLI tool