---
name: formatter-development
description: Guide for implementing formatting rules using Biome's IR-based formatter infrastructure. Use when working on formatters for JavaScript, CSS, JSON, HTML, or other languages. Examples:User needs to implement formatting for a new syntax nodeUser wants to handle comments in formatted outputUser is comparing Biome's formatting against Prettier
---
## 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 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 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