From 6610f3d632f41c001e7d302cf69f6e9a18697f13 Mon Sep 17 00:00:00 2001 From: Jacob Ley <37151850+JacobLey@users.noreply.github.com> Date: Wed, 30 Apr 2025 15:59:24 -0500 Subject: [PATCH] fix(core)!: respect packageManager field in package.json when detecting version (#29249) Attept to read package manager version from config before invoking package manager CLI BREAK CHANGE: If you have a mismatch between the `packageManager` field in `package.json` and the actual version installed in the environment, it may lead to unexpected behavior when installing. This should not be a problem if you are using corepack already. ## Related Issue(s) https://github.com/nrwl/nx/issues/29244 --------- Co-authored-by: Jack Hsu --- packages/nx/src/utils/package-manager.spec.ts | 26 ++++++++ packages/nx/src/utils/package-manager.ts | 66 ++++++++++++------- 2 files changed, 68 insertions(+), 24 deletions(-) diff --git a/packages/nx/src/utils/package-manager.spec.ts b/packages/nx/src/utils/package-manager.spec.ts index 7a98017186..b7d19a3c02 100644 --- a/packages/nx/src/utils/package-manager.spec.ts +++ b/packages/nx/src/utils/package-manager.spec.ts @@ -15,6 +15,7 @@ import { isWorkspacesEnabled, modifyYarnRcToFitNewDirectory, modifyYarnRcYmlToFitNewDirectory, + parseVersionFromPackageManagerField, PackageManager, } from './package-manager'; @@ -521,4 +522,29 @@ describe('package-manager', () => { }); }); }); + + describe('parseVersionFromPackageManagerField', () => { + it('should return null for invalid semver', () => { + expect(parseVersionFromPackageManagerField('yarn', 'bad')).toEqual(null); + expect(parseVersionFromPackageManagerField('yarn', '2.1')).toEqual(null); + expect( + parseVersionFromPackageManagerField( + 'yarn', + 'https://registry.npmjs.org/@yarnpkg/cli-dist/-/cli-dist-3.2.3.tgz#sha224.16a0797d1710d1fb7ec40ab5c3801b68370a612a9b66ba117ad9924b' + ) + ).toEqual(null); + }); + + it('should .. version', () => { + expect(parseVersionFromPackageManagerField('yarn', 'yarn@3.2.3')).toEqual( + '3.2.3' + ); + expect( + parseVersionFromPackageManagerField( + 'yarn', + 'yarn@3.2.3+sha224.953c8233f7a92884eee2de69a1b92d1f2ec1655e66d08071ba9a02fa' + ) + ).toEqual('3.2.3'); + }); + }); }); diff --git a/packages/nx/src/utils/package-manager.ts b/packages/nx/src/utils/package-manager.ts index 4b251a834a..3ed9f5f676 100644 --- a/packages/nx/src/utils/package-manager.ts +++ b/packages/nx/src/utils/package-manager.ts @@ -11,7 +11,7 @@ import { } from 'yaml'; import { rm } from 'node:fs/promises'; import { dirname, join, relative } from 'path'; -import { gte, lt } from 'semver'; +import { gte, lt, parse } from 'semver'; import { dirSync } from 'tmp'; import { promisify } from 'util'; @@ -213,29 +213,24 @@ export function getPackageManagerVersion( packageManager: PackageManager = detectPackageManager(), cwd = process.cwd() ): string { - let version; - try { - version = execSync(`${packageManager} --version`, { - cwd, - encoding: 'utf-8', - windowsHide: true, - }).trim(); - } catch { - if (existsSync(join(cwd, 'package.json'))) { - const packageVersion = readJsonFile( - join(cwd, 'package.json') - )?.packageManager; - if (packageVersion) { - const [packageManagerFromPackageJson, versionFromPackageJson] = - packageVersion.split('@'); - if ( - packageManagerFromPackageJson === packageManager && - versionFromPackageJson - ) { - version = versionFromPackageJson; - } - } - } + let version: string; + if (existsSync(join(cwd, 'package.json'))) { + const packageManagerEntry = readJsonFile( + join(cwd, 'package.json') + )?.packageManager; + version = parseVersionFromPackageManagerField( + packageManager, + packageManagerEntry + ); + } + if (!version) { + try { + version = execSync(`${packageManager} --version`, { + cwd, + encoding: 'utf-8', + windowsHide: true, + }).trim(); + } catch {} } if (!version) { throw new Error(`Cannot determine the version of ${packageManager}.`); @@ -243,6 +238,29 @@ export function getPackageManagerVersion( return version; } +export function parseVersionFromPackageManagerField( + requestedPackageManager: string, + packageManagerFieldValue: string | undefined +): null | string { + if (!packageManagerFieldValue) return null; + const [packageManagerFromPackageJson, versionFromPackageJson] = + packageManagerFieldValue.split('@'); + if ( + versionFromPackageJson && + // If it's a URL, it's not a valid range by default, unless users set `COREPACK_ENABLE_UNSAFE_CUSTOM_URLS=1`. + // In the unsafe case, there's no way to reliably pare out the version since it could be anything, e.g. http://mydomain.com/bin/yarn.js. + // See: https://github.com/nodejs/corepack/blob/2b43f26/sources/corepackUtils.ts#L110-L112 + !URL.canParse(versionFromPackageJson) && + packageManagerFromPackageJson === requestedPackageManager && + versionFromPackageJson + ) { + // The range could have a validation hash attached, like "3.2.3+sha224.953c8233f7a92884eee2de69a1b92d1f2ec1655e66d08071ba9a02fa". + // We just want to parse out the "..". Semver treats "+" as a build, which is not included in the resulting version. + return parse(versionFromPackageJson)?.version ?? null; + } + return null; +} + /** * Checks for a project level npmrc file by crawling up the file tree until * hitting a package.json file, as this is how npm finds them as well.