Litestore · Developer guide
Codebase

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).

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, 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 format

This runs bun biome format --write .. It only formats.

bun run lint

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

bun run verify

This 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:

  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

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:

  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

This 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:

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"

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.

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

The 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.

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

The 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.

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

Four 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.

On this page