Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .yarn/versions/92c64300.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
releases:
"@yarnpkg/sdks": minor
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ The `yarn dlx @yarnpkg/sdks` command will look at the content of your *root* `pa
| [vscode-eslint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) | [eslint](https://yarnpkg.com/package/eslint) |
| [prettier-vscode](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) | [prettier](https://yarnpkg.com/package/prettier) |
| [relay](https://marketplace.visualstudio.com/items?itemName=meta.relay) | [relay](https://relay.dev/)
| [oxc](https://marketplace.visualstudio.com/items?itemName=oxc.oxc-vscode) | [oxlint](https://yarnpkg.com/package/oxlint)
| [oxc](https://marketplace.visualstudio.com/items?itemName=oxc.oxc-vscode) | [oxfmt](https://yarnpkg.com/package/oxfmt)

If you'd like to contribute more, [take a look here!](https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-sdks/sources/generateSdk.ts)

Expand Down
20 changes: 19 additions & 1 deletion packages/yarnpkg-sdks/sources/generateSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,10 @@ export type SupportedSdk =
| `typescript-language-server`
| `typescript`
| `svelte-language-server`
| `flow-bin`;
| `flow-bin`
| `oxfmt`
| `oxlint`
| `oxlint-tsgolint`;

export type BaseSdks = Array<[
SupportedSdk,
Expand Down Expand Up @@ -343,6 +346,21 @@ export class Wrapper {
return absWrapperPath;
}

async writeRaw(relPackagePath: PortablePath, content: string, options: {mode?: number} = {}) {
const topLevelInformation = this.pnpApi.getPackageInformation(this.pnpApi.topLevel)!;
const projectRoot = npath.toPortablePath(topLevelInformation.packageLocation);

const absPath = ppath.join(this.target, this.name, relPackagePath);
const relProjectPath = ppath.relative(projectRoot, absPath);

await xfs.mkdirPromise(ppath.dirname(absPath), {recursive: true});
await xfs.writeFilePromise(absPath, content, {mode: options.mode});

this.paths.set(relPackagePath, relProjectPath);

return absPath;
}

getProjectPathTo(relPackagePath: PortablePath) {
const relProjectPath = this.paths.get(relPackagePath);

Expand Down
68 changes: 67 additions & 1 deletion packages/yarnpkg-sdks/sources/sdks/base.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {PortablePath} from '@yarnpkg/fslib';
import {npath, PortablePath, ppath} from '@yarnpkg/fslib';
import {PnpApi} from '@yarnpkg/pnp';

import {Wrapper, GenerateBaseWrapper, BaseSdks} from '../generateSdk';
Expand Down Expand Up @@ -285,6 +285,69 @@ export const generateFlowBinBaseWrapper: GenerateBaseWrapper = async (pnpApi: Pn
return wrapper;
};

export const generateOxfmtBaseWrapper: GenerateBaseWrapper = async (pnpApi: PnpApi, target: PortablePath) => {
const wrapper = new Wrapper(`oxfmt` as PortablePath, {pnpApi, target});

await wrapper.writeDefaults();

return wrapper;
};

export const generateOxlintBaseWrapper: GenerateBaseWrapper = async (pnpApi: PnpApi, target: PortablePath) => {
const wrapper = new Wrapper(`oxlint` as PortablePath, {pnpApi, target});
await wrapper.writeDefaults();

// The following workaround is necessary because:
// 1. oxlint uses top-level await, which prevents the use of `require`.
// 2. Neither dist/cli.js nor bin/oxlint are exposed as exports in the package.json.
const topLevelInformation = pnpApi.getPackageInformation(pnpApi.topLevel)!;
const dependencyReference = topLevelInformation.packageDependencies.get(`oxlint`)!;
const pkgInformation = pnpApi.getPackageInformation(pnpApi.getLocator(`oxlint`, dependencyReference))!;
const absPath = pkgInformation.packageLocation;
const binPath = npath.join(npath.fromPortablePath(
wrapper.getProjectPathTo(`bin/oxlint` as PortablePath),
));
const relPath = npath.relative(npath.dirname(binPath), absPath);

// 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`)}\`);
Comment thread
slainless marked this conversation as resolved.
Outdated
`;
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.

Copy link
Copy Markdown
Author

@slainless slainless Apr 6, 2026

Choose a reason for hiding this comment

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

This should be resolved by 1d28b26.
Though, it is resolved via package.json but I hope that will be sufficient.

My only concern left is the resolution for oxc-tsgolint. At the moment, it doesn't have any export restrictions so I just left it as is.

What do you think @clemyan ?


// This intentionally produces a dummy export; the main logic is in the import above.
// Originally, the binary does not export anything.
await wrapper.writeFile(`bin/oxlint` as PortablePath, {
requirePath: `` as PortablePath,
wrapModule: oxlintMonkeyPatch,
});

return wrapper;
};

export const generateOxlintTsgolintBaseWrapper: GenerateBaseWrapper = async (pnpApi: PnpApi, target: PortablePath) => {
const wrapper = new Wrapper(`oxlint-tsgolint` as PortablePath, {pnpApi, target});

// We are using the oxc_linter tsgolint resolution mechanism via the PATH environment variable
// since it's the only realistic approach to correctly resolve the tsgolint binary when using Yarn PnP.
// Ref: https://github.com/oxc-project/oxc/blob/d3dcf5bc9718ebb4839be27062b5d82da2118e2e/crates/oxc_linter/src/tsgolint.rs#L1164-L1225
// With this approach, we need to add a Windows executable shim for tsgolint.js.
const tsgolintCmd = `
@echo off
node "%~dp0tsgolint.js" %*
`.trim().replace(/^ {4}/gm, ``);
Comment thread
slainless marked this conversation as resolved.

await wrapper.writeDefaults();
await wrapper.writeFile(`bin/tsgolint` as PortablePath);
await wrapper.writeRaw(`bin/tsgolint.cmd` as PortablePath, tsgolintCmd);

return wrapper;
};

export const BASE_SDKS: BaseSdks = [
[`@astrojs/language-server`, generateAstroLanguageServerBaseWrapper],
[`eslint`, generateEslintBaseWrapper],
Expand All @@ -294,4 +357,7 @@ export const BASE_SDKS: BaseSdks = [
[`typescript`, generateTypescriptBaseWrapper],
[`svelte-language-server`, generateSvelteLanguageServerBaseWrapper],
[`flow-bin`, generateFlowBinBaseWrapper],
[`oxfmt`, generateOxfmtBaseWrapper],
[`oxlint`, generateOxlintBaseWrapper],
[`oxlint-tsgolint`, generateOxlintTsgolintBaseWrapper],
];
22 changes: 22 additions & 0 deletions packages/yarnpkg-sdks/sources/sdks/cocvim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,29 @@ export const generateTypescriptWrapper: GenerateIntegrationWrapper = async (pnpA
});
};

export const generateOxfmtWrapper: GenerateIntegrationWrapper = async (pnpApi: PnpApi, target: PortablePath, wrapper: Wrapper) => {
await addCocVimWorkspaceConfiguration(pnpApi, CocVimConfiguration.settings, {
[`oxc.oxfmt.binPath`]: npath.fromPortablePath(
wrapper.getProjectPathTo(
ppath.normalize(wrapper.manifest.bin.oxfmt),
),
),
});
};

export const generateOxlintWrapper: GenerateIntegrationWrapper = async (pnpApi: PnpApi, target: PortablePath, wrapper: Wrapper) => {
await addCocVimWorkspaceConfiguration(pnpApi, CocVimConfiguration.settings, {
[`oxc.oxlint.binPath`]: npath.fromPortablePath(
wrapper.getProjectPathTo(
ppath.normalize(wrapper.manifest.bin.oxlint),
),
),
});
};

export const COC_VIM_SDKS: IntegrationSdks = [
[`eslint`, generateEslintWrapper],
[`typescript`, generateTypescriptWrapper],
[`oxfmt`, generateOxfmtWrapper],
[`oxlint`, generateOxlintWrapper],
];
38 changes: 38 additions & 0 deletions packages/yarnpkg-sdks/sources/sdks/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,41 @@ export const generateFlowBinWrapper: GenerateIntegrationWrapper = async (pnpApi:
});
};

export const generateOxfmtWrapper: GenerateIntegrationWrapper = async (pnpApi: PnpApi, target: PortablePath, wrapper: Wrapper) => {
await addVSCodeWorkspaceConfiguration(pnpApi, VSCodeConfiguration.settings, {
[`oxc.path.oxfmt`]: npath.fromPortablePath(
wrapper.getProjectPathTo(
ppath.normalize(wrapper.manifest.bin.oxfmt),
),
),
});

await addVSCodeWorkspaceConfiguration(pnpApi, VSCodeConfiguration.extensions, {
[`recommendations`]: [
`oxc.oxc-vscode`,
],
});
};

export const generateOxlintWrapper: GenerateIntegrationWrapper = async (pnpApi: PnpApi, target: PortablePath, wrapper: Wrapper) => {
await addVSCodeWorkspaceConfiguration(pnpApi, VSCodeConfiguration.settings, {
[`oxc.path.oxlint`]: npath.fromPortablePath(
wrapper.getProjectPathTo(
ppath.normalize(wrapper.manifest.bin.oxlint),
),
),
});

await addVSCodeWorkspaceConfiguration(pnpApi, VSCodeConfiguration.extensions, {
[`recommendations`]: [
`oxc.oxc-vscode`,
],
});
};

export const generateOxlintTsgolintWrapper: GenerateIntegrationWrapper = async (pnpApi: PnpApi, target: PortablePath, wrapper: Wrapper) => {
};

export const VSCODE_SDKS: IntegrationSdks = [
[null, generateDefaultWrapper],
[`@astrojs/language-server`, generateAstroLanguageServerWrapper],
Expand All @@ -149,4 +184,7 @@ export const VSCODE_SDKS: IntegrationSdks = [
[`typescript`, generateTypescriptWrapper],
[`svelte-language-server`, generateSvelteLanguageServerWrapper],
[`flow-bin`, generateFlowBinWrapper],
[`oxfmt`, generateOxfmtWrapper],
[`oxlint`, generateOxlintWrapper],
[`oxlint-tsgolint`, generateOxlintTsgolintWrapper],
];
Loading