/
allProjectsAreUpToDate.ts
136 lines (130 loc) · 4.84 KB
/
allProjectsAreUpToDate.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import path from 'path'
import { type ProjectOptions } from '@pnpm/get-context'
import {
type Lockfile,
type ProjectSnapshot,
} from '@pnpm/lockfile-file'
import { satisfiesPackageManifest } from '@pnpm/lockfile-utils'
import { safeReadPackageJsonFromDir } from '@pnpm/read-package-json'
import { type WorkspacePackages } from '@pnpm/resolver-base'
import {
DEPENDENCIES_FIELDS,
type DependencyManifest,
type ProjectManifest,
} from '@pnpm/types'
import pEvery from 'p-every'
import any from 'ramda/src/any'
import semver from 'semver'
export async function allProjectsAreUpToDate (
projects: Array<ProjectOptions & { id: string }>,
opts: {
autoInstallPeers: boolean
excludeLinksFromLockfile: boolean
linkWorkspacePackages: boolean
wantedLockfile: Lockfile
workspacePackages: WorkspacePackages
}
) {
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,
})
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,
}: {
linkWorkspacePackages: boolean
manifestsByDir: Record<string, DependencyManifest>
workspacePackages: WorkspacePackages
},
project: {
dir: string
manifest: ProjectManifest
snapshot: ProjectSnapshot
}
) {
for (const depField of DEPENDENCIES_FIELDS) {
const lockfileDeps = project.snapshot[depField]
const manifestDeps = project.manifest[depField]
if ((lockfileDeps == null) || (manifestDeps == null)) continue
const depNames = Object.keys(lockfileDeps)
for (const depName of depNames) {
const currentSpec = manifestDeps[depName]
if (!currentSpec) continue
const lockfileRef = lockfileDeps[depName]
const isLinked = lockfileRef.startsWith('link:')
if (
isLinked &&
(
currentSpec.startsWith('link:') ||
currentSpec.startsWith('file:') ||
currentSpec.startsWith('workspace:.')
)
) {
continue
}
const linkedDir = isLinked
? path.join(project.dir, lockfileRef.slice(5))
: workspacePackages?.[depName]?.[lockfileRef]?.dir
if (!linkedDir) continue
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
continue
}
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
}
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 ?? {}))
}
function refIsLocalTarball (ref: string) {
return ref.startsWith('file:') && (ref.endsWith('.tgz') || ref.endsWith('.tar.gz') || ref.endsWith('.tar'))
}