Due to historical reasons, the IR was passed as `[Doc, (Doc, ...Doc)]` only when processing `gql-in-js`, whilst for other cases such as `css-in-js` and `html-in-js`, it was always `[Doc]`.
Now, every result is single `Doc`.
Fixes#20525, (thanks @oxura !)
```sh
# Many files that route to the Prettier `formatFile` callback and fail to parse
mkdir -p /tmp/repro
for i in $(seq 1 500); do printf '<template><div></template>' > /tmp/repro/broken$i.vue; done
# Run repeatedly with default concurrency
for run in $(seq 1 10); do node apps/oxfmt/dist/cli.js --write /tmp/repro; done
```
Within a few runs it crashes non-deterministically (exit codes varied: 133 / 138 / 139, and sometimes 2 = clean error report), with the same fatal as #20525:
```
# Fatal error in , line 0
# Check failed: object_ != kGlobalHandleZapValue. (or: node->IsInUse())
...
napi_reference_unref
<napi::error::Error as ...Drop>::drop
core::ptr::drop_in_place<napi::error::Error>
oxfmt::core::external_formatter::wrap_format_file::{{closure}}
```
Each broken file makes the `formatFile` callback reject; the resulting `napi::Error` is dropped on the TSFN await path and reaches `napi_reference_unref` during teardown → V8 fatal check.
After this fix (callback resolves errors as data), the same workload runs clean.
Part of #15180
gql-in-js is the simplest among other xxx-in-js cases.
It works by:
- splitting the template with `${}`, converting each part into an IR using:
- `prettier({ parser:"graphql" })` and `oxc_formatter` respectively
- and then interleaving them
However, for this to work, two things are essential:
- Obtaining a formatted `Doc` from Prettier, rather than just a formatted `string`.
- Converting the `Doc` JSON into `oxc_formatter`'s IR.
The diffs primarily arise from the implementation of these two.
### TODOs
- [x] Implement by Claude
- [x] Self review and fix up
- [x] Check bench, ecosystem
### Overview
Part of #16608
This PR applies `oxc_formatter` to the js-in-vue section.
However, it is not complete and is only applied to the following parts within js-in-vue:
- `script` block
- and `generic="..."`
- `v-for` LHS
- `v-slot`
In the `script` block, `external_callback` is used like a regular JS file, but it isn't for others.
TL;DR: Now you can use sort-imports for Vue files.
### Review points
The points I'd like to ask your review are the following:
- Addition of `format_node()` to `oxc_formatter`
- Made it possible to process any type that implements `Format`, not just `Program`
- The implementation of `js-in-vue` fragment was separated as much as possible to avoid impacting existing code
- `oxc_formatter` IR to prettier `Doc`
### TODOs
- [x] Planning
- [x] Implementation
- [x] Check benchmark
- [x] Check coverage
- [x] Understand & refactor
- [x] oxfmt: JS side
- [x] oxfmt: Rust side
- [x] oxfmt: to_doc.rs
- [x] oxc_formatter: fragment_format
- [x] Check benchmark again
- [x] Check coverage again
- [x] Check ecosystem-ci
- [x] Tests 👉🏻#19563
Part of #16608
Prettier's `textToDoc()` context has special handling based on hidden flags.
Initially, for Markdown, I implemented with heuristic string manipulation.
However,
- Similar processing is needed for HTML and Vue
- There are cases that cannot be handled by string manipulation alone
- Considering adding options to the `oxc_formatter` side
For these reasons, a refactoring was performed as preparation.
Follow up on #19000.
- Replace `fs::canonicalize()` with `utils::normalize_relative_path()`
- to avoid Windows `\\?\` prefix
- Fix `config_dir` being `None` due to `Option::take()` in `build_and_validate()`
- Which made the previous path resolution dead code 🙈
- Move tailwind path resolution earlier:
- from JSON Value manipulation in `finalize_external_options()`
- to typed `FormatConfig::resolve_tailwind_paths()`
- Pass `cwd` explicitly to `resolve_options_from_value()` instead of calling `current_dir()` internally
Fixes#19194
- Changed from passing file name as `filepath` to using absolute path
- For Tailwind's sorter, retrieve it via `options.filepath`
- Ensure `filepath` is passed in all code paths
- Also, path resolution should be handled at a higher flow
Previously, both `oxc_formatter` (SUPPORTED_TAGS) and `oxfmt` JS (TAG_TO_PARSER) had knowledge about supported embedded language tags.
This created duplication and unclear responsibility boundaries.
- Remove `SUPPORTED_TAGS` and `is_supported_tag()` from `oxc_formatter`
- Add `tag_to_prettier_parser()` in `oxfmt` Rust side as single source of truth
- Change JS callback signature from `tagName` to `parserName`
- Simplify JS `formatEmbeddedCode()` to just call Prettier with given parser
Based on https://github.com/oxc-project/oxc/pull/16826
## Summary
Add experimental Tailwind CSS class sorting support to Oxfmt via integration with `prettier-plugin-tailwindcss`.
This implementation uses a patch-based approach to export necessary functions from `prettier-plugin-tailwindcss`. We've requested better integration support from the Tailwind team at https://github.com/tailwindlabs/prettier-plugin-tailwindcss/issues/430, and can improve the integration when that's available.
## Features
- ✅ Sorts Tailwind CSS classes in `className` and `class` attributes
- ✅ Sorts classes in custom JSX attributes (configurable)
- ✅ Sorts classes in function calls like `clsx()`, `cn()`, `tw()` (configurable)
- ✅ Supports template literals with expressions
- ✅ Supports Tailwind v3 config files and v4 stylesheets
- ✅ Preserves whitespace and duplicates (configurable)
- ✅ **Fixes [prettier-plugin-tailwindcss#426](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/issues/426)** - Correctly handles strings in nested non-Tailwind call expressions
- ⏳ Non-JS file support (HTML, Vue, Svelte, etc.) - Coming in follow-up PR
- ⏳ Tailwind CSS config migration in `oxfmt --migrate=prettier` - Coming in follow-up PR
## Example Usage
### Basic Usage
Enable Tailwind CSS sorting with default options by setting `experimentalTailwindcss` to an empty object in your `.oxfmtrc.json`:
```json
{
"experimentalTailwindcss": {}
}
```
### With Configuration Options
```json
{
"experimentalTailwindcss": {
"config": "./tailwind.config.js",
"functions": ["clsx", "cn", "cva", "tw"],
"attributes": ["myCustomClass"],
"preserveWhitespace": false,
"preserveDuplicates": false
}
}
```
### Configuration Options
All options from `prettier-plugin-tailwindcss` are supported (with `tailwind` prefix removed):
| Option | Type | Description | Default |
|--------|------|-------------|---------|
| `config` | `string` | Path to your Tailwind CSS v3 configuration file. Paths are resolved relative to the Oxfmt configuration file. | `"./tailwind.config.js"` |
| `stylesheet` | `string` | Path to your Tailwind CSS v4 stylesheet. Paths are resolved relative to the Oxfmt configuration file. | `undefined` |
| `functions` | `string[]` | List of custom function names that contain Tailwind CSS classes (e.g., `["clsx", "cn", "cva", "tw"]`). | `[]` |
| `attributes` | `string[]` | List of custom JSX attributes that should be sorted (e.g., `["myCustomClass"]`). Note: `class` and `className` are always sorted by default. | `[]` |
| `preserveWhitespace` | `boolean` | Preserve whitespace around classes. | `false` |
| `preserveDuplicates` | `boolean` | Preserve duplicate classes. | `false` |
## Migration from prettier-plugin-tailwindcss
If you're currently using `prettier-plugin-tailwindcss`, you can migrate to Oxfmt's built-in support:
### Manual Migration (Current)
#### Before (Prettier)
```json
{
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindConfig": "./tailwind.config.js",
"tailwindFunctions": ["clsx", "cn"]
}
```
#### After (Oxfmt)
```json
{
"experimentalTailwindcss": {
"config": "./tailwind.config.js",
"functions": ["clsx", "cn"]
}
}
```
**Key differences:**
- Remove `tailwind` prefix from all option names (e.g., `tailwindConfig` → `config`)
- No need to install `prettier-plugin-tailwindcss` separately, it is bundled within Oxfmt
### Automated Migration (Follow-up PR)
Tailwind CSS configuration migration will be added to the existing `oxfmt --migrate=prettier` command in a follow-up PR. This will automatically convert your `prettier-plugin-tailwindcss` settings from `.prettierrc` to `.oxfmtrc.json`.
## Implementation Details
### Technical Approach
- Uses `pnpm patch` to export necessary functions from `prettier-plugin-tailwindcss`
- Implements Rust-side detection for Tailwind contexts (JSX attributes, function calls, template literals)
- Passes class strings to the JS plugin for sorting via external callback
- Optimized context tracking to avoid repeated AST traversal
### Improvements Over prettier-plugin-tailwindcss
- **Context-based quasi position tracking** ([tailwindcss.rs:273-276](https://github.com/oxc-project/oxc/blob/main/crates/oxc_formatter/src/utils/tailwindcss.rs#L273-L276)) - Stores template literal position in context stack instead of traversing AST, improving performance
- **Fixes [issue #426](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/issues/426)** - Correctly handles strings in nested non-Tailwind call expressions (e.g., `value.includes("\n")` no longer gets corrupted)
- **Comprehensive edge case testing** - Added 58 tests covering edge cases, nested scenarios, error handling, and performance
## Changes
- Changed `experimentalTailwindcss` option from `boolean` to `TailwindcssOptions` object
- Added all configuration options from `prettier-plugin-tailwindcss` (without `tailwind` prefix)
- Exported necessary functions from `prettier-plugin-tailwindcss` via patch
- Added comprehensive test suite with 58 tests
- Improved documentation for all configuration options
## Test Plan
- [x] `pnpm test` passes (58 tests)
- [x] `cargo clippy` passes
- [x] `just fmt` passes
- [x] Basic Tailwind class sorting works with `class` and `className` attributes
- [x] `attributes` option sorts custom JSX attributes
- [x] `functions` option sorts string arguments in function calls (e.g., `clsx()`, `cn()`)
- [x] Template literals with expressions handle whitespace correctly
- [x] Edge cases: empty strings, Unicode, emoji, special characters
- [x] Nested scenarios: deeply nested template literals, function calls
- [x] Strings in nested non-Tailwind calls are preserved (fixes [#426](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/issues/426))
- [x] Test in https://github.com/tailwindlabs/headlessui and https://github.com/dubinc/dub
## Follow-up Work
- **Non-JS file support** - HTML, Vue, Svelte, etc. by enabling original plugin when `experimentalTailwindcss` is set
- **Migration support** - Add Tailwind CSS config migration to existing `oxfmt --migrate=prettier` command
- **Better integration** - Improve when https://github.com/tailwindlabs/prettier-plugin-tailwindcss/issues/430 is resolved
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Refactoring for #15850
### Before this PR
- The config file and its option values were resolved only once at the beginning
- They were kept for `OxcFormatter` and also being cached via `setupConfig()` for `ExternalFormatter`
- When calling `SourceFormatter.format()`, it always referenced those values
### After this PR
- The config file and its option values are resolved only once at the beginning
- Those values are still kept as before
- But, this is for a fast path when there are no per-path overrides in `.editorconfig`
- When calling `SourceFormatter.format()`
- If `.editorconfig` exists and has per-path overrides, they are re-resolved per path
- Otherwise, it continues to reference the initially cached values
We only support the root `.editorconfig`, but since glob-based per-path overrides can be written there, we have to resolve options on each path. Some performance impact is unavoidable.
However, in scenarios where `.editorconfig` doesn't exist, or exists but has no per-path overrides, the cost is limited to just cloning the cached values per path.
Previously,
- `oxfmt(napi) not-exists.js` returns `exitCode: 1`
- `oxfmt(rust) not-exists.js` returns `exitCode: 2` = Expected
To fix this, I updated napi callback return value from `bool`.
Additionally, I realized that by returning the processing `Mode` simultaneously, Rust can handle the CLI arguments in better way. Like `--init --help`, which now shows help.
And finally changed napi binding name from `format()` to `runCli()` for the future Node API.
Part of #16606
- Use `worker_threads` via `tinypool` lib
- Until now, the Rust side had been operating entirely in a multithreaded manner, while the JS side remained single-threaded, resulting in sequential execution and waiting
- Number of threads are the same value for Rust side
- Also can be applied to embedded formatting path
- Since `tinypool` couldn't be bundled, defined as `dependencies` in the `npm/oxfmt/package.json`
First step for #16530
- Added new napi callback `setupConfig()`
- And call it after resolving config in Rust side
At this time, there is no impact on end users. Passing `{}` is equivalent to passing nothing.
- [x] Implement
- walk.rs: Only collect files that need formatting
- This is needed to ignore binaries like `.png`, etc
- support.rs: Process to determine eligibility based on `path`
- Currently still only JS/TS
- Plan to add more supported files in the next PR
- prettier-proxy.js: Changed the signature for calling Prettier from `file_name` to `parser` specification
- Allows skipping `inferParser`
- [x] Decide how to address problems below
- 👉🏻 Maintain our own target list, same as prettier, biome
- Bonus: specifying parser can skip `inferParser` step in prettier
- For plugins, read config file beforehand in JS...
- 👉🏻 Always ignore unknown files
- It means we don't implement `--ignore-unknown`
- Only supports JS/TS + Prettier default supported + Defined by plugins
<details>
<summary>Problems</summary>
- Let Prettier handle all non-JS/TS files is fine
- But there are still 2 kinds of unsupported files
- Non binary unknown format like `.svelte`, etc
- Binary format like `.png`, etc
- For former: Prettier throws `UndefinedParsererror`
- We need to detect and handle this with `--ignore-unknown` or always ignore
- For latter
- Prettier also throws if `prettier ./src/public/foo.png`
- But NOT for `prettier ./src` 🤯
- Easiest solution is just let handle to prettier but it is really slow
- Because it means: read file, call prettier, check result for every single file in the repo
- Non-binary detection may be good idea, but it still requires reading file
</details>
Also still doesn't affect existing use cases.
- Pass `formatFile()` callback to main `format()` entry
- Refactor `ExternalFormatter` initialization
- Expose `external_formatter.format_file()`
Doesn't affect existing use cases; it just slightly increases the size of the Rust.