Linting
This page shows you which command formats and checks your code, and which checks run automatically when you commit and push.
Litestore formats and lints its code with Biome and checks its structure with its own scripts. This page shows you which command to run while you work and which checks Git runs for you.
Biome is the only formatter and the only general-purpose linter. The repository has no ESLint or Prettier configuration.
All the structural checks are in the scripts/check-*.ts harness instead. Biome decides what the code looks like. The harness decides what the code is allowed to do.
package.json combines the two into three commands, format, lint and verify. Lefthook runs two of them for you when you use Git.
Biome
biome.json uses the 1.9.4 schema, which matches the installed @biomejs/biome (^1.9.4).
"formatter": {
"enabled": true,
"lineWidth": 100,
"indentStyle": "space",
"indentWidth": 2,
"formatWithErrors": true
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"semicolons": "asNeeded",
"arrowParentheses": "asNeeded",
"bracketSpacing": true,
"trailingCommas": "all"
}
}For JavaScript and TypeScript, Biome formats code like this:
- Lines are up to 100 columns wide.
- Indents are two spaces.
- Strings use double quotes.
- Semicolons appear only where they are required.
- A single arrow function parameter has no parentheses.
- Trailing commas appear everywhere they are allowed.
Because formatWithErrors is on, Biome still formats a file that doesn't parse, as far as it can.
JSON has its own settings. The parser accepts trailing commas but the formatter never writes them, and comments are rejected.
organizeImports is on, so Biome sorts imports and nobody has to keep them in order by hand.
vcs.useIgnoreFile is on with clientKind: "git", so Biome respects .gitignore. files.ignore adds three more paths:
**/.next
**/.source
**/woocommerce-product-feeds/**Rules that are turned off on purpose
The linter runs Biome's recommended rules with eleven of them turned off:
noExplicitAny(suspicious)noArrayIndexKey(suspicious)noConfusingLabels(suspicious)noShadowRestrictedNames(suspicious)useExhaustiveDependencies(correctness)noDangerouslySetInnerHtml(security)noNonNullAssertion(style)noBlankTarget(a11y)useAnchorContent(a11y)useValidAnchor(a11y)useAltText(a11y)
Two of them are off because something else already handles them.
noDangerouslySetInnerHtml is off because the HTML Litestore renders has already been cleaned with sanitize-html.
noExplicitAny is off because scripts/check-no-as-any.ts handles any instead, as a ratchet.
That doesn't mean you can add any freely. check-no-as-any.ts scans the whole source tree for as any and as unknown as, records the ones that already exist, and fails on any new one.
The three commands
bun run formatThis runs bun biome format --write .. It only formats.
bun run lintThis runs biome check --write ., then six named checks, then the lint:cleanliness aggregate.
bun run verifyThis runs biome check ., then eleven named checks, then lint:cleanliness, then vitest run, then typecheck.
Use lint while you work. After Biome writes its fixes, it runs:
lint:cachelint:disciplinelint:deadlint:flexlint:critical-testslint:no-as-any- the
lint:cleanlinessaggregate
lint doesn't run everything. It skips theme boundaries, event completeness and the three visibility checks.
verify is the full gate. Its most important difference is the first step. It runs biome check . without --write, so an unformatted file fails instead of being fixed silently.
Both commands run lint:cleanliness, which is made of thirteen scripts:
lint:orphanslint:dead-actionslint:dead-schemalint:field-balancelint:dupeslint:depslint:hackslint:eventslint:cache-tagslint:guaranteeslint:client-boundarylint:client-bundlelint:hygiene
bun run verify:parallelThis runs the same steps at the same time through scripts/verify.ts.
Type checking is separate from Biome, and it needs extra memory to finish:
"typecheck": "NODE_OPTIONS='--max-old-space-size=12288' tsc --noEmit",
"typecheck:strict": "NODE_OPTIONS='--max-old-space-size=12288' tsc --noEmit --strict --noImplicitAny --noImplicitReturns --noImplicitThis --noUnusedLocals --noUnusedParameters"verify doesn't run typecheck:strict.
Git hooks
lefthook.yml defines three hooks. They install once through the prepare script (lefthook install || true), which Bun runs during bun install.
pre-commit:
parallel: false
commands:
discipline:
run: bun run lint:discipline
critical-tests:
run: bun run lint:critical-tests
unit:
run: bun run vitest runThe pre-commit hook runs its steps one after another. It runs the discipline rules, the critical-tests check and the unit suite. It leaves out tsc on purpose, because type checking is the slow step, and that keeps commits fast.
pre-push:
commands:
verify:
run: bun run verifyThe pre-push hook runs the full verify. That adds type checking and the rest of the checks before your code leaves your machine.
The commit-message hook
The commit-msg hook rejects subject lines that look like keyboard mashing. Messages like that make git bisect and git blame useless when you're digging through history later.
The first line must be at least 10 characters long and contain at least 2 words.
msg=$(head -1 {1})
case "$msg" in
Merge*|Revert*|fixup!*|squash!*) exit 0 ;;
esac
words=$(echo "$msg" | wc -w)
if [ ${#msg} -lt 10 ] || [ "$words" -lt 2 ]; then
echo "commit-msg: \"$msg\" is too short — write what changed and why (≥2 words, ≥10 chars)."
exit 1
fiFour kinds of generated message are exempt. The hook matches them by their start:
Merge
Revert
fixup!
squash!So any message that Git or an interactive rebase writes for you skips the length and word checks.
The hook only reads the first line. You don't need a body, and the hook doesn't count it.
CI is the real check
Lefthook is there for your convenience, but it doesn't guarantee anything. --no-verify
skips the hooks, and they aren't installed until someone runs bun install. What has to
pass is bun run verify in CI.
Which command to run
Run format when you only want to reformat files. Run lint while you're working, because it fixes formatting in place and runs the cheap structural checks too.
Run verify before you push. It runs the same checks as CI, and it fails on an unformatted file instead of fixing it.
Run verify:parallel when several things are already broken and you want to see every failure at once.
The hooks follow one rule, and it's worth remembering even when you reach for --no-verify:
Run the fast checks when you commit, and the full check before your code leaves your machine.
Related
Style
Learn the naming, import and file-layout conventions the codebase follows, and the two scripts — check-no-as-any.ts and check-discipline.ts — that fail on a new violation.
Harness
Learn about the 25 CI checks that run on your fork, and the 8-guide rule set behind them.
Commands
Learn what every script in package.json runs — dev, database, the lint checks and the verify gate.