feat: pnpm add <pkg> autoinstalls any missing peer dependencies (#4213)

ref #3995
This commit is contained in:
Zoltan Kochan
2022-01-11 16:27:12 +02:00
committed by GitHub
parent b58013d719
commit e76151f669
7 changed files with 85 additions and 17 deletions

View File

@@ -0,0 +1,8 @@
---
"@pnpm/plugin-commands-installation": minor
"pnpm": minor
"@pnpm/config": minor
---
New setting supported: `auto-install-peers`. When it is set to `true`, `pnpm add <pkg>` automatically installs any missing peer dependencies as `devDependencies`.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/core": minor
---
`mutateModules()` returns the peer dependency issues of each installed project.

View File

@@ -13,6 +13,7 @@ export interface Config {
selectedProjectsGraph?: ProjectsGraph
allowNew: boolean
autoInstallPeers?: boolean
bail: boolean
color: 'always' | 'auto' | 'never'
cliOptions: Record<string, any>, // eslint-disable-line

View File

@@ -49,6 +49,7 @@ import {
DependenciesField,
DependencyManifest,
PackageExtension,
PeerDependencyIssues,
PeerDependencyRules,
ProjectManifest,
ReadPackageHook,
@@ -149,7 +150,7 @@ export async function mutateModules (
maybeOpts: InstallOptions & {
preferredVersions?: PreferredVersions
}
) {
): Promise<UpdatedProject[]> {
const reporter = maybeOpts?.reporter
if ((reporter != null) && typeof reporter === 'function') {
streamParser.on('data', reporter)
@@ -195,7 +196,7 @@ export async function mutateModules (
return result
async function _install (): Promise<Array<{ rootDir: string, manifest: ProjectManifest }>> {
async function _install (): Promise<UpdatedProject[]> {
const scriptsOpts: RunLifecycleHooksConcurrentlyOptions = {
extraBinPaths: opts.extraBinPaths,
rawConfig: opts.rawConfig,
@@ -598,6 +599,17 @@ export type ImporterToUpdate = {
wantedDependencies: Array<WantedDependency & { isNew?: boolean, updateSpec?: boolean }>
} & DependenciesMutation
export interface UpdatedProject {
manifest: ProjectManifest
peerDependencyIssues?: PeerDependencyIssues
rootDir: string
}
interface InstallFunctionResult {
newLockfile: Lockfile
projects: UpdatedProject[]
}
type InstallFunction = (
projects: ImporterToUpdate[],
ctx: PnpmContext<DependenciesMutation>,
@@ -611,7 +623,7 @@ type InstallFunction = (
pruneVirtualStore: boolean
currentLockfileIsUpToDate: boolean
}
) => Promise<{ projects: Array<{ rootDir: string, manifest: ProjectManifest }>, newLockfile: Lockfile }>
) => Promise<InstallFunctionResult>
const _installInContext: InstallFunction = async (projects, ctx, opts) => {
if (opts.lockfileOnly && ctx.existsCurrentLockfile) {
@@ -950,7 +962,11 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
return {
newLockfile,
projects: projects.map(({ manifest, rootDir }) => ({ rootDir, manifest })),
projects: projects.map(({ id, manifest, rootDir }) => ({
manifest,
peerDependencyIssues: peerDependencyIssuesByProjects[id],
rootDir,
})),
}
}

View File

@@ -238,6 +238,7 @@ by any dependencies, so it is an emulation of a flat node_modules',
export type InstallCommandOptions = Pick<Config,
| 'allProjects'
| 'autoInstallPeers'
| 'bail'
| 'bin'
| 'cliOptions'

View File

@@ -13,11 +13,13 @@ import { IncludedDependencies, Project } from '@pnpm/types'
import {
install,
mutateModules,
MutatedProject,
WorkspacePackages,
} from '@pnpm/core'
import logger from '@pnpm/logger'
import { sequenceGraph } from '@pnpm/sort-packages'
import isSubdir from 'is-subdir'
import isEmpty from 'ramda/src/isEmpty'
import getOptionsFromRootManifest from './getOptionsFromRootManifest'
import getPinnedVersion from './getPinnedVersion'
import getSaveType from './getSaveType'
@@ -33,6 +35,7 @@ const OVERWRITE_UPDATE_OPTIONS = {
export type InstallDepsOptions = Pick<Config,
| 'allProjects'
| 'autoInstallPeers'
| 'bail'
| 'bin'
| 'cliOptions'
@@ -218,20 +221,37 @@ when running add/update with the --workspace option')
}
}
if (params?.length) {
const [updatedImporter] = await mutateModules([
{
allowNew: opts.allowNew,
binsDir: installOpts.bin,
dependencySelectors: params,
manifest,
mutation: 'installSome',
peer: opts.savePeer,
pinnedVersion: getPinnedVersion(opts),
rootDir: installOpts.dir,
targetDependenciesField: getSaveType(installOpts),
},
], installOpts)
const mutatedProject: MutatedProject = {
allowNew: opts.allowNew,
binsDir: installOpts.bin,
dependencySelectors: params,
manifest,
mutation: 'installSome',
peer: opts.savePeer,
pinnedVersion: getPinnedVersion(opts),
rootDir: installOpts.dir,
targetDependenciesField: getSaveType(installOpts),
}
let [updatedImporter] = await mutateModules([mutatedProject], installOpts)
if (opts.save !== false) {
if (opts.autoInstallPeers && !isEmpty(updatedImporter.peerDependencyIssues?.intersections ?? {})) {
logger.info({
message: 'Installing missing peer dependencies',
prefix: opts.dir,
})
const dependencySelectors = Object.entries(updatedImporter.peerDependencyIssues!.intersections)
.map(([name, version]: [string, string]) => `${name}@${version}`)
const result = await mutateModules([
{
...mutatedProject,
dependencySelectors,
manifest: updatedImporter.manifest,
peer: false,
targetDependenciesField: 'devDependencies',
},
], installOpts)
updatedImporter = result[0]
}
await writeProjectManifest(updatedImporter.manifest)
}
return

View File

@@ -288,3 +288,20 @@ test('pnpm add - should add prefix when set in .npmrc when a range is not specif
).toMatch(/~([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?$/)
}
})
test('pnpm add automatically installs missing peer dependencies', async () => {
prepare()
await add.handler({
...DEFAULT_OPTIONS,
autoInstallPeers: true,
dir: process.cwd(),
linkWorkspacePackages: false,
}, ['abc@1.0.0'])
const manifest = (await import(path.resolve('package.json')))
expect(manifest.dependencies['abc']).toBe('1.0.0')
expect(manifest.devDependencies['peer-a']).toBe('^1.0.0')
expect(manifest.devDependencies['peer-b']).toBe('^1.0.0')
expect(manifest.devDependencies['peer-c']).toBe('^1.0.0')
})