fix: auto install root peer deps when auto-install-peers=true (#5067)

close #5028
This commit is contained in:
Zoltan Kochan
2022-07-21 18:15:16 +03:00
committed by GitHub
parent 7334b347b9
commit e3f4d131cc
13 changed files with 195 additions and 11 deletions

View File

@@ -0,0 +1,22 @@
---
"@pnpm/core": patch
"@pnpm/resolve-dependencies": patch
"pnpm": patch
---
When `auto-install-peers` is set to `true`, automatically install direct peer dependencies [#5028](https://github.com/pnpm/pnpm/pull/5067).
So if your project the next manifest:
```json
{
"dependencies": {
"lodash": "^4.17.21"
},
"peerDependencies": {
"react": "^18.2.0"
}
}
```
pnpm will install both lodash and react as a regular dependencies.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/manifest-utils": patch
---
getSpecFromPackageManifest should also read spec from peerDependencies.

View File

@@ -0,0 +1,6 @@
---
"@pnpm/headless": minor
"@pnpm/lockfile-utils": minor
---
New option added: autoInstallPeers.

View File

@@ -19,6 +19,7 @@ import semver from 'semver'
export default async function allProjectsAreUpToDate (
projects: Array<ProjectOptions & { id: string }>,
opts: {
autoInstallPeers: boolean
linkWorkspacePackages: boolean
wantedLockfile: Lockfile
workspacePackages: WorkspacePackages
@@ -34,7 +35,7 @@ export default async function allProjectsAreUpToDate (
return pEvery(projects, (project) => {
const importer = opts.wantedLockfile.importers[project.id]
return !hasLocalTarballDepsInRoot(importer) &&
_satisfiesPackageManifest(project.manifest, project.id) &&
_satisfiesPackageManifest(project.manifest, project.id, { autoInstallPeers: opts.autoInstallPeers }) &&
_linkedPackagesAreUpToDate({
dir: project.rootDir,
manifest: project.manifest,

View File

@@ -277,6 +277,7 @@ export async function mutateModules (
ctx.existsWantedLockfile &&
ctx.wantedLockfile.lockfileVersion === LOCKFILE_VERSION &&
await allProjectsAreUpToDate(ctx.projects, {
autoInstallPeers: opts.autoInstallPeers,
linkWorkspacePackages: opts.linkWorkspacePackagesDepth >= 0,
wantedLockfile: ctx.wantedLockfile,
workspacePackages: opts.workspacePackages,
@@ -428,6 +429,7 @@ export async function mutateModules (
async function installCase (project: any) { // eslint-disable-line
const wantedDependencies = getWantedDependencies(project.manifest, {
autoInstallPeers: opts.autoInstallPeers,
includeDirect: opts.includeDirect,
updateWorkspaceDependencies: opts.update,
nodeExecPath: opts.nodeExecPath,

View File

@@ -30,6 +30,7 @@ test('allProjectsAreUpToDate(): works with packages linked through the workspace
rootDir: 'foo',
},
], {
autoInstallPeers: false,
linkWorkspacePackages: true,
wantedLockfile: {
importers: {
@@ -68,6 +69,7 @@ test('allProjectsAreUpToDate(): works with aliased local dependencies', async ()
rootDir: 'foo',
},
], {
autoInstallPeers: false,
linkWorkspacePackages: true,
wantedLockfile: {
importers: {
@@ -106,6 +108,7 @@ test('allProjectsAreUpToDate(): works with aliased local dependencies that speci
rootDir: 'foo',
},
], {
autoInstallPeers: false,
linkWorkspacePackages: true,
wantedLockfile: {
importers: {
@@ -144,6 +147,7 @@ test('allProjectsAreUpToDate(): returns false if the aliased dependency version
rootDir: 'foo',
},
], {
autoInstallPeers: false,
linkWorkspacePackages: true,
wantedLockfile: {
importers: {
@@ -194,6 +198,7 @@ test('allProjectsAreUpToDate(): use link and registry version if linkWorkspacePa
},
],
{
autoInstallPeers: false,
linkWorkspacePackages: false,
wantedLockfile: {
importers: {
@@ -247,6 +252,7 @@ test('allProjectsAreUpToDate(): returns false if dependenciesMeta differs', asyn
rootDir: 'foo',
},
], {
autoInstallPeers: false,
linkWorkspacePackages: true,
wantedLockfile: {
importers: {
@@ -290,6 +296,7 @@ test('allProjectsAreUpToDate(): returns true if dependenciesMeta matches', async
rootDir: 'foo',
},
], {
autoInstallPeers: false,
linkWorkspacePackages: true,
wantedLockfile: {
importers: {

View File

@@ -1,6 +1,7 @@
import { addDependenciesToPackage } from '@pnpm/core'
import { addDependenciesToPackage, install, mutateModules } from '@pnpm/core'
import { prepareEmpty, preparePackages } from '@pnpm/prepare'
import { addDistTag, REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
import rimraf from '@zkochan/rimraf'
import { testDefaults } from '../utils'
test('auto install non-optional peer dependencies', async () => {
@@ -139,3 +140,84 @@ test('don\'t install the same missing peer dependency twice', async () => {
'/has-y-peer/1.0.0_@pnpm+y@1.0.0',
])
})
test('automatically install root peer dependencies', async () => {
const project = prepareEmpty()
let manifest = await install({
dependencies: {
'is-negative': '^1.0.0',
},
peerDependencies: {
'is-positive': '^1.0.0',
},
}, await testDefaults({ autoInstallPeers: true }))
await project.has('is-positive')
await project.has('is-negative')
{
const lockfile = await project.readLockfile()
expect(lockfile.specifiers).toStrictEqual({
'is-positive': '^1.0.0',
'is-negative': '^1.0.0',
})
expect(lockfile.dependencies).toStrictEqual({
'is-positive': '1.0.0',
'is-negative': '1.0.1',
})
}
// Automatically install the peer dependency when the lockfile is up-to-date
await rimraf('node_modules')
await install(manifest, await testDefaults({ autoInstallPeers: true, frozenLockfile: true }))
await project.has('is-positive')
await project.has('is-negative')
// The auto installed peer is not removed when a new dependency is added
manifest = await addDependenciesToPackage(manifest, ['is-odd@1.0.0'], await testDefaults({ autoInstallPeers: true }))
await project.has('is-odd')
await project.has('is-positive')
await project.has('is-negative')
{
const lockfile = await project.readLockfile()
expect(lockfile.specifiers).toStrictEqual({
'is-odd': '1.0.0',
'is-positive': '^1.0.0',
'is-negative': '^1.0.0',
})
expect(lockfile.dependencies).toStrictEqual({
'is-odd': '1.0.0',
'is-positive': '1.0.0',
'is-negative': '1.0.1',
})
}
// The auto installed peer is not removed when a dependency is removed
await mutateModules([
{
dependencyNames: ['is-odd'],
manifest,
mutation: 'uninstallSome',
rootDir: process.cwd(),
},
], await testDefaults({ autoInstallPeers: true }))
await project.hasNot('is-odd')
await project.has('is-positive')
await project.has('is-negative')
{
const lockfile = await project.readLockfile()
expect(lockfile.specifiers).toStrictEqual({
'is-positive': '^1.0.0',
'is-negative': '^1.0.0',
})
expect(lockfile.dependencies).toStrictEqual({
'is-positive': '1.0.0',
'is-negative': '1.0.1',
})
}
})

View File

@@ -90,6 +90,7 @@ export interface Project {
}
export interface HeadlessOptions {
autoInstallPeers?: boolean
childConcurrency?: number
currentLockfile?: Lockfile
currentEngine: {
@@ -171,7 +172,7 @@ export default async (opts: HeadlessOptions) => {
if (!opts.ignorePackageManifest) {
for (const { id, manifest, rootDir } of opts.projects) {
if (!satisfiesPackageManifest(wantedLockfile, manifest, id)) {
if (!satisfiesPackageManifest(wantedLockfile, manifest, id, { autoInstallPeers: opts.autoInstallPeers })) {
throw new PnpmError('OUTDATED_LOCKFILE',
`Cannot install with "frozen-lockfile" because ${WANTED_LOCKFILE} is not up-to-date with ` +
path.relative(lockfileDir, path.join(rootDir, 'package.json')), {

View File

@@ -5,10 +5,24 @@ import {
} from '@pnpm/types'
import equals from 'ramda/src/equals.js'
export default (lockfile: Lockfile, pkg: ProjectManifest, importerId: string) => {
export default (lockfile: Lockfile, pkg: ProjectManifest, importerId: string, opts?: { autoInstallPeers?: boolean }) => {
const importer = lockfile.importers[importerId]
if (!importer) return false
if (!equals({ ...pkg.devDependencies, ...pkg.dependencies, ...pkg.optionalDependencies }, importer.specifiers)) {
let existingDeps = { ...pkg.devDependencies, ...pkg.dependencies, ...pkg.optionalDependencies }
if (opts?.autoInstallPeers) {
existingDeps = {
...pkg.peerDependencies,
...existingDeps,
}
pkg = {
...pkg,
dependencies: {
...pkg.peerDependencies,
...pkg.dependencies,
},
}
}
if (!equals(existingDeps, importer.specifiers)) {
return false
}
if (!equals(pkg.dependenciesMeta ?? {}, importer.dependenciesMeta ?? {})) return false

View File

@@ -259,4 +259,28 @@ test('satisfiesPackageManifest()', () => {
},
dependenciesMeta: {},
}, '.')).toBe(true)
expect(satisfiesPackageManifest({
...DEFAULT_LOCKFILE_FIELDS,
importers: {
'.': {
dependencies: {
foo: '1.0.0',
bar: '1.0.0',
},
specifiers: {
foo: '1.0.0',
bar: '^1.0.0',
},
},
},
}, {
...DEFAULT_PKG_FIELDS,
dependencies: {
foo: '1.0.0',
},
peerDependencies: {
bar: '^1.0.0',
},
}, '.', { autoInstallPeers: true })).toBe(true)
})

View File

@@ -1,11 +1,12 @@
import { ProjectManifest } from '@pnpm/types'
export default (
manifest: Pick<ProjectManifest, 'devDependencies' | 'dependencies' | 'optionalDependencies'>,
manifest: Pick<ProjectManifest, 'devDependencies' | 'dependencies' | 'optionalDependencies' | 'peerDependencies'>,
depName: string
) => {
return manifest.optionalDependencies?.[depName] ??
manifest.dependencies?.[depName] ??
manifest.devDependencies?.[depName] ??
manifest.peerDependencies?.[depName] ??
''
}

View File

@@ -19,24 +19,32 @@ export interface WantedDependency {
}
export default function getWantedDependencies (
pkg: Pick<ProjectManifest, 'devDependencies' | 'dependencies' | 'optionalDependencies' | 'dependenciesMeta'>,
pkg: Pick<ProjectManifest, 'devDependencies' | 'dependencies' | 'optionalDependencies' | 'dependenciesMeta' | 'peerDependencies'>,
opts?: {
autoInstallPeers?: boolean
includeDirect?: IncludedDependencies
nodeExecPath?: string
updateWorkspaceDependencies?: boolean
}
): WantedDependency[] {
const depsToInstall = filterDependenciesByType(pkg,
let depsToInstall = filterDependenciesByType(pkg,
opts?.includeDirect ?? {
dependencies: true,
devDependencies: true,
optionalDependencies: true,
})
if (opts?.autoInstallPeers) {
depsToInstall = {
...pkg.peerDependencies,
...depsToInstall,
}
}
return getWantedDependenciesFromGivenSet(depsToInstall, {
dependencies: pkg.dependencies ?? {},
devDependencies: pkg.devDependencies ?? {},
optionalDependencies: pkg.optionalDependencies ?? {},
dependenciesMeta: pkg.dependenciesMeta ?? {},
peerDependencies: pkg.peerDependencies ?? {},
updatePref: opts?.updateWorkspaceDependencies === true
? updateWorkspacePref
: (pref) => pref,
@@ -53,6 +61,7 @@ function getWantedDependenciesFromGivenSet (
dependencies: Dependencies
devDependencies: Dependencies
optionalDependencies: Dependencies
peerDependencies: Dependencies
dependenciesMeta: DependenciesMeta
nodeExecPath?: string
updatePref: (pref: string) => string
@@ -65,6 +74,7 @@ function getWantedDependenciesFromGivenSet (
if (opts.optionalDependencies[alias] != null) depType = 'optional'
else if (opts.dependencies[alias] != null) depType = 'prod'
else if (opts.devDependencies[alias] != null) depType = 'dev'
else if (opts.peerDependencies[alias] != null) depType = 'prod'
return {
alias,
dev: depType === 'dev',

View File

@@ -140,7 +140,8 @@ export default async function (
projectSnapshot,
resolvedImporter.linkedDependencies,
resolvedImporter.directDependencies,
opts.registries
opts.registries,
opts.autoInstallPeers
)
}
@@ -305,7 +306,8 @@ function addDirectDependenciesToLockfile (
projectSnapshot: ProjectSnapshot,
linkedPackages: Array<{alias: string}>,
directDependencies: ResolvedDirectDependency[],
registries: Registries
registries: Registries,
autoInstallPeers?: boolean
): ProjectSnapshot {
const newProjectSnapshot = {
dependencies: {},
@@ -323,7 +325,14 @@ function addDirectDependenciesToLockfile (
return acc
}, {})
const allDeps = Array.from(new Set(Object.keys(getAllDependenciesFromManifest(newManifest))))
let allDepsObj = getAllDependenciesFromManifest(newManifest)
if (autoInstallPeers) {
allDepsObj = {
...newManifest.peerDependencies,
...allDepsObj,
}
}
const allDeps = Array.from(new Set(Object.keys(allDepsObj)))
for (const alias of allDeps) {
if (directDependenciesByAlias[alias]) {