Commit Graph

41 Commits

Author SHA1 Message Date
leaysgur 1a83a8ddb2 fix(oxfmt): mirror printer line suppression and restore dedent-to-root (#25573)
Fixes #25568, #25569
2026-08-13 03:50:19 +00:00
leaysgur 033ddf6d38 refactor(oxfmt,formatter): simplify js-in-html-in-js handling (#25464)
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.
2026-08-10 06:02:17 +00:00
leaysgur 8213e72f37 refactor(formatter,oxfmt): flatten embedded dispatch contract (#25391)
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`.
2026-08-09 14:32:11 +00:00
leaysgur a9e76e9e1f fix(oxfmt): rework tsx-in-vue support (#25106)
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.
2026-07-30 05:27:07 +00:00
leaysgur 1b57f783fb feat(oxfmt): support tsx-in-vue (#25063)
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)
2026-07-29 07:27:43 +00:00
leaysgur 2aedd52ce6 fix(oxfmt): avoid JS promise rejects for all TSFN call sites (#23107)
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.
2026-06-08 02:54:09 +00:00
leaysgur f6114d3f48 refactor(oxfmt): Drop unused resolvePlugins stub (#22518) 2026-05-18 06:31:22 +00:00
leaysgur 4dd83ddaa0 fix(oxfmt): Send expandedStates variants as shared refs (#22366)
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.
2026-05-13 01:55:13 +00:00
leaysgur 6e8e8185c0 feat(oxfmt): Experimental .svelte support (#21700)
Part of #19715

Note that:

> Users are required to perform 2 steps below
> 1️⃣ Install svelte/compiler@^5.0.0 and/or @astrojs/compiler@^2.9.1 manually
> 2️⃣ Enable option in config
2026-05-11 01:24:37 +00:00
leaysgur 7faba51ae4 refactor(oxfmt): Use FormatConfig as SoT (#21981)
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.
2026-05-01 01:52:26 +00:00
leaysgur 3f370d6999 refactor(oxfmt): Intro loadCached JS helper (#21696)
Prepare for upcoming svelte+astro plugins.
2026-04-24 06:20:37 +00:00
leaysgur 519e0b8191 refactor(oxfmt): Use explicit CACHES variable (#20852)
Instead of defining `xxxCache` variables individually, combine them into a single variable `CACHES`.
2026-03-30 01:16:04 +00:00
leaysgur c1a90f789d refactor(oxfmt): Avoid useless await when module cache hit (#20786)
Rewrite this pattern:

```ts
// Always
const plugin = await loadPlugin();

// ---

async function loadPlugin() {
  if (cache) return cache;
  cache = await import();
}
```

to:

```ts
const plugin = cache ?? (await loadPlugin());

// ---

async function loadPlugin() {
  cache = await import();
}
```

This will avoid useless `await` (`Promise` creation and microtask) when cache hit, most cases.

Also introduce cache for tailwind sorter.
2026-03-30 01:16:03 +00:00
leaysgur b02fe6e778 refactor(formatter,oxfmt): Fix up xxx-in-js logic (#20722)
JSDoc formatting also uses the same logic as xxx-in-js.

So, adjustments have been made to accommodate this.
2026-03-25 05:42:28 +00:00
leaysgur 23050faba6 feat(oxfmt): Support markdown-in-js substitution (#20683)
Part of #15180, and the last piece...!
2026-03-24 13:13:56 +00:00
leaysgur 4087295040 feat(oxfmt): Support angular-in-js substitution (#20676)
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)
2026-03-24 13:13:56 +00:00
leaysgur c21c5a7e72 feat(oxfmt): Support html-in-js substitution (#20193)
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.
2026-03-23 07:30:16 +00:00
leaysgur 5e98677d14 chore(oxfmt): Align the definition order of external APIs (#19896)
Cosmetic changes; Reorder napi callbacks:

- init+dispose
- format file (for non-js)
- embedded (for js)
- sort tailwind (for js)
2026-03-02 01:40:51 +00:00
leaysgur ca68ea6192 feat(oxfmt): Support gql-in-js substitution (#19670)
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
2026-02-26 15:24:01 +00:00
leaysgur 035933c443 feat(formatter,oxfmt): Support js-in-vue (partially) (#19514)
### 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
2026-02-24 10:17:35 +00:00
leaysgur 984dc07f4f feat(oxfmt): Strip "experimental"SortXxx prefix (#19567)
- `experimentalSortImports` 👉🏻 `sortImports`
- `experimentalSortPackageJson` 👉🏻 `sortPackageJson`
- `experimentalTailwindcss` 👉🏻 `sortTailwindcss`

To avoid breaking changes, the old name is retained as an alias.
2026-02-20 08:56:22 +00:00
leaysgur c4a3cb5fad refactor(oxfmt): Move textToDoc() implementatin to Rust (#19307)
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.
2026-02-12 09:05:56 +00:00
leaysgur db8355b80c refactor(oxfmt): Fix all options.filepath usage (#19200)
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
2026-02-10 07:12:18 +00:00
leaysgur d1e3537fb2 refactor(oxfmt): Insert options.(parser|filepath) on Rust side (#19199)
- Reduce work load on JS
- Also simplify `wrap_xxx()` in Rust
2026-02-10 07:12:18 +00:00
Dunqing 23c0753951 feat(oxfmt): better Tailwind CSS intergration (#19000)
### 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
```
2026-02-09 11:43:40 +00:00
leaysgur 5ea5bda86c fix(oxfmt): Handle isSingleJsxExpressionStatementInMarkdown() check for js-in-md (#19042) 2026-02-09 08:59:37 +00:00
leaysgur 5b53e83b79 refactor(oxfmt): Split oxfmt-plugin files (#19031)
Extract main `textToDoc()` implementation.
2026-02-06 07:53:00 +00:00
leaysgur f39c96cb78 fix(oxfmt): Do not override babel-ts for now (#19030)
Just for sure.

There is no specific reason to do this until we override `__ts_expression`.
2026-02-06 07:53:00 +00:00
leaysgur eb8a91c198 refactor(oxfmt): Handle all options in Rust to reduce JSON payload size (#18874)
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)
2026-02-06 05:50:05 +00:00
leaysgur 6ee2d59cd6 feat(oxfmt): Use oxc_formatter in js-in-xxx part (#18373)
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
2026-02-06 05:50:04 +00:00
leaysgur b2df8fb065 perf(oxfmt): Enable tailwind plugin only for relevant parser (#18418)
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.
2026-01-23 02:53:00 +00:00
leaysgur d71c15d02a feat(oxfmt): Enable tailwind sort inside xxx-in-js (#18417)
Fixes #18416
2026-01-23 02:53:00 +00:00
leaysgur d96ca05c81 refactor(oxfmt): Move tailwind option mapping to Rust (#18281)
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.
2026-01-20 08:58:36 +00:00
leaysgur e0e925c3ab refactor(formatter,oxfmt): Move tag-to-parser logic from oxc_formatter to oxfmt (#18277)
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
2026-01-20 08:58:35 +00:00
leaysgur efacb138be fix(oxfmt): Do not wrap with block_indent() if format_embedded fails (#17975)
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.
2026-01-14 05:10:28 +00:00
leaysgur 1736af586f refactor(oxfmt): Update loadXxx() func usage (#17783)
Follow up #17782.

Since the bundled dependencies are critical, do not ignore import failures silently. Instead, throw an error.
2026-01-08 05:47:13 +00:00
Dunqing a680d99a38 feat(formatter): support migration of prettier-plugin-tailwindcss in --migrate:prettier (#17652)
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)
2026-01-05 12:10:06 +00:00
Dunqing c20ef0c075 feat(formatter): add TailwindCSS support for non-JS/TS files (#17648)
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.
2026-01-05 11:35:37 +00:00
Dunqing 26ed46bc02 feat(formatter): add TailwindCSS support for JS/TS files (#16990)
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)
2026-01-05 10:07:07 +00:00
leaysgur 0c3e743a3e refactor(oxfmt): Restore accidentally removed TS types (#17098)
If we pass the wrong argument, the test will just fail, but well, type safety is a good.
2025-12-19 04:32:28 +00:00
leaysgur b4e8605a4d refactor(oxfmt): Clean up src-js files and directories (#17040)
```
.
├── bindings.d.ts
├── bindings.js
├── cli
│   ├── migration
│   │   ├── init.ts
│   │   ├── migrate-prettier.ts
│   │   └── shared.ts
│   └── worker-proxy.ts
├── cli-worker.ts    # CLI worker endpoint
├── cli.ts           # CLI endpoint
├── index.ts         # API endpoint
└── libs
    └── prettier.ts  # Core logic shared by index, cli-worker, cli/worker-proxy
```

By doing this, Node.js API `format()` does not use `worker_threads` anymore.
2025-12-18 10:44:28 +00:00