Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions common/changes/@microsoft/rush/fix-5602_2026-02-07-17-45.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Fix allPreferredVersions and allowedAlternativeVersions missing in subspace pnpmfileSettings.json",
"type": "none"
}
],
"packageName": "@microsoft/rush"
}
3 changes: 1 addition & 2 deletions libraries/rush-lib/src/logic/pnpm/IPnpmfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IPnpmfileShimSettings, 'workspaceVersions'> {
workspaceProjects: Record<string, IWorkspaceProjectInfo>;
subspaceProjects: Record<string, IWorkspaceProjectInfo>;
userPnpmfilePath?: string;
Expand Down
74 changes: 8 additions & 66 deletions libraries/rush-lib/src/logic/pnpm/PnpmfileConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,15 @@

import * as path from 'node:path';

import * as semver from 'semver';

import { FileSystem, Import, type IPackageJson, JsonFile, MapExtensions } from '@rushstack/node-core-library';
import { FileSystem, type IPackageJson } from '@rushstack/node-core-library';

import type { PnpmPackageManager } from '../../api/packageManager/PnpmPackageManager';
import type { RushConfiguration } from '../../api/RushConfiguration';
import type { CommonVersionsConfiguration } from '../../api/CommonVersionsConfiguration';
import type { PnpmOptionsConfiguration } from './PnpmOptionsConfiguration';
import * as pnpmfile from './PnpmfileShim';
import { pnpmfileShimFilename, scriptsFolderPath } from '../../utilities/PathConstants';
import type { IPnpmfileContext, IPnpmfileShimSettings } from './IPnpmfile';
import type { IPnpmfileContext } from './IPnpmfile';
import type { Subspace } from '../../api/Subspace';
import { PnpmfileSettingsFile } from './PnpmfileSettingsFile';

/**
* Loads PNPM's pnpmfile.js configuration, and invokes it to preprocess package.json files,
Expand Down Expand Up @@ -42,11 +39,7 @@ export class PnpmfileConfiguration {
// Set the context to swallow log output and store our settings
const context: IPnpmfileContext = {
log: (message: string) => {},
pnpmfileShimSettings: await PnpmfileConfiguration._getPnpmfileShimSettingsAsync(
rushConfiguration,
subspace,
variant
)
pnpmfileShimSettings: PnpmfileSettingsFile.getPnpmfileShimSettings(rushConfiguration, subspace, variant)
};

return new PnpmfileConfiguration(context);
Expand Down Expand Up @@ -75,62 +68,11 @@ export class PnpmfileConfiguration {
destinationPath: pnpmfilePath
});

const pnpmfileShimSettings: IPnpmfileShimSettings =
await PnpmfileConfiguration._getPnpmfileShimSettingsAsync(rushConfiguration, subspace, variant);

// Write the settings file used by the shim
await JsonFile.saveAsync(pnpmfileShimSettings, path.join(targetDir, 'pnpmfileSettings.json'), {
ensureFolderExists: true
});
}

private static async _getPnpmfileShimSettingsAsync(
rushConfiguration: RushConfiguration,
subspace: Subspace,
variant: string | undefined
): Promise<IPnpmfileShimSettings> {
let allPreferredVersions: { [dependencyName: string]: string } = {};
let allowedAlternativeVersions: { [dependencyName: string]: readonly string[] } = {};
const workspaceVersions: Record<string, string> = {};

// Only workspaces shims in the common versions using pnpmfile
if ((rushConfiguration.packageManagerOptions as PnpmOptionsConfiguration).useWorkspaces) {
const commonVersionsConfiguration: CommonVersionsConfiguration = subspace.getCommonVersions(variant);
const preferredVersions: Map<string, string> = new Map();
MapExtensions.mergeFromMap(
preferredVersions,
rushConfiguration.getImplicitlyPreferredVersions(subspace, variant)
);
for (const [name, version] of commonVersionsConfiguration.getAllPreferredVersions()) {
// Use the most restrictive version range available
if (!preferredVersions.has(name) || semver.subset(version, preferredVersions.get(name)!)) {
preferredVersions.set(name, version);
}
}
allPreferredVersions = MapExtensions.toObject(preferredVersions);
allowedAlternativeVersions = MapExtensions.toObject(
commonVersionsConfiguration.allowedAlternativeVersions
);

for (const project of rushConfiguration.projects) {
workspaceVersions[project.packageName] = project.packageJson.version;
}
}

const settings: IPnpmfileShimSettings = {
allPreferredVersions,
allowedAlternativeVersions,
workspaceVersions,
semverPath: Import.resolveModule({ modulePath: 'semver', baseFolderPath: __dirname })
};

// Use the provided path if available. Otherwise, use the default path.
const userPnpmfilePath: string | undefined = subspace.getPnpmfilePath(variant);
if (userPnpmfilePath && FileSystem.exists(userPnpmfilePath)) {
settings.userPnpmfilePath = userPnpmfilePath;
}

return settings;
await PnpmfileSettingsFile.writeSettingsFileAsync(
PnpmfileSettingsFile.getPnpmfileShimSettings(rushConfiguration, subspace, variant),
targetDir
);
}

/**
Expand Down
105 changes: 105 additions & 0 deletions libraries/rush-lib/src/logic/pnpm/PnpmfileSettingsFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import * as path from 'node:path';

import * as semver from 'semver';

import { FileSystem, Import, JsonFile, MapExtensions } from '@rushstack/node-core-library';

import type { CommonVersionsConfiguration } from '../../api/CommonVersionsConfiguration';
import type { RushConfiguration } from '../../api/RushConfiguration';
import type { Subspace } from '../../api/Subspace';
import type { IPnpmfileShimSettings } from './IPnpmfile';
import type { PnpmOptionsConfiguration } from './PnpmOptionsConfiguration';

export type IPnpmfileCommonShimSettings = Omit<IPnpmfileShimSettings, 'workspaceVersions'>;
export type IPnpmfileReferredAndAlternativeShimSettings = Required<
Pick<
IPnpmfileShimSettings,
'allPreferredVersions' | 'semverPath' | 'allowedAlternativeVersions' | 'userPnpmfilePath'
>
>;

export class PnpmfileSettingsFile {
public static readonly filename: string = 'pnpmfileSettings.json';

public static async writeSettingsFileAsync(
settings: IPnpmfileCommonShimSettings | IPnpmfileShimSettings,
targetDir: string
): Promise<void> {
await JsonFile.saveAsync(settings, path.join(targetDir, PnpmfileSettingsFile.filename), {
ensureFolderExists: true
});
}

public static getCommonPnpmfileShimSettings(
rushConfiguration: RushConfiguration,
subspace: Subspace,
variant: string | undefined
): IPnpmfileReferredAndAlternativeShimSettings {
let allPreferredVersions: { [dependencyName: string]: string } = {};
let allowedAlternativeVersions: { [dependencyName: string]: readonly string[] } = {};

const pnpmOptions: PnpmOptionsConfiguration =
rushConfiguration.packageManagerOptions as PnpmOptionsConfiguration;
if (pnpmOptions?.useWorkspaces) {
const commonVersionsConfiguration: CommonVersionsConfiguration = subspace.getCommonVersions(variant);
const preferredVersions: Map<string, string> = new Map();
MapExtensions.mergeFromMap(
preferredVersions,
rushConfiguration.getImplicitlyPreferredVersions(subspace, variant)
);

for (const [name, version] of commonVersionsConfiguration.getAllPreferredVersions()) {
// Use the most restrictive version range available.
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: IPnpmfileReferredAndAlternativeShimSettings = {
allPreferredVersions,
allowedAlternativeVersions,
semverPath: Import.resolveModule({
modulePath: 'semver',
baseFolderPath: __dirname
}),
userPnpmfilePath: ''
};

const userPnpmfilePath: string = subspace.getPnpmfilePath(variant);
if (FileSystem.exists(userPnpmfilePath)) {
settings.userPnpmfilePath = userPnpmfilePath;
}

return settings;
}

public static getPnpmfileShimSettings(
rushConfiguration: RushConfiguration,
subspace: Subspace,
variant: string | undefined
): IPnpmfileShimSettings {
const workspaceVersions: Record<string, string> = {};

const pnpmOptions: PnpmOptionsConfiguration =
rushConfiguration.packageManagerOptions as PnpmOptionsConfiguration;
if (pnpmOptions?.useWorkspaces) {
for (const project of rushConfiguration.projects) {
workspaceVersions[project.packageName] = project.packageJson.version;
}
}

return {
...PnpmfileSettingsFile.getCommonPnpmfileShimSettings(rushConfiguration, subspace, variant),
workspaceVersions
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@

import * as path from 'node:path';

import { FileSystem, Import, JsonFile, type IDependenciesMetaTable } from '@rushstack/node-core-library';
import { FileSystem, 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 { RushConstants } from '../RushConstants';
import type { Subspace } from '../../api/Subspace';
import type { PnpmOptionsConfiguration } from './PnpmOptionsConfiguration';
import { PnpmfileSettingsFile } from './PnpmfileSettingsFile';

/**
* Loads PNPM's pnpmfile.js configuration, and invokes it to preprocess package.json files,
Expand Down Expand Up @@ -48,13 +48,7 @@ export class SubspacePnpmfileConfiguration {
SubspacePnpmfileConfiguration.getSubspacePnpmfileShimSettings(rushConfiguration, subspace, variant);

// Write the settings file used by the shim
await JsonFile.saveAsync(
subspaceGlobalPnpmfileShimSettings,
path.join(targetDir, 'pnpmfileSettings.json'),
{
ensureFolderExists: true
}
);
await PnpmfileSettingsFile.writeSettingsFileAsync(subspaceGlobalPnpmfileShimSettings, targetDir);
}

public static getSubspacePnpmfileShimSettings(
Expand All @@ -81,20 +75,11 @@ export class SubspacePnpmfileConfiguration {
}

const settings: ISubspacePnpmfileShimSettings = {
...PnpmfileSettingsFile.getCommonPnpmfileShimSettings(rushConfiguration, subspace, variant),
workspaceProjects,
subspaceProjects,
semverPath: Import.resolveModule({ modulePath: 'semver', baseFolderPath: __dirname })
subspaceProjects
};

// common/config/subspaces/<subspace_name>/.pnpmfile.cjs
const userPnpmfilePath: string = path.join(
subspace.getVariantDependentSubspaceConfigFolderPath(variant),
(rushConfiguration.packageManagerWrapper as PnpmPackageManager).pnpmfileFilename
);
if (FileSystem.exists(userPnpmfilePath)) {
settings.userPnpmfilePath = userPnpmfilePath;
}

return settings;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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 { PnpmfileSettingsFile } from '../PnpmfileSettingsFile';
import type { IPnpmfileShimSettings } from '../IPnpmfile';

describe(PnpmfileSettingsFile.name, () => {
const repoPath: string = `${__dirname}/repo-with-subspace`;
const rushFilename: string = `${repoPath}/rush.json`;
const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile(rushFilename);
const subspace = rushConfiguration.defaultSubspace;

it('gets common pnpmfile shim settings for a subspace', () => {
const settings: Omit<IPnpmfileShimSettings, 'workspaceVersions'> =
PnpmfileSettingsFile.getCommonPnpmfileShimSettings(rushConfiguration, subspace, undefined);

// project "a" has @rushstack/terminal@~0.19.0
// common-versions.json has @rushstack/terminal@0.19.2, which satisfies the "~0.19.0"
// so the preferred version for @rushstack/terminal should be 0.19.2
expect(settings.allPreferredVersions).toHaveProperty('@rushstack/terminal', '0.19.2');
});
});
Original file line number Diff line number Diff line change
@@ -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<string, readonly string[]>
| undefined;
expect(allowedAlternativeVersions).toBeDefined();
expect(allowedAlternativeVersions).toHaveProperty('foo', ['1.0.0']);
});
});
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"useWorkspaces": true
}
Original file line number Diff line number Diff line change
@@ -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"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
registry=https://registry.npmjs.org/
always-auth=false
Loading
Loading