feat: autofix merge conflicts in pnpm-lock.yaml

close #2036
PR #2965
This commit is contained in:
Zoltan Kochan
2020-11-03 22:17:24 +02:00
committed by GitHub
parent 89a276390f
commit 3776b5a525
19 changed files with 798 additions and 24 deletions

View File

@@ -0,0 +1,5 @@
---
"@pnpm/lockfile-file": minor
---
New function added that reads the lockfile and autofixes any merge conflicts.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/merge-lockfiles": major
---
Initial release.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/get-context": minor
---
A new option added to the context: lockfileHadConflicts.

View File

@@ -28,6 +28,7 @@ export interface PnpmContext<T> {
existsCurrentLockfile: boolean
existsWantedLockfile: boolean
extraBinPaths: string[]
lockfileHadConflicts: boolean
hoistedDependencies: HoistedDependencies
include: IncludedDependencies
modulesFile: Modules | null
@@ -62,6 +63,7 @@ interface HookOptions {
export default async function getContext<T> (
projects: Array<ProjectOptions & HookOptions & T>,
opts: {
autofixMergeConflicts?: boolean
force: boolean
forceNewModules?: boolean
forceSharedLockfile: boolean
@@ -156,6 +158,7 @@ export default async function getContext<T> (
storeDir: opts.storeDir,
virtualStoreDir,
...await readLockfileFile({
autofixMergeConflicts: opts.autofixMergeConflicts === true,
force: opts.force,
forceSharedLockfile: opts.forceSharedLockfile,
lockfileDir: opts.lockfileDir,
@@ -322,6 +325,7 @@ export interface PnpmSingleContext {
existsCurrentLockfile: boolean
existsWantedLockfile: boolean
extraBinPaths: string[]
lockfileHadConflicts: boolean
hoistedDependencies: HoistedDependencies
hoistedModulesDir: string
hoistPattern: string[] | undefined
@@ -345,6 +349,7 @@ export interface PnpmSingleContext {
export async function getContextForSingleImporter (
manifest: ProjectManifest,
opts: {
autofixMergeConflicts?: boolean
force: boolean
forceNewModules?: boolean
forceSharedLockfile: boolean
@@ -453,6 +458,7 @@ export async function getContextForSingleImporter (
storeDir,
virtualStoreDir,
...await readLockfileFile({
autofixMergeConflicts: opts.autofixMergeConflicts === true,
force: opts.force,
forceSharedLockfile: opts.forceSharedLockfile,
lockfileDir: opts.lockfileDir,

View File

@@ -8,6 +8,7 @@ import {
Lockfile,
readCurrentLockfile,
readWantedLockfile,
readWantedLockfileAndAutofixConflicts,
} from '@pnpm/lockfile-file'
import logger from '@pnpm/logger'
import isCI = require('is-ci')
@@ -22,6 +23,7 @@ export interface PnpmContext {
export default async function (
opts: {
autofixMergeConflicts: boolean
force: boolean
forceSharedLockfile: boolean
projects: Array<{
@@ -39,6 +41,7 @@ export default async function (
existsCurrentLockfile: boolean
existsWantedLockfile: boolean
wantedLockfile: Lockfile
lockfileHadConflicts: boolean
}> {
// ignore `pnpm-lock.yaml` on CI servers
// a latest pnpm should not break all the builds
@@ -46,16 +49,32 @@ export default async function (
ignoreIncompatible: opts.force || isCI,
wantedVersion: LOCKFILE_VERSION,
}
const fileReads = [] as Array<Promise<Lockfile | undefined | null>>
let lockfileHadConflicts: boolean = false
if (opts.useLockfile) {
if (opts.autofixMergeConflicts) {
fileReads.push(
readWantedLockfileAndAutofixConflicts(opts.lockfileDir, lockfileOpts)
.then(({ lockfile, hadConflicts }) => {
lockfileHadConflicts = hadConflicts
return lockfile
})
)
} else {
fileReads.push(readWantedLockfile(opts.lockfileDir, lockfileOpts))
}
} else {
if (await existsWantedLockfile(opts.lockfileDir)) {
logger.warn({
message: `A ${WANTED_LOCKFILE} file exists. The current configuration prohibits to read or write a lockfile`,
prefix: opts.lockfileDir,
})
}
fileReads.push(Promise.resolve(undefined))
}
fileReads.push(readCurrentLockfile(opts.virtualStoreDir, lockfileOpts))
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
const files = await Promise.all<Lockfile | null | undefined>([
(opts.useLockfile && readWantedLockfile(opts.lockfileDir, lockfileOpts)) ||
(await existsWantedLockfile(opts.lockfileDir) &&
logger.warn({
message: `A ${WANTED_LOCKFILE} file exists. The current configuration prohibits to read or write a lockfile`,
prefix: opts.lockfileDir,
})) || undefined,
readCurrentLockfile(opts.virtualStoreDir, lockfileOpts),
])
const files = await Promise.all<Lockfile | null | undefined>(fileReads)
const sopts = { lockfileVersion: LOCKFILE_VERSION }
const importerIds = opts.projects.map((importer) => importer.id)
const currentLockfile = files[1] ?? createLockfileObject(importerIds, sopts)
@@ -82,5 +101,6 @@ export default async function (
existsCurrentLockfile: !!files[1],
existsWantedLockfile: !!files[0],
wantedLockfile,
lockfileHadConflicts,
}
}

View File

@@ -49,13 +49,14 @@
"@pnpm/constants": "workspace:4.1.0",
"@pnpm/error": "workspace:1.3.1",
"@pnpm/lockfile-types": "workspace:2.1.1",
"@pnpm/merge-lockfiles": "workspace:^0.0.0",
"@pnpm/types": "workspace:6.3.1",
"@zkochan/rimraf": "^1.0.0",
"js-yaml": "^3.14.0",
"mz": "^2.7.0",
"normalize-path": "^3.0.0",
"ramda": "^0.27.1",
"read-yaml-file": "^2.0.0",
"strip-bom": "^4.0.0",
"write-file-atomic": "^3.0.3"
},
"funding": "https://opencollective.com/pnpm"

View File

@@ -0,0 +1,55 @@
import { Lockfile } from '@pnpm/lockfile-types'
import mergeLockfiles from '@pnpm/merge-lockfiles'
import yaml = require('js-yaml')
const MERGE_CONFLICT_PARENT = '|||||||'
const MERGE_CONFLICT_END = '>>>>>>>'
const MERGE_CONFLICT_THEIRS = '======='
const MERGE_CONFLICT_OURS = '<<<<<<<'
export function autofixMergeConflicts (fileContent: string) {
const { ours, theirs } = parseMergeFile(fileContent)
const oursParsed = yaml.safeLoad(ours) as Lockfile
return mergeLockfiles({
base: oursParsed,
ours: oursParsed,
theirs: yaml.safeLoad(theirs) as Lockfile,
})
}
function parseMergeFile (fileContent: string) {
const lines = fileContent.split(/[\n\r]+/g) as string[]
let state: 'top' | 'ours' | 'theirs' | 'parent' = 'top'
const ours = []
const theirs = []
const base = []
while (lines.length > 0) {
const line = lines.shift() as string
if (line.startsWith(MERGE_CONFLICT_PARENT)) {
state = 'parent'
continue
}
if (line.startsWith(MERGE_CONFLICT_OURS)) {
state = 'ours'
continue
}
if (line === MERGE_CONFLICT_THEIRS) {
state = 'theirs'
continue
}
if (line.startsWith(MERGE_CONFLICT_END)) {
state = 'top'
continue
}
if (state === 'top' || state === 'ours') ours.push(line)
if (state === 'top' || state === 'theirs') theirs.push(line)
if (state === 'top' || state === 'parent') base.push(line)
}
return { ours: ours.join('\n'), theirs: theirs.join('\n'), base: base.join('\n') }
}
export function isDiff (fileContent: string) {
return fileContent.includes(MERGE_CONFLICT_OURS) &&
fileContent.includes(MERGE_CONFLICT_THEIRS) &&
fileContent.includes(MERGE_CONFLICT_END)
}

View File

@@ -4,12 +4,15 @@ import {
} from '@pnpm/constants'
import { Lockfile } from '@pnpm/lockfile-types'
import { DEPENDENCIES_FIELDS } from '@pnpm/types'
import readYamlFile from 'read-yaml-file'
import { LockfileBreakingChangeError } from './errors'
import { autofixMergeConflicts, isDiff } from './gitMergeFile'
import logger from './logger'
import yaml = require('js-yaml')
import path = require('path')
import stripBom = require('strip-bom')
import fs = require('mz/fs')
export function readCurrentLockfile (
export async function readCurrentLockfile (
virtualStoreDir: string,
opts: {
wantedVersion?: number
@@ -17,10 +20,24 @@ export function readCurrentLockfile (
}
): Promise<Lockfile | null> {
const lockfilePath = path.join(virtualStoreDir, 'lock.yaml')
return _read(lockfilePath, virtualStoreDir, opts)
return (await _read(lockfilePath, virtualStoreDir, opts)).lockfile
}
export function readWantedLockfile (
export function readWantedLockfileAndAutofixConflicts (
pkgPath: string,
opts: {
wantedVersion?: number
ignoreIncompatible: boolean
}
): Promise<{
lockfile: Lockfile | null
hadConflicts: boolean
}> {
const lockfilePath = path.join(pkgPath, WANTED_LOCKFILE)
return _read(lockfilePath, pkgPath, { ...opts, autofixMergeConflicts: true })
}
export async function readWantedLockfile (
pkgPath: string,
opts: {
wantedVersion?: number
@@ -28,25 +45,48 @@ export function readWantedLockfile (
}
): Promise<Lockfile | null> {
const lockfilePath = path.join(pkgPath, WANTED_LOCKFILE)
return _read(lockfilePath, pkgPath, opts)
return (await _read(lockfilePath, pkgPath, opts)).lockfile
}
async function _read (
lockfilePath: string,
prefix: string,
opts: {
autofixMergeConflicts?: boolean
wantedVersion?: number
ignoreIncompatible: boolean
}
): Promise<Lockfile | null> {
let lockfile
): Promise<{
lockfile: Lockfile | null
hadConflicts: boolean
}> {
let lockfileRawContent
try {
lockfile = await readYamlFile<Lockfile>(lockfilePath)
lockfileRawContent = stripBom(await fs.readFile(lockfilePath, 'utf8'))
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
throw err
}
return null
return {
lockfile: null,
hadConflicts: false,
}
}
let lockfile: Lockfile
let hadConflicts!: boolean
try {
lockfile = yaml.safeLoad(lockfileRawContent) as Lockfile
hadConflicts = false
} catch (err) {
if (!opts.autofixMergeConflicts || !isDiff(lockfileRawContent)) {
throw err
}
hadConflicts = true
lockfile = autofixMergeConflicts(lockfileRawContent)
logger.info({
message: `Merge conflict detected in ${WANTED_LOCKFILE} and successfully merged`,
prefix: path.dirname(lockfilePath),
})
}
/* eslint-disable @typescript-eslint/dot-notation */
if (typeof lockfile?.['specifiers'] !== 'undefined') {
@@ -73,7 +113,7 @@ async function _read (
prefix,
})
}
return lockfile
return { lockfile, hadConflicts }
}
}
if (opts.ignoreIncompatible) {
@@ -81,7 +121,7 @@ async function _read (
message: `Ignoring not compatible lockfile at ${lockfilePath}`,
prefix,
})
return null
return { lockfile: null, hadConflicts: false }
}
throw new LockfileBreakingChangeError(lockfilePath)
}

View File

@@ -18,6 +18,9 @@
{
"path": "../lockfile-types"
},
{
"path": "../merge-lockfiles"
},
{
"path": "../types"
}

View File

@@ -0,0 +1,13 @@
# @pnpm/merge-lockfiles
> Merges lockfiles. Can automatically fix merge conflicts
## Install
```
pnpm add @pnpm/merge-lockfiles
```
## License
[MIT](LICENSE)

View File

@@ -0,0 +1 @@
module.exports = require('../../jest.config.js')

View File

@@ -0,0 +1,43 @@
{
"name": "@pnpm/merge-lockfiles",
"version": "0.0.0",
"description": "Merges lockfiles. Can automatically fix merge conflicts",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
"engines": {
"node": ">=10.16"
},
"files": [
"lib",
"!*.map"
],
"scripts": {
"lint": "eslint -c ../../eslint.json src/**/*.ts test/**/*.ts",
"_test": "jest",
"test": "pnpm run compile && pnpm run _test",
"prepublishOnly": "pnpm run compile",
"compile": "rimraf lib tsconfig.tsbuildinfo && tsc --build"
},
"repository": "https://github.com/pnpm/pnpm/blob/master/packages/merge-lockfiles",
"keywords": [
"pnpm",
"shrinkwrap",
"lockfile"
],
"author": "Zoltan Kochan <z@kochan.io> (https://www.kochan.io/)",
"license": "MIT",
"bugs": {
"url": "https://github.com/pnpm/pnpm/issues"
},
"homepage": "https://github.com/pnpm/pnpm/blob/master/packages/merge-lockfiles#readme",
"dependencies": {
"@pnpm/lockfile-types": "workspace:^2.1.1",
"ramda": "^0.27.1",
"semver": "^7.3.2"
},
"funding": "https://opencollective.com/pnpm",
"devDependencies": {
"@types/ramda": "^0.27.32",
"@types/semver": "^7.3.4"
}
}

View File

@@ -0,0 +1,113 @@
import { Lockfile } from '@pnpm/lockfile-types'
import R = require('ramda')
import semver = require('semver')
export default function mergeLockfile (
opts: {
base: Lockfile
ours: Lockfile
theirs: Lockfile
}
) {
const newLockfile: Lockfile = {
importers: {},
lockfileVersion: Math.max(opts.base.lockfileVersion, opts.ours.lockfileVersion),
}
for (const importerId of Array.from(new Set([...Object.keys(opts.ours.importers), ...Object.keys(opts.theirs.importers)]))) {
newLockfile.importers[importerId] = {
specifiers: {},
}
for (const key of ['dependencies', 'devDependencies', 'optionalDependencies']) {
newLockfile.importers[importerId][key] = mergeDict(
opts.ours.importers[importerId]?.[key] ?? {},
opts.base.importers[importerId]?.[key] ?? {},
opts.theirs.importers[importerId]?.[key] ?? {},
key,
mergeVersions
)
if (!Object.keys(newLockfile.importers[importerId][key]).length) {
delete newLockfile.importers[importerId][key]
}
}
newLockfile.importers[importerId].specifiers = mergeDict(
opts.ours.importers[importerId]?.specifiers ?? {},
opts.base.importers[importerId]?.specifiers ?? {},
opts.theirs.importers[importerId]?.specifiers ?? {},
'specifiers',
takeChangedValue
)
}
const packages = {}
for (const depPath of Array.from(new Set([...Object.keys(opts.ours.packages ?? {}), ...Object.keys(opts.theirs.packages ?? {})]))) {
const basePkg = opts.base.packages?.[depPath]
const ourPkg = opts.ours.packages?.[depPath]
const theirPkg = opts.theirs.packages?.[depPath]
const pkg = {
...basePkg,
...ourPkg,
...theirPkg,
}
for (const key of ['dependencies', 'optionalDependencies']) {
pkg[key] = mergeDict(
ourPkg?.[key] ?? {},
basePkg?.[key] ?? {},
theirPkg?.[key] ?? {},
key,
mergeVersions
)
if (!Object.keys(pkg[key]).length) {
delete pkg[key]
}
}
packages[depPath] = pkg
}
newLockfile.packages = packages
return newLockfile
}
type ValueMerger<T> = (ourValue: T, baseValue: T, theirValue: T, fieldName: string) => T
function mergeDict<T> (
ourDict: Record<string, T>,
baseDict: Record<string, T>,
theirDict: Record<string, T>,
fieldName: string,
valueMerger: ValueMerger<T>
) {
const newDict = {}
for (const key of R.keys(ourDict).concat(R.keys(theirDict))) {
const changedValue = valueMerger(
ourDict[key],
baseDict[key],
theirDict[key],
`${fieldName}.${key}`
)
if (changedValue) {
newDict[key] = changedValue
}
}
return newDict
}
function takeChangedValue<T> (ourValue: T, baseValue: T, theirValue: T, fieldName: string): T {
if (ourValue === theirValue) return ourValue
if (baseValue === ourValue) return theirValue
if (baseValue === theirValue) return ourValue
// eslint-disable-next-line
throw new Error(`Cannot resolve '${fieldName}'. Base value: ${baseValue}. Our: ${ourValue}. Their: ${theirValue}`)
}
function mergeVersions (ourValue: string, baseValue: string, theirValue: string, fieldName: string) {
if (ourValue === theirValue) return ourValue
if (baseValue === ourValue) return theirValue
if (baseValue === theirValue) return ourValue
const [ourVersion] = ourValue.split('_')
const [theirVersion] = theirValue.split('_')
if (semver.gt(ourVersion, theirVersion)) {
return ourValue
}
return theirValue
}

View File

@@ -0,0 +1,383 @@
import { Lockfile } from '@pnpm/lockfile-types'
import mergeLockfile from '../src'
const simpleLockfile = {
importers: {
'.': {
dependencies: {
foo: '1.0.0',
},
specifiers: {
foo: '1.0.0',
},
},
},
lockfileVersion: 5.2,
packages: {
'/foo/1.0.0': {
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
},
}
test('fails when specifiers differ', () => {
expect(() => {
mergeLockfile({
base: simpleLockfile,
ours: {
...simpleLockfile,
importers: {
'.': {
...simpleLockfile.importers['.'],
specifiers: { foo: '^1.0.0' },
},
},
},
theirs: {
...simpleLockfile,
importers: {
'.': {
...simpleLockfile.importers['.'],
specifiers: { foo: '^1.1.0' },
},
},
},
})
}).toThrowError(/Cannot resolve 'specifiers.foo'/)
})
test('picks the newer version when dependencies differ inside importer', () => {
const mergedLockfile = mergeLockfile({
base: simpleLockfile,
ours: {
...simpleLockfile,
importers: {
'.': {
...simpleLockfile.importers['.'],
dependencies: {
foo: '1.2.0',
bar: '3.0.0_qar@1.0.0',
zoo: '4.0.0_qar@1.0.0',
},
},
},
},
theirs: {
...simpleLockfile,
importers: {
'.': {
...simpleLockfile.importers['.'],
dependencies: {
foo: '1.1.0',
bar: '4.0.0_qar@1.0.0',
zoo: '3.0.0_qar@1.0.0',
},
},
},
},
})
expect(mergedLockfile.importers['.'].dependencies?.foo).toBe('1.2.0')
expect(mergedLockfile.importers['.'].dependencies?.bar).toBe('4.0.0_qar@1.0.0')
expect(mergedLockfile.importers['.'].dependencies?.zoo).toBe('4.0.0_qar@1.0.0')
})
test('picks the newer version when dependencies differ inside package', () => {
const base: Lockfile = {
importers: {
'.': {
dependencies: {
a: '1.0.0',
},
specifiers: {},
},
},
lockfileVersion: 5.2,
packages: {
'/a/1.0.0': {
dependencies: {
foo: '1.0.0',
},
resolution: {
integrity: '',
},
},
'/foo/1.0.0': {
resolution: {
integrity: '',
},
},
},
}
const mergedLockfile = mergeLockfile({
base,
ours: {
...base,
packages: {
...base.packages,
'/a/1.0.0': {
dependencies: {
linked: 'link:../1',
foo: '1.2.0',
bar: '3.0.0_qar@1.0.0',
zoo: '4.0.0_qar@1.0.0',
qar: '1.0.0',
},
resolution: {
integrity: '',
},
},
'/bar/3.0.0_qar@1.0.0': {
dependencies: {
qar: '1.0.0',
},
resolution: {
integrity: '',
},
},
'/zoo/4.0.0_qar@1.0.0': {
dependencies: {
qar: '1.0.0',
},
resolution: {
integrity: '',
},
},
'/foo/1.2.0': {
resolution: {
integrity: '',
},
},
'/qar/1.0.0': {
resolution: {
integrity: '',
},
},
},
},
theirs: {
...base,
packages: {
...base.packages,
'/a/1.0.0': {
dependencies: {
linked: 'link:../1',
foo: '1.1.0',
bar: '4.0.0_qar@1.0.0',
zoo: '3.0.0_qar@1.0.0',
qar: '1.0.0',
},
resolution: {
integrity: '',
},
},
'/bar/4.0.0_qar@1.0.0': {
dependencies: {
qar: '1.0.0',
},
resolution: {
integrity: '',
},
},
'/zoo/3.0.0_qar@1.0.0': {
dependencies: {
qar: '1.0.0',
},
resolution: {
integrity: '',
},
},
'/foo/1.1.0': {
resolution: {
integrity: '',
},
},
'/qar/1.0.0': {
resolution: {
integrity: '',
},
},
},
},
})
expect(mergedLockfile.packages?.['/a/1.0.0'].dependencies?.linked).toBe('link:../1')
expect(mergedLockfile.packages?.['/a/1.0.0'].dependencies?.foo).toBe('1.2.0')
expect(mergedLockfile.packages?.['/a/1.0.0'].dependencies?.bar).toBe('4.0.0_qar@1.0.0')
expect(mergedLockfile.packages?.['/a/1.0.0'].dependencies?.zoo).toBe('4.0.0_qar@1.0.0')
expect(Object.keys(mergedLockfile.packages ?? {}).sort()).toStrictEqual([
'/a/1.0.0',
'/bar/3.0.0_qar@1.0.0',
'/bar/4.0.0_qar@1.0.0',
'/foo/1.0.0',
'/foo/1.1.0',
'/foo/1.2.0',
'/qar/1.0.0',
'/zoo/3.0.0_qar@1.0.0',
'/zoo/4.0.0_qar@1.0.0',
])
})
test('prefers our lockfile resolutions when it has newer packages', () => {
const mergedLockfile = mergeLockfile({
base: simpleLockfile,
ours: {
...simpleLockfile,
packages: {
'/foo/1.0.0': {
dependencies: {
bar: '1.0.0',
},
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
'/bar/1.0.0': {
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
},
},
theirs: {
...simpleLockfile,
packages: {
'/foo/1.0.0': {
dependencies: {
bar: '1.1.0',
},
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
'/bar/1.1.0': {
dependencies: {
qar: '1.0.0',
},
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
'/qar/1.0.0': {
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
},
},
})
expect(mergedLockfile).toStrictEqual({
...simpleLockfile,
packages: {
'/foo/1.0.0': {
dependencies: {
bar: '1.1.0',
},
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
'/bar/1.0.0': {
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
'/bar/1.1.0': {
dependencies: {
qar: '1.0.0',
},
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
'/qar/1.0.0': {
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
},
})
})
test('prefers our lockfile resolutions when it has newer packages', () => {
const mergedLockfile = mergeLockfile({
base: simpleLockfile,
theirs: {
...simpleLockfile,
packages: {
'/foo/1.0.0': {
dependencies: {
bar: '1.0.0',
},
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
'/bar/1.0.0': {
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
},
},
ours: {
...simpleLockfile,
packages: {
'/foo/1.0.0': {
dependencies: {
bar: '1.1.0',
},
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
'/bar/1.1.0': {
dependencies: {
qar: '1.0.0',
},
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
'/qar/1.0.0': {
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
},
},
})
expect(mergedLockfile).toStrictEqual({
...simpleLockfile,
packages: {
'/foo/1.0.0': {
dependencies: {
bar: '1.1.0',
},
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
'/bar/1.0.0': {
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
'/bar/1.1.0': {
dependencies: {
qar: '1.0.0',
},
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
'/qar/1.0.0': {
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
},
})
})

View File

@@ -0,0 +1,16 @@
{
"extends": "@pnpm/tsconfig",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
},
"include": [
"src/**/*.ts",
"../../typings/**/*.d.ts"
],
"references": [
{
"path": "../lockfile-types"
}
]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": [
"src/**/*.ts",
"test/**/*.ts",
"../../typings/**/*.d.ts"
]
}

View File

@@ -136,6 +136,7 @@ export async function mutateModules (
const installsOnly = projects.every((project) => project.mutation === 'install')
opts['forceNewModules'] = installsOnly
opts['autofixMergeConflicts'] = !opts.frozenLockfile
const ctx = await getContext(projects, opts)
const rootProjectManifest = ctx.projects.find(({ id }) => id === '.')?.manifest ??
// When running install/update on a subset of projects, the root project might not be included,
@@ -177,6 +178,7 @@ export async function mutateModules (
const frozenLockfile = opts.frozenLockfile ||
opts.frozenLockfileIfExists && ctx.existsWantedLockfile
if (
!ctx.lockfileHadConflicts &&
!opts.lockfileOnly &&
!opts.update &&
installsOnly &&
@@ -635,7 +637,8 @@ async function installInContext (
const forceFullResolution = ctx.wantedLockfile.lockfileVersion !== LOCKFILE_VERSION ||
!opts.currentLockfileIsUpToDate ||
opts.force ||
overridesChanged
overridesChanged ||
ctx.lockfileHadConflicts
const _toResolveImporter = toResolveImporter.bind(null, {
defaultUpdateDepth: (opts.update || opts.updateMatching) ? opts.depth : -1,
lockfileOnly: opts.lockfileOnly,

View File

@@ -1229,3 +1229,41 @@ test('tarball installed through non-standard URL endpoint from the registry doma
},
})
})
test('a lockfile with merge conflicts is autofixed', async (t: tape.Test) => {
const project = prepareEmpty(t)
await fs.writeFile(WANTED_LOCKFILE, `\
importers:
.:
dependencies:
<<<<<<< HEAD
dep-of-pkg-with-1-dep: 100.0.0
=======
dep-of-pkg-with-1-dep: 100.1.0
>>>>>>> next
specifiers:
dep-of-pkg-with-1-dep: '>100.0.0'
lockfileVersion: ${LOCKFILE_VERSION}
packages:
<<<<<<< HEAD
/dep-of-pkg-with-1-dep/100.0.0:
dev: false
resolution:
integrity: ${getIntegrity('dep-of-pkg-with-1-dep', '100.0.0')}
=======
/dep-of-pkg-with-1-dep/100.1.0:
dev: false
resolution:
integrity: ${getIntegrity('dep-of-pkg-with-1-dep', '100.1.0')}
>>>>>>> next`, 'utf8')
await install({
dependencies: {
'dep-of-pkg-with-1-dep': '>100.0.0',
},
}, await testDefaults())
const lockfile = await project.readLockfile()
t.equal(lockfile.dependencies['dep-of-pkg-with-1-dep'], '100.1.0')
})

20
pnpm-lock.yaml generated
View File

@@ -938,13 +938,14 @@ importers:
'@pnpm/constants': 'link:../constants'
'@pnpm/error': 'link:../error'
'@pnpm/lockfile-types': 'link:../lockfile-types'
'@pnpm/merge-lockfiles': 'link:../merge-lockfiles'
'@pnpm/types': 'link:../types'
'@zkochan/rimraf': 1.0.0
js-yaml: 3.14.0
mz: 2.7.0
normalize-path: 3.0.0
ramda: 0.27.1
read-yaml-file: 2.0.0
strip-bom: 4.0.0
write-file-atomic: 3.0.3
devDependencies:
'@pnpm/lockfile-file': 'link:'
@@ -963,6 +964,7 @@ importers:
'@pnpm/lockfile-file': 'link:'
'@pnpm/lockfile-types': 'workspace:2.1.1'
'@pnpm/logger': ^3.2.2
'@pnpm/merge-lockfiles': 'workspace:^0.0.0'
'@pnpm/types': 'workspace:6.3.1'
'@types/js-yaml': ^3.12.5
'@types/mz': ^2.7.2
@@ -974,7 +976,7 @@ importers:
mz: ^2.7.0
normalize-path: ^3.0.0
ramda: ^0.27.1
read-yaml-file: ^2.0.0
strip-bom: ^4.0.0
tempy: ^1.0.0
write-file-atomic: ^3.0.3
write-yaml-file: ^4.1.1
@@ -1123,6 +1125,20 @@ importers:
specifiers:
'@pnpm/matcher': 'link:'
escape-string-regexp: ^4.0.0
packages/merge-lockfiles:
dependencies:
'@pnpm/lockfile-types': 'link:../lockfile-types'
ramda: 0.27.1
semver: 7.3.2
devDependencies:
'@types/ramda': 0.27.32
'@types/semver': 7.3.4
specifiers:
'@pnpm/lockfile-types': 'workspace:^2.1.1'
'@types/ramda': ^0.27.32
'@types/semver': ^7.3.4
ramda: ^0.27.1
semver: ^7.3.2
packages/modules-cleaner:
dependencies:
'@pnpm/core-loggers': 'link:../core-loggers'