lint-staged与ls-lint配合使用时的陷阱
问题背景
在最近的一个 React + TypeScript 项目中,我使用了 lint-staged 配合 ls-lint 来实现 Git 提交时的文件名规范自动检查。配置看起来很简单:
{
"lint-staged": {
"*.{ts,tsx,js,jsx}": ["eslint --fix", "ls-lint"]
}
}
.ls-lint.yml 配置文件规定了命名规则:
ignore:
- node_modules
- .git
- dist
ls:
.tsx: PascalCase # .tsx 文件应该使用帕斯卡命名(首字母大写)
.ts: kebab-case # .ts 文件使用短横线命名
.js: kebab-case
.css: kebab-case
按照这个规则,src/main.tsx 这样的文件名(小写开头)应该在提交时被拦截并报错。但奇怪的是,git commit 时检测通过了,而手动执行却报错了。
问题复现
场景一:手动执行 ls-lint(相对路径)
$ npx ls-lint src/main.tsx
src/main.tsx failed for `.tsx` rules: pascalcase
✅ 符合预期:检测到命名不规范,退出码为 1。
场景二:Git 提交时通过 lint-staged 执行
$ git add src/main.tsx
$ git commit -m "test"
✔ Preparing...
✔ Running tasks...
✔ eslint --fix
✔ ls-lint
✔ Applying modifications...
❌ 不符合预期:ls-lint git提交成功,没有检测到命名问题!
深入调试
为了找出原因,我创建了一个调试脚本来查看 lint-staged 实际传递给 ls-lint 的参数:
#!/bin/bash
# test-ls-lint.sh
echo "Arguments received: $@" >> /tmp/lint-staged-debug.log
echo "Number of arguments: $#" >> /tmp/lint-staged-debug.log
npx ls-lint "$@" 2>&1
exit $?
修改 package.json 临时使用这个脚本:
{
"lint-staged": {
"*.{ts,tsx,js,jsx}": ["eslint --fix", "./test-ls-lint.sh"]
}
}
再次提交后查看日志:
$ cat /tmp/lint-staged-debug.log
Arguments received: /Users/jesse/Web/study/React/my-app/src/main.tsx
Number of arguments: 1
关键发现:lint-staged 传递的是绝对路径,而不是相对路径!
验证假设
我分别测试了相对路径和绝对路径的情况:
# 测试 1:相对路径
$ npx ls-lint src/main.tsx
src/main.tsx failed for `.tsx` rules: pascalcase
$ echo $?
1 # 报错,符合预期
# 测试 2:绝对路径
$ npx ls-lint /Users/jesse/Web/study/React/my-app/src/main.tsx
$ echo $?
0 # 通过,不符合预期!
真相大白:ls-lint 在处理绝对路径时,无法正确应用 .ls-lint.yml 中定义的命名规则,导致检测被静默绕过。
根本原因分析
经过分析和查阅 ls-lint 的文档,我发现:
-
ls-lint的设计初衷:它是一个基于项目结构的文件命名 lint 工具,需要根据文件相对于项目根目录的路径来应用规则。 -
绝对路径的问题:当传入绝对路径时,
ls-lint无法正确解析文件在项目中的相对位置,导致规则匹配失败或被忽略。 -
lint-staged的行为:默认情况下,lint-staged会将暂存文件的绝对路径作为参数传递给命令,这是为了保证命令在任何工作目录下都能正确找到文件。
这就造成了一个矛盾:
-
lint-staged传递绝对路径(为了保证可靠性) -
ls-lint需要相对路径(为了正确应用规则)
官方回应
这个问题已经被社区发现并报告给 ls-lint 开发团队。在 GitHub Issue #321 中,有开发者提出了相同的疑问。
好消息:ls-lint 开发团队已经确认了这个问题,并计划在 v2.4.0 版本中添加对绝对路径的支持。
但在 v2.4.0 发布之前,我们仍然需要使用本文提到的解决方案来确保文件名校验正常工作。
最终解决方案
经过多次尝试,我找到了最优雅的解决方案:
在 Husky 的 pre-commit 钩子中直接调用 ls-lint,并使用 git diff --staged --name-only 获取暂存文件的相对路径列表。
实现步骤
1. 从 lint-staged 配置中移除 ls-lint
修改 package.json,将 ls-lint 从 lint-staged 配置中移除:
{
"lint-staged": {
"*.{ts,tsx,js,jsx}": ["eslint --fix"],
"*.{css,scss}": "stylelint --fix"
}
}
2. 在 pre-commit 钩子中添加 ls-lint 检查
编辑 .husky/pre-commit 文件,在执行 lint-staged 之前添加 ls-lint 检查:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# 检查暂存文件的命名规范(使用相对路径)
pnpm exec ls-lint $(git diff --staged --name-only)
# 执行其他 lint-staged 检查
pnpm exec lint-staged
关键点:
-
git diff --staged --name-only返回的是相对路径(如src/main.tsx),这正是ls-lint所需要的 - 在
lint-staged之前执行,可以尽早发现命名问题 - 使用
pnpm exec确保使用项目本地安装的ls-lint版本
验证效果
现在再次提交不符合命名规范的文件:
$ git add src/main.tsx
$ git commit -m "test"
src/main.tsx failed for `.tsx` rules: pascalcase
husky - pre-commit hook exited with code 1 (error)
✅ 成功拦截:现在可以正确检测到命名问题了!
提交符合规范的文件:
$ git add src/MainComponent.tsx
$ git commit -m "feat: add main component"
✔ Preparing...
✔ Running tasks...
✔ Applying modifications...
✅ 正常通过:符合规范的文件可以正常提交。
注意事项
1. 首次提交时的边界情况
如果是第一次提交(没有任何历史提交),git diff --staged --name-only 仍然可以正常工作,它会列出所有暂存的文件。
2. 空暂存区的处理
如果暂存区为空,git diff --staged --name-only 会返回空字符串,ls-lint 会自动跳过检查,不会报错。
3. 删除文件的处理
git diff --staged --name-only 也会包含被删除的文件。如果这些文件已经不存在,ls-lint 会忽略它们,不会影响检查结果。
4. 团队协作建议
- 确保所有团队成员都安装了 Husky:
pnpm exec husky install - 在项目的
README.md中说明命名规范和检查机制 - 考虑在 CI/CD 流程中也加入
ls-lint检查,作为双重保障
总结
这次问题的根本原因是:lint-staged 传递绝对路径,而 ls-lint 需要相对路径才能正确应用规则。
最终的解决方案是:在 Husky 的 pre-commit 钩子中使用 git diff --staged --name-only 获取相对路径列表,然后直接传递给 ls-lint,同时从 lint-staged 配置中移除 ls-lint。
这个方案简洁、高效、可靠,完美解决了绝对路径导致的检测失效问题。希望这篇文章能帮助你避免类似的坑!