diff --git a/.changeset/long-squids-wash.md b/.changeset/long-squids-wash.md new file mode 100644 index 0000000000..353622f386 --- /dev/null +++ b/.changeset/long-squids-wash.md @@ -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. diff --git a/packages/supi/src/install/allProjectsAreUpToDate.ts b/packages/supi/src/install/allProjectsAreUpToDate.ts index b379028fb5..dc3188f307 100644 --- a/packages/supi/src/install/allProjectsAreUpToDate.ts +++ b/packages/supi/src/install/allProjectsAreUpToDate.ts @@ -19,18 +19,27 @@ import semver = require('semver') export default async function allProjectsAreUpToDate ( projects: Array, 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, - workspacePackages: WorkspacePackages, - manifest: ProjectManifest, - projectSnapshot: ProjectSnapshot, - projectDir: string + { + linkWorkspacePackages, + manifestsByDir, + workspacePackages, + }: { + linkWorkspacePackages: boolean, + manifestsByDir: Record, + 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 diff --git a/packages/supi/src/install/index.ts b/packages/supi/src/install/index.ts index ed10d2efed..b1e7ac682d 100644 --- a/packages/supi/src/install/index.ts +++ b/packages/supi/src/install/index.ts @@ -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) { diff --git a/packages/supi/test/allProjectsAreUpToDate.test.ts b/packages/supi/test/allProjectsAreUpToDate.test.ts index 615c5be2b4..965da561ca 100644 --- a/packages/supi/test/allProjectsAreUpToDate.test.ts +++ b/packages/supi/test/allProjectsAreUpToDate.test.ts @@ -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, + } + ) + ) +}) diff --git a/packages/supi/test/install/multipleImporters.ts b/packages/supi/test/install/multipleImporters.ts index 726f7b7cd3..bed06a96a3 100644 --- a/packages/supi/test/install/multipleImporters.ts +++ b/packages/supi/test/install/multipleImporters.ts @@ -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',