mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-23 23:29:17 -05:00
5
.changeset/fifty-seals-approve.md
Normal file
5
.changeset/fifty-seals-approve.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/lockfile-file": minor
|
||||
---
|
||||
|
||||
New function added that reads the lockfile and autofixes any merge conflicts.
|
||||
5
.changeset/moody-toys-develop.md
Normal file
5
.changeset/moody-toys-develop.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/merge-lockfiles": major
|
||||
---
|
||||
|
||||
Initial release.
|
||||
5
.changeset/shy-cheetahs-judge.md
Normal file
5
.changeset/shy-cheetahs-judge.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/get-context": minor
|
||||
---
|
||||
|
||||
A new option added to the context: lockfileHadConflicts.
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
55
packages/lockfile-file/src/gitMergeFile.ts
Normal file
55
packages/lockfile-file/src/gitMergeFile.ts
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
{
|
||||
"path": "../lockfile-types"
|
||||
},
|
||||
{
|
||||
"path": "../merge-lockfiles"
|
||||
},
|
||||
{
|
||||
"path": "../types"
|
||||
}
|
||||
|
||||
13
packages/merge-lockfiles/README.md
Normal file
13
packages/merge-lockfiles/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# @pnpm/merge-lockfiles
|
||||
|
||||
> Merges lockfiles. Can automatically fix merge conflicts
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
pnpm add @pnpm/merge-lockfiles
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
1
packages/merge-lockfiles/jest.config.js
Normal file
1
packages/merge-lockfiles/jest.config.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('../../jest.config.js')
|
||||
43
packages/merge-lockfiles/package.json
Normal file
43
packages/merge-lockfiles/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
113
packages/merge-lockfiles/src/index.ts
Normal file
113
packages/merge-lockfiles/src/index.ts
Normal 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
|
||||
}
|
||||
383
packages/merge-lockfiles/test/index.ts
Normal file
383
packages/merge-lockfiles/test/index.ts
Normal 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==',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
16
packages/merge-lockfiles/tsconfig.json
Normal file
16
packages/merge-lockfiles/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "@pnpm/tsconfig",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"../../typings/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../lockfile-types"
|
||||
}
|
||||
]
|
||||
}
|
||||
8
packages/merge-lockfiles/tsconfig.lint.json
Normal file
8
packages/merge-lockfiles/tsconfig.lint.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts",
|
||||
"../../typings/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
20
pnpm-lock.yaml
generated
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user