解决husky 4.x 在部分电脑上无法使用问题
- Published on
- 发布于·预估阅读8分钟
- Authors
- Name
- willson-wang
目录
背景
基于react开发了一套通用模版,在模版内集成了,通用的eslint、prettier等规则,结合huksy+lintstaged来做代码质量与规范的检查,husky选用的是4.3.8版本,package.json如下图所示;
{
"name": "test",
"version": "0.0.1",
"main": "./dist/index.js",
"typings": "dist/index.d.ts",
"description": "a little globber",
"scripts": {
"ci": "yarn tsc --noEmit && yarn lint:all && yarn spell-check:all && yarn test:coverage",
"lint:all": "yrc eslint --ext .js,.jsx,.ts,.tsx ./src",
"spell-check:all": "echo '开始拼写检查' && yrc cspell \"**/*.{txt,ts,tsx,js,json,md}\"",
"prettier": "yrc prettier --write \"**/**.{js,jsx,tsx,ts,less,md,json}\"",
},
"engines": {
"node": ">=10.13.0"
},
"files": [
"dist",
"bin"
],
"husky": {
"hooks": {
"pre-commit": "yarn setPushFollowTags && yarn tsc --noEmit && lint-staged --verbose",
"commit-msg": "yrc commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"*.{js,jsx,ts,tsx,md,json}": [
"yrc prettier --write",
"yrc cspell --no-must-find-files"
],
"*.{js,jsx,ts,tsx}": [
"yquality lint --source",
"yrc eslint --fix "
]
},
"devDependencies": {
"husky": "4.3.8",
"lint-staged": "10.5.4",
},
}
主要使用到husky的两个钩子pre-commit
与commit-msg
但是在部分同事的电脑上,install之后,huksy没有初始化成功,也没有错误提示,导致无法正常使用husky的功能,为了彻底解决这个问题,决定去看一下husky的内部实现
husky4.3.8源码分析
husky是在install的时候,往.git/hooks目录下注入对应的commit钩子,那么先看package.json
"scripts": {
"install": "node husky install",
"preuninstall": "node husky uninstall",
"postinstall": "opencollective-postinstall || exit 0"
},
关键上面这三个script hook
先看postinstall
这个钩子就是做了一些辅助工具,无需关注
重点是install
这个钩子,install这个钩子做的事情如下
检查git版本 => 从环境变量INIT_CWD中获取到当前工作目录 => 创建各种git hook => 创建husky.local.sh 与 husky.sh 用于具体执行huksy命令的脚本
初始化准备工作
// lib/installer/bin.js
// Get INIT_CWD env variable
function getInitCwdEnv() {
const { INIT_CWD } = process.env;
if (INIT_CWD === undefined) {
const { name, version } = which_pm_runs_1.default();
throw new Error(`INIT_CWD is not set, please check that your package manager supports it (${name} ${version})
Alternatively, you could set it manually:
INIT_CWD="$(pwd)" npm install husky --save-dev
Or upgrade to husky v5`);
}
debug_1.debug(`INIT_CWD is set to ${INIT_CWD}`);
return INIT_CWD;
}
function run() {
const action = process.argv[2];
try {
if (action === 'install') {
checkSkipInstallEnv();
checkGitVersion_1.checkGitVersion();
}
const INIT_CWD = getInitCwdEnv();
const userPkgDir = getUserPkgDir(INIT_CWD);
checkGitDirEnv_1.checkGitDirEnv();
const { absoluteGitCommonDir, relativeUserPkgDir } = getDirs(userPkgDir);
if (action === 'install') {
const { name: pmName } = which_pm_runs_1.default();
_1.install({
absoluteGitCommonDir,
relativeUserPkgDir,
userPkgDir,
pmName,
isCI: ci_info_1.isCI,
});
}
else {
_1.uninstall({ absoluteGitCommonDir, userPkgDir });
}
console.log(`husky > Done`);
}(err){}
}
// lib/installer/checkGitVersion.js
function checkGitVersion() {
const { status, stderr, stdout } = cp.spawnSync('git', ['--version']);
if (status !== 0) {
throw new Error(`git --version command failed. Got ${String(stderr)}.`);
}
const [version] = find_versions_1.default(String(stdout));
if (compare_versions_1.default(version, '2.13.0') === -1) {
throw new Error(`Husky requires Git >=2.13.0. Got v${version}.`);
}
}
在执行install or uninstall之前会做一些当前工作目录及git版本的检查,这里两个地方会中断执行,导致install失败
- checkGitVersion内的git版本检查,husky 4.3.8版本要求git版本必须大于等于2.13.0;
- 无法从环境变量process.env中获取INIT_CWD参数
创建hook
当满足上述条件之后,就会执行对应的install or unstall方法
// lib/installer/index.js
function install({ absoluteGitCommonDir, relativeUserPkgDir, userPkgDir, pmName, // package manager name
isCI, }) {
// Get conf from package.json or .huskyrc
const conf = getConf_1.getConf(userPkgDir);
// Create hooks directory if it doesn't exist
const gitHooksDir = getGitHooksDir(absoluteGitCommonDir);
// 判断.git/hooks目录是否存在,如果不存在则创建
if (!fs_1.default.existsSync(gitHooksDir)) {
fs_1.default.mkdirSync(gitHooksDir);
}
// 创建commit-msg等一系列hook
hooks_1.createHooks(gitHooksDir);
// 往.git/hooks目录内生成husky.local.sh脚本,用于执行husky具体命令
localScript_1.createLocalScript(gitHooksDir, pmName, relativeUserPkgDir);
// 往.git/hooks目录内生成husky.sh脚本,用于执行husky具体命令
mainScript_1.createMainScript(gitHooksDir);
}
function uninstall({ absoluteGitCommonDir, userPkgDir, }) {
if (isInNodeModules(userPkgDir)) {
console.log('Trying to uninstall from node_modules directory, skipping Git hooks uninstallation.');
return;
}
// Remove hooks
const gitHooksDir = getGitHooksDir(absoluteGitCommonDir);
hooks_1.removeHooks(gitHooksDir);
localScript_1.removeLocalScript(gitHooksDir);
mainScript_1.removeMainScript(gitHooksDir);
}
install成功之后,我们可以在.git/hooks内看到生成的hook,如下图所示
具体的hook内容如下所示
#!/bin/sh
# husky
# Created by Husky v4.3.8 (https://github.com/typicode/husky#readme)
# At: 2022-4-2 2:37:25 ├F10: PM┤
# From: xxxx/node_modules/husky (https://github.com/typicode/husky#readme)
. "$(dirname "$0")/husky.sh"
到这里我们基本已经知道为什么部分电脑husky不生效,原因主要就是git版本低于指定的2.13.0版本,还有就是无法从环境变量中获取到INIT_CWD
排查错误并手动执行install
const { execSync } = require('child_process');
const command = process.argv[2] || 'install';
const cwd = process.cwd();
const userAgent = execSync('yarnpkg config get user-agent', {
encoding: 'utf-8',
}).replace('\n', '');
let pkg = {};
try {
pkg = require(`${cwd}/package.json`);
} catch (error) {
console.log('获取package.json错误', error);
}
const huksy = pkg.devDependencies.husky || pkg.dependencies.husky;
if (huksy !== '4.3.8') {
console.log('仅支持husky 4.3.8版本, 推荐yarn remove husky && yarn add [email protected] -D');
process.exit(1);
}
const lastCommand = `HUSKY_DEBUG=true INIT_CWD=${cwd} npm_config_user_agent="${userAgent}" node ./node_modules/husky/lib/installer/bin.js ${command}`;
console.log('实际执行命令:', lastCommand);
execSync(lastCommand, {
stdio: 'inherit',
});
这里把husky内的debug日志开启,便于排查问题 所以当我们提示的是git版本低于要求版本时,则可以通过升级git版本来解决该问题
husky执行命令实现
function runCommand(cwd, hookName, cmd, env) {
console.log(`husky > ${hookName} (node ${process.version})`);
const { status } = child_process_1.spawnSync('sh', ['-c', cmd], {
cwd,
env: Object.assign(Object.assign({}, process.env), env),
stdio: 'inherit',
});
if (status !== 0) {
const noVerifyMessage = [
'commit-msg',
'pre-commit',
'pre-rebase',
'pre-push',
].includes(hookName)
? '(add --no-verify to bypass)'
: '(cannot be bypassed with --no-verify due to Git specs)';
console.log(`husky > ${hookName} hook failed ${noVerifyMessage}`);
}
// If shell exits with 127 it means that some command was not found.
// However, if husky has been deleted from node_modules, it'll be a 127 too.
// To be able to distinguish between both cases, 127 is changed to 1.
if (status === 127) {
return 1;
}
return status || 0;
}
通过shell方式执行,然后通过status来判断成功还是失败,所以如果我们自己自定义了一些工具,那么如果需要借助huksy来执行,出现错误的场景,一定需要通过process.exit(code)把code向上传递出来
总结
husky的runCommand执行方式值得我们在写类似工具时借鉴一二,另外就是为什么我们在install的时候,husky如果初始化失败,为什么没有中断整个install流程?后续有时间在去了解下这里