mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-23 23:29:17 -05:00
5
.changeset/big-cows-hide.md
Normal file
5
.changeset/big-cows-hide.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/lockfile-types": minor
|
||||
---
|
||||
|
||||
A new optional field added to the lockfile type: resolutions.
|
||||
5
.changeset/fair-terms-serve.md
Normal file
5
.changeset/fair-terms-serve.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/types": minor
|
||||
---
|
||||
|
||||
A new optional field added to the ProjectManifest type: resolutions.
|
||||
5
.changeset/red-lizards-type.md
Normal file
5
.changeset/red-lizards-type.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/resolve-dependencies": patch
|
||||
---
|
||||
|
||||
When the version in the lockfile doesn't satisfy the range in the dependency's manifest, re-resolve the dependency.
|
||||
5
.changeset/tough-cheetahs-walk.md
Normal file
5
.changeset/tough-cheetahs-walk.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"supi": patch
|
||||
---
|
||||
|
||||
A resolutions field in the root project's manifest may be used to override the version ranges in dependencies of dependencies.
|
||||
@@ -2,6 +2,7 @@ export interface Lockfile {
|
||||
importers: Record<string, ProjectSnapshot>
|
||||
lockfileVersion: number
|
||||
packages?: PackageSnapshots
|
||||
resolutions?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface ProjectSnapshot {
|
||||
|
||||
@@ -399,29 +399,34 @@ function getDepsToResolve (
|
||||
// be merged with the resolved peers.
|
||||
let proceedAll = options.proceed
|
||||
const allPeers = new Set<string>()
|
||||
const satisfiesWanted2Args = referenceSatisfiesWantedSpec.bind(null, {
|
||||
lockfile: wantedLockfile,
|
||||
prefix: options.prefix,
|
||||
})
|
||||
for (const wantedDependency of wantedDependencies) {
|
||||
let reference = wantedDependency.alias && resolvedDependencies[wantedDependency.alias]
|
||||
let reference = undefined as undefined | string
|
||||
let proceed = proceedAll
|
||||
|
||||
// If dependencies that were used by the previous version of the package
|
||||
// satisfy the newer version's requirements, then pnpm tries to keep
|
||||
// the previous dependency.
|
||||
// So for example, if foo@1.0.0 had bar@1.0.0 as a dependency
|
||||
// and foo was updated to 1.1.0 which depends on bar ^1.0.0
|
||||
// then bar@1.0.0 can be reused for foo@1.1.0
|
||||
if (!reference && wantedDependency.alias && semver.validRange(wantedDependency.pref) !== null && // eslint-disable-line
|
||||
preferredDependencies[wantedDependency.alias] &&
|
||||
preferedSatisfiesWanted(
|
||||
preferredDependencies[wantedDependency.alias],
|
||||
wantedDependency as {alias: string, pref: string},
|
||||
wantedLockfile,
|
||||
{
|
||||
prefix: options.prefix,
|
||||
}
|
||||
)
|
||||
) {
|
||||
proceed = true
|
||||
reference = preferredDependencies[wantedDependency.alias]
|
||||
if (wantedDependency.alias) {
|
||||
const satisfiesWanted = satisfiesWanted2Args.bind(null, wantedDependency)
|
||||
if (
|
||||
resolvedDependencies[wantedDependency.alias] &&
|
||||
satisfiesWanted(resolvedDependencies[wantedDependency.alias])
|
||||
) {
|
||||
reference = resolvedDependencies[wantedDependency.alias]
|
||||
} else if (
|
||||
// If dependencies that were used by the previous version of the package
|
||||
// satisfy the newer version's requirements, then pnpm tries to keep
|
||||
// the previous dependency.
|
||||
// So for example, if foo@1.0.0 had bar@1.0.0 as a dependency
|
||||
// and foo was updated to 1.1.0 which depends on bar ^1.0.0
|
||||
// then bar@1.0.0 can be reused for foo@1.1.0
|
||||
semver.validRange(wantedDependency.pref) !== null && // eslint-disable-line
|
||||
preferredDependencies[wantedDependency.alias] &&
|
||||
satisfiesWanted(preferredDependencies[wantedDependency.alias])
|
||||
) {
|
||||
proceed = true
|
||||
reference = preferredDependencies[wantedDependency.alias]
|
||||
}
|
||||
}
|
||||
const infoFromLockfile = getInfoFromLockfile(wantedLockfile, options.registries, reference, wantedDependency.alias)
|
||||
if (
|
||||
@@ -458,17 +463,17 @@ function getDepsToResolve (
|
||||
return extendedWantedDeps
|
||||
}
|
||||
|
||||
function preferedSatisfiesWanted (
|
||||
preferredRef: string,
|
||||
wantedDep: {alias: string, pref: string},
|
||||
lockfile: Lockfile,
|
||||
function referenceSatisfiesWantedSpec (
|
||||
opts: {
|
||||
lockfile: Lockfile
|
||||
prefix: string
|
||||
}
|
||||
},
|
||||
wantedDep: {alias: string, pref: string},
|
||||
preferredRef: string
|
||||
) {
|
||||
const depPath = dp.refToRelative(preferredRef, wantedDep.alias)
|
||||
if (depPath === null) return false
|
||||
const pkgSnapshot = lockfile.packages?.[depPath]
|
||||
const pkgSnapshot = opts.lockfile.packages?.[depPath]
|
||||
if (!pkgSnapshot) {
|
||||
logger.warn({
|
||||
message: `Could not find preferred package ${depPath} in lockfile`,
|
||||
|
||||
@@ -19,6 +19,7 @@ import semver = require('semver')
|
||||
export default function allProjectsAreUpToDate (
|
||||
projects: Array<ProjectOptions & { id: string }>,
|
||||
opts: {
|
||||
resolutions?: Record<string, string>
|
||||
linkWorkspacePackages: boolean
|
||||
wantedLockfile: Lockfile
|
||||
workspacePackages: WorkspacePackages
|
||||
@@ -31,9 +32,9 @@ export default function allProjectsAreUpToDate (
|
||||
manifestsByDir,
|
||||
workspacePackages: opts.workspacePackages,
|
||||
})
|
||||
return pEvery(projects, (project) => {
|
||||
return R.equals(opts.wantedLockfile.resolutions ?? {}, opts.resolutions ?? {}) && pEvery(projects, (project) => {
|
||||
const importer = opts.wantedLockfile.importers[project.id]
|
||||
return importer && !hasLocalTarballDepsInRoot(importer) &&
|
||||
return !hasLocalTarballDepsInRoot(importer) &&
|
||||
_satisfiesPackageManifest(project.manifest, project.id) &&
|
||||
_linkedPackagesAreUpToDate({
|
||||
dir: project.rootDir,
|
||||
|
||||
37
packages/supi/src/install/createVersionsReplacer.ts
Normal file
37
packages/supi/src/install/createVersionsReplacer.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Dependencies, PackageManifest, ReadPackageHook } from '@pnpm/types'
|
||||
import parseWantedDependency from '@pnpm/parse-wanted-dependency'
|
||||
|
||||
export default function (resolutions: Record<string, string>): ReadPackageHook {
|
||||
const replacements = Object.entries(resolutions)
|
||||
.map(([rawWantedDependency, newPref]) => ({
|
||||
newPref,
|
||||
wantedDependency: parseWantedDependency(rawWantedDependency),
|
||||
} as VersionReplacement))
|
||||
return ((pkg: PackageManifest) => {
|
||||
if (pkg.dependencies) replaceDeps(replacements, pkg.dependencies)
|
||||
if (pkg.optionalDependencies) replaceDeps(replacements, pkg.optionalDependencies)
|
||||
return pkg
|
||||
}) as ReadPackageHook
|
||||
}
|
||||
|
||||
interface VersionReplacement {
|
||||
wantedDependency: {
|
||||
alias: string
|
||||
pref?: string
|
||||
}
|
||||
newPref: string
|
||||
}
|
||||
|
||||
function replaceDeps (replacements: VersionReplacement[], deps: Dependencies) {
|
||||
for (const replacement of replacements) {
|
||||
if (
|
||||
deps[replacement.wantedDependency.alias] &&
|
||||
(
|
||||
!replacement.wantedDependency.pref ||
|
||||
deps[replacement.wantedDependency.alias] === replacement.wantedDependency.pref
|
||||
)
|
||||
) {
|
||||
deps[replacement.wantedDependency.alias] = replacement.newPref
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,11 +42,13 @@ import {
|
||||
DependenciesField,
|
||||
DependencyManifest,
|
||||
ProjectManifest,
|
||||
ReadPackageHook,
|
||||
} from '@pnpm/types'
|
||||
import parseWantedDependencies from '../parseWantedDependencies'
|
||||
import safeIsInnerLink from '../safeIsInnerLink'
|
||||
import removeDeps from '../uninstall/removeDeps'
|
||||
import allProjectsAreUpToDate from './allProjectsAreUpToDate'
|
||||
import createVersionsReplacer from './createVersionsReplacer'
|
||||
import extendOptions, {
|
||||
InstallOptions,
|
||||
StrictInstallOptions,
|
||||
@@ -135,6 +137,18 @@ export async function mutateModules (
|
||||
const installsOnly = projects.every((project) => project.mutation === 'install')
|
||||
opts['forceNewModules'] = installsOnly
|
||||
const ctx = await getContext(projects, opts)
|
||||
const rootProject = ctx.projects.find(({ id }) => id === '.')
|
||||
if (!R.isEmpty(rootProject?.manifest.resolutions ?? {})) {
|
||||
const versionsReplacer = createVersionsReplacer(rootProject!.manifest.resolutions!)
|
||||
if (opts.hooks.readPackage) {
|
||||
opts.hooks.readPackage = R.pipe(
|
||||
opts.hooks.readPackage,
|
||||
versionsReplacer
|
||||
) as ReadPackageHook
|
||||
} else {
|
||||
opts.hooks.readPackage = versionsReplacer
|
||||
}
|
||||
}
|
||||
|
||||
for (const { manifest, rootDir } of ctx.projects) {
|
||||
if (!manifest) {
|
||||
@@ -164,6 +178,7 @@ export async function mutateModules (
|
||||
ctx.existsWantedLockfile &&
|
||||
ctx.wantedLockfile.lockfileVersion === LOCKFILE_VERSION &&
|
||||
await allProjectsAreUpToDate(ctx.projects, {
|
||||
resolutions: rootProject?.manifest.resolutions,
|
||||
linkWorkspacePackages: opts.linkWorkspacePackagesDepth >= 0,
|
||||
wantedLockfile: ctx.wantedLockfile,
|
||||
workspacePackages: opts.workspacePackages,
|
||||
@@ -401,6 +416,7 @@ export async function mutateModules (
|
||||
...opts,
|
||||
currentLockfileIsUpToDate: !ctx.existsWantedLockfile || ctx.currentLockfileIsUpToDate,
|
||||
makePartialCurrentLockfile,
|
||||
resolutions: rootProject?.manifest.resolutions,
|
||||
update: opts.update || !installsOnly,
|
||||
updateLockfileMinorVersion: true,
|
||||
})
|
||||
@@ -547,6 +563,7 @@ async function installInContext (
|
||||
makePartialCurrentLockfile: boolean
|
||||
updateLockfileMinorVersion: boolean
|
||||
preferredVersions?: PreferredVersions
|
||||
resolutions?: Record<string, string>
|
||||
currentLockfileIsUpToDate: boolean
|
||||
}
|
||||
) {
|
||||
@@ -571,6 +588,12 @@ async function installInContext (
|
||||
}
|
||||
}
|
||||
}
|
||||
const resolutionsChanged = !R.equals(opts.resolutions ?? {}, ctx.wantedLockfile.resolutions ?? {})
|
||||
if (!R.isEmpty(opts.resolutions ?? {})) {
|
||||
ctx.wantedLockfile.resolutions = opts.resolutions
|
||||
} else {
|
||||
delete ctx.wantedLockfile.resolutions
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
projects
|
||||
@@ -598,7 +621,8 @@ async function installInContext (
|
||||
)
|
||||
const forceFullResolution = ctx.wantedLockfile.lockfileVersion !== LOCKFILE_VERSION ||
|
||||
!opts.currentLockfileIsUpToDate ||
|
||||
opts.force
|
||||
opts.force ||
|
||||
resolutionsChanged
|
||||
const _toResolveImporter = toResolveImporter.bind(null, {
|
||||
defaultUpdateDepth: (opts.update || opts.updateMatching) ? opts.depth : -1,
|
||||
lockfileOnly: opts.lockfileOnly,
|
||||
|
||||
@@ -18,6 +18,7 @@ import './only'
|
||||
import './optionalDependencies'
|
||||
import './peerDependencies'
|
||||
import './reporting'
|
||||
import './resolutions'
|
||||
import './sideEffects'
|
||||
import './store'
|
||||
import './update'
|
||||
|
||||
54
packages/supi/test/install/resolutions.ts
Normal file
54
packages/supi/test/install/resolutions.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { prepareEmpty } from '@pnpm/prepare'
|
||||
import { addDistTag } from '@pnpm/registry-mock'
|
||||
import { addDependenciesToPackage, mutateModules } from 'supi'
|
||||
import promisifyTape from 'tape-promise'
|
||||
import {
|
||||
testDefaults,
|
||||
} from '../utils'
|
||||
import tape = require('tape')
|
||||
|
||||
const test = promisifyTape(tape)
|
||||
|
||||
test('versions are replaced with versions specified through resolutions field', async (t: tape.Test) => {
|
||||
const project = prepareEmpty(t)
|
||||
|
||||
await addDistTag({ package: 'bar', version: '100.0.0', distTag: 'latest' })
|
||||
|
||||
const manifest = await addDependenciesToPackage({
|
||||
resolutions: {
|
||||
'bar@^100.0.0': '100.1.0',
|
||||
'dep-of-pkg-with-1-dep': '101.0.0',
|
||||
},
|
||||
}, ['pkg-with-1-dep@100.0.0', 'foobar@100.0.0'], await testDefaults())
|
||||
|
||||
{
|
||||
const lockfile = await project.readLockfile()
|
||||
t.ok(lockfile.packages['/dep-of-pkg-with-1-dep/101.0.0'])
|
||||
t.ok(lockfile.packages['/bar/100.1.0'])
|
||||
t.deepEqual(lockfile.resolutions, {
|
||||
'bar@^100.0.0': '100.1.0',
|
||||
'dep-of-pkg-with-1-dep': '101.0.0',
|
||||
})
|
||||
}
|
||||
|
||||
// The lockfile is updated if the resolutions are changed
|
||||
manifest.resolutions['bar@^100.0.0'] = '100.0.0'
|
||||
await mutateModules([
|
||||
{
|
||||
buildIndex: 0,
|
||||
manifest,
|
||||
mutation: 'install',
|
||||
rootDir: process.cwd(),
|
||||
},
|
||||
], await testDefaults())
|
||||
|
||||
{
|
||||
const lockfile = await project.readLockfile()
|
||||
t.ok(lockfile.packages['/dep-of-pkg-with-1-dep/101.0.0'])
|
||||
t.ok(lockfile.packages['/bar/100.0.0'])
|
||||
t.deepEqual(lockfile.resolutions, {
|
||||
'bar@^100.0.0': '100.0.0',
|
||||
'dep-of-pkg-with-1-dep': '101.0.0',
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -83,6 +83,7 @@ export type DependencyManifest = BaseManifest & Required<Pick<BaseManifest, 'nam
|
||||
|
||||
export type ProjectManifest = BaseManifest & {
|
||||
private?: boolean
|
||||
resolutions?: Record<string, string>
|
||||
}
|
||||
|
||||
export type PackageManifest = DependencyManifest & {
|
||||
|
||||
Reference in New Issue
Block a user