mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-10 18:18:56 -04:00
fix(resolution): respect linked-workspaces = false in up-to-date check
* fix(up-to-date-projects): respect linked-workspaces = false in up-to-date check When linked-workspaces = false and the lockfile is up to date, the projects up-to-date check is incorrectly re-running the resolution step. This happens in workspaces where packages are using both the workspace version of a package, and a registry version of the package. Fixes #2618 PR #2619 * fix: lockfile up-to-date check Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
5
.changeset/long-squids-wash.md
Normal file
5
.changeset/long-squids-wash.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"supi": patch
|
||||
---
|
||||
|
||||
Perform headless installation when dependencies should not be linked from the workspace, and they are not indeed linked from the workspace.
|
||||
@@ -19,18 +19,27 @@ import semver = require('semver')
|
||||
export default async function allProjectsAreUpToDate (
|
||||
projects: Array<ProjectOptions & { id: string }>,
|
||||
opts: {
|
||||
linkWorkspacePackages: boolean,
|
||||
wantedLockfile: Lockfile,
|
||||
workspacePackages: WorkspacePackages,
|
||||
}
|
||||
) {
|
||||
const manifestsByDir = opts.workspacePackages ? getWorkspacePackagesByDirectory(opts.workspacePackages) : {}
|
||||
const _satisfiesPackageManifest = satisfiesPackageManifest.bind(null, opts.wantedLockfile)
|
||||
const _linkedPackagesAreUpToDate = linkedPackagesAreUpToDate.bind(null, manifestsByDir, opts.workspacePackages)
|
||||
const _linkedPackagesAreUpToDate = linkedPackagesAreUpToDate.bind(null, {
|
||||
linkWorkspacePackages: opts.linkWorkspacePackages,
|
||||
manifestsByDir,
|
||||
workspacePackages: opts.workspacePackages,
|
||||
})
|
||||
return pEvery(projects, async (project) => {
|
||||
const importer = opts.wantedLockfile.importers[project.id]
|
||||
return importer && !hasLocalTarballDepsInRoot(importer) &&
|
||||
_satisfiesPackageManifest(project.manifest, project.id) &&
|
||||
_linkedPackagesAreUpToDate(project.manifest, importer, project.rootDir)
|
||||
_linkedPackagesAreUpToDate({
|
||||
dir: project.rootDir,
|
||||
manifest: project.manifest,
|
||||
snapshot: importer,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -45,15 +54,24 @@ function getWorkspacePackagesByDirectory (workspacePackages: WorkspacePackages)
|
||||
}
|
||||
|
||||
async function linkedPackagesAreUpToDate (
|
||||
manifestsByDir: Record<string, DependencyManifest>,
|
||||
workspacePackages: WorkspacePackages,
|
||||
manifest: ProjectManifest,
|
||||
projectSnapshot: ProjectSnapshot,
|
||||
projectDir: string
|
||||
{
|
||||
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 = projectSnapshot[depField]
|
||||
const manifestDeps = manifest[depField]
|
||||
const lockfileDeps = project.snapshot[depField]
|
||||
const manifestDeps = project.manifest[depField]
|
||||
if (!lockfileDeps || !manifestDeps) continue
|
||||
const depNames = Object.keys(lockfileDeps)
|
||||
for (const depName of depNames) {
|
||||
@@ -71,9 +89,14 @@ async function linkedPackagesAreUpToDate (
|
||||
continue
|
||||
}
|
||||
const linkedDir = isLinked
|
||||
? path.join(projectDir, lockfileRef.substr(5))
|
||||
? path.join(project.dir, lockfileRef.substr(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 safeReadPkgFromDir(linkedDir)
|
||||
const availableRange = getVersionRange(currentSpec)
|
||||
// This should pass the same options to semver as @pnpm/npm-resolver
|
||||
|
||||
@@ -170,7 +170,11 @@ export async function mutateModules (
|
||||
(!opts.pruneLockfileImporters || Object.keys(ctx.wantedLockfile.importers).length === ctx.projects.length) &&
|
||||
ctx.existsWantedLockfile &&
|
||||
ctx.wantedLockfile.lockfileVersion === LOCKFILE_VERSION &&
|
||||
await allProjectsAreUpToDate(ctx.projects, { wantedLockfile: ctx.wantedLockfile, workspacePackages: opts.workspacePackages })
|
||||
await allProjectsAreUpToDate(ctx.projects, {
|
||||
linkWorkspacePackages: opts.linkWorkspacePackagesDepth >= 0,
|
||||
wantedLockfile: ctx.wantedLockfile,
|
||||
workspacePackages: opts.workspacePackages,
|
||||
})
|
||||
)
|
||||
) {
|
||||
if (!ctx.existsWantedLockfile) {
|
||||
|
||||
@@ -34,6 +34,7 @@ test('allProjectsAreUpToDate(): works with aliased local dependencies', async (t
|
||||
rootDir: 'foo',
|
||||
},
|
||||
], {
|
||||
linkWorkspacePackages: true,
|
||||
wantedLockfile: {
|
||||
importers: {
|
||||
bar: {
|
||||
@@ -71,6 +72,7 @@ test('allProjectsAreUpToDate(): works with aliased local dependencies that speci
|
||||
rootDir: 'foo',
|
||||
},
|
||||
], {
|
||||
linkWorkspacePackages: true,
|
||||
wantedLockfile: {
|
||||
importers: {
|
||||
bar: {
|
||||
@@ -108,6 +110,7 @@ test('allProjectsAreUpToDate(): returns false if the aliased dependency version
|
||||
rootDir: 'foo',
|
||||
},
|
||||
], {
|
||||
linkWorkspacePackages: true,
|
||||
wantedLockfile: {
|
||||
importers: {
|
||||
bar: {
|
||||
@@ -127,3 +130,63 @@ test('allProjectsAreUpToDate(): returns false if the aliased dependency version
|
||||
workspacePackages,
|
||||
}))
|
||||
})
|
||||
|
||||
test('allProjectsAreUpToDate(): use link and registry version if linkWorkspacePackages = false', async (t: tape.Test) => {
|
||||
t.ok(
|
||||
await allProjectsAreUpToDate(
|
||||
[
|
||||
{
|
||||
id: 'bar',
|
||||
manifest: {
|
||||
dependencies: {
|
||||
foo: 'workspace:*',
|
||||
},
|
||||
},
|
||||
rootDir: 'bar',
|
||||
},
|
||||
{
|
||||
id: 'bar2',
|
||||
manifest: {
|
||||
dependencies: {
|
||||
foo: '1.0.0',
|
||||
},
|
||||
},
|
||||
rootDir: 'bar2',
|
||||
},
|
||||
{
|
||||
id: 'foo',
|
||||
manifest: fooManifest,
|
||||
rootDir: 'foo',
|
||||
},
|
||||
],
|
||||
{
|
||||
linkWorkspacePackages: false,
|
||||
wantedLockfile: {
|
||||
importers: {
|
||||
bar: {
|
||||
dependencies: {
|
||||
foo: 'link:../foo',
|
||||
},
|
||||
specifiers: {
|
||||
foo: 'workspace:*',
|
||||
},
|
||||
},
|
||||
bar2: {
|
||||
dependencies: {
|
||||
foo: '1.0.0',
|
||||
},
|
||||
specifiers: {
|
||||
foo: '1.0.0',
|
||||
},
|
||||
},
|
||||
foo: {
|
||||
specifiers: {},
|
||||
},
|
||||
},
|
||||
lockfileVersion: 5,
|
||||
},
|
||||
workspacePackages,
|
||||
}
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
@@ -402,6 +402,77 @@ test('headless install is used with an up-to-date lockfile when package referenc
|
||||
await projects['project-2'].has('is-negative')
|
||||
})
|
||||
|
||||
test('headless install is used when packages are not linked from the workspace (unless workspace ranges are used)', async (t) => {
|
||||
const foo = {
|
||||
name: 'foo',
|
||||
version: '1.0.0',
|
||||
|
||||
dependencies: {
|
||||
'qar': 'workspace:*',
|
||||
},
|
||||
}
|
||||
const bar = {
|
||||
name: 'bar',
|
||||
version: '1.0.0',
|
||||
|
||||
dependencies: {
|
||||
'qar': '100.0.0',
|
||||
},
|
||||
}
|
||||
const qar = {
|
||||
name: 'qar',
|
||||
version: '100.0.0',
|
||||
}
|
||||
const projects = preparePackages(t, [foo, bar, qar])
|
||||
|
||||
const importers: MutatedProject[] = [
|
||||
{
|
||||
buildIndex: 0,
|
||||
manifest: foo,
|
||||
mutation: 'install',
|
||||
rootDir: path.resolve('foo'),
|
||||
},
|
||||
{
|
||||
buildIndex: 0,
|
||||
manifest: bar,
|
||||
mutation: 'install',
|
||||
rootDir: path.resolve('bar'),
|
||||
},
|
||||
{
|
||||
buildIndex: 0,
|
||||
manifest: qar,
|
||||
mutation: 'install',
|
||||
rootDir: path.resolve('qar'),
|
||||
},
|
||||
]
|
||||
const workspacePackages = {
|
||||
'qar': {
|
||||
'100.0.0': {
|
||||
dir: path.resolve('qar'),
|
||||
manifest: qar,
|
||||
},
|
||||
},
|
||||
}
|
||||
await mutateModules(importers, await testDefaults({
|
||||
linkWorkspacePackagesDepth: -1,
|
||||
lockfileOnly: true,
|
||||
workspacePackages,
|
||||
}))
|
||||
|
||||
const reporter = sinon.spy()
|
||||
await mutateModules(importers, await testDefaults({
|
||||
linkWorkspacePackagesDepth: -1,
|
||||
reporter,
|
||||
workspacePackages,
|
||||
}))
|
||||
|
||||
t.ok(reporter.calledWithMatch({
|
||||
level: 'info',
|
||||
message: 'Lockfile is up-to-date, resolution step is skipped',
|
||||
name: 'pnpm',
|
||||
}), 'start of headless installation logged')
|
||||
})
|
||||
|
||||
test('current lockfile contains only installed dependencies when adding a new importer to workspace with shared lockfile', async (t) => {
|
||||
const pkg1 = {
|
||||
name: 'project-1',
|
||||
|
||||
Reference in New Issue
Block a user