【从零到一手撕脚手架 | 第五节】自定义命令行下载cli工具

【从零到一手撕脚手架 | 第五节】自定义命令行下载cli工具

Hello大家好我是⛄,之前我们已经成功搭建了一套Vue3的快速开发模板,提高我们搭建新项目的效率,但是当我们的模板逐渐增多,如果依然使用git clone的方式去下载模板较为繁琐。为了解决这个问题,我们就可以自己去搭建一个命令行交互式的工具包去生成我们需要的模板。

  • GitHub

    • Vue3开发模板:LonelySnowman/sv3-template
    • 自定义cli工具包:LonelySnowman/arceus-cli
  • 官方文档:SV3-Family | Vue3

基础结构

  • 我们这次的目标就是搭建一个类似于vue-cli,create-react-app等cli工具类似的工具包。要实现的核心功能就是使用命令行交互的效果去生成我们需要的Vue项目模板。
  • 首先把项目文件结构创建一下,一步步教大家实现。
arceus-cli/|- bin/ # node 命令配置|- src/ # 项目资源|- command/  # 命令逻辑|- utils/   # 公共方法|- constants.ts  # 存放公共变量|- types.ts  # 类型文件|- index.ts  # 命令入口文件

用到的依赖

建议大家可以预先去了解下这些依赖的用途和一些基础的使用方法。

Tip:大家注意chalklog-symbols都要安装4版本,因为高版本已经不支持commonjs,但是我们最后输出的是commonjs模块。

  • 命令行交互

    • commander:解析命令行指令
    • ora:终端加载动画
    • progress-estimator:终端加载条动画
    • log-symbols:终端输出符号
    • chalk:终端字体美化
    • @inquirer/prompts:终端输入交互
  • 打包工具

    • rollup(打包工具有很多选择,webpack,vite,rollup,tsup…)

      • 这里选用 rollup 是因为它相对更适合 npm 包的打包,并且之前没有使用过,尝试一下,大家也可以选用其他打包工具
    • @rollup/plugin-node-resolve:支持rollup打包node.js模块

    • @rollup/plugin-commonjs:支持rollup打包commonjs模块

    • @rollup/plugin-json:支持rollup打包json文件

    • rollup-plugin-typescript2:支持rollup打包ts文件

    • @rollup/plugin-terser:压缩打包代码

    • rollup-plugin-node-externals:使rollup自动识别外部依赖

配置打包命令

  • 我们先解决打包的问题,安装好需要的依赖,然后按照下面的配置文件内容,新建一个rollup.config.js
npm install -D rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-json rollup-plugin-typescript2 @rollup/plugin-terser rollup-plugin-node-externals
import { defineConfig } from 'rollup';
import nodeResolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import externals from "rollup-plugin-node-externals";
import json from "@rollup/plugin-json";
import terser from "@rollup/plugin-terser";
import typescript from 'rollup-plugin-typescript2';export default defineConfig([{input: {index: 'src/index.ts', // 打包入口文件},output: [{dir: 'dist', // 输出目标文件夹format: 'cjs', // 输出 commonjs 文件}],// 这些依赖的作用上文提到过plugins: [nodeResolve(),externals({devDeps: false, // 可以识别我们 package.json 中的依赖当作外部依赖处理 不会直接将其中引用的方法打包出来}),typescript(),json(),commonjs(),terser(),],},
]);

我们还需要在package.json中配置一个打包命令。

  • -c 指定 rollup 配置文件,–bundleConfigAsCjs 将配置转为 commonjs 执行。
{// ..."build": "rollup -c rollup.config.js --bundleConfigAsCjs"
}

编写指令

指令交互

  • 这里带大家写一个create指令,在我们的入口文件src/index.ts编写。
  • 我们需要用到commander,可以帮助我们解析用户在命令行输入的指令。
  • 这里会给大家讲解一些基础的用法,更详细的使用方式请查阅官方文档:commander.js。

首先初始化一个Command对象,传入的参数作为我们的指令名称。

import { Command } from 'commander'
// 这里我们用 arceus 当作我的指令名称
// 命令行中使用 arceus xxx 即可触发
const program = new Command('arceus');

接下来我们就可以配置我们需要的命令了。

  • 使用version可以实现最基础的查看版本的指令。
import { version } from '../package.json'
// .vesion 表示可以使用 -V --version 参数查看当前SDK版本
// 我们直接使用 package.json 中的 version 即可
program.version(version);
// 调用 version 的参数可以自定义
// .version(version, '-v --version')
  • 使用commandaction实现自定义指令。

    • command 为我们需要的命令名称。
    • description 为命令添加描述。
    • action 为指令触发执行的回调。
    • argument 为我们命令需要的参数,[]包裹代表可选,<>包裹代表必填。

下面的示例就是我们编写好的指令,指令回调我们稍后实现,输入arceus update会打印update command,输入arceus create test,会打印create test。action 回调中会将 argument 中的参数传入。

// ...
​
program.command('update').description('更新 arceus 至最新版本').action(async () => {console.log('update command')});
​
program.command('create').description('创建一个新项目').argument('[name]', '项目名称').action(async (name) => {if(name) console.log(`create ${name}`)else console.log(`create command`)});
  • 解析指令
// ...// parse 会解析 process.argv 中的内容
// 也就是我们输入的指令
program.parse();

输出提示

  • 在编写命令代码前我们先封装一个公共方法。
  • src/utils/log.ts中封装一个带icon的输出提示。
  • 我们需要用到log-symbols,他内置了 error,success,warning,info 对应的 icon ,并且帮我们兼容不支持 icon 的终端。并且后续我们需要用到的ora作为加载动画,它也是用的log-symbols进行提示,我们这里保持一致。(大家也可以自定义一些emoji图标,效果也不错,就是需要自己兼容终端不支持emoji的情况)
import logSymbols from 'log-symbols'export const log = {error: (msg: string) => {console.log(logSymbols.error, msg)},success: (msg: string) => {console.log(logSymbols.success, msg)},warning: (msg: string) => {console.log(logSymbols.warning, msg)},info: (msg: string) => {console.log(logSymbols.info, msg)},
}export default log

下载项目

我们先实现create命令,可以让用户选择下载我们预设的模板。

  • src/command/create.ts文件下编写create命令核心代码。
  • 导出一个可以传入项目名称的方法,如果用户直接传入了项目名称则让用户选择模板,否则需要先让用户输入项目名称。
  • 这里我们用到了@inquirer/prompts,可以帮助我们让用户在终端进行输入或选择的操作,更多使用方法请查阅官方文档:inquirer.js。
import { select, input } from '@inquirer/prompts';
export default async function create(prjName?: string) {// 文件名称未传入需要输入if (!prjName) prjName = await input({ message: '请输入项目名称' });// 如果文件已存在需要让用户判断是否覆盖原文件const filePath = path.resolve(process.cwd(), prjName)if (fs.existsSync(filePath)) {const run = await isOverwrite(prjName)if (run) {await fs.remove(filePath)} else {return // 不覆盖直接结束}}
}
  • src/utils/file.ts中封装一个判断用户是否覆盖的公共方法。
import { select } from '@inquirer/prompts';
import log from "./log";export const isOverwrite = async (fileName: string) => {log.warning(`${fileName} 文件已存在 !`)return select({message: '是否覆盖原文件: ',choices: [{name: '覆盖', value: true},{name: '取消', value: false}]});
}
  • 然后我们就需要让用户选择我们的预设模板,这里在src/constants.ts保存我们拥有的模板,定义成map的形式是方便我们根据key获取项目的信息。
  • 下载模板的方式有很多种,可以将模板文件保存在 SDK 中,使用 cjs 或者其他方法动态选择生成,使用 fs 模块写入,或者存放在 git 仓库中进行 clone,这里我们选用后者,其他方法大家可以自行探索。
  • 这里我定义了 TemplateInfo 类型,大家可以根据自己的需要自行定义,需要存储项目名称,下载地址,描述,代码分支。
import { TemplateInfo } from "./types";// 这里保存了我写好的两个预设模板 感兴趣的大家可以看看往期文章
export const templates: Map<string, TemplateInfo> = new Map([["sv3-template", {name: "sv3-template",downloadUrl: 'git@github.com:LonelySnowman/sv3-template.git',description: 'vue3快速开发模板',branch: 'main'}],["sv3-template-thin", {name: "sv3-template-thin",downloadUrl: 'git@github.com:LonelySnowman/sv3-template.git',description: 'vue3快速开发模板(精简版)',branch: 'thin'}]]
)
  • 接下来我们就可以让用户选择需要的模板。
import { select, input } from '@inquirer/prompts';
import { templates } from "../constants";
import { TemplateInfo } from "../types";
import log from "../utils/log";export default async function create(prjName?: string) {// ...// 我们需要将我们的 map 处理成 @inquirer/prompts select 需要的形式// 大家也可以封装成一个方法去处理const templateList = [...templates.entries()].map((item: [string, TemplateInfo]) => {const [name, info] = item;return {name,value: name,description: info.description}})// 选择模板const templateName = await select({message: '请选择需要初始化的模板:',choices: templateList,});// 下载模板const gitRepoInfo = templates.get(templateName)if (gitRepoInfo) {await clone(gitRepoInfo.downloadUrl , prjName, ['-b', `${gitRepoInfo.branch}`])} else {log.error(`${templateName} 模板不存在`)}
}
  • 我们还需要实现我们刚刚使用过的clone方法,下载仓库中的模板。

  • 我们在src/utils/clone.ts中实现下。

    • 这里我们用到,simple-git用于拉取 git 仓库,progress-estimator设置预估git clone的时间并展示进度条。
  • 这里我就直接展示代码和注释了,思路都很简单。

import simpleGit, { SimpleGit, SimpleGitOptions } from 'simple-git';
import log from "./log";
import createLogger from "progress-estimator";
import chalk from "chalk";const logger = createLogger({ // 初始化进度条spinner: {interval: 300, // 变换时间 msframes: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'].map(item=>chalk.blue(item)) // 设置加载动画}
})const gitOptions: Partial<SimpleGitOptions> = {baseDir: process.cwd(), // 根目录binary: 'git',maxConcurrentProcesses: 6, // 最大并发进程数
};export const clone = async (url: string, prjName: string, options: string[]): Promise<any> => {const git: SimpleGit = simpleGit(gitOptions)try {// 开始下载代码并展示预估时间进度条await logger(git.clone(url, prjName, options), '代码急速下载中: ', {estimate: 7000 // 展示预估时间})// 下面就是一些相关的提示console.log()console.log(chalk.blueBright(`==================================`))console.log(chalk.blueBright(`=== 欢迎使用 arceus-cli 脚手架 ===`))console.log(chalk.blueBright(`==================================`))console.log()
​log.success(`项目创建成功 ${chalk.blueBright(prjName)}`)log.success(`执行以下命令启动项目:`)log.info(`cd ${chalk.blueBright(prjName)}`)log.info(`${chalk.yellow('pnpm')} install`)log.info(`${chalk.yellow('pnpm')} run dev`)} catch (err: any) {log.error("下载失败")log.error(String(err))}
}
  • 至此,我们的create命令就编写完毕了,我们可以将其添加到src/index.ts中去调用。
// ...
program.command('create').description('创建一个新项目').argument('[name]', '项目名称').action(async (dirName) => {await create(dirName);});
// ...

检测项目更新

  • 当我们更新模板后,希望用户第一时间用到,可以在用户使用过程中添加一些更新提示。

src/utils/npm.ts中编写方法,用于获取npm包的信息及版本号。

// npm 包提供了根据包名称查询包信息的接口
// 我们在这里直接使用 axios 请求调用即可
export const getNpmInfo = async (npmName: string) => {const npmUrl = 'https://registry.npmjs.org/' + npmNamelet res = {}try {res = await axios.get(npmUrl)} catch (err) {log.error(err as string)}return res
}

npm包信息中包含了该包的最新版本,我们在这里直接引用即可。

export const getNpmLatestVersion = async (npmName: string) => {// data['dist-tags'].latest 为最新版本号const { data } = (await getNpmInfo(npmName)) as AxiosResponsereturn data['dist-tags'].latest
}

然后对比版本号版本,判断是否需要更新,如需更新进行提示。

export const isNeedUpdate = async (name: string, curVersion: string) => {const latestVersion = await getNpmLatestVersion(name)const need = lodash.gt(latestVersion, curVersion)if(need) {log.info(`检测到 arceus 最新版:${chalk.blueBright(latestVersion)} 当前版本:${chalk.blueBright(curVersion)} ~`)log.info(`可使用 ${chalk.yellow('npm')} install arceus-cli@latest 更新 ~`)}return need
}

然后我们将这个判断更新的方法添加到create方法中。

export default async function create(prjName?: string) {// ...await isNeedUpdate(name, version) // 检测版本更新// ...
}

当我们发布新的版本,用户可以第一时间看到。

如何调用

  • 我们已经完成了核心的代码逻辑,现在想要使用命令行去调用我们编写好的逻辑,我们可以先自己在本地执行测试,然后将其上传到 npm 就可以供他人使用了。

本地调试

  • 我们之前已经配置好了 rollup 打包的脚本,接下来就可以执行 npm run build,打包后的代码会输出到dist/index.js中。
  • 我们可以使用node在本地执行,先测试一下我们编写好的create命令。
node .\dist\index.js create

不出意外是可以看到我们写好的交互逻辑,如果有报错,大家可以根据对应的问题查询下,也可以给我留言。

发布npm包

  • 本地调试没有问题后我们就可以将其发布在npm上。
  • npm账号注册、登录等基础操作,这里就不过多赘述了,主要讲一下如何让发布的包能以arceus作为命令名调用。

需要我们修改一下package.json文件,下面是一些必要的配置,都加上了注释,我们需要重点关注bin这一项。

  • bin中的配置是一个对象,需要有 “key” 和 “value”。

    • key 会被放置在 node_modules 的 .bin 目录中,value 是 key 对应需要执行的文件。
    • 我们使用 npx arceus 就会调用我们的 bin/index.js。
    • 当我们全局安装对应包的时候会放在全局的 node_modules 的 .bin 目录中,相当于添加了系统环境变量,这样我们就可以直接在终端中调用。
{"name": "arceus-cli", // 包名称"version": "x.x.x", // 包版本"description": "arceus脚手架", // 包描述"main": "dist/index.js", // 库入口文件"engines": { // 推荐使用 node 版本号"node": ">=16"},"keywords": [ // 包查询关键词"sv3-template"],"files": [ // npm 包需要上传的文件"dist","bin","README.md"],"author": { // 作者信息"name": "lonelysnowman"},"bin": {"arceus": "bin/index.js" // npm 会在 .bin 目录中配置 arceus 执行 bin/index.js},// ...
}

编写bin/index.js

#!/usr/bin/env node
require('../dist'); // 执行我们打包好的 dist/index.js 文件

需要在第一行加入#!/usr/bin/env node/usr/bin/env就是告诉系统可以在PATH目录中查找,#!/usr/bin/env node就是解决了不同的用户node路径不同的问题,可以让系统动态的去查找node来执行你的脚本文件。

然后我们就可以将npm包发布啦。

npm login # 发布前需要先登录下npm publish # 会按照我们 package.json 中的 files 配置的文件发布 name 作为包名称# 如果需要迭代包的版本 要先修改版本号再发布
npm version patch # 0.0.0 -> 0.0.1
npm version minor # 0.0.0 -> 0.1.0
npm version major # 0.0.0 -> 1.0.0

发布完成后我们就可以安装npm全局包然后进行使用啦。

npm install arceus-cli -g

结语

到这里我们就解决了所有的问题,实现了一个简易的cli工具,做到了clone在git仓库中的固定模板。如果我们想实现动态模板就需要使用其他的技术,可以使用字符串插值或者cjs模板,根据用户的选择动态生成需要的模板代码。大家可以根据自己的需要自行拓展😀。

该项目已开源:LonelySnowman/arceus-cli,可以使用npm install arceus-cli -g安装后使用arceus create进行体验。

参考学习项目

  • pure-admin-cli

如果有任何不正确的地方请指正,我会及时更改。

更文不易,如果对你有帮助的话,请给我点个赞吧👍

关注我,后续文章不迷路⛄

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/798232.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用LIKE进行模糊查询

查询包含字符‘e’的信息 % 代表不确定个数的字符&#xff08;零个或多个&#xff09; SELECT employee_id, first_name FROM employees WHERE first_name LIKE %e%; 查询以字符‘e’开头的字符 SELECT employee_id, first_name FROM employees WHERE first_name LIKE e%; _…

腾讯云2024年优惠券领取及使用常见问题

腾讯云作为国内领先的云计算服务提供商&#xff0c;经常会推出各种优惠活动&#xff0c;以此来吸引用户上云。其中&#xff0c;优惠券作为一种常见的促销方式&#xff0c;受到了众多用户的青睐。然而&#xff0c;在领取和使用优惠券的过程中&#xff0c;大家可能会遇到一些常见…

1.《C语言》—— [常见概念]

前言: C语言是学习编程的一门语言&#xff0c;C语言概念少&#xff0c;词汇少&#xff0c;包含了基本的编程元素&#xff0c;再后来的很多语言如&#xff08;C&#xff0c;Java&#xff09;等都参考了C语言&#xff0c;所以想要学好编程&#xff0c;C语言是必不可少的一门&…

77、WAF攻防——权限控制代码免杀异或运算变量覆盖混淆加密传参

文章目录 WAF规则webshell免杀变异 WAF规则 函数匹配 工具指纹 webshell免杀变异 php 传参带入 eval可以用assert来替换,assert也可以将字符串当作php代码执行漏洞 php 变量覆盖 php 加密 使用加密算法对php后门进行加密 php 异或运算 简化:无字符webshellP 无数字字母rc…

《米小圈上学记》——让孩子爱上阅读一点也不难!

阅读能力的培养是小学语文素质教育重要的组成部分&#xff0c;阅读能力的高低&#xff0c;直接关系到学生的理解能力、运用知识的能力以及表达能力的提升。提高小学生的阅读能力不仅关系到小学生语文素养的培养&#xff0c;而且对他们开阔视野、提高内涵、增加底蕴、放飞心灵有…

hibernate执行外部sql

开发背景 公司多年前项目&#xff0c;使用hibernate作为持久层&#xff0c;部分sql查询采用spring的JdbcTemplate&#xff0c;sql穿插在java代码中。因此&#xff0c;需要统一使用hibernate&#xff0c;并且sql部分需要类似Mybatis一样从文件中读取。由于引入Mybatis需要对项目…

哈希存节点,双dp数组存选和不选

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口&#xff0c;我们称之为 root 。 除了 root 之外&#xff0c;每栋房子有且只有一个“父“房子与之相连。一番侦察之后&#xff0c;聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连…

Linux:安装zabbix-agent被监控端(2)

本章是结合着上一篇文章的续作 Linux&#xff1a;部署搭建zabbix6&#xff08;1&#xff09;-CSDN博客https://blog.csdn.net/w14768855/article/details/137426966?spm1001.2014.3001.5501本章将在两台centos部署agent端&#xff0c;然后使用server进行连接监控 agent1 在1…

11、子串-滑动窗口最大值

题解&#xff1a; 双端队列是一种特殊的队列&#xff0c;允许你在队列的两端进行插入和删除操作。在滑动窗口问题中&#xff0c;我们使用它来存储可能是当前窗口最大值的元素的索引。 维护队列的顺序&#xff1a; 当新元素进入窗口时&#xff0c;我们将它与队列尾部的元素进…

RSA相关学习存档

什么是RSA&#xff0c;以及RSA算法&#xff1a; https://zhuanlan.zhihu.com/p/450180396 https://blog.csdn.net/m0_51607907/article/details/123884953 https://blog.csdn.net/firechungelaile/article/details/39974379 https://blog.csdn.net/lesczx/article/details/…

【LeetCode热题100】4. 寻找两个正序数组的中位数(二分)

一.题目要求 给定两个大小分别为 m 和 n 的正序&#xff08;从小到大&#xff09;数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。 算法的时间复杂度应该为 O(log (mn)) 。 二.题目难度 困难 三.输入样例 示例 1&#xff1a; 输入&#xff1a;nums1 [1,3…

子集(迭代)(leetcode 78)

核心逻辑&#xff1a; 根据子数组包含的元素个数迭代&#xff1a; 现有子集的基础上通过添加这个新元素来翻倍子集的数量 f(n)2f(n−1) vector<vector<int>> subsets(vector<int>& nums) {vector<vector<int>> ans;int i,j,k;ans.p…

《科技创业月刊》是什么级别的期刊?是正规期刊吗?能评职称吗?

问题解答&#xff1a;问&#xff1a;《科技创业月刊》是什么级别的刊物&#xff1f; 答&#xff1a;省级&#xff0c;主管单位&#xff1a; 湖北省科学技术厅 &#xff1b;主办单位&#xff1a;湖北省科技信息研究院 问&#xff1a;《科技创业月刊》是c刊吗&#xff1f; 答&…

spring面试八股

常用的注册bean的方式 ComponentScan扫描到的service和Controller等的注解 Configration配置类或者是xml文件的定义。 spring中有几种依赖注入的方式 1.构造器注入。 2.setter方法注入。 3.使用field属性的方式注入。 applicationContext是什么 spring bean spring aop Aop…

Linux命令-dpkg-deb命令(Debian Linux下的软件包管理工具)

说明 dpkg-deb命令 是Debian Linux下的软件包管理工具&#xff0c;它可以对软件包执行打包和解包操作以及提 供软件包信息。 语法 dpkg-deb(选项)(参数)选项 -c&#xff1a;显示软件包中的文件列表&#xff1b; -e&#xff1a;将主控信息解压&#xff1b; -f&#xff1a;把…

java种Hutools常用方法

目录 一、依赖二、Convert三、DateUtil四、StrUtil五、ReflectUtil六、IdUtil七、RandomUtil八、BeanUtil九、JSONUtil Hutool是一个小而全的Java工具类库&#xff0c;通过静态方法封装&#xff0c;降低相关API的学习成本&#xff0c;提高工作效率&#xff0c;使Java拥有函数式…

2-django、http、web框架、django及django请求生命周期、路由控制、视图层

1 http 2 web框架 3 django 3.1 django请求生命周期 4 路由控制 5 视图层 1 http #1 http 是什么 #2 http特点 #3 请求协议详情-请求首行---》请求方式&#xff0c;请求地址&#xff0c;请求协议版本-请求头---》key:value形式-referer&#xff1a;上一次访问的地址-user-agen…

【算法-数组】移除元素

这里写自定义目录标题 暴力解法双指针思路 leecode27 : https://leetcode.cn/problems/remove-element/submissions/521113648/ 暴力解法 循环匹配&#xff0c;每次匹配到就将数组匹配到的元素的后面元素向前移动一位 【注意】 注意最后一位元素&#xff0c;避免数组越界 pu…

tcp/ip细节

主动方: 发送fin进入fin_wait1,收到fin的ack进入fin_wait2&#xff0c;发送fin时接收缓冲区还有数据 - 应用层不读数据你就close?发reset告知对端你出问题了 默认&#xff1a;linger关闭&#xff0c;close调用立即返回(发出发送缓冲区未发出数据和fin后) linger开启&#xff0…

Sora是什么?Sora怎么使用?Sora最新案例视频以及常见问题答疑

Sora 是什么&#xff1f; 2024年2月16日&#xff0c;OpenAI 在其官网上面正式宣布推出文本生成视频的大模型Sora 这样说吧给你一段话&#xff0c; 让你写一篇800字的论文&#xff0c;你的理解很可能都有偏差&#xff0c;那么作为OpenAi要做文生视频到底有多难&#xff0c;下面…