创建脚手架工具

引入

我们在初始vue-cli的时候,经常使用如下的命令

1
vue init webpack test

那么是怎么实现的呢?如何自己搭建一个cli工具。

搭建具体流程

  • 新建一个 ***-cli 的文件夹,如 cpm-cli
  • npm init 初始化一个package.json
  • 编写自动化下载的脚本

    1. 使用 gitlab group 或 github organization 存储脚手架项目

    2. 使用 gitlab api 或 github api 读取项目列表
      以 github 为例:https://api.github.com/orgs/${orgName}/repos

    3. 使用交互式命令供用户选择项目

    4. 使用 gitlab api 或 github api 下载脚手架
      以 github 为例:github:${orgName}/${project}

    5. 重写package.json等文件

  • npm publish

npm包

  • commander:解析命令行
  • download-git-repo:下载git仓库
  • inquirer:交互式命令
  • axios:调用github接口
  • fs:读取和修改文件
  • ora:控制台输出loading
  • chalk:控制台输出不同颜色的文案
  • symbols:控制台console前面的类型

注意事项

  • 入口文件顶部:#!/usr/bin/env node
  • package.json添加:”bin”: { “cpm-cli”: “index.js” }
  • 安装依赖包时使用-S安装,不要使用-D安装
  • 调用program.parse(process.argv)时需要单独写,不要和其他的一起链式调用
  • download脚手架偶尔会报错“被拒绝连接connect ECONNREFUSED”,稍后重试即可
  • 需要配置github ssh key,否则无法download

代码

访问github

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
#!/usr/bin/env node
'use strict'

/**
* cli脚手架搭建参考:
* https://developer.github.com/v3/
* https://www.jianshu.com/p/edeff714e8a3
* https://blog.csdn.net/weixin_38080573/article/details/97897767
*
* 注意:
* 入口文件顶部添加:#!/usr/bin/env node
* package.josn添加:"bin": { "cpm-cli": "index.js" }
*/

const program = require('commander');
const download = require('download-git-repo');
const inquirer = require('inquirer');
const axios = require('axios');
const fs = require('fs');
const ora = require('ora');
const chalk = require('chalk'); // green/success、red/error、yellow/tip
const symbols = require('log-symbols');

// config
const orgName = 'cpm-cli'; // github organization name

/**
* 抓取github api获取脚手架列表
*/
function getOrgTemplateList () {
return new Promise((resolve, reject) => {
const spinner = ora(chalk.yellow('Request template list ...'));
spinner.start();
axios
.get(`https://api.github.com/orgs/${orgName}/repos`)
.then(res => {
if (res.data && res.data.length) {
const list = res.data.map(item => item.full_name.replace(`${orgName}/`, ''));
spinner.succeed();
resolve(list);
} else {
spinner.fail();
reject();
}
})
.catch((err) => {
spinner.fail();
reject(err);
})
})
}

/**
* 交互式命令获取用户输入和选择
* @param {Array} list 脚手架列表
*/
function showInquirer (list) {
return new Promise((resolve, reject) => {
inquirer
.prompt([
{
type: 'input',
message: 'please input package.json name',
name: 'name',
default: 'demo'
},
{
type: 'input',
message: 'please input package.json description',
name: 'description',
default: 'a demo project'
},
{
type: 'input',
message: 'please input package.json author',
name: 'author',
default: 'chenpengmin'
},
{
type: 'list',
message: 'please choice template',
name: 'template',
choices: list
}
])
.then((answers) => {
resolve(answers);
})
.catch((err) => {
reject(err);
})
})
}

/**
* 下载脚手架
* @param {Object} answers
* @param {String} projectName
*/
function downloadTemp (answers, projectName) {
return new Promise((resolve, reject) => {
const spinner = ora(chalk.yellow(`Downloading ${answers.template} template ...`));
spinner.start();
download(`github:${orgName}/${answers.template}`, projectName, { clone: true }, (err) => {
if (err) {
spinner.fail();
reject(err);
return;
}
spinner.succeed();
resolve();
})
})
}

/**
* 重写package.json,修改name、description、author等参数
* @param {Object} answers
* @param {String} projectName
*/
function rewritePackageJson (answers, projectName) {
const { name, description, author } = answers;
const packagePath = `${projectName}/package.json`;
if (fs.existsSync(packagePath)) {
const packageJson = fs.readFileSync(packagePath);
const packageResult = JSON.stringify(
Object.assign(
{},
JSON.parse(packageJson),
{
name,
description,
author
}
),
null,
'\t'
);
fs.writeFileSync(packagePath, packageResult);
}
}

/**
* 主方法
* @param {String} projectName
*/
async function main (projectName) {
try {
// 第一步:读取github organization列表
const list = await getOrgTemplateList();

// 第二步:交互式命令获取用户选择
const answers = await showInquirer(list);

// 第三步:下载脚手架
await downloadTemp(answers, projectName);

// 第四步:重写package.json
rewritePackageJson(answers, projectName);

console.log(symbols.success, chalk.green(`project ${projectName} was created successfully`));
} catch (err) {
console.log(symbols.error, chalk.red(err || 'something was wrong'));
}
}

program
.version(require('./package.json').version)
.command('init <projectName>')
.action((projectName) => {
if (fs.existsSync(projectName)) {
console.log(symbols.error, chalk.red(`project ${projectName} already exist`));
return;
}
main(projectName);
})
program.parse(process.argv);

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
{
"name": "cpm-cli",
"version": "0.1.7",
"description": "cpm-cli",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "chenpengmin",
"homepage": "https://github.com/cpm828/cpm-cli",
"keywords": [
"cli",
"cpm",
"cpm-cli"
],
"license": "ISC",
"bin": {
"cpm-cli": "index.js"
},
"dependencies": {
"axios": "^0.19.2",
"chalk": "^4.1.0",
"commander": "^5.1.0",
"download-git-repo": "^3.0.2",
"fs": "0.0.1-security",
"inquirer": "^7.2.0",
"log-symbols": "^4.0.0",
"ora": "^4.0.4",
"shell": "^0.5.0"
}
}