SOROUSHβ„’
MODE

Migrate from Prettier to Oxfmt with Oxfmt-quick

🧹 Migrating From pretty-quick to oxfmt-quick

Many JavaScript and TypeScript projects run Prettier in a pre-commit hook through pretty-quick. The hook formats only the staged files, so every commit is formatted and nobody has to run the whole codebase through the formatter by hand. The previous article set that up with Husky.

Oxfmt is a Rust formatter from the Oxc project. Its output closely matches Prettier's, and it runs much faster. The catch is that pretty-quick is built to call Prettier. If you switch formatters and leave the hook alone, the hook keeps running Prettier.

oxfmt-quick does the same job for Oxfmt. It finds your staged files, formats them with Oxfmt and stages the result again. This guide covers the full migration: moving your Prettier config to Oxfmt, replacing pretty-quick with oxfmt-quick in the pre-commit hook, and keeping CI as the final check.

npm i -D oxfmt oxfmt-quick

🐌 Why a staged-file tool still matters

I first assumed this was about performance. It isn't, and knowing that up front will save you the detour I took.

With Prettier, limiting the run to staged files saves real time. Process startup, module resolution and parsing every file in a JavaScript VM all add up. Oxfmt is in a different league. On the monorepo where this started, it formats 1,554 files in about 860 milliseconds. Narrowing that to the few files in a typical commit brings it down to about 300 milliseconds, a difference you won't notice.

So if formatting the whole repo is already that cheap, why bother with staged-file mode?

Re-staging. Git commits what is in the index, not what is on disk. A hook that runs plain oxfmt formats your working tree, and the commit records the older, unformatted version from the index. The hook appears to succeed, but the commit is still unformatted.

pretty-quick has always handled this for Prettier, and oxfmt-quick handles it for Oxfmt. The speed gain is a bonus.

πŸ” Moving your config from Prettier

Most of the migration maps one-to-one:

Prettier setupOxc setup
prettier --write .oxfmt
prettier --check .oxfmt --check
pretty-quick --stagedoxfmt-quick --staged
.prettierrc.oxfmtrc.json (generate it with oxfmt --migrate=prettier)
eslint-plugin-prettierremove it and run oxfmt --check in CI instead

The last row is worth a closer look. Prettier's own docs recommend keeping formatting out of your lint rules. A separate --check step in CI is cleaner and faster, and your editor no longer shows formatting issues as lint errors.

Once the config is migrated, remove the packages you no longer need:

npm uninstall prettier pretty-quick eslint-plugin-prettier

A practical tip for the switch: make the bulk reformat its own standalone commit and add its SHA to a .git-blame-ignore-revs file, so git blame keeps pointing at the people who actually wrote each line.

✨ Rewiring the hook

The Husky part carries over unchanged. Everything the previous article said about husky init and the prepare script still applies. If you're starting fresh:

npm i -D husky && npx husky init

Then .husky/pre-commit needs a single line:

npx oxfmt-quick --staged

That's the entire setup. Every commit formats its own staged files and stages the result again. If anything goes wrong, oxfmt-quick exits with a non-zero code and Git stops the commit.

Before this package existed, the same hook in my repo was nine lines of shell piping git diff into xargs into oxfmt into git add. It was hard to test, and it relied on GNU xargs behavior that differs on macOS. One line is better.

🎯 Two modes

oxfmt-quick            # everything changed since the merge-base, plus untracked files
oxfmt-quick --staged   # only the index, re-staged after formatting

The default mode mirrors pretty-quick and formats everything you've changed on your branch. Use it from the terminal to tidy up work in progress.

--staged is the pre-commit mode. It looks only at the index, because that's exactly what the commit will contain.

Both modes compare against the merge-base, so a feature branch that has fallen behind main stays focused on your own changes and doesn't reformat files someone else touched.

πŸ›‘οΈ Partially staged files

This is the case that makes a formatting hook tricky.

You stage a file, then keep editing it. Now the index and your working tree hold different versions of that file, and only the staged version goes into the commit.

A naive hook formats the file on disk and runs git add on it, which pulls in every edit you deliberately left out. The commit ends up larger than you intended.

oxfmt-quick formats the file, leaves it unstaged, tells you about it and exits with a non-zero code, so you decide what happens next:

🎯  Found 3 changed files.
✍️   Fixing up src/hooks/useCopyToClipboard.ts.
βœ—  Found partially staged file src/utils/format.ts.
βœ—  Partially staged files were formatted but left unstaged, so this commit is not
   widened with edits you did not stage. Stage them before committing.

Here, doing less is the right call: your unstaged work stays unstaged, and you choose what goes in. Tools handle this differently. Biome, for example, has a known bug where --staged reads the working-directory content instead of the staged blob.

🧩 What it leaves to Oxfmt

oxfmt-quick has no config file, no ignore handling and no extension filter, and that's deliberate.

Oxfmt already reads .oxfmtrc.json, .gitignore, .prettierignore and .editorconfig, and it skips files it can't format. Reimplementing any of that would create a second source of truth that would drift over time. So the file list goes straight to Oxfmt, and Oxfmt decides what to format. Ignored files never reach it anyway, because the list comes from Git, which already excludes them.

It has two runtime dependencies: mri and picocolors.

βš™οΈ The flags

FlagWhat it does
--stagedPre-commit mode: index only, re-staged after formatting
--since <rev>Compare against a revision instead of the merge-base
--branch <name>Branch to find the merge-base against (default main)
--checkReport without writing, exit non-zero if anything is unformatted
--bailFormat files, but exit non-zero if any of them needed formatting
--no-restageFormat without re-staging
--config <path>Pass an Oxfmt config file through
--verbosePrint every file considered

oxfmt-quick also works as a library if you want to build on it:

import { oxfmtQuick } from 'oxfmt-quick'

const { success, errors } = oxfmtQuick(process.cwd(), {
  staged: true,
  onWriteFile: (file) => console.log(`formatted ${file}`),
})

Every callback is optional. The CLI is a thin reporting layer over this one function.

πŸͺŸ A note for Windows users

Two Windows details matter here, and oxfmt-quick handles both for you.

On Windows, oxfmt on PATH is a .CMD shim, and Node needs a shell to spawn it. A shell brings back all the quoting problems that passing an argument array is meant to avoid. So oxfmt-quick locates Oxfmt through its own bin script and runs it with the current node.

File lists are read with git diff -z and split on NUL characters. When Git prints paths separated by newlines, it escapes non-ASCII characters, so splitting on \n would corrupt those file names. Splitting on NUL keeps them intact.

The test suite runs on Linux, macOS and Windows for every commit.

βœ… Final thoughts

The Husky and pretty-quick setup from the previous article works well and will keep working for as long as you use Prettier. If you've already moved to Oxfmt, oxfmt-quick fills the last gap in that workflow.

Either way, treat the pre-commit hook as a convenience and CI as the authority:

- run: npx oxfmt --check

Because a full-repo run is now so fast, this step costs almost nothing, which was never quite true of the Prettier equivalent.

  • Oxfmt formats your code, fast
  • oxfmt-quick makes sure the formatted version is what you commit
  • Husky wires it into Git with one line

Install the packages, add one line to your hook, and formatting stops being something you have to think about.

npm i -D oxfmt oxfmt-quick

πŸ“¦ oxfmt-quick on npm Β· πŸ™ Source on GitHub Β· MIT


Tags: oxc, oxfmt, prettier, pretty-quick, husky, pre-commit, clean code, typescript, javascript, formatting

Cookie-Free by Design. The only cookies we like are the ones that come fresh from the oven.