項(xiàng)目中一鍵添加husky實(shí)現(xiàn)詳解
關(guān)于husky
前置條件:項(xiàng)目已關(guān)聯(lián)了git。
husky有什么用?
當(dāng)我們commit message時(shí),可以進(jìn)行測(cè)試和lint操作,保證倉庫里的代碼是優(yōu)雅的。 當(dāng)我們進(jìn)行commit操作時(shí),會(huì)觸發(fā)pre-commit,在此階段,可進(jìn)行test和lint。其后,會(huì)觸發(fā)commit-msg,對(duì)commit的message內(nèi)容進(jìn)行驗(yàn)證。
pre-commit
一般的lint會(huì)全局掃描,但是在此階段,我們僅需要對(duì)暫存區(qū)的代碼進(jìn)行l(wèi)int即可。所以使用lint-staged插件。
commit-msg
在此階段,可用 @commitlint/cli @commitlint/config-conventional 對(duì)提交信息進(jìn)行驗(yàn)證。但是記信息格式規(guī)范真的太太太太麻煩了,所以可用 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鉤子
看上去簡(jiǎn)簡(jiǎn)單單輕輕松松,那么,開干!
先把用到的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"; // 載入動(dòng)畫(用于安裝依賴的時(shí)候) import { exec } from "node:child_process";
package驗(yàn)證
既然是為項(xiàng)目添加,那當(dāng)然得有package.json文件啦!
const projectDirectory = cwd(); const pakFile = resolve(projectDirectory, "package.json"); if (!existsSync(pakFile)) { console.log(red("未在當(dāng)前目錄中找到package.json,請(qǐng)?jiān)陧?xiàng)目根目錄下運(yùn)行哦~")); return; }
既然需要lint,那當(dāng)然也要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,是否繼續(xù)?", }, { // 處理上一步的確認(rèn)值。如果用戶沒同意,拋出異常。同意了就繼續(xù) type: (_, { isContinue } = {}) => { if (isContinue === false) { throw new Error(red("? 取消操作")); } return null; }, name: "isContinueChecker", }, { type: "multiselect", name: "selectLint", message: "請(qǐng)選擇已安裝的依賴:", choices: [ { title: "eslint", value: "eslint", }, { title: "prettier", value: "prettier", }, { title: "stylelint", value: "stylelint", }, ], }, ]; const questions = pakHasLint.length === 0 ? [...noLintQuestions, ...huskyQuestions] : huskyQuestions; // huskyQuestions的husky安裝的詢問語句,下面會(huì)講
husky安裝詢問
因?yàn)椴煌陌芾砥饔胁煌陌惭b命令,以及有些項(xiàng)目會(huì)不需要commit msg驗(yàn)證。所有就會(huì)有以下詢問的出現(xiàn)啦
const huskyQuestions = [ { type: "select", name: "manager", message: "請(qǐng)選擇包管理器:", choices: [ { title: "npm", value: "npm", }, { title: "yarn1", value: "yarn1", }, { title: "yarn2+", value: "yarn2", }, { title: "pnpm", value: "pnpm", }, { title: "pnpm 根工作區(qū)", value: "pnpmw", }, ], }, { type: "confirm", name: "commitlint", message: "是否需要commit信息驗(yàn)證?", }, ];
使用prompts進(jìn)行交互提示
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 項(xiàng)目使用的包管理
- commitlint 是否需要commit msg驗(yàn)證
- selectLint 用戶自己選擇的已安裝的lint依賴
生成命令
通過manager和commitlint,可以生成要運(yùn)行的命令
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"'`; // 需要?jiǎng)?chuàng)建鉤子的命令 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"], };
所以呢,需要根據(jù)項(xiàng)目使用的lint來生成lint-staged.config.js:
// 簡(jiǎn)單粗暴的函數(shù) 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 內(nèi)容 const lintStagedContent = `module.exports =${JSON.stringify(getLintStagedOption(selectLint || pakHasLint))}`; // lint-staged.config.js 文件 const lintStagedFile = resolve(projectDirectory, "lint-staged.config.js");
commitlint 配置
因?yàn)閏ommitlint.config.js中的配置過于復(fù)雜。所以,我選擇在安裝完依賴后直接copy文件!被copy的文件內(nèi)容:
// @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: "選擇一個(gè)提交范圍(可選):", customScope: "請(qǐng)輸入自定義的提交范圍 :", subject: "填寫簡(jiǎn)短精煉的變更描述 :\n", body: '填寫更加詳細(xì)的變更描述(可選)。使用 "|" 換行 :\n', breaking: '列舉非兼容性重大的變更(可選)。使用 "|" 換行 :\n', footerPrefixsSelect: "選擇關(guān)聯(lián)issue前綴(可選):", customFooterPrefixs: "輸入自定義issue前綴 :", footer: "列舉關(guān)聯(lián)issue (可選) 例如: #31, #I3244 :\n", confirmCommit: "是否提交或修改commit ?", }, types: [ { value: "feat", name: "feat: ??新增功能 | A new feature", emoji: "??" }, { value: "fix", name: "fix: ??修復(fù)缺陷 | 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: ??代碼重構(gòu) | 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: ??測(cè)試相關(guān) | Adding missing tests or correcting existing tests", emoji: "??" }, { value: "build", name: "build: ??構(gòu)建相關(guān) | Changes that affect the build system or external dependencies", emoji: "??" }, { value: "ci", name: "ci: ??持續(xù)集成 | 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 作為開發(fā)管理 { value: "link", name: "link: 鏈接 ISSUES 進(jìn)行中" }, { value: "closed", name: "closed: 標(biāo)記 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: "", }, };
被復(fù)制的路徑,和目標(biāo)路徑
const commitlintFile = resolve(projectDirectory, "commitlint.config.js"); const commitlintFileTemplateDir = resolve(fileURLToPath(import.meta.url), "../src/template", "commitlint.config.js");
準(zhǔn)備就緒,開始安裝!
- 執(zhí)行剛剛生成的安裝命令
- 更改package.json內(nèi)容
- 寫入配置文件
- 添加鉤子
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內(nèi)容 開始 */ let newPakContent = JSON.parse(readFileSync(pakFile));// 獲取最新的包內(nèi)容 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內(nèi)容 結(jié)束 */ writeFileSync(lintStagedFile, lintStagedContent);// 寫入lint-staged配置 copyFileSync(commitlintFileTemplateDir, commitlintFile);// 復(fù)制commitlint配置至項(xiàng)目中 spinner.success({ text: green("安裝成功~準(zhǔn)備添加鉤子! ??"), mark: "?" });// 包安裝成功啦~ const hookSpinner = createSpinner("添加husky鉤子中...").start();// 開始裝鉤子 exec(`${createHookCommand}`, { cwd: projectDirectory }, (error) => { if (error) { hookSpinner.error({ text: red(`添加鉤子失敗,請(qǐng)手動(dòng)執(zhí)行${createHookCommand}`), mark: "?", }); console.error(error); return; } hookSpinner.success({ text: green("一切就緒! ??"), mark: "?" });// 鉤子安裝成功啦~一切ok~~ }); });
發(fā)包
最后,發(fā)下包,就可以在其他項(xiàng)目中使用啦
結(jié)尾
這個(gè)是本萌新因?yàn)閼杏窒氚裧it提交規(guī)范下又不想每次創(chuàng)項(xiàng)目都要翻文檔安裝的產(chǎn)物,沒有經(jīng)過測(cè)試,中間部分代碼會(huì)有更好的解決方案~
本代碼倉庫
以上就是項(xiàng)目中一鍵添加husky實(shí)現(xiàn)詳解的詳細(xì)內(nèi)容,更多關(guān)于項(xiàng)目一鍵添加husky的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Vue登錄功能實(shí)現(xiàn)全套詳解(含封裝axios)
登錄功能對(duì)于前端剛?cè)腴T不久的同學(xué)來說較為困難,下面這篇文章主要給大家介紹了關(guān)于Vue登錄功能實(shí)現(xiàn)(含封裝axios)的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-12-12vue-router路由跳轉(zhuǎn)問題 replace
這篇文章主要介紹了vue-router路由跳轉(zhuǎn)問題 replace,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09Vue3-KeepAlive,多個(gè)頁面使用keepalive方式
這篇文章主要介紹了Vue3-KeepAlive,多個(gè)頁面使用keepalive方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08vue中watch監(jiān)聽路由傳來的參數(shù)變化問題
這篇文章主要介紹了vue中watch監(jiān)聽路由傳來的參數(shù)變化,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-07-07vue+elementUI實(shí)現(xiàn)當(dāng)渲染文本超出一定字?jǐn)?shù)時(shí)顯示省略號(hào)
這篇文章主要介紹了vue+elementUI實(shí)現(xiàn)當(dāng)渲染文本超出一定字?jǐn)?shù)時(shí)顯示省略號(hào),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-10-10Element?Plus在el-form-item中設(shè)置justify-content無效的解決方案
這篇文章主要介紹了Element?Plus在el-form-item中設(shè)置justify-content無效的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-10-10