From 81a69ed9e50c0598577746435c2224f0b507f072 Mon Sep 17 00:00:00 2001 From: Clement Yan Date: Tue, 2 Apr 2024 23:23:02 +0800 Subject: [PATCH 1/9] feat: Rework peer requests and peer warnings --- packages/yarnpkg-core/sources/Project.ts | 477 +++++++++++-------- packages/yarnpkg-core/sources/structUtils.ts | 49 +- 2 files changed, 301 insertions(+), 225 deletions(-) diff --git a/packages/yarnpkg-core/sources/Project.ts b/packages/yarnpkg-core/sources/Project.ts index 2e07b8690766..2e473c2cc9f2 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(); @@ -2174,14 +2200,7 @@ function applyVirtualResolutionMutations({ 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,6 +2267,7 @@ 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 = []; @@ -2309,7 +2329,7 @@ function applyVirtualResolutionMutations({ const missingPeerDependencies = new Set(); - let nextPeerSlots: Map; + const peerRequests = new Map(); firstPass.push(() => { virtualizedDescriptor = structUtils.virtualizeDescriptor(descriptor, parentLocator.locatorHash); @@ -2328,48 +2348,76 @@ function applyVirtualResolutionMutations({ }); secondPass.push(() => { - nextPeerSlots = new Map(); + allPeerRequests.set(virtualizedPackage.locatorHash, peerRequests); - for (const peerRequest of virtualizedPackage.peerDependencies.values()) { - let peerDescriptor = parentPackage.dependencies.get(peerRequest.identHash); + for (const peerDescriptor of virtualizedPackage.peerDependencies.values()) { + const peerRequirement = miscUtils.getFactoryWithDefault(parentPeerRequirements, peerDescriptor.identHash, (): PeerRequirementNode => { + let parentRequest = parentPeerRequests.get(peerDescriptor.identHash) ?? null; - 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); + // 1. Try to resolve the peer request with parent's dependencies (siblings) + let peerProvision = parentPackage.dependencies.get(peerDescriptor.identHash); - allDescriptors.set(peerDescriptor.descriptorHash, peerDescriptor); - allResolutions.set(peerDescriptor.descriptorHash, parentLocator.locatorHash); + // 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); - volatileDescriptors.delete(peerDescriptor.descriptorHash); + allDescriptors.set(peerProvision.descriptorHash, peerProvision); + allResolutions.set(peerProvision.descriptorHash, parentLocator.locatorHash); + + 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); + 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 @@ -2395,7 +2443,7 @@ 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); }); @@ -2412,19 +2460,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 +2550,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 +2571,192 @@ 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()) { + if (!requirement.root) continue; - for (const dependentHash of dependents) { - const dependent = allPackages.get(dependentHash); + const dependent = allPackages.get(requirement.subject.locatorHash); - // 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 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; +} From dbe9b2366293b362df4cd2680e85748ab1d307e1 Mon Sep 17 00:00:00 2001 From: Clement Yan Date: Tue, 2 Apr 2024 23:56:15 +0800 Subject: [PATCH 2/9] refactor: Tidy up comments In the peer resolution code, many comments became outdated or out-of-place over many refactors and code movements. We take this opportunity to fix those and add new comments for the new logic --- packages/yarnpkg-core/sources/Project.ts | 33 ++++++++++++------------ 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/yarnpkg-core/sources/Project.ts b/packages/yarnpkg-core/sources/Project.ts index 2e473c2cc9f2..dee46ea2aea6 100644 --- a/packages/yarnpkg-core/sources/Project.ts +++ b/packages/yarnpkg-core/sources/Project.ts @@ -2196,7 +2196,7 @@ 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>(); @@ -2274,11 +2274,6 @@ function applyVirtualResolutionMutations({ 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 @@ -2308,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); @@ -2328,9 +2318,9 @@ function applyVirtualResolutionMutations({ let virtualizedPackage: Package; const missingPeerDependencies = new Set(); - const peerRequests = new 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); @@ -2347,6 +2337,9 @@ 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(() => { allPeerRequests.set(virtualizedPackage.locatorHash, peerRequests); @@ -2408,7 +2401,7 @@ function applyVirtualResolutionMutations({ 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. + // the descriptor will be deduplicated. if (structUtils.isVirtualDescriptor(peerProvision)) { const dependents = miscUtils.getSetWithDefault(allVirtualDependents, peerProvision.descriptorHash); dependents.add(virtualizedPackage.locatorHash); @@ -2426,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; @@ -2447,11 +2447,9 @@ function applyVirtualResolutionMutations({ 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`); @@ -2572,6 +2570,7 @@ function applyVirtualResolutionMutations({ } for (const requirement of peerRequirementNodes.values()) { + // Only generate warnings on root requirements if (!requirement.root) continue; From 75988c7abe311f02c4dcfb60fe80f09f39d8735b Mon Sep 17 00:00:00 2001 From: Clement Yan Date: Wed, 3 Apr 2024 00:02:04 +0800 Subject: [PATCH 3/9] feat: Reimplement explain peer-requirements --- .../commands/explain/peerRequirements.ts | 172 +++++++++--------- 1 file changed, 85 insertions(+), 87 deletions(-) diff --git a/packages/plugin-essentials/sources/commands/explain/peerRequirements.ts b/packages/plugin-essentials/sources/commands/explain/peerRequirements.ts index 0b87eea3fbf5..802e4db4b236 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} 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 { @@ -46,20 +47,35 @@ export default class ExplainPeerRequirementsCommand extends BaseCommand { await project.applyLightResolution(); - return await explainPeerRequirements(this.hash, project, { + return await explainPeerRequirement(this.hash, 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 makeTreeNode = (request: PeerRequestNode): treeUtils.TreeNode => { + 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), + child ? makeTreeNode(child) : false, + ]; + }), + ), + }; + }; + 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 +83,69 @@ 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)})`); - } - - 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)}`); - } + 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`); } - } 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(); } From 89279d55f9ee908f6b8071d521998ff984ab2099 Mon Sep 17 00:00:00 2001 From: Clement Yan Date: Wed, 3 Apr 2024 00:42:24 +0800 Subject: [PATCH 4/9] feat: Reimplement explain peer-requirements --- .../commands/explain/peerRequirements.ts | 108 ++++++++++++++++-- 1 file changed, 99 insertions(+), 9 deletions(-) diff --git a/packages/plugin-essentials/sources/commands/explain/peerRequirements.ts b/packages/plugin-essentials/sources/commands/explain/peerRequirements.ts index 802e4db4b236..bc7897027e07 100644 --- a/packages/plugin-essentials/sources/commands/explain/peerRequirements.ts +++ b/packages/plugin-essentials/sources/commands/explain/peerRequirements.ts @@ -1,9 +1,9 @@ -import {BaseCommand} from '@yarnpkg/cli'; -import type {PeerRequestNode} from '@yarnpkg/core/sources/Project'; -import {Configuration, MessageName, Project, StreamReport, structUtils, formatUtils, treeUtils, 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} 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 { @@ -32,6 +32,7 @@ export default class ExplainPeerRequirementsCommand extends BaseCommand { }); hash = Option.String({ + required: false, validator: t.cascade(t.isString(), [ t.matchesRegExp(/^p[0-9a-f]{5}$/), ]), @@ -47,9 +48,15 @@ export default class ExplainPeerRequirementsCommand extends BaseCommand { await project.applyLightResolution(); - return await explainPeerRequirement(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, + }); + } } } @@ -147,5 +154,88 @@ export async function explainPeerRequirement(peerRequirementsHash: string, proje }); + return report.exitCode(); +} + +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); + + 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); + } + } 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); + } + } + } + }); + + return report.exitCode(); } From f060ab6e7d4894d53e468979c910f7e451db1334 Mon Sep 17 00:00:00 2001 From: Clement Yan Date: Wed, 3 Apr 2024 20:55:25 +0800 Subject: [PATCH 5/9] test: Update expected peer warning message in tests --- .../sources/features/peerDependenciesMeta.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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\)/); }, ), ); From 326a30484f7dcfd7e894e53cb7cce93d810292e6 Mon Sep 17 00:00:00 2001 From: Clement Yan Date: Thu, 4 Apr 2024 16:42:16 +0800 Subject: [PATCH 6/9] chore: Add version file --- .yarn/versions/594fa39a.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) 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" From 9354798b010507d4f5893d6fe68843d8d56817e2 Mon Sep 17 00:00:00 2001 From: Clement Yan Date: Fri, 5 Apr 2024 12:45:33 +0800 Subject: [PATCH 7/9] refactor: Change description of `explain peer-requirements` to match terminology --- .../sources/commands/explain/peerRequirements.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/plugin-essentials/sources/commands/explain/peerRequirements.ts b/packages/plugin-essentials/sources/commands/explain/peerRequirements.ts index bc7897027e07..7998e35f9a2e 100644 --- a/packages/plugin-essentials/sources/commands/explain/peerRequirements.ts +++ b/packages/plugin-essentials/sources/commands/explain/peerRequirements.ts @@ -14,19 +14,19 @@ 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`, ]], }); From d327714734aef131439ee67c6f84ea61e39252d8 Mon Sep 17 00:00:00 2001 From: Clement Yan Date: Fri, 5 Apr 2024 13:03:46 +0800 Subject: [PATCH 8/9] fix: Fix infinite recursion when explaining cirucular peer requirements --- .../commands/explain/peerRequirements.ts | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/plugin-essentials/sources/commands/explain/peerRequirements.ts b/packages/plugin-essentials/sources/commands/explain/peerRequirements.ts index 7998e35f9a2e..ccb24e823551 100644 --- a/packages/plugin-essentials/sources/commands/explain/peerRequirements.ts +++ b/packages/plugin-essentials/sources/commands/explain/peerRequirements.ts @@ -1,9 +1,9 @@ -import {BaseCommand} from '@yarnpkg/cli'; -import type {PeerRequestNode} from '@yarnpkg/core/sources/Project'; -import {Configuration, MessageName, Project, StreamReport, structUtils, formatUtils, treeUtils, PeerWarningType, miscUtils} 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 { @@ -65,14 +65,25 @@ export async function explainPeerRequirement(peerRequirementsHash: string, proje 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: [ + {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), - child ? makeTreeNode(child) : false, + makeTreeNode(child), ]; }), ), From 4fea2702489e4cd569613744a635f33f47bcd60d Mon Sep 17 00:00:00 2001 From: Clement Yan Date: Thu, 16 May 2024 21:19:39 +0800 Subject: [PATCH 9/9] fix: Remove recursive elipsis when node has no children --- .../sources/commands/explain/peerRequirements.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/plugin-essentials/sources/commands/explain/peerRequirements.ts b/packages/plugin-essentials/sources/commands/explain/peerRequirements.ts index ccb24e823551..6455d31ad924 100644 --- a/packages/plugin-essentials/sources/commands/explain/peerRequirements.ts +++ b/packages/plugin-essentials/sources/commands/explain/peerRequirements.ts @@ -70,9 +70,9 @@ export async function explainPeerRequirement(peerRequirementsHash: string, proje if (seen.has(request.requester.locatorHash)) { return { value: formatUtils.tuple(formatUtils.Type.DEPENDENT, {locator: request.requester, descriptor: request.descriptor}), - children: [ - {value: formatUtils.tuple(formatUtils.Type.NO_HINT, `...`)}, - ], + children: request.children.size > 0 + ? [{value: formatUtils.tuple(formatUtils.Type.NO_HINT, `...`)}] + : [], }; }