diff --git a/common/changes/@microsoft/rush/fix-5602_2026-02-07-17-45.json b/common/changes/@microsoft/rush/fix-5602_2026-02-07-17-45.json new file mode 100644 index 00000000000..a4b0094ff7f --- /dev/null +++ b/common/changes/@microsoft/rush/fix-5602_2026-02-07-17-45.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Fix allPreferredVersions and allowedAlternativeVersions missing in subspace pnpmfileSettings.json", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/libraries/rush-lib/src/logic/pnpm/IPnpmfile.ts b/libraries/rush-lib/src/logic/pnpm/IPnpmfile.ts index 50e9ddadd25..5a2ea3f48d1 100644 --- a/libraries/rush-lib/src/logic/pnpm/IPnpmfile.ts +++ b/libraries/rush-lib/src/logic/pnpm/IPnpmfile.ts @@ -32,8 +32,7 @@ export interface IWorkspaceProjectInfo * The `settings` parameter passed to {@link IPnpmfileShim.hooks.readPackage} and * {@link IPnpmfileShim.hooks.afterAllResolved}. */ -export interface ISubspacePnpmfileShimSettings { - semverPath: string; +export interface ISubspacePnpmfileShimSettings extends Omit { workspaceProjects: Record; subspaceProjects: Record; userPnpmfilePath?: string; diff --git a/libraries/rush-lib/src/logic/pnpm/SubspacePnpmfileConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/SubspacePnpmfileConfiguration.ts index 6493ea0fa66..fc1cbb3960e 100644 --- a/libraries/rush-lib/src/logic/pnpm/SubspacePnpmfileConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/SubspacePnpmfileConfiguration.ts @@ -3,13 +3,22 @@ import * as path from 'node:path'; -import { FileSystem, Import, JsonFile, type IDependenciesMetaTable } from '@rushstack/node-core-library'; +import * as semver from 'semver'; + +import { + FileSystem, + Import, + JsonFile, + MapExtensions, + type IDependenciesMetaTable +} from '@rushstack/node-core-library'; import { subspacePnpmfileShimFilename, scriptsFolderPath } from '../../utilities/PathConstants'; import type { ISubspacePnpmfileShimSettings, IWorkspaceProjectInfo } from './IPnpmfile'; import type { RushConfiguration } from '../../api/RushConfiguration'; import type { RushConfigurationProject } from '../../api/RushConfigurationProject'; import type { PnpmPackageManager } from '../../api/packageManager/PnpmPackageManager'; +import type { CommonVersionsConfiguration } from '../../api/CommonVersionsConfiguration'; import { RushConstants } from '../RushConstants'; import type { Subspace } from '../../api/Subspace'; import type { PnpmOptionsConfiguration } from './PnpmOptionsConfiguration'; @@ -80,9 +89,35 @@ export class SubspacePnpmfileConfiguration { (subspace.contains(project) ? subspaceProjects : workspaceProjects)[packageName] = workspaceProjectInfo; } + let allPreferredVersions: { [dependencyName: string]: string } = {}; + let allowedAlternativeVersions: { [dependencyName: string]: readonly string[] } = {}; + + // Populate preferred versions from subspace's common-versions.json (same as non-subspace pnpmfile shim) + const pnpmOptions: PnpmOptionsConfiguration = + rushConfiguration.packageManagerOptions as PnpmOptionsConfiguration; + if (pnpmOptions?.useWorkspaces) { + const commonVersionsConfiguration: CommonVersionsConfiguration = subspace.getCommonVersions(variant); + const preferredVersions: Map = new Map(); + MapExtensions.mergeFromMap( + preferredVersions, + rushConfiguration.getImplicitlyPreferredVersions(subspace, variant) + ); + for (const [name, version] of commonVersionsConfiguration.getAllPreferredVersions()) { + if (!preferredVersions.has(name) || semver.subset(version, preferredVersions.get(name)!)) { + preferredVersions.set(name, version); + } + } + allPreferredVersions = MapExtensions.toObject(preferredVersions); + allowedAlternativeVersions = MapExtensions.toObject( + commonVersionsConfiguration.allowedAlternativeVersions + ); + } + const settings: ISubspacePnpmfileShimSettings = { workspaceProjects, subspaceProjects, + allPreferredVersions, + allowedAlternativeVersions, semverPath: Import.resolveModule({ modulePath: 'semver', baseFolderPath: __dirname }) }; diff --git a/libraries/rush-lib/src/logic/pnpm/test/SubspacePnpmfileConfiguration.test.ts b/libraries/rush-lib/src/logic/pnpm/test/SubspacePnpmfileConfiguration.test.ts new file mode 100644 index 00000000000..609b20b1eb4 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/SubspacePnpmfileConfiguration.test.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { RushConfiguration } from '../../../api/RushConfiguration'; +import { SubspacePnpmfileConfiguration } from '../SubspacePnpmfileConfiguration'; +import { JsonFile, type JsonObject } from '@rushstack/node-core-library'; + +describe(SubspacePnpmfileConfiguration.name, () => { + const repoPath: string = `${__dirname}/repo-with-subspace`; + const rushFilename: string = `${repoPath}/rush.json`; + const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile(rushFilename); + const shimPath: string = `${rushConfiguration.defaultSubspace.getSubspaceTempFolderPath()}/pnpmfileSettings.json`; + + beforeAll(async () => { + const subspace = rushConfiguration.defaultSubspace; + await SubspacePnpmfileConfiguration.writeCommonTempSubspaceGlobalPnpmfileAsync( + rushConfiguration, + subspace, + undefined + ); + }); + + it('should use the smallest-available SemVer range (preferredVersions)', async () => { + const shimJson: JsonObject = await JsonFile.loadAsync(shimPath); + expect(shimJson.allPreferredVersions).toHaveProperty('@rushstack/terminal', '0.19.2'); + }); + + it('should record allPreferredVersions in pnpmfileSettings.json', async () => { + const shimJson: JsonObject = await JsonFile.loadAsync(shimPath); + expect(shimJson.allPreferredVersions).toHaveProperty('@rushstack/terminal', '0.19.2'); + }); + + it('should record allowedAlternativeVersions in pnpmfileSettings.json', async () => { + const shimJson: JsonObject = await JsonFile.loadAsync(shimPath); + const allowedAlternativeVersions = shimJson.allowedAlternativeVersions as + | Record + | undefined; + expect(allowedAlternativeVersions).toBeDefined(); + expect(allowedAlternativeVersions).toHaveProperty('foo', ['1.0.0']); + }); +}); diff --git a/libraries/rush-lib/src/logic/pnpm/test/repo-with-subspace/a/package.json b/libraries/rush-lib/src/logic/pnpm/test/repo-with-subspace/a/package.json new file mode 100644 index 00000000000..3d1559549c9 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/repo-with-subspace/a/package.json @@ -0,0 +1,8 @@ +{ + "name": "a", + "version": "1.0.0", + "description": "Test package a to test subspace pnpmfile shim with preferred versions", + "dependencies": { + "@rushstack/terminal": "~0.19.0" + } +} diff --git a/libraries/rush-lib/src/logic/pnpm/test/repo-with-subspace/common/config/rush/pnpm-config.json b/libraries/rush-lib/src/logic/pnpm/test/repo-with-subspace/common/config/rush/pnpm-config.json new file mode 100644 index 00000000000..e783afd001e --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/repo-with-subspace/common/config/rush/pnpm-config.json @@ -0,0 +1,3 @@ +{ + "useWorkspaces": true +} diff --git a/libraries/rush-lib/src/logic/pnpm/test/repo-with-subspace/common/config/rush/subspaces.json b/libraries/rush-lib/src/logic/pnpm/test/repo-with-subspace/common/config/rush/subspaces.json new file mode 100644 index 00000000000..9abc6b36fa8 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/repo-with-subspace/common/config/rush/subspaces.json @@ -0,0 +1,10 @@ +/** + * This configuration file manages the experimental "subspaces" feature for Rush, + * which allows multiple PNPM lockfiles to be used in a single Rush workspace. + * For full documentation, please see https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/subspaces.schema.json", + "subspacesEnabled": true, + "subspaceNames": ["default"] +} diff --git a/libraries/rush-lib/src/logic/pnpm/test/repo-with-subspace/common/config/subspaces/default/.npmrc b/libraries/rush-lib/src/logic/pnpm/test/repo-with-subspace/common/config/subspaces/default/.npmrc new file mode 100644 index 00000000000..ef0ca82a609 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/repo-with-subspace/common/config/subspaces/default/.npmrc @@ -0,0 +1,2 @@ +registry=https://registry.npmjs.org/ +always-auth=false diff --git a/libraries/rush-lib/src/logic/pnpm/test/repo-with-subspace/common/config/subspaces/default/.pnpmfile.cjs b/libraries/rush-lib/src/logic/pnpm/test/repo-with-subspace/common/config/subspaces/default/.pnpmfile.cjs new file mode 100644 index 00000000000..90860f3b564 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/repo-with-subspace/common/config/subspaces/default/.pnpmfile.cjs @@ -0,0 +1,23 @@ +'use strict'; + +/** + * When using the PNPM package manager, you can use pnpmfile.js to workaround + * dependencies that have mistakes in their package.json file. (This feature is + * functionally similar to Yarn's "resolutions".) + * + * For details, see the PNPM documentation: + * https://pnpm.io/pnpmfile#hooks + * + * IMPORTANT: SINCE THIS FILE CONTAINS EXECUTABLE CODE, MODIFYING IT IS LIKELY TO INVALIDATE + * ANY CACHED DEPENDENCY ANALYSIS. After any modification to pnpmfile.js, it's recommended to run + * "rush update --full" so that PNPM will recalculate all version selections. + */ +module.exports = { + hooks: { + readPackage + } +}; + +function readPackage(packageJson, _context) { + return packageJson; +} diff --git a/libraries/rush-lib/src/logic/pnpm/test/repo-with-subspace/common/config/subspaces/default/common-versions.json b/libraries/rush-lib/src/logic/pnpm/test/repo-with-subspace/common/config/subspaces/default/common-versions.json new file mode 100644 index 00000000000..b2a8b4e2fc7 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/repo-with-subspace/common/config/subspaces/default/common-versions.json @@ -0,0 +1,28 @@ +/** + * This configuration file specifies NPM dependency version selections that affect all projects + * in a Rush repo. More documentation is available on the Rush website: https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/common-versions.schema.json", + + /** + * A table that specifies a "preferred version" for a given NPM package. This feature is typically used + * to hold back an indirect dependency to a specific older version, or to reduce duplication of indirect dependencies. + * + * The "preferredVersions" value can be any SemVer range specifier (e.g. "~1.2.3"). Rush injects these values into + * the "dependencies" field of the top-level common/temp/package.json, which influences how the package manager + * will calculate versions. The specific effect depends on your package manager. Generally it will have no + * effect on an incompatible or already constrained SemVer range. If you are using PNPM, similar effects can be + * achieved using the pnpmfile.js hook. See the Rush documentation for more details. + * + * After modifying this field, it's recommended to run "rush update --full" so that the package manager + * will recalculate all version selections. + */ + "preferredVersions": { + "@rushstack/terminal": "0.19.2" + }, + "ensureConsistentVersions": false, + "allowedAlternativeVersions": { + "foo": ["1.0.0"] + } +} diff --git a/libraries/rush-lib/src/logic/pnpm/test/repo-with-subspace/rush.json b/libraries/rush-lib/src/logic/pnpm/test/repo-with-subspace/rush.json new file mode 100644 index 00000000000..f99820418f1 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/repo-with-subspace/rush.json @@ -0,0 +1,18 @@ +/** + * This is the main configuration file for Rush. + * For full documentation, please see https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush.schema.json", + "rushVersion": "5.166.0", + "pnpmVersion": "10.27.0", + "nodeSupportedVersionRange": "*", + // "projectFolderMinDepth": 1, + // "projectFolderMaxDepth": 2, + "projects": [ + { + "packageName": "a", + "projectFolder": "a" + } + ] +}