Commit Graph

26 Commits

Author SHA1 Message Date
leaysgur a26f856402 refactor(oxfmt): rename external_formatter to external_services (#25429)
`ExternalFormatter` is merely a transport that calls the JS side via napi; it is not referred to as a formatter.
2026-08-09 15:07:40 +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 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 172fc072f3 feat(oxfmt): .js/.ts config file support (#20135)
Fixes #19711, also resolves some of the demands for nested configs.
2026-03-11 04:34:48 +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
Yuji Sugiura 8e3842d0f2 fix(oxfmt): avoid embedded TSFN crash by returning errors as data (take2) (#19806)
Fixes #19713 closes #19742

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-28 11:18:52 +08: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 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 5f4cf309c9 fix(oxfmt): Fix relative -> absolute path resolution with refactoring (#19202)
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
2026-02-10 07:12:19 +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
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
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 7368d6699a refactor(oxfmt): Resolve options on each path, preparing for .editorconfig support (#16991)
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.
2025-12-18 10:44:28 +00:00
leaysgur 8c33ff4778 feat(oxfmt): Expose Node.js API: format(fileName, sourceText, options?) (#16939)
Fixes #15913
2025-12-16 13:43:16 +00:00
leaysgur 5e3ceb838e feat(oxfmt): Support oxfmt --stdin-filepath (#16868)
Fixes #14720
2025-12-15 09:01:26 +00:00
leaysgur bc2e0f8a12 fix(oxfmt): Report exitCode correctly (#16770)
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.
2025-12-12 05:56:10 +00:00
leaysgur 6f3aaba024 perf(oxfmt): Use worker_threads by tinypool for prettier formatting (#16618)
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`
2025-12-09 04:19:29 +00:00
Yuji Sugiura 3184f17782 feat(oxfmt): Pass filepath field to prettier formatting (#16591)
- Pass `filepath` to Prettier

Doing this means that plugins that depend on `filepath` now work.
2025-12-08 17:49:44 +09:00
leaysgur a83a2ec613 feat(oxfmt): Expose setupConfig(configJSON: string) napi callback (#16579)
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.
2025-12-08 08:29:23 +00:00
leaysgur 4767926ea4 feat(oxfmt): Prepare non-js/ts file support with prettier (#16480)
- [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.
2025-12-04 10:03:50 +00:00
leaysgur 0867d2fda8 feat(oxfmt): Set up JS formatFile() function for Rust via napi (#16415)
- 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.
2025-12-04 10:03:50 +00:00
Brooooooklyn 0faa9785e8 fix(oxfmt): JsFormatEmbeddedCb types (#16324) 2025-12-01 07:51:45 +00:00
Yuji Sugiura 1846f86f8b fix(oxfmt): Commit bindings for release (#15349)
This is needed to fix the release CI.


https://github.com/oxc-project/oxc/actions/runs/19103452427/job/54581409298
2025-11-06 09:31:56 +08:00