当前位置 : 主页 > 网络编程 > JavaScript >

项目中一键添加husky实现详解

来源:互联网 收集:自由互联 发布时间:2023-02-08
目录 关于husky pre-commit commit-msg 一键添加husky 先把用到的import拿出来溜溜 package验证 husky安装询问 使用prompts进行交互提示 生成命令 lint-staged 配置 commitlint 配置 准备就绪,开始安装!
目录
  • 关于husky
    • pre-commit
    • commit-msg
  • 一键添加husky
    • 先把用到的import拿出来溜溜
    • package验证
    • husky安装询问
    • 使用prompts进行交互提示
    • 生成命令
    • lint-staged 配置
    • commitlint 配置
    • 准备就绪,开始安装!
    • 发包
  • 结尾

    关于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:      
    网友评论