Skip to content

fix: suppress packageManager/devEngines.packageManager conflict warning when values match exactly#11307

Merged
zkochan merged 3 commits into
mainfrom
fix/11301
Apr 19, 2026
Merged

fix: suppress packageManager/devEngines.packageManager conflict warning when values match exactly#11307
zkochan merged 3 commits into
mainfrom
fix/11301

Conversation

@zkochan

@zkochan zkochan commented Apr 19, 2026

Copy link
Copy Markdown
Member

Summary

  • Suppress the Cannot use both "packageManager" and "devEngines.packageManager" in package.json. "packageManager" will be ignored warning only when both fields specify the exact same package manager name and the exact same version string. Any other divergence (different name, range vs. exact version, prefixed versions like v1.2.3, etc.) still warns.
  • Lets projects keep both fields during migration (e.g. so v10 installs still auto-switch via packageManager, while v11 uses devEngines.packageManager and npm install still errors) without a noisy warning — as long as the two values are kept in sync.

Closes #11301

Test plan

  • pnpm --filter pnpm test packageManagerCheck.test.ts — added cases for same-exact-version (no warning), name mismatch (warns), and range vs. exact version (warns). All tests pass.
  • pnpm --filter @pnpm/config.reader run lint clean.
  • Manual: running pnpm install in a project with matching "packageManager": "pnpm@X" and "devEngines.packageManager": { name: "pnpm", version: "X" } no longer prints the warning; any divergence still prints it.

…ng when compatible

Only emit the "Cannot use both packageManager and devEngines.packageManager"
warning when the two fields actually conflict: different package manager name,
or the legacy packageManager exact version does not satisfy the
devEngines.packageManager version range. This lets projects keep both fields
during migration without a noisy warning.

Closes #11301
Copilot AI review requested due to automatic review settings April 19, 2026 21:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refines pnpm’s packageManager vs devEngines.packageManager warning behavior so projects can keep both fields during migration without noisy warnings when they’re effectively compatible.

Changes:

  • Only emits the “Cannot use both …” warning when the two fields actually conflict (PM name differs, or legacy packageManager exact version does not satisfy the devEngines.packageManager range).
  • Adds test coverage for compatible vs conflicting combinations.
  • Adds a changeset for patch releases of pnpm and @pnpm/config.reader.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
config/reader/src/index.ts Adds compatibility/conflict detection to suppress the warning when fields align.
pnpm/test/packageManagerCheck.test.ts Adds new test cases for warning suppression and conflict warnings.
.changeset/dev-engines-package-manager-compatible.md Documents the user-facing behavior change for release notes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread config/reader/src/index.ts Outdated
// If either side lacks a version, there's nothing concrete to conflict on.
if (pmFromDevEngines.version == null || legacyPm.version == null) return false
const legacyVersion = semver.valid(legacyPm.version)
if (legacyVersion == null) return true

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

packageManagerFieldsConflict() treats legacy packageManager versions like pnpm@v1.2.3 as compatible because semver.valid() normalizes them to 1.2.3. However, elsewhere in this same function (getWantedPackageManager when only packageManager is used) non-canonical versions are explicitly rejected (cleanVersion !== pm.version). With both fields present, this change can now suppress the conflict warning even though the legacy packageManager value would be ignored by pnpm v10/v11 auto-switching logic. Consider treating cleanVersion !== legacyPm.version as a conflict (or otherwise preserving the warning) so the warning is only suppressed when packageManager contains a valid exact version string.

Suggested change
if (legacyVersion == null) return true
if (legacyVersion == null) return true
if (legacyVersion !== legacyPm.version) return true

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in e563e5d. packageManagerFieldsConflict now returns true when the legacy version isn't a canonical exact version, mirroring the dedicated "not a valid exact version" warning from the non-devEngines path.


expect(stderr.toString()).not.toContain('Cannot use both')
})

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new warning-suppression behavior isn’t covered for non-canonical legacy versions (e.g. packageManager: "pnpm@v1.2.3") which currently become “compatible” due to semver normalization. Adding a test for this case would prevent regressions and clarify the intended behavior (warn vs suppress) when packageManager is not a strict exact version.

Suggested change
test('no warning when packageManager uses a non-canonical legacy exact version compatible via semver normalization', async () => {
prepare({
packageManager: 'pnpm@v1.2.3',
devEngines: {
packageManager: {
name: 'pnpm',
version: '1.2.3',
onFail: 'ignore',
},
},
})
const { stderr } = execPnpmSync(['install'])
expect(stderr.toString()).not.toContain('Cannot use both')
})

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this test in e563e5d — it asserts the warning still fires for pnpm@v1.2.3 even when the devEngines.packageManager value would normalize to match.

zkochan added 2 commits April 19, 2026 23:29
Address Copilot review feedback: the legacy `packageManager` field is only
respected for canonical exact versions (e.g. "pnpm@1.2.3"). A value like
"pnpm@v1.2.3" is normally rejected by the dedicated "not a valid exact version"
warning. When both fields are present, we should not silently suppress that
signal by normalizing the value via semver.valid — keep the warning so the
malformed legacy value still surfaces.
…er fields

Simplify the compatibility check: the "Cannot use both" warning is suppressed
only when the `packageManager` and `devEngines.packageManager` fields specify
the same package manager name and the exact same version string. Any other
mismatch — including ranges like `>=1.2.3` that would have previously been
considered "satisfied" — now warns.
@zkochan zkochan changed the title fix: suppress packageManager/devEngines.packageManager conflict warning when fields are compatible fix: suppress packageManager/devEngines.packageManager conflict warning when values match exactly Apr 19, 2026
@zkochan zkochan merged commit 7d25bc1 into main Apr 19, 2026
12 checks passed
@zkochan zkochan deleted the fix/11301 branch April 19, 2026 23:08
lawrence-forooghian added a commit to ably/ably-ai-transport-js that referenced this pull request May 27, 2026
Adopts pnpm for the lib and both demo apps, replacing npm. The
substantive motivation is supply-chain hardening: pnpm ships
configurable guardrails (publish cooldown, post-install-script
allowlist) and a strict node_modules layout that npm doesn't have.
Secondary motivation is fixing the cross-platform optional-deps
bug in npm's lockfile [1] that has been hitting demo app builds.

Based on the realtime PR [2] but diverges from it in three
deliberate ways:

- Strict node_modules layout (the pnpm default), rather than
  realtime's `nodeLinker: hoisted`. Realtime opted for hoisted
  because its codebase had phantom dependencies to clean up
  first; this project is younger and we'd rather catch any
  phantom imports immediately by relying on strict isolation
  from the start.
- `devEngines.packageManager` instead of the `only-allow`
  preinstall hook. `only-allow` was archived on 22/4/2026 [3];
  `devEngines.packageManager` is its successor and requires
  pnpm 11+. We're not constrained to pnpm 10 (realtime is,
  hence its choice).
- No mise. Realtime uses mise as a polyglot toolchain manager
  (Go + Node + Erlang + protoc) and as its task runner. This
  project is single-language and has no other tools to
  coordinate. Skip mise; rely on Corepack to dispatch the
  pinned pnpm version, and on the existing `engines.node` to
  constrain Node.

Lib root changes (package.json + pnpm-workspace.yaml):

- `packageManager: "pnpm@11.3.0"` for Corepack / CI dispatch.
- `devEngines.packageManager` with the same exact version pin,
  so the two fields don't conflict. pnpm flags any discrepancy
  between them with a warning; matching values suppresses it [4].
- `pnpm-workspace.yaml` carries supply-chain config: an
  `allowBuilds` allowlist (esbuild, unrs-resolver — derived
  empirically from ERR_PNPM_IGNORED_BUILDS), a 7-day publish
  cooldown via `minimumReleaseAge`, and an exclude list for
  Ably's own packages.
- `package-lock.json` replaced with `pnpm-lock.yaml`.
- The lib's `prepare`, `build` and `precommit` script chains
  now invoke `pnpm run` rather than `npm run` — required
  because the `prepare` hook runs at install time and
  `devEngines.packageManager` blocks npm from running inside
  the repo.

Demos (use-chat and use-client-session):

- Each demo's `package-lock.json` is replaced with
  `pnpm-lock.yaml`.
- Each demo gets its own `pnpm-workspace.yaml` mirroring the
  root (with `sharp` added to `allowBuilds` for Next.js image
  optimisation).
- Each demo's `package.json` gains its own `packageManager`
  and `devEngines.packageManager` fields. Without these,
  `npm install` in a demo directory would proceed silently
  (the root's devEngines is not visible to npm in a
  subdirectory), then fail later when the lib's prepare
  invokes pnpm. The duplication across lib + demos is kept
  in sync manually.
- `use-chat` demo's `prebuild` and `vercel.json`
  `installCommand` switch from `npm install` to
  `pnpm install`.
- Each demo's `allowBuilds` was derived the same way as the
  lib: running pnpm install in the demo and inspecting the
  ERR_PNPM_IGNORED_BUILDS output.

The decision to enforce pnpm in the demos as well as the lib is
deliberate. A softer position — letting npm drive demo installs
while still requiring pnpm for the lib build — would give npm
users non-reproducible installs (their `package-lock.json` would
drift from our committed `pnpm-lock.yaml`) and would bypass the
supply-chain hardening config (cooldown, allowBuilds) which is
pnpm-specific. Whether the demos are a thing users actually
clone and run or whether they're primarily internal references
is not entirely clear; for now we've optimised for contributor
experience consistency (one PM everywhere). A follow-up could
add a README to each demo documenting the pnpm commands.

Claude suggested a follow-up converting the demos to formal pnpm
workspace members. The motivation would be to eliminate the
`cd ../../../.. && pnpm install && pnpm run build` prebuild
dance, collapse the three lockfiles to one root lockfile, and
pull all the duplicated config (the per-demo
`pnpm-workspace.yaml`, the duplicated `packageManager` /
`devEngines.packageManager` in each demo `package.json`) up
into a single root declaration. Deferred: workspaces are an npm
feature too (since npm 7) and not pnpm-specific, so the fact
that this project hadn't already adopted them under npm
suggests the question of whether they're a good fit deserves
its own consideration. The pnpm migration doesn't change that
calculus.

CI (all five workflows):

- `pnpm/action-setup@v4` added before each `actions/setup-node`
  step (the action picks up the version from `packageManager`).
  Order matters: setup-node's `cache: pnpm` input invokes
  `pnpm store path` and requires pnpm on PATH.
- `npm ci` → `pnpm install --frozen-lockfile`.
- `npm run X` → `pnpm run X`.
- `release.yml` uses `pnpm publish --no-git-checks` because
  pnpm's default publish-branch check fails when HEAD is
  detached: `release: types: [published]` puts
  actions/checkout in detached HEAD on the release tag, and
  pnpm then errors with ERR_PNPM_GIT_UNKNOWN_BRANCH ("Git HEAD
  may not [be] attached to any branch, but your
  'publish-branch' is set to 'master|main'"). Reproduced
  locally via `pnpm publish --dry-run` against a checked-out
  tag; not yet exercised against a real CI release, so this
  rationale is partly inferred.

Contributor docs (CONTRIBUTING.md, README.md Development
section, CLAUDE.md, .claude/rules/TESTS.md,
.claude/skills/release/SKILL.md):

- All `npm` invocations switched to `pnpm`.
- CONTRIBUTING.md and CLAUDE.md gain a short note explaining
  that `npm install` is rejected by `devEngines.packageManager`
  and that contributors should `corepack enable` to pick up
  the pinned pnpm version.
- Consumer-facing install instructions in README.md and
  docs/get-started/ deliberately stay as `npm install` — end
  users of the published package use whichever package
  manager they prefer.

[1] npm/cli#4828
[2] ably/realtime#8261
[3] https://github.com/pnpm/only-allow
[4] pnpm/pnpm#11307

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lawrence-forooghian added a commit to ably/ably-ai-transport-js that referenced this pull request May 28, 2026
Adopts pnpm for the lib and both demo apps, replacing npm. The
substantive motivation is supply-chain hardening: pnpm ships
configurable guardrails (publish cooldown, post-install-script
allowlist) and a strict node_modules layout that npm doesn't have.
Secondary motivation is fixing the cross-platform optional-deps
bug in npm's lockfile [1] that has been hitting demo app builds.

Based on the realtime PR [2] but diverges from it in three
deliberate ways:

- Strict node_modules layout (the pnpm default), rather than
  realtime's `nodeLinker: hoisted`. Realtime opted for hoisted
  because its codebase had phantom dependencies to clean up
  first; this project is younger and we'd rather catch any
  phantom imports immediately by relying on strict isolation
  from the start.
- `devEngines.packageManager` instead of the `only-allow`
  preinstall hook. `only-allow` was archived on 22/4/2026 [3];
  `devEngines.packageManager` is its successor and requires
  pnpm 11+. We're not constrained to pnpm 10 (realtime is,
  hence its choice).
- No mise. Realtime uses mise as a polyglot toolchain manager
  (Go + Node + Erlang + protoc) and as its task runner. This
  project is single-language and has no other tools to
  coordinate. Skip mise; rely on Corepack to dispatch the
  pinned pnpm version, and on the existing `engines.node` to
  constrain Node.

Lib root changes (package.json + pnpm-workspace.yaml):

- `packageManager: "pnpm@11.3.0"` for Corepack / CI dispatch.
- `devEngines.packageManager` with the same exact version pin,
  so the two fields don't conflict. pnpm flags any discrepancy
  between them with a warning; matching values suppresses it [4].
- `pnpm-workspace.yaml` carries supply-chain config: an
  `allowBuilds` allowlist (esbuild, unrs-resolver — derived
  empirically from ERR_PNPM_IGNORED_BUILDS), a 7-day publish
  cooldown via `minimumReleaseAge`, and an exclude list for
  Ably's own packages.
- `package-lock.json` replaced with `pnpm-lock.yaml`.
- The lib's `prepare`, `build` and `precommit` script chains
  now invoke `pnpm run` rather than `npm run` — required
  because the `prepare` hook runs at install time and
  `devEngines.packageManager` blocks npm from running inside
  the repo.

Demos (use-chat and use-client-session):

- Each demo's `package-lock.json` is replaced with
  `pnpm-lock.yaml`.
- Each demo gets its own `pnpm-workspace.yaml` mirroring the
  root (with `sharp` added to `allowBuilds` for Next.js image
  optimisation).
- Each demo's `package.json` gains its own `packageManager`
  and `devEngines.packageManager` fields. Without these,
  `npm install` in a demo directory would proceed silently
  (the root's devEngines is not visible to npm in a
  subdirectory), then fail later when the lib's prepare
  invokes pnpm. The duplication across lib + demos is kept
  in sync manually.
- `use-chat` demo's `prebuild` and `vercel.json`
  `installCommand` switch from `npm install` to
  `pnpm install`.
- Each demo's `allowBuilds` was derived the same way as the
  lib: running pnpm install in the demo and inspecting the
  ERR_PNPM_IGNORED_BUILDS output.

The decision to enforce pnpm in the demos as well as the lib is
deliberate. A softer position — letting npm drive demo installs
while still requiring pnpm for the lib build — would give npm
users non-reproducible installs (their `package-lock.json` would
drift from our committed `pnpm-lock.yaml`) and would bypass the
supply-chain hardening config (cooldown, allowBuilds) which is
pnpm-specific. Whether the demos are a thing users actually
clone and run or whether they're primarily internal references
is not entirely clear; for now we've optimised for contributor
experience consistency (one PM everywhere). A follow-up could
add a README to each demo documenting the pnpm commands.

Claude suggested a follow-up converting the demos to formal pnpm
workspace members. The motivation would be to eliminate the
`cd ../../../.. && pnpm install && pnpm run build` prebuild
dance, collapse the three lockfiles to one root lockfile, and
pull all the duplicated config (the per-demo
`pnpm-workspace.yaml`, the duplicated `packageManager` /
`devEngines.packageManager` in each demo `package.json`) up
into a single root declaration. Deferred: workspaces are an npm
feature too (since npm 7) and not pnpm-specific, so the fact
that this project hadn't already adopted them under npm
suggests the question of whether they're a good fit deserves
its own consideration. The pnpm migration doesn't change that
calculus.

CI (all five workflows):

- `pnpm/action-setup@v4` added before each `actions/setup-node`
  step (the action picks up the version from `packageManager`).
  Order matters: setup-node's `cache: pnpm` input invokes
  `pnpm store path` and requires pnpm on PATH.
- `npm ci` → `pnpm install --frozen-lockfile`.
- `npm run X` → `pnpm run X`.
- `release.yml` uses `pnpm publish --no-git-checks` because
  pnpm's default publish-branch check fails when HEAD is
  detached: `release: types: [published]` puts
  actions/checkout in detached HEAD on the release tag, and
  pnpm then errors with ERR_PNPM_GIT_UNKNOWN_BRANCH ("Git HEAD
  may not [be] attached to any branch, but your
  'publish-branch' is set to 'master|main'"). Reproduced
  locally via `pnpm publish --dry-run` against a checked-out
  tag; not yet exercised against a real CI release, so this
  rationale is partly inferred.

Contributor docs (CONTRIBUTING.md, README.md Development
section, CLAUDE.md, .claude/rules/TESTS.md,
.claude/skills/release/SKILL.md):

- All `npm` invocations switched to `pnpm`.
- CONTRIBUTING.md and CLAUDE.md gain a short note explaining
  that `npm install` is rejected by `devEngines.packageManager`
  and that contributors should `corepack enable` to pick up
  the pinned pnpm version.
- Consumer-facing install instructions in README.md and
  docs/get-started/ deliberately stay as `npm install` — end
  users of the published package use whichever package
  manager they prefer.

[1] npm/cli#4828
[2] ably/realtime#8261
[3] https://github.com/pnpm/only-allow
[4] pnpm/pnpm#11307

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lawrence-forooghian added a commit to ably/ably-ai-transport-js that referenced this pull request May 28, 2026
Adopts pnpm for the lib and both demo apps, replacing npm. The
substantive motivation is supply-chain hardening: pnpm ships
configurable guardrails (publish cooldown, post-install-script
allowlist) and a strict node_modules layout that npm doesn't have.
Secondary motivation is fixing the cross-platform optional-deps
bug in npm's lockfile [1] that has been hitting demo app builds.

Based on the realtime PR [2] but diverges from it in three
deliberate ways:

- Strict node_modules layout (the pnpm default), rather than
  realtime's `nodeLinker: hoisted`. Realtime opted for hoisted
  because its codebase had phantom dependencies to clean up
  first; this project is younger and we'd rather catch any
  phantom imports immediately by relying on strict isolation
  from the start.
- `devEngines.packageManager` instead of the `only-allow`
  preinstall hook. `only-allow` was archived on 22/4/2026 [3];
  `devEngines.packageManager` is its successor and requires
  pnpm 11+. We're not constrained to pnpm 10 (realtime is,
  hence its choice).
- No mise. Realtime uses mise as a polyglot toolchain manager
  (Go + Node + Erlang + protoc) and as its task runner. This
  project is single-language and has no other tools to
  coordinate. Skip mise; rely on Corepack to dispatch the
  pinned pnpm version, and on the existing `engines.node` to
  constrain Node.

Lib root changes (package.json + pnpm-workspace.yaml):

- `packageManager: "pnpm@11.3.0"` for Corepack / CI dispatch.
- `devEngines.packageManager` with the same exact version pin,
  so the two fields don't conflict. pnpm flags any discrepancy
  between them with a warning; matching values suppresses it [4].
- `pnpm-workspace.yaml` carries supply-chain config: an
  `allowBuilds` allowlist (esbuild, unrs-resolver — derived
  empirically from ERR_PNPM_IGNORED_BUILDS), a 7-day publish
  cooldown via `minimumReleaseAge`, and an exclude list for
  Ably's own packages.
- `package-lock.json` replaced with `pnpm-lock.yaml`.
- The lib's `prepare`, `build` and `precommit` script chains
  now invoke `pnpm run` rather than `npm run` — required
  because the `prepare` hook runs at install time and
  `devEngines.packageManager` blocks npm from running inside
  the repo.

Demos (use-chat and use-client-session):

- Each demo's `package-lock.json` is replaced with
  `pnpm-lock.yaml`.
- Each demo gets its own `pnpm-workspace.yaml` mirroring the
  root (with `sharp` added to `allowBuilds` for Next.js image
  optimisation).
- Each demo's `package.json` gains its own `packageManager`
  and `devEngines.packageManager` fields. Without these,
  `npm install` in a demo directory would proceed silently
  (the root's devEngines is not visible to npm in a
  subdirectory), then fail later when the lib's prepare
  invokes pnpm. The duplication across lib + demos is kept
  in sync manually.
- `use-chat` demo's `prebuild` and `vercel.json`
  `installCommand` switch from `npm install` to
  `pnpm install`.
- Each demo's `allowBuilds` was derived the same way as the
  lib: running pnpm install in the demo and inspecting the
  ERR_PNPM_IGNORED_BUILDS output.

The decision to enforce pnpm in the demos as well as the lib is
deliberate. A softer position — letting npm drive demo installs
while still requiring pnpm for the lib build — would give npm
users non-reproducible installs (their `package-lock.json` would
drift from our committed `pnpm-lock.yaml`) and would bypass the
supply-chain hardening config (cooldown, allowBuilds) which is
pnpm-specific. Whether the demos are a thing users actually
clone and run or whether they're primarily internal references
is not entirely clear; for now we've optimised for contributor
experience consistency (one PM everywhere). A follow-up could
add a README to each demo documenting the pnpm commands.

Claude suggested a follow-up converting the demos to formal pnpm
workspace members. The motivation would be to eliminate the
`cd ../../../.. && pnpm install && pnpm run build` prebuild
dance, collapse the three lockfiles to one root lockfile, and
pull all the duplicated config (the per-demo
`pnpm-workspace.yaml`, the duplicated `packageManager` /
`devEngines.packageManager` in each demo `package.json`) up
into a single root declaration. Deferred: workspaces are an npm
feature too (since npm 7) and not pnpm-specific, so the fact
that this project hadn't already adopted them under npm
suggests the question of whether they're a good fit deserves
its own consideration. The pnpm migration doesn't change that
calculus.

CI (all five workflows):

- `pnpm/action-setup@v4` added before each `actions/setup-node`
  step (the action picks up the version from `packageManager`).
  Order matters: setup-node's `cache: pnpm` input invokes
  `pnpm store path` and requires pnpm on PATH.
- `npm ci` → `pnpm install --frozen-lockfile`.
- `npm run X` → `pnpm run X`.
- `release.yml` uses `pnpm publish --no-git-checks` because
  pnpm's default publish-branch check fails when HEAD is
  detached: `release: types: [published]` puts
  actions/checkout in detached HEAD on the release tag, and
  pnpm then errors with ERR_PNPM_GIT_UNKNOWN_BRANCH ("Git HEAD
  may not [be] attached to any branch, but your
  'publish-branch' is set to 'master|main'"). Reproduced
  locally via `pnpm publish --dry-run` against a checked-out
  tag; not yet exercised against a real CI release, so this
  rationale is partly inferred.

Contributor docs (CONTRIBUTING.md, README.md Development
section, CLAUDE.md, .claude/rules/TESTS.md,
.claude/skills/release/SKILL.md):

- All `npm` invocations switched to `pnpm`.
- CONTRIBUTING.md and CLAUDE.md gain a short note explaining
  that `npm install` is rejected by `devEngines.packageManager`
  and that contributors should `corepack enable` to pick up
  the pinned pnpm version.
- Consumer-facing install instructions in README.md and
  docs/get-started/ deliberately stay as `npm install` — end
  users of the published package use whichever package
  manager they prefer.

[1] npm/cli#4828
[2] ably/realtime#8261
[3] https://github.com/pnpm/only-allow
[4] pnpm/pnpm#11307

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zkochan added a commit that referenced this pull request Jun 16, 2026
…kageManager match (#12287)

When `package.json` sets both `packageManager` and `devEngines.packageManager` to the same pnpm version with the same integrity hash pnpm prints a spurious warning on every command. For example, a `package.json` file that looks like:

```json
{
	"packageManager": "pnpm@11.5.1+sha512.93f7b57422ea7068257235b4c16eb60762eb68e1dc23723199cc739043ea9be2c4143274a399d8c6defa2b1176226d9ca1c4b63482d6200c1a8fbaa78c1d1485",
	"devEngines": {
		"packageManager": {
			"name": "pnpm",
			"version": "11.5.1+sha512.93f7b57422ea7068257235b4c16eb60762eb68e1dc23723199cc739043ea9be2c4143274a399d8c6defa2b1176226d9ca1c4b63482d6200c1a8fbaa78c1d1485",
			"onFail": "ignore"
		},
		"runtime": [
			{
				"name": "node",
				"version": "26.3.0",
				"onFail": "ignore"
			}
		]
	}
}
```

Issues a warning on every `pnpm` command:

> Cannot use both "packageManager" and "devEngines.packageManager" in package.json. "packageManager" will be ignored.

## Root cause

`getWantedPackageManager` compares the two fields to decide whether to warn, but the two sides were normalized differently:

  - `parsePackageManager` **strips the integrity hash** from the legacy `packageManager` field → `11.5.1`
  - the `devEngines.packageManager` version was compared **with its hash intact** → `11.5.1+sha512.93f7b57…`

So, `"11.5.1" !== "11.5.1+sha512…"` was always true and the warning fired, even for identical specs. An earlier fix in #11307 only suppressed the warning when *neither* side carried a hash.

## Fix

`parsePackageManager` now also returns the hash (via a shared `splitPackageManagerVersion`), and `getPackageManagerConflictWarning` compares the fields structurally. The warning is suppressed **only when the two specifiers are identical** (name + version + hash, both-absent counts as equal):

| name | version | hash | result |
|------|---------|------|--------|
| same | same | both absent, or both present & equal | ✅ no warning |
| same | same | present on **one side only** | ⚠️ generic "Cannot use both…" |
| same | same | both present & **differ** | ⚠️ "…contradictory integrity hashes" |
| same | **differ** | — | ⚠️ "…different versions of pnpm" |
| **differ** | — | — | ⚠️ "…different package managers" |

A hash on only one side is still a divergence — dropping the ignored `packageManager` field would lose that hash — so it warns with the original generic message. Two contradictory hashes for one version (a likely wrong-hash mistake) get a dedicated message. The generic single message is otherwise replaced by one tailored to each conflict, each ending with `"packageManager" will be ignored`.

Closes #12028.

---------

Signed-off-by: C. Spencer Beggs <spencer@beggs.codes>
Signed-off-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Zoltan Kochan <z@kochan.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Better migration path for devEngines.packageManager

2 participants