> ## Documentation Index
> Fetch the complete documentation index at: https://site.aspect.build/llms.txt
> Use this file to discover all available pages before exploring further.

# Formatting & Linting

> Format and lint JavaScript in Bazel with aspect_rules_lint, wiring Prettier and ESLint into pre-commit hooks, CI checks, and the Aspect CLI workflow.

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](https://prettier.io) is one of the most popular formatting tools in the JavaScript ecosystem. The [starter repository](https://github.com/aspect-starters/js) 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:

```bash theme={null}
format                 # every file the formatter understands
format packages/lib/src/index.ts
```

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

```bash theme={null}
aspect format               # files changed on this branch
aspect format --scope=all   # the whole tree
```

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

<Tip>
  Want formatting to happen without thinking about it? Put <code>format</code> 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.
</Tip>

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

<Warning>
  CI does not apply formatting automatically.
  Developers must run <code>format</code> locally and update the pull request for the check to pass.
</Warning>

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

<Note>
  The linting tools are pre-configured in the `tools/lint` folder and will automatically check all TypeScript and JavaScript files in your project.
</Note>

### 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`](/docs/cli/tasks/lint) task drives this for you, so you don't have to remember which aspects to apply or which output groups to request.

<Steps>
  <Step title="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`:

    ```typescript theme={null}
    let unused: number = 1;
    ```

    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
  </Step>

  <Step title="Run the linter">
    Lint everything beneath `//packages/` with the Aspect CLI:

    ```bash theme={null}
    aspect lint --strategy=hard //packages/...
    ```

    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:

    ```
    🧹 Linters (1): ESLint
    Lint findings after hard (3)
      🚨 packages/web/src/hello.ts:13 · ESLint — Type number trivially inferred from a number literal, remove type annotation. (@typescript-eslint/no-inferrable-types)
      🚨 packages/web/src/hello.ts:13 · ESLint — 'unused' is never reassigned. Use 'const' instead. (prefer-const)
      🚨 packages/web/src/hello.ts:13 · ESLint — 'unused' is assigned a value but never used. (@typescript-eslint/no-unused-vars)
    → ❌ Failed lint task (exit code 1) in 3.1s · 3 errors
    ```

    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.
  </Step>
</Steps>

<Info>
  With the [`tools/bazel` wrapper](/docs/cli/install#keep-your-team-typing-bazel-with-the-tools/bazel-wrapper) installed, your team can keep typing `bazel lint //packages/...` and Bazelisk routes it through the Aspect CLI.
</Info>

### 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`:

| Strategy                  | When it fails                                      |
| ------------------------- | -------------------------------------------------- |
| `hold-the-line` (default) | Error-severity findings on changed lines           |
| `hold-the-file`           | Error-severity findings anywhere in a changed file |
| `hard`                    | Any error-severity finding in any linted target    |
| `soft`                    | Never, report only                                 |

```bash theme={null}
aspect lint --strategy=hard //packages/...   # zero tolerance
aspect lint --strategy=soft //packages/...   # informational only
```

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](/docs/cli/tasks/lint) for the full set of options, including posting findings as pull request comments.

<Warning>
  Do not add <code>--\@aspect\_rules\_lint//lint:fail\_on\_violation</code> to your <code>.bazelrc</code> when using <code>aspect lint</code>. 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.
</Warning>

<Note>
  <code>aspect lint</code> wraps <code>aspect\_rules\_lint</code>'s aspects. If you can't use the Aspect CLI, you can apply the aspect by hand with vanilla Bazel, <code>bazel build --aspects=//tools/lint:linters.bzl%eslint --output\_groups=rules\_lint\_human //packages/...</code>, then read the per-target <code>\*.AspectRulesLintESLint.out</code> report from <code>bazel-bin</code>. The CLI exists so you don't have to.
</Note>
