feat: resolutions field

close #1221
PR #2939
This commit is contained in:
Zoltan Kochan
2020-10-18 22:51:24 +03:00
committed by GitHub
parent 3905b7e448
commit d54043ee4f
12 changed files with 174 additions and 30 deletions

View File

@@ -0,0 +1,5 @@
---
"@pnpm/lockfile-types": minor
---
A new optional field added to the lockfile type: resolutions.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/types": minor
---
A new optional field added to the ProjectManifest type: resolutions.

View 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.

View 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.

View File

@@ -2,6 +2,7 @@ export interface Lockfile {
importers: Record<string, ProjectSnapshot>
lockfileVersion: number
packages?: PackageSnapshots
resolutions?: Record<string, string>
}
export interface ProjectSnapshot {

View File

@@ -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`,

View File

@@ -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,

View 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
}
}
}

View File

@@ -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,

View File

@@ -18,6 +18,7 @@ import './only'
import './optionalDependencies'
import './peerDependencies'
import './reporting'
import './resolutions'
import './sideEffects'
import './store'
import './update'

View 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',
})
}
})

View File

@@ -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 & {