05.06 — Husky + lint-staged + commitlint + commitizen#
Strict commit discipline. Conventional, signed, format-checked.
Tools#
| Tool | Role |
|---|---|
| Husky | Installs local Git hooks (pre-commit, commit-msg) |
| lint-staged | Runs tools only on staged files |
| commitlint | Verifies the commit message follows the convention |
| commitizen + cz-commitlint | Interactive CLI to compose a compliant message |
Config#
.husky/#
Folder containing the hook scripts:
.husky/
├── pre-commit
└── commit-msgpre-commit:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm exec lint-stagedcommit-msg:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm exec commitlint --edit "$1".lintstagedrc.cjs#
module.exports = {
"*.{js,ts,jsx,tsx,json,md,mdx,html,css,scss,yml,yaml}": ["prettier --write"],
};→ On each staged file: prettier --write. If Prettier modifies anything, the changes get auto-restaged into the commit.
.commitlintrc.cjs#
module.exports = { extends: ["@commitlint/config-conventional"] };→ Inherits from @commitlint/config-conventional:
<type>(<scope>): <subject>
<body>
<footer>Accepted types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert.
.czrc#
{ "path": "@commitlint/cz-commitlint" }→ Tells commitizen to use @commitlint/cz-commitlint as the adapter. pnpm commit fires the interactive CLI.
Commit chain#
1. Developer: git add foo.tsx
↓
2. Developer: pnpm commit
↓
3. commitizen (cz-commitlint) opens an interactive CLI:
? Select the type of change: feat
? What is the scope (optional): dashboard
? Write a short, imperative tense description: add OTP screen
? Provide a longer description: ...
? Are there any breaking changes? no
? Issues this commit closes: -
↓
4. The message is validated locally against the conventional config
↓
5. git-cz calls `git commit` with the built message
↓
6. .husky/pre-commit runs:
lint-staged → prettier --write on staged files
↓
7. .husky/commit-msg runs:
commitlint --edit COMMIT_EDITMSG
→ verifies one last time (in case the user did git commit -m directly)
↓
8. If everything passes: commit createdAlternate pipeline: git commit -m "..."#
Developer shortcuts via git commit -m "fix some bug" (without pnpm commit):
pre-commitrunslint-staged(prettier on staged files).commit-msgrunscommitlint --edit "$1".- “fix: some bug” passes. “stuff” gets rejected.
So you can shortcut, but commitlint enforces the convention anyway.
is-ci || husky in prepare#
"scripts": {
"prepare": "is-ci || husky"
}→ The prepare script runs automatically on pnpm install (or npm install).
- CI (
is-cireturnstrue) →||short-circuits, husky isn’t installed. - Local (
is-cireturnsfalse) →huskyis installed (the.husky/*hooks get registered in.git/hooks/*).
Discipline#
| Benefit | Detail |
|---|---|
| Readable git history | git log --oneline gives feat(dashboard): add OTP screen, usable by gh release (changelog) |
| No PR with broken format | Prettier auto-applies on staged, we never commit unformatted code |
| No more formatting debates in review | Format is decided by Prettier, not by review |
Format vs content#
The pre-commit lint-staged applies Prettier but does not apply ESLint.
- Prettier fixes the format without changing semantics. Auto-safe.
- ESLint can catch semantic errors (unused var, missing dep in
useEffect). Running it pre-commit could block a commit over something worth discussing in review.
→ Linting goes to CI (wireit lint). Formatting stays immediate.
Why does Igoristan have all this for a “demo” project#
To earn Napoleon’s approval.