Skip to content

pnpm update: an active override referencing a catalog is applied with the pre-update catalog during resolution (graph stays stale) #12159

Description

@fengmk2

Follow-up to #12158. That PR fixes the lockfile overrides metadata going stale after pnpm update bumps a catalog. This issue tracks a deeper case the metadata fix does not cover.

Problem

When an override references a catalog (overrides: { 'parent>child': 'catalog:' }) and the override is active (it forces a dependency that is actually in the resolved graph), a pnpm update that bumps the catalog applies the old override value to the graph. The resolution readPackageHook is built from opts.parsedOverrides, which is resolved once against the pre-update catalog in installing/deps-installer/src/install/extendInstallOptions.ts. updatedCatalogs is only known after resolveDependencies, so the resolved snapshots keep the pre-update version.

With #12158 applied (which fixes lockfile.overrides metadata), the result is a lockfile that records an override the graph does not honor:

after `pnpm update --latest` (catalog @pnpm.e2e/foo 100.0.0 -> latest 100.1.0):
  catalogs.default['@pnpm.e2e/foo']                         : { specifier: 100.1.0, version: 100.1.0 }
  overrides['@pnpm.e2e/foobar>@pnpm.e2e/foo']               : 100.1.0   # fixed metadata (#12158)
  snapshots['@pnpm.e2e/foobar@100.0.0'].dependencies.foo    : 100.0.0   # STALE graph
  foo snapshots present                                     : 100.0.0 AND 100.1.0

A fresh pnpm install (with the post-update catalog) resolves @pnpm.e2e/foobar's @pnpm.e2e/foo to 100.1.0, so the pnpm update result diverges from a clean resolution. pnpm install --frozen-lockfile passes (the metadata is now consistent) but silently installs the old version for that transitive dependency. Without #12158 this case surfaced loudly as ERR_PNPM_LOCKFILE_CONFIG_MISMATCH; the metadata fix removes that error while the graph is still stale.

Failing test

Drop into installing/deps-installer/test/catalogs.ts (inside describe('update')):

test('an active catalog-backed override on a transitive dep stays consistent with the graph after update', async () => {
  await addDistTag({ package: '@pnpm.e2e/foo', version: '100.1.0', distTag: 'latest' })
  const { options, projects, readLockfile } = preparePackagesAndReturnObjects([{
    name: 'project1',
    dependencies: { '@pnpm.e2e/foo': 'catalog:', '@pnpm.e2e/foobar': '100.0.0' },
  }])
  const mutateOpts = {
    ...options,
    lockfileOnly: true,
    catalogs: { default: { '@pnpm.e2e/foo': '100.0.0' } },
    overrides: { '@pnpm.e2e/foobar>@pnpm.e2e/foo': 'catalog:' },
  }
  await mutateModules(installProjects(projects), mutateOpts)
  await addDependenciesToPackage(
    projects['project1' as ProjectId],
    ['@pnpm.e2e/foo'],
    { ...mutateOpts, dir: path.join(options.lockfileDir, 'project1'), update: true, updateToLatest: true })
  const lf = readLockfile()
  expect(lf.catalogs.default).toEqual({ '@pnpm.e2e/foo': { specifier: '100.1.0', version: '100.1.0' } })
  expect(lf.overrides).toEqual({ '@pnpm.e2e/foobar>@pnpm.e2e/foo': '100.1.0' })
  // Fails today: @pnpm.e2e/foobar's @pnpm.e2e/foo is still 100.0.0.
  expect(lf.snapshots['@pnpm.e2e/foobar@100.0.0'].dependencies?.['@pnpm.e2e/foo']).toBe('100.1.0')
})

Likely fix direction

Resolution needs to apply overrides using the updated catalog. Options:

  • Re-resolve when an override references a catalog entry that updatedCatalogs bumped (a second pass, only when needed), or
  • Pre-compute the catalog --latest bumps before resolution so the readPackageHook / parsedOverrides are built from the updated catalog and a single pass is correct.

Related: #12158 (metadata fix for the common case).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions