Skip to content

Commit

Permalink
Reimplements yarn explain peer-requirements (#5834)
Browse files Browse the repository at this point in the history
**What's the problem this PR addresses?**

I didn't update the `yarn explain peer-requirements` when I refactored
the peer dependency warnings to report aggregates rather than
independent entries. As a result, the reported hash doesn't work.

Fixes #5826

**How did you fix it?**

I reimplemented `yarn explain peer-requirements` to take those warnings
into account. I also updated the output to use a more natural language
which might help better understand the situation:

<img width="933" alt="Screenshot 2023-10-24 at 12 04 20"
src="https://github.com/yarnpkg/berry/assets/1037931/62b4dbc7-d029-4997-9990-52bdd752f0a2">

Note that I temporarily removed support for calling `yarn explain
peer-requirements` on missing peer dependencies, as I'd like to take
another look at those warnings in general (I noticed they aren't
aggregated, but they probably should be too). Since they are fairly easy
to understand by themselves, I don't feel like this is a significant
regression.

**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.
  • Loading branch information
arcanis authored Oct 24, 2023
1 parent ccb28f2 commit 70b7c10
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 164 deletions.
34 changes: 34 additions & 0 deletions .yarn/versions/17861fba.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
releases:
"@yarnpkg/cli": patch
"@yarnpkg/core": patch
"@yarnpkg/plugin-essentials": patch

declined:
- "@yarnpkg/plugin-compat"
- "@yarnpkg/plugin-constraints"
- "@yarnpkg/plugin-dlx"
- "@yarnpkg/plugin-exec"
- "@yarnpkg/plugin-file"
- "@yarnpkg/plugin-git"
- "@yarnpkg/plugin-github"
- "@yarnpkg/plugin-http"
- "@yarnpkg/plugin-init"
- "@yarnpkg/plugin-interactive-tools"
- "@yarnpkg/plugin-link"
- "@yarnpkg/plugin-nm"
- "@yarnpkg/plugin-npm"
- "@yarnpkg/plugin-npm-cli"
- "@yarnpkg/plugin-pack"
- "@yarnpkg/plugin-patch"
- "@yarnpkg/plugin-pnp"
- "@yarnpkg/plugin-pnpm"
- "@yarnpkg/plugin-stage"
- "@yarnpkg/plugin-typescript"
- "@yarnpkg/plugin-version"
- "@yarnpkg/plugin-workspace-tools"
- "@yarnpkg/builder"
- "@yarnpkg/doctor"
- "@yarnpkg/extensions"
- "@yarnpkg/nm"
- "@yarnpkg/pnpify"
- "@yarnpkg/sdks"
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe(`Features`, () => {
async ({path, run, source}) => {
const {stdout} = await run(`install`);

expect(stdout).toContain(`no-deps is listed by your project with version 1.1.0, which doesn't satisfy what mismatched-peer-deps-lvl0 requests (1.0.0)`);
expect(stdout).toMatch(/no-deps is listed by your project with version 1\.1\.0, which doesn't satisfy what mismatched-peer-deps-lvl0 \(p[a-f0-9]{5}\) and other dependencies request \(1\.0\.0\)/);
},
),
);
Expand Down
251 changes: 93 additions & 158 deletions packages/plugin-essentials/sources/commands/explain/peerRequirements.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {BaseCommand} from '@yarnpkg/cli';
import {Configuration, MessageName, miscUtils, Project, StreamReport, structUtils, semverUtils, formatUtils, PeerRequirement} from '@yarnpkg/core';
import {Command, Option} from 'clipanion';
import {Writable} from 'stream';
import * as t from 'typanion';
import {BaseCommand} from '@yarnpkg/cli';
import {Configuration, MessageName, Project, StreamReport, structUtils, semverUtils, formatUtils, PeerWarningType} from '@yarnpkg/core';
import {Command, Option} from 'clipanion';
import {Writable} from 'stream';
import * as t from 'typanion';

// eslint-disable-next-line arca/no-default-export
export default class ExplainPeerRequirementsCommand extends BaseCommand {
Expand Down Expand Up @@ -31,8 +31,7 @@ export default class ExplainPeerRequirementsCommand extends BaseCommand {
});

hash = Option.String({
required: false,
validator: t.applyCascade(t.isString(), [
validator: t.cascade(t.isString(), [
t.matchesRegExp(/^p[0-9a-f]{5}$/),
]),
});
Expand All @@ -45,172 +44,108 @@ export default class ExplainPeerRequirementsCommand extends BaseCommand {
restoreResolutions: false,
});

// peerRequirements aren't stored inside the install state
await project.applyLightResolution();

if (typeof this.hash !== `undefined`) {
return await explainPeerRequirements(this.hash, project, {
stdout: this.context.stdout,
});
}

const report = await StreamReport.start({
configuration,
return await explainPeerRequirements(this.hash, project, {
stdout: this.context.stdout,
includeFooter: false,
}, async report => {
const sortCriterias: Array<(opts: [string, PeerRequirement]) => string> = [
([, requirement]) => structUtils.stringifyLocator(project.storedPackages.get(requirement.subject)!),
([, requirement]) => structUtils.stringifyIdent(requirement.requested),
];

for (const [hash, requirement] of miscUtils.sortMap(project.peerRequirements, sortCriterias)) {
const subject = project.storedPackages.get(requirement.subject);
if (typeof subject === `undefined`)
throw new Error(`Assertion failed: Expected the subject package to have been registered`);

const rootRequester = project.storedPackages.get(requirement.rootRequester);
if (typeof rootRequester === `undefined`)
throw new Error(`Assertion failed: Expected the root package to have been registered`);

const providedDescriptor = subject.dependencies.get(requirement.requested.identHash) ?? null;

const prettyHash = formatUtils.pretty(configuration, hash, formatUtils.Type.CODE);
const prettySubject = structUtils.prettyLocator(configuration, subject);
const prettyIdent = structUtils.prettyIdent(configuration, requirement.requested);
const prettyRoot = structUtils.prettyIdent(configuration, rootRequester);

const descendantCount = requirement.allRequesters.length - 1;

const pluralized = `descendant${descendantCount === 1 ? `` : `s`}`;
const maybeDescendants = descendantCount > 0 ? ` and ${descendantCount} ${pluralized}` : ``;
const provides = providedDescriptor !== null ? `provides` : `doesn't provide`;

report.reportInfo(null, `${prettyHash}${prettySubject} ${provides} ${prettyIdent} to ${prettyRoot}${maybeDescendants}`);
}
});

return report.exitCode();
}
}

export async function explainPeerRequirements(peerRequirementsHash: string, project: Project, opts: {stdout: Writable}) {
const {configuration} = project;
const warning = project.peerWarnings.find(warning => {
return warning.hash === peerRequirementsHash;
});

const requirement = project.peerRequirements.get(peerRequirementsHash);
if (typeof requirement === `undefined`)
if (typeof warning === `undefined`)
throw new Error(`No peerDependency requirements found for hash: "${peerRequirementsHash}"`);

const report = await StreamReport.start({
configuration,
configuration: project.configuration,
stdout: opts.stdout,
includeFooter: false,
includePrefix: false,
}, async report => {
const subject = project.storedPackages.get(requirement.subject);
if (typeof subject === `undefined`)
throw new Error(`Assertion failed: Expected the subject package to have been registered`);

const rootRequester = project.storedPackages.get(requirement.rootRequester);
if (typeof rootRequester === `undefined`)
throw new Error(`Assertion failed: Expected the root package to have been registered`);

const providedDescriptor = subject.dependencies.get(requirement.requested.identHash) ?? null;

const providedResolution = providedDescriptor !== null
? project.storedResolutions.get(providedDescriptor.descriptorHash)
: null;

if (typeof providedResolution === `undefined`)
throw new Error(`Assertion failed: Expected the resolution to have been registered`);

const provided = providedResolution !== null
? project.storedPackages.get(providedResolution)
: null;

if (typeof provided === `undefined`)
throw new Error(`Assertion failed: Expected the provided package to have been registered`);

const allRequesters = [...requirement.allRequesters.values()].map(requesterHash => {
const pkg = project.storedPackages.get(requesterHash);
if (typeof pkg === `undefined`)
throw new Error(`Assertion failed: Expected the package to be registered`);

const devirtualizedLocator = structUtils.devirtualizeLocator(pkg);
const devirtualizedPkg = project.storedPackages.get(devirtualizedLocator.locatorHash);
if (typeof devirtualizedPkg === `undefined`)
throw new Error(`Assertion failed: Expected the package to be registered`);

const peerDependency = devirtualizedPkg.peerDependencies.get(requirement.requested.identHash);
if (typeof peerDependency === `undefined`)
throw new Error(`Assertion failed: Expected the peer dependency to be registered`);

return {pkg, peerDependency};
});

if (provided !== null) {
const satisfiesAllRanges = allRequesters.every(({peerDependency}) => {
return semverUtils.satisfiesWithPrereleases(provided.version, peerDependency.range);
});

report.reportInfo(MessageName.UNNAMED, `${
structUtils.prettyLocator(configuration, subject)
} provides ${
structUtils.prettyLocator(configuration, provided)
} with version ${
structUtils.prettyReference(configuration, provided.version ?? `<missing>`)
}, which ${satisfiesAllRanges ? `satisfies` : `doesn't satisfy`} the following requirements:`);
} else {
report.reportInfo(MessageName.UNNAMED, `${
structUtils.prettyLocator(configuration, subject)
} doesn't provide ${
structUtils.prettyIdent(configuration, requirement.requested)
}, breaking the following requirements:`);
}

report.reportSeparator();

const Mark = formatUtils.mark(configuration);

const requirements: Array<{
stringifiedLocator: string;
prettyLocator: string;
prettyRange: string;
mark: string;
}> = [];

for (const {pkg, peerDependency} of miscUtils.sortMap(allRequesters, requester => structUtils.stringifyLocator(requester.pkg))) {
const isSatisfied = provided !== null
? semverUtils.satisfiesWithPrereleases(provided.version, peerDependency.range)
: false;

const mark = isSatisfied ? Mark.Check : Mark.Cross;

requirements.push({
stringifiedLocator: structUtils.stringifyLocator(pkg),
prettyLocator: structUtils.prettyLocator(configuration, pkg),
prettyRange: structUtils.prettyRange(configuration, peerDependency.range),
mark,
});
}

const maxStringifiedLocatorLength = Math.max(...requirements.map(({stringifiedLocator}) => stringifiedLocator.length));
const maxPrettyRangeLength = Math.max(...requirements.map(({prettyRange}) => prettyRange.length));

for (const {stringifiedLocator, prettyLocator, prettyRange, mark} of miscUtils.sortMap(requirements, ({stringifiedLocator}) => stringifiedLocator)) {
report.reportInfo(null, `${
// We have to do this because prettyLocators can contain multiple colors
prettyLocator.padEnd(maxStringifiedLocatorLength + (prettyLocator.length - stringifiedLocator.length), ` `)
}${
prettyRange.padEnd(maxPrettyRangeLength, ` `)
} ${mark}`);
}

if (requirements.length > 1) {
report.reportSeparator();

report.reportInfo(MessageName.UNNAMED, `Note: these requirements start with ${
structUtils.prettyLocator(project.configuration, rootRequester)
}`);
const Marks = formatUtils.mark(project.configuration);

switch (warning.type) {
case PeerWarningType.NotCompatibleAggregate: {
report.reportInfo(MessageName.UNNAMED, `We have a problem with ${formatUtils.pretty(project.configuration, warning.requested, formatUtils.Type.IDENT)}, which is provided with version ${structUtils.prettyReference(project.configuration, warning.version)}.`);
report.reportInfo(MessageName.UNNAMED, `It is needed by the following direct dependencies of workspaces in your project:`);

report.reportSeparator();

for (const dependent of warning.requesters.values()) {
const dependentPkg = project.storedPackages.get(dependent.locatorHash);
if (!dependentPkg)
throw new Error(`Assertion failed: Expected the package to be registered`);

const descriptor = dependentPkg?.peerDependencies.get(warning.requested.identHash);
if (!descriptor)
throw new Error(`Assertion failed: Expected the package to list the peer dependency`);

const mark = semverUtils.satisfiesWithPrereleases(warning.version, descriptor.range)
? Marks.Check
: Marks.Cross;

report.reportInfo(null, ` ${mark} ${structUtils.prettyLocator(project.configuration, dependent)} (via ${structUtils.prettyRange(project.configuration, descriptor.range)})`);
}

const transitiveLinks = [...warning.links.values()].filter(link => {
return !warning.requesters.has(link.locatorHash);
});

if (transitiveLinks.length > 0) {
report.reportSeparator();
report.reportInfo(MessageName.UNNAMED, `However, those packages themselves have more dependencies listing ${structUtils.prettyIdent(project.configuration, warning.requested)} as peer dependency:`);
report.reportSeparator();

for (const link of transitiveLinks) {
const linkPkg = project.storedPackages.get(link.locatorHash);
if (!linkPkg)
throw new Error(`Assertion failed: Expected the package to be registered`);

const descriptor = linkPkg?.peerDependencies.get(warning.requested.identHash);
if (!descriptor)
throw new Error(`Assertion failed: Expected the package to list the peer dependency`);

const mark = semverUtils.satisfiesWithPrereleases(warning.version, descriptor.range)
? Marks.Check
: Marks.Cross;

report.reportInfo(null, ` ${mark} ${structUtils.prettyLocator(project.configuration, link)} (via ${structUtils.prettyRange(project.configuration, descriptor.range)})`);
}
}

const allRanges = Array.from(warning.links.values(), locator => {
const pkg = project.storedPackages.get(locator.locatorHash);
if (typeof pkg === `undefined`)
throw new Error(`Assertion failed: Expected the package to be registered`);

const peerDependency = pkg.peerDependencies.get(warning.requested.identHash);
if (typeof peerDependency === `undefined`)
throw new Error(`Assertion failed: Expected the ident to be registered`);

return peerDependency.range;
});

if (allRanges.length > 1) {
const resolvedRange = semverUtils.simplifyRanges(allRanges);

report.reportSeparator();

if (resolvedRange === null) {
report.reportInfo(MessageName.UNNAMED, `Unfortunately, put together, we found no single range that can satisfy all those peer requirements.`);
report.reportInfo(MessageName.UNNAMED, `Your best option may be to try to upgrade some dependencies with ${formatUtils.pretty(project.configuration, `yarn up`, formatUtils.Type.CODE)}, or silence the warning via ${formatUtils.pretty(project.configuration, `logFilters`, formatUtils.Type.CODE)}.`);
} else {
report.reportInfo(MessageName.UNNAMED, `Put together, the final range we computed is ${formatUtils.pretty(project.configuration, resolvedRange, formatUtils.Type.RANGE)}`);
}
}
} break;

default: {
report.reportInfo(MessageName.UNNAMED, `The ${formatUtils.pretty(project.configuration, `yarn explain peer-requirements`, formatUtils.Type.CODE)} command doesn't support this warning type yet.`);
} break;
}
});

Expand Down
6 changes: 3 additions & 3 deletions packages/yarnpkg-core/sources/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2615,7 +2615,7 @@ function applyVirtualResolutionMutations({
requesters: new Map(),
links: new Map(),
version: peerVersion,
hash: `p${hashUtils.makeHash(identStr).slice(0, 5)}`,
hash: `p${peerResolution.locatorHash.slice(0, 5)}`,
}));

aggregatedWarning.dependents.set(dependent.locatorHash, dependent);
Expand Down Expand Up @@ -2670,7 +2670,7 @@ function emitPeerDependencyWarnings(project: Project, report: Report) {
return peerDependency.range;
});

const andDescendants = warning.dependents.size > 1
const andDescendants = warning.links.size > 1
? `and other dependencies request`
: `requests`;

Expand All @@ -2685,7 +2685,7 @@ function emitPeerDependencyWarnings(project: Project, report: Report) {
structUtils.prettyReference(project.configuration, warning.version)
}, which doesn't satisfy what ${
structUtils.prettyIdent(project.configuration, warning.requesters.values().next().value)
} ${andDescendants} (${rangeDescription}).`;
} (${formatUtils.pretty(project.configuration, warning.hash, formatUtils.Type.CODE)}) ${andDescendants} (${rangeDescription}).`;
}) ?? [];

const omittedWarnings = warningsByType[PeerWarningType.NotProvided]?.map(warning => {
Expand Down
4 changes: 2 additions & 2 deletions packages/yarnpkg-core/sources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ export type {AllDependencies, HardDependencies, DependencyMeta, PeerDependencyMe
export {MessageName, parseMessageName, stringifyMessageName} from './MessageName';
export {MultiFetcher} from './MultiFetcher';
export type {CommandContext, Hooks, Plugin, WrapNetworkRequestInfo} from './Plugin';
export type {PeerRequirement} from './Project';
export {LOCKFILE_VERSION, Project, InstallMode} from './Project';
export type {PeerRequirement, PeerWarning} from './Project';
export {LOCKFILE_VERSION, PeerWarningType, Project, InstallMode} from './Project';
export {ReportError, Report} from './Report';
export type {Resolver, ResolveOptions, MinimalResolveOptions} from './Resolver';
export {StreamReport, reportOptionDeprecations} from './StreamReport';
Expand Down

0 comments on commit 70b7c10

Please sign in to comment.