From bfa6489467e0e11ee87268e01e38e4f7e8d4d4b0 Mon Sep 17 00:00:00 2001 From: Clement Yan Date: Fri, 17 May 2024 18:47:27 +0800 Subject: [PATCH] Rework peer requirement and warning system (#6205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **What's the problem this PR addresses?** There are a number of issues with the current peer requirement and peer warning system: ---- Firstly, since v4, we lost the ability to list all peer requirements in the project and to explain missing peer dependency warnings, which is a huge degradation to DX. ---- Secondly, also since v4, transitive peer warnings are no longer displayed. The rationale given was "those are not actionable". But they *are* actionable because we have `packageExtensions`. More importantly, there are no indication nor discoverability when this happens. Yarn exits without even a warning when transitive peer warnings can put the project in an invalid state. Even when the user somehow knows that there are transitive peer warnings, they are impossible to investigate and fix without the proper tools. ---- Thirdly, as I just reported, the peer warning aggregation is aggregating unrelated peer requests, causing `yarn explain peer-requirements` to give incorrect advice. #6203 ---- Finally, the peer resolution pipeline has been refactored and added to many times over the years which make the code quite difficult to understand. This is compounded by misplaced comments and overloading terms like "peer descriptor" in the code. ---- Closes #5841 Closes #5977 Closes #6016 Closes #6118 Closes #6203 **How did you fix it?** On the last problem, it is quite impossible to completely solve in one go, so I only took small bites and at least rewrote some of the comments to better capture the current peer pipeline. I have also unified the terminology and change variable names to use those terminology to reduce ambiguity. Maybe we should add these to the lexicon? Let's say - Package `A` (regular-)depends on `B`, which peer-depends on `C` - Package `A` also depends on `C` ``` A@1.0.0 --> B@^1.0.0 (resolves to B@1.1.0) ==> C@^1.0.0 --> C@^1.2.0 (resolves to C@1.3.0) ``` In the scope of this single level - The package `A@1.0.0` is known as the **subject** or **requestee** - The package `B@1.1.0` is known as the **requester**. Also called the **virtualized package** in the code because of what the pipeline is doing - The ident `C` is known as the **peer ident** and the descriptor `C@^1.0.0` is known as the **peer descriptor** - The descriptor `C@^1.2.0` is the **provided descriptor**. Similarly, `C@1.3.0` is the **provided locator/package**. **Provision** can refer to any of those depending on context. - Note that the provision is not necessarily `A`'s dependency. `A` itself could be used if its ident *were* matching, or a package can be not provided at all. Based on those we can create some composite data structures: - A **peer request** encapsulates a requester + peer ident. - If a subject does not provide to the request itself, but instead requests a peer dependency under the same ident, that subject and the new peer request it issues are both said to **forward** the original peer request. - A **peer requirement** encapsulates a subject + peer ident + one or more peer requests. This captures the fact that a single subject that depends on multiple peer requesters can satisfy multiple peer requests at the same time - Note that peer requests and peer requirements is a many-to-many relationship. A peer request is included in multiple peer requirements if the requester is depended upon multiple times (which can happen due to deduplication). - A peer requirement is said to be a **root peer requirement** if the subject does not forward the peer requests. ---- With those concepts established, the core of this PR changes the peer requirement and peer warning system to use a tree-based structure. The nodes of the tree are either peer requests or peer requirements. A peer request's children are the requests it forwards, and a peer requirement's children are the requests it includes. Note that a tree can only ever include peer requirements and requests regarding a single peer ident. ![Untitled Diagram](https://github.com/yarnpkg/berry/assets/41266433/166595b0-b7be-4e8c-a589-64a7d16ac462) By storing all peer requirement nodes in `Project`, other places can easily display information on peer requirements/requests/warnings by retrieving them form the tree (using the new tree-view UI, even). This allows us to reimplement `yarn explain peer-requirements` and `yarn explain peer-requirements ` to list all requirements and explain any peer requirement (even without warnings) respectively. This also fixes #6203 because the peer requirement nodes correctly group the peer requests. ![image](https://github.com/yarnpkg/berry/assets/41266433/7f40f8f3-53da-48d4-bb10-38a8c23ccec4) ---- To solve the discoverability problem without bringing back the clutter, I've added an additional CTA for transitive peer warnings, so all transitive peer warnings are reduced to a single line. ``` ➤ YN0000: ┌ Post-resolution validation ➤ YN0002: │ workspace-b@workspace:workspace-b doesn't provide p (p427bb), requested by y. ➤ YN0086: │ Some peer dependencies are incorrectly met by your project; run yarn explain peer-requirements for details, where is the six-letter p-prefixed code. ➤ YN0086: │ Some peer dependencies are incorrectly met by dependencies; run yarn explain peer-requirements for details. ➤ YN0000: └ Completed ``` A user wishing to view the transitive warnings can do so with the reimplemented `yarn explain peer-requirements` command. ---- Lastly, I don't know how much of the original peer requirement and warning system matter for BC. For now, I have recreated the data structures created by the original system, except the aggregated warnings which is the wrong abstraction as noted. I have added TODO comments to remove those code for the next major. Please advice if the original system can be safely removed in a minor. **Checklist** - [x] I have read the [Contributing Guide](https://yarnpkg.com/advanced/contributing). - [x] I have set the packages that need to be released for my changes to be effective. - [x] I will check that all automated PR checks pass before the PR gets reviewed. --------- Co-authored-by: Maël Nison --- .yarn/versions/594fa39a.yml | 34 ++ .../features/peerDependenciesMeta.test.ts | 2 +- .../commands/explain/peerRequirements.ts | 275 +++++++--- packages/yarnpkg-core/sources/Project.ts | 510 ++++++++++-------- packages/yarnpkg-core/sources/structUtils.ts | 49 +- 5 files changed, 539 insertions(+), 331 deletions(-) create mode 100644 .yarn/versions/594fa39a.yml diff --git a/.yarn/versions/594fa39a.yml b/.yarn/versions/594fa39a.yml new file mode 100644 index 000000000000..b3aacffd1f6d --- /dev/null +++ b/.yarn/versions/594fa39a.yml @@ -0,0 +1,34 @@ +releases: + "@yarnpkg/cli": minor + "@yarnpkg/core": minor + "@yarnpkg/plugin-essentials": minor + +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" diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/features/peerDependenciesMeta.test.ts b/packages/acceptance-tests/pkg-tests-specs/sources/features/peerDependenciesMeta.test.ts index 4919799b30e4..4179989d2414 100644 --- a/packages/acceptance-tests/pkg-tests-specs/sources/features/peerDependenciesMeta.test.ts +++ b/packages/acceptance-tests/pkg-tests-specs/sources/features/peerDependenciesMeta.test.ts @@ -40,7 +40,7 @@ describe(`Features`, () => { async ({path, run, source}) => { const {stdout} = await run(`install`); - 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\)/); + expect(stdout).toMatch(/no-deps is listed by your project with version 1\.1\.0 \(p[a-f0-9]{5}\), which doesn't satisfy what mismatched-peer-deps-lvl0 and other dependencies request \(1\.0\.0\)/); }, ), ); diff --git a/packages/plugin-essentials/sources/commands/explain/peerRequirements.ts b/packages/plugin-essentials/sources/commands/explain/peerRequirements.ts index 0b87eea3fbf5..6455d31ad924 100644 --- a/packages/plugin-essentials/sources/commands/explain/peerRequirements.ts +++ b/packages/plugin-essentials/sources/commands/explain/peerRequirements.ts @@ -1,8 +1,9 @@ -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'; +import {BaseCommand} from '@yarnpkg/cli'; +import type {PeerRequestNode} from '@yarnpkg/core/sources/Project'; +import {Configuration, MessageName, Project, StreamReport, structUtils, formatUtils, treeUtils, PeerWarningType, miscUtils, type LocatorHash} 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 { @@ -13,24 +14,25 @@ export default class ExplainPeerRequirementsCommand extends BaseCommand { static usage = Command.Usage({ description: `explain a set of peer requirements`, details: ` - A set of peer requirements represents all peer requirements that a dependent must satisfy when providing a given peer request to a requester and its descendants. + A peer requirement represents all peer requests that a subject must satisfy when providing a requested package to requesters. - When the hash argument is specified, this command prints a detailed explanation of all requirements of the set corresponding to the hash and whether they're satisfied or not. + When the hash argument is specified, this command prints a detailed explanation of the peer requirement corresponding to the hash and whether it is satisfied or not. - When used without arguments, this command lists all sets of peer requirements and the corresponding hash that can be used to get detailed information about a given set. + When used without arguments, this command lists all peer requirements and the corresponding hash that can be used to get detailed information about a given requirement. **Note:** A hash is a six-letter p-prefixed code that can be obtained from peer dependency warnings or from the list of all peer requirements (\`yarn explain peer-requirements\`). `, examples: [[ - `Explain the corresponding set of peer requirements for a hash`, + `Explain the corresponding peer requirement for a hash`, `$0 explain peer-requirements p1a4ed`, ], [ - `List all sets of peer requirements`, + `List all peer requirements`, `$0 explain peer-requirements`, ]], }); hash = Option.String({ + required: false, validator: t.cascade(t.isString(), [ t.matchesRegExp(/^p[0-9a-f]{5}$/), ]), @@ -46,20 +48,52 @@ export default class ExplainPeerRequirementsCommand extends BaseCommand { await project.applyLightResolution(); - return await explainPeerRequirements(this.hash, project, { - stdout: this.context.stdout, - }); + if (typeof this.hash !== `undefined`) { + return await explainPeerRequirement(this.hash, project, { + stdout: this.context.stdout, + }); + } else { + return await explainPeerRequirements(project, { + stdout: this.context.stdout, + }); + } } } -export async function explainPeerRequirements(peerRequirementsHash: string, project: Project, opts: {stdout: Writable}) { +export async function explainPeerRequirement(peerRequirementsHash: string, project: Project, opts: {stdout: Writable}) { + const root = project.peerRequirementNodes.get(peerRequirementsHash); + if (typeof root === `undefined`) + throw new Error(`No peerDependency requirements found for hash: "${peerRequirementsHash}"`); + + const seen = new Set(); + const makeTreeNode = (request: PeerRequestNode): treeUtils.TreeNode => { + if (seen.has(request.requester.locatorHash)) { + return { + value: formatUtils.tuple(formatUtils.Type.DEPENDENT, {locator: request.requester, descriptor: request.descriptor}), + children: request.children.size > 0 + ? [{value: formatUtils.tuple(formatUtils.Type.NO_HINT, `...`)}] + : [], + }; + } + + seen.add(request.requester.locatorHash); + return { + value: formatUtils.tuple(formatUtils.Type.DEPENDENT, {locator: request.requester, descriptor: request.descriptor}), + children: Object.fromEntries( + Array.from(request.children.values(), child => { + return [ + structUtils.stringifyLocator(child.requester), + makeTreeNode(child), + ]; + }), + ), + }; + }; + const warning = project.peerWarnings.find(warning => { return warning.hash === peerRequirementsHash; }); - if (typeof warning === `undefined`) - throw new Error(`No peerDependency requirements found for hash: "${peerRequirementsHash}"`); - const report = await StreamReport.start({ configuration: project.configuration, stdout: opts.stdout, @@ -67,87 +101,152 @@ export async function explainPeerRequirements(peerRequirementsHash: string, proj includePrefix: false, }, async report => { const Marks = formatUtils.mark(project.configuration); + const mark = warning ? Marks.Cross : Marks.Check; + + report.reportInfo(MessageName.UNNAMED, `Package ${ + formatUtils.pretty(project.configuration, root.subject, formatUtils.Type.LOCATOR) + } is requested to provide ${ + formatUtils.pretty(project.configuration, root.ident, formatUtils.Type.IDENT) + } by its descendants`); + + report.reportSeparator(); + + report.reportInfo(MessageName.UNNAMED, formatUtils.pretty(project.configuration, root.subject, formatUtils.Type.LOCATOR)); + treeUtils.emitTree({ + children: Object.fromEntries( + Array.from(root.requests.values(), request => { + return [ + structUtils.stringifyLocator(request.requester), + makeTreeNode(request), + ]; + }), + ), + }, { + configuration: project.configuration, + stdout: opts.stdout, + json: false, + }); - 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)})`); + report.reportSeparator(); + + if (root.provided.range === `missing:`) { + const problem = warning ? `` : ` , but all peer requests are optional`; + + report.reportInfo(MessageName.UNNAMED, `${mark} Package ${ + formatUtils.pretty(project.configuration, root.subject, formatUtils.Type.LOCATOR) + } does not provide ${ + formatUtils.pretty(project.configuration, root.ident, formatUtils.Type.IDENT) + }${problem}.`); + } else { + const providedLocatorHash = project.storedResolutions.get(root.provided.descriptorHash); + if (!providedLocatorHash) + throw new Error(`Assertion failed: Expected the descriptor to be registered`); + + const providedPackage = project.storedPackages.get(providedLocatorHash); + if (!providedPackage) + throw new Error(`Assertion failed: Expected the package to be registered`); + + report.reportInfo(MessageName.UNNAMED, `${mark} Package ${ + formatUtils.pretty(project.configuration, root.subject, formatUtils.Type.LOCATOR) + } provides ${ + formatUtils.pretty(project.configuration, root.ident, formatUtils.Type.IDENT) + } with version ${ + structUtils.prettyReference(project.configuration, providedPackage.version ?? `0.0.0`) + }, ${warning ? `which does not satisfy all requests.` : `which satisfies all requests`}`); + + if (warning?.type === PeerWarningType.NodeNotCompatible) { + if (warning.range) { + report.reportInfo(MessageName.UNNAMED, ` The combined requested range is ${formatUtils.pretty(project.configuration, warning.range, formatUtils.Type.RANGE)}`); + } else { + report.reportInfo(MessageName.UNNAMED, ` Unfortunately, the requested ranges have no overlap`); } + } + } + }); - 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`); + return report.exitCode(); +} - const mark = semverUtils.satisfiesWithPrereleases(warning.version, descriptor.range) - ? Marks.Check - : Marks.Cross; +export async function explainPeerRequirements(project: Project, opts: {stdout: Writable}) { + const report = await StreamReport.start({ + configuration: project.configuration, + stdout: opts.stdout, + includeFooter: false, + includePrefix: false, + }, async report => { + const Marks = formatUtils.mark(project.configuration); - report.reportInfo(null, ` ${mark} ${structUtils.prettyLocator(project.configuration, link)} (via ${structUtils.prettyRange(project.configuration, descriptor.range)})`); - } + const sorted = miscUtils.sortMap(project.peerRequirementNodes, [ + ([, requirement]) => structUtils.stringifyLocator(requirement.subject), + ([, requirement]) => structUtils.stringifyIdent(requirement.ident), + ]); + + for (const [,peerRequirement] of sorted.values()) { + if (!peerRequirement.root) + continue; + + const warning = project.peerWarnings.find(warning => { + return warning.hash === peerRequirement.hash; + }); + + const allRequests = [...structUtils.allPeerRequests(peerRequirement)]; + let andOthers; + if (allRequests.length > 2) + andOthers = ` and ${allRequests.length - 1} other dependencies`; + else if (allRequests.length === 2) + andOthers = ` and 1 other dependency`; + else + andOthers = ``; + + if (peerRequirement.provided.range !== `missing:`) { + const providedResolution = project.storedResolutions.get(peerRequirement.provided.descriptorHash); + if (!providedResolution) + throw new Error(`Assertion failed: Expected the resolution to have been registered`); + + const providedPkg = project.storedPackages.get(providedResolution); + if (!providedPkg) + throw new Error(`Assertion failed: Expected the provided package to have been registered`); + + const message = `${ + formatUtils.pretty(project.configuration, peerRequirement.hash, formatUtils.Type.CODE) + } → ${ + warning ? Marks.Cross : Marks.Check + } ${ + structUtils.prettyLocator(project.configuration, peerRequirement.subject) + } provides ${ + structUtils.prettyLocator(project.configuration, providedPkg) + } to ${ + structUtils.prettyLocator(project.configuration, allRequests[0]!.requester) + }${andOthers}`; + + if (warning) { + report.reportWarning(MessageName.UNNAMED, message); + } else { + report.reportInfo(MessageName.UNNAMED, message); } - - 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)}`); - } + } else { + const message = `${ + formatUtils.pretty(project.configuration, peerRequirement.hash, formatUtils.Type.CODE) + } → ${ + warning ? Marks.Cross : Marks.Check + } ${ + structUtils.prettyLocator(project.configuration, peerRequirement.subject) + } doesn't provide ${ + structUtils.prettyIdent(project.configuration, peerRequirement.ident) + } to ${ + structUtils.prettyLocator(project.configuration, allRequests[0]!.requester) + }${andOthers}`; + + if (warning) { + report.reportWarning(MessageName.UNNAMED, message); + } else { + report.reportInfo(MessageName.UNNAMED, message); } - } 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; + } } }); + return report.exitCode(); } diff --git a/packages/yarnpkg-core/sources/Project.ts b/packages/yarnpkg-core/sources/Project.ts index 2e07b8690766..dee46ea2aea6 100644 --- a/packages/yarnpkg-core/sources/Project.ts +++ b/packages/yarnpkg-core/sources/Project.ts @@ -19,7 +19,7 @@ import {Installer, BuildDirective, BuildDirectiveType, InstallStatus} from './ import {LegacyMigrationResolver} from './LegacyMigrationResolver'; import {Linker, LinkOptions} from './Linker'; import {LockfileResolver} from './LockfileResolver'; -import {DependencyMeta, Manifest} from './Manifest'; +import {DependencyMeta, Manifest, type PeerDependencyMeta} from './Manifest'; import {MessageName} from './MessageName'; import {MultiResolver} from './MultiResolver'; import {Report, ReportError} from './Report'; @@ -166,6 +166,25 @@ type RestoreInstallStateOpts = { // Just a type that's the union of all the fields declared in `INSTALL_STATE_FIELDS` type InstallState = Pick; +export type PeerRequestNode = { + requester: Locator; + descriptor: Descriptor; + meta: PeerDependencyMeta | undefined; + + children: Map; +}; + +export type PeerRequirementNode = { + subject: Locator; + ident: Ident; + provided: Descriptor; + root: boolean; + + requests: Map; + + hash: string; +}; + export type PeerRequirement = { subject: LocatorHash; requested: Ident; @@ -176,7 +195,8 @@ export type PeerRequirement = { export enum PeerWarningType { NotProvided, NotCompatible, - NotCompatibleAggregate, + NodeNotProvided, + NodeNotCompatible, } export type PeerWarning = { @@ -194,13 +214,13 @@ export type PeerWarning = { hash: string; requirementCount: number; } | { - type: PeerWarningType.NotCompatibleAggregate; - subject: Locator; - requested: Ident; - dependents: Map; - requesters: Map; - links: Map; - version: string; + type: PeerWarningType.NodeNotProvided; + node: PeerRequirementNode; + hash: string; +} | { + type: PeerWarningType.NodeNotCompatible; + node: PeerRequirementNode; + range: string | null; hash: string; }; @@ -256,6 +276,7 @@ export class Project { */ public peerRequirements: Map = new Map(); public peerWarnings: Array = []; + public peerRequirementNodes: Map = new Map(); /** * Contains whatever data the linkers (cf `Linker.ts`) want to persist @@ -997,6 +1018,7 @@ export class Project { const accessibleLocators = new Set(); const peerRequirements: Project['peerRequirements'] = new Map(); const peerWarnings: Project['peerWarnings'] = []; + const peerRequirementNodes: Project['peerRequirementNodes'] = new Map(); applyVirtualResolutionMutations({ project: this, @@ -1006,6 +1028,7 @@ export class Project { optionalBuilds, peerRequirements, peerWarnings, + peerRequirementNodes, allDescriptors, allResolutions, @@ -1066,6 +1089,7 @@ export class Project { this.optionalBuilds = optionalBuilds; this.peerRequirements = peerRequirements; this.peerWarnings = peerWarnings; + this.peerRequirementNodes = peerRequirementNodes; } async fetchEverything({cache, report, fetcher: userFetcher, mode, persistProject = true}: InstallOptions) { @@ -2150,6 +2174,7 @@ function applyVirtualResolutionMutations({ optionalBuilds = new Set(), peerRequirements = new Map(), peerWarnings = [], + peerRequirementNodes = new Map(), volatileDescriptors = new Set(), }: { project: Project; @@ -2162,6 +2187,7 @@ function applyVirtualResolutionMutations({ optionalBuilds?: Set; peerRequirements?: Project['peerRequirements']; peerWarnings?: Project['peerWarnings']; + peerRequirementNodes?: Project['peerRequirementNodes']; volatileDescriptors?: Set; }) { const virtualStack = new Map(); @@ -2170,18 +2196,11 @@ function applyVirtualResolutionMutations({ const allIdents = new Map(); // We'll be keeping track of all virtual descriptors; once they have all - // been generated we'll check whether they can be consolidated into one. + // been generated we'll check whether they can be deduplicated into one. const allVirtualInstances = new Map>(); const allVirtualDependents = new Map>(); - // First key is the first package that requests the peer dependency. Second - // key is the name of the package in the peer dependency. Value is the list - // of all packages that extend the original peer requirement. - const peerDependencyLinks: Map>> = new Map(); - - // We keep track on which package depend on which other package with peer - // dependencies; this way we can emit warnings for them later on. - const peerDependencyDependents = new Map>(); + const allPeerRequests = new Map>(); // We must keep a copy of the workspaces original dependencies, because they // may be overridden during the virtual package resolution - cf Dragon Test #5 @@ -2223,18 +2242,18 @@ function applyVirtualResolutionMutations({ return pkg; }; - const resolvePeerDependencies = (parentDescriptor: Descriptor, parentLocator: Locator, peerSlots: Map, {top, optional}: {top: LocatorHash, optional: boolean}) => { + const resolvePeerDependencies = (parentDescriptor: Descriptor, parentLocator: Locator, parentPeerRequests: Map, {top, optional}: {top: LocatorHash, optional: boolean}) => { if (resolutionStack.length > 1000) reportStackOverflow(); resolutionStack.push(parentLocator); - const result = resolvePeerDependenciesImpl(parentDescriptor, parentLocator, peerSlots, {top, optional}); + const result = resolvePeerDependenciesImpl(parentDescriptor, parentLocator, parentPeerRequests, {top, optional}); resolutionStack.pop(); return result; }; - const resolvePeerDependenciesImpl = (parentDescriptor: Descriptor, parentLocator: Locator, peerSlots: Map, {top, optional}: {top: LocatorHash, optional: boolean}) => { + const resolvePeerDependenciesImpl = (parentDescriptor: Descriptor, parentLocator: Locator, parentPeerRequests: Map, {top, optional}: {top: LocatorHash, optional: boolean}) => { if (!optional) optionalBuilds.delete(parentLocator.locatorHash); @@ -2248,17 +2267,13 @@ function applyVirtualResolutionMutations({ throw new Error(`Assertion failed: The package (${structUtils.prettyLocator(project.configuration, parentLocator)}) should have been registered`); const newVirtualInstances: Array<[Locator, Descriptor, Package]> = []; + const parentPeerRequirements = new Map(); const firstPass = []; const secondPass = []; const thirdPass = []; const fourthPass = []; - // During this first pass we virtualize the descriptors. This allows us - // to reference them from their sibling without being order-dependent, - // which is required to solve cases where packages with peer dependencies - // have peer dependencies themselves. - for (const descriptor of Array.from(parentPackage.dependencies.values())) { // We shouldn't virtualize the package if it was obtained through a peer // dependency (which can't be the case for workspaces when resolved @@ -2288,11 +2303,6 @@ function applyVirtualResolutionMutations({ const resolution = allResolutions.get(descriptor.descriptorHash); if (!resolution) - // Note that we can't use `getPackageFromDescriptor` (defined below, - // because when doing the initial tree building right after loading the - // project it's possible that we get some entries that haven't been - // registered into the lockfile yet - for example when the user has - // manually changed the package.json dependencies) throw new Error(`Assertion failed: The resolution (${structUtils.prettyDescriptor(project.configuration, descriptor)}) should have been registered`); const pkg = originalWorkspaceDefinitions.get(resolution) || allPackages.get(resolution); @@ -2308,9 +2318,9 @@ function applyVirtualResolutionMutations({ let virtualizedPackage: Package; const missingPeerDependencies = new Set(); + const peerRequests = new Map(); - let nextPeerSlots: Map; - + // In the first pass we virtualize the requester's descriptor and package firstPass.push(() => { virtualizedDescriptor = structUtils.virtualizeDescriptor(descriptor, parentLocator.locatorHash); virtualizedPackage = structUtils.virtualizePackage(pkg, parentLocator.locatorHash); @@ -2327,49 +2337,80 @@ function applyVirtualResolutionMutations({ newVirtualInstances.push([pkg, virtualizedDescriptor, virtualizedPackage]); }); + // In the second pass we resolve the peer requests to their provision. + // This must be done in a separate pass since the provided package may + // itself be a virtualized sibling package. secondPass.push(() => { - nextPeerSlots = new Map(); + allPeerRequests.set(virtualizedPackage.locatorHash, peerRequests); + + for (const peerDescriptor of virtualizedPackage.peerDependencies.values()) { + const peerRequirement = miscUtils.getFactoryWithDefault(parentPeerRequirements, peerDescriptor.identHash, (): PeerRequirementNode => { + let parentRequest = parentPeerRequests.get(peerDescriptor.identHash) ?? null; - for (const peerRequest of virtualizedPackage.peerDependencies.values()) { - let peerDescriptor = parentPackage.dependencies.get(peerRequest.identHash); + // 1. Try to resolve the peer request with parent's dependencies (siblings) + let peerProvision = parentPackage.dependencies.get(peerDescriptor.identHash); - if (!peerDescriptor && structUtils.areIdentsEqual(parentLocator, peerRequest)) { - // If the parent isn't installed under an alias we can skip unnecessary steps - if (parentDescriptor.identHash === parentLocator.identHash) { - peerDescriptor = parentDescriptor; - } else { - peerDescriptor = structUtils.makeDescriptor(parentLocator, parentDescriptor.range); + // 2. Try to resolve the peer request with the parent itself + if (!peerProvision && structUtils.areIdentsEqual(parentLocator, peerDescriptor)) { + // If the parent isn't installed under an alias we can skip unnecessary steps + if (parentDescriptor.identHash === parentLocator.identHash) { + peerProvision = parentDescriptor; + } else { + peerProvision = structUtils.makeDescriptor(parentLocator, parentDescriptor.range); - allDescriptors.set(peerDescriptor.descriptorHash, peerDescriptor); - allResolutions.set(peerDescriptor.descriptorHash, parentLocator.locatorHash); + allDescriptors.set(peerProvision.descriptorHash, peerProvision); + allResolutions.set(peerProvision.descriptorHash, parentLocator.locatorHash); - volatileDescriptors.delete(peerDescriptor.descriptorHash); + volatileDescriptors.delete(peerProvision.descriptorHash); + + parentRequest = null; + } } - } - // If the peerRequest isn't provided by the parent then fall back to dependencies - if ((!peerDescriptor || peerDescriptor.range === `missing:`) && virtualizedPackage.dependencies.has(peerRequest.identHash)) { - virtualizedPackage.peerDependencies.delete(peerRequest.identHash); + if (!peerProvision) + peerProvision = structUtils.makeDescriptor(peerDescriptor, `missing:`); + + return { + subject: parentLocator, + ident: peerDescriptor, + provided: peerProvision, + root: !parentRequest, + + requests: new Map(), + + hash: `p${hashUtils.makeHash(parentLocator.locatorHash, peerDescriptor.identHash).slice(0, 5)}`, + }; + }); + + const peerProvision = peerRequirement.provided; + // 3. Try package's own dependencies (peer-with-default) + // This is done outside of the factory above because this resolution is not sharable between sibling virtuals + if (peerProvision.range === `missing:` && virtualizedPackage.dependencies.has(peerDescriptor.identHash)) { + virtualizedPackage.peerDependencies.delete(peerDescriptor.identHash); continue; } - if (!peerDescriptor) - peerDescriptor = structUtils.makeDescriptor(peerRequest, `missing:`); + peerRequests.set(peerDescriptor.identHash, { + requester: virtualizedPackage, + descriptor: peerDescriptor, + meta: virtualizedPackage.peerDependenciesMeta.get(structUtils.stringifyIdent(peerDescriptor)), + + children: new Map(), + }); - virtualizedPackage.dependencies.set(peerDescriptor.identHash, peerDescriptor); + virtualizedPackage.dependencies.set(peerDescriptor.identHash, peerProvision); // Need to track when a virtual descriptor is set as a dependency in case - // the descriptor will be consolidated. - if (structUtils.isVirtualDescriptor(peerDescriptor)) { - const dependents = miscUtils.getSetWithDefault(allVirtualDependents, peerDescriptor.descriptorHash); + // the descriptor will be deduplicated. + if (structUtils.isVirtualDescriptor(peerProvision)) { + const dependents = miscUtils.getSetWithDefault(allVirtualDependents, peerProvision.descriptorHash); dependents.add(virtualizedPackage.locatorHash); } - allIdents.set(peerDescriptor.identHash, peerDescriptor); - if (peerDescriptor.range === `missing:`) - missingPeerDependencies.add(peerDescriptor.identHash); - - nextPeerSlots.set(peerRequest.identHash, peerSlots.get(peerRequest.identHash) ?? virtualizedPackage.locatorHash); + allIdents.set(peerProvision.identHash, peerProvision); + if (peerProvision.range === `missing:`) { + missingPeerDependencies.add(peerProvision.identHash); + } } // Since we've had to add new dependencies we need to sort them all over again @@ -2378,6 +2419,13 @@ function applyVirtualResolutionMutations({ })); }); + // Between the second and third passes, the deduplication passes happen. + + // In the third pass, we recurse to resolve the peer request for the + // virtual package, but only if it is not deduplicated. If the virtual + // package is deduplicated, this would already have been done. This must + // be done after the deduplication passes as otherwise it could lead to + // infinite recursion when dealing with circular dependencies. thirdPass.push(() => { if (!allPackages.has(virtualizedPackage.locatorHash)) return; @@ -2395,15 +2443,13 @@ function applyVirtualResolutionMutations({ const next = typeof current !== `undefined` ? current + 1 : 1; virtualStack.set(pkg.locatorHash, next); - resolvePeerDependencies(virtualizedDescriptor, virtualizedPackage, nextPeerSlots, {top, optional: isOptional}); + resolvePeerDependencies(virtualizedDescriptor, virtualizedPackage, peerRequests, {top, optional: isOptional}); virtualStack.set(pkg.locatorHash, next - 1); }); + // In the fourth pass, we register information about the peer requirement + // and peer request trees, using the post-deduplication information. fourthPass.push(() => { - // Regardless of whether the initial virtualized package got deduped - // or not, we now register that *this* package is now a dependent on - // whatever its peer dependencies have been resolved to. We'll later - // use this information to generate warnings. const finalDescriptor = parentPackage.dependencies.get(descriptor.identHash); if (typeof finalDescriptor === `undefined`) throw new Error(`Assertion failed: Expected the peer dependency to have been turned into a dependency`); @@ -2412,19 +2458,25 @@ function applyVirtualResolutionMutations({ if (typeof finalResolution === `undefined`) throw new Error(`Assertion failed: Expected the descriptor to be registered`); - miscUtils.getSetWithDefault(peerDependencyDependents, finalResolution).add(parentLocator.locatorHash); - - if (!allPackages.has(virtualizedPackage.locatorHash)) - return; + const finalPeerRequests = allPeerRequests.get(finalResolution); + if (typeof finalPeerRequests === `undefined`) + throw new Error(`Assertion failed: Expected the peer requests to be registered`); - for (const descriptor of virtualizedPackage.peerDependencies.values()) { - const root = nextPeerSlots.get(descriptor.identHash); - if (typeof root === `undefined`) - throw new Error(`Assertion failed: Expected the peer dependency ident to be registered`); + for (const peerRequirement of parentPeerRequirements.values()) { + const peerRequest = finalPeerRequests.get(peerRequirement.ident.identHash); + if (!peerRequest) + continue; - miscUtils.getArrayWithDefault(miscUtils.getMapWithDefault(peerDependencyLinks, root), structUtils.stringifyIdent(descriptor)).push(virtualizedPackage.locatorHash); + peerRequirement.requests.set(finalDescriptor.descriptorHash, peerRequest); + peerRequirementNodes.set(peerRequirement.hash, peerRequirement); + if (!peerRequirement.root) { + parentPeerRequests.get(peerRequirement.ident.identHash)?.children.set(finalDescriptor.descriptorHash, peerRequest); + } } + if (!allPackages.has(virtualizedPackage.locatorHash)) + return; + for (const missingPeerDependency of missingPeerDependencies) { virtualizedPackage.dependencies.delete(missingPeerDependency); } @@ -2496,6 +2548,12 @@ function applyVirtualResolutionMutations({ pkg.dependencies.set(virtualDescriptor.identHash, masterDescriptor); } + + for (const peerRequirement of parentPeerRequirements.values()) { + if (peerRequirement.provided.descriptorHash === virtualDescriptor.descriptorHash) { + peerRequirement.provided = masterDescriptor; + } + } } } while (!stable); @@ -2511,197 +2569,193 @@ function applyVirtualResolutionMutations({ resolvePeerDependencies(workspace.anchoredDescriptor, locator, new Map(), {top: locator.locatorHash, optional: false}); } - const aggregatedInvalidPeerDependencyWarnings = new Map>(); - - for (const [rootHash, dependents] of peerDependencyDependents) { - const root = allPackages.get(rootHash); - if (typeof root === `undefined`) - throw new Error(`Assertion failed: Expected the root to be registered`); - - // We retrieve the set of packages that provide complementary peer - // dependencies to the one already offered by our root package, and to - // whom other package. - // - // We simply skip if the record doesn't exist because a package may not - // have any records if it didn't contribute any new peer (it only exists - // if the package has at least one peer that isn't listed by its parent - // packages). - // - const rootLinks = peerDependencyLinks.get(rootHash); - if (typeof rootLinks === `undefined`) + for (const requirement of peerRequirementNodes.values()) { + // Only generate warnings on root requirements + if (!requirement.root) continue; - for (const dependentHash of dependents) { - const dependent = allPackages.get(dependentHash); - - // The package may have been pruned during a deduplication - if (typeof dependent === `undefined`) - continue; - - // We don't care about warning about incomplete transitive peer - // dependencies; in practice, there are just too many of them. - if (!project.tryWorkspaceByLocator(dependent)) - continue; - - for (const [identStr, linkHashes] of rootLinks) { - const ident = structUtils.parseIdent(identStr); - - // This dependent may have a peer dep itself, in which case it's not - // the true root, and we can ignore it - if (dependent.peerDependencies.has(ident.identHash)) - continue; + const dependent = allPackages.get(requirement.subject.locatorHash); - const hash = `p${hashUtils.makeHash(dependentHash, identStr, rootHash).slice(0, 5)}`; - - peerRequirements.set(hash, { - subject: dependentHash, - requested: ident, - rootRequester: rootHash, - allRequesters: linkHashes, - }); + // The package may have been pruned during a deduplication + if (typeof dependent === `undefined`) + continue; - // Note: this can be undefined when the peer dependency isn't provided at all - const resolvedDescriptor = root.dependencies.get(ident.identHash); + // For backwards-compatibility + // TODO: Remove for next major + for (const peerRequest of requirement.requests.values()) { + const hash = `p${hashUtils.makeHash(requirement.subject.locatorHash, structUtils.stringifyIdent(requirement.ident), peerRequest.requester.locatorHash).slice(0, 5)}`; - if (typeof resolvedDescriptor !== `undefined`) { - const peerResolution = getPackageFromDescriptor(resolvedDescriptor); - const peerVersion = peerResolution.version ?? `0.0.0`; + peerRequirements.set(hash, { + subject: requirement.subject.locatorHash, + requested: requirement.ident, + rootRequester: peerRequest.requester.locatorHash, + allRequesters: Array.from(structUtils.allPeerRequests(peerRequest), request => request.requester.locatorHash), + }); + } - const ranges = new Set(); + const allRequests = [...structUtils.allPeerRequests(requirement)]; - for (const linkHash of linkHashes) { - const link = allPackages.get(linkHash); - if (typeof link === `undefined`) - throw new Error(`Assertion failed: Expected the link to be registered`); + if (requirement.provided.range !== `missing:`) { + const peerPackage = getPackageFromDescriptor(requirement.provided); + const peerVersion = peerPackage.version ?? `0.0.0`; - const peerDependency = link.peerDependencies.get(ident.identHash); - if (typeof peerDependency === `undefined`) - throw new Error(`Assertion failed: Expected the ident to be registered`); + const resolveWorkspaceRange = (range: string) => { + if (range.startsWith(WorkspaceResolver.protocol)) { + if (!project.tryWorkspaceByLocator(peerPackage)) + return null; - ranges.add(peerDependency.range); + range = range.slice(WorkspaceResolver.protocol.length); + if (range === `^` || range === `~`) { + range = `*`; } + } - const satisfiesAll = [...ranges].every(range => { - if (range.startsWith(WorkspaceResolver.protocol)) { - if (!project.tryWorkspaceByLocator(peerResolution)) - return false; + return range; + }; - range = range.slice(WorkspaceResolver.protocol.length); - if (range === `^` || range === `~`) { - range = `*`; - } - } + let satisfiesAll = true; + for (const peerRequest of allRequests) { + const range = resolveWorkspaceRange(peerRequest.descriptor.range); + if (range === null) { + satisfiesAll = false; + continue; + } - return semverUtils.satisfiesWithPrereleases(peerVersion, range); + if (!semverUtils.satisfiesWithPrereleases(peerVersion, range)) { + satisfiesAll = false; + + // For backwards-compatibility + // TODO: Remove for next major + const hash = `p${hashUtils.makeHash(requirement.subject.locatorHash, structUtils.stringifyIdent(requirement.ident), peerRequest.requester.locatorHash).slice(0, 5)}`; + + peerWarnings.push({ + type: PeerWarningType.NotCompatible, + subject: dependent, + requested: requirement.ident, + requester: peerRequest.requester, + version: peerVersion, + hash, + requirementCount: allRequests.length, }); + } + } - if (!satisfiesAll) { - const aggregatedWarning = miscUtils.getFactoryWithDefault(aggregatedInvalidPeerDependencyWarnings, peerResolution.locatorHash, () => ({ - type: PeerWarningType.NotCompatibleAggregate as const, - requested: ident, - subject: peerResolution, - dependents: new Map(), - requesters: new Map(), - links: new Map(), - version: peerVersion, - hash: `p${peerResolution.locatorHash.slice(0, 5)}`, - })); - - aggregatedWarning.dependents.set(dependent.locatorHash, dependent); - aggregatedWarning.requesters.set(root.locatorHash, root); - - for (const linkHash of linkHashes) - aggregatedWarning.links.set(linkHash, allPackages.get(linkHash)!); + if (!satisfiesAll) { + const allRanges = allRequests.map(peerRequest => resolveWorkspaceRange(peerRequest.descriptor.range)); - peerWarnings.push({ - type: PeerWarningType.NotCompatible, - subject: dependent, - requested: ident, - requester: root, - version: peerVersion, - hash, - requirementCount: linkHashes.length, - }); - } - } else { - const peerDependencyMeta = root.peerDependenciesMeta.get(identStr); - - if (!peerDependencyMeta?.optional) { - peerWarnings.push({ - type: PeerWarningType.NotProvided, - subject: dependent, - requested: ident, - requester: root, - hash, - }); - } + peerWarnings.push({ + type: PeerWarningType.NodeNotCompatible, + node: requirement, + range: allRanges.includes(null) + ? null + : semverUtils.simplifyRanges(allRanges as Array), + hash: requirement.hash, + }); + } + } else { + let satisfiesAll = true; + for (const peerRequest of allRequests) { + if (!peerRequest.meta?.optional) { + satisfiesAll = false; + + // For backwards-compatibility + // TODO: Remove for next major + const hash = `p${hashUtils.makeHash(requirement.subject.locatorHash, structUtils.stringifyIdent(requirement.ident), peerRequest.requester.locatorHash).slice(0, 5)}`; + + peerWarnings.push({ + type: PeerWarningType.NotProvided, + subject: dependent, + requested: requirement.ident, + requester: peerRequest.requester, + hash, + }); } } + if (!satisfiesAll) { + peerWarnings.push({ + type: PeerWarningType.NodeNotProvided, + node: requirement, + hash: requirement.hash, + }); + } } } - - peerWarnings.push(...aggregatedInvalidPeerDependencyWarnings.values()); } function emitPeerDependencyWarnings(project: Project, report: Report) { - const warningsByType = miscUtils.groupBy(project.peerWarnings, `type`); + const incompatibleWarnings: Array = []; + const missingWarnings: Array = []; + let hasTransitiveWarnings = false; - const notCompatibleAggregateWarnings = warningsByType[PeerWarningType.NotCompatibleAggregate]?.map(warning => { - const allRanges = Array.from(warning.links.values(), locator => { - const pkg = project.storedPackages.get(locator.locatorHash); - if (typeof pkg === `undefined`) + for (const warning of project.peerWarnings) { + if (warning.type === PeerWarningType.NotCompatible || warning.type === PeerWarningType.NotProvided) + continue; + + if (!project.tryWorkspaceByLocator(warning.node.subject)) { + hasTransitiveWarnings = true; + continue; + } + + if (warning.type === PeerWarningType.NodeNotCompatible) { + const peerLocatorHash = project.storedResolutions.get(warning.node.provided.descriptorHash); + if (typeof peerLocatorHash === `undefined`) + throw new Error(`Assertion failed: Expected the descriptor to be registered`); + + const peerPackage = project.storedPackages.get(peerLocatorHash); + if (typeof peerPackage === `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`); + const otherPackages = [...structUtils.allPeerRequests(warning.node)].length > 1 + ? `and other dependencies request` + : `requests`; + + const rangeDescription = warning.range + ? structUtils.prettyRange(project.configuration, warning.range) + : formatUtils.pretty(project.configuration, `but they have non-overlapping ranges!`, `redBright`); + + incompatibleWarnings.push(`${ + structUtils.prettyIdent(project.configuration, warning.node.ident) + } is listed by your project with version ${ + structUtils.prettyReference(project.configuration, peerPackage.version ?? `0.0.0`) + } (${ + formatUtils.pretty(project.configuration, warning.hash, formatUtils.Type.CODE) + }), which doesn't satisfy what ${ + structUtils.prettyIdent(project.configuration, warning.node.requests.values().next().value.requester) + } ${otherPackages} (${rangeDescription}).`); + } - return peerDependency.range; - }); + if (warning.type === PeerWarningType.NodeNotProvided) { + const otherPackages = warning.node.requests.size > 1 + ? ` and other dependencies` + : ``; - const andDescendants = warning.links.size > 1 - ? `and other dependencies request` - : `requests`; - - const resolvedRange = semverUtils.simplifyRanges(allRanges); - const rangeDescription = resolvedRange - ? structUtils.prettyRange(project.configuration, resolvedRange) - : formatUtils.pretty(project.configuration, `but they have non-overlapping ranges!`, `redBright`); - - return `${ - structUtils.prettyIdent(project.configuration, warning.requested) - } is listed by your project with version ${ - structUtils.prettyReference(project.configuration, warning.version) - }, which doesn't satisfy what ${ - structUtils.prettyIdent(project.configuration, warning.requesters.values().next().value) - } (${formatUtils.pretty(project.configuration, warning.hash, formatUtils.Type.CODE)}) ${andDescendants} (${rangeDescription}).`; - }) ?? []; - - const omittedWarnings = warningsByType[PeerWarningType.NotProvided]?.map(warning => { - return `${ - structUtils.prettyLocator(project.configuration, warning.subject) - } doesn't provide ${ - structUtils.prettyIdent(project.configuration, warning.requested) - } (${ - formatUtils.pretty(project.configuration, warning.hash, formatUtils.Type.CODE) - }), requested by ${ - structUtils.prettyIdent(project.configuration, warning.requester) - }.`; - }) ?? []; + missingWarnings.push(`${ + structUtils.prettyLocator(project.configuration, warning.node.subject) + } doesn't provide ${ + structUtils.prettyIdent(project.configuration, warning.node.ident) + } (${ + formatUtils.pretty(project.configuration, warning.hash, formatUtils.Type.CODE) + }), requested by ${ + structUtils.prettyIdent(project.configuration, warning.node.requests.values().next().value.requester) + }${otherPackages}.`); + } + } report.startSectionSync({ reportFooter: () => { - report.reportWarning(MessageName.EXPLAIN_PEER_DEPENDENCIES_CTA, `Some peer dependencies are incorrectly met; run ${formatUtils.pretty(project.configuration, `yarn explain peer-requirements `, formatUtils.Type.CODE)} for details, where ${formatUtils.pretty(project.configuration, ``, formatUtils.Type.CODE)} is the six-letter p-prefixed code.`); + report.reportWarning(MessageName.EXPLAIN_PEER_DEPENDENCIES_CTA, `Some peer dependencies are incorrectly met by your project; run ${formatUtils.pretty(project.configuration, `yarn explain peer-requirements `, formatUtils.Type.CODE)} for details, where ${formatUtils.pretty(project.configuration, ``, formatUtils.Type.CODE)} is the six-letter p-prefixed code.`); }, skipIfEmpty: true, }, () => { - for (const warning of miscUtils.sortMap(notCompatibleAggregateWarnings, line => formatUtils.stripAnsi(line))) + for (const warning of miscUtils.sortMap(incompatibleWarnings, line => formatUtils.stripAnsi(line))) report.reportWarning(MessageName.INCOMPATIBLE_PEER_DEPENDENCY, warning); - for (const warning of miscUtils.sortMap(omittedWarnings, line => formatUtils.stripAnsi(line))) { + for (const warning of miscUtils.sortMap(missingWarnings, line => formatUtils.stripAnsi(line))) { report.reportWarning(MessageName.MISSING_PEER_DEPENDENCY, warning); } }); + + if (hasTransitiveWarnings) { + report.reportWarning(MessageName.EXPLAIN_PEER_DEPENDENCIES_CTA, `Some peer dependencies are incorrectly met by dependencies; run ${formatUtils.pretty(project.configuration, `yarn explain peer-requirements`, formatUtils.Type.CODE)} for details.`); + } } diff --git a/packages/yarnpkg-core/sources/structUtils.ts b/packages/yarnpkg-core/sources/structUtils.ts index b47708ebed8a..1bf0489639b9 100644 --- a/packages/yarnpkg-core/sources/structUtils.ts +++ b/packages/yarnpkg-core/sources/structUtils.ts @@ -1,17 +1,18 @@ -import {Filename, PortablePath} from '@yarnpkg/fslib'; -import querystring from 'querystring'; -import semver from 'semver'; -import {makeParser} from 'tinylogic'; - -import {Configuration} from './Configuration'; -import {Workspace} from './Workspace'; -import * as formatUtils from './formatUtils'; -import * as hashUtils from './hashUtils'; -import * as miscUtils from './miscUtils'; -import * as nodeUtils from './nodeUtils'; -import * as structUtils from './structUtils'; -import {IdentHash, DescriptorHash, LocatorHash} from './types'; -import {Ident, Descriptor, Locator, Package} from './types'; +import {Filename, PortablePath} from '@yarnpkg/fslib'; +import querystring from 'querystring'; +import semver from 'semver'; +import {makeParser} from 'tinylogic'; + +import {Configuration} from './Configuration'; +import type {PeerRequestNode, PeerRequirementNode} from './Project'; +import {Workspace} from './Workspace'; +import * as formatUtils from './formatUtils'; +import * as hashUtils from './hashUtils'; +import * as miscUtils from './miscUtils'; +import * as nodeUtils from './nodeUtils'; +import * as structUtils from './structUtils'; +import {IdentHash, DescriptorHash, LocatorHash} from './types'; +import {Ident, Descriptor, Locator, Package} from './types'; const VIRTUAL_PROTOCOL = `virtual:`; const VIRTUAL_ABBREVIATE = 5; @@ -879,3 +880,23 @@ export function isPackageCompatible(pkg: Package, architectures: nodeUtils.Archi return supported ? supported.includes(value) : true; }); } + +export function allPeerRequests(root: PeerRequestNode | PeerRequirementNode): Iterable { + const requests = new Set(); + + if (`children` in root) { + requests.add(root); + } else { + for (const request of root.requests.values()) { + requests.add(request); + } + } + + for (const request of requests) { + for (const child of request.children.values()) { + requests.add(child); + } + } + + return requests; +}