Skip to content

Commit

Permalink
feat: add experimental use-inline-specifiers-lockfile-format
Browse files Browse the repository at this point in the history
  • Loading branch information
gluxon committed Jul 26, 2022
1 parent 01c5834 commit 57053e0
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 7 deletions.
10 changes: 10 additions & 0 deletions .changeset/fifty-rats-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@pnpm/config": minor
"@pnpm/core": minor
"@pnpm/lockfile-file": minor
"pnpm": minor
---

Add experimental lockfile format that should merge conflict less in the `importers` section. Enabled by setting the `use-inline-specifiers-lockfile-format = true` feature flag in `.npmrc`.

If this feature flag is committed to a repo, we recommend setting the minimum allowed version of pnpm to this release in the `package.json` `engines` field. Once this is set, older pnpm versions will throw on invalid lockfile versions.
3 changes: 3 additions & 0 deletions packages/config/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ export interface Config {
changedFilesIgnorePattern?: string[]
rootProjectManifest?: ProjectManifest
userConfig: Record<string, string>

// feature flags for experimental testing
useInlineSpecifiersLockfileFormat?: boolean // For https://github.com/pnpm/pnpm/issues/4725
}

export interface ConfigWithDeprecatedSettings extends Config {
Expand Down
2 changes: 2 additions & 0 deletions packages/config/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export const types = Object.assign({
stream: Boolean,
'strict-peer-dependencies': Boolean,
'use-beta-cli': Boolean,
'use-inline-specifiers-lockfile-format': Boolean,
'use-node-version': String,
'use-running-store-server': Boolean,
'use-store-server': Boolean,
Expand Down Expand Up @@ -222,6 +223,7 @@ export default async (
'strict-peer-dependencies': true,
'unsafe-perm': npmDefaults['unsafe-perm'],
'use-beta-cli': false,
'use-inline-specifiers-lockfile-format': false,
userconfig: npmDefaults.userconfig,
'virtual-store-dir': 'node_modules/.pnpm',
'workspace-concurrency': 4,
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/install/extendInstallOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface StrictInstallOptions {
saveLockfile: boolean
useGitBranchLockfile: boolean
mergeGitBranchLockfiles: boolean
useInlineSpecifiersLockfileFormat: boolean
linkWorkspacePackagesDepth: number
lockfileOnly: boolean
fixLockfile: boolean
Expand Down Expand Up @@ -176,6 +177,7 @@ const defaults = async (opts: InstallOptions) => {
useLockfile: true,
saveLockfile: true,
useGitBranchLockfile: false,
useInlineSpecifiersLockfileFormat: false,
mergeGitBranchLockfiles: false,
userAgent: `${packageManager.name}/${packageManager.version} npm/? node/${process.version} ${process.platform} ${process.arch}`,
verifyStoreIntegrity: true,
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/install/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -857,7 +857,12 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
}

const depsStateCache: DepsStateCache = {}
const lockfileOpts = { forceSharedFormat: opts.forceSharedLockfile, useGitBranchLockfile: opts.useGitBranchLockfile, mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles }
const lockfileOpts = {
forceSharedFormat: opts.forceSharedLockfile,
useInlineSpecifiersFormat: opts.useInlineSpecifiersLockfileFormat,
useGitBranchLockfile: opts.useGitBranchLockfile,
mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
}
if (!opts.lockfileOnly && opts.enableModulesDir) {
const result = await linkPackages(
projects,
Expand Down
38 changes: 38 additions & 0 deletions packages/lockfile-file/src/experiments/InlineSpecifiersLockfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { Lockfile } from '@pnpm/lockfile-types'
import type { DependenciesMeta } from '@pnpm/types'

export const INLINE_SPECIFIERS_FORMAT_LOCKFILE_VERSION_SUFFIX = '-inlineSpecifiers'

/**
* Similar to the current Lockfile importers format (lockfile version 5.4 at
* time of writing), but specifiers are moved to each ResolvedDependencies block
* instead of being declared on its own dictionary.
*
* This is an experiment to reduce one flavor of merge conflicts in lockfiles.
* For more info: https://github.com/pnpm/pnpm/issues/4725.
*/
export interface InlineSpecifiersLockfile extends Omit<Lockfile, 'lockfileVersion' | 'importers'> {
lockfileVersion: string
importers: Record<string, InlineSpecifiersProjectSnapshot>
}

/**
* Similar to the current ProjectSnapshot interface, but omits the "specifiers"
* field in favor of inlining each specifier next to its version resolution in
* dependency blocks.
*/
export interface InlineSpecifiersProjectSnapshot {
dependencies?: InlineSpecifiersResolvedDependencies
devDependencies?: InlineSpecifiersResolvedDependencies
optionalDependencies?: InlineSpecifiersResolvedDependencies
dependenciesMeta?: DependenciesMeta
}

export interface InlineSpecifiersResolvedDependencies {
[depName: string]: SpecifierAndResolution
}

export interface SpecifierAndResolution {
specifier: string
version: string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import type { Lockfile, ProjectSnapshot, ResolvedDependencies } from '@pnpm/lockfile-types'
import {
INLINE_SPECIFIERS_FORMAT_LOCKFILE_VERSION_SUFFIX,
InlineSpecifiersLockfile,
InlineSpecifiersProjectSnapshot,
InlineSpecifiersResolvedDependencies,
} from './InlineSpecifiersLockfile'

export function isExperimentalInlineSpecifiersFormat (
lockfile: InlineSpecifiersLockfile | Lockfile
): lockfile is InlineSpecifiersLockfile {
const { lockfileVersion } = lockfile
return 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),
}
}

export function revertFromInlineSpecifiersFormatIfNecessary (lockfile: Lockfile | InlineSpecifiersLockfile): Lockfile {
return isExperimentalInlineSpecifiersFormat(lockfile)
? revertFromInlineSpecifiersFormat(lockfile)
: lockfile
}

export function revertFromInlineSpecifiersFormat (lockfile: InlineSpecifiersLockfile): Lockfile {
const { lockfileVersion, importers, ...rest } = lockfile

const originalVersionStr = lockfileVersion.replace(INLINE_SPECIFIERS_FORMAT_LOCKFILE_VERSION_SUFFIX, '')
const originalVersion = Number(originalVersionStr)
if (isNaN(originalVersion)) {
throw new Error(`Unable to revert lockfile from inline specifiers format. Invalid version parsed: ${originalVersionStr}`)
}

return {
...rest,
lockfileVersion: originalVersion,
importers: mapValues(importers, revertProjectSnapshot),
}
}

function convertProjectSnapshotToInlineSpecifiersFormat (
projectSnapshot: ProjectSnapshot
): InlineSpecifiersProjectSnapshot {
const { specifiers, ...rest } = projectSnapshot
const convertBlock = (block?: ResolvedDependencies) =>
block != null
? convertResolvedDependenciesToInlineSpecifiersFormat(block, { specifiers })
: block
return {
...rest,
dependencies: convertBlock(projectSnapshot.dependencies),
optionalDependencies: convertBlock(projectSnapshot.optionalDependencies),
devDependencies: convertBlock(projectSnapshot.devDependencies),
}
}

function convertResolvedDependenciesToInlineSpecifiersFormat (
resolvedDependencies: ResolvedDependencies,
{ specifiers }: { specifiers: ResolvedDependencies}
): InlineSpecifiersResolvedDependencies {
return mapValues(resolvedDependencies, (version, depName) => ({
specifier: specifiers[depName],
version,
}))
}

function revertProjectSnapshot (from: InlineSpecifiersProjectSnapshot): ProjectSnapshot {
const specifiers: ResolvedDependencies = {}

function moveSpecifiers (from: InlineSpecifiersResolvedDependencies): ResolvedDependencies {
const resolvedDependencies: ResolvedDependencies = {}
for (const [depName, { specifier, version }] of Object.entries(from)) {
const existingValue = specifiers[depName]
if (existingValue != null && existingValue !== specifier) {
throw new Error(`Project snapshot lists the same dependency more than once with conflicting versions: ${depName}`)
}

specifiers[depName] = specifier
resolvedDependencies[depName] = version
}
return resolvedDependencies
}

const dependencies = from.dependencies == null
? from.dependencies
: moveSpecifiers(from.dependencies)
const devDependencies = from.devDependencies == null
? from.devDependencies
: moveSpecifiers(from.devDependencies)
const optionalDependencies = from.optionalDependencies == null
? from.optionalDependencies
: moveSpecifiers(from.optionalDependencies)

return {
...from,
specifiers,
dependencies,
devDependencies,
optionalDependencies,
}
}

function mapValues<T, U> (obj: Record<string, T>, mapper: (val: T, key: string) => U): Record<string, U> {
const result = {}
for (const [key, value] of Object.entries(obj)) {
result[key] = mapper(value, key)
}
return result
}
3 changes: 2 additions & 1 deletion packages/lockfile-file/src/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import logger from './logger'
import { LockfileFile } from './write'
import { getWantedLockfileName } from './lockfileName'
import { getGitBranchLockfileNames } from './gitBranchLockfile'
import { revertFromInlineSpecifiersFormatIfNecessary } from './experiments/inlineSpecifiersLockfileConverters'

export async function readCurrentLockfile (
virtualStoreDir: string,
Expand Down Expand Up @@ -101,7 +102,7 @@ async function _read (
})
}
if (lockfileFile) {
const lockfile = convertFromLockfileFileMutable(lockfileFile)
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()))) {
Expand Down
23 changes: 18 additions & 5 deletions packages/lockfile-file/src/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import writeFileAtomicCB from 'write-file-atomic'
import logger from './logger'
import { sortLockfileKeys } from './sortLockfileKeys'
import { getWantedLockfileName } from './lockfileName'
import { convertToInlineSpecifiersFormat } from './experiments/inlineSpecifiersLockfileConverters'

async function writeFileAtomic (filename: string, data: string) {
return new Promise<void>((resolve, reject) => writeFileAtomicCB(filename, data, {}, (err?: Error) => (err != null) ? reject(err) : resolve()))
Expand All @@ -29,6 +30,7 @@ export async function writeWantedLockfile (
wantedLockfile: Lockfile,
opts?: {
forceSharedFormat?: boolean
useInlineSpecifiersFormat?: boolean
useGitBranchLockfile?: boolean
mergeGitBranchLockfiles?: boolean
}
Expand All @@ -48,13 +50,16 @@ export async function writeCurrentLockfile (
return writeLockfile('lock.yaml', virtualStoreDir, currentLockfile, opts)
}

interface LockfileFormatOptions {
forceSharedFormat?: boolean
useInlineSpecifiersFormat?: boolean
}

async function writeLockfile (
lockfileFilename: string,
pkgPath: string,
wantedLockfile: Lockfile,
opts?: {
forceSharedFormat?: boolean
}
opts?: LockfileFormatOptions
) {
const lockfilePath = path.join(pkgPath, lockfileFilename)

Expand All @@ -63,7 +68,11 @@ async function writeLockfile (
return rimraf(lockfilePath)
}

const yamlDoc = yamlStringify(wantedLockfile, opts?.forceSharedFormat === true)
const lockfileToStringify = (opts?.useInlineSpecifiersFormat ?? false)
? convertToInlineSpecifiersFormat(wantedLockfile) as unknown as Lockfile
: wantedLockfile

const yamlDoc = yamlStringify(lockfileToStringify, opts?.forceSharedFormat === true)

return writeFileAtomic(lockfilePath, yamlDoc)
}
Expand Down Expand Up @@ -145,6 +154,7 @@ export function normalizeLockfile (lockfile: Lockfile, forceSharedFormat: boolea
export default async function writeLockfiles (
opts: {
forceSharedFormat?: boolean
useInlineSpecifiersFormat?: boolean
wantedLockfile: Lockfile
wantedLockfileDir: string
currentLockfile: Lockfile
Expand All @@ -167,7 +177,10 @@ export default async function writeLockfiles (
}

const forceSharedFormat = opts?.forceSharedFormat === true
const yamlDoc = yamlStringify(opts.wantedLockfile, forceSharedFormat)
const wantedLockfileToStringify = (opts.useInlineSpecifiersFormat ?? false)
? convertToInlineSpecifiersFormat(opts.wantedLockfile) as unknown as Lockfile
: opts.wantedLockfile
const yamlDoc = yamlStringify(wantedLockfileToStringify, forceSharedFormat)

// in most cases the `pnpm-lock.yaml` and `node_modules/.pnpm-lock.yaml` are equal
// in those cases the YAML document can be stringified only once for both files
Expand Down

0 comments on commit 57053e0

Please sign in to comment.