# Formatter Development

> Guide for implementing formatting rules using Biome's IR-based formatter infrastructure. Use when implementing formatting for new syntax nodes, handling comments in formatted output, or comparing Biome's formatting against Prettier for JavaScript, CSS, JSON, HTML, or other languages. Use when this capability is needed.

- Skill: `tomevault-io/formatter-development` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/formatter-development`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/formatter-development/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/formatter-development

---


## Purpose

Use this skill when implementing or modifying Biome's formatters. It covers the trait-based formatting system, IR generation, comment handling, and testing with Prettier comparison.

## Prerequisites

1. Install required tools: `just install-tools` (includes `wasm-bindgen-cli` and `wasm-opt`)
2. Language-specific crates must exist: `biome_{lang}_syntax`, `biome_{lang}_formatter`
3. For Prettier comparison: Install `bun` and run `pnpm install` in repo root

## Common Workflows

### Generate Formatter Boilerplate

For a new language (e.g., HTML):

```shell
just gen-formatter html
```

This generates `FormatNodeRule` implementations for all syntax nodes. Initial implementations use `format_verbatim_node` (formats code as-is).

### Implement FormatNodeRule for a Node

Example: Formatting `JsIfStatement`:

```rust
use crate::prelude::*;
use biome_formatter::write;
use biome_js_syntax::{JsIfStatement, JsIfStatementFields};

#[derive(Debug, Clone, Default)]
pub(crate) struct FormatJsIfStatement;

impl FormatNodeRule<JsIfStatement> for FormatJsIfStatement {
    fn fmt_fields(&self, node: &JsIfStatement, f: &mut JsFormatter) -> FormatResult<()> {
        let JsIfStatementFields {
            if_token,
            l_paren_token,
            test,
            r_paren_token,
            consequent,
            else_clause,
        } = node.as_fields();

        write!(
            f,
            [
                if_token.format(),
                space(),
                l_paren_token.format(),
                test.format(),
                r_paren_token.format(),
                space(),
                consequent.format(),
            ]
        )?;

        if let Some(else_clause) = else_clause {
            write!(f, [space(), else_clause.format()])?;
        }

        Ok(())
    }
}
```

### Using IR Primitives

Common formatting building blocks:

```rust
use biome_formatter::{format_args, write};

write!(f, [
    token("if"),           // Static text
    space(),               // Single space
    soft_line_break(),     // Break if line is too long
    hard_line_break(),     // Always break

    // Grouping and indentation
    group(&format_args![
        token("("),
        soft_block_indent(&format_args![
            node.test.format(),
        ]),
        token(")"),
    ]),

    // Conditional formatting
    format_with(|f| {
        if condition {
            write!(f, [token("something")])
        } else {
            write!(f, [token("other")])
        }
    }),
])?;
```

### Handle Comments

```rust
use biome_formatter::format_args;
use biome_formatter::prelude::*;

impl FormatNodeRule<JsObjectExpression> for FormatJsObjectExpression {
    fn fmt_fields(&self, node: &JsObjectExpression, f: &mut JsFormatter) -> FormatResult<()> {
        let JsObjectExpressionFields {
            l_curly_token,
            members,
            r_curly_token,
        } = node.as_fields();

        write!(
            f,
            [
                l_curly_token.format(),
                block_indent(&format_args![
                    members.format(),
                    // Handle dangling comments (comments not attached to any node)
                    format_dangling_comments(node.syntax()).with_soft_block_indent()
                ]),
                r_curly_token.format(),
            ]
        )
    }
}
```

Leading and trailing comments are handled automatically by the formatter infrastructure.

### Compare Against Prettier

After implementing formatting, validate against Prettier:

```shell
# Compare a code snippet
bun packages/prettier-compare/bin/prettier-compare.js --rebuild 'const x={a:1,b:2}'

# Compare with explicit language
bun packages/prettier-compare/bin/prettier-compare.js --rebuild -l ts 'const x: number = 1'

# Compare a file
bun packages/prettier-compare/bin/prettier-compare.js --rebuild -f path/to/file.tsx

# From stdin (useful for editor selections)
echo 'const x = 1' | bun packages/prettier-compare/bin/prettier-compare.js --rebuild -l js
```

**Always use `--rebuild`** to ensure WASM bundle matches your Rust changes.

### Create Snapshot Tests

Create test files in `tests/specs/` organized by feature:

```
crates/biome_js_formatter/tests/specs/js/
├── statement/
│   ├── if_statement/
│   │   ├── basic.js
│   │   ├── nested.js
│   │   └── with_comments.js
│   └── for_statement/
│       └── various.js
```

Example test file `basic.js`:
```javascript
if (condition) {
  doSomething();
}

if (condition) doSomething();

if (condition) {
  doSomething();
} else {
  doOther();
}
```

Run tests:
```shell
cd crates/biome_js_formatter
cargo test
```

Review snapshots:
```shell
cargo insta review
```

### Test with Custom Options

Create `options.json` in the test folder:

```json
{
  "formatter": {
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 80
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "single",
      "semicolons": "asNeeded"
    }
  }
}
```

This applies to all test files in that folder.

### Format and Build

After changes:

```shell
just f              # Format Rust code
just l              # Lint
just gen-formatter  # Regenerate formatter infrastructure if needed
```

## Tips

- **format_verbatim_node**: Initial generated code uses this - replace it with proper IR as you implement formatting
- **Space tokens**: Use `space()` instead of `token(" ")` for semantic spacing
- **Breaking**: Use `soft_line_break()` for optional breaks, `hard_line_break()` for mandatory breaks
- **Grouping**: Wrap related elements in `group()` to keep them together when possible
- **Indentation**: Use `block_indent()` for block-level indentation, `indent()` for inline
- **Lists**: Use `join_nodes_with_soft_line()` or `join_nodes_with_hardline()` for formatting lists
- **Mandatory tokens**: Use `node.token().format()` for tokens that exist in AST, not `token("(")`
- **Debugging**: Use `dbg_write!` macro (like `dbg!`) to see IR elements: `dbg_write!(f, [token("hello")])?;`
- **Don't fix code**: Formatter should format existing code, not attempt to fix syntax errors

## IR Primitives Reference

```rust
// Whitespace
space()                    // Single space
soft_line_break()         // Break if needed
hard_line_break()         // Always break
soft_line_break_or_space() // Space or break

// Indentation
indent(&content)          // Indent content
block_indent(&content)    // Block-level indent
soft_block_indent(&content) // Indent with soft breaks

// Grouping
group(&content)           // Keep together if possible
conditional_group(&content) // Advanced grouping

// Text
token("text")             // Static text
dynamic_token(&text, pos) // Dynamic text with position

// Utility
format_with(|f| { ... })  // Custom formatting function
format_args![a, b, c]     // Combine multiple items
if_group_breaks(&content) // Only if group breaks
if_group_fits_on_line(&content) // Only if fits
```

## References

- Full guide: `crates/biome_formatter/CONTRIBUTING.md`
- JS-specific: `crates/biome_js_formatter/CONTRIBUTING.md`
- Prettier comparison tool: `packages/prettier-compare/`
- Examples: `crates/biome_js_formatter/src/js/` for real implementations

---
> Converted and distributed by [TomeVault](https://tomevault.io/claim/biomejs) — claim your Tome and manage your conversions.
<!-- tomevault:4.0:skill_md:2026-04-11 -->

