feat: pnpm.overrides

PR #2946
This commit is contained in:
Zoltan Kochan
2020-10-22 12:05:40 +03:00
committed by GitHub
parent 0b8fa39670
commit b5d694e7ff
10 changed files with 173 additions and 108 deletions

View File

@@ -0,0 +1,7 @@
---
"@pnpm/lockfile-types": patch
"supi": patch
"@pnpm/types": patch
---
Use pnpm.overrides instead of resolutions. Still support resolutions for partial compatibility with Yarn and for avoiding a breaking change.

View File

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

View File

@@ -19,7 +19,7 @@ import semver = require('semver')
export default function allProjectsAreUpToDate (
projects: Array<ProjectOptions & { id: string }>,
opts: {
resolutions?: Record<string, string>
overrides?: Record<string, string>
linkWorkspacePackages: boolean
wantedLockfile: Lockfile
workspacePackages: WorkspacePackages
@@ -32,7 +32,7 @@ export default function allProjectsAreUpToDate (
manifestsByDir,
workspacePackages: opts.workspacePackages,
})
return R.equals(opts.wantedLockfile.resolutions ?? {}, opts.resolutions ?? {}) && pEvery(projects, (project) => {
return R.equals(opts.wantedLockfile.overrides ?? {}, opts.overrides ?? {}) && pEvery(projects, (project) => {
const importer = opts.wantedLockfile.importers[project.id]
return !hasLocalTarballDepsInRoot(importer) &&
_satisfiesPackageManifest(project.manifest, project.id) &&

View File

@@ -0,0 +1,37 @@
import { Dependencies, PackageManifest, ReadPackageHook } from '@pnpm/types'
import parseWantedDependency from '@pnpm/parse-wanted-dependency'
export default function (overrides: Record<string, string>): ReadPackageHook {
const versionOverrides = Object.entries(overrides)
.map(([rawWantedDependency, newPref]) => ({
newPref,
wantedDependency: parseWantedDependency(rawWantedDependency),
} as VersionOverride))
return ((pkg: PackageManifest) => {
if (pkg.dependencies) overrideDeps(versionOverrides, pkg.dependencies)
if (pkg.optionalDependencies) overrideDeps(versionOverrides, pkg.optionalDependencies)
return pkg
}) as ReadPackageHook
}
interface VersionOverride {
wantedDependency: {
alias: string
pref?: string
}
newPref: string
}
function overrideDeps (versionOverrides: VersionOverride[], deps: Dependencies) {
for (const versionOverride of versionOverrides) {
if (
deps[versionOverride.wantedDependency.alias] &&
(
!versionOverride.wantedDependency.pref ||
deps[versionOverride.wantedDependency.alias] === versionOverride.wantedDependency.pref
)
) {
deps[versionOverride.wantedDependency.alias] = versionOverride.newPref
}
}
}

View File

@@ -1,37 +0,0 @@
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

@@ -48,7 +48,7 @@ import parseWantedDependencies from '../parseWantedDependencies'
import safeIsInnerLink from '../safeIsInnerLink'
import removeDeps from '../uninstall/removeDeps'
import allProjectsAreUpToDate from './allProjectsAreUpToDate'
import createVersionsReplacer from './createVersionsReplacer'
import createVersionsOverrider from './createVersionsOverrider'
import extendOptions, {
InstallOptions,
StrictInstallOptions,
@@ -138,15 +138,21 @@ export async function mutateModules (
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!)
// We read Yarn's resolutions field for compatibility
// but we really replace the version specs to any other version spec, not only to exact versions,
// so we cannot call it resolutions
const overrides = rootProject
? rootProject.manifest.pnpm?.overrides ?? rootProject.manifest.resolutions
: undefined
if (!R.isEmpty(overrides ?? {})) {
const versionsOverrider = createVersionsOverrider(overrides!)
if (opts.hooks.readPackage) {
opts.hooks.readPackage = R.pipe(
opts.hooks.readPackage,
versionsReplacer
versionsOverrider
) as ReadPackageHook
} else {
opts.hooks.readPackage = versionsReplacer
opts.hooks.readPackage = versionsOverrider
}
}
@@ -178,8 +184,8 @@ export async function mutateModules (
ctx.existsWantedLockfile &&
ctx.wantedLockfile.lockfileVersion === LOCKFILE_VERSION &&
await allProjectsAreUpToDate(ctx.projects, {
resolutions: rootProject?.manifest.resolutions,
linkWorkspacePackages: opts.linkWorkspacePackagesDepth >= 0,
overrides,
wantedLockfile: ctx.wantedLockfile,
workspacePackages: opts.workspacePackages,
})
@@ -417,7 +423,7 @@ export async function mutateModules (
...opts,
currentLockfileIsUpToDate: !ctx.existsWantedLockfile || ctx.currentLockfileIsUpToDate,
makePartialCurrentLockfile,
resolutions: rootProject?.manifest.resolutions,
overrides,
update: opts.update || !installsOnly,
updateLockfileMinorVersion: true,
})
@@ -562,9 +568,9 @@ async function installInContext (
ctx: PnpmContext<DependenciesMutation>,
opts: StrictInstallOptions & {
makePartialCurrentLockfile: boolean
overrides?: Record<string, string>
updateLockfileMinorVersion: boolean
preferredVersions?: PreferredVersions
resolutions?: Record<string, string>
currentLockfileIsUpToDate: boolean
}
) {
@@ -589,11 +595,14 @@ async function installInContext (
}
}
}
const resolutionsChanged = !R.equals(opts.resolutions ?? {}, ctx.wantedLockfile.resolutions ?? {})
if (!R.isEmpty(opts.resolutions ?? {})) {
ctx.wantedLockfile.resolutions = opts.resolutions
const overridesChanged = !R.equals(opts.overrides ?? {}, ctx.wantedLockfile.overrides ?? {})
if (!R.isEmpty(opts.overrides ?? {})) {
ctx.wantedLockfile.overrides = opts.overrides
} else {
delete ctx.wantedLockfile.resolutions
delete ctx.wantedLockfile.overrides
// We were only setting the resolutions field in pnpm v5.10.0, which was never latest,
// so we can probably remove this line safely in future pnpm versions.
delete ctx.wantedLockfile['resolutions']
}
await Promise.all(
@@ -623,7 +632,7 @@ async function installInContext (
const forceFullResolution = ctx.wantedLockfile.lockfileVersion !== LOCKFILE_VERSION ||
!opts.currentLockfileIsUpToDate ||
opts.force ||
resolutionsChanged
overridesChanged
const _toResolveImporter = toResolveImporter.bind(null, {
defaultUpdateDepth: (opts.update || opts.updateMatching) ? opts.depth : -1,
lockfileOnly: opts.lockfileOnly,

View File

@@ -16,9 +16,9 @@ import './modulesDir'
import './multipleImporters'
import './only'
import './optionalDependencies'
import './overrides'
import './peerDependencies'
import './reporting'
import './resolutions'
import './sideEffects'
import './store'
import './update'

View File

@@ -0,0 +1,100 @@
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 pnpm.overrides field', async (t: tape.Test) => {
const project = prepareEmpty(t)
await addDistTag({ package: 'bar', version: '100.0.0', distTag: 'latest' })
const manifest = await addDependenciesToPackage({
pnpm: {
overrides: {
'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.overrides, {
'bar@^100.0.0': '100.1.0',
'dep-of-pkg-with-1-dep': '101.0.0',
})
}
// The lockfile is updated if the overrides are changed
manifest.pnpm.overrides['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.overrides, {
'bar@^100.0.0': '100.0.0',
'dep-of-pkg-with-1-dep': '101.0.0',
})
}
})
test('versions are replaced with versions specified through "resolutions" field (for Yarn compatibility)', 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.overrides, {
'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.overrides, {
'bar@^100.0.0': '100.0.0',
'dep-of-pkg-with-1-dep': '101.0.0',
})
}
})

View File

@@ -1,54 +0,0 @@
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

@@ -82,6 +82,9 @@ interface BaseManifest {
export type DependencyManifest = BaseManifest & Required<Pick<BaseManifest, 'name' | 'version'>>
export type ProjectManifest = BaseManifest & {
pnpm?: {
overrides?: Record<string, string>
}
private?: boolean
resolutions?: Record<string, string>
}