Litestore · Developer guide

Linting

Learn how Biome formats and lints the codebase, what bun run lint and bun run verify actually run, and which lefthook hooks fire on commit and push.

Biome is the only formatter and the only general-purpose linter. There is no ESLint or Prettier configuration in the repository.

Everything structural sits 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 composes both into three commands — format, lint and verify — and Lefthook wires two of them to git.

Biome

biome.json pins the 1.9.4 schema and matches the installed @biomejs/biome (^1.9.4).

biome.json
"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 that means:

  • 100-column lines
  • Two-space indent
  • Double quotes
  • No semicolons unless required
  • No parentheses around a single arrow parameter
  • Trailing commas everywhere

formatWithErrors means a file that does not parse is still formatted as far as it can be.

JSON is formatted separately, with trailing commas allowed by the parser but never written by the formatter, and comments rejected.

organizeImports is enabled, so import order is Biome's job rather than a convention anyone maintains by hand.

vcs.useIgnoreFile is on with clientKind: "git", so .gitignore is honoured. files.ignore adds three more paths:

**/.next
**/.source
**/woocommerce-product-feeds/**

Rules that are deliberately off

The linter runs the recommended set with eleven rules disabled:

  • 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 owns the concern.

noDangerouslySetInnerHtml is off because the HTML being rendered has already been sanitised through sanitize-html.

noExplicitAny is off because scripts/check-no-as-any.ts owns this instead, and it ratchets.

Turning noExplicitAny off does not make any free. check-no-as-any.ts scans the whole source tree for as any and as unknown as, baselines what exists, and fails on anything new.

The three commands

bun run format

Runs bun biome format --write .. It only formats.

bun run lint

Runs biome check --write ., then six named checks, then the lint:cleanliness aggregate.

bun run verify

Runs biome check ., then eleven named checks, then lint:cleanliness, then vitest run, then typecheck.

lint is the working loop. After Biome writes its fixes, it runs:

  1. lint:cache
  2. lint:discipline
  3. lint:dead
  4. lint:flex
  5. lint:critical-tests
  6. lint:no-as-any
  7. the lint:cleanliness aggregate

It is not the full gate. It skips theme boundaries, event completeness and the three visibility checks.

verify is the gate. The difference that matters most is the first leg: it runs biome check . without --write, so an unformatted file fails rather than being quietly fixed.

lint:cleanliness, which both commands run, is itself thirteen scripts:

  1. lint:orphans
  2. lint:dead-actions
  3. lint:dead-schema
  4. lint:field-balance
  5. lint:dupes
  6. lint:deps
  7. lint:hacks
  8. lint:events
  9. lint:cache-tags
  10. lint:guarantees
  11. lint:client-boundary
  12. lint:client-bundle
  13. lint:hygiene
bun run verify:parallel

Runs the same legs concurrently through scripts/verify.ts.

Typecheck is separate from Biome, and needs headroom to complete:

package.json
"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"

typecheck:strict is not part of verify.

Git hooks

lefthook.yml defines three hooks. They install once, through the prepare script (lefthook install || true), which Bun runs on bun install.

lefthook.yml
pre-commit:
  parallel: false
  commands:
    discipline:
      run: bun run lint:discipline
    critical-tests:
      run: bun run lint:critical-tests
    unit:
      run: bun run vitest run

Pre-commit is sequential and deliberately excludes tsc, which is the slow leg. It runs the discipline rules, the critical-tests check and the unit suite, so committing stays fast.

lefthook.yml
pre-push:
  commands:
    verify:
      run: bun run verify

Pre-push runs the full verify, adding typecheck and the rest of the checks before anything leaves the machine.

The commit-message hook

commit-msg rejects keyboard-mash subject lines, because they destroy git bisect and git blame archaeology.

The first line must be at least 10 characters and at least 2 words.

lefthook.yml
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
fi

The four exemptions are prefix matches on generated messages:

Merge
Revert
fixup!
squash!

Anything Git or an interactive rebase writes for you passes without the length and word checks.

The hook inspects only the first line, so a body is neither required nor counted.

Lefthook is a local convenience, not the enforcement boundary. Hooks are skipped by --no-verify and are absent until someone runs bun install, so bun run verify is what has to pass in CI.

Which command to run

Run format when you only want the file reshaped, and lint while you are working — it fixes formatting in place and runs the cheap structural checks with it.

Run verify before you push. It is the same set of checks CI runs, and it is the one that fails on an unformatted file instead of correcting it.

Run verify:parallel when several things are already broken and you want every failure in one pass.

The rule the hooks encode is worth keeping in mind even with --no-verify in your fingers:

Fast checks belong on commit, the full gate belongs before the code leaves your machine.

On this page