Skip to main content
Bazel doesn’t come with a built-in formatting and linting tool for your code. Aspect solves this by providing aspect_rules_lint, a Bazel rule set that teaches Bazel how to run existing linting and formatting tools like Prettier and ESLint inside Bazel projects. Aspect CLI also makes running checks for linting and formatting easier and more accessible for you.

What you’ll learn

In this guide, you’ll learn:
  • Formatting code locally with the format tool and the Aspect CLI’s format task
  • Performing formatting checks in continuous integration (CI)
  • Running ESLint with the Aspect CLI’s lint task
  • Choosing which lint findings fail the command so CI can gate on it

Formatting

Prettier is one of the most popular formatting tools in the JavaScript ecosystem. The starter repository comes preconfigured with Prettier in the tools/format/ directory at your repository root. Formatting should be automatic, unavoidable, and fast. Developers format locally with one command, and CI rejects anything that slipped through.

Format locally

The starter exposes the formatter two ways. format is a binary that bazel_env puts on your PATH. Return to the Bazel module root (the directory containing MODULE.bazel), then use it to run Prettier, built by Bazel, against the working tree:
aspect format is the Aspect CLI task. By default it only touches files that differ from the merge base, which is what you want on a branch:
Why format only changed files
  • Formatters are fast and do not disrupt your workflow.
  • Git already knows which files were edited, so only those files are touched.
  • Reformatting untouched files buries the real change in a pull request.
Want formatting to happen without thinking about it? Put format in a Git pre-commit hook or your editor’s format-on-save. The starter does not ship a hook, so this is your choice to make.

Formatting enforcement in continuous integration

Not every change goes through a developer’s terminal. Edits made in the GitHub web editor, or by a contributor who skipped the setup, arrive unformatted. The starter’s .github/workflows/ci.yaml has a format job that runs aspect format. On a CI host the task’s --severity resolves to fail, so if Prettier would change any file in the pull request the job exits non-zero and the pull request is blocked.
CI does not apply formatting automatically. Developers must run format locally and update the pull request for the check to pass.

Linting

This starter repository uses ESLint to automatically check your code for common mistakes and style issues. ESLint is the most widely used code checker in the JavaScript world, though alternatives like oxlint are also available.
The linting tools are pre-configured in the tools/lint folder and will automatically check all TypeScript and JavaScript files in your project.

How linting works

The linting system uses Bazel’s “aspect” feature, which visits your existing targets without requiring dedicated lint targets in your BUILD files. rules_js and rules_ts expose their declared JavaScript and TypeScript sources to the aspect, so ESLint checks the same inputs Bazel builds rather than walking an unrelated copy of the source tree. The Aspect CLI’s lint task drives this for you, so you don’t have to remember which aspects to apply or which output groups to request.
1

Add a test code violation

First, let’s add some intentionally problematic code to see the linter in action. Add this line to the end of packages/web/src/hello.ts:
This code has three issues:
  • The variable type is unnecessarily specified
  • The variable should use const instead of let
  • The variable is declared but never used
2

Run the linter

Lint everything beneath //packages/ with the Aspect CLI:
This exercise uses the hard strategy so it works in a newly created repository that may not have a merge base yet.Bazel runs ESLint once per target, then the CLI collects the reports and prints each finding:
Two of these have automatic fixes. Apply them with aspect lint --fix --strategy=hard //packages/.... The command changes the line to const unused = 1;; rerun the linter to see the one remaining unused-variable finding.
With the tools/bazel wrapper installed, your team can keep typing bazel lint //packages/... and Bazelisk routes it through the Aspect CLI.

Choosing which findings fail the command

The command above exited non-zero because the hard strategy fails on every error-severity finding. In day-to-day development you can omit --strategy=hard: aspect lint then uses the default hold-the-line strategy, which fails only on error-severity findings on lines you changed relative to the merge base. Existing violations elsewhere in the repository are still visible but do not block the pull request, so a team can adopt linting without cleaning up the whole codebase first. If no merge base is available, the CLI warns and falls back to hard rather than silently skipping findings. Pick a different strategy with --strategy:
The lint job in the starter’s .github/workflows/ci.yaml runs aspect lint //... with the default strategy, so a pull request fails only on the errors it introduced. See the lint task reference for the full set of options, including posting findings as pull request comments.
Do not add —@aspect_rules_lint//lint:fail_on_violation to your .bazelrc when using aspect lint. That flag makes the Bazel build itself fail, so the CLI never gets to collect the reports and you lose the per-finding output above.
aspect lint wraps aspect_rules_lint’s aspects. If you can’t use the Aspect CLI, you can apply the aspect by hand with vanilla Bazel, bazel build —aspects=//tools/lint:linters.bzl%eslint —output_groups=rules_lint_human //packages/…, then read the per-target *.AspectRulesLintESLint.out report from bazel-bin. The CLI exists so you don’t have to.