Files
pnpm/pkg-manager/core/src/install/allProjectsAreUpToDate.ts
await-ovo b8cb91cf48 fix: treat the linked dependency which version type is tag as update-to-date (#6791)
* fix(core): treat the linked dependency which version type is tag as update-to-date

* docs: add changeset

* chore: code style
2023-07-12 03:58:54 +03:00

192 lines
7.2 KiB
TypeScript

import path from 'path'
import { type ProjectOptions } from '@pnpm/get-context'
import {
type PackageSnapshot,
type Lockfile,
type ProjectSnapshot,
type PackageSnapshots,
} from '@pnpm/lockfile-file'
import { refIsLocalDirectory, refIsLocalTarball, satisfiesPackageManifest } from '@pnpm/lockfile-utils'
import { safeReadPackageJsonFromDir } from '@pnpm/read-package-json'
import { type DirectoryResolution, type WorkspacePackages } from '@pnpm/resolver-base'
import {
DEPENDENCIES_FIELDS,
DEPENDENCIES_OR_PEER_FIELDS,
type DependencyManifest,
type ProjectManifest,
} from '@pnpm/types'
import pEvery from 'p-every'
import any from 'ramda/src/any'
import semver from 'semver'
import getVersionSelectorType from 'version-selector-type'
export async function allProjectsAreUpToDate (
projects: Array<ProjectOptions & { id: string }>,
opts: {
autoInstallPeers: boolean
excludeLinksFromLockfile: boolean
linkWorkspacePackages: boolean
wantedLockfile: Lockfile
workspacePackages: WorkspacePackages
lockfileDir: string
}
) {
const manifestsByDir = opts.workspacePackages ? getWorkspacePackagesByDirectory(opts.workspacePackages) : {}
const _satisfiesPackageManifest = satisfiesPackageManifest.bind(null, {
autoInstallPeers: opts.autoInstallPeers,
excludeLinksFromLockfile: opts.excludeLinksFromLockfile,
})
const _linkedPackagesAreUpToDate = linkedPackagesAreUpToDate.bind(null, {
linkWorkspacePackages: opts.linkWorkspacePackages,
manifestsByDir,
workspacePackages: opts.workspacePackages,
lockfilePackages: opts.wantedLockfile.packages,
lockfileDir: opts.lockfileDir,
})
return pEvery(projects, (project) => {
const importer = opts.wantedLockfile.importers[project.id]
return !hasLocalTarballDepsInRoot(importer) &&
_satisfiesPackageManifest(importer, project.manifest).satisfies &&
_linkedPackagesAreUpToDate({
dir: project.rootDir,
manifest: project.manifest,
snapshot: importer,
})
})
}
function getWorkspacePackagesByDirectory (workspacePackages: WorkspacePackages) {
const workspacePackagesByDirectory: Record<string, DependencyManifest> = {}
Object.keys(workspacePackages || {}).forEach((pkgName) => {
Object.keys(workspacePackages[pkgName] || {}).forEach((pkgVersion) => {
workspacePackagesByDirectory[workspacePackages[pkgName][pkgVersion].dir] = workspacePackages[pkgName][pkgVersion].manifest
})
})
return workspacePackagesByDirectory
}
async function linkedPackagesAreUpToDate (
{
linkWorkspacePackages,
manifestsByDir,
workspacePackages,
lockfilePackages,
lockfileDir,
}: {
linkWorkspacePackages: boolean
manifestsByDir: Record<string, DependencyManifest>
workspacePackages: WorkspacePackages
lockfilePackages?: PackageSnapshots
lockfileDir: string
},
project: {
dir: string
manifest: ProjectManifest
snapshot: ProjectSnapshot
}
) {
return pEvery(
DEPENDENCIES_FIELDS,
(depField) => {
const lockfileDeps = project.snapshot[depField]
const manifestDeps = project.manifest[depField]
if ((lockfileDeps == null) || (manifestDeps == null)) return true
const depNames = Object.keys(lockfileDeps)
return pEvery(
depNames,
async (depName) => {
const currentSpec = manifestDeps[depName]
if (!currentSpec) return true
const lockfileRef = lockfileDeps[depName]
if (refIsLocalDirectory(project.snapshot.specifiers[depName])) {
return isLocalFileDepUpdated(lockfileDir, lockfilePackages?.[lockfileRef])
}
const isLinked = lockfileRef.startsWith('link:')
if (
isLinked &&
(
currentSpec.startsWith('link:') ||
currentSpec.startsWith('file:') ||
currentSpec.startsWith('workspace:.')
)
) {
return true
}
// https://github.com/pnpm/pnpm/issues/6592
// if the dependency is linked and the specified version type is tag, we consider it to be up-to-date to skip full resolution.
if (isLinked && getVersionSelectorType(currentSpec)?.type === 'tag') {
return true
}
const linkedDir = isLinked
? path.join(project.dir, lockfileRef.slice(5))
: workspacePackages?.[depName]?.[lockfileRef]?.dir
if (!linkedDir) return true
if (!linkWorkspacePackages && !currentSpec.startsWith('workspace:')) {
// we found a linked dir, but we don't want to use it, because it's not specified as a
// workspace:x.x.x dependency
return true
}
const linkedPkg = manifestsByDir[linkedDir] ?? await safeReadPackageJsonFromDir(linkedDir)
const availableRange = getVersionRange(currentSpec)
// This should pass the same options to semver as @pnpm/npm-resolver
const localPackageSatisfiesRange = availableRange === '*' || availableRange === '^' || availableRange === '~' ||
linkedPkg && semver.satisfies(linkedPkg.version, availableRange, { loose: true })
if (isLinked !== localPackageSatisfiesRange) return false
return true
}
)
}
)
}
async function isLocalFileDepUpdated (lockfileDir: string, pkgSnapshot: PackageSnapshot | undefined) {
if (!pkgSnapshot) return false
const localDepDir = path.join(lockfileDir, (pkgSnapshot.resolution as DirectoryResolution).directory)
const manifest = await safeReadPackageJsonFromDir(localDepDir)
if (!manifest) return false
for (const depField of DEPENDENCIES_OR_PEER_FIELDS) {
if (depField === 'devDependencies') continue
const manifestDeps = manifest[depField] ?? {}
const lockfileDeps = pkgSnapshot[depField] ?? {}
// Lock file has more dependencies than the current manifest, e.g. some dependencies are removed.
if (Object.keys(lockfileDeps).some(depName => !manifestDeps[depName])) {
return false
}
for (const depName of Object.keys(manifestDeps)) {
// If a dependency does not exist in the lock file, e.g. a new dependency is added to the current manifest.
// We need to do full resolution again.
if (!lockfileDeps[depName]) {
return false
}
const currentSpec = manifestDeps[depName]
// We do not care about the link dependencies of local dependency.
if (currentSpec.startsWith('file:') || currentSpec.startsWith('link:') || currentSpec.startsWith('workspace:')) continue
if (semver.satisfies(lockfileDeps[depName], getVersionRange(currentSpec), { loose: true })) {
continue
} else {
return false
}
}
}
return true
}
function getVersionRange (spec: string) {
if (spec.startsWith('workspace:')) return spec.slice(10)
if (spec.startsWith('npm:')) {
spec = spec.slice(4)
const index = spec.indexOf('@', 1)
if (index === -1) return '*'
return spec.slice(index + 1) || '*'
}
return spec
}
function hasLocalTarballDepsInRoot (importer: ProjectSnapshot) {
return any(refIsLocalTarball, Object.values(importer.dependencies ?? {})) ||
any(refIsLocalTarball, Object.values(importer.devDependencies ?? {})) ||
any(refIsLocalTarball, Object.values(importer.optionalDependencies ?? {}))
}