Commit Graph

44 Commits

Author SHA1 Message Date
Boshen 1193c9dbaf chore(oxfmt): dogfood operator position at line start (#25937)
dogfood
2026-08-20 14:36:47 +00:00
leaysgur c07fe7c217 feat(oxfmt): support experimentalOperatorPosition (#25643)
Fixes #16366

---

Option name requires `experimental` prefix for now.

Values are:

- `"start"`
- `"end"`(default)

This is also available on playground.
2026-08-14 08:12:16 +00:00
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 c9d1a5b603 perf(oxfmt): spawn tinypool lazily (#25298)
Partially fixes #25196

- Before: create `child_process` worker pool on startup
  - Spawned even when nothing needs JS side delegation (e.g. JS/TS-only projects)
  - For LSP, the idle workers stayed resident for the whole session
- After: create the pool on the first format that actually delegates to JS side
  - Runs that never reach JS side spawn zero workers
  - As more formatters are rewritten in Rust, the share of such runs keeps growing

Trade-off: the first JS-delegated format in an LSP session now pays the spawn cost.
But that is what every one-shot CLI run already pays, so it is far from slow, and subsequent formats are unchanged.
2026-08-05 04:25:24 +00:00
leaysgur 2121a552ba fix(oxfmt): reuse tinypool process during the same LSP process (#24197)
Fixes #24147, closes #24183.
2026-07-06 00:55:31 +00:00
leaysgur ea5d09534d fix(oxfmt): update --migrate prettier (#23963)
A bit outdated, updated it to reflect the latest situation.

- Support `svelte` migration
- Better (but not yet complete) `overrides` detection
- Remove warning about `embeddedLanguageFormatting` (already supported)
- etc
2026-06-30 00:59:07 +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 1ccaf767d6 refactor(oxlint,oxfmt): Fix up apps/shared usage (#22457)
Final clean up.

- Share `isObject()` function
- Clean up directory structure
- Use `Error.cause` for TS hint error
2026-05-20 08:21:39 +00:00
leaysgur 1a752eab42 refactor(oxfmt): Use shared/loadViteConfigField (#22455)
Move `loadViteConfigField()` from Oxfmt to shared.
2026-05-20 08:21:38 +00:00
leaysgur 16b8058cd6 feat(oxfmt): Support vite-plus/resolveConfig for vite.config.ts (#22454)
Closes #21603, fixes #21418

To simplify the review process, I'll first add support to Oxfmt.

In the next PR, I'll move that implementation to `shared`.
2026-05-20 08:21:37 +00:00
leaysgur dcc6d38330 refactor(oxlint,oxfmt): Use shared/importJsConfig for js config (#22453)
Consolidate the following parts:

- Using native `import()`s
- Specifying `cacheKey`
- Extracting default exports
- Returning hints for TS incompatibility errors
2026-05-18 08:05:48 +00:00
leaysgur 37d49fcd40 refactor(oxlint,oxfmt): Share src-js code via apps/shared (#22452)
New place to share code for use in `src-js`, similar to how `crates/oxc_config` works.

Initially, I considered creating an `apps/oxc_cli` and moving `crates/oxc_config` there. However, it felt unnatural deps for `crates/oxc_linter` -> `apps/oxc_cli`, so I decided to keep the Rust code as is for now and share only the JS code.

Since `apps/shared` exports `.ts` files directly, each app is responsible for bundling them via `@oxapps/shared`. Tests still live in `apps/shared`, so separate CI is also added and hooked by each app.

To start, I've only shared the function that reports TS incompatibility errors. This is just for verification purposes and will be fixed in a later PR.
2026-05-18 07:56:26 +00:00
leaysgur f6114d3f48 refactor(oxfmt): Drop unused resolvePlugins stub (#22518) 2026-05-18 06:31:22 +00:00
leaysgur e3081e12a9 feat(oxfmt): Gate vite.config.ts recognition behind VP_VERSION env var (#21295)
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)
2026-04-13 01:10:46 +00:00
leaysgur 7ac69fa3f7 refactor(oxfmt): Arrange cli/js_config dir (#21078)
- Add `/js_config/` dir
- Use inline tests
- Always return `Error` instance
2026-04-06 02:27:59 +00:00
camc314 35cf6e87c0 feat(oxfmt): add node version hint for ts config import failures (#21046)
Refs:  #21032
2026-04-06 02:27:58 +00:00
leaysgur 89ce30b641 fix(oxlint,oxfmt): Redirect JS stdout to stderr for LSP (#20321)
Fixes #20320

(It's fine to release this as scheduled one)
2026-03-16 10:15:23 +00:00
leaysgur 68fb0d0f6c fix(oxfmt): Skip vite.config.ts which fails to import (#20326)
If oxfmt.config.ts fails to load, it is the user's responsibility, so reporting it as an error is correct.
However, this does not apply to vite.config.ts, so we ignore it.

Also refactored to report error message from JS.
2026-03-13 14:24:37 +09:00
leaysgur 4ea67decf5 fix(oxlint,oxfmt): Skip vite.config.ts exports defineConfig(fn) (#20260)
`vite.config.ts` may return function from `defineConfig(({ mode }) => ({ ... }))`, but we can't evaluate.

Just skip it like no `.lint|fmt` config.
2026-03-12 04:56:57 +00:00
leaysgur fdaca97be4 refactor(oxfmt): extract vite fmt config in JS loader (#20257)
Closes #20243

The same as #20256, but for Oxfmt.
2026-03-12 04:14:27 +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 a447840432 refactor(oxfmt): Apply rethrow error for all pool.run() calls (#19700)
Since everything is called via `child_process`, let's standardize it.
(Currently, even if the caller doesn't look at it at all.)
2026-02-26 05:18:58 +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 bba146a4e3 refactor(oxfmt): Prettify external_formatter errors (#19565)
Fixes #19527

### Before

```
Failed to format file with external formatter: /u/p/c/oxc/apps/oxfmt/a.yml
JS formatFile promise rejected: GenericFailure, [object Object]
```

### After

```
SyntaxError: Nested mappings are not allowed in compact mappings (1:6)
[a.yml]
> 1 | key: value: nested
    |      ^^^^^^^^^^^^^
> 2 |
    | ^
```
2026-02-20 07:48:35 +00:00
Long Ho f084ea66df fix(oxfmt): Explicitly pass process.env for the forked process (#19380)
# Summary
- Explicitly pass process.env to Tinypool's env option when spawning
child processes in the external formatter worker pool
- Filters out undefined values from process.env to satisfy the
Record<string, string> type requirement

# Context
When Tinypool uses the child_process runtime, child processes may not
always reliably inherit the parent's environment variables. This
explicitly forwards the environment to ensure child processes have
access to the same environment (e.g., PATH, NODE_ENV, etc.).

---------

Signed-off-by: Yuji Sugiura <6259812+leaysgur@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Yuji Sugiura <6259812+leaysgur@users.noreply.github.com>
2026-02-14 22:52:03 +09: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 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 e754b18dca fix(oxfmt/migrate-prettier): Set experimentalSortPackagejson: false by default (#18831)
Fixes #18741
2026-02-02 01:52:22 +00:00
leaysgur f38986e8a2 refactor(oxfmt): Ensure to destroy worker pool before exit (#18689)
In most cases, it should be fine, but this approach is cleaner.
2026-01-29 08:46:03 +00:00
leaysgur 77b23c9d18 refactor(oxfmt/migrate-biome): Borrow tiny-jsonc implementation (#18684)
Follow up on #18638

Replace JSONC parse logic with `tiny-jsonc`.
2026-01-29 08:18:40 +00:00
Luca Fischer ee30de98cf feat(oxfmt): add config migration from biome (#18638)
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
2026-01-28 21:09:13 +09: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 9a5e3e2b20 chore(oxfmt): Add comments for migrate overrides (#18216)
Please refer to #18215 for details.
2026-01-19 10:46:25 +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 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 8fd4ea905a feat(oxfmt): options.embeddedLanguageFormatting is now "auto" by default (#17649)
Fixes #16763
2026-01-05 05:15:17 +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