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"
}
}