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 <jack.hsu@gmail.com>
This commit is contained in:
Jacob Ley 2025-04-30 15:59:24 -05:00 committed by GitHub
parent d89b7743c6
commit 6610f3d632
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 68 additions and 24 deletions

View File

@ -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 <major>.<minor>.<patch> 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');
});
});
});

View File

@ -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<PackageJson>(
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<PackageJson>(
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 "<major>.<minor>.<patch>". 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.