mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-25 08:08:14 -05:00
feat: pnpm add <pkg> autoinstalls any missing peer dependencies (#4213)
ref #3995
This commit is contained in:
8
.changeset/light-schools-buy.md
Normal file
8
.changeset/light-schools-buy.md
Normal 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`.
|
||||
|
||||
5
.changeset/short-spoons-sit.md
Normal file
5
.changeset/short-spoons-sit.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/core": minor
|
||||
---
|
||||
|
||||
`mutateModules()` returns the peer dependency issues of each installed project.
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user