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 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.
just install-tools (includes wasm-bindgen-cli and wasm-opt)biome_{lang}_syntax, biome_{lang}_formatterbun and run pnpm install in repo rootFor a new language (e.g., HTML):
just gen-formatter html
This generates implementations for all syntax nodes. Initial implementations use (formats code as-is).
FormatNodeRuleformat_verbatim_nodeExample: Formatting JsIfStatement:
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(())
}
}
Common formatting building blocks:
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")])
}
}),
])?;
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.
After implementing formatting, validate against Prettier:
# 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 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:
if (condition) {
doSomething();
}
if (condition) doSomething();
if (condition) {
doSomething();
} else {
doOther();
}
Run tests:
cd crates/biome_js_formatter
cargo test
Review snapshots:
cargo insta review
Create options.json in the test folder:
{
"formatter": {
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 80
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"semicolons": "asNeeded"
}
}
}
This applies to all test files in that folder.
After changes:
just f # Format Rust code
just l # Lint
just gen-formatter # Regenerate formatter infrastructure if needed
space() instead of token(" ") for semantic spacingsoft_line_break() for optional breaks, hard_line_break() for mandatory breaksgroup() to keep them together when possibleblock_indent() for block-level indentation, indent() for inlinejoin_nodes_with_soft_line() or join_nodes_with_hardline() for formatting listsnode.token().format() for tokens that exist in AST, not token("(")dbg_write! macro (like dbg!) to see IR elements: dbg_write!(f, [token("hello")])?;// 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
crates/biome_formatter/CONTRIBUTING.mdcrates/biome_js_formatter/CONTRIBUTING.mdpackages/prettier-compare/crates/biome_js_formatter/src/js/ for real implementations