Linting & Formatting
The static analysis toolchain — what each tool does, where it's configured, and how to work with it.
Toast uses six static analysis tools, each owning one concern. They all run as part of pnpm check and are enforced by git hooks and CI. This page explains what each tool does, when you'll encounter it, and how to work with it.
Why so many tools?
The goal is early, fast feedback that's easy to maintain.
Most of Toast's code is written by AI agents. Agents read terminal output, act on errors, and ignore everything else. That shapes how we configure the toolchain:
- Every rule is an error. Warnings fill up agent context windows with noise they won't act on. If a pattern is worth linting for, it's worth blocking on. If it's not worth blocking on, remove the rule.
- Feedback comes from the terminal, not from PR review. The tools run in seconds (Oxlint and Oxfmt are Rust-native), so agents get lint feedback on every commit — not three hours later in a review comment they may not see.
- Rules are cheap to add. When a pattern keeps showing up in PRs, add a rule. The custom Toast Oxlint plugin and ast-grep rules are straightforward to write — AI can generate them from a description of the problem. The toolchain should keep growing.
- The codebase keeps improving. Each rule is a ratchet. Once a bad pattern is eliminated, the rule prevents it from returning. Over time the linter does the work that reviewers used to do.
Which tool does what?
| I need to... | Tool | Why this one |
|---|---|---|
| Enforce syntax, style, and per-file correctness | Oxlint | Fast Rust linter with TypeScript support and a custom Toast plugin for project-specific rules |
| Format code consistently | Oxfmt | Rust-based formatter for JS/TS/JSON/MD/YAML/CSS/HTML — runs in milliseconds |
| Format SQL files | Prettier | Narrowly scoped to *.sql via prettier-plugin-sql |
| Detect unused exports, dead files, unused deps | knip | Cross-module visibility that per-file linters can't provide |
| Enforce architecture and import boundaries | dependency-cruiser | Operates on the full import graph — apps can't import from other apps, routes can't skip the service layer |
| Enforce structural code patterns | ast-grep | AST-level pattern matching for dependency injection and architectural rules |
Config files
Each tool has exactly one config file at the repo root. These are the source of truth for what's enforced — not this page.
| Tool | Config | Notes |
|---|---|---|
| Oxlint | .oxlintrc.json | Built-in rules + custom Toast plugin at tools/oxlint-plugin-toast/plugin.mjs |
| Oxfmt | .oxfmtrc.json | Formatting options (single quotes, trailing commas, 100 char width) |
| Prettier | .prettierrc | SQL-only — prettier-plugin-sql with PostgreSQL dialect |
| knip | knip.json | Workspace-level entry points, project files, and ignore patterns |
| dependency-cruiser | .dependency-cruiser.cjs | forbidden and allowed rule arrays |
| ast-grep | sgconfig.yml | Points to rule files in tools/ast-grep-rules/*.yml |
Running the tools
pnpm lint # Oxlint check
pnpm lint:fix # Oxlint auto-fix
pnpm format # Oxfmt + Prettier (SQL) check
pnpm format:fix # Oxfmt + Prettier (SQL) auto-fix
pnpm check:analysis # Static analysis only: dependency-cruiser, knip, ast-grep
pnpm check # Everything: lint, format, analysis, typecheck, test, build (with auto-fix)
pnpm check:strict # Same checks, no auto-fix — this is what the pre-push hook runsThe full pipeline runs in this order:
- Lint — Oxlint with the custom Toast plugin
- Format — Oxfmt for code, Prettier for SQL
- Static analysis (parallelised) — dependency-cruiser, knip, ast-grep
- Typecheck — TypeScript in strict mode
- Test — Vitest with 100% coverage enforcement
- Build — Turbo-orchestrated build
Oxlint in detail
Oxlint is the primary linter. It replaces ESLint entirely — there is no ESLint config in this repo.
The config in .oxlintrc.json starts with all categories disabled and opts in to specific rules. This means every rule is a deliberate choice, not something inherited from a preset.
Plugins enabled: oxc, typescript, unicorn, import, react, vitest, jsx-a11y
Every rule is an error. If a pattern is worth linting for, it blocks CI. There are no warning-level rules — warnings generate noise that agents ignore while filling up their context windows. If a rule isn't worth blocking on, remove it instead of downgrading to a warning.
Overrides tailor rules to different parts of the codebase. Test files relax noConsole and useAwait. Scripts allow noConsole for CLI output. The packages/ui directory is excluded entirely (third-party adapted code).
The custom Toast plugin
The file tools/oxlint-plugin-toast/plugin.mjs adds project-specific rules that enforce Toast conventions. These are things no upstream linter knows about — like requiring requirePermission() middleware on API routes, preventing process.env access outside config modules, or ensuring Zustand stores use selectors.
To see the full list of custom rules, read the plugin file directly. Each rule has a comment explaining what it catches and why.
Dependency-cruiser in detail
Dependency-cruiser enforces architectural boundaries by analysing the import graph. If you violate a boundary, you get a clear error like no-routes-to-repositories: routes/foo.ts → repositories/bar.ts. The key boundaries:
- Packages can't import from apps — shared code flows downward only
- Apps can't import from each other — admin ↛ api, api ↛ admin, docs ↛ either
- Routes can't import repositories or
@toast/dbdirectly — go through the service layer - No circular runtime dependencies — type-only cycles are allowed
knip in detail
knip detects dead code across the entire monorepo: unused exports, unused dependencies in package.json, files that nothing imports. Each workspace has tailored entry points so knip knows what's "in use" — route files for the API, page components for the admin app, and so on.
ast-grep in detail
ast-grep runs six custom rules that enforce dependency injection conventions from ADR-014. These catch singleton patterns like getDb(), getAuth(), or direct eventBus imports that should be injected via factory function parameters instead.
All ast-grep rules are errors — they fail the build like everything else. When adding a new ast-grep rule to track a migration in progress, fix all existing violations first so the rule can start at zero.
Adding a new rule
- Pick the right tool using the table at the top of this page.
- Add the rule to the config file. The config is the source of truth — don't create a separate document describing the rule.
- Run
pnpm checkto confirm the rule fires where expected and doesn't false-positive elsewhere. - If you're adding an Oxlint guard rule, add it to
tools/oxlint-plugin-toast/plugin.mjsand enable it in.oxlintrc.jsonwith an explicit severity. - If you're adding an ast-grep rule, create a new YAML file in
tools/ast-grep-rules/and test withsg scan --rule tools/ast-grep-rules/your-rule.yml.
Editor setup
Install the Oxc VS Code extension for real-time lint and format feedback. Errors and warnings show as squiggly underlines — fix them as you go.
Git hooks
Three hooks enforce quality at different points:
| Hook | What it runs | When |
|---|---|---|
pre-commit | lint-staged (Oxlint + Oxfmt on staged files) | Every commit |
commit-msg | commitlint (conventional commit format) | Every commit |
pre-push | pnpm check:strict (the full quality gate) | Every push |
Never pass --no-verify to skip these hooks. If a hook fails, fix the underlying issue.