From 1fad508b07bdb4f2a68d0edc2cbb54b07b484171 Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Thu, 5 Jan 2023 15:13:40 +0200 Subject: [PATCH] feat: resolve peer dependencies from workspace root (#5882) partially reverts #4469 --- .changeset/clean-seals-boil.md | 9 +++ config/config/src/Config.ts | 1 + config/config/src/index.ts | 1 + .../core/src/install/extendInstallOptions.ts | 2 + pkg-manager/core/src/install/index.ts | 1 + .../core/test/install/peerDependencies.ts | 76 +++++++++++++++++++ .../src/installDeps.ts | 9 ++- .../src/recursive.ts | 13 ++++ pkg-manager/resolve-dependencies/src/index.ts | 1 + .../src/resolveDependencyTree.ts | 1 + .../resolve-dependencies/src/resolvePeers.ts | 30 +++++--- pnpm/test/monorepo/index.ts | 29 +++++++ 12 files changed, 163 insertions(+), 10 deletions(-) create mode 100644 .changeset/clean-seals-boil.md diff --git a/.changeset/clean-seals-boil.md b/.changeset/clean-seals-boil.md new file mode 100644 index 00000000000..27399d09bc4 --- /dev/null +++ b/.changeset/clean-seals-boil.md @@ -0,0 +1,9 @@ +--- +"@pnpm/plugin-commands-installation": minor +"@pnpm/resolve-dependencies": minor +"@pnpm/core": minor +"@pnpm/config": minor +"pnpm": minor +--- + +When the `resolve-peers-from-workspace-root` setting is set to `true`, pnpm will use dependencies installed in the root of the workspace to resolve peer dependencies in any of the workspace's projects [#5882](https://github.com/pnpm/pnpm/pull/5882). diff --git a/config/config/src/Config.ts b/config/config/src/Config.ts index 39e7089d1bd..998a8603c67 100644 --- a/config/config/src/Config.ts +++ b/config/config/src/Config.ts @@ -87,6 +87,7 @@ export interface Config { resolutionMode?: 'highest' | 'time-based' registrySupportsTimeField?: boolean failedToLoadBuiltInConfig: boolean + resolvePeersFromWorkspaceRoot?: boolean // proxy httpProxy?: string diff --git a/config/config/src/index.ts b/config/config/src/index.ts index 0d240f9a7f5..b5c270ff45f 100644 --- a/config/config/src/index.ts +++ b/config/config/src/index.ts @@ -92,6 +92,7 @@ export const types = Object.assign({ 'recursive-install': Boolean, reporter: String, 'resolution-mode': ['highest', 'time-based'], + 'resolve-peers-from-workspace-root': Boolean, 'aggregate-output': Boolean, 'save-peer': Boolean, 'save-workspace-protocol': Boolean, diff --git a/pkg-manager/core/src/install/extendInstallOptions.ts b/pkg-manager/core/src/install/extendInstallOptions.ts index 21bbaf3ed82..c60a11b115d 100644 --- a/pkg-manager/core/src/install/extendInstallOptions.ts +++ b/pkg-manager/core/src/install/extendInstallOptions.ts @@ -104,6 +104,7 @@ export interface StrictInstallOptions { allowNonAppliedPatches: boolean preferSymlinkedExecutables: boolean resolutionMode: 'highest' | 'time-based' + resolvePeersFromWorkspaceRoot: boolean publicHoistPattern: string[] | undefined hoistPattern: string[] | undefined @@ -204,6 +205,7 @@ const defaults = async (opts: InstallOptions) => { modulesCacheMaxAge: 7 * 24 * 60, resolveSymlinksInInjectedDirs: false, dedupeDirectDeps: false, + resolvePeersFromWorkspaceRoot: false, } as StrictInstallOptions } diff --git a/pkg-manager/core/src/install/index.ts b/pkg-manager/core/src/install/index.ts index 49e90e3836d..8043915b70b 100644 --- a/pkg-manager/core/src/install/index.ts +++ b/pkg-manager/core/src/install/index.ts @@ -834,6 +834,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => { workspacePackages: opts.workspacePackages, patchedDependencies: opts.patchedDependencies, lockfileIncludeTarballUrl: opts.lockfileIncludeTarballUrl, + resolvePeersFromWorkspaceRoot: opts.resolvePeersFromWorkspaceRoot, } ) if (!opts.include.optionalDependencies || !opts.include.devDependencies || !opts.include.dependencies) { diff --git a/pkg-manager/core/test/install/peerDependencies.ts b/pkg-manager/core/test/install/peerDependencies.ts index 0673e8bdcfc..257688fd161 100644 --- a/pkg-manager/core/test/install/peerDependencies.ts +++ b/pkg-manager/core/test/install/peerDependencies.ts @@ -233,6 +233,82 @@ test('strict-peer-dependencies: error is thrown when cannot resolve peer depende }) }) +test('peer dependency is resolved from the dependencies of the workspace root project', async () => { + const projects = preparePackages([ + { + location: '.', + package: { name: 'root' }, + }, + { + location: 'pkg', + package: {}, + }, + ]) + const allProjects = [ + { + buildIndex: 0, + manifest: { + name: 'root', + version: '1.0.0', + + dependencies: { + ajv: '4.10.0', + }, + }, + rootDir: process.cwd(), + }, + { + buildIndex: 0, + manifest: { + name: 'pkg', + version: '1.0.0', + + dependencies: { + 'ajv-keywords': '1.5.0', + }, + }, + rootDir: path.resolve('pkg'), + }, + ] + const reporter = jest.fn() + await mutateModules([ + { + mutation: 'install', + rootDir: process.cwd(), + }, + { + mutation: 'install', + rootDir: path.resolve('pkg'), + }, + ], await testDefaults({ allProjects, reporter, resolvePeersFromWorkspaceRoot: true })) + + expect(reporter).not.toHaveBeenCalledWith(expect.objectContaining({ + name: 'pnpm:peer-dependency-issues', + })) + + { + const lockfile = await projects.root.readLockfile() + expect(lockfile.importers.pkg?.dependencies?.['ajv-keywords']).toBe('1.5.0_ajv@4.10.0') + } + + allProjects[1].manifest.dependencies['is-positive'] = '1.0.0' + await mutateModules([ + { + mutation: 'install', + rootDir: process.cwd(), + }, + { + mutation: 'install', + rootDir: path.resolve('pkg'), + }, + ], await testDefaults({ allProjects, reporter, resolvePeersFromWorkspaceRoot: true })) + + { + const lockfile = await projects.root.readLockfile() + expect(lockfile.importers.pkg?.dependencies?.['ajv-keywords']).toBe('1.5.0_ajv@4.10.0') + } +}) + test('warning is reported when cannot resolve peer dependency for non-top-level dependency', async () => { prepareEmpty() await addDistTag({ package: '@pnpm.e2e/abc-parent-with-ab', version: '1.0.0', distTag: 'latest' }) diff --git a/pkg-manager/plugin-commands-installation/src/installDeps.ts b/pkg-manager/plugin-commands-installation/src/installDeps.ts index 1280914bd51..10b9f6e0e2b 100644 --- a/pkg-manager/plugin-commands-installation/src/installDeps.ts +++ b/pkg-manager/plugin-commands-installation/src/installDeps.ts @@ -143,13 +143,20 @@ when running add/update with the --workspace option') }) } + let allProjectsGraph = selectedProjectsGraph + if (!allProjectsGraph[opts.workspaceDir]) { + allProjectsGraph = { + ...allProjectsGraph, + ...selectProjectByDir(allProjects, opts.workspaceDir), + } + } await recursive(allProjects, params, { ...opts, forceHoistPattern, forcePublicHoistPattern, - allProjectsGraph: selectedProjectsGraph, + allProjectsGraph, selectedProjectsGraph, workspaceDir: opts.workspaceDir, }, diff --git a/pkg-manager/plugin-commands-installation/src/recursive.ts b/pkg-manager/plugin-commands-installation/src/recursive.ts index 4c466c4a0b2..0310e064856 100755 --- a/pkg-manager/plugin-commands-installation/src/recursive.ts +++ b/pkg-manager/plugin-commands-installation/src/recursive.ts @@ -246,6 +246,19 @@ export async function recursive ( } as MutatedProject) } })) + if (!opts.selectedProjectsGraph[opts.workspaceDir] && manifestsByPath[opts.workspaceDir] != null) { + const localConfig = await memReadLocalConfig(opts.workspaceDir) + const modulesDir = localConfig.modulesDir ?? opts.modulesDir + const { manifest, writeProjectManifest } = manifestsByPath[opts.workspaceDir] + writeProjectManifests.push(writeProjectManifest) + mutatedImporters.push({ + buildIndex: 0, + manifest, + modulesDir, + mutation: 'install', + rootDir: opts.workspaceDir, + } as MutatedProject) + } if ((mutatedImporters.length === 0) && cmdFullName === 'update' && opts.depth === 0) { throw new PnpmError('NO_PACKAGE_IN_DEPENDENCIES', 'None of the specified packages were found in the dependencies of any of the projects.') diff --git a/pkg-manager/resolve-dependencies/src/index.ts b/pkg-manager/resolve-dependencies/src/index.ts index 8eddd0ad823..2511a4b29d2 100644 --- a/pkg-manager/resolve-dependencies/src/index.ts +++ b/pkg-manager/resolve-dependencies/src/index.ts @@ -207,6 +207,7 @@ export async function resolveDependencies ( lockfileDir: opts.lockfileDir, projects: projectsToLink, virtualStoreDir: opts.virtualStoreDir, + resolvePeersFromWorkspaceRoot: Boolean(opts.resolvePeersFromWorkspaceRoot), }) for (const { id, manifest } of projectsToLink) { diff --git a/pkg-manager/resolve-dependencies/src/resolveDependencyTree.ts b/pkg-manager/resolve-dependencies/src/resolveDependencyTree.ts index e33dd2bcdab..8ffd082351f 100644 --- a/pkg-manager/resolve-dependencies/src/resolveDependencyTree.ts +++ b/pkg-manager/resolve-dependencies/src/resolveDependencyTree.ts @@ -77,6 +77,7 @@ export interface ResolveDependenciesOptions { preferredVersions?: PreferredVersions preferWorkspacePackages?: boolean resolutionMode?: 'highest' | 'time-based' + resolvePeersFromWorkspaceRoot?: boolean updateMatching?: (pkgName: string) => boolean linkWorkspacePackagesDepth?: number lockfileDir: string diff --git a/pkg-manager/resolve-dependencies/src/resolvePeers.ts b/pkg-manager/resolve-dependencies/src/resolvePeers.ts index 442bcbbf743..868dcf0e722 100644 --- a/pkg-manager/resolve-dependencies/src/resolvePeers.ts +++ b/pkg-manager/resolve-dependencies/src/resolvePeers.ts @@ -46,19 +46,22 @@ export interface GenericDependenciesGraph { [depPath: string]: T & GenericDependenciesGraphNode } +export interface ProjectToResolve { + directNodeIdsByAlias: { [alias: string]: string } + // only the top dependencies that were already installed + // to avoid warnings about unresolved peer dependencies + topParents: Array<{ name: string, version: string }> + rootDir: string // is only needed for logging + id: string +} + export function resolvePeers ( opts: { - projects: Array<{ - directNodeIdsByAlias: { [alias: string]: string } - // only the top dependencies that were already installed - // to avoid warnings about unresolved peer dependencies - topParents: Array<{ name: string, version: string }> - rootDir: string // is only needed for logging - id: string - }> + projects: ProjectToResolve[] dependenciesTree: DependenciesTree virtualStoreDir: string lockfileDir: string + resolvePeersFromWorkspaceRoot?: boolean } ): { dependenciesGraph: GenericDependenciesGraph @@ -68,11 +71,15 @@ export function resolvePeers ( const depGraph: GenericDependenciesGraph = {} const pathsByNodeId = {} const _createPkgsByName = createPkgsByName.bind(null, opts.dependenciesTree) + const rootPkgsByName = opts.resolvePeersFromWorkspaceRoot ? getRootPkgsByName(opts.dependenciesTree, opts.projects) : {} const peerDependencyIssuesByProjects: PeerDependencyIssuesByProjects = {} for (const { directNodeIdsByAlias, topParents, rootDir, id } of opts.projects) { const peerDependencyIssues: Pick = { bad: {}, missing: {} } - const pkgsByName = _createPkgsByName({ directNodeIdsByAlias, topParents }) + const pkgsByName = { + ...rootPkgsByName, + ..._createPkgsByName({ directNodeIdsByAlias, topParents }), + } resolvePeersOfChildren(directNodeIdsByAlias, pkgsByName, { dependenciesTree: opts.dependenciesTree, @@ -108,6 +115,11 @@ export function resolvePeers ( } } +function getRootPkgsByName (dependenciesTree: DependenciesTree, projects: ProjectToResolve[]) { + const rootProject = projects.length > 1 ? projects.find(({ id }) => id === '.') : null + return rootProject == null ? {} : createPkgsByName(dependenciesTree, rootProject) +} + function createPkgsByName ( dependenciesTree: DependenciesTree, { directNodeIdsByAlias, topParents }: { diff --git a/pnpm/test/monorepo/index.ts b/pnpm/test/monorepo/index.ts index 9020ce8f45b..22dd2eea08b 100644 --- a/pnpm/test/monorepo/index.ts +++ b/pnpm/test/monorepo/index.ts @@ -1717,3 +1717,32 @@ packages/alfa test: Done packages/alfa test: OK` ) }) + +test('peer dependencies are resolved from the root of the workspace when a new dependency is added to a workspace project', async () => { + const projects = preparePackages([ + { + location: '.', + package: { + name: 'project-1', + version: '1.0.0', + + dependencies: { + ajv: '4.10.4', + }, + }, + }, + { + name: 'project-2', + version: '1.0.0', + }, + ]) + + await writeYamlFile('pnpm-workspace.yaml', { packages: ['**', '!store/**'] }) + + process.chdir('project-2') + + await execPnpm(['add', 'ajv-keywords@1.5.0', '--strict-peer-dependencies', '--config.resolve-peers-from-workspace-root=true']) + + const lockfile = await projects['project-1'].readLockfile() + expect(lockfile.packages).toHaveProperty(['/ajv-keywords/1.5.0_ajv@4.10.4']) +})