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 baf8c0d
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 7 deletions.
8 changes: 8 additions & 0 deletions .changeset/fifty-rats-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@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`.
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 InlineSpecifiersFormatLockfileKey = 'inlineSpecifiersFormat'

/**
* 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, 'importers'> {
[InlineSpecifiersFormatLockfileKey]: true
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,105 @@
import type { Lockfile, ProjectSnapshot, ResolvedDependencies } from '@pnpm/lockfile-types'
import type { InlineSpecifiersLockfile, InlineSpecifiersProjectSnapshot, InlineSpecifiersResolvedDependencies } from './InlineSpecifiersLockfile'

export function isExperimentalInlineSpecifiersFormat (
lockfile: InlineSpecifiersLockfile | Lockfile
): lockfile is InlineSpecifiersLockfile {
return (lockfile as InlineSpecifiersLockfile).inlineSpecifiersFormat ?? false
}

export function convertToInlineSpecifiersFormat (lockfile: Lockfile): InlineSpecifiersLockfile {
return {
inlineSpecifiersFormat: true,
...lockfile,
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 { importers, inlineSpecifiersFormat, ...rest } = lockfile

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

function convertProjectSnapshotToInlineSpecifiersFormat (
projectSnapshot: ProjectSnapshot
): InlineSpecifiersProjectSnapshot {
const { specifiers } = projectSnapshot
return {
dependencies: projectSnapshot.dependencies != null
? convertResolvedDependenciesToInlineSpecifiersFormat(projectSnapshot.dependencies, { specifiers })
: projectSnapshot.dependencies,
optionalDependencies: projectSnapshot.optionalDependencies != null
? convertResolvedDependenciesToInlineSpecifiersFormat(projectSnapshot.optionalDependencies, { specifiers })
: projectSnapshot.optionalDependencies,
devDependencies: projectSnapshot.devDependencies != null
? convertResolvedDependenciesToInlineSpecifiersFormat(projectSnapshot.devDependencies, { specifiers })
: projectSnapshot.devDependencies,
dependenciesMeta: projectSnapshot.dependenciesMeta,
}
}

function convertResolvedDependenciesToInlineSpecifiersFormat (
resolvedDependencies: ResolvedDependencies,
{ specifiers }: { specifiers: ResolvedDependencies}
): InlineSpecifiersResolvedDependencies {
const result: InlineSpecifiersResolvedDependencies = {}
for (const [depName, version] of Object.entries(resolvedDependencies)) {
const specifier = specifiers[depName]
result[depName] = { specifier, version }
}
return result
}

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: (el: T) => U): Record<string, U> {
const result = {}
for (const [key, value] of Object.entries(obj)) {
result[key] = mapper(value)
}
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
2 changes: 2 additions & 0 deletions packages/lockfile-file/src/sortLockfileKeys.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import sortKeys from 'sort-keys'
import { InlineSpecifiersFormatLockfileKey } from './experiments/InlineSpecifiersLockfile'
import { LockfileFile } from './write'

const ORDERED_KEYS = {
Expand Down Expand Up @@ -38,6 +39,7 @@ const ROOT_KEYS_ORDER = {
overrides: 3,
packageExtensionsChecksum: 4,
patchedDependencies: 5,
[InlineSpecifiersFormatLockfileKey]: 6,
specifiers: 10,
dependencies: 11,
optionalDependencies: 12,
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 baf8c0d

Please sign in to comment.