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, isWorkspacesEnabled,
modifyYarnRcToFitNewDirectory, modifyYarnRcToFitNewDirectory,
modifyYarnRcYmlToFitNewDirectory, modifyYarnRcYmlToFitNewDirectory,
parseVersionFromPackageManagerField,
PackageManager, PackageManager,
} from './package-manager'; } 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'; } from 'yaml';
import { rm } from 'node:fs/promises'; import { rm } from 'node:fs/promises';
import { dirname, join, relative } from 'path'; import { dirname, join, relative } from 'path';
import { gte, lt } from 'semver'; import { gte, lt, parse } from 'semver';
import { dirSync } from 'tmp'; import { dirSync } from 'tmp';
import { promisify } from 'util'; import { promisify } from 'util';
@ -213,29 +213,24 @@ export function getPackageManagerVersion(
packageManager: PackageManager = detectPackageManager(), packageManager: PackageManager = detectPackageManager(),
cwd = process.cwd() cwd = process.cwd()
): string { ): string {
let version; let version: string;
try { if (existsSync(join(cwd, 'package.json'))) {
version = execSync(`${packageManager} --version`, { const packageManagerEntry = readJsonFile<PackageJson>(
cwd, join(cwd, 'package.json')
encoding: 'utf-8', )?.packageManager;
windowsHide: true, version = parseVersionFromPackageManagerField(
}).trim(); packageManager,
} catch { packageManagerEntry
if (existsSync(join(cwd, 'package.json'))) { );
const packageVersion = readJsonFile<PackageJson>( }
join(cwd, 'package.json') if (!version) {
)?.packageManager; try {
if (packageVersion) { version = execSync(`${packageManager} --version`, {
const [packageManagerFromPackageJson, versionFromPackageJson] = cwd,
packageVersion.split('@'); encoding: 'utf-8',
if ( windowsHide: true,
packageManagerFromPackageJson === packageManager && }).trim();
versionFromPackageJson } catch {}
) {
version = versionFromPackageJson;
}
}
}
} }
if (!version) { if (!version) {
throw new Error(`Cannot determine the version of ${packageManager}.`); throw new Error(`Cannot determine the version of ${packageManager}.`);
@ -243,6 +238,29 @@ export function getPackageManagerVersion(
return version; 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 * 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. * hitting a package.json file, as this is how npm finds them as well.