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:
Josiah Grace
2020-06-08 05:41:26 -07:00
committed by GitHub
parent 2ebb7af333
commit 13630c6593
5 changed files with 177 additions and 11 deletions

View 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.

View File

@@ -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

View File

@@ -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) {

View File

@@ -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,
}
)
)
})

View File

@@ -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',