mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-25 15:07:06 -04:00
* fix: respect peer dep range in hoistPeers when preferred versions exist Previously, hoistPeers used semver.maxSatisfying(versions, '*') which picked the highest preferred version from the lockfile regardless of the peer dep range. This caused overrides that narrow a peer dep range to be ignored when a stale version existed in the lockfile. Now hoistPeers first tries semver.maxSatisfying(versions, range) to find a preferred version that satisfies the actual peer dep range. If none satisfies it and autoInstallPeers is enabled, it falls back to the range itself so pnpm resolves a matching version from the registry. * fix: only fall back to exact-version range for overrides, handle workspace: protocol - When no preferred version satisfies the peer dep range, only use the range directly if it is an exact version (e.g. "4.3.0" from an override). For semver ranges (e.g. "1", "^2.0.0"), fall back to the old behavior of picking the highest preferred version for deduplication. - Guard against workspace: protocol ranges that would cause semver.maxSatisfying to throw. - Add unit tests for hoisting deduplication and workspace: ranges. * fix: only apply range-constrained peer selection for exact versions The previous approach used semver.maxSatisfying(versions, range) for all peer dep ranges, which broke aliased-dependency deduplication — e.g. when three aliases of @pnpm.e2e/peer-c existed at 1.0.0, 1.0.1, and 2.0.0, range ^1.0.0 would pick 1.0.1 instead of 2.0.0. Now the range-aware logic only activates when the range is an exact version (semver.valid), which is the override case (e.g. "4.3.0"). Regular semver ranges fall back to picking the highest preferred version.
708 lines
25 KiB
TypeScript
708 lines
25 KiB
TypeScript
import path from 'path'
|
|
import { assertProject } from '@pnpm/assert-project'
|
|
import { addDependenciesToPackage, install, mutateModules, mutateModulesInSingleProject, type PackageManifest } from '@pnpm/core'
|
|
import { prepareEmpty, preparePackages } from '@pnpm/prepare'
|
|
import { addDistTag, REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
|
|
import { type ProjectRootDir } from '@pnpm/types'
|
|
import { sync as rimraf } from '@zkochan/rimraf'
|
|
import { createPeerDepGraphHash } from '@pnpm/dependency-path'
|
|
import { testDefaults } from '../utils/index.js'
|
|
|
|
test('auto install non-optional peer dependencies', async () => {
|
|
await addDistTag({ package: '@pnpm.e2e/peer-a', version: '1.0.0', distTag: 'latest' })
|
|
const project = prepareEmpty()
|
|
await addDependenciesToPackage({}, ['@pnpm.e2e/abc-optional-peers@1.0.0'], testDefaults({ autoInstallPeers: true }))
|
|
const lockfile = project.readLockfile()
|
|
expect(Object.keys(lockfile.snapshots)).toStrictEqual([
|
|
'@pnpm.e2e/abc-optional-peers@1.0.0(@pnpm.e2e/peer-a@1.0.0)',
|
|
'@pnpm.e2e/peer-a@1.0.0',
|
|
])
|
|
project.hasNot('@pnpm.e2e/peer-a')
|
|
})
|
|
|
|
test('auto install the common peer dependency', async () => {
|
|
await addDistTag({ package: '@pnpm.e2e/peer-c', version: '1.0.1', distTag: 'latest' })
|
|
const project = prepareEmpty()
|
|
await addDependenciesToPackage({}, ['@pnpm.e2e/wants-peer-c-1', '@pnpm.e2e/wants-peer-c-1.0.0'], testDefaults({ autoInstallPeers: true }))
|
|
const lockfile = project.readLockfile()
|
|
expect(Object.keys(lockfile.snapshots).sort()).toStrictEqual([
|
|
'@pnpm.e2e/peer-c@1.0.0',
|
|
'@pnpm.e2e/wants-peer-c-1.0.0@1.0.0(@pnpm.e2e/peer-c@1.0.0)',
|
|
'@pnpm.e2e/wants-peer-c-1@1.0.0(@pnpm.e2e/peer-c@1.0.0)',
|
|
].sort())
|
|
})
|
|
|
|
test('do not auto install when there is no common peer dependency range intersection', async () => {
|
|
const project = prepareEmpty()
|
|
await addDependenciesToPackage({}, ['@pnpm.e2e/wants-peer-c-1', '@pnpm.e2e/wants-peer-c-2'], testDefaults({ autoInstallPeers: true }))
|
|
const lockfile = project.readLockfile()
|
|
expect(Object.keys(lockfile.snapshots)).toStrictEqual([
|
|
'@pnpm.e2e/wants-peer-c-1@1.0.0',
|
|
'@pnpm.e2e/wants-peer-c-2@1.0.0',
|
|
])
|
|
})
|
|
|
|
test('auto install latest when there is no common peer dependency range intersection', async () => {
|
|
await addDistTag({ package: '@pnpm.e2e/peer-c', version: '2.0.0', distTag: 'latest' })
|
|
const project = prepareEmpty()
|
|
await addDependenciesToPackage({}, ['@pnpm.e2e/wants-peer-c-1', '@pnpm.e2e/wants-peer-c-2'], testDefaults({
|
|
autoInstallPeers: true,
|
|
autoInstallPeersFromHighestMatch: true,
|
|
}))
|
|
const lockfile = project.readLockfile()
|
|
expect(Object.keys(lockfile.snapshots).sort()).toStrictEqual([
|
|
'@pnpm.e2e/peer-c@2.0.0',
|
|
'@pnpm.e2e/wants-peer-c-1@1.0.0(@pnpm.e2e/peer-c@2.0.0)',
|
|
'@pnpm.e2e/wants-peer-c-2@1.0.0(@pnpm.e2e/peer-c@2.0.0)',
|
|
].sort())
|
|
})
|
|
|
|
test('don\'t fail on linked package, when peers are auto installed', async () => {
|
|
const pkgManifest = {
|
|
dependencies: {
|
|
linked: 'link:../linked',
|
|
},
|
|
}
|
|
preparePackages([
|
|
{
|
|
location: 'linked',
|
|
package: {
|
|
name: 'linked',
|
|
peerDependencies: {
|
|
'peer-c': '1.0.0',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
location: 'pkg',
|
|
package: pkgManifest,
|
|
},
|
|
])
|
|
process.chdir('pkg')
|
|
const { updatedManifest } = await addDependenciesToPackage(pkgManifest, ['@pnpm.e2e/peer-b'], testDefaults({ autoInstallPeers: true }))
|
|
expect(Object.keys(updatedManifest.dependencies ?? {})).toStrictEqual(['linked', '@pnpm.e2e/peer-b'])
|
|
})
|
|
|
|
test('hoist a peer dependency in order to reuse it by other dependencies, when it satisfies them', async () => {
|
|
const project = prepareEmpty()
|
|
await addDependenciesToPackage({}, ['@pnpm/xyz-parent-parent-parent-parent', '@pnpm/xyz-parent-parent-with-xyz'], testDefaults({ autoInstallPeers: true }))
|
|
const lockfile = project.readLockfile()
|
|
const suffix = createPeerDepGraphHash([{ name: '@pnpm/x', version: '1.0.0' }, { name: '@pnpm/y', version: '1.0.0' }, { name: '@pnpm/z', version: '1.0.0' }])
|
|
expect(Object.keys(lockfile.snapshots).sort()).toStrictEqual([
|
|
'@pnpm/x@1.0.0',
|
|
'@pnpm/xyz-parent-parent-with-xyz@1.0.0',
|
|
'@pnpm/y@1.0.0',
|
|
'@pnpm/z@1.0.0',
|
|
`@pnpm/xyz-parent-parent-parent-parent@1.0.0${suffix}`,
|
|
`@pnpm/xyz-parent-parent-parent@1.0.0${suffix}`,
|
|
`@pnpm/xyz-parent-parent@1.0.0${suffix}`,
|
|
`@pnpm/xyz-parent@1.0.0${suffix}`,
|
|
`@pnpm/xyz@1.0.0${suffix}`,
|
|
].sort())
|
|
})
|
|
|
|
test('don\'t hoist a peer dependency when there is a root dependency by that name', async () => {
|
|
const project = prepareEmpty()
|
|
await addDependenciesToPackage({}, [
|
|
'@pnpm/xyz-parent-parent-parent-parent',
|
|
'@pnpm/xyz-parent-parent-with-xyz',
|
|
'@pnpm/x@npm:@pnpm.e2e/peer-a@1.0.0',
|
|
`http://localhost:${REGISTRY_MOCK_PORT}/@pnpm/y/-/y-2.0.0.tgz`,
|
|
], testDefaults({ autoInstallPeers: true }))
|
|
const lockfile = project.readLockfile()
|
|
const suffix1 = createPeerDepGraphHash([{ name: '@pnpm/y', version: '2.0.0' }, { name: '@pnpm/z', version: '1.0.0' }, { name: '@pnpm.e2e/peer-a', version: '1.0.0' }])
|
|
const suffix2 = createPeerDepGraphHash([{ name: '@pnpm/x', version: '1.0.0' }, { name: '@pnpm/y', version: '1.0.0' }, { name: '@pnpm/z', version: '1.0.0' }])
|
|
expect(Object.keys(lockfile.snapshots).sort()).toStrictEqual([
|
|
'@pnpm.e2e/peer-a@1.0.0',
|
|
'@pnpm/x@1.0.0',
|
|
`@pnpm/xyz-parent-parent-parent-parent@1.0.0${suffix1}`,
|
|
`@pnpm/xyz-parent-parent-parent@1.0.0${suffix1}`,
|
|
'@pnpm/xyz-parent-parent-with-xyz@1.0.0',
|
|
`@pnpm/xyz-parent-parent@1.0.0${suffix1}`,
|
|
`@pnpm/xyz-parent@1.0.0${suffix1}`,
|
|
`@pnpm/xyz-parent@1.0.0${suffix2}`,
|
|
`@pnpm/xyz@1.0.0${suffix1}`,
|
|
`@pnpm/xyz@1.0.0${suffix2}`,
|
|
'@pnpm/y@1.0.0',
|
|
'@pnpm/y@2.0.0',
|
|
'@pnpm/z@1.0.0',
|
|
].sort())
|
|
})
|
|
|
|
test('don\'t auto-install a peer dependency, when that dependency is in the root', async () => {
|
|
const project = prepareEmpty()
|
|
await addDependenciesToPackage({}, [
|
|
'@pnpm/xyz-parent-parent-parent-parent',
|
|
'@pnpm/x@npm:@pnpm.e2e/peer-a@1.0.0',
|
|
`http://localhost:${REGISTRY_MOCK_PORT}/@pnpm/y/-/y-2.0.0.tgz`,
|
|
], testDefaults({ autoInstallPeers: true }))
|
|
const lockfile = project.readLockfile()
|
|
const suffix = createPeerDepGraphHash([{ name: '@pnpm/y', version: '2.0.0' }, { name: '@pnpm/z', version: '1.0.0' }, { name: '@pnpm.e2e/peer-a', version: '1.0.0' }])
|
|
expect(Object.keys(lockfile.snapshots).sort()).toStrictEqual([
|
|
`@pnpm/xyz-parent-parent-parent-parent@1.0.0${suffix}`,
|
|
`@pnpm/xyz-parent-parent-parent@1.0.0${suffix}`,
|
|
`@pnpm/xyz-parent-parent@1.0.0${suffix}`,
|
|
`@pnpm/xyz-parent@1.0.0${suffix}`,
|
|
`@pnpm/xyz@1.0.0${suffix}`,
|
|
'@pnpm/y@2.0.0',
|
|
'@pnpm/z@1.0.0',
|
|
'@pnpm.e2e/peer-a@1.0.0',
|
|
].sort())
|
|
})
|
|
|
|
test('don\'t install the same missing peer dependency twice', async () => {
|
|
await addDistTag({ package: '@pnpm/y', version: '2.0.0', distTag: 'latest' })
|
|
const project = prepareEmpty()
|
|
await addDependenciesToPackage({}, [
|
|
'@pnpm.e2e/has-has-y-peer-peer',
|
|
], testDefaults({ autoInstallPeers: true }))
|
|
const lockfile = project.readLockfile()
|
|
expect(Object.keys(lockfile.snapshots).sort()).toStrictEqual([
|
|
'@pnpm/y@1.0.0',
|
|
'@pnpm.e2e/has-has-y-peer-peer@1.0.0(@pnpm.e2e/has-y-peer@1.0.0(@pnpm/y@1.0.0))(@pnpm/y@1.0.0)',
|
|
'@pnpm.e2e/has-y-peer@1.0.0(@pnpm/y@1.0.0)',
|
|
].sort())
|
|
})
|
|
|
|
// Covers https://github.com/pnpm/pnpm/issues/5373
|
|
test('prefer the peer dependency version already used in the root', async () => {
|
|
await addDistTag({ package: '@pnpm/y', version: '2.0.0', distTag: 'latest' })
|
|
const project = prepareEmpty()
|
|
await install({
|
|
peerDependencies: {
|
|
'@pnpm.e2e/has-y-peer': '1.0.0',
|
|
'@pnpm/y': '^1.0.0',
|
|
},
|
|
}, testDefaults({ autoInstallPeers: true }))
|
|
const lockfile = project.readLockfile()
|
|
expect(Object.keys(lockfile.snapshots).sort()).toStrictEqual([
|
|
'@pnpm/y@1.0.0',
|
|
'@pnpm.e2e/has-y-peer@1.0.0(@pnpm/y@1.0.0)',
|
|
].sort())
|
|
})
|
|
|
|
test('automatically install root peer dependencies', async () => {
|
|
const project = prepareEmpty()
|
|
|
|
let { updatedManifest: manifest } = await install({
|
|
dependencies: {
|
|
'is-negative': '^1.0.1',
|
|
},
|
|
peerDependencies: {
|
|
'is-positive': '^1.0.0',
|
|
},
|
|
}, testDefaults({ autoInstallPeers: true, resolutionMode: 'lowest-direct' }))
|
|
|
|
project.has('is-positive')
|
|
project.has('is-negative')
|
|
|
|
{
|
|
const lockfile = project.readLockfile()
|
|
expect(lockfile.importers['.'].dependencies).toStrictEqual({
|
|
'is-positive': {
|
|
specifier: '^1.0.0',
|
|
version: '1.0.0',
|
|
},
|
|
'is-negative': {
|
|
specifier: '^1.0.1',
|
|
version: '1.0.1',
|
|
},
|
|
})
|
|
}
|
|
|
|
// Automatically install the peer dependency when the lockfile is up to date
|
|
rimraf('node_modules')
|
|
|
|
await install(manifest, testDefaults({ autoInstallPeers: true, frozenLockfile: true }))
|
|
|
|
project.has('is-positive')
|
|
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'], testDefaults({ autoInstallPeers: true, resolutionMode: 'lowest-direct' }))).updatedManifest
|
|
project.has('is-odd')
|
|
project.has('is-positive')
|
|
project.has('is-negative')
|
|
|
|
{
|
|
const lockfile = project.readLockfile()
|
|
expect(lockfile.importers['.'].dependencies).toStrictEqual({
|
|
'is-odd': {
|
|
specifier: '1.0.0',
|
|
version: '1.0.0',
|
|
},
|
|
'is-positive': {
|
|
specifier: '^1.0.0',
|
|
version: '1.0.0',
|
|
},
|
|
'is-negative': {
|
|
specifier: '^1.0.1',
|
|
version: '1.0.1',
|
|
},
|
|
})
|
|
}
|
|
|
|
// The auto installed peer is not removed when a dependency is removed
|
|
await mutateModulesInSingleProject({
|
|
dependencyNames: ['is-odd'],
|
|
manifest,
|
|
mutation: 'uninstallSome',
|
|
rootDir: process.cwd() as ProjectRootDir,
|
|
}, testDefaults({ autoInstallPeers: true, resolutionMode: 'lowest-direct' }))
|
|
project.hasNot('is-odd')
|
|
project.has('is-positive')
|
|
project.has('is-negative')
|
|
|
|
{
|
|
const lockfile = project.readLockfile()
|
|
expect(lockfile.importers['.'].dependencies).toStrictEqual({
|
|
'is-positive': {
|
|
specifier: '^1.0.0',
|
|
version: '1.0.0',
|
|
},
|
|
'is-negative': {
|
|
specifier: '^1.0.1',
|
|
version: '1.0.1',
|
|
},
|
|
})
|
|
}
|
|
})
|
|
|
|
test('automatically install peer dependency when it is a dev dependency in another workspace project', async () => {
|
|
prepareEmpty()
|
|
|
|
await mutateModules([
|
|
{
|
|
mutation: 'install',
|
|
rootDir: path.resolve('project-1') as ProjectRootDir,
|
|
},
|
|
{
|
|
mutation: 'install',
|
|
rootDir: path.resolve('project-2') as ProjectRootDir,
|
|
},
|
|
], testDefaults({
|
|
allProjects: [
|
|
{
|
|
buildIndex: 0,
|
|
manifest: {
|
|
name: 'project-1',
|
|
devDependencies: {
|
|
'is-positive': '1.0.0',
|
|
},
|
|
},
|
|
rootDir: path.resolve('project-1') as ProjectRootDir,
|
|
},
|
|
{
|
|
buildIndex: 0,
|
|
manifest: {
|
|
name: 'project-2',
|
|
peerDependencies: {
|
|
'is-positive': '1.0.0',
|
|
},
|
|
},
|
|
rootDir: path.resolve('project-2') as ProjectRootDir,
|
|
},
|
|
],
|
|
autoInstallPeers: true,
|
|
}))
|
|
|
|
const project = assertProject(process.cwd())
|
|
const lockfile = project.readLockfile()
|
|
expect(lockfile.importers['project-1'].devDependencies).toStrictEqual({
|
|
'is-positive': {
|
|
specifier: '1.0.0',
|
|
version: '1.0.0',
|
|
},
|
|
})
|
|
expect(lockfile.importers['project-2'].dependencies).toStrictEqual({
|
|
'is-positive': {
|
|
specifier: '1.0.0',
|
|
version: '1.0.0',
|
|
},
|
|
})
|
|
})
|
|
|
|
// Covers https://github.com/pnpm/pnpm/issues/4820
|
|
test('auto install peer deps in a workspace. test #1', async () => {
|
|
await addDistTag({ package: '@pnpm.e2e/peer-c', version: '1.0.0', distTag: 'latest' })
|
|
const project = prepareEmpty()
|
|
await mutateModules([
|
|
{
|
|
mutation: 'install',
|
|
rootDir: path.resolve('project1') as ProjectRootDir,
|
|
},
|
|
{
|
|
mutation: 'install',
|
|
rootDir: path.resolve('project2') as ProjectRootDir,
|
|
},
|
|
], testDefaults({
|
|
autoInstallPeers: true,
|
|
allProjects: [
|
|
{
|
|
buildIndex: 0,
|
|
manifest: {
|
|
name: 'root-project',
|
|
devDependencies: {
|
|
'@pnpm.e2e/abc-parent-with-ab': '1.0.0',
|
|
},
|
|
},
|
|
rootDir: path.resolve('project1') as ProjectRootDir,
|
|
},
|
|
{
|
|
buildIndex: 0,
|
|
manifest: {
|
|
name: 'project',
|
|
peerDependencies: {
|
|
'@pnpm.e2e/abc-parent-with-ab': '1.0.0',
|
|
},
|
|
},
|
|
rootDir: path.resolve('project2') as ProjectRootDir,
|
|
},
|
|
],
|
|
dedupePeerDependents: false,
|
|
}))
|
|
const lockfile = project.readLockfile()
|
|
expect(lockfile.importers['project1'].devDependencies?.['@pnpm.e2e/abc-parent-with-ab']?.version).toBe('1.0.0(@pnpm.e2e/peer-c@1.0.0)')
|
|
expect(lockfile.importers['project2'].dependencies?.['@pnpm.e2e/abc-parent-with-ab']?.version).toBe('1.0.0(@pnpm.e2e/peer-c@1.0.0)')
|
|
})
|
|
|
|
test('auto install peer deps in a workspace. test #2', async () => {
|
|
prepareEmpty()
|
|
await mutateModules([
|
|
{
|
|
mutation: 'install',
|
|
rootDir: process.cwd() as ProjectRootDir,
|
|
},
|
|
{
|
|
mutation: 'install',
|
|
rootDir: path.resolve('project') as ProjectRootDir,
|
|
},
|
|
], testDefaults({
|
|
autoInstallPeers: true,
|
|
allProjects: [
|
|
{
|
|
buildIndex: 0,
|
|
manifest: {
|
|
name: 'root-project',
|
|
devDependencies: {
|
|
'@pnpm.e2e/abc-parent-with-ab': '1.0.0',
|
|
'@pnpm.e2e/peer-c': '1.0.0',
|
|
},
|
|
},
|
|
rootDir: process.cwd() as ProjectRootDir,
|
|
},
|
|
{
|
|
buildIndex: 0,
|
|
manifest: {
|
|
name: 'project',
|
|
peerDependencies: {
|
|
'@pnpm.e2e/abc-parent-with-ab': '1.0.0',
|
|
},
|
|
},
|
|
rootDir: path.resolve('project') as ProjectRootDir,
|
|
},
|
|
],
|
|
}))
|
|
})
|
|
|
|
// This test may be removed if autoInstallPeers will become true by default
|
|
test('installation on a package with many complex circular dependencies does not fail when auto install peers is on', async () => {
|
|
prepareEmpty()
|
|
await addDependenciesToPackage({}, ['webpack@4.46.0'], testDefaults({ autoInstallPeers: true }))
|
|
})
|
|
|
|
// This test may be removed if autoInstallPeers will become true by default
|
|
test('installation on a workspace with many complex circular dependencies does not fail when auto install peers is on', async () => {
|
|
prepareEmpty()
|
|
await mutateModules([
|
|
{
|
|
mutation: 'install',
|
|
rootDir: path.resolve('project1') as ProjectRootDir,
|
|
},
|
|
{
|
|
mutation: 'install',
|
|
rootDir: path.resolve('project2') as ProjectRootDir,
|
|
},
|
|
], testDefaults({
|
|
autoInstallPeers: true,
|
|
ignoreScripts: true,
|
|
lockfileOnly: true,
|
|
strictPeerDependencies: false,
|
|
registries: {
|
|
// A temporary workaround due to stylus removal from the npm registry.
|
|
// Related issue: https://github.com/stylus/stylus/issues/2938
|
|
default: 'https://registry.npmmirror.com',
|
|
},
|
|
allProjects: [
|
|
{
|
|
buildIndex: 0,
|
|
manifest: {
|
|
name: 'project1',
|
|
dependencies: {
|
|
'@angular/common': '14.2.4',
|
|
'@angular/core': '14.2.4',
|
|
'@angular/forms': '14.2.4',
|
|
'@angular/platform-browser': '14.2.4',
|
|
'@angular/platform-browser-dynamic': '14.2.4',
|
|
'@angular/router': '14.2.4',
|
|
'@capacitor/app': '4.0.1',
|
|
'@capacitor/core': '4.3.0',
|
|
'@capacitor/haptics': '4.0.1',
|
|
'@capacitor/keyboard': '4.0.1',
|
|
'@capacitor/status-bar': '4.0.1',
|
|
'@ionic/angular': '6.2.9',
|
|
'@ionic/core': '6.2.9',
|
|
ionicons: '6.0.3',
|
|
'ng-particles': '3.3.3',
|
|
rxjs: '7.5.7',
|
|
tslib: '2.4.0',
|
|
tsparticles: '2.3.4',
|
|
'tsparticles-engine': '2.3.3',
|
|
'tsparticles-interaction-external-attract': '2.3.3',
|
|
'tsparticles-interaction-external-bounce': '2.3.3',
|
|
'tsparticles-interaction-external-bubble': '2.3.3',
|
|
'tsparticles-interaction-external-connect': '2.3.3',
|
|
'tsparticles-interaction-external-grab': '2.3.3',
|
|
'tsparticles-interaction-external-pause': '2.3.3',
|
|
'tsparticles-interaction-external-push': '2.3.3',
|
|
'tsparticles-interaction-external-remove': '2.3.3',
|
|
'tsparticles-interaction-external-repulse': '2.3.4',
|
|
'tsparticles-interaction-external-slow': '2.3.3',
|
|
'tsparticles-interaction-external-trail': '2.3.3',
|
|
'tsparticles-interaction-particles-attract': '2.3.3',
|
|
'tsparticles-interaction-particles-collisions': '2.3.3',
|
|
'tsparticles-interaction-particles-links': '2.3.3',
|
|
'tsparticles-move-base': '2.3.3',
|
|
'tsparticles-move-parallax': '2.3.3',
|
|
'tsparticles-particles.js': '2.3.3',
|
|
'tsparticles-plugin-absorbers': '2.3.4',
|
|
'tsparticles-plugin-emitters': '2.3.4',
|
|
'tsparticles-plugin-polygon-mask': '2.3.3',
|
|
'tsparticles-shape-circle': '2.3.3',
|
|
'tsparticles-shape-image': '2.3.3',
|
|
'tsparticles-shape-line': '2.3.3',
|
|
'tsparticles-shape-polygon': '2.3.3',
|
|
'tsparticles-shape-square': '2.3.3',
|
|
'tsparticles-shape-star': '2.3.3',
|
|
'tsparticles-shape-text': '2.3.3',
|
|
'tsparticles-slim': '2.3.4',
|
|
'tsparticles-updater-angle': '2.3.3',
|
|
'tsparticles-updater-color': '2.3.3',
|
|
'tsparticles-updater-destroy': '2.3.3',
|
|
'tsparticles-updater-life': '2.3.3',
|
|
'tsparticles-updater-opacity': '2.3.3',
|
|
'tsparticles-updater-out-modes': '2.3.3',
|
|
'tsparticles-updater-roll': '2.3.3',
|
|
'tsparticles-updater-size': '2.3.3',
|
|
'tsparticles-updater-stroke-color': '2.3.3',
|
|
'tsparticles-updater-tilt': '2.3.3',
|
|
'tsparticles-updater-twinkle': '2.3.3',
|
|
'tsparticles-updater-wobble': '2.3.3',
|
|
'zone.js': '0.11.8',
|
|
},
|
|
devDependencies: {
|
|
'@angular-devkit/build-angular': '14.2.4',
|
|
'@angular-eslint/builder': '14.1.2',
|
|
'@angular-eslint/eslint-plugin': '14.1.2',
|
|
'@angular-eslint/eslint-plugin-template': '14.1.2',
|
|
'@angular-eslint/template-parser': '14.1.2',
|
|
'@angular/cli': '14.2.4',
|
|
'@angular/compiler': '14.2.4',
|
|
'@angular/compiler-cli': '14.2.4',
|
|
'@angular/language-service': '14.2.4',
|
|
'@capacitor/cli': '4.3.0',
|
|
'@ionic/angular-toolkit': '7.0.0',
|
|
'@types/jasmine': '4.3.0',
|
|
'@types/jasminewd2': '2.0.10',
|
|
'@types/node': '18.7.23',
|
|
'@typescript-eslint/eslint-plugin': '5.38.1',
|
|
'@typescript-eslint/parser': '5.38.1',
|
|
eslint: '8.24.0',
|
|
'eslint-plugin-import': '2.26.0',
|
|
'eslint-plugin-jsdoc': '39.3.6',
|
|
'eslint-plugin-prefer-arrow': '1.2.3',
|
|
'jasmine-core': '4.4.0',
|
|
'jasmine-spec-reporter': '7.0.0',
|
|
karma: '6.4.1',
|
|
'karma-chrome-launcher': '3.1.1',
|
|
'karma-coverage': '2.2.0',
|
|
'karma-coverage-istanbul-reporter': '3.0.3',
|
|
'karma-jasmine': '5.1.0',
|
|
'karma-jasmine-html-reporter': '2.0.0',
|
|
protractor: '7.0.0',
|
|
'ts-node': '10.9.1',
|
|
typescript: '4.8.4',
|
|
},
|
|
},
|
|
rootDir: path.resolve('project1') as ProjectRootDir,
|
|
},
|
|
{
|
|
buildIndex: 0,
|
|
manifest: {
|
|
name: 'project2',
|
|
devDependencies: {
|
|
'@typescript-eslint/eslint-plugin': '5.38.1',
|
|
'@typescript-eslint/parser': '5.38.1',
|
|
copyfiles: '2.4.1',
|
|
enzyme: '3.11.0',
|
|
'enzyme-adapter-preact-pure': '4.0.1',
|
|
eslint: '8.24.0',
|
|
'eslint-config-preact': '1.3.0',
|
|
'eslint-config-prettier': '8.5.0',
|
|
'eslint-plugin-react-hooks': '4.6.0',
|
|
'identity-obj-proxy': '3.0.0',
|
|
'preact-cli': '3.4.1',
|
|
prettier: '2.7.1',
|
|
'sirv-cli': '2.0.2',
|
|
},
|
|
dependencies: {
|
|
preact: '10.11.0',
|
|
'preact-particles': '2.3.3',
|
|
'preact-render-to-string': '5.2.4',
|
|
'preact-router': '4.1.0',
|
|
tsparticles: '2.3.4',
|
|
'tsparticles-engine': '2.3.3',
|
|
},
|
|
},
|
|
rootDir: path.resolve('project2') as ProjectRootDir,
|
|
},
|
|
],
|
|
}))
|
|
})
|
|
|
|
test('do not override the direct dependency with an auto installed peer dependency', async () => {
|
|
const includedDeps = new Set([
|
|
'@angular-devkit/build-angular',
|
|
'@angular/platform-browser-dynamic',
|
|
'inquirer',
|
|
'rxjs',
|
|
'@angular/common',
|
|
'rxjs',
|
|
])
|
|
const project = prepareEmpty()
|
|
await install({
|
|
dependencies: {
|
|
rxjs: '6.6.7',
|
|
},
|
|
devDependencies: {
|
|
'jest-preset-angular': '12.0.1',
|
|
},
|
|
}, testDefaults({
|
|
autoInstallPeers: true,
|
|
hooks: {
|
|
// This hook may be removed and the test will still be valid.
|
|
// The only reason the hook was added to remove the packages that aren't needed for the tests and make the test faster.
|
|
readPackage: [
|
|
(pkg: PackageManifest) => {
|
|
for (const depType of ['dependencies', 'optionalDependencies', 'peerDependencies', 'peerDependenciesMeta'] as const) {
|
|
if (pkg[depType]) {
|
|
for (const depName of Object.keys(pkg[depType] ?? {})) {
|
|
if (!includedDeps.has(depName)) {
|
|
delete pkg[depType]![depName]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (pkg.name === '@angular-devkit/build-angular' && pkg.dependencies) {
|
|
delete pkg.dependencies.rxjs
|
|
}
|
|
return pkg
|
|
},
|
|
],
|
|
},
|
|
}))
|
|
const lockfile = project.readLockfile()
|
|
expect(lockfile.importers['.'].dependencies?.rxjs.version).toBe('6.6.7')
|
|
})
|
|
|
|
test('auto install hoisted peer dependency', async () => {
|
|
await addDistTag({ package: '@pnpm.e2e/peer-c', version: '1.0.0', distTag: 'latest' })
|
|
const project = prepareEmpty()
|
|
await addDependenciesToPackage({}, ['@pnpm.e2e/has-peer-c-in-deps@1.0.0', '@pnpm.e2e/abc'], testDefaults({ autoInstallPeers: true }))
|
|
const lockfile = project.readLockfile()
|
|
expect(Object.keys(lockfile.snapshots).filter((depPath) => depPath.startsWith('@pnpm.e2e/peer-c@'))).toStrictEqual([
|
|
'@pnpm.e2e/peer-c@2.0.0',
|
|
])
|
|
})
|
|
|
|
test('auto install peer of optional peer', async () => {
|
|
const project = prepareEmpty()
|
|
await mutateModules([
|
|
{
|
|
mutation: 'install',
|
|
rootDir: path.resolve('project-1') as ProjectRootDir,
|
|
},
|
|
{
|
|
mutation: 'install',
|
|
rootDir: path.resolve('project-2') as ProjectRootDir,
|
|
},
|
|
], testDefaults({
|
|
allProjects: [
|
|
{
|
|
buildIndex: 0,
|
|
manifest: {
|
|
name: 'project-1',
|
|
dependencies: {
|
|
'@pnpm.e2e/has-optional-peer-with-peer': '1.0.0',
|
|
},
|
|
},
|
|
rootDir: path.resolve('project-1') as ProjectRootDir,
|
|
},
|
|
{
|
|
buildIndex: 0,
|
|
manifest: {
|
|
name: 'project-2',
|
|
dependencies: {
|
|
'@pnpm.e2e/has-y-peer': '1.0.0',
|
|
},
|
|
},
|
|
rootDir: path.resolve('project-2') as ProjectRootDir,
|
|
},
|
|
],
|
|
autoInstallPeers: true,
|
|
}))
|
|
const lockfile = project.readLockfile()
|
|
expect(Object.keys(lockfile.snapshots)).toStrictEqual([
|
|
'@pnpm.e2e/has-optional-peer-with-peer@1.0.0(@pnpm.e2e/has-y-peer@1.0.0(@pnpm/y@2.0.0))',
|
|
'@pnpm.e2e/has-y-peer@1.0.0(@pnpm/y@2.0.0)',
|
|
'@pnpm/y@2.0.0',
|
|
])
|
|
project.hasNot('@pnpm.e2e/peer-a')
|
|
})
|
|
|
|
test('override narrows auto-installed peer dep range on subsequent install', async () => {
|
|
// Scenario: a lockfile has peer-c@1.0.1 from a previous install.
|
|
// Then the user adds an override to pin peer-c to 1.0.0.
|
|
// The override should be respected, not the stale lockfile version.
|
|
await addDistTag({ package: '@pnpm.e2e/peer-c', version: '1.0.1', distTag: 'latest' })
|
|
const project = prepareEmpty()
|
|
|
|
// Step 1: install without override — auto-installs peer-c@1.0.1
|
|
const manifest = {
|
|
dependencies: {
|
|
'@pnpm.e2e/wants-peer-c-1': '1.0.0',
|
|
},
|
|
}
|
|
await install(manifest, testDefaults({ autoInstallPeers: true }))
|
|
|
|
{
|
|
const lockfile = project.readLockfile()
|
|
expect(lockfile.packages).toHaveProperty(['@pnpm.e2e/peer-c@1.0.1'])
|
|
}
|
|
|
|
// Step 2: reinstall with override narrowing peer-c to 1.0.0
|
|
const overrides = { '@pnpm.e2e/peer-c': '1.0.0' }
|
|
await mutateModulesInSingleProject({
|
|
manifest,
|
|
mutation: 'install',
|
|
rootDir: process.cwd() as ProjectRootDir,
|
|
}, testDefaults({ autoInstallPeers: true, overrides }))
|
|
|
|
{
|
|
const lockfile = project.readLockfile()
|
|
// peer-c should now be 1.0.0, not the stale 1.0.1 from the previous lockfile
|
|
expect(lockfile.packages).toHaveProperty(['@pnpm.e2e/peer-c@1.0.0'])
|
|
expect(lockfile.packages).not.toHaveProperty(['@pnpm.e2e/peer-c@1.0.1'])
|
|
}
|
|
})
|