Fixes#20416 for Oxfmt
- Gate `vite.config.ts` recognition behind `VP_VERSION` env var
- With it, only vite.config.ts is recognized
- Without it, excluded from config candidates (.oxfmtrc.json, oxfmt.config.ts are used)
- Refactor JS config loader into `loadJsConfig` / `loadVitePlusConfig` with shared `importJsConfig` helper
- `loadVitePlusConfig` should be updated to use `resolveConfig` from `vite-plus` soon
- Split tests into `vite_config` (non VP mode) and `vite_plus` (VP mode)
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
Node.js < 25.4.0 has a race condition with ThreadsafeFunction cleanup
during V8 shutdown that causes crashes on large codebases. This adds a
50ms delay before `process.exit()` to allow pending NAPI operations to
complete.
The crash manifests as `Check failed: node->IsInUse()` or `Check failed:
object_ != kGlobalHandleZapValue` in `napi_reference_unref`, occurring
at ~39% rate on large codebases (12,000+ files) without the workaround.
The issue is fixed in Node.js 25.4.0+, so the workaround is only applied
for earlier versions.
See: https://github.com/nodejs/node/issues/55706
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Added Biome-to-oxfmt configuration migration, based on the Prettier
implementation.
I wasn't sure if I should exclude settings that match oxfmt's defaults
from the generated config, but I'm happy to do so if you’d like
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.
After giving it some thought, I decided to lean these features toward JS after all.
The reasons are:
- I want Rust side to focus on complex formatting tasks, while moving infrequently used commands elsewhere
- Code defining `napi` callbacks tends to become verbose
While `--init` itself worked on the Rust side, it will share some lines with upcoming `--migrate`, so I'm extracting this beforehand too.
Also, We were generating `.jsonc`, but I've switched to creating `.json` for now.
This is because running `oxfmt` immediately after generating `.jsonc` causes trailing comma diffs. 😨
I was hesitant to introduce a jsonc writer, so `.json` will enough for now.
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.
- 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.
- Rename `embedded.ts` > `prettier-proxy.ts`
- Add `formatFile(fileName, code)` fn to use later
Doesn't affect existing use cases; it just slightly increases the size of the JS.
- Rename `run.rs` to `main_napi.rs`
- `main.rs` is the entrypoint for Rust CLI, and `main_napi.rs` is for JS CLI
- Fix up `cli.ts` to use `.exitCode`, rather than `.exit()`
## Summary
Adds support for formatting embedded languages (CSS, GraphQL, HTML, Markdown) in JavaScript/TypeScript template literals using Prettier via NAPI bindings.
## Motivation
JavaScript/TypeScript developers often embed CSS, GraphQL queries, HTML, and Markdown in their code using tagged template literals. Without proper formatting support, these embedded languages remain unformatted, leading to inconsistent code style and reduced readability.
This PR enables oxfmt to format these embedded languages using Prettier, providing a seamless formatting experience similar to Prettier's own embedded language support.
## Changes
### Core Functionality
- **Embedded language detection**: Recognizes `css`, `styled`, `gql`, `graphql`, `html`, `md`, and `markdown` template tags
- **Prettier integration**: Uses `@prettier/sync` for synchronous formatting of embedded code
- **NAPI bridge**: Rust-to-JavaScript interop using NAPI ThreadsafeFunction
- **Graceful fallback**: Returns original code if formatting fails or tag is unsupported
### Implementation Details
**Rust side** (`crates/oxc_formatter/`, `apps/oxfmt/src/prettier_plugins/`):
- New `EmbeddedFormatter` trait with callback-based API
- NAPI bindings using `FnArgs` pattern for clean multi-argument calls
- Template formatter enhancement to detect and format tagged templates
- Proper indentation handling: splits Prettier output into lines, uses `hard_line_break()` separators, wraps in `indent()` for context-aware indentation
- Arena allocator pattern to avoid borrow checker conflicts
**JavaScript side** (`apps/oxfmt/src-js/`):
- TypeScript bindings with clean signatures: `formatEmbeddedCode(tagName: string, code: string)`
- Prettier configuration: 80 char width, 2 space indent, semicolons, double quotes
- Tag-to-parser mapping for supported languages
- Error handling with console logging and fallback
**Testing**:
- E2E test suite using Vitest (8 test cases)
- Test fixtures for each supported language
- Snapshot testing with timing normalization for stability
- All tests passing consistently
## Examples
**Before** (unformatted):
```js
const styles = css`.button { color: red; background: blue; }`;
```
**After** (formatted with proper indentation):
```js
const styles = css`
.button {
color: red;
background: blue;
}
`;
```
**Supported tags:**
```js
// CSS
const styles = css`...`;
const component = styled`...`;
// GraphQL
const query = gql`...`;
const schema = graphql`...`;
// HTML
const template = html`...`;
// Markdown
const docs = md`...`;
const readme = markdown`...`;
```
## Testing
```bash
# Run oxfmt tests
just oxfmt
# Watch mode for development
just watch-oxfmt
```
All 8 E2E tests pass:
- ✓ embedded_css
- ✓ embedded_graphql
- ✓ embedded_html
- ✓ embedded_markdown
- ✓ mixed_embedded
- ✓ no_embedded
- ✓ unsupported_tag
- ✓ check_mode
## Related
Related to #13427
## Future Work
Potential enhancements:
- Support for additional embedded languages (SQL, YAML, etc.)
- Custom Prettier configuration via oxfmt config
- Performance optimization for large embedded blocks
- Integration with language servers for inline diagnostics