diff --git a/packages/resolve-dependencies/src/resolveDependencies.ts b/packages/resolve-dependencies/src/resolveDependencies.ts index 22f0ae25904..caa5cbb59cf 100644 --- a/packages/resolve-dependencies/src/resolveDependencies.ts +++ b/packages/resolve-dependencies/src/resolveDependencies.ts @@ -41,6 +41,7 @@ import { import * as dp from 'dependency-path' import exists from 'path-exists' import isEmpty from 'ramda/src/isEmpty' +import zipWith from 'ramda/src/zipWith' import semver from 'semver' import encodePkgId from './encodePkgId' import getNonDevWantedDependencies, { WantedDependency } from './getNonDevWantedDependencies' @@ -218,7 +219,7 @@ export interface ResolvedPackage { type ParentPkg = Pick -type ParentPkgAliases = Record +export type ParentPkgAliases = Record interface ResolvedDependenciesOptions { currentDepth: number @@ -239,44 +240,37 @@ type PostponedResolutionFunction = (preferredVersions: PreferredVersions, parent resolvedPeers: ResolvedPeers }> -export async function resolveRootDependencies ( - ctx: ResolutionContext, - preferredVersions: PreferredVersions, - wantedDependencies: Array, - options: ResolvedDependenciesOptions -): Promise> { - const pkgAddresses: Array = [] - const parentPkgAliases: ParentPkgAliases = {} - for (const wantedDep of wantedDependencies) { - if (wantedDep.alias) { - parentPkgAliases[wantedDep.alias] = true - } - } - while (true) { - const result = await resolveDependencies(ctx, preferredVersions, wantedDependencies, { - ...options, - parentPkgAliases, - }) - pkgAddresses.push(...result.pkgAddresses) - if (!ctx.autoInstallPeers) break - for (const pkgAddress of result.pkgAddresses) { - parentPkgAliases[pkgAddress.alias] = true - } - for (const missingPeerName of Object.keys(result.missingPeers ?? {})) { - parentPkgAliases[missingPeerName] = true - } - // All the missing peers should get installed in the root. - // Otherwise, pending nodes will not work. - // even those peers should be hoisted that are not autoinstalled - for (const [resolvedPeerName, resolvedPeerAddress] of Object.entries(result.resolvedPeers ?? {})) { - if (!parentPkgAliases[resolvedPeerName]) { - pkgAddresses.push(resolvedPeerAddress) +export async function resolveRootDependencies (importers: ImporterToResolve[]): Promise>> { + const pkgAddressesByImportersWithoutPeers = await resolveDependenciesOfImporters(importers) + return Promise.all(zipWith(async (importerResolutionResult, { ctx, parentPkgAliases, preferredVersions, options }) => { + const pkgAddresses = importerResolutionResult.pkgAddresses + if (!ctx.autoInstallPeers) return pkgAddresses + while (true) { + for (const pkgAddress of importerResolutionResult.pkgAddresses) { + parentPkgAliases[pkgAddress.alias] = true + } + for (const missingPeerName of Object.keys(importerResolutionResult.missingPeers ?? {})) { + parentPkgAliases[missingPeerName] = true + } + // All the missing peers should get installed in the root. + // Otherwise, pending nodes will not work. + // even those peers should be hoisted that are not autoinstalled + for (const [resolvedPeerName, resolvedPeerAddress] of Object.entries(importerResolutionResult.resolvedPeers ?? {})) { + if (!parentPkgAliases[resolvedPeerName]) { + pkgAddresses.push(resolvedPeerAddress) + } } + if (!Object.keys(importerResolutionResult.missingPeers).length) break + const wantedDependencies = getNonDevWantedDependencies({ dependencies: importerResolutionResult.missingPeers }) + + importerResolutionResult = await resolveDependencies(ctx, preferredVersions, wantedDependencies, { + ...options, + parentPkgAliases, + }) + pkgAddresses.push(...importerResolutionResult.pkgAddresses) } - if (!Object.keys(result.missingPeers).length) break - wantedDependencies = getNonDevWantedDependencies({ dependencies: result.missingPeers }) - } - return pkgAddresses + return pkgAddresses + }, pkgAddressesByImportersWithoutPeers, importers)) } interface ResolvedDependenciesResult { @@ -285,13 +279,95 @@ interface ResolvedDependenciesResult { resolvedPeers: ResolvedPeers } +export interface ImporterToResolve { + ctx: ResolutionContext + preferredVersions: PreferredVersions + parentPkgAliases: ParentPkgAliases + wantedDependencies: Array + options: Omit +} + +export async function resolveDependenciesOfImporters ( + importers: ImporterToResolve[] +): Promise { + const extendedWantedDepsByImporters = importers.map(({ ctx, wantedDependencies, options }) => getDepsToResolve(wantedDependencies, ctx.wantedLockfile, { + preferredDependencies: options.preferredDependencies, + prefix: ctx.prefix, + proceed: options.proceed || ctx.forceFullResolution, + registries: ctx.registries, + resolvedDependencies: options.resolvedDependencies, + })) + const resolveResults = await Promise.all( + zipWith(async (extendedWantedDeps, importer) => { + const postponedResolutionsQueue: PostponedResolutionFunction[] = [] + const pkgAddresses: PkgAddress[] = [] + ;(await Promise.all( + extendedWantedDeps.map((extendedWantedDep) => resolveDependenciesOfDependency( + importer.ctx, + importer.preferredVersions, + { ...importer.options, parentPkgAliases: importer.parentPkgAliases }, + extendedWantedDep + )) + )).forEach(({ resolveDependencyResult, postponedResolution }) => { + if (resolveDependencyResult) { + pkgAddresses.push(resolveDependencyResult as PkgAddress) + } + if (postponedResolution) { + postponedResolutionsQueue.push(postponedResolution) + } + }) + return { pkgAddresses, postponedResolutionsQueue } + }, extendedWantedDepsByImporters, importers) + ) + return Promise.all(zipWith(async (importer, { pkgAddresses, postponedResolutionsQueue }) => { + const newPreferredVersions = { ...importer.preferredVersions } + const newParentPkgAliases = { ...importer.parentPkgAliases } + for (const pkgAddress of pkgAddresses) { + if (newParentPkgAliases[pkgAddress.alias] !== true) { + newParentPkgAliases[pkgAddress.alias] = pkgAddress + } + if (pkgAddress.updated) { + importer.ctx.updatedSet.add(pkgAddress.alias) + } + const resolvedPackage = importer.ctx.resolvedPackagesByDepPath[pkgAddress.depPath] + if (!resolvedPackage) continue // This will happen only with linked dependencies + if (!newPreferredVersions[resolvedPackage.name]) { + newPreferredVersions[resolvedPackage.name] = {} + } + newPreferredVersions[resolvedPackage.name][resolvedPackage.version] = 'version' + } + const childrenResults = await Promise.all( + postponedResolutionsQueue.map( + async (postponedResolution) => postponedResolution(newPreferredVersions, newParentPkgAliases) + ) + ) + if (!importer.ctx.autoInstallPeers) { + return { + missingPeers: {}, + pkgAddresses, + resolvedPeers: {}, + } + } + const allMissingPeers = mergePkgsDeps( + [ + ...pkgAddresses, + ...childrenResults, + ].map(({ missingPeers }) => missingPeers).filter(Boolean) + ) + return { + missingPeers: allMissingPeers, + pkgAddresses, + resolvedPeers: [...pkgAddresses, ...childrenResults].reduce((acc, { resolvedPeers }) => Object.assign(acc, resolvedPeers), {}), + } + }, importers, resolveResults)) +} + export async function resolveDependencies ( ctx: ResolutionContext, preferredVersions: PreferredVersions, wantedDependencies: Array, options: ResolvedDependenciesOptions ): Promise { - const postponedResolutionsQueue: PostponedResolutionFunction[] = [] const extendedWantedDeps = getDepsToResolve(wantedDependencies, ctx.wantedLockfile, { preferredDependencies: options.preferredDependencies, prefix: ctx.prefix, @@ -299,17 +375,23 @@ export async function resolveDependencies ( registries: ctx.registries, resolvedDependencies: options.resolvedDependencies, }) - const pkgAddresses = ( - await Promise.all( - extendedWantedDeps.map(async (extendedWantedDep) => resolveDependenciesOfDependency( - postponedResolutionsQueue, - ctx, - preferredVersions, - options, - extendedWantedDep - )) - ) - ).filter(Boolean) as PkgAddress[] + const postponedResolutionsQueue: PostponedResolutionFunction[] = [] + const pkgAddresses: PkgAddress[] = [] + ;(await Promise.all( + extendedWantedDeps.map((extendedWantedDep) => resolveDependenciesOfDependency( + ctx, + preferredVersions, + options, + extendedWantedDep + )) + )).forEach(({ resolveDependencyResult, postponedResolution }) => { + if (resolveDependencyResult) { + pkgAddresses.push(resolveDependencyResult as PkgAddress) + } + if (postponedResolution) { + postponedResolutionsQueue.push(postponedResolution) + } + }) const newPreferredVersions = { ...preferredVersions } const newParentPkgAliases = { ...options.parentPkgAliases } for (const pkgAddress of pkgAddresses) { @@ -373,13 +455,17 @@ interface ExtendedWantedDependency { wantedDependency: WantedDependency & { updateDepth?: number } } +interface ResolveDependenciesOfDependency { + postponedResolution?: PostponedResolutionFunction + resolveDependencyResult: ResolveDependencyResult +} + async function resolveDependenciesOfDependency ( - postponedResolutionsQueue: PostponedResolutionFunction[], ctx: ResolutionContext, preferredVersions: PreferredVersions, options: ResolvedDependenciesOptions, extendedWantedDep: ExtendedWantedDependency -) { +): Promise { const updateDepth = typeof extendedWantedDep.wantedDependency.updateDepth === 'number' ? extendedWantedDep.wantedDependency.updateDepth : options.updateDepth @@ -413,7 +499,7 @@ async function resolveDependenciesOfDependency ( } const resolveDependencyResult = await resolveDependency(extendedWantedDep.wantedDependency, ctx, resolveDependencyOpts) - if (resolveDependencyResult == null) return null + if (resolveDependencyResult == null) return { resolveDependencyResult: null } if (resolveDependencyResult.isLinkedDependency) { ctx.dependenciesTree[resolveDependencyResult.pkgId] = { children: {}, @@ -424,24 +510,24 @@ async function resolveDependenciesOfDependency ( version: resolveDependencyResult.version, }, } - return resolveDependencyResult + return { resolveDependencyResult } } - if (!resolveDependencyResult.isNew) return resolveDependencyResult - - postponedResolutionsQueue.push(async (preferredVersions, parentPkgAliases) => - resolveChildren( - ctx, - resolveDependencyResult, - parentPkgAliases, - extendedWantedDep.infoFromLockfile?.dependencyLockfile, - options.workspacePackages, - options.currentDepth, - updateDepth, - preferredVersions - ) - ) + if (!resolveDependencyResult.isNew) return { resolveDependencyResult } - return resolveDependencyResult + return { + resolveDependencyResult, + postponedResolution: async (preferredVersions, parentPkgAliases) => + resolveChildren( + ctx, + resolveDependencyResult, + parentPkgAliases, + extendedWantedDep.infoFromLockfile?.dependencyLockfile, + options.workspacePackages, + options.currentDepth, + updateDepth, + preferredVersions + ), + } } async function resolveChildren ( @@ -689,11 +775,13 @@ interface ResolveDependencyOptions { workspacePackages?: WorkspacePackages } +type ResolveDependencyResult = PkgAddress | LinkedDependency | null + async function resolveDependency ( wantedDependency: WantedDependency, ctx: ResolutionContext, options: ResolveDependencyOptions -): Promise { +): Promise { const currentPkg = options.currentPkg ?? {} const currentLockfileContainsTheDep = currentPkg.depPath diff --git a/packages/resolve-dependencies/src/resolveDependencyTree.ts b/packages/resolve-dependencies/src/resolveDependencyTree.ts index de583f201f1..758ffc54594 100644 --- a/packages/resolve-dependencies/src/resolveDependencyTree.ts +++ b/packages/resolve-dependencies/src/resolveDependencyTree.ts @@ -7,7 +7,9 @@ import { ReadPackageHook, Registries, } from '@pnpm/types' +import fromPairs from 'ramda/src/fromPairs' import partition from 'ramda/src/partition' +import zipObj from 'ramda/src/zipObj' import { WantedDependency } from './getNonDevWantedDependencies' import { createNodeId, @@ -17,6 +19,8 @@ import { ChildrenByParentDepPath, DependenciesTree, LinkedDependency, + ImporterToResolve, + ParentPkgAliases, PendingNode, PkgAddress, resolveRootDependencies, @@ -85,8 +89,6 @@ export default async function ( importers: Array>, opts: ResolveDependenciesOptions ) { - const directDepsByImporterId = {} as {[id: string]: Array} - const wantedToBeSkippedPackageIds = new Set() const ctx = { autoInstallPeers: opts.autoInstallPeers === true, @@ -119,7 +121,7 @@ export default async function ( appliedPatches: new Set(), } - await Promise.all(importers.map(async (importer) => { + const resolveArgs: ImporterToResolve[] = importers.map((importer) => { const projectSnapshot = opts.wantedLockfile.importers[importer.id] // This array will only contain the dependencies that should be linked in. // The already linked-in dependencies will not be added. @@ -144,7 +146,6 @@ export default async function ( depPath: importer.id, rootDir: importer.rootDir, }, - parentPkgAliases: {}, proceed, resolvedDependencies: { ...projectSnapshot.dependencies, @@ -154,13 +155,18 @@ export default async function ( updateDepth: -1, workspacePackages: opts.workspacePackages, } - directDepsByImporterId[importer.id] = await resolveRootDependencies( - resolveCtx, - importer.preferredVersions ?? {}, - importer.wantedDependencies, - resolveOpts - ) - })) + return { + ctx: resolveCtx, + parentPkgAliases: fromPairs( + importer.wantedDependencies.filter(({ alias }) => alias).map(({ alias }) => [alias, true]) + ) as ParentPkgAliases, + preferredVersions: importer.preferredVersions ?? {}, + wantedDependencies: importer.wantedDependencies, + options: resolveOpts, + } + }) + const pkgAddressesByImporters = await resolveRootDependencies(resolveArgs) + const directDepsByImporterId = zipObj(importers.map(({ id }) => id), pkgAddressesByImporters) ctx.pendingNodes.forEach((pendingNode) => { ctx.dependenciesTree[pendingNode.nodeId] = {