项目中一键添加husky实现详解
作者:Yomuki
关于husky
前置条件:项目已关联了git。
husky有什么用?
当我们commit message时,可以进行测试和lint操作,保证仓库里的代码是优雅的。 当我们进行commit操作时,会触发pre-commit,在此阶段,可进行test和lint。其后,会触发commit-msg,对commit的message内容进行验证。
pre-commit
一般的lint会全局扫描,但是在此阶段,我们仅需要对暂存区的代码进行lint即可。所以使用lint-staged插件。
commit-msg
在此阶段,可用 @commitlint/cli @commitlint/config-conventional 对提交信息进行验证。但是记信息格式规范真的太太太太麻烦了,所以可用 commitizen cz-git 生成提交信息。
一键添加husky
从上述说明中,可以得出husky配置的基本流程:
- 安装husky;安装lint-staged @commitlint/cli @commitlint/config-conventional commitizen cz-git
- 写commitlint和lint-staged的配置文件
- 修改package.json中的scripts和config
- 添加pre-commit和commit-msg钩子
看上去简简单单轻轻松松,那么,开干!
先把用到的import拿出来溜溜
import { red, cyan, green } from "kolorist"; // 打印颜色文字 import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs"; import { resolve } from "node:path"; import { cwd } from "node:process"; import prompts from "prompts";// 命令行交互提示 import { fileURLToPath } from "node:url"; import { getLintStagedOption } from "./src/index.js";// 获取lint-staged配置 ,后头说 import { createSpinner } from "nanospinner"; // 载入动画(用于安装依赖的时候) import { exec } from "node:child_process";
package验证
既然是为项目添加,那当然得有package.json文件啦!
const projectDirectory = cwd(); const pakFile = resolve(projectDirectory, "package.json"); if (!existsSync(pakFile)) { console.log(red("未在当前目录中找到package.json,请在项目根目录下运行哦~")); return; }
既然需要lint,那当然也要eslint/prettier/stylelint啦~
const pakContent = JSON.parse(readFileSync(pakFile)); const devs = { ...(pakContent?.devDependencies || {}), ...(pakContent?.dependencies || {}), }; const pakHasLint = needDependencies.filter((item) => { return item in devs; });
但是考虑到有可能lint安装在了全局,所以这边就不直接return了,而是向questions中插入一些询问来确定到底安装了哪些lint。
const noLintQuestions = [ { type: "confirm", name: "isContinue", message: "未在package.json中找到eslint/prettier/stylelint,是否继续?", }, { // 处理上一步的确认值。如果用户没同意,抛出异常。同意了就继续 type: (_, { isContinue } = {}) => { if (isContinue === false) { throw new Error(red("✖ 取消操作")); } return null; }, name: "isContinueChecker", }, { type: "multiselect", name: "selectLint", message: "请选择已安装的依赖:", choices: [ { title: "eslint", value: "eslint", }, { title: "prettier", value: "prettier", }, { title: "stylelint", value: "stylelint", }, ], }, ]; const questions = pakHasLint.length === 0 ? [...noLintQuestions, ...huskyQuestions] : huskyQuestions; // huskyQuestions的husky安装的询问语句,下面会讲
husky安装询问
因为不同的包管理器有不同的安装命令,以及有些项目会不需要commit msg验证。所有就会有以下询问的出现啦
const huskyQuestions = [ { type: "select", name: "manager", message: "请选择包管理器:", choices: [ { title: "npm", value: "npm", }, { title: "yarn1", value: "yarn1", }, { title: "yarn2+", value: "yarn2", }, { title: "pnpm", value: "pnpm", }, { title: "pnpm 根工作区", value: "pnpmw", }, ], }, { type: "confirm", name: "commitlint", message: "是否需要commit信息验证?", }, ];
使用prompts进行交互提示
let result = {}; try { result = await prompts(questions, { onCancel: () => { throw new Error(red("❌Bye~")); }, }); } catch (cancelled) { console.log(cancelled.message); return; } const { selectLint, manager, commitlint } = result;
这样子,我们就获取到了:
- manager 项目使用的包管理
- commitlint 是否需要commit msg验证
- selectLint 用户自己选择的已安装的lint依赖
生成命令
通过manager和commitlint,可以生成要运行的命令
const huskyCommandMap = { npm: "npx husky-init && npm install && npm install --save-dev ", yarn1: "npx husky-init && yarn && yarn add --dev ", yarn2: "yarn dlx husky-init --yarn2 && yarn && yarn add --dev ", pnpm: "pnpm dlx husky-init && pnpm install && pnpm install --save-dev ", pnpmw: "pnpm dlx husky-init && pnpm install -w && pnpm install --save-dev -w ", }; const preCommitPackages = "lint-staged"; const commitMsgPackages = "@commitlint/cli @commitlint/config-conventional commitizen cz-git"; // 需要安装的包 const packages = commitlint ? `${preCommitPackages} ${commitMsgPackages}` : preCommitPackages; // 需要安装的包的安装命令 const command = `${huskyCommandMap[manager]}${packages}`; const createCommitHook = `npx husky set .husky/pre-commit "npm run lint:lint-staged"`; const createMsgHook = `npx husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'`; // 需要创建钩子的命令 const createHookCommand = commitlint ? `${createCommitHook} && ${createMsgHook}` : createCommitHook;
lint-staged 配置
一般的lint-staged.config.js长这样:
module.exports = { "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"], "{!(package)*.json,*.code-snippets,.!(browserslist)*rc}": ["prettier --write--parser json"], "package.json": ["prettier --write"], "*.vue": ["eslint --fix", "prettier --write", "stylelint --fix"], "*.{scss,less,styl,html}": ["stylelint --fix", "prettier --write"], "*.md": ["prettier --write"], };
所以呢,需要根据项目使用的lint来生成lint-staged.config.js:
// 简单粗暴的函数 export function getLintStagedOption(lint) { const jsOp = [], jsonOp = [], pakOp = [], vueOp = [], styleOp = [], mdOp = []; if (lint.includes("eslint")) { jsOp.push("eslint --fix"); vueOp.push("eslint --fix"); } if (lint.includes("prettier")) { jsOp.push("prettier --write"); vueOp.push("prettier --write"); mdOp.push("prettier --write"); jsonOp.push("prettier --write--parser json"); pakOp.push("prettier --write"); styleOp.push("prettier --write"); } if (lint.includes("stylelint")) { vueOp.push("stylelint --fix"); styleOp.push("stylelint --fix"); } return { "*.{js,jsx,ts,tsx}": jsOp, "{!(package)*.json,*.code-snippets,.!(browserslist)*rc}": jsonOp, "package.json": pakOp, "*.vue": vueOp, "*.{scss,less,styl,html}": styleOp, "*.md": mdOp, }; } // lint-staged.config.js 内容 const lintStagedContent = `module.exports =${JSON.stringify(getLintStagedOption(selectLint || pakHasLint))}`; // lint-staged.config.js 文件 const lintStagedFile = resolve(projectDirectory, "lint-staged.config.js");
commitlint 配置
因为commitlint.config.js中的配置过于复杂。所以,我选择在安装完依赖后直接copy文件!被copy的文件内容:
// @see: https://cz-git.qbenben.com/zh/guide /** @type {import('cz-git').UserConfig} */ module.exports = { ignores: [(commit) => commit.includes("init")], extends: ["@commitlint/config-conventional"], // parserPreset: "conventional-changelog-conventionalcommits", rules: { // @see: https://commitlint.js.org/#/reference-rules "body-leading-blank": [2, "always"], "footer-leading-blank": [1, "always"], "header-max-length": [2, "always", 108], "subject-empty": [2, "never"], "type-empty": [2, "never"], "subject-case": [0], "type-enum": [2, "always", ["feat", "fix", "docs", "style", "refactor", "perf", "test", "build", "ci", "chore", "revert"]], }, prompt: { alias: { fd: "docs: fix typos" }, messages: { type: "选择你要提交的类型 :", scope: "选择一个提交范围(可选):", customScope: "请输入自定义的提交范围 :", subject: "填写简短精炼的变更描述 :\n", body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n', breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n', footerPrefixsSelect: "选择关联issue前缀(可选):", customFooterPrefixs: "输入自定义issue前缀 :", footer: "列举关联issue (可选) 例如: #31, #I3244 :\n", confirmCommit: "是否提交或修改commit ?", }, types: [ { value: "feat", name: "feat: 🚀新增功能 | A new feature", emoji: "🚀" }, { value: "fix", name: "fix: 🐛修复缺陷 | A bug fix", emoji: "🐛" }, { value: "docs", name: "docs: 📚文档更新 | Documentation only changes", emoji: "📚" }, { value: "style", name: "style: 🎨代码格式 | Changes that do not affect the meaning of the code", emoji: "🎨" }, { value: "refactor", name: "refactor: 📦代码重构 | A code change that neither fixes a bug nor adds a feature", emoji: "📦", }, { value: "perf", name: "perf: ⚡️性能提升 | A code change that improves performance", emoji: "⚡️" }, { value: "test", name: "test: 🚨测试相关 | Adding missing tests or correcting existing tests", emoji: "🚨" }, { value: "build", name: "build: 🛠构建相关 | Changes that affect the build system or external dependencies", emoji: "🛠" }, { value: "ci", name: "ci: 🎡持续集成 | Changes to our CI configuration files and scripts", emoji: "🎡" }, { value: "revert", name: "revert: ⏪️回退代码 | Revert to a commit", emoji: "⏪️" }, { value: "chore", name: "chore: 🔨其他修改 | Other changes that do not modify src or test files", emoji: "🔨" }, ], useEmoji: true, emojiAlign: "center", themeColorCode: "", scopes: [], allowCustomScopes: true, allowEmptyScopes: true, customScopesAlign: "bottom", customScopesAlias: "custom | 以上都不是?我要自定义", emptyScopesAlias: "empty | 跳过", upperCaseSubject: false, markBreakingChangeMode: false, allowBreakingChanges: ["feat", "fix"], breaklineNumber: 100, breaklineChar: "|", skipQuestions: [], issuePrefixs: [ // 如果使用 gitee 作为开发管理 { value: "link", name: "link: 链接 ISSUES 进行中" }, { value: "closed", name: "closed: 标记 ISSUES 已完成" }, ], customIssuePrefixsAlign: "top", emptyIssuePrefixsAlias: "skip | 跳过", customIssuePrefixsAlias: "custom | 自定义前缀", allowCustomIssuePrefixs: true, allowEmptyIssuePrefixs: true, confirmColorize: true, maxHeaderLength: Infinity, maxSubjectLength: Infinity, minSubjectLength: 0, scopeOverrides: undefined, defaultBody: "", defaultIssues: "", defaultScope: "", defaultSubject: "", }, };
被复制的路径,和目标路径
const commitlintFile = resolve(projectDirectory, "commitlint.config.js"); const commitlintFileTemplateDir = resolve(fileURLToPath(import.meta.url), "../src/template", "commitlint.config.js");
准备就绪,开始安装!
- 执行刚刚生成的安装命令
- 更改package.json内容
- 写入配置文件
- 添加钩子
const spinner = createSpinner("Installing packages...").start(); exec(`${command}`, { cwd: projectDirectory }, (error) => { if (error) { spinner.error({ text: red("Failed to install packages!"), mark: "✖", }); console.error(error); return; } /* 更改package.json内容 开始 */ let newPakContent = JSON.parse(readFileSync(pakFile));// 获取最新的包内容 newPakContent.scripts = { ...newPakContent.scripts, "lint:lint-staged": "lint-staged", commit: "git add . && git-cz", }; newPakContent.config = { ...(newPakContent?.config || {}), commitizen: { path: "node_modules/cz-git", }, }; writeFileSync(pakFile, JSON.stringify(newPakContent));// 写入 /* 更改package.json内容 结束 */ writeFileSync(lintStagedFile, lintStagedContent);// 写入lint-staged配置 copyFileSync(commitlintFileTemplateDir, commitlintFile);// 复制commitlint配置至项目中 spinner.success({ text: green("安装成功~准备添加钩子! 🎉"), mark: "✔" });// 包安装成功啦~ const hookSpinner = createSpinner("添加husky钩子中...").start();// 开始装钩子 exec(`${createHookCommand}`, { cwd: projectDirectory }, (error) => { if (error) { hookSpinner.error({ text: red(`添加钩子失败,请手动执行${createHookCommand}`), mark: "✖", }); console.error(error); return; } hookSpinner.success({ text: green("一切就绪! 🎉"), mark: "✔" });// 钩子安装成功啦~一切ok~~ }); });
发包
最后,发下包,就可以在其他项目中使用啦
结尾
这个是本萌新因为懒又想把git提交规范下又不想每次创项目都要翻文档安装的产物,没有经过测试,中间部分代码会有更好的解决方案~
本代码仓库
以上就是项目中一键添加husky实现详解的详细内容,更多关于项目一键添加husky的资料请关注脚本之家其它相关文章!