Skip to content

feat: Add editor SDK support for oxc (oxfmt & oxlint)#7078

Open
slainless wants to merge 12 commits intoyarnpkg:masterfrom
slainless:oxc-support
Open

feat: Add editor SDK support for oxc (oxfmt & oxlint)#7078
slainless wants to merge 12 commits intoyarnpkg:masterfrom
slainless:oxc-support

Conversation

@slainless
Copy link
Copy Markdown

@slainless slainless commented Mar 19, 2026

What's the problem this PR addresses?

Add editor SDK support for oxc. This should enable optional editor integration with both oxfmt and oxlint.

How did you fix it?

Added oxlint, oxfmt and oxlint-tsgolint as supported integration and documented it.

Oxlint and oxlint-tsgolint addition is a bit hacky and I don't know whether this is the right direction or not. Rationale can be read from the comment there.

Loader option added to sdk setupEnv to prevent oxfmt tinypool from being broken due to failing to resolve tinypool/dist/process.js.

I haven't tried this with vim yet since I don't use it so if anyone would kindly help test whether this will work on vim or not, thanks!

oxlint type-aware feature (tsgolint)

The biggest challenge here is the type-aware feature (tsgolint) support. The rust side of oxlint spawn tsgolint a bit different from how the oxlint itself is spawned:

https://github.com/oxc-project/oxc/blob/d3dcf5bc9718ebb4839be27062b5d82da2118e2e/crates/oxc_linter/src/tsgolint.rs#L265-L270

Instead of using js entrypoint, it directly spawn executable so the wrapper approach is not directly applicable here. Luckily, there are some strategies that the oxc_linter uses to resolve tsgolint:

  • via PATH
  • via node_modules search
  • direct path via OXLINT_TSGOLINT_PATH (which is what oxc.path.tsgolint does basically)

The only realistic strategy is to use PATH. For the sdks, it will generate a windows executable shim (cmd) that will basically just spawn tsgolint.js, else should fallback to tsgolint.

Be noted however that this will not fix fundamental issue with typescript-go module resolution, which requires this to be merged first:

This PR just enable the usage of tsgo regardless of whether it can resolve with pnp or not in the type-checking.

Reference

Coc config:
https://github.com/oxc-project/coc-oxc/blob/7a7f0d8f503d5cdb65da37232da6b865f159b42a/src/common.ts#L27-L36

Vscode oxc binary resolution reference:
https://github.com/oxc-project/oxc-vscode/blob/main/client/findBinary.ts

Oxlint-tsgolint executable resolution:
https://github.com/oxc-project/oxc/blob/d3dcf5bc9718ebb4839be27062b5d82da2118e2e/crates/oxc_linter/src/tsgolint.rs#L1164-L1225

Checklist

  • I have read the Contributing Guide.
  • I have set the packages that need to be released for my changes to be effective.
  • I will check that all automated PR checks pass before the PR gets reviewed.

Comment on lines +312 to +320
// We are using pass-through here since we don't really need to change the default behavior of the wrapper.
// Since the oxlint wrapper is the one that spawns the actual oxlint binary, we extend the PATH here
// to enable the tsgolint PATH resolution strategy in the next tsgolint wrapper.
const oxlintMonkeyPatch = `
module => module;

process.env.PATH += \`;\${resolve(__dirname, '../../oxlint-tsgolint/bin')}\`;
import(\`${ppath.join(npath.toPortablePath(relPath), `dist/cli.js`)}\`);
`;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I think oxlint not exporting bin/oxlint or even package.json is a problem that should be fixed upstream.
  2. Unfortunately, we cannot fully support ESM SDKs until import.meta.resolve(specifier, parent) is unflagged. But for now I think a stopgap similar to this is acceptable

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the import is still necessary even if we change the exports setup and the entrypoint back to bin/oxlint. The top-level await is the blocker :/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With proper exports from oxlint we can use require.resolve to get the package directory instead of needing to go through the PnP API and/or to read oxlint/package.json to get the correct bin path instead of hardcoding it.

The top-level await is the blocker

The more proper fix would be to actually support generating ESM SDK files, but to do so we need import.meta.resolve(specifier, parent) as a replacement for createRequire.

Short of that, this stopgap is fine for me for the time being like I said.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With proper exports from oxlint we can use require.resolve to get the package directory instead of needing to go through the PnP API and/or to read oxlint/package.json to get the correct bin path instead of hardcoding it.

Ah, I understand now, thanks! I haven't though that far. It does more natural to resolve via the created require, yeah. I will try to notify upstream package.

@slainless
Copy link
Copy Markdown
Author

slainless commented Mar 19, 2026

Is adding --loader/--experimental-loader flag to setupEnv acceptable? After testing for a bit, looks like this PR can be free from tinylibs/tinypool#129 by adding the esm loader flag :/

However, I don't know the impact of this if applied alongside the setupEnv flag without its own dedicated option flag. I assume it is ok since the default runner inject both --require and --experimental-loader by default?

@slainless slainless requested a review from clemyan March 24, 2026 02:29
const wrapper = new Wrapper(`oxfmt` as PortablePath, {pnpApi, target});

await wrapper.writeDefaults();
await wrapper.writeFile(`bin/oxfmt` as PortablePath, {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed this the first time around, but binary files should use writeBinary to set the executable flag. Similarly for the other 2 wrappers

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, it should be fixed now

@clemyan
Copy link
Copy Markdown
Member

clemyan commented Mar 26, 2026

Is adding --loader/--experimental-loader flag to setupEnv acceptable? After testing for a bit, looks like this PR can be free from tinylibs/tinypool#129 by adding the esm loader flag :/

I feel like we should have done that already, but just never needed to so nobody did.

However, I don't know the impact of this if applied alongside the setupEnv flag without its own dedicated option flag. I assume it is ok since the default runner inject both --require and --experimental-loader by default?

Looks like currently nothing else uses the setupEnv flag. So that shouldn't break anything.


No developer likes hackiness, but I think this is as far as we can get. So it's fine by me if it does solve a blocker for some people.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants