/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, writing or debugging formatter snapshot tests, diagnosing idempotency failures, or comparing
$ npx -y skills add biomejs/biome --skill formatter-development --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/formatter-development
Context preview
The summary Claude sees to decide when to auto-load this skill.
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, writing or debugging formatter snapshot tests, diagnosing idempotency failures, or comparing
SKILL.md
formatter-development.SKILL.mdname: formatter-development
description: 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, writing or debugging formatter snapshot tests, diagnosing idempotency failures, or comparing Biome's formatting against Prettier for JavaScript, CSS, JSON, HTML, Markdown, or other languages.
compatibility: Designed for coding agents working on the Biome codebase (github.com/biomejs/biome).
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):
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.
Tips:
- Pass code snippets in single quotes to avoid shell interpretation.
- A literal backslash-n in a CLI argument is not converted to a newline — use a real newline or a file (`-f`).
- Safe to run in read-only or planning mode: `--rebuild` only rebuilds the Biome WASM bundle and does not modify source files.
- See `packages/prettier-compare/README.md` for full CLI details.
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 th
Read more
name: formatter-development description: 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, writing or debugging formatter snapshot tests, diagnosing idempotency failures, or comparing Biome's formatting against Prettier for JavaScript, CSS, JSON, HTML, Markdown, or other languages. compatibility: Designed for coding agents working on the Biome codebase (github.com/biomejs/biome).
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):
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.
Tips:
- Pass code snippets in single quotes to avoid shell interpretation.
- A literal backslash-n in a CLI argument is not converted to a newline — use a real newline or a file (`-f`).
- Safe to run in read-only or planning mode: `--rebuild` only rebuilds the Biome WASM bundle and does not modify source files.
- See `packages/prettier-compare/README.md` for full CLI details.
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 th
A toolchain for web projects, aimed to provide functionalities to maintain them. Biome offers formatter and linter, usable via CLI and LSP.
Repo: biomejs/biome
Other skills on biome.
- /biome-code-review
Static, read-only code review of a Biome (github.com/biomejs/biome) pull request, local branch, commit range, diff, or working tree being prepared as a PR. Applies Biome's own conventions - nursery placement and naming for lint rules, diagnostics quality, allocation and borrow
Open skill - /biome-developer
General development best practices and common gotchas when working on Biome. Use for avoiding common mistakes, understanding Biome-specific patterns (AST, syntax nodes, string extraction, embedded languages), and learning technical tips.
Open skill - /changeset
Guide for creating and writing proper changesets for Biome PRs. Use when a PR introduces user-visible changes (bug fixes, new features, rule changes, formatter changes, parser changes) that need a changeset entry for the CHANGELOG. Trigger when creating changesets, writing
Open skill - /diagnostics-development
Guide for creating high-quality, user-friendly diagnostics in Biome. Use when creating diagnostics for lint rules, adding helpful advice to error messages, implementing code frame displays, or improving diagnostic quality.
Open skill - /doc-comments
How to write inline comments, rustdoc, and module documentation in the Biome codebase. The audience is Biome developers reading the source, not end users. Use whenever writing or editing `//` comments, `///` item docs, or `//!` module docs — including comments added incidentally
Open skill - /eslint-migrate-options
Guide for implementing ESLint-to-Biome rule option migrators inside `biome migrate eslint`. Use whenever you add or update a Biome lint rule that has an ESLint source rule with configurable options, need to deserialize plugin-specific ESLint options, or need custom migration
Open skill

