Skip to content

Commit

Permalink
Infer which files should SDKs include using manifest data (#5703)
Browse files Browse the repository at this point in the history
**What's the problem this PR addresses?**
<!-- Describe the rationale of your PR. -->
<!-- Link all issues that it closes. (Closes/Resolves #xxxx.) -->

Some SDKs do not include all exposed files, making some features
inaccessible to some editor integration. In particular, the ESLint SDK
does not include `lib/unsupported-api.js` needed for its experimental
flat config feature.

Closes #5231
Closes #5471

**How did you fix it?**
<!-- A detailed description of your implementation. -->

As outlined in #5231, the list of exposed files can be inferred by
reading the `main`, `bin`, and `exports` fields of the manifest. This PR
switches all existing SDK generators to doing so. Some SDKs (e.g.
Typescript) still requires some special treatment so the PR keeps some
escape hatches.

**Questions**

1. I don't know if this is considered a breaking change, I have marked
`@yarnpkg/sdks` for a minor release for now.
2. `typescript-language-server` has not exposed a CJS entrypoint since
1.0.0, and as far as I know the SDK mechanism cannot support ESM files
since loaders cannot be dynamically added once userland code starts
execution. Should we just remove that SDK?
3. In the same vein, should we just ignore any conditional exports that
require the `import` condition? `.mjs` files?

**Checklist**
<!--- Don't worry if you miss something, chores are automatically
tested. -->
<!--- This checklist exists to help you remember doing the chores when
you submit a PR. -->
<!--- Put an `x` in all the boxes that apply. -->
- [x] I have read the [Contributing
Guide](https://yarnpkg.com/advanced/contributing).

<!-- See
https://yarnpkg.com/advanced/contributing#preparing-your-pr-to-be-released
for more details. -->
<!-- Check with `yarn version check` and fix with `yarn version check
-i` -->
- [x] I have set the packages that need to be released for my changes to
be effective.

<!-- The "Testing chores" workflow validates that your PR follows our
guidelines. -->
<!-- If it doesn't pass, click on it to see details as to what your PR
might be missing. -->
- [x] I will check that all automated PR checks pass before the PR gets
reviewed.

---------

Co-authored-by: Maël Nison <[email protected]>
  • Loading branch information
clemyan and arcanis authored Oct 18, 2023
1 parent 453f411 commit 3c1be68
Show file tree
Hide file tree
Showing 11 changed files with 145 additions and 68 deletions.
20 changes: 20 additions & 0 deletions .yarn/sdks/eslint/lib/unsupported-api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env node

const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);

const relPnpApiPath = "../../../../.pnp.cjs";

const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = createRequire(absPnpApiPath);

if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require eslint/use-at-your-own-risk
require(absPnpApiPath).setup();
}
}

// Defer to the real eslint/use-at-your-own-risk your application uses
module.exports = absRequire(`eslint/use-at-your-own-risk`);
10 changes: 9 additions & 1 deletion .yarn/sdks/eslint/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,13 @@
"name": "eslint",
"version": "8.45.0-sdk",
"main": "./lib/api.js",
"type": "commonjs"
"type": "commonjs",
"bin": {
"eslint": "./bin/eslint.js"
},
"exports": {
"./package.json": "./package.json",
".": "./lib/api.js",
"./use-at-your-own-risk": "./lib/unsupported-api.js"
}
}
6 changes: 3 additions & 3 deletions .yarn/sdks/typescript/lib/typescript.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ const absRequire = createRequire(absPnpApiPath);

if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/lib/typescript.js
// Setup the environment to be able to require typescript
require(absPnpApiPath).setup();
}
}

// Defer to the real typescript/lib/typescript.js your application uses
module.exports = absRequire(`typescript/lib/typescript.js`);
// Defer to the real typescript your application uses
module.exports = absRequire(`typescript`);
6 changes: 5 additions & 1 deletion .yarn/sdks/typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@
"name": "typescript",
"version": "5.3.0-beta-sdk",
"main": "./lib/typescript.js",
"type": "commonjs"
"type": "commonjs",
"bin": {
"tsc": "./bin/tsc",
"tsserver": "./bin/tsserver"
}
}
2 changes: 2 additions & 0 deletions .yarn/versions/4d2d8afa.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 @@ -21,6 +21,7 @@ describe(`Features`, () => {
await xfs.writeJsonPromise(manifestPath, {
name: `eslint`,
version: `1.0.0`,
bin: `./bin/eslint.js`,
dependencies: {
[`no-deps`]: `1.0.0`,
},
Expand Down Expand Up @@ -58,6 +59,7 @@ describe(`Features`, () => {
await xfs.writeJsonPromise(manifestPath, {
name: `eslint`,
version: `1.0.0`,
bin: `./bin/eslint.js`,
dependencies: {
[`no-deps`]: `1.0.0`,
},
Expand Down
2 changes: 1 addition & 1 deletion packages/yarnpkg-sdks/sources/commands/SdkCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export default class SdkCommand extends Command {
}

if (nextProjectRoot === currProjectRoot)
throw new Error(`This tool can only be used with projects using Yarn Plug'n'Play`);
throw new UsageError(`This tool can only be used with projects using Yarn Plug'n'Play`);

const configuration = Configuration.create(currProjectRoot);
const pnpPath = ppath.join(currProjectRoot, pnpFilename);
Expand Down
98 changes: 85 additions & 13 deletions packages/yarnpkg-sdks/sources/generateSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,13 +184,22 @@ export type SupportedSdk =
| 'svelte-language-server'
| 'flow-bin';

export type BaseSdks = Array<[SupportedSdk, GenerateBaseWrapper]>;
export type BaseSdks = Array<[
SupportedSdk,
GenerateBaseWrapper,
]>;

export type IntegrationSdks = Array<
| [null, GenerateDefaultWrapper | null]
| [SupportedSdk, GenerateIntegrationWrapper | null]
>;

export type PackageExports =
| {[key: string]: PackageExports}
| Array<PackageExports>
| string
| null;

export class Wrapper {
private name: PortablePath;

Expand All @@ -199,33 +208,95 @@ export class Wrapper {

private paths: Map<PortablePath, PortablePath> = new Map();

constructor(name: PortablePath, {pnpApi, target}: {pnpApi: PnpApi, target: PortablePath}) {
public readonly manifest: Record<string, any>;

constructor(name: PortablePath, {pnpApi, target, manifestOverrides = {}}: {pnpApi: PnpApi, target: PortablePath, manifestOverrides?: Record<string, any>}) {
this.name = name;

this.pnpApi = pnpApi;
this.target = target;
}

async writeManifest(rawManifest: Record<string, any> = {}) {
const absWrapperPath = ppath.join(this.target, this.name, `package.json`);
this.manifest = this.loadManifest();
Object.assign(this.manifest, manifestOverrides);
}

private loadManifest() {
const topLevelInformation = this.pnpApi.getPackageInformation(this.pnpApi.topLevel)!;
const dependencyReference = topLevelInformation.packageDependencies.get(this.name)!;

const pkgInformation = this.pnpApi.getPackageInformation(this.pnpApi.getLocator(this.name, dependencyReference));
if (pkgInformation === null)
throw new Error(`Assertion failed: Package ${this.name} isn't a dependency of the top-level`);

const manifest = dynamicRequire(npath.join(pkgInformation.packageLocation, `package.json`));
const manifestPath = npath.join(pkgInformation.packageLocation, Filename.manifest);
const manifest = dynamicRequire(manifestPath);

await xfs.mkdirPromise(ppath.dirname(absWrapperPath), {recursive: true});
await xfs.writeJsonPromise(absWrapperPath, {
return {
name: this.name,
version: `${manifest.version}-sdk`,
main: manifest.main,
type: `commonjs`,
...rawManifest,
});
bin: manifest.bin,
exports: manifest.exports,
};
}

async writeDefaults() {
if (this.manifest.main)
await this.writeFile(ppath.normalize(this.manifest.main as PortablePath), {requirePath: PortablePath.dot});

if (this.manifest.exports)
await this.writePackageExports();

if (this.manifest.bin)
await this.writePackageBinaries();

await this.writeManifest();
}

async writePackageBinaries() {
if (typeof this.manifest.bin === `string`) {
await this.writeBinary(ppath.normalize(this.manifest.bin as PortablePath));
} else {
for (const relPackagePath of Object.values(this.manifest.bin)) {
await this.writeBinary(ppath.normalize(relPackagePath as PortablePath));
}
}
}

async writePackageExports(packageExports: PackageExports = this.manifest.exports, requirePath = PortablePath.dot) {
if (typeof packageExports === `string`) {
if (!packageExports.includes(`*`)) {
await this.writeFile(ppath.normalize(packageExports as PortablePath), {requirePath});
}
} else if (Array.isArray(packageExports)) {
await Promise.all(
packageExports.map(packageExport => this.writePackageExports(packageExport, requirePath)),
);
} else if (packageExports !== null) {
await Promise.all(
Object.entries(packageExports).map(async ([key, value]) => {
if (key.startsWith(`.`)) {
await this.writePackageExports(value, key as PortablePath);
} else {
await this.writePackageExports(value, requirePath);
}
}),
);
}
}

async writeManifest() {
const topLevelInformation = this.pnpApi.getPackageInformation(this.pnpApi.topLevel)!;
const projectRoot = npath.toPortablePath(topLevelInformation.packageLocation);

const absWrapperPath = ppath.join(this.target, this.name, `package.json`);
const relProjectPath = ppath.relative(projectRoot, absWrapperPath);

await xfs.mkdirPromise(ppath.dirname(absWrapperPath), {recursive: true});
await xfs.writeJsonPromise(absWrapperPath, this.manifest);

this.paths.set(Filename.manifest, relProjectPath);
}

async writeBinary(relPackagePath: PortablePath, options: TemplateOptions & {requirePath?: PortablePath} = {}) {
Expand All @@ -242,10 +313,11 @@ export class Wrapper {
const absPnpApiPath = npath.toPortablePath(this.pnpApi.resolveRequest(`pnpapi`, null)!);
const relPnpApiPath = ppath.relative(ppath.dirname(absWrapperPath), absPnpApiPath);

const moduleReqPath = ppath.join(this.name, options.requirePath ?? relPackagePath);
const wrapperScript = TEMPLATE(relPnpApiPath, moduleReqPath, options);

await xfs.mkdirPromise(ppath.dirname(absWrapperPath), {recursive: true});
await xfs.writeFilePromise(absWrapperPath, TEMPLATE(relPnpApiPath, ppath.join(this.name, options.requirePath ?? relPackagePath), options), {
mode: options.mode,
});
await xfs.writeFilePromise(absWrapperPath, wrapperScript, {mode: options.mode});

this.paths.set(relPackagePath, relProjectPath);

Expand Down
45 changes: 9 additions & 36 deletions packages/yarnpkg-sdks/sources/sdks/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,58 +6,39 @@ import {Wrapper, GenerateBaseWrapper, BaseSdks} from '../generateSdk';
export const generateAstroLanguageServerBaseWrapper: GenerateBaseWrapper = async (pnpApi: PnpApi, target: PortablePath) => {
const wrapper = new Wrapper(`@astrojs/language-server` as PortablePath, {pnpApi, target});

await wrapper.writeManifest();

await wrapper.writeBinary(`bin/nodeServer.js` as PortablePath);
await wrapper.writeDefaults();

return wrapper;
};

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

await wrapper.writeManifest();

await wrapper.writeBinary(`bin/eslint.js` as PortablePath);
await wrapper.writeFile(`lib/api.js` as PortablePath, {
// Empty path to use the entrypoint and let Node.js resolve the correct path itself
requirePath: `` as PortablePath,
});
await wrapper.writeDefaults();

return wrapper;
};

export const generatePrettierBaseWrapper: GenerateBaseWrapper = async (pnpApi: PnpApi, target: PortablePath) => {
const wrapper = new Wrapper(`prettier` as PortablePath, {pnpApi, target});
const wrapper = new Wrapper(`prettier` as PortablePath, {pnpApi, target, manifestOverrides: {exports: undefined}});

await wrapper.writeManifest({
main: `./index.js`,
});

await wrapper.writeBinary(`index.js` as PortablePath, {
// Empty path to use the entrypoint and let Node.js resolve the correct path itself
requirePath: `` as PortablePath,
});
await wrapper.writeDefaults();

return wrapper;
};

export const generateRelayCompilerBaseWrapper: GenerateBaseWrapper = async (pnpApi: PnpApi, target: PortablePath) => {
const wrapper = new Wrapper(`relay-compiler` as PortablePath, {pnpApi, target});

await wrapper.writeManifest();

await wrapper.writeBinary(`cli.js` as PortablePath);
await wrapper.writeDefaults();

return wrapper;
};

export const generateTypescriptLanguageServerBaseWrapper: GenerateBaseWrapper = async (pnpApi: PnpApi, target: PortablePath) => {
const wrapper = new Wrapper(`typescript-language-server` as PortablePath, {pnpApi, target});

await wrapper.writeManifest();

await wrapper.writeBinary(`lib/cli.js` as PortablePath);
await wrapper.writeDefaults();

return wrapper;
};
Expand Down Expand Up @@ -272,14 +253,10 @@ export const generateTypescriptBaseWrapper: GenerateBaseWrapper = async (pnpApi:

const wrapper = new Wrapper(`typescript` as PortablePath, {pnpApi, target});

await wrapper.writeManifest();

await wrapper.writeBinary(`bin/tsc` as PortablePath);
await wrapper.writeBinary(`bin/tsserver` as PortablePath);
await wrapper.writeDefaults();

await wrapper.writeFile(`lib/tsc.js` as PortablePath);
await wrapper.writeFile(`lib/tsserver.js` as PortablePath, {wrapModule: tsServerMonkeyPatch});
await wrapper.writeFile(`lib/typescript.js` as PortablePath);
await wrapper.writeFile(`lib/tsserverlibrary.js` as PortablePath, {wrapModule: tsServerMonkeyPatch});

return wrapper;
Expand All @@ -288,19 +265,15 @@ export const generateTypescriptBaseWrapper: GenerateBaseWrapper = async (pnpApi:
export const generateSvelteLanguageServerBaseWrapper: GenerateBaseWrapper = async (pnpApi: PnpApi, target: PortablePath) => {
const wrapper = new Wrapper(`svelte-language-server` as PortablePath, {pnpApi, target});

await wrapper.writeManifest();

await wrapper.writeBinary(`bin/server.js` as PortablePath);
await wrapper.writeDefaults();

return wrapper;
};

export const generateFlowBinBaseWrapper: GenerateBaseWrapper = async (pnpApi: PnpApi, target: PortablePath) => {
const wrapper = new Wrapper(`flow-bin` as PortablePath, {pnpApi, target});

await wrapper.writeManifest();

await wrapper.writeBinary(`cli.js` as PortablePath);
await wrapper.writeDefaults();

return wrapper;
};
Expand Down
10 changes: 4 additions & 6 deletions packages/yarnpkg-sdks/sources/sdks/cocvim.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {PortablePath, npath, ppath} from '@yarnpkg/fslib';
import {Filename, PortablePath, npath, ppath} from '@yarnpkg/fslib';
import {PnpApi} from '@yarnpkg/pnp';

import {Wrapper, GenerateIntegrationWrapper, IntegrationSdks} from '../generateSdk';
Expand All @@ -17,11 +17,9 @@ export const generateEslintWrapper: GenerateIntegrationWrapper = async (pnpApi:
await addCocVimWorkspaceConfiguration(pnpApi, CocVimConfiguration.settings, {
[`eslint.packageManager`]: `yarn`,
[`eslint.nodePath`]: npath.fromPortablePath(
ppath.dirname(ppath.dirname(ppath.dirname(
wrapper.getProjectPathTo(
`lib/api.js` as PortablePath,
),
))),
ppath.dirname(ppath.dirname(
wrapper.getProjectPathTo(Filename.manifest),
)),
),
});
};
Expand Down
12 changes: 5 additions & 7 deletions packages/yarnpkg-sdks/sources/sdks/vscode.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {PortablePath, npath, ppath} from '@yarnpkg/fslib';
import {Filename, PortablePath, npath, ppath} from '@yarnpkg/fslib';
import {PnpApi} from '@yarnpkg/pnp';

import {Wrapper, GenerateIntegrationWrapper, GenerateDefaultWrapper, IntegrationSdks} from '../generateSdk';
Expand Down Expand Up @@ -48,11 +48,9 @@ export const generateAstroLanguageServerWrapper: GenerateIntegrationWrapper = as
export const generateEslintWrapper: GenerateIntegrationWrapper = async (pnpApi: PnpApi, target: PortablePath, wrapper: Wrapper) => {
await addVSCodeWorkspaceConfiguration(pnpApi, VSCodeConfiguration.settings, {
[`eslint.nodePath`]: npath.fromPortablePath(
ppath.dirname(ppath.dirname(ppath.dirname(
wrapper.getProjectPathTo(
`lib/api.js` as PortablePath,
),
))),
ppath.dirname(ppath.dirname(
wrapper.getProjectPathTo(Filename.manifest),
)),
),
});

Expand All @@ -67,7 +65,7 @@ export const generatePrettierWrapper: GenerateIntegrationWrapper = async (pnpApi
await addVSCodeWorkspaceConfiguration(pnpApi, VSCodeConfiguration.settings, {
[`prettier.prettierPath`]: npath.fromPortablePath(
wrapper.getProjectPathTo(
`index.js` as PortablePath,
ppath.normalize(wrapper.manifest.main),
),
),
});
Expand Down

0 comments on commit 3c1be68

Please sign in to comment.