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 Install required tools: just install-tools (includes wasm-bindgen-cli and wasm-opt ) Language-specific crates must exist: biome_{lang}syntax , biome_formatter For Prettier comparison: Install bun and run pnpm install in repo root Common Workflows Generate Formatter Boilerplate For a new language (e.g., HTML): 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 : 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: 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 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:
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 : 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 Test with Custom Options 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. Format and Build After changes: 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 // 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