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.