mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-23 23:29:17 -05:00
fix: auto install root peer deps when auto-install-peers=true (#5067)
close #5028
This commit is contained in:
22
.changeset/green-drinks-teach.md
Normal file
22
.changeset/green-drinks-teach.md
Normal 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.
|
||||
5
.changeset/neat-apes-lay.md
Normal file
5
.changeset/neat-apes-lay.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/manifest-utils": patch
|
||||
---
|
||||
|
||||
getSpecFromPackageManifest should also read spec from peerDependencies.
|
||||
6
.changeset/thin-trees-mate.md
Normal file
6
.changeset/thin-trees-mate.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/headless": minor
|
||||
"@pnpm/lockfile-utils": minor
|
||||
---
|
||||
|
||||
New option added: autoInstallPeers.
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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')), {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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] ??
|
||||
''
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
Reference in New Issue
Block a user