Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
biomejs avatar

Formatter Development

  • 89 installs
  • 25.5k repo stars
  • Updated August 5, 2026
  • biomejs/biome

formatter-development is a skill that guides Biome contributors to implement IR-based formatting rules for syntax nodes.

About

This skill guides a Biome contributor to implement formatting rules using Biome's IR-based formatter infrastructure. It covers the trait-based formatting system, IR generation, comment handling, and snapshot testing with Prettier comparison. A contributor uses it when adding formatting for new syntax nodes or diagnosing idempotency failures.

  • Guides implementing Biome's IR-based formatter with the FormatNodeRule trait
  • Covers comment handling, idempotency testing, and IR primitives like soft_line_break and group
  • Explains comparing Biome formatting against Prettier via prettier-compare

Formatter Development by the numbers

  • 89 all-time installs (skills.sh)
  • Ranked #470 of 1,352 Code Review & Quality skills by installs in the Skillselion catalog
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
At a glance

formatter-development capabilities & compatibility

Capabilities
prettier compare · testing codegen
Use cases
testing · refactoring
From the docs

What formatter-development says it does

Use this skill when implementing or modifying Biome's formatters.
SKILL.md
The testing infrastructure is designed for catching idempotency cases
SKILL.md
Always use `--rebuild`** to ensure WASM bundle matches your Rust changes.
SKILL.md
npx skills add https://github.com/biomejs/biome --skill formatter-development

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs89
repo stars25.5k
Last updatedAugust 5, 2026
Repositorybiomejs/biome

What it does

Implement Biome formatting for syntax nodes using the IR-based FormatNodeRule infrastructure.

Who is it for?

Contributors implementing or debugging Biome formatters

Skip if: Formatting your own project with Biome as an end user

When should I use this skill?

Implementing formatting for new syntax nodes or diagnosing idempotency failures

What you get

A FormatNodeRule implementation validated by snapshot tests and Prettier comparison.

By the numbers

  • Two idempotency checks: printed output and IR output must be stable

Files

SKILL.mdMarkdownGitHub ↗

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

Code Standards

CRITICAL: No Emojis

Emojis are BANNED in all formatter code:

  • NO emojis in code comments
  • NO emojis in rustdoc documentation
  • NO emojis in test files
  • NO emojis in debug output or error messages

Keep all code professional and emoji-free.

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.

Format and Build

After changes:

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

Testing infrastructure

The testing infrastructure of the formatters is divided in two main pieces. Internal and external.

The testing infrastructure is designed for catching idempotency cases, which means that each file inside the infrastructure is designed to fail if:

  • the final printed output differs on a second formatting run.
  • the final IR output differs on a second formatting run.

Both must be fixed.

Run cargo t twice. The first run may write or update snapshots; the second run re-formats the just-written output and confirms it is stable. Skipping the second run hides idempotency bugs — a broken formatter can look green on a single pass because the snapshot it wrote matches itself.

quick_test.rs

  • Use quick_test.rs inside the crate for testing theories and formatting.
  • Only modify the source string literal inside the test. Do not change the parse/format/assert scaffolding around it — that scaffolding already verifies idempotency and prints the CST and IR you need for debugging.

External infra

The external infra relies on a human pulling the tests inside the repository, inside the folder <crate>/tests/prettier.

Once the tests are ported, the infrastructure produces two files for each original file:

  • <file_name>.<ext>.prettier-snap which contains the output generated by Prettier at the moment the test was ported.
  • <file_name>.<ext>.snap which contains three sections
  • the input source
  • the list of diffs between Prettier and Biome
  • the output generated by Biome

The .snap file is only created when Biome's output differs from Prettier's. When the two agree, no .snap file is written.

The absence of a .snap file is positive — it means Biome matches Prettier for that input.

Internal infrastructure

The internal infrastructure relies on creating new test files. For each test, place two snippets in the same file:

  • a piece of source code already formatted the way Biome should produce it
  • the same code in an unformatted shape

After running the formatter, both snippets should produce identical output. That identity proves the formatter converges on a canonical form and is idempotent.

Always create new test cases when implementing a feature or fixing a bug. Internal tests exercise the exact shape you care about and survive even if the Prettier corpus changes.

Do not rely on Prettier `.snap` files disappearing as proof of correctness. A missing .snap only means Biome and Prettier agree on that specific ported input. It does not cover the edge cases you introduced — write internal tests for those, and do not delete a Prettier .snap to make a diff "go away".

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.

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

Related skills

Code Review & Qualitybackendtesting

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.