前言
🔧 手把手教你开发一个自己的cli工具(二) :juejin.cn/post/735947...
这里呢,我准备分享一下如何开发自己的cli工具,以及介绍一下cli工具的具体能做哪些事情
涉及到的技术栈
- typescript:类型提示方便我们的开发,
- node.js:需要path和fs模块的基本使用即可
- rollup.js:非常简易而且轻量级的打包工具📦,但是同时又有很强大的打包能力,我们熟知的vite的📦打包方式就是rollup.js
- 服务端:如果有些可能要实现打开网页的效果就会涉及到服务端框架,这里有express.js、nest.js等服务端框架
- npm:这里需要对npm包和package.json文件等有一些了解,毕竟会涉及到发包
初始化
创建工程
创建一个npm工程
sh
npm init -y
文件结构
sh
├── bin // 🌸 运行目录
│ └── index.js
├── index.ts
├── lib // 🌸 打包后的文件
│ └── main.js
├── src // 🌸 核心代码
├── package-lock.json
├── package.json
└── tsconfig.json
项目架构
这里我们最好采用monorepo的架构,至于lerna、pnpm+monorepo、yarn+workspace大家可以去自行研究,我这里就采用pnpm+monorepo的架构
先在全局安装pnpm
sh
npm i pnpm -g
# mac Linux
sudo npm i pnpm -g
在我们的项目的根目录创建一个pnpm-workspace.yaml 这里声明packages和scripts、docs、template-lite文件夹下的所有文件都可以存放我们的项目,也就是一个项目多个子项目,通俗来讲,但凡在这个目录下发现了package.json文件,那么那个目录就会被视作一个子项目进行管理,这样统一进行管理。
sh
packages:
- "packages/**"
- "scripts/**"
- "docs/**"
- "template-lite/**"
假如我们创建了一个 packages/cli
的目录,然后终端打开这个目录,输入pnmp init
,那么这个cli目录就会被视作一个子项目
安装依赖
会用到的dependencies
- commander: 生成命令行的一个js工具包
- inquirer: 同样也是生成命令行工具包,这个是会提供问答的功能
- chalk: 为我们的命令行提供颜色,美化命令行
会用到的devDependencies
- rollup: 这里包含rollup的一堆plugins我先省略了,大家安装的时候复制黏贴即可
- @types/node: 给node类型提示
- rimraf: 删除文件(I/O)
- eslint
这里我给大家列出来,大家复制粘贴即可
先打开我们的子项目目录的终端 ,我这里是 packages/cli
,然后输入以下脚本 终端
sh
pnpm add chalk commander inquirer
pnpm add @rollup/plugin-node-resolve @rollup/plugin-terser @rollup/plugin-commonjs @rollup/plugin-json @rollup/plugin-babel rollup-plugin-esbuild rollup @types/node @types/inquirer --save-dev
下载完之后,你会发现在项目的根目录会多一个pnpm-lock.yaml,我们点进去看一下就会发现,pnpm已经将我们的package/cli视为一个子项目,然后下面列出了该项目下载的依赖和第三方包.
代码规范检查(可选)
sh
pnpm add eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser prettier
创建一下 eslint和prettier的config文件,这里直接复制我的就可以啦
.eslintrc
json
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"no-console": "off"
},
// set eslint env
"env": {
"node": true
}
}
.prettierrc.js
js
export default {
// 一行的字符数,如果超过会进行换行,默认为80
printWidth: 80,
// 一个tab代表几个空格数,默认为2
tabWidth: 2,
// 是否使用tab进行缩进,默认为false,表示用空格进行缩减
useTabs: false,
// 字符串是否使用单引号,默认为false,使用双引号
singleQuote: true,
// 行位是否使用分号,默认为true
semi: false,
// 是否使用尾逗号,有三个可选值"<none|es5|all>"
trailingComma: 'none',
// 对象大括号直接是否有空格,默认为true,效果:{ foo: bar }
bracketSpacing: true
}
然后在package.json添加一下js脚本
json
{
"lint": "eslint ./src --ext .ts --fix",
"format": "prettier --write \"./**/*.{ts,js,json,md}\""
}
代码提交规范(可选)
如果想一步到位直接看full-featured-commit标题下的内容
首先初始化一下git仓库
sh
git init
这里涉及到的包括:commitlint+husky+cz-customable+commitlint-config-gitmoji
这里要用到cz-customable,但是他的导入导出方式是cjs,因此他的config文件:cz-config.js也是cjs格式,但是我们的项目会用到ts,所以package.json的type为"module",那么这就和配置文件的模式冲突了。
那么有同学问:如果把js文件改成cjs不就行了,但是cz-customable源代码中导入config文件的匹配格式是cz-config.js,会找不到config文件。
那么我们的解决办法是下载full-featured-cz(我自己魔改的让他支持cjs文件),如果有更好的方式还请小伙伴们在评论区教给我,我学习一下嘿嘿。
那让我们下载依赖
sh
pnpm add husky full-featured-cz commitlint commitlint-config-gitmoji --save-dev
创建cz-config.cjs文件
js
module.exports = {
types: [
{
value: ':sparkles: feat',
name: '✨ feat: 新功能'
},
{
value: ':bug: fix',
name: '🐛 fix: 修复 bug'
},
{
value: ':tada: init',
name: '🎉 init: 初始化'
},
{
value: ':pencil2: docs',
name: '✏️ docs: 文档变更'
},
{
value: ':lipstick: style',
name: '💄 style: 代码样式美化'
},
{
value: ':recycle: refactor',
name: '♻️ refactor: 重构'
},
{
value: ':zap: perf',
name: '⚡️ perf: 性能优化'
},
{
value: ':white_check_mark: test',
name: '✅ test: 测试'
},
{
value: ':rewind: revert',
name: '⏪️ revert: 回退'
},
{
value: ':package: build',
name: '📦️ build: 打包'
},
{
value: ':rocket: chore',
name: '🚀 chore: 构建/工程依赖/工具'
},
{
value: ':construction_worker: ci',
name: '👷 ci: CI 相关变更'
}
],
messages: {
type: '请选择提交类型(必填)',
customScope: '请输入文件修改范围(可选)',
subject: '请简要描述提交(必填)',
body: '请输入详细描述(可选)',
breaking: '列出任何 BREAKING CHANGES(可选)',
footer: '请输入要关闭的 issue(可选)',
confirmCommit: '确定提交此说明吗?'
},
allowCustomScopes: true,
allowBreakingChanges: [':sparkles: feat', ':bug: fix'],
subjectLimit: 72
}
创建.commitlintrc.cjs
js
module.exports = {
extends: ['./node_modules/commitlint-config-gitmoji', 'cz'],
rules: {
'type-empty': [
2,
'never',
[
':art:',
':newspaper:',
':pencil:',
':memo:',
':zap:',
':fire:',
':books:',
':bug:',
':ambulance:',
':penguin:',
':apple:',
':checkered_flag:',
':robot:',
':green_ale:',
':tractor:',
':recycle:',
':white_check_mark:',
':microscope:',
':green_heart:',
':lock:',
':arrow_up:',
':arrow_down:',
':fast_forward:',
':rewind:',
':rotating_light:',
':lipstick:',
':wheelchair:',
':globe_with_meridians:',
':construction:',
':gem:',
':bookmark:',
':tada:',
':loud_sound:',
':mute:',
':sparkles:',
':speech_balloon:',
':bulb:',
':construction_worker:',
':chart_with_upwards_trend:',
':ribbon:',
':rocket:',
':heavy_minus_sign:',
':heavy_plus_sign:',
':wrench:',
':hankey:',
':leaves:',
':bank:',
':whale:',
':twisted_rightwards_arrows:',
':pushpin:',
':busts_in_silhouette:',
':children_crossing:',
':iphone:',
':clown_face:',
':ok_hand:',
':boom:',
':bento:',
':pencil2:',
':package:',
':alien:',
':truck:',
':age_facing_up:',
':busts_in_silhouette:',
':card_file_box:',
':loud-sound:',
':mute:',
':egg:',
':see-no-evil:',
':camera-flash:',
':alembic:',
':mag:',
':wheel-of-dharma:',
':label:'
]
],
'subject-empty': [2, 'never']
}
}
配置husky
sh
npx husky install
然后在husky下添加pre-commit文件(也可以在官方生成的_/下加入也可以,不过这样更直观一些)
yaml
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run lint && npm run format
这样我们的配置文件就添加结束了
🔧 full-featured-commit 🔧
如果觉得上面的配置浪费时间或者不想去操作,尝试一下🔧full-featured工具包
full-featured是我自己写的cli命令行工具,其实是一个工具包,旗下又一个工具包是full-featured-commit,功能是给项目一键生成代码风格检查和规范化、代码提交信息规范化和美化功能full-featured-commit会自动安装依赖,添加脚本,检查项目环境,添加config文件,从而达到开箱即用的效果,当然你可以选择不用git或者不添加eslint等,是一个可定制方案。
full-featured当然还有更多功能,其目的和核心观念就是给开发者提供便利,节省时间。
github:github.com/liliphoenix...
npm:www.npmjs.com/package/ful...
项目文档(正在补全):liliphoenix.github.io/full-featur...
liliphoenix.github.io/full-featur...
安装
sh
npm install full-featured-cli -g
full-featrued -v
给项目添加功能
sh
full-featured init --commit
rollup
配置文件
rollup配置其实不算复杂,其实看官方文档就可以自己写出来,我这里给出我的方案和解释,大家可以参考一下
首先创建一个rollup.config.default.js、rollup.config.dev.js和rollup.config.prod.js 这里我们配置了两个环境dev和prod所以我们要先写一个default文件
rollup.config.default.js
js
import commonjs from '@rollup/plugin-commonjs'
import resolve from '@rollup/plugin-node-resolve'
import babel from '@rollup/plugin-babel'
import json from '@rollup/plugin-json'
import esbuild from 'rollup-plugin-esbuild'
const plugins = [
// 转换成es5一下来适配低版本浏览器
babel({ babelHelpers: 'bundled' }),
// 用来插件可以让 Rollup 找到外部模块
resolve({
preferBuiltins: true
}),
// 识别和处理json文件
json(),
// 传唤成commonjs
commonjs(),
// 使用esbuild处理esm格式,和vite预处理一个原理
esbuild()
]
const entries = ['src/index.ts']
export default [
...entries.map((input) => ({
input,
output: [
{
file: input.replace('src/', 'lib/').replace('.ts', '.js'),
format: 'esm'
}
],
// 处理循环依赖直接外部引入即可
external: ['readable-stream', 'chalk', 'semver'],
plugins
}))
]
从上往下看,我们配置了几个plugins,然后因为rollup可以配置多入口,所以我们这里把input定义为一个数组,这样保证多入口配置,然后output->file是输出文件名字和路径,format是输出文件的格式,我们这里是esm格式即可,external的作用是为了防止一些个依赖出现循环依赖和冲突问题,我们将这些依赖设置为外部依赖
然后创建 rollup.config.dev.js和rollup.config.prod.js
js
//prod
import config from './rollup.config.default.js'
import replace from '@rollup/plugin-replace'
export default config.map((config) => {
config.plugins.push(
replace({
values: {
// 🌸 替换打包文件关于环境变量的部分
'process.env.NODE_ENV': JSON.stringify('development')
},
// 🌸 防止字符串后面有等号然后进行替换
preventAssignment: true
})
)
return config
})
//dev
import config from './rollup.config.default.js'
import replace from '@rollup/plugin-replace'
export default config.map((config) => {
config.plugins.push(
replace({
values: {
// 🌸 替换打包文件关于环境变量的部分
'process.env.NODE_ENV': JSON.stringify('production')
},
// 🌸 防止字符串后面有等号然后进行替换
preventAssignment: true
})
)
return config
})
然后我们添加一下脚本,这里给大家推荐一种开发方式,分别在开发目录打开两个终端,一个终端输入npm run dev
这样rollup会监听入口文件的变化,只要入口文件内容变化就会自动打包,因为我们用了esbuild
所以打包速度很快,然后在另一个终端测试功能即可
脚本添加
json
"dev": "rimraf ./lib && rollup -c rollup.config.dev.js --watch",
"build": "rimraf ./lib && rollup -c rollup.config.prod.js",
入口文件添加
在 bin/index.js 添加 一定不要忘记写上面那句注释!!!这是告诉系统用node.js来解析该文件
js
#!/usr/bin/env node
import main from '../lib/index.js'
main()
在 src/index.ts 添加
ts
funciton main(){
console.log("\nHello David Tao\n")
}
export default main
然后我们运行一下
sh
npm run dev
node ./bin/index.js
打包成功📦
运行成功✅
剩下的内容我们下一篇文章来讲
🔧 手把手教你开发一个自己的cli工具(二) :juejin.cn/post/735947...