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/main_2026-01-31-22-28.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Add catalog support to `rush-pnpm update`.",
"type": "none"
}
],
"packageName": "@microsoft/rush"
}
2 changes: 1 addition & 1 deletion libraries/rush-lib/config/heft.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
{
"sourcePath": "src/cli/test",
"destinationFolders": ["lib-intermediate-commonjs/cli/test"],
"fileExtensions": [".js"]
"fileExtensions": [".js", ".yaml"]
},
{
"sourcePath": "src/logic/pnpm/test",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!temp
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"globalCatalogs": {
"default": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
packages:
- '../../apps/*'
catalogs:
default:
react: ^18.0.0
react-dom: ^18.0.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush.schema.json",
"rushVersion": "5.166.0",
"pnpmVersion": "10.28.1",
"nodeSupportedVersionRange": ">=18.0.0",
"projects": []
}
32 changes: 28 additions & 4 deletions libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -640,16 +640,40 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration
public updateGlobalPatchedDependencies(patchedDependencies: Record<string, string> | undefined): void {
this._globalPatchedDependencies = patchedDependencies;
this._json.globalPatchedDependencies = patchedDependencies;
if (this.jsonFilename) {
JsonFile.save(this._json, this.jsonFilename, { updateExistingFile: true });
}
JsonFile.save(this._json, this.jsonFilename as string, { updateExistingFile: true });
}

/**
* Updates globalOnlyBuiltDependencies field of the PNPM options in the common/config/rush/pnpm-config.json file.
*/
public updateGlobalOnlyBuiltDependencies(onlyBuiltDependencies: string[] | undefined): void {
public async updateGlobalOnlyBuiltDependenciesAsync(
onlyBuiltDependencies: string[] | undefined
): Promise<void> {
this._json.globalOnlyBuiltDependencies = onlyBuiltDependencies;
await JsonFile.saveAsync(this._json, this.jsonFilename as string, {
updateExistingFile: true,
ignoreUndefinedValues: true
});
}

/**
* Updates globalCatalogs field of the PNPM options in the common/config/rush/pnpm-config.json file.
*/
public async updateGlobalCatalogsAsync(
catalogs: Record<string, Record<string, string>> | undefined
): Promise<void> {
this._json.globalCatalogs = catalogs;
await JsonFile.saveAsync(this._json, this.jsonFilename as string, {
updateExistingFile: true,
ignoreUndefinedValues: true
});
}

/**
* Updates globalAllowBuilds field of the PNPM options in the common/config/rush/pnpm-config.json file.
*/
public updateGlobalAllowBuilds(allowBuilds: Record<string, boolean> | undefined): void {
this._json.globalAllowBuilds = allowBuilds;
if (this.jsonFilename) {
JsonFile.save(this._json, this.jsonFilename, { updateExistingFile: true });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@
// See LICENSE in the project root for license information.

import * as path from 'node:path';
import { FileSystem, JsonFile } from '@rushstack/node-core-library';
import { PnpmOptionsConfiguration } from '../PnpmOptionsConfiguration';
import { TestUtilities } from '@rushstack/heft-config-file';

const fakeCommonTempFolder: string = path.join(__dirname, 'common', 'temp');
const PACKAGE_ROOT: string = path.resolve(__dirname, '../../../..');
const TEST_TEMP_FOLDER: string = `${PACKAGE_ROOT}/temp/pnpm-config-update-test`;

const fakeCommonTempFolder: string = `${__dirname}/common/temp`;

describe(PnpmOptionsConfiguration.name, () => {
afterEach(async () => {
await FileSystem.deleteFolderAsync(TEST_TEMP_FOLDER);
});

it('throw error if pnpm-config.json does not exist', () => {
expect(() => {
PnpmOptionsConfiguration.loadFromJsonFileOrThrow(
Expand Down Expand Up @@ -169,4 +177,171 @@ describe(PnpmOptionsConfiguration.name, () => {
}
});
});

describe('updateGlobalCatalogs', () => {
it('updates and saves globalCatalogs to pnpm-config.json', async () => {
const testConfigPath: string = `${TEST_TEMP_FOLDER}/pnpm-config-update-test.json`;

const initialConfig = {
globalCatalogs: {
default: {
react: '^18.0.0'
}
}
};
await JsonFile.saveAsync(initialConfig, testConfigPath, { ensureFolderExists: true });

const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow(
testConfigPath,
fakeCommonTempFolder
);

const updatedCatalogs = {
default: {
react: '^18.2.0',
'react-dom': '^18.2.0'
},
frontend: {
vue: '^3.4.0'
}
};
await pnpmConfiguration.updateGlobalCatalogsAsync(updatedCatalogs);

const reloadedConfig: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow(
testConfigPath,
fakeCommonTempFolder
);

expect(TestUtilities.stripAnnotations(reloadedConfig.globalCatalogs)).toEqual(updatedCatalogs);
});

it('handles undefined catalogs', async () => {
const testConfigPath: string = `${TEST_TEMP_FOLDER}/pnpm-config-undefined-test.json`;

const initialConfig = {
globalCatalogs: {
default: {
react: '^18.0.0'
}
}
};
await JsonFile.saveAsync(initialConfig, testConfigPath, { ensureFolderExists: true });

const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow(
testConfigPath,
fakeCommonTempFolder
);

await pnpmConfiguration.updateGlobalCatalogsAsync(undefined);

const reloadedConfig: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow(
testConfigPath,
fakeCommonTempFolder
);

expect(reloadedConfig.globalCatalogs).toBeUndefined();
});
});

describe('$schema handling', () => {
it('does not fail when $schema is undefined', async () => {
const testConfigPath: string = `${TEST_TEMP_FOLDER}/pnpm-config-no-schema.json`;

const configWithoutSchema = {
globalCatalogs: {
default: {
react: '^18.0.0'
}
}
};
await JsonFile.saveAsync(configWithoutSchema, testConfigPath, { ensureFolderExists: true });

const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow(
testConfigPath,
fakeCommonTempFolder
);

const updatedCatalogs = {
default: {
react: '^18.2.0'
}
};

await expect(pnpmConfiguration.updateGlobalCatalogsAsync(updatedCatalogs)).resolves.not.toThrow();

const reloadedConfig: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow(
testConfigPath,
fakeCommonTempFolder
);

expect(TestUtilities.stripAnnotations(reloadedConfig.globalCatalogs)).toEqual(updatedCatalogs);
});

it('preserves $schema when it exists', async () => {
const testConfigPath: string = `${TEST_TEMP_FOLDER}/pnpm-config-with-schema.json`;

const configWithSchema = {
$schema: 'https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json',
globalCatalogs: {
default: {
react: '^18.0.0'
}
}
};
await JsonFile.saveAsync(configWithSchema, testConfigPath, { ensureFolderExists: true });

const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow(
testConfigPath,
fakeCommonTempFolder
);

const updatedCatalogs = {
default: {
react: '^18.2.0'
}
};
await pnpmConfiguration.updateGlobalCatalogsAsync(updatedCatalogs);

const savedConfig = await JsonFile.loadAsync(testConfigPath);
expect(savedConfig.$schema).toBe(
'https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json'
);
});

it('handles undefined in updateGlobalOnlyBuiltDependenciesAsync', async () => {
const testConfigPath: string = `${TEST_TEMP_FOLDER}/pnpm-config-undefined-test.json`;

const initialConfig = {
$schema: 'https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json',
globalOnlyBuiltDependencies: ['node-gyp', 'esbuild']
};
await JsonFile.saveAsync(initialConfig, testConfigPath, { ensureFolderExists: true });

const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow(
testConfigPath,
fakeCommonTempFolder
);

expect(TestUtilities.stripAnnotations(pnpmConfiguration.globalOnlyBuiltDependencies)).toEqual([
'node-gyp',
'esbuild'
]);

await expect(
pnpmConfiguration.updateGlobalOnlyBuiltDependenciesAsync(undefined)
).resolves.not.toThrow();

const reloadedConfig: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow(
testConfigPath,
fakeCommonTempFolder
);
expect(reloadedConfig.globalOnlyBuiltDependencies).toBeUndefined();

const savedConfigJson = await JsonFile.loadAsync(testConfigPath);
expect(savedConfigJson.$schema).toBe(
'https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json'
);
expect(savedConfigJson.globalOnlyBuiltDependencies).toBeUndefined();
});
});
});
Loading