Files
oxc-project__oxc/apps/oxfmt/AGENTS.md
leaysgur f94b22296c test(formatter,formatter_css,formatter_json,formatter_yaml): restore coverage lost in fixture consolidation (#26129)
- Restore wrongly consolidated fixtures
- State new fixtures policy and apply
- Check `endOfLine` option and output at test harness level
2026-08-27 06:12:52 +00:00

228 lines
12 KiB
Markdown

# Coding agent guides for `apps/oxfmt`
Oxfmt is the integration layer that dispatches between the Rust formatters built on `oxc_formatter_core` and Prettier.
## Overview
The `oxfmt` implemented under this directory serves several purposes.
- JS/Rust hybrid CLI using `napi-rs`
- Full feature set like CLI, Stdin, LSP, and more
- Format many languages with embedded language formatting support like Prettier
- Entry point: `src-js/cli.ts` which uses `run_cli()` from `src/main_napi.rs`
- Build with `pnpm build`
- Pure Rust CLI
- Limited feature set, CLI usage only, no LSP, no Stdin support
- Formats only languages which we rewrite formatter in Rust
- Entry point: `main()` in `src/main.rs`
- Build with `cargo build --no-default-features`
- Node.js API using napi-rs
- Caller-supplied options, no config discovery
- Entry point: `src-js/index.ts` which uses `format()` from `src/main_napi.rs`
- Build with `pnpm build`
When making changes, consider the impact on all paths.
Cross-cutting behavior is applied through several entry points:
- CLI: `src/cli/walk_runner.rs` and `src/cli/walk.rs`
- Stdin: `src/cli/stdin_runner.rs`
- LSP: `src/lsp/server_formatter.rs`
- NAPI direct-document API: `src/api/format_api.rs`
- NAPI `textToDoc()` API for `prettier-plugin-oxfmt`: `src/api/text_to_doc_api.rs`
Check the relevant path before assuming that behavior is shared across entry points.
### Platform considerations
Oxfmt is built for multiple platforms (Linux, macOS, Windows) and architectures.
When working with file paths in CLI code, be aware of Windows path differences:
- Use `std::path::Path` / `PathBuf` instead of manual string manipulation with `/`
- Be cautious with path comparisons and normalization across platforms
- Avoid hardcoding `/` as a path separator; prefer `Path::join()`
- Windows uses `\` as a path separator and has drive letter prefixes (e.g., `C:\`)
## CLI implementations
Oxfmt shares code with Oxlint regarding its CLI implementation.
- Rust implementation: `crates/oxc_config`
- JS implementation: `apps/shared`
Please exercise extra caution when making changes to these files.
### Ignore architecture
Ignore handling is intentionally split across entry points.
Use the entry-point map above; keep detailed behavior and rationale next to the relevant implementation and tests.
The stable distinction is:
- Formatter-owned ignores control formatting eligibility where the entry point supports them
- These include `.prettierignore`, CLI `--ignore-path`, `!` patterns and config `ignorePatterns`
- They exclude an explicitly requested document
- Git-derived ignores scope filesystem discovery
- `.gitignore` and `.git/info/exclude`
- They do not exclude an explicitly requested document
## Formatter implementations
Oxfmt utilizes different implementations depending on the file extension and filename:
- Tier 1: Rust implementations using `oxc_formatter`, `oxc_formatter_json`, etc found in this repository
- Tier 2: Rust implementations using external libraries like `oxc_toml`
- Tier 3: Delegations to Prettier via NAPI-JS calls (e.g., for Vue or Markdown)
- Tier 4: Delegations to Prettier that require additional Prettier plugins (e.g., for Svelte)
NOTE: Rust written formatters never fall back to Prettier, since they exist to reduce the dependency on Prettier.
### Divergence vs Prettier
Known divergences live in DIVERGENCES.md (embedding / dispatch layer only; single-language ones live in the owning crate's DIVERGENCES.md).
- Pin convention: a minimal repro lives in `conformance/fixtures/edge-cases/`
any external fixture containing the same diff carries the same NOTE in `conformance/run.ts`;
the duplication is deliberate, not a consolidation target
- All current entries are embedding-layer decisions that outlive the Prettier delegation;
if an entry about the Prettier-fallback boundary itself is ever added, group it under a
separate heading in DIVERGENCES.md so the removal scope stays obvious when the fallback goes away
### Embedded language formatting
Embedded languages (e.g. css-in-js, CSS front matter YAML) go through the `FormatDispatcher` (defined in `oxc_formatter_core`) assembled by `src/core/embed/dispatcher.rs`.
Routing is ONE table (`dispatcher::route`): `Native` languages (css/graphql/yaml/json/...) get a Rust branch, the `Prettier` set (html/angular/markdown) goes to the Prettier Doc→IR channel (`embed/prettier_doc.rs`, napi only), everything else is deliberately preserved.
Vocabulary: "fallback" = the dispatcher's optional `PrettierDocFallback` slot (a build/root may not install one).
The pure Rust build runs fallback-less, so non-native embeds (html-in-js, TOML/custom front matter) deliberately stay verbatim.
Three roots install `SessionServices`, all via `embed/services.rs::for_root` (one definition per build; the napi one takes the `ExternalServices` transport, and adds the Prettier fallback / string embedder / Tailwind sorter to the registry dispatcher): the JS/TS and CSS file roots (`core/format.rs`, `PhysicalFile` sessions, both builds) and the Vue/Svelte `<script>` root (`api/text_to_doc_api.rs`, `VirtualDocument` session, napi only).
Which languages may dispatch AT ALL from a given host is the host crate's own gate (e.g. `oxc_formatter_css` dispatches only `yaml`/`toml` front matter); the shared `route()` table then decides who serves the language. Adding a dispatch call to a host crate is therefore a routing decision (check `route()` and the embedded conformance when doing so. A root needing a bespoke service set would assemble the `SessionServices` struct literally), none does today.
`embeddedLanguageFormatting: off` installs no dispatcher, every builder consults the same off-gate, `ResolvedDispatchConfig::is_embedded_formatting_enabled`.
Tracing span namespaces: `oxfmt::embed::` = pure Rust work, `oxfmt::external::` = napi-crossing calls.
Per-language options are NOT built up front: `ResolvedDispatchConfig` maps them lazily at dispatch time (`OnceLock`-memoized) from the host file's resolved config, including the Prettier options JSON for the JS-side consumers. `src/core/external_services.rs` bridges the napi callbacks into these factories.
A separate string-out channel (the session's `string_embedder` service, NOT the dispatcher) carries JSDoc's string-in/string-out consumer:
- JSDoc fenced code blocks: routing follows ONE rule, the same `dispatcher::route` table
- a `Native` fence language formats through `FormatSession::dispatch_to_string` via a thin string adapter (`embed/jsdoc_fence.rs::format_native_fence`, EVERY build, the pure Rust build wires it via `services::for_root`)
- md/html/angular fences stay on the Prettier string path (`embed/prettier_string.rs`, napi only; their Doc→IR conversion has unrepresentable cases);
- everything else stays verbatim
- the embedder carries the caller's effective print width; both branches honor it (native via `PrintWidth` override, Prettier via `printWidth` in the options JSON), so a fence prints at the same width a JS/TS snippet in the same position would (see `upstream-jsdoc-bugs.md` #11 for the deliberate divergence from upstream's flat `printWidth - 4`)
NOTE: The string-out channel outlives the md/html/angular rewrites; its full exit criterion is owned by `oxc_formatter_core`'s AGENTS.md (domain (4)).
The half owned here: JSDoc's string-out is NOT structural, fences can move to IR-out (session dispatch inside the comment IR) once the printer grows a per-line prefix mechanism for the `*` continuation, deferred for verification time, not by design.
### Tailwind CSS class sorting
Tailwind class sorting (`sortTailwindcss`) splits responsibilities:
- Rust collects classes into `FormatElement::TailwindClass`
- `className` etc. in JS/TS, `@apply` in CSS — incl. css-in-js and JSDoc fenced blocks
- and the JS side sorts them in one batch
- `sortTailwindClasses` → tailwind's `getClassOrder`, which needs the resolved Tailwind config
Embedded boundaries carry classes through `DispatchPayload::tailwind_classes`;
each embed site consumes the doc via `DispatchPayload::into_doc(collector)`, which merges them into the parent's class space.
The four data paths (JS/TS top-level / standalone CSS / embedded CSS / JSDoc fenced CSS) are documented at `embed::services::for_root` (napi definition).
No CSS goes to Prettier for this; the pure Rust build never collects at all (both mappers gate collection behind napi, since no sorter exists there).
Consequently, managing these various formatter implementations and handling their respective options are also part of Oxfmt's responsibilities.
## Verification
```sh
cargo c
cargo c --no-default-features
cargo c --features detect_code_removal
```
Also run `clippy` for the same configurations and resolve all warnings.
Run tests with:
```sh
# Run unit test in Rust
cargo t
# Run E2E test
pnpm build-dev && pnpm t
# Update snapshots
pnpm t -u
# Run conformance test for xxx-in-js and js-in-xxx
pnpm build-dev && pnpm download-fixtures && pnpm conformance
```
To manually verify the CLI behavior after building:
```sh
pnpm build-test
# Show help
node ./dist/cli.js --help
# Stdin (Prettier's `--config=<cfg> <file>` equivalent)
cat <file> | node ./dist/cli.js --config=<cfg> --stdin-filepath=<file>
# With log
OXC_LOG=debug node ./dist/cli.js --threads=1 <file>
```
NOTE: `pnpm build-dev` combines `pnpm build-js` and `pnpm build-napi`, so you don't need to run them separately.
To compare formatting output with Prettier:
```sh
# Use a shared config file (e.g., fmt.json) because Oxfmt and Prettier have different default printWidth.
# Example fmt.json: { "printWidth": 80 }
cat <file> | node ./dist/cli.js --config=fmt.json --stdin-filepath=<file>
node ./node_modules/prettier/bin/prettier.cjs --config=fmt.json <file>
```
## Test organization (`test/` directory)
Tests are organized into specific domains, each with its own structure.
### `test/api/`: Formatting result tests
Focuses on verifying formatting output. Use the Node.js API. No fixtures, test inputs are inline in each test file.
- Multiple `*.test.ts` files coexist in a flat directory (no subdirectories)
- Snapshots are colocated in `__snapshots__/` by Vitest
### `test/cli/`: CLI fixture-driven tests
A single `cli.test.ts` auto-discovers and runs all fixture directories via `utils.ts`.
- Each fixture directory contains:
- `options.json` — array of test cases (args, cwd, env, stdin, etc.)
- `fixtures/` — input files for the test cases
- `*.snap.md` — file snapshots (one per test case, named `0.snap.md`, `1.snap.md`, …)
- Adding a new CLI test: create a new directory with `options.json` and `fixtures/`, then run the test to generate snapshots
- `fixtures/` represents a single project structure
- Multiple test cases (different args, cwd, etc.) against the same structure go in one `options.json`
- If a scenario needs a different project layout, create a separate directory
- Name related directories with a shared prefix (e.g., `nested_config/`, `nested_config_no_root/`)
- If exceptional test cases are required, place a separate `*.test.ts` file for them
### `test/lsp/`: LSP integration tests
Each test directory follows the 1:1:1 rule:
- 1 test file (`*.test.ts` with the same name as the directory)
- 0 or 1 `fixtures/` directory
- Snapshots are colocated in `__snapshots__/` by Vitest
Shared helpers are in `utils.ts` at the `test/lsp/` level.
## After updating `Oxfmtrc` (`src/core/oxfmtrc.rs`)
When modifying the `Oxfmtrc` struct (and configuration options):
- Run `just formatter-schema-json` to update `npm/oxfmt/configuration_schema.json`
- Run `just formatter-config-ts` to regenerate `src-js/config.generated.ts` from the schema
- Run `cargo test -p website_formatter` to update schema markdown snapshots
- Then, `cargo insta accept`