前言
之前为了学习webpack和ts编译,写了几个脚手架,自制的脚手架包括vue和react框架,可选择js与ts语言,脚手架只包括最基本的依赖。为了更好的管理与使用自己的写的脚手架,就写了个cli npm包。使用方式如下:
npm i @sanhuamao1/sancli // 安装
san-cli create <project-name> // 使用
-
npm包地址:@sanhuamao1/sancli
-
源码地址:github.com/san-cli (包含了npm包和所有模板源码)
项目说明
基本结构
- lib
- config
- constants // 常量
- repoMap // 仓库地址的映射
- core
- commands // 命令遍历
- create // create命令执行逻辑
- help // --help命令执行逻辑
- index // 入口
- package.json
所需依赖
- commander:命令行操作
- download-git-repo:用于下载仓库
- inquirer@8:交互
- ora@4:可视加载
先来安装一下吧~
npm i commander download-git-repo inquirer@8 ora@4
注意下载的版本,因为inquirer@8和ora@4才支持
CommanJs
基本配置
-
npm init -y
初始化 -
进入
package.json
需要进行一些配置:
{
"main": "./index.js", // 入口文件
"bin": {
"san-cli": "./index.js" // 通过执行`san-cli`可执行对应的文件
},
}
- 进入
index.js
文件,声明执行环境
#!/usr/bin/env node
console.log('okk')
- 全局链接命令
npm link
- 尝试执行,此时终端会打印出okk
san-cli
命令与文件说明
version与help
lib/config/constants.js
// 01 获取版本信息
const {version} = require('../../package.json')
// 02 配置所有需要的命令。写法是commander规定的,具体可在官网查看
const actions = {
create: { // create 命令,会通过`san-cli create`执行
alias: "c", // 别名
description: "create a project", //描述
examples: ["san-cli create <project-name>"],
},
"*": { // 其他没有被匹配的命令
alias: "",
description: "command not found",
examples: [],
},
}
module.exports={
version,
actions
}
lib/core/help.js
const program=require('commander')
const {actions} =require('../config/constants') // 导入所有注册的命令
const helpOptions=()=>{
program.on("--help", () => {
console.log("\nExamples:") // 依次遍历打印所有命令的例子
Reflect.ownKeys(actions).forEach((action) => {
actions[action].examples.forEach((example) => {
console.log(example)
})
})
})
}
module.exports=helpOptions
index.js
:现在将上面的内容注册在入口文件中
#!/usr/bin/env node
const program=require('commander')
const helpOptions=require('./lib/core/help')
const { version } = require("./lib/config/constants")
program.version(version,'-v,--version') //设置版本命令
helpOptions() // 设置help命令
program.parse(process.argv); // 最后需要执行下这条才会生效(!)
尝试一下:
san-cli -v
san-cli --help
注册所有命令
在上面已经写过actions
常量了,现在根据常量注册一下所有命令。
lib/core/commands.js
const {actions} =require('../config/constants')
const program=require('commander')
const path=require('path')
const commands=()=>{
Reflect.ownKeys(actions).forEach((action) => {
program
.command(action) // 配置命令名称
.alias(actions[action].alias) // 配置命令别名
.description(actions[action].description) // 配置命令描述
.action(() => {
if (action === "*") {
console.log(actions[action].description) // 匹配不到时
} else {
// 匹配到命令时,获取对应的文件(该文件返回一个函数),并执行该函数
// 假设action为create,那么下面的操作就是去导入create.js的函数,并执行它
// 简单说明一下process.argv的结构:
// [node可执行文件的绝对路径,当前执行文件的路径,其他参数]
// 现在具体到 san-cli create myproject,
// (由于前面在package.json进行了配置,其实node就可忽略,san-cli代表的就是index.js)
// 翻译一下,实际执行的是:node index.js create myproject
// 所以process.argv的结构是:
// [node, index.js, create, myproject]
// 现在通过.slice(3)剔除掉前面三个后就是[myproject]
// 最后再解构一下,传入的就是 myproject,也就是写入的项目名称
require(path.resolve(__dirname, action))(...process.argv.slice(3))
}
})
})
}
module.exports=commands
现在回到入口文件注册一下:
index.js
#!/usr/bin/env node
//...
const commands=require('./lib/core/commands')
commands()
//...
program.parse(process.argv);
create
现在就是写create命令的逻辑了。
下载仓库当然要下载地址,关于下载地址的写法可参考download-git-repo
官网,我这边采用direct
方式。
由于我提供了交互,我将根据交互结果为用户提供指定的模板。为了更好的管理模板,建议创建一个组织。下面san-cli
就是我的组织,在组织里面可以创建很多仓库。
点进某个仓库之后,我需要的地址如图所示:
知道自己的地址结构后,就可以安心地写映射了:
lib/config/repoMap.js
const getReportUrl=(name)=>{
return `direct:https://github.com/san-cli/${name}.git#main`
} // 可以通过#xxx来指明分支
const vueTsRepo=getReportUrl('vue-ts')
const vueJSRepo=getReportUrl('vue-js')
const reactJSRepo=getReportUrl('react-js')
const reactTsRepo=getReportUrl('react-ts')
const tsRepo=getReportUrl('ts')
// 由于是根据用户的交互结果来下载某个模板的
// 前面的key就是用户交互的结果,我要根据答案给用户提供模板
// 我这边设置的是:
// - 是否使用模板 0|无模板 1|vue 2|react
// - 使用的语言: 1|js 2|ts
// 这个比较灵活,根据自身需求写就行
const repoMap={
'02':tsRepo,
'11':vueJSRepo,
'12':vueTsRepo,
'21':reactJSRepo,
'22':reactTsRepo
}
module.exports=repoMap
lib/core/create.js
const { promisify } = require("util") // 转为异步
const download = promisify(require("download-git-repo"))
const ora=require('ora') // 可视加载
const inquirer = require("inquirer") // 交互
const repoMap = require("../config/repoMap") // 仓库地址映射
// 询问1:选择框架
const useFrame = ()=>{
return inquirer.prompt([
{
type: "list",
name: "frame", // 交互结果会放在这个字段中
message: "chose frame:",
default: 0,
choices: [
{ value: 0, name: "(no frame)" },
{ value: 1, name: "Vue" },
{ value: 2, name: "React" },
],
}
])
}
// 询问2:选择语言
const useLang = ()=>{
return inquirer.prompt([
{
type: "list",
name: "lang",
message: "chose lang:",
default: 1,
choices: [
{ value: 1, name: "JavaScript" },
{ value: 2, name: "TypeScript" },
],
}
])
}
const getResult=async ()=> {
const frame = await useFrame()
// 当使用框架时,让用户选择语言
if(frame.frame!==0){
const lang = await useLang()
return `${frame.frame}${lang.lang}`
}else{
// 否则跳过第二个问题,直接返回结果
return `${frame.frame}2`
}
}
const createProjectAction = async (projectName) => {
const result = await getResult()
const spinner = ora('Fetch...').start(); // 开始可视加载
// 传入仓库地址,项目名称,配置
await download(repoMap[result], projectName, { clone: true }).then(()=>{
spinner.succeed("success!") // 结束可视加载
console.log(`- cd ${projectName}`)
console.log("- npm install -- to install dependencies")
console.log("- npm start -- to run the project")
console.log("- npm run build -- to build the project")
}).catch((error)=>{
spinner.fail("error!",error) // 结束可视加载
})
}
module.exports = createProjectAction
尝试一下~
san-cli create mydemo
npm包
发布包
- 先去官网注册一个账号
- 执行如下命令
npm config set registry https://registry.npmjs.org // 切换镜像
npm adduser // 添加或验证用户
username // 输入用户名
password // 输入密码(不要怀疑,在这里不管你有没有输入,光标都是不会动的,输入密码后按回车就可)
email // 输入邮箱
code // 邮箱会接收一个一次性密码,输入即可
npm login // 登录
npm adduser // 忘记要不要输入这个了 看着来
username
password
email
code
npm publish // 发布,如果有问题,就换个名称,在package.json中修改name
npm config set registry http://registry.npm.taobao.org/ // 最后记得切换回来
- 最后记得保存好自己的npm包
更新包
npm version <update_type> // 类型有:major minor patch
npm publish