feat!: lockfile format v6 (#5810)

This commit is contained in:
Zoltan Kochan
2023-01-09 14:37:05 +02:00
committed by GitHub
parent f76a39973e
commit 3ebce5db7a
26 changed files with 528 additions and 48 deletions

View File

@@ -0,0 +1,5 @@
---
"@pnpm/constants": minor
---
Exported a constant for the new lockfile format version: `LOCKFILE_FORMAT_V6`.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/lockfile-file": major
---
Breaking change to the API of the read functions. Instead of one wanted lockfile version, it now expects an array of `wantedVersions`.

View File

@@ -0,0 +1,10 @@
---
"@pnpm/plugin-commands-installation": minor
"@pnpm/resolve-dependencies": minor
"@pnpm/get-context": minor
"@pnpm/core": minor
"@pnpm/config": minor
"pnpm": minor
---
Added support for `pnpm-lock.yaml` format v6. This new format will be the new lockfile format in pnpm v8. To use the new lockfile format, use the `use-lockfile-v6=true` setting in `.npmrc`. Or run `pnpm install --use-lockfile-v6` [#5810](https://github.com/pnpm/pnpm/pull/5810).

View File

@@ -0,0 +1,5 @@
---
"@pnpm/dependency-path": minor
---
Updated the functions to support dependency paths used in the 6th version of the lockfile. Exported a new function: createPeersFolderSuffixNewFormat.

View File

@@ -88,6 +88,7 @@ export interface Config {
registrySupportsTimeField?: boolean
failedToLoadBuiltInConfig: boolean
resolvePeersFromWorkspaceRoot?: boolean
useLockfileV6?: boolean
// proxy
httpProxy?: string

View File

@@ -111,6 +111,7 @@ export const types = Object.assign({
'strict-peer-dependencies': Boolean,
'use-beta-cli': Boolean,
'use-inline-specifiers-lockfile-format': Boolean,
'use-lockfile-v6': Boolean,
'use-node-version': String,
'use-running-store-server': Boolean,
'use-store-server': Boolean,

View File

@@ -1,3 +1,4 @@
import * as dp from '@pnpm/dependency-path'
import type { Lockfile, ProjectSnapshot, ResolvedDependencies } from '@pnpm/lockfile-types'
import {
INLINE_SPECIFIERS_FORMAT_LOCKFILE_VERSION_SUFFIX,
@@ -10,15 +11,89 @@ export function isExperimentalInlineSpecifiersFormat (
lockfile: InlineSpecifiersLockfile | Lockfile
): lockfile is InlineSpecifiersLockfile {
const { lockfileVersion } = lockfile
return typeof lockfileVersion === 'string' && lockfileVersion.endsWith(INLINE_SPECIFIERS_FORMAT_LOCKFILE_VERSION_SUFFIX)
return lockfileVersion.toString().startsWith('6.') || typeof lockfileVersion === 'string' && lockfileVersion.endsWith(INLINE_SPECIFIERS_FORMAT_LOCKFILE_VERSION_SUFFIX)
}
export function convertToInlineSpecifiersFormat (lockfile: Lockfile): InlineSpecifiersLockfile {
return {
...lockfile,
lockfileVersion: `${lockfile.lockfileVersion}${INLINE_SPECIFIERS_FORMAT_LOCKFILE_VERSION_SUFFIX}`,
importers: mapValues(lockfile.importers, convertProjectSnapshotToInlineSpecifiersFormat),
let importers = lockfile.importers
let packages = lockfile.packages
if (lockfile.lockfileVersion.toString().startsWith('6.')) {
importers = Object.fromEntries(
Object.entries(lockfile.importers ?? {})
.map(([importerId, pkgSnapshot]: [string, ProjectSnapshot]) => {
const newSnapshot = { ...pkgSnapshot }
if (newSnapshot.dependencies != null) {
newSnapshot.dependencies = mapValues(newSnapshot.dependencies, convertOldRefToNewRef)
}
if (newSnapshot.optionalDependencies != null) {
newSnapshot.optionalDependencies = mapValues(newSnapshot.optionalDependencies, convertOldRefToNewRef)
}
if (newSnapshot.devDependencies != null) {
newSnapshot.devDependencies = mapValues(newSnapshot.devDependencies, convertOldRefToNewRef)
}
return [importerId, newSnapshot]
})
)
packages = Object.fromEntries(
Object.entries(lockfile.packages ?? {})
.map(([depPath, pkgSnapshot]) => {
const newSnapshot = { ...pkgSnapshot }
if (newSnapshot.dependencies != null) {
newSnapshot.dependencies = mapValues(newSnapshot.dependencies, convertOldRefToNewRef)
}
if (newSnapshot.optionalDependencies != null) {
newSnapshot.optionalDependencies = mapValues(newSnapshot.optionalDependencies, convertOldRefToNewRef)
}
return [convertOldDepPathToNewDepPath(depPath), newSnapshot]
})
)
}
const newLockfile = {
...lockfile,
packages,
lockfileVersion: lockfile.lockfileVersion.toString().startsWith('6.')
? lockfile.lockfileVersion.toString()
: (
lockfile.lockfileVersion.toString().endsWith(INLINE_SPECIFIERS_FORMAT_LOCKFILE_VERSION_SUFFIX)
? lockfile.lockfileVersion.toString()
: `${lockfile.lockfileVersion}${INLINE_SPECIFIERS_FORMAT_LOCKFILE_VERSION_SUFFIX}`
),
importers: mapValues(importers, convertProjectSnapshotToInlineSpecifiersFormat),
}
if (lockfile.lockfileVersion.toString().startsWith('6.') && newLockfile.time) {
newLockfile.time = Object.fromEntries(
Object.entries(newLockfile.time)
.map(([depPath, time]) => [convertOldDepPathToNewDepPath(depPath), time])
)
}
return newLockfile
}
function convertOldDepPathToNewDepPath (oldDepPath: string) {
const parsedDepPath = dp.parse(oldDepPath)
if (!parsedDepPath.name || !parsedDepPath.version) return oldDepPath
let newDepPath = `/${parsedDepPath.name}@${parsedDepPath.version}`
if (parsedDepPath.peersSuffix) {
if (parsedDepPath.peersSuffix.startsWith('(')) {
newDepPath += parsedDepPath.peersSuffix
} else {
newDepPath += `_${parsedDepPath.peersSuffix}`
}
}
if (parsedDepPath.host) {
newDepPath = `${parsedDepPath.host}${newDepPath}`
}
return newDepPath
}
function convertOldRefToNewRef (oldRef: string) {
if (oldRef.startsWith('link:') || oldRef.startsWith('file:')) {
return oldRef
}
if (oldRef.includes('/')) {
return convertOldDepPathToNewDepPath(oldRef)
}
return oldRef
}
export function revertFromInlineSpecifiersFormatIfNecessary (lockfile: Lockfile | InlineSpecifiersLockfile): Lockfile {
@@ -36,11 +111,69 @@ export function revertFromInlineSpecifiersFormat (lockfile: InlineSpecifiersLock
throw new Error(`Unable to revert lockfile from inline specifiers format. Invalid version parsed: ${originalVersionStr}`)
}
return {
...rest,
lockfileVersion: originalVersion,
importers: mapValues(importers, revertProjectSnapshot),
let revertedImporters = mapValues(importers, revertProjectSnapshot)
let packages = lockfile.packages
if (originalVersion === 6) {
revertedImporters = Object.fromEntries(
Object.entries(revertedImporters ?? {})
.map(([importerId, pkgSnapshot]: [string, ProjectSnapshot]) => {
const newSnapshot = { ...pkgSnapshot }
if (newSnapshot.dependencies != null) {
newSnapshot.dependencies = mapValues(newSnapshot.dependencies, convertNewRefToOldRef)
}
if (newSnapshot.optionalDependencies != null) {
newSnapshot.optionalDependencies = mapValues(newSnapshot.optionalDependencies, convertNewRefToOldRef)
}
if (newSnapshot.devDependencies != null) {
newSnapshot.devDependencies = mapValues(newSnapshot.devDependencies, convertNewRefToOldRef)
}
return [importerId, newSnapshot]
})
)
packages = Object.fromEntries(
Object.entries(lockfile.packages ?? {})
.map(([depPath, pkgSnapshot]) => {
const newSnapshot = { ...pkgSnapshot }
if (newSnapshot.dependencies != null) {
newSnapshot.dependencies = mapValues(newSnapshot.dependencies, convertNewRefToOldRef)
}
if (newSnapshot.optionalDependencies != null) {
newSnapshot.optionalDependencies = mapValues(newSnapshot.optionalDependencies, convertNewRefToOldRef)
}
return [convertNewDepPathToOldDepPath(depPath), newSnapshot]
})
)
}
const newLockfile = {
...rest,
lockfileVersion: lockfileVersion.endsWith(INLINE_SPECIFIERS_FORMAT_LOCKFILE_VERSION_SUFFIX) ? originalVersion : lockfileVersion,
packages,
importers: revertedImporters,
}
if (originalVersion === 6 && newLockfile.time) {
newLockfile.time = Object.fromEntries(
Object.entries(newLockfile.time)
.map(([depPath, time]) => [convertNewDepPathToOldDepPath(depPath), time])
)
}
return newLockfile
}
export function convertNewDepPathToOldDepPath (oldDepPath: string) {
if (!oldDepPath.includes('@', 2)) return oldDepPath
const index = oldDepPath.indexOf('@', oldDepPath.indexOf('/@') + 2)
if (oldDepPath.includes('(') && index > oldDepPath.indexOf('(')) return oldDepPath
return `${oldDepPath.substring(0, index)}/${oldDepPath.substring(index + 1)}`
}
function convertNewRefToOldRef (oldRef: string) {
if (oldRef.startsWith('link:') || oldRef.startsWith('file:')) {
return oldRef
}
if (oldRef.includes('@')) {
return convertNewDepPathToOldDepPath(oldRef)
}
return oldRef
}
function convertProjectSnapshotToInlineSpecifiersFormat (

View File

@@ -23,7 +23,7 @@ import { revertFromInlineSpecifiersFormatIfNecessary } from './experiments/inlin
export async function readCurrentLockfile (
virtualStoreDir: string,
opts: {
wantedVersion?: number
wantedVersions?: string[]
ignoreIncompatible: boolean
}
): Promise<Lockfile | null> {
@@ -34,7 +34,7 @@ export async function readCurrentLockfile (
export async function readWantedLockfileAndAutofixConflicts (
pkgPath: string,
opts: {
wantedVersion?: number
wantedVersions?: string[]
ignoreIncompatible: boolean
useGitBranchLockfile?: boolean
mergeGitBranchLockfiles?: boolean
@@ -52,7 +52,7 @@ export async function readWantedLockfileAndAutofixConflicts (
export async function readWantedLockfile (
pkgPath: string,
opts: {
wantedVersion?: number
wantedVersions?: string[]
ignoreIncompatible: boolean
useGitBranchLockfile?: boolean
mergeGitBranchLockfiles?: boolean
@@ -66,7 +66,7 @@ async function _read (
prefix: string, // only for logging
opts: {
autofixMergeConflicts?: boolean
wantedVersion?: number
wantedVersions?: string[]
ignoreIncompatible: boolean
}
): Promise<{
@@ -105,14 +105,21 @@ async function _read (
const lockfile = revertFromInlineSpecifiersFormatIfNecessary(convertFromLockfileFileMutable(lockfileFile))
const lockfileSemver = comverToSemver((lockfile.lockfileVersion ?? 0).toString())
/* eslint-enable @typescript-eslint/dot-notation */
if (typeof opts.wantedVersion !== 'number' || semver.major(lockfileSemver) === semver.major(comverToSemver(opts.wantedVersion.toString()))) {
if (typeof opts.wantedVersion === 'number' && semver.gt(lockfileSemver, comverToSemver(opts.wantedVersion.toString()))) {
logger.warn({
message: `Your ${WANTED_LOCKFILE} was generated by a newer version of pnpm. ` +
`It is a compatible version but it might get downgraded to version ${opts.wantedVersion}`,
prefix,
})
}
if (
!opts.wantedVersions ||
opts.wantedVersions.length === 0 ||
opts.wantedVersions.some((wantedVersion) => {
if (semver.major(lockfileSemver) !== semver.major(comverToSemver(wantedVersion))) return false
if (semver.gt(lockfileSemver, comverToSemver(wantedVersion))) {
logger.warn({
message: `Your ${WANTED_LOCKFILE} was generated by a newer version of pnpm. ` +
`It is a compatible version but it might get downgraded to version ${wantedVersion}`,
prefix,
})
}
return true
})
) {
return { lockfile, hadConflicts }
}
}
@@ -129,7 +136,7 @@ async function _read (
export function createLockfileObject (
importerIds: string[],
opts: {
lockfileVersion: number
lockfileVersion: number | string
}
) {
const importers = importerIds.reduce((acc, importerId) => {
@@ -148,7 +155,7 @@ export function createLockfileObject (
async function _readWantedLockfile (
pkgPath: string,
opts: {
wantedVersion?: number
wantedVersions?: string[]
ignoreIncompatible: boolean
useGitBranchLockfile?: boolean
mergeGitBranchLockfiles?: boolean
@@ -184,7 +191,7 @@ async function _mergeGitBranchLockfiles (
prefix: string,
opts: {
autofixMergeConflicts?: boolean
wantedVersion?: number
wantedVersions?: string[]
ignoreIncompatible: boolean
}
): Promise<Lockfile | null> {
@@ -210,7 +217,7 @@ async function _readGitBranchLockfiles (
prefix: string,
opts: {
autofixMergeConflicts?: boolean
wantedVersion?: number
wantedVersions?: string[]
ignoreIncompatible: boolean
}
): Promise<Array<{

View File

@@ -71,7 +71,7 @@ async function writeLockfile (
return rimraf(lockfilePath)
}
const lockfileToStringify = (opts?.useInlineSpecifiersFormat ?? false)
const lockfileToStringify = (Boolean(opts?.useInlineSpecifiersFormat) || wantedLockfile['lockfileVersion'].toString().startsWith('6.'))
? convertToInlineSpecifiersFormat(wantedLockfile) as unknown as Lockfile
: wantedLockfile
@@ -143,7 +143,7 @@ export function normalizeLockfile (lockfile: Lockfile, opts: NormalizeLockfileOp
}
}
if (lockfileToSave.time) {
lockfileToSave.time = pruneTime(lockfileToSave.time, lockfile.importers)
lockfileToSave.time = (lockfileToSave.lockfileVersion.toString().startsWith('6.') ? pruneTimeInLockfileV6 : pruneTime)(lockfileToSave.time, lockfile.importers)
}
if ((lockfileToSave.overrides != null) && isEmpty(lockfileToSave.overrides)) {
delete lockfileToSave.overrides
@@ -167,6 +167,41 @@ export function normalizeLockfile (lockfile: Lockfile, opts: NormalizeLockfileOp
return lockfileToSave
}
function pruneTimeInLockfileV6 (time: Record<string, string>, importers: Record<string, ProjectSnapshot>): Record<string, string> {
const rootDepPaths = new Set<string>()
for (const importer of Object.values(importers)) {
for (const depType of DEPENDENCIES_FIELDS) {
for (let [depName, ref] of Object.entries(importer[depType] ?? {})) {
if (ref['version']) {
ref = ref['version']
}
const suffixStart = ref.indexOf('(')
const refWithoutPeerSuffix = suffixStart === -1 ? ref : ref.slice(0, suffixStart)
const depPath = refToRelative(refWithoutPeerSuffix, depName)
if (!depPath) continue
rootDepPaths.add(depPath)
}
}
}
return pickBy((_, depPath) => rootDepPaths.has(depPath), time)
}
function refToRelative (
reference: string,
pkgName: string
): string | null {
if (reference.startsWith('link:')) {
return null
}
if (reference.startsWith('file:')) {
return reference
}
if (!reference.includes('/') || !reference.replace(/(\([^)]+\))+$/, '').includes('/')) {
return `/${pkgName}@${reference}`
}
return reference
}
function pruneTime (time: Record<string, string>, importers: Record<string, ProjectSnapshot>): Record<string, string> {
const rootDepPaths = new Set<string>()
for (const importer of Object.values(importers)) {
@@ -212,7 +247,7 @@ export async function writeLockfiles (
}
const forceSharedFormat = opts?.forceSharedFormat === true
const wantedLockfileToStringify = (opts.useInlineSpecifiersFormat ?? false)
const wantedLockfileToStringify = (Boolean(opts.useInlineSpecifiersFormat) || opts.wantedLockfile.lockfileVersion.toString().startsWith('6.'))
? convertToInlineSpecifiersFormat(opts.wantedLockfile) as unknown as Lockfile
: opts.wantedLockfile
const normalizeOpts = {

View File

@@ -1 +1 @@
version: 2
lockfileVersion: 2

View File

@@ -0,0 +1,171 @@
import { convertToInlineSpecifiersFormat, revertFromInlineSpecifiersFormat } from '../lib/experiments/inlineSpecifiersLockfileConverters'
test('convertToInlineSpecifiersFormat()', () => {
const lockfileV5 = {
lockfileVersion: 5.0,
importers: {
project1: {
specifiers: {
foo: '^1.0.0',
bar: '^1.0.0',
qar: '^1.0.0',
tarball: '^1.0.0',
},
dependencies: {
foo: '1.0.0',
tarball: '@registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
},
devDependencies: {
bar: '/@bar/bar/1.0.0_@babel+core@2.0.0',
},
optionalDependencies: {
qar: 'reg.com/qar/1.0.0',
},
},
},
packages: {
'/foo/1.0.0': {
resolution: { integrity: '' },
},
'/@bar/bar/1.0.0_@babel+core@2.0.0': {
resolution: { integrity: '' },
},
'reg.com/qar/1.0.0': {
resolution: { integrity: '' },
},
'@registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz': {
resolution: { integrity: '' },
},
},
}
const lockfileV6 = {
lockfileVersion: '5-inlineSpecifiers',
importers: {
project1: {
dependencies: {
foo: {
specifier: '^1.0.0',
version: '1.0.0',
},
tarball: {
specifier: '^1.0.0',
version: '@registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
},
},
devDependencies: {
bar: {
specifier: '^1.0.0',
version: '/@bar/bar/1.0.0_@babel+core@2.0.0',
},
},
optionalDependencies: {
qar: {
specifier: '^1.0.0',
version: 'reg.com/qar/1.0.0',
},
},
},
},
packages: {
'/foo/1.0.0': {
resolution: { integrity: '' },
},
'/@bar/bar/1.0.0_@babel+core@2.0.0': {
resolution: { integrity: '' },
},
'reg.com/qar/1.0.0': {
resolution: { integrity: '' },
},
'@registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz': {
resolution: { integrity: '' },
},
},
}
expect(convertToInlineSpecifiersFormat(lockfileV5)).toEqual(lockfileV6)
expect(revertFromInlineSpecifiersFormat(lockfileV6)).toEqual(lockfileV5)
})
test('convertToInlineSpecifiersFormat() with lockfile v6', () => {
const lockfileV5 = {
lockfileVersion: '6.0',
importers: {
project1: {
specifiers: {
foo: '^1.0.0',
bar: '^1.0.0',
qar: '^1.0.0',
tarball: '^1.0.0',
},
dependencies: {
foo: '1.0.0',
tarball: '@registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
},
devDependencies: {
bar: '/@bar/bar/1.0.0_@babel+core@2.0.0',
},
optionalDependencies: {
qar: 'reg.com/qar/1.0.0',
},
},
},
packages: {
'/foo/1.0.0': {
resolution: { integrity: '' },
},
'/@bar/bar/1.0.0_@babel+core@2.0.0': {
resolution: { integrity: '' },
},
'reg.com/qar/1.0.0': {
resolution: { integrity: '' },
},
'@registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz': {
resolution: { integrity: '' },
},
},
}
const lockfileV6 = {
lockfileVersion: '6.0',
importers: {
project1: {
dependencies: {
foo: {
specifier: '^1.0.0',
version: '1.0.0',
},
tarball: {
specifier: '^1.0.0',
version: '@registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
},
},
devDependencies: {
bar: {
specifier: '^1.0.0',
version: '/@bar/bar@1.0.0_@babel+core@2.0.0',
},
},
optionalDependencies: {
qar: {
specifier: '^1.0.0',
version: 'reg.com/qar@1.0.0',
},
},
},
},
packages: {
'/foo@1.0.0': {
resolution: { integrity: '' },
},
'/@bar/bar@1.0.0_@babel+core@2.0.0': {
resolution: { integrity: '' },
},
'reg.com/qar@1.0.0': {
resolution: { integrity: '' },
},
'@registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz': {
resolution: { integrity: '' },
},
},
}
expect(convertToInlineSpecifiersFormat(lockfileV5)).toEqual(lockfileV6)
expect(revertFromInlineSpecifiersFormat(lockfileV6)).toEqual(lockfileV5)
})

View File

@@ -35,7 +35,7 @@ test('readWantedLockfile()', async () => {
try {
await readWantedLockfile(path.join('fixtures', '3'), {
ignoreIncompatible: false,
wantedVersion: 3,
wantedVersions: ['3'],
})
fail()
} catch (err: any) { // eslint-disable-line
@@ -47,7 +47,7 @@ test('readWantedLockfile() when lockfileVersion is a string', async () => {
{
const lockfile = await readWantedLockfile(path.join('fixtures', '4'), {
ignoreIncompatible: false,
wantedVersion: 3,
wantedVersions: ['3'],
})
expect(lockfile!.lockfileVersion).toEqual('v3')
}
@@ -55,7 +55,7 @@ test('readWantedLockfile() when lockfileVersion is a string', async () => {
{
const lockfile = await readWantedLockfile(path.join('fixtures', '5'), {
ignoreIncompatible: false,
wantedVersion: 3,
wantedVersions: ['3'],
})
expect(lockfile!.lockfileVersion).toEqual('3')
}

View File

@@ -4,7 +4,7 @@ export { PatchFile }
export interface Lockfile {
importers: Record<string, ProjectSnapshot>
lockfileVersion: number
lockfileVersion: number | string
time?: Record<string, string>
packages?: PackageSnapshots
neverBuiltDependencies?: string[]

View File

@@ -1,5 +1,6 @@
export const WANTED_LOCKFILE = 'pnpm-lock.yaml'
export const LOCKFILE_VERSION = 5.4
export const LOCKFILE_VERSION_V6 = '6.0'
export const ENGINE_NAME = `${process.platform}-${process.arch}-node-${process.version.split('.')[0]}`
export const LAYOUT_VERSION = 5

View File

@@ -30,6 +30,10 @@ export function tryGetPackageId (registries: Registries, relDepPath: string) {
if (relDepPath[0] !== '/') {
return null
}
const sepIndex = relDepPath.indexOf('(', relDepPath.lastIndexOf('/'))
if (sepIndex !== -1) {
return resolve(registries, relDepPath.slice(0, sepIndex))
}
const underscoreIndex = relDepPath.indexOf('_', relDepPath.lastIndexOf('/'))
if (underscoreIndex !== -1) {
return resolve(registries, relDepPath.slice(0, underscoreIndex))
@@ -45,7 +49,7 @@ export function refToAbsolute (
if (reference.startsWith('link:')) {
return null
}
if (!reference.includes('/')) {
if (!reference.includes('/') || reference.includes('(') && reference.lastIndexOf('/', reference.indexOf('(')) === -1) {
const registryName = encodeRegistry(getRegistryByPackageName(registries, pkgName))
return `${registryName}/${pkgName}/${reference}`
}
@@ -83,7 +87,7 @@ export function refToRelative (
if (reference.startsWith('file:')) {
return reference
}
if (!reference.includes('/')) {
if (!reference.includes('/') || reference.includes('(') && reference.lastIndexOf('/', reference.indexOf('(')) === -1) {
return `/${pkgName}/${reference}`
}
return reference
@@ -104,13 +108,22 @@ export function parse (dependencyPath: string) {
const name = parts[0].startsWith('@')
? `${parts.shift()}/${parts.shift()}` // eslint-disable-line @typescript-eslint/restrict-template-expressions
: parts.shift()
let version = parts.shift()
let version = parts.join('/')
if (version) {
const underscoreIndex = version.indexOf('_')
let peerSepIndex!: number
let peersSuffix: string | undefined
if (underscoreIndex !== -1) {
peersSuffix = version.substring(underscoreIndex + 1)
version = version.substring(0, underscoreIndex)
if (version.includes('(') && version.endsWith(')')) {
peerSepIndex = version.indexOf('(')
if (peerSepIndex !== -1) {
peersSuffix = version.substring(peerSepIndex)
version = version.substring(0, peerSepIndex)
}
} else {
peerSepIndex = version.indexOf('_')
if (peerSepIndex !== -1) {
peersSuffix = version.substring(peerSepIndex + 1)
version = version.substring(0, peerSepIndex)
}
}
if (semver.valid(version)) {
return {
@@ -142,12 +155,17 @@ function depPathToFilenameUnescaped (depPath: string) {
if (depPath.startsWith('/')) {
depPath = depPath.substring(1)
}
const index = depPath.lastIndexOf('/')
const index = depPath.lastIndexOf('/', depPath.includes('(') ? depPath.indexOf('(') - 1 : depPath.length)
return `${depPath.substring(0, index)}@${depPath.slice(index + 1)}`
}
return depPath.replace(':', '+')
}
export function createPeersFolderSuffixNewFormat (peers: Array<{ name: string, version: string }>): string {
const folderName = peers.map(({ name, version }) => `${name}@${version}`).sort().join(')(')
return `(${folderName})`
}
export function createPeersFolderSuffix (peers: Array<{ name: string, version: string }>): string {
const folderName = peers.map(({ name, version }) => `${name.replace('/', '+')}@${version}`).sort().join('+')

View File

@@ -82,6 +82,22 @@ test('parse()', () => {
version: '1.0.0',
})
expect(parse('example.com/foo/1.0.0(bar@2.0.0)')).toStrictEqual({
host: 'example.com',
isAbsolute: true,
name: 'foo',
peersSuffix: '(bar@2.0.0)',
version: '1.0.0',
})
expect(parse('/foo/1.0.0(@types/babel__core@7.1.14)(foo@1.0.0)')).toStrictEqual({
host: undefined,
isAbsolute: false,
name: 'foo',
peersSuffix: '(@types/babel__core@7.1.14)(foo@1.0.0)',
version: '1.0.0',
})
expect(() => parse('/foo/bar')).toThrow(/\/foo\/bar is an invalid relative dependency path/)
})
@@ -95,15 +111,21 @@ test('refToAbsolute()', () => {
expect(refToAbsolute('registry.npmjs.org/foo/1.0.0', 'foo', registries)).toEqual('registry.npmjs.org/foo/1.0.0')
expect(refToAbsolute('/foo/1.0.0', 'foo', registries)).toEqual('registry.npmjs.org/foo/1.0.0')
expect(refToAbsolute('/@foo/foo/1.0.0', '@foo/foo', registries)).toEqual('foo.com/@foo/foo/1.0.0')
expect(refToAbsolute('/@foo/foo@1.0.0(@foo/bar@1.0.0)', '@foo/foo', registries)).toEqual('foo.com/@foo/foo@1.0.0(@foo/bar@1.0.0)')
expect(refToAbsolute('/@foo/foo@1.0.0(@foo/bar@1.0.0)(@foo/qar@1.0.0)', '@foo/foo', registries)).toEqual('foo.com/@foo/foo@1.0.0(@foo/bar@1.0.0)(@foo/qar@1.0.0)')
// linked dependencies don't have an absolute path
expect(refToAbsolute('link:../foo', 'foo', registries)).toBeNull()
})
test('refToRelative()', () => {
expect(refToRelative('/@most/multicast/1.3.0/most@1.7.3', '@most/multicast')).toEqual('/@most/multicast/1.3.0/most@1.7.3')
expect(refToRelative('/@most/multicast/1.3.0/most@1.7.3(@foo/bar@1.0.0)', '@most/multicast')).toEqual('/@most/multicast/1.3.0/most@1.7.3(@foo/bar@1.0.0)')
expect(refToRelative('/@most/multicast/1.3.0/most@1.7.3(@foo/bar@1.0.0)(@foo/qar@1.0.0)', '@most/multicast')).toEqual('/@most/multicast/1.3.0/most@1.7.3(@foo/bar@1.0.0)(@foo/qar@1.0.0)')
// linked dependencies don't have a relative path
expect(refToRelative('link:../foo', 'foo')).toBeNull()
expect(refToRelative('file:../tarball.tgz', 'foo')).toEqual('file:../tarball.tgz')
expect(refToRelative('1.3.0(@foo/bar@1.0.0)', '@qar/bar')).toEqual('/@qar/bar/1.3.0(@foo/bar@1.0.0)')
expect(refToRelative('1.3.0(@foo/bar@1.0.0)(@foo/qar@1.0.0)', '@qar/bar')).toEqual('/@qar/bar/1.3.0(@foo/bar@1.0.0)(@foo/qar@1.0.0)')
})
test('relative()', () => {
@@ -143,4 +165,5 @@ test('depPathToFilename()', () => {
test('tryGetPackageId', () => {
expect(tryGetPackageId({ default: 'https://registry.npmjs.org/' }, '/foo/1.0.0_@types+babel__core@7.1.14')).toEqual('registry.npmjs.org/foo/1.0.0')
expect(tryGetPackageId({ default: 'https://registry.npmjs.org/' }, '/foo/1.0.0(@types+babel__core@7.1.14)')).toEqual('registry.npmjs.org/foo/1.0.0')
})

View File

@@ -120,6 +120,7 @@ export interface StrictInstallOptions {
allProjects: ProjectOptions[]
resolveSymlinksInInjectedDirs: boolean
dedupeDirectDeps: boolean
useLockfileV6: boolean
}
export type InstallOptions =
@@ -206,6 +207,7 @@ const defaults = async (opts: InstallOptions) => {
resolveSymlinksInInjectedDirs: false,
dedupeDirectDeps: false,
resolvePeersFromWorkspaceRoot: false,
useLockfileV6: false,
} as StrictInstallOptions
}

View File

@@ -4,6 +4,7 @@ import { buildModules, DepsStateCache, linkBinsOfDependencies } from '@pnpm/buil
import {
LAYOUT_VERSION,
LOCKFILE_VERSION,
LOCKFILE_VERSION_V6,
WANTED_LOCKFILE,
} from '@pnpm/constants'
import {
@@ -286,7 +287,8 @@ export async function mutateModules (
packageExtensionsChecksum,
patchedDependencies,
}) ||
opts.fixLockfile
opts.fixLockfile ||
opts.useLockfileV6 && !ctx.wantedLockfile.lockfileVersion.toString().startsWith('6.')
if (needsFullResolution) {
ctx.wantedLockfile.overrides = opts.overrides
ctx.wantedLockfile.neverBuiltDependencies = opts.neverBuiltDependencies
@@ -836,6 +838,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
patchedDependencies: opts.patchedDependencies,
lockfileIncludeTarballUrl: opts.lockfileIncludeTarballUrl,
resolvePeersFromWorkspaceRoot: opts.resolvePeersFromWorkspaceRoot,
useLockfileV6: opts.useLockfileV6,
}
)
if (!opts.include.optionalDependencies || !opts.include.devDependencies || !opts.include.dependencies) {
@@ -873,7 +876,11 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
: newLockfile
if (opts.updateLockfileMinorVersion) {
newLockfile.lockfileVersion = LOCKFILE_VERSION
if (opts.useLockfileV6 || newLockfile.lockfileVersion.toString().startsWith('6.')) {
newLockfile.lockfileVersion = LOCKFILE_VERSION_V6
} else {
newLockfile.lockfileVersion = LOCKFILE_VERSION
}
}
const depsStateCache: DepsStateCache = {}

View File

@@ -1374,3 +1374,44 @@ test('include tarball URL', async () => {
expect((lockfile.packages['/@pnpm.e2e/pkg-with-1-dep/100.0.0'].resolution as TarballResolution).tarball)
.toBe(`http://localhost:${REGISTRY_MOCK_PORT}/@pnpm.e2e%2fpkg-with-1-dep/-/pkg-with-1-dep-100.0.0.tgz`)
})
test('lockfile v6', async () => {
prepareEmpty()
const manifest = await addDependenciesToPackage({}, ['@pnpm.e2e/pkg-with-1-dep@100.0.0'], await testDefaults({ useLockfileV6: true }))
{
const lockfile = await readYamlFile<any>(WANTED_LOCKFILE) // eslint-disable-line @typescript-eslint/no-explicit-any
expect(lockfile.lockfileVersion).toBe('6.0')
expect(lockfile.packages).toHaveProperty(['/@pnpm.e2e/pkg-with-1-dep@100.0.0'])
}
await addDependenciesToPackage(manifest, ['@pnpm.e2e/foo@100.0.0'], await testDefaults())
{
const lockfile = await readYamlFile<any>(WANTED_LOCKFILE) // eslint-disable-line @typescript-eslint/no-explicit-any
expect(lockfile.lockfileVersion).toBe('6.0')
expect(lockfile.packages).toHaveProperty(['/@pnpm.e2e/pkg-with-1-dep@100.0.0'])
expect(lockfile.packages).toHaveProperty(['/@pnpm.e2e/foo@100.0.0'])
}
})
test('lockfile v5 is converted to lockfile v6', async () => {
prepareEmpty()
const manifest = await addDependenciesToPackage({}, ['@pnpm.e2e/pkg-with-1-dep@100.0.0'], await testDefaults())
{
const lockfile = await readYamlFile<any>(WANTED_LOCKFILE) // eslint-disable-line @typescript-eslint/no-explicit-any
expect(lockfile.lockfileVersion).toBe(5.4)
expect(lockfile.packages).toHaveProperty(['/@pnpm.e2e/pkg-with-1-dep/100.0.0'])
}
await install(manifest, await testDefaults({ useLockfileV6: true }))
{
const lockfile = await readYamlFile<any>(WANTED_LOCKFILE) // eslint-disable-line @typescript-eslint/no-explicit-any
expect(lockfile.lockfileVersion).toBe('6.0')
expect(lockfile.packages).toHaveProperty(['/@pnpm.e2e/pkg-with-1-dep@100.0.0'])
}
})

View File

@@ -91,6 +91,8 @@ export interface GetContextOptions {
publicHoistPattern?: string[] | undefined
forcePublicHoistPattern?: boolean
useLockfileV6?: boolean
}
export async function getContext (
@@ -180,6 +182,7 @@ export async function getContext (
useGitBranchLockfile: opts.useGitBranchLockfile,
mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
virtualStoreDir,
useLockfileV6: opts.useLockfileV6,
}),
}
contextLogger.debug({
@@ -386,6 +389,7 @@ export async function getContextForSingleImporter (
publicHoistPattern?: string[] | undefined
forcePublicHoistPattern?: boolean
useLockfileV6?: boolean
},
alreadyPurged: boolean = false
): Promise<PnpmSingleContext> {
@@ -485,6 +489,7 @@ export async function getContextForSingleImporter (
useGitBranchLockfile: opts.useGitBranchLockfile,
mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
virtualStoreDir,
useLockfileV6: opts.useLockfileV6,
}),
}
packageManifestLogger.debug({

View File

@@ -1,5 +1,6 @@
import {
LOCKFILE_VERSION,
LOCKFILE_VERSION_V6,
WANTED_LOCKFILE,
} from '@pnpm/constants'
import {
@@ -37,6 +38,7 @@ export async function readLockfiles (
useGitBranchLockfile?: boolean
mergeGitBranchLockfiles?: boolean
virtualStoreDir: string
useLockfileV6?: boolean
}
): Promise<{
currentLockfile: Lockfile
@@ -46,11 +48,12 @@ export async function readLockfiles (
wantedLockfile: Lockfile
lockfileHadConflicts: boolean
}> {
const wantedLockfileVersion = opts.useLockfileV6 ? LOCKFILE_VERSION_V6 : LOCKFILE_VERSION
// ignore `pnpm-lock.yaml` on CI servers
// a latest pnpm should not break all the builds
const lockfileOpts = {
ignoreIncompatible: opts.force || isCI,
wantedVersion: LOCKFILE_VERSION,
wantedVersions: [LOCKFILE_VERSION.toString(), LOCKFILE_VERSION_V6],
useGitBranchLockfile: opts.useGitBranchLockfile,
mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
}
@@ -99,7 +102,7 @@ export async function readLockfiles (
})()
)
const files = await Promise.all<Lockfile | null | undefined>(fileReads)
const sopts = { lockfileVersion: LOCKFILE_VERSION }
const sopts = { lockfileVersion: wantedLockfileVersion }
const importerIds = opts.projects.map((importer) => importer.id)
const currentLockfile = files[1] ?? createLockfileObject(importerIds, sopts)
for (const importerId of importerIds) {

View File

@@ -62,6 +62,7 @@ export function rcOptionsTypes () {
'only',
'optional',
'unsafe-perm',
'use-lockfile-v6',
'use-running-store-server',
'use-store-server',
'verify-store-integrity',

View File

@@ -20,7 +20,7 @@ export function depPathToRef (
}
if (depPath[0] === '/' && opts.alias === opts.realName) {
const ref = depPath.replace(`/${opts.realName}/`, '')
if (!ref.includes('/')) return ref
if (!ref.includes('/') || !ref.replace(/(\([^)]+\))+$/, '').includes('/')) return ref
}
return depPath
}

View File

@@ -208,6 +208,7 @@ export async function resolveDependencies (
projects: projectsToLink,
virtualStoreDir: opts.virtualStoreDir,
resolvePeersFromWorkspaceRoot: Boolean(opts.resolvePeersFromWorkspaceRoot),
useLockfileV6: Boolean(opts.useLockfileV6),
})
for (const { id, manifest } of projectsToLink) {

View File

@@ -87,6 +87,7 @@ export interface ResolveDependenciesOptions {
virtualStoreDir: string
wantedLockfile: Lockfile
workspacePackages: WorkspacePackages
useLockfileV6?: boolean
}
export async function resolveDependencyTree<T> (

View File

@@ -6,7 +6,7 @@ import {
PeerDependencyIssues,
PeerDependencyIssuesByProjects,
} from '@pnpm/types'
import { depPathToFilename, createPeersFolderSuffix } from '@pnpm/dependency-path'
import { depPathToFilename, createPeersFolderSuffix, createPeersFolderSuffixNewFormat } from '@pnpm/dependency-path'
import { KeyValuePair } from 'ramda'
import isEmpty from 'ramda/src/isEmpty'
import mapValues from 'ramda/src/map'
@@ -62,6 +62,7 @@ export function resolvePeers<T extends PartialResolvedPackage> (
virtualStoreDir: string
lockfileDir: string
resolvePeersFromWorkspaceRoot?: boolean
useLockfileV6?: boolean
}
): {
dependenciesGraph: GenericDependenciesGraph<T>
@@ -91,6 +92,7 @@ export function resolvePeers<T extends PartialResolvedPackage> (
purePkgs: new Set(),
rootDir,
virtualStoreDir: opts.virtualStoreDir,
useLockfileV6: opts.useLockfileV6,
})
if (!isEmpty(peerDependencyIssues.bad) || !isEmpty(peerDependencyIssues.missing)) {
peerDependencyIssuesByProjects[id] = {
@@ -176,6 +178,7 @@ function resolvePeersOfNode<T extends PartialResolvedPackage> (
purePkgs: Set<string> // pure packages are those that don't rely on externally resolved peers
rootDir: string
lockfileDir: string
useLockfileV6?: boolean
}
): PeersResolution {
const node = ctx.dependenciesTree[nodeId]
@@ -256,7 +259,7 @@ function resolvePeersOfNode<T extends PartialResolvedPackage> (
if (isEmpty(allResolvedPeers)) {
depPath = resolvedPackage.depPath
} else {
const peersFolderSuffix = createPeersFolderSuffix(
const peersFolderSuffix = (ctx.useLockfileV6 ? createPeersFolderSuffixNewFormat : createPeersFolderSuffix)(
Object.entries(allResolvedPeers)
.map(([alias, nodeId]) => {
if (nodeId.startsWith('link:')) {
@@ -372,6 +375,7 @@ function resolvePeersOfChildren<T extends PartialResolvedPackage> (
dependenciesTree: DependenciesTree<T>
rootDir: string
lockfileDir: string
useLockfileV6?: boolean
}
): PeersResolution {
const allResolvedPeers: Record<string, string> = {}