Prettier uses the `conditionalGroup` IR for JS and YAML.
When this appeared in JS(containing `conditionalGroup`)-in-HTML(`script` tags)-in-JS (`oxc_formatter`), we previously bypassed the `Doc` to IR conversion.
This PR adds support for this conversion and removes the string format fallback path.
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`.
Follow-up to #25063.
tsx-in-vue was supported in #25063 with a parse-retry (ts fails → retry as tsx), chosen to keep the common ts path single-parse.
However, this leaves a hole: a `lang="tsx"` block without JSX parses fine as plain ts, so the retry never fires and the lone generic comma is removed:
```vue
<script setup lang="tsx">
const identity = <T,>(x: T) => x; // → <T>(x: T) => x, breaks as TS17008
</script>
```
Since parse failure fundamentally cannot detect this, the only fix is to scan the `lang` declaration itself.
The JS host now scans the SFC once per file and passes `_oxfmtVueScriptLang: "tsx"` down to `textToDoc()`. This makes the retry obsolete, so it is removed.
This PR also settles the comma policy: keep it only where it is load-bearing.
- JSX grammar (`<T>` would be a JSX opener)
- and `.mts/.cts` (TS7060, #18928)
and remove it everywhere else, keyed on the grammar, regardless of context.
Deliberate divergence: Prettier keys on filepath and keeps the comma in ts-in-vue/svelte (while its own ts-in-md embed uses `dummy.ts` as `filepath` and removes it). Plain-TS sources now behave identically wherever they live.
Part of #16608, closes#25046.
---
Even with a simple regexp, I don't want to impact the performance of the vast majority of ts-in-vue paths.
With this approach, only tsx-in-vue use cases will be affected, and even that will be kept to a minimum since it can be handled entirely on the Rust side. (JS <-> Rust roundtrip is also slow)
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.
Fixes#22350
Currently, the js-in-vue process involves formatting the code on the Rust side using `oxc_formatter`, converting it into an IR, transforming that into a Prettier `Doc`, serializing it into JSON, and then sending it to the JS side, where it is finally passed to Prettier.
Some types of `Docs` include multiple variations that are selected based on the `printWidth`, and all of these variations were being included as part of the JSON payload.
As a result, deep nesting caused the payload size to grow exponentially, leading to hangs.
This PR resolves the issue by sending these variations separately as shared references.
In Oxfmt, there is only one config file, but there were multiple conversion paths to the specific options used by subsequent formatters.
For example, it would generate `FormatOptions` for `oxc_formatter`, while assembling the necessary `JSON` for `prettier`.
There was a lack of consistency here, as some paths referenced the parsed `FormatConfig` while others referenced the raw JSON of `Oxfmtrc`. This inconsistency led to issues like the one mentioned in #21919.
In this PR, I have established `FormatConfig` as the single SoT, ensuring that all options are derived from it.
Part of #15180, closes#18891
Angular-in-JS consists of 2 cases: `template` and `styles`.
- template: Similar to html-in-js, use the `parser: angular`
- styles: Same as css-in-js (already supported)
Part of #15180
The basic flow is the same as css-in-js: replace `${expr}` with placeholder strings, let Prettier format the HTML, then substitute back with formatted `expr`s.
However, there are a few key differences:
### `options.htmlWhitespaceSensitivity`
This HTML-specific option also affects html-in-js (template wrapping strategy, hug detection), so it needed to be held in `oxc_formatter::FormatOptions`.
### `prettier.__internal.formatOptionsHiddenDefaults`
Prettier adds hidden fields to the `options` object during its internal print phase for embedded formatters.
However, `__debug.printToDoc()` runs `normalizeFormatOptions()` which strips unknown keys. Unlike internal `textToDoc()` (which uses `passThrough: true`), `printToDoc()` does not preserve them.
To work around this, we pre-register keys in `formatOptionsHiddenDefaults` so they survive normalization.
### NOTE: Not fully supported cases
#### js-in-html-in-js
Currently, the process of converting Docs to our IR does not support `conditionalGroup`.
Since Prettier only uses this for YAML and JS formatting.
YAML is never embedded, and we would like to use `oxc_formatter` for JS.
However, applying `oxc_formatter` to the **js**-in-html-in-js case was not straightforward. Therefore, I have implemented a fallback to a simpler formatting process for that case for now.
#### `label({ embed, hug })` equivalent
With current implementation, `is_huggable_html_embed_single_arg()` always hugs via early-return, ignoring line width.
Prettier uses `label({ embed, hug })` + `conditionalGroup` to try hugged first and fall back to expanded.
We have the equivalent `BestFitting` 3-variant flow in `write_grouped_arguments`, but `ExpandParent` from HTML template's `hardline` propagates through `will_break` / `propagate_expands` / fit check, forcing the expanded variant to always be chosen.
To fix, we need to add new IR for boundary (prevents `ExpandParent` propagation in `will_break` / `propagate_expands` / fit check), then add HTML embeds to `can_group_expression_argument` so they enter the existing `BestFitting` 3-variant flow.
However, I'm not sure this is worth to add. So just leave it for now.
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
- `experimentalSortImports` 👉🏻 `sortImports`
- `experimentalSortPackageJson` 👉🏻 `sortPackageJson`
- `experimentalTailwindcss` 👉🏻 `sortTailwindcss`
To avoid breaking changes, the old name is retained as an alias.
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.
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
### Resolve issues:
* close: https://github.com/oxc-project/oxc/issues/18072
The new version of `prettier-plugin-tailwindcss` solved all root causes mentioned in the description of the above issue, see the commit history at https://github.com/tailwindlabs/prettier-plugin-tailwindcss/commits/main/
* close: https://github.com/oxc-project/oxc/issues/17972
* close: https://github.com/oxc-project/oxc/issues/18459
* close: https://github.com/oxc-project/oxc/issues/18628
These three issues are related to resolving the TailwindCSS configuration of that plugin, the new version decoupled with Prettier, and provides a public `createSorter` sorting API to `/sorter`, so that we can configure `base` correctly, which is the key for the plugin to resolve `stylesheet` and `config` from `base`.
### Key change on Oxfmt side
In this PR, the `stylesheets` and `config` options are normalized to absolute paths using the Oxfmt configuration directory before calling `createSorter` and delegating to `Prettier`.
The reason is as follows:
For formatting non-JS/TS files, Oxfmt delegates to Prettier. Therefore, we should continue using `prettier-plugin-tailwindcss` to handle this, ensuring that Prettier's configuration resolution remains intact. I made a change in [that plugin](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/blob/125a8bc77639529a5a0c7e4e8a02174d7ed2d70b/src/config.ts#L50-L54) to prevent resolution when both `stylesheet` and `config` are absolute paths. This change is valid and could enhance the plugin's performance for both Oxfmt and Prettier users. The plugin needs to resolve the Prettier configuration to obtain the directory of the configuration file and join it with `stylesheet` and `config`. If they are already absolute, there is no need to join them.
### Benchmark
> Copy from https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/438
Note: `new` means the current branch, `old` means the latest package in npm, so it includes a few optimizations that have already been merged into the main branch, not all in this branch.
Benchmark repo: https://github.com/dubinc/dub
```shell
Benchmark 3: oxfmt-new
Time (mean ± σ): 999.8 ms ± 32.1 ms [User: 8968.9 ms, System: 2246.7 ms]
Range (min … max): 977.2 ms … 1022.5 ms 2 runs
Benchmark 4: oxfmt-old
Time (mean ± σ): 1.628 s ± 0.027 s [User: 13.239 s, System: 4.980 s]
Range (min … max): 1.609 s … 1.647 s 2 runs
Summary
oxfmt-new ran
1.63 ± 0.06 times faster than oxfmt-old
```
Benchmark repo: https://github.com/tailwindcsslab/headlessui
```shell
Benchmark 3: oxfmt-new
Time (mean ± σ): 778.8 ms ± 28.9 ms [User: 7633.6 ms, System: 912.9 ms]
Range (min … max): 738.3 ms … 806.9 ms 4 runs
Benchmark 4: oxfmt-old
Time (mean ± σ): 1.033 s ± 0.034 s [User: 10.048 s, System: 1.454 s]
Range (min … max): 0.994 s … 1.073 s 4 runs
Summary
oxfmt-new ran
1.33 ± 0.07 times faster than oxfmt-old
```
Follow up on #18373
Aims to reduce:
- Rust -> JS payload size
- Logic itself in JS side
by handling all options in Rust side.
Before:
- Plugin options handled globally during config caching
- All files received full JSON
- JS side filtered by `parser` name to decide plugin loading
After:
- Plugin options added per-file in `finalize_external_options()` if needed
- Only relevant files receive plugin flags based on `FormatFileStrategy`
- JS side just checks flag existence (`parser` knowledge not needed)
Fixes#16608 partially.
This PR overrides Prettier's default JS/TS formatting process with a custom transform-plugin.
As a result, `oxc_formatter` will be used in the `script` section of Vue, code block of Markdown, etc. And also for example, sort-imports and other features are now available.
After verification, we found that if we override `babel`, it is also applied to some expression parts of Vue.
This was unexpected and more time is needed to do things properly.
Therefore, as a first support, only enables for `markdown` and `mdx`.
Changes:
- Changed `tinypool` mode from `worker_threads` to `child_process`
- Calling NAPI (API) again via `worker_threads` from NAPI (CLI) causes a hang...
- Implemented `prettier-plugin-oxfmt` using the existing `format()` API
- What's truly desired is something equivalent to `textToDoc()`, but this is the only option currently we have
- Handling of options corresponding to Prettier's `embed()` processing flow
- Plugins only support primitive types
- NAPI also cannot handle options that cannot be serialized to JSON
Following PRs will cover:
- Improvements to option handling
- Tests
Generally, languages that support Tailwind classes are limited.
For example, in a project using only TypeScript and JSON, there's no point in loading this plugin on paths that handle JSON.
Follow up on #17648
Move the `experimentalTailwindcss` to `tailwindXxx` option mapping logic from JS to Rust side (`populate_prettier_config`).
Now, consolidate all Prettier option processing in one place (`populate_prettier_config`).
This may also slightly improve performance by reducing JS runtime overhead a bit.
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
Fixes#17948 and may fix `next.js` repo diffs. => Confirmed all xxx-in-js diffs are resolved ✨
---
When Prettier fails to format embedded code (e.g., valid `css` tag but with invalid content), the previous implementation returned the original code as a successful result.
This caused issues because `block_indent` is always added (assuming Prettier has normalized root indentation).
As a result, indentation/newlines grow infinitely on each format run.
This PR adds support for migrating `prettier-plugin-tailwindcss` configuration when running `oxfmt --migrate prettier`.
### Example
Prettier config (input):
```json
{
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindConfig": "./tailwind.config.js",
"tailwindFunctions": ["clsx", "cn"],
"tailwindAttributes": ["myClass"],
"tailwindPreserveWhitespace": true
}
```
Oxfmt config (output):
```json
{
"printWidth": 80,
"experimentalTailwindcss": {
"config": "./tailwind.config.js",
"functions": ["clsx", "cn"],
"attributes": ["myClass"],
"preserveWhitespace": true
},
"ignorePatterns": []
}
```
### Notes
- If the plugin is listed but no Tailwind options are specified, `experimentalTailwindcss` will be set to an empty object `{}` to enable the feature
- Tailwind options without the plugin in the plugins array will NOT be migrated (they are skipped)
Add Tailwind CSS class sorting support for non-JS/TS files (HTML, Vue, Svelte, etc.) by enabling prettier-plugin-tailwindcss when experimentalTailwindcss is set. This PR also refactors the Tailwind CSS integration code to share plugin caching and option mapping logic between JS and non-JS file formatting paths.
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)