From 9c65b96f2ce92783cef1fc78e5e1ea12de38208c Mon Sep 17 00:00:00 2001 From: Ryo Matsukawa <76232929+ryo-manba@users.noreply.github.com> Date: Mon, 13 Oct 2025 17:00:56 +0900 Subject: [PATCH] fix: preserve version and hasBin for variations packages (#10065) close #10022 --- .changeset/pink-numbers-behave.md | 6 + .../src/resolveDependencies.ts | 20 ++- .../test/getManifestFromResponse.test.ts | 151 ++++++++++++++++++ 3 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 .changeset/pink-numbers-behave.md create mode 100644 pkg-manager/resolve-dependencies/test/getManifestFromResponse.test.ts diff --git a/.changeset/pink-numbers-behave.md b/.changeset/pink-numbers-behave.md new file mode 100644 index 0000000000..54ea8376c2 --- /dev/null +++ b/.changeset/pink-numbers-behave.md @@ -0,0 +1,6 @@ +--- +"@pnpm/resolve-dependencies": patch +pnpm: patch +--- + +Preserve version and hasBin for variations packages [#10022](https://github.com/pnpm/pnpm/issues/10022). diff --git a/pkg-manager/resolve-dependencies/src/resolveDependencies.ts b/pkg-manager/resolve-dependencies/src/resolveDependencies.ts index 9bb6e74f42..dc1f9c9bf1 100644 --- a/pkg-manager/resolve-dependencies/src/resolveDependencies.ts +++ b/pkg-manager/resolve-dependencies/src/resolveDependencies.ts @@ -60,6 +60,8 @@ import { wantedDepIsLocallyAvailable } from './wantedDepIsLocallyAvailable.js' import { type CatalogLookupMetadata } from './resolveDependencyTree.js' import { replaceVersionInBareSpecifier } from './replaceVersionInBareSpecifier.js' +export type { WantedDependency } + const dependencyResolvedLogger = logger('_dependency_resolved') const omitDepsFields = omit(['dependencies', 'optionalDependencies', 'peerDependencies', 'peerDependenciesMeta']) @@ -1422,7 +1424,7 @@ async function resolveDependency ( let prepare!: boolean let hasBin!: boolean - let pkg: PackageManifest = getManifestFromResponse(pkgResponse, wantedDependency) + let pkg: PackageManifest = getManifestFromResponse(pkgResponse, wantedDependency, currentPkg) if (!pkg.dependencies) { pkg.dependencies = {} } @@ -1506,7 +1508,9 @@ async function resolveDependency ( ) { pkg.deprecated = currentPkg.dependencyLockfile.deprecated } - hasBin = Boolean((pkg.bin && !(pkg.bin === '' || Object.keys(pkg.bin).length === 0)) ?? pkg.directories?.bin) + hasBin = (currentPkg.dependencyLockfile?.hasBin != null && !pkg.bin) + ? currentPkg.dependencyLockfile.hasBin + : Boolean((pkg.bin && !(pkg.bin === '' || Object.keys(pkg.bin).length === 0)) ?? pkg.directories?.bin) } if (options.currentDepth === 0 && pkgResponse.body.latest && pkgResponse.body.latest !== pkg.version) { ctx.outdatedDependencies[pkgResponse.body.id] = pkgResponse.body.latest @@ -1649,11 +1653,19 @@ async function resolveDependency ( } } -function getManifestFromResponse ( +export function getManifestFromResponse ( pkgResponse: PackageResponse, - wantedDependency: WantedDependency + wantedDependency: WantedDependency, + currentPkg?: Partial ): PackageManifest { if (pkgResponse.body.manifest) return pkgResponse.body.manifest + + if (currentPkg?.name && currentPkg?.version) { + return { + name: currentPkg.name, + version: currentPkg.version, + } + } return { name: wantedDependency.alias ? wantedDependency.alias : wantedDependency.bareSpecifier.split('/').pop()!, version: '0.0.0', diff --git a/pkg-manager/resolve-dependencies/test/getManifestFromResponse.test.ts b/pkg-manager/resolve-dependencies/test/getManifestFromResponse.test.ts new file mode 100644 index 0000000000..6ffb643a94 --- /dev/null +++ b/pkg-manager/resolve-dependencies/test/getManifestFromResponse.test.ts @@ -0,0 +1,151 @@ +import { getManifestFromResponse, type WantedDependency } from '../lib/resolveDependencies.js' +import type { PackageResponse } from '@pnpm/store-controller-types' + +test('getManifestFromResponse returns manifest from pkgResponse when available', () => { + const pkgResponse = { + body: { + manifest: { + name: 'foo', + version: '1.0.0', + }, + }, + } as PackageResponse + + const wantedDependency = { + alias: 'foo', + bareSpecifier: 'foo', + dev: false, + optional: false, + } as WantedDependency + + const result = getManifestFromResponse(pkgResponse, wantedDependency) + + expect(result).toEqual({ + name: 'foo', + version: '1.0.0', + }) +}) + +test('getManifestFromResponse returns currentPkg info when manifest is undefined', () => { + const pkgResponse = { + body: { + manifest: undefined, + }, + } as PackageResponse + + const wantedDependency = { + alias: 'node', + bareSpecifier: 'runtime:^22.0.0', + dev: false, + optional: false, + } as WantedDependency + + const currentPkg = { + name: 'node', + version: '22.20.0', + } + + const result = getManifestFromResponse(pkgResponse, wantedDependency, currentPkg) + + expect(result).toEqual({ + name: 'node', + version: '22.20.0', + }) +}) + +test('getManifestFromResponse returns default 0.0.0 when manifest and currentPkg are unavailable', () => { + const pkgResponse = { + body: { + manifest: undefined, + }, + } as PackageResponse + + const wantedDependency = { + alias: 'foo', + bareSpecifier: 'foo@^1.0.0', + dev: false, + optional: false, + } as WantedDependency + + const result = getManifestFromResponse(pkgResponse, wantedDependency) + + expect(result).toEqual({ + name: 'foo', + version: '0.0.0', + }) +}) + +test('getManifestFromResponse extracts name from bareSpecifier when no alias', () => { + const pkgResponse = { + body: { + manifest: undefined, + }, + } as PackageResponse + + const wantedDependency = { + bareSpecifier: '@scope/package@^1.0.0', + dev: false, + optional: false, + } as WantedDependency + + const result = getManifestFromResponse(pkgResponse, wantedDependency) + + expect(result).toEqual({ + name: 'package@^1.0.0', + version: '0.0.0', + }) +}) + +test('getManifestFromResponse does not use currentPkg when only name is available', () => { + const pkgResponse = { + body: { + manifest: undefined, + }, + } as PackageResponse + + const wantedDependency = { + alias: 'foo', + bareSpecifier: 'foo', + dev: false, + optional: false, + } as WantedDependency + + const currentPkg = { + name: 'foo', + version: undefined, + } + + const result = getManifestFromResponse(pkgResponse, wantedDependency, currentPkg) + + expect(result).toEqual({ + name: 'foo', + version: '0.0.0', + }) +}) + +test('getManifestFromResponse does not use currentPkg when only version is available', () => { + const pkgResponse = { + body: { + manifest: undefined, + }, + } as PackageResponse + + const wantedDependency = { + alias: 'foo', + bareSpecifier: 'foo', + dev: false, + optional: false, + } as WantedDependency + + const currentPkg = { + name: undefined, + version: '1.0.0', + } + + const result = getManifestFromResponse(pkgResponse, wantedDependency, currentPkg) + + expect(result).toEqual({ + name: 'foo', + version: '0.0.0', + }) +})