diff --git a/.changeset/rare-avocados-listen.md b/.changeset/rare-avocados-listen.md new file mode 100644 index 0000000000..1b66be053e --- /dev/null +++ b/.changeset/rare-avocados-listen.md @@ -0,0 +1,6 @@ +--- +"@pnpm/core": patch +pnpm: patch +--- + +Fix a bug causing catalog snapshots to be removed from the `pnpm-lock.yaml` file when using `--fix-lockfile` and `--filter`. [#8639](https://github.com/pnpm/pnpm/issues/8639) diff --git a/pkg-manager/core/src/install/index.ts b/pkg-manager/core/src/install/index.ts index 085791e4b7..0056c35c38 100644 --- a/pkg-manager/core/src/install/index.ts +++ b/pkg-manager/core/src/install/index.ts @@ -1433,74 +1433,56 @@ const installInContext: InstallFunction = async (projects, ctx, opts) => { const allProjectsLocatedInsideWorkspace = Object.values(ctx.projects) .filter((project) => isPathInsideWorkspace(project.rootDirRealPath ?? project.rootDir)) if (allProjectsLocatedInsideWorkspace.length > projects.length) { - if ( - allMutationsAreInstalls(projects) && - await allProjectsAreUpToDate(allProjectsLocatedInsideWorkspace, { - catalogs: opts.catalogs, - autoInstallPeers: opts.autoInstallPeers, - excludeLinksFromLockfile: opts.excludeLinksFromLockfile, - linkWorkspacePackages: opts.linkWorkspacePackagesDepth >= 0, - wantedLockfile: ctx.wantedLockfile, - workspacePackages: ctx.workspacePackages, - lockfileDir: opts.lockfileDir, - }) - ) { - return installInContext(projects, ctx, { - ...opts, - frozenLockfile: true, - }) - } else { - const newProjects = [...projects] - const getWantedDepsOpts = { - autoInstallPeers: opts.autoInstallPeers, - includeDirect: opts.includeDirect, - updateWorkspaceDependencies: false, - nodeExecPath: opts.nodeExecPath, - injectWorkspacePackages: opts.injectWorkspacePackages, - } - const _isWantedDepPrefSame = isWantedDepPrefSame.bind(null, ctx.wantedLockfile.catalogs, opts.catalogs) - for (const project of allProjectsLocatedInsideWorkspace) { - if (!newProjects.some(({ rootDir }) => rootDir === project.rootDir)) { - // This code block mirrors the installCase() function in - // mutateModules(). Consider a refactor that combines this logic - // to deduplicate code. - const wantedDependencies = getWantedDependencies(project.manifest, getWantedDepsOpts) - .map((wantedDependency) => ({ ...wantedDependency, updateSpec: true, preserveNonSemverVersionSpec: true })) - forgetResolutionsOfPrevWantedDeps(ctx.wantedLockfile.importers[project.id], wantedDependencies, _isWantedDepPrefSame) - newProjects.push({ - mutation: 'install', - ...project, - wantedDependencies, - pruneDirectDependencies: false, - updatePackageManifest: false, - }) - } - } - const result = await installInContext(newProjects, ctx, { - ...opts, - lockfileOnly: true, - }) - const { stats, ignoredBuilds } = await headlessInstall({ - ...ctx, - ...opts, - currentEngine: { - nodeVersion: opts.nodeVersion, - pnpmVersion: opts.packageManager.name === 'pnpm' ? opts.packageManager.version : '', - }, - currentHoistedLocations: ctx.modulesFile?.hoistedLocations, - selectedProjectDirs: projects.map((project) => project.rootDir), - allProjects: ctx.projects, - prunedAt: ctx.modulesFile?.prunedAt, - wantedLockfile: result.newLockfile, - useLockfile: opts.useLockfile && ctx.wantedLockfileIsModified, - hoistWorkspacePackages: opts.hoistWorkspacePackages, - }) - return { - ...result, - stats, - ignoredBuilds, + const newProjects = [...projects] + const getWantedDepsOpts = { + autoInstallPeers: opts.autoInstallPeers, + includeDirect: opts.includeDirect, + updateWorkspaceDependencies: false, + nodeExecPath: opts.nodeExecPath, + injectWorkspacePackages: opts.injectWorkspacePackages, + } + const _isWantedDepPrefSame = isWantedDepPrefSame.bind(null, ctx.wantedLockfile.catalogs, opts.catalogs) + for (const project of allProjectsLocatedInsideWorkspace) { + if (!newProjects.some(({ rootDir }) => rootDir === project.rootDir)) { + // This code block mirrors the installCase() function in + // mutateModules(). Consider a refactor that combines this logic to + // deduplicate code. + const wantedDependencies = getWantedDependencies(project.manifest, getWantedDepsOpts) + .map((wantedDependency) => ({ ...wantedDependency, updateSpec: true, preserveNonSemverVersionSpec: true })) + forgetResolutionsOfPrevWantedDeps(ctx.wantedLockfile.importers[project.id], wantedDependencies, _isWantedDepPrefSame) + newProjects.push({ + mutation: 'install', + ...project, + wantedDependencies, + pruneDirectDependencies: false, + updatePackageManifest: false, + }) } } + const result = await installInContext(newProjects, ctx, { + ...opts, + lockfileOnly: true, + }) + const { stats, ignoredBuilds } = await headlessInstall({ + ...ctx, + ...opts, + currentEngine: { + nodeVersion: opts.nodeVersion, + pnpmVersion: opts.packageManager.name === 'pnpm' ? opts.packageManager.version : '', + }, + currentHoistedLocations: ctx.modulesFile?.hoistedLocations, + selectedProjectDirs: projects.map((project) => project.rootDir), + allProjects: ctx.projects, + prunedAt: ctx.modulesFile?.prunedAt, + wantedLockfile: result.newLockfile, + useLockfile: opts.useLockfile && ctx.wantedLockfileIsModified, + hoistWorkspacePackages: opts.hoistWorkspacePackages, + }) + return { + ...result, + stats, + ignoredBuilds, + } } } if (opts.nodeLinker === 'hoisted' && !opts.lockfileOnly) { diff --git a/pkg-manager/core/test/catalogs.ts b/pkg-manager/core/test/catalogs.ts index 452725abf4..81b2a0d814 100644 --- a/pkg-manager/core/test/catalogs.ts +++ b/pkg-manager/core/test/catalogs.ts @@ -3,6 +3,7 @@ import { type ProjectRootDir, type ProjectId, type ProjectManifest } from '@pnpm import { prepareEmpty } from '@pnpm/prepare' import { addDistTag } from '@pnpm/registry-mock' import { type MutatedProject, mutateModules, type ProjectOptions, type MutateModulesOptions, addDependenciesToPackage } from '@pnpm/core' +import { type CatalogSnapshots } from '@pnpm/lockfile.types' import { sync as loadJsonFile } from 'load-json-file' import path from 'path' import { testDefaults } from './utils' @@ -379,6 +380,61 @@ test('lockfile catalog snapshots do not contain stale references on --filter', a expect(loadJsonFile(pathToIsPositivePkgJson)?.version).toBe('3.1.0') }) +// Regression test for https://github.com/pnpm/pnpm/issues/8639 +test('--fix-lockfile with --filter does not erase catalog snapshots', async () => { + const { options, projects, readLockfile } = preparePackagesAndReturnObjects([ + { + name: 'project1', + dependencies: { + 'is-negative': 'catalog:', + }, + }, + { + name: 'project2', + dependencies: { + 'is-positive': 'catalog:', + }, + }, + ]) + + const catalogs = { + default: { + 'is-positive': '^1.0.0', + 'is-negative': '^1.0.0', + }, + } + + const expectedCatalogsSnapshot: CatalogSnapshots = { + default: { + 'is-negative': { specifier: '^1.0.0', version: '1.0.0' }, + 'is-positive': { specifier: '^1.0.0', version: '1.0.0' }, + }, + } + + await mutateModules(installProjects(projects), { + ...options, + lockfileOnly: true, + catalogs, + }) + + // Sanity check this test is set up correctly. + expect(readLockfile().catalogs).toStrictEqual(expectedCatalogsSnapshot) + + // The catalogs snapshot should still be the same after performing a filtered + // install with --fix-lockfile. + const onlyProject1 = installProjects(projects).slice(0, 1) + expect(onlyProject1).toMatchObject([{ id: 'project1' }]) + + await mutateModules(onlyProject1, { + ...options, + lockfileOnly: true, + fixLockfile: true, + catalogs, + }) + + expect(readLockfile().catalogs).toStrictEqual(expectedCatalogsSnapshot) +}) + test('external dependency using catalog protocol errors', async () => { const { options, projects } = preparePackagesAndReturnObjects([ {