创建自己的node cli

经常使用create-react-app作为命令行生成项目,总想着自己能配置一个属于自己的命令行工具,很酷炫的样子。最近有时间,终于来实现了

初始化项目

1
2
mkdir y-cli && cd y-cli
npm init -y

新建一个文件 ./bin/yyt.js,并写一点简单的东西

1
2
3
4
// 这里告诉系统要以node环境执行:
#!/usr/bin/env node

console.log('hello,node cli!')

再在 package.json 里添加 bin 配置,需要指定一个自定义命令,如 y-cli

1
2
3
"bin": {
"y-cli": "./bin/yyt.js"
}

在命令行里运行:

1
2
3
npm link
# 或者在全局install:
npm install . -g

可以看到如下,npm将命令作为软链接连接至对应的js文件

1
2
3
4
5
6
7
8
9
xxx@xxxMBP y-cli % sudo npm link
npm WARN [email protected] No description
npm WARN [email protected] No repository field.

up to date in 0.832s
found 0 vulnerabilities

/usr/local/bin/yyt -> /usr/local/lib/node_modules/y-cli/bin/yyt.js
/usr/local/lib/node_modules/y-cli -> /Users/xxx/Documents/code/y-cli

所用到的库

cli需要获取用户的输入,选择并做相应的事情,就需要使用到一些库来帮忙我们更方便的写cli

核心:

  • commander - 处理核心命令 主要模块,在终端输出各种信息全靠这个模块
  • download-git-repo - 下载git模块

美化:

  • chalk  —  美化终端字符显示
  • figlet  —  在终端输出大型字符
  • inquirer  —  命令行参数输入交互
  • shelljs  —  在js文件写命令行
  • ora - 实行loading效果

让我们来把这些库全部安装一遍

commander

根据commander文档的一些参数,我们来编写脚本 yyt.js

1
2
3
4
5
6
7
8
#!/usr/bin/env node
const program = require('commander')

// 获取package.json中的版本信息
program.version(require("../package.json").version)

// 解析指令,这点很重要 不解析指令则无效
program.parse(process.argv)

commander自带 -V-h 两个命令,运行 yyt -h可看到:

1
2
3
4
5
6
xxx@xxxdeMacBook-Pro y-cli % yyt -h
Usage: yyt [options]

Options:
-V, --version output the version number
-h, --help display help for command

.option() 加入自定义命令

1
program.option('-d, --description', 'yyt 脚手架简介:。。。。')

可以发现已经加上该命令:

1
2
3
4
5
6
7
xxx@xxxdeMacBook-Pro y-cli % yyt -h
Usage: yyt [options]

Options:
-V, --version output the version number
-d, --description yyt 脚手架简介:。。。。
-h, --help display help for command

.command() 自定义命令,

基础格式:

1
program.option('hello <param1> [param2]')

值得注意的是,尖括号<>里的参数是必填参数,方括号[]里的参数是可选参数

利用.command()写一个简单的demo,弄清整个基础流程:

1
2
3
4
5
6
7
8
9
program
.command('hello <param1> [param2]')
// 命令里利用.usage()参数化,供后面的.action()回调使用:
.usage('<command> <param1> [param2]')
// 回调处理:
.action((param1, param2)=>{
console.log(param1);
console.log(param2);
})
1
2
3
4
5
6
7
8
ziyouzhiyi@ziyouzhiyideMacBook-Pro y-cli % yyt hello
error: missing required argument 'param1'
ziyouzhiyi@ziyouzhiyideMacBook-Pro y-cli % yyt hello t
t
undefined
ziyouzhiyi@ziyouzhiyideMacBook-Pro y-cli % yyt hello t y
t
y

上面这些参数也可通过.option()来实现,但需要加上-,如-react,如以下的例子,利用参数创建项目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
program.command('create <projectName>')
.option('-react')
.option('-vue')
.action((projectName, option) => {
const type = Object.keys(option)[0]
switch(type) {
case "Vue":
console.log("创建vue项目")
break;
case "React":
console.log("创建react项目")
break;
default:
console.log("未添加参数,不能下载")
}
})

执行命令后可以看到打印:

1
2
ziyouzhiyi@ziyouzhiyideMacBook-Pro y-cli % yyt create test -react
创建react项目

download-git-repo

下载的步骤把他替换成真的仓库,需要用到 download-git-repo

把console.log替换成真实的git仓库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
case "React":
download(
// 此处应加上 #main 分支名称,否则控制台会显示报错
'direct:[email protected]:ys558/yyt-template.git#main',
projectName,
{ clone: true },
(err, x) => {
// 错误回调:
if (err) console.log(err)
console.log(`${projectName} 项目创建成功`)
}
)
break;
default:
console.log("未添加参数,不能下载")
...

inquirer

👆的步骤下载的仓库是在命令行下直接完成的,如果要像vue一样能让用户输入生成的,可以用该库,我们把代码改造一下,实际上就是在 .action() 的回调中进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
const questions = [
{
type: 'input',
name: 'projectName',
message: '请输入项目名称',
},
{
type: 'list',
name: 'frameWork',
message: '请选择框架',
default: 'React',
choices: ['React','Vue']
},
]

program
.command('create')
.action(() => {
inquirer.prompt(questions)
.then(({projectName, frameWork}) => {
switch (frameWork) {
case 'React':
download(
'direct:[email protected]:ys558/yyt-template.git',
projectName,
{ clone: true },
(err) => {
// 错误回调:
if (err) {
console.log(err)
return
}else{
console.log(`${projectName} 项目创建成功`)
}
})
break;
case 'Vue':
download(
'direct:[email protected]:ys558/yyt-template.git',
projectName,
{ clone: true },
(err) => {
// 错误回调:
if (err) {
console.log(err)
return
}else{
console.log(`${projectName} 项目创建成功`)
}
})
break;
default:
break;
}
})
})

美化

chalkfiglet

美化终端字体,如 chalk:

1
2
3
4
5
6
if (err) {
console.log(chalk.bgYellow(err))
return
}else{
console.log(`${chalk.bgGrey(projectName)} ${chalk.greenBright('项目创建成功')}`)
}

figlet:

1
2
3
4
figlet.defaults({font: 'Standard'})
function logo(){
console.log(figlet.textSync('hi yyt!'))
}

ora

这是一个比较麻烦的模块,他是 es module 类型的import导入模块,而 node 用的是commonjsrequire 导入模块,而且须在 package.json 里添加 { "type" : "module" } 才能正常加载,下面贴上相关代码:

1
2
3
4
5
6

const spinner = ora("下载初始化模板中...")
spinner.start()
spinner.succeed(`${chalk.bgGrey(projectName)} ${chalk.greenBright('项目创建成功')}`)
spinner.stop()

完整代码仓库