From ea6ee3da91143775bdeb09acc407615fde824681 Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Sat, 31 Jan 2026 14:07:33 -0800 Subject: [PATCH 01/25] Add 'rush-pnpm up' support for catalogs --- .../rush/main_2026-01-31-22-28.json | 10 + common/reviews/api/rush-lib.api.md | 1 + .../src/cli/RushPnpmCommandLineParser.ts | 24 ++ .../test/RushPnpmCommandLineParser.test.ts | 162 +++++++++++++ .../logic/pnpm/PnpmOptionsConfiguration.ts | 33 ++- .../src/logic/pnpm/PnpmWorkspaceFile.ts | 28 ++- .../test/PnpmOptionsConfiguration.test.ts | 216 ++++++++++++++++++ .../logic/pnpm/test/PnpmWorkspaceFile.test.ts | 43 ++++ 8 files changed, 512 insertions(+), 5 deletions(-) create mode 100644 common/changes/@microsoft/rush/main_2026-01-31-22-28.json create mode 100644 libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts diff --git a/common/changes/@microsoft/rush/main_2026-01-31-22-28.json b/common/changes/@microsoft/rush/main_2026-01-31-22-28.json new file mode 100644 index 00000000000..6721a364f5e --- /dev/null +++ b/common/changes/@microsoft/rush/main_2026-01-31-22-28.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "add catalog support for 'rush-pnpm update'", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 1f8def84f93..292570b2291 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -1183,6 +1183,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration readonly resolutionMode: PnpmResolutionMode | undefined; readonly strictPeerDependencies: boolean; readonly unsupportedPackageJsonSettings: unknown | undefined; + updateGlobalCatalogs(catalogs: Record> | undefined): void; updateGlobalOnlyBuiltDependencies(onlyBuiltDependencies: string[] | undefined): void; updateGlobalPatchedDependencies(patchedDependencies: Record | undefined): void; readonly useWorkspaces: boolean; diff --git a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts index 1980c57f085..fd159df9339 100644 --- a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts +++ b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts @@ -32,6 +32,7 @@ import type { IInstallManagerOptions } from '../logic/base/BaseInstallManagerTyp import { Utilities } from '../utilities/Utilities'; import type { Subspace } from '../api/Subspace'; import type { PnpmOptionsConfiguration } from '../logic/pnpm/PnpmOptionsConfiguration'; +import { PnpmWorkspaceFile } from '../logic/pnpm/PnpmWorkspaceFile'; import { EnvironmentVariableNames } from '../api/EnvironmentConfiguration'; import { initializeDotEnv } from '../logic/dotenv'; @@ -595,6 +596,29 @@ export class RushPnpmCommandLineParser { } break; } + case 'up': + case 'update': { + const pnpmOptions: PnpmOptionsConfiguration | undefined = this._subspace.getPnpmOptions(); + if (pnpmOptions === undefined) { + break; + } + + const workspaceYamlFilename: string = path.join(subspaceTempFolder, 'pnpm-workspace.yaml'); + const newCatalogs: Record> | undefined = + PnpmWorkspaceFile.loadCatalogsFromFile(workspaceYamlFilename); + const currentCatalogs: Record> | undefined = + pnpmOptions.globalCatalogs; + + if (!Objects.areDeepEqual(currentCatalogs, newCatalogs)) { + pnpmOptions.updateGlobalCatalogs(newCatalogs); + + this._terminal.writeWarningLine( + `Rush refreshed the ${RushConstants.pnpmConfigFilename} with updated catalog definitions.\n` + + ` Run "rush update --recheck" to update the lockfile, then commit these changes to Git.` + ); + } + break; + } } } diff --git a/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts b/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts new file mode 100644 index 00000000000..cdb8803ec1e --- /dev/null +++ b/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts @@ -0,0 +1,162 @@ +// 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 { FileSystem, JsonFile } from '@rushstack/node-core-library'; +import { TestUtilities } from '@rushstack/heft-config-file'; +import { RushConfiguration } from '../../api/RushConfiguration'; + +describe('RushPnpmCommandLineParser', () => { + describe('catalog syncing', () => { + let testRepoPath: string; + let pnpmConfigPath: string; + let pnpmWorkspacePath: string; + + beforeEach(() => { + testRepoPath = path.join(__dirname, 'temp', 'catalog-sync-test-repo'); + FileSystem.ensureFolder(testRepoPath); + + const rushJsonPath: string = path.join(testRepoPath, 'rush.json'); + const rushJson = { + $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: [] + }; + JsonFile.save(rushJson, rushJsonPath, { ensureFolderExists: true }); + + const configDir: string = path.join(testRepoPath, 'common', 'config', 'rush'); + FileSystem.ensureFolder(configDir); + + pnpmConfigPath = path.join(configDir, 'pnpm-config.json'); + const pnpmConfig = { + globalCatalogs: { + default: { + react: '^18.0.0', + 'react-dom': '^18.0.0' + } + } + }; + JsonFile.save(pnpmConfig, pnpmConfigPath); + + const tempDir: string = path.join(testRepoPath, 'common', 'temp'); + FileSystem.ensureFolder(tempDir); + + pnpmWorkspacePath = path.join(tempDir, 'pnpm-workspace.yaml'); + const workspaceYaml = `packages: + - '../../apps/*' +catalogs: + default: + react: ^18.0.0 + react-dom: ^18.0.0 +`; + FileSystem.writeFile(pnpmWorkspacePath, workspaceYaml); + }); + + afterEach(() => { + if (FileSystem.exists(testRepoPath)) { + FileSystem.deleteFolder(testRepoPath); + } + }); + + it('syncs updated catalogs from pnpm-workspace.yaml to pnpm-config.json', () => { + const updatedWorkspaceYaml = `packages: + - '../../apps/*' +catalogs: + default: + react: ^18.2.0 + react-dom: ^18.2.0 + typescript: ~5.3.0 + frontend: + vue: ^3.4.0 +`; + FileSystem.writeFile(pnpmWorkspacePath, updatedWorkspaceYaml); + + const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile( + path.join(testRepoPath, 'rush.json') + ); + + const subspace = rushConfiguration.getSubspace('default'); + const pnpmOptions = subspace.getPnpmOptions(); + + expect(TestUtilities.stripAnnotations(pnpmOptions?.globalCatalogs)).toEqual({ + default: { + react: '^18.0.0', + 'react-dom': '^18.0.0' + } + }); + + const { PnpmWorkspaceFile } = require('../../logic/pnpm/PnpmWorkspaceFile'); + const newCatalogs = PnpmWorkspaceFile.loadCatalogsFromFile(pnpmWorkspacePath); + + pnpmOptions?.updateGlobalCatalogs(newCatalogs); + + const updatedRushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile( + path.join(testRepoPath, 'rush.json') + ); + const updatedSubspace = updatedRushConfiguration.getSubspace('default'); + const updatedPnpmOptions = updatedSubspace.getPnpmOptions(); + + expect(TestUtilities.stripAnnotations(updatedPnpmOptions?.globalCatalogs)).toEqual({ + default: { + react: '^18.2.0', + 'react-dom': '^18.2.0', + typescript: '~5.3.0' + }, + frontend: { + vue: '^3.4.0' + } + }); + }); + + it('does not update pnpm-config.json when catalogs are unchanged', () => { + const { PnpmWorkspaceFile } = require('../../logic/pnpm/PnpmWorkspaceFile'); + const newCatalogs = PnpmWorkspaceFile.loadCatalogsFromFile(pnpmWorkspacePath); + + const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile( + path.join(testRepoPath, 'rush.json') + ); + const subspace = rushConfiguration.getSubspace('default'); + const pnpmOptions = subspace.getPnpmOptions(); + + pnpmOptions?.updateGlobalCatalogs(newCatalogs); + + const savedConfig = JsonFile.load(pnpmConfigPath); + expect(savedConfig.globalCatalogs).toEqual({ + default: { + react: '^18.0.0', + 'react-dom': '^18.0.0' + } + }); + }); + + it('removes catalogs when pnpm-workspace.yaml has no catalogs', () => { + const workspaceWithoutCatalogs = `packages: + - '../../apps/*' +`; + FileSystem.writeFile(pnpmWorkspacePath, workspaceWithoutCatalogs); + + const { PnpmWorkspaceFile } = require('../../logic/pnpm/PnpmWorkspaceFile'); + const newCatalogs = PnpmWorkspaceFile.loadCatalogsFromFile(pnpmWorkspacePath); + + expect(newCatalogs).toBeUndefined(); + + const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile( + path.join(testRepoPath, 'rush.json') + ); + const subspace = rushConfiguration.getSubspace('default'); + const pnpmOptions = subspace.getPnpmOptions(); + + pnpmOptions?.updateGlobalCatalogs(newCatalogs); + + const updatedRushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile( + path.join(testRepoPath, 'rush.json') + ); + const updatedSubspace = updatedRushConfiguration.getSubspace('default'); + const updatedPnpmOptions = updatedSubspace.getPnpmOptions(); + + expect(updatedPnpmOptions?.globalCatalogs).toBeUndefined(); + }); + }); +}); diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts index 026ecd29339..4b4970d08f4 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts @@ -517,7 +517,12 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration terminal, jsonFilePath ); - pnpmConfigJson.$schema = pnpmOptionsConfigFile.getSchemaPropertyOriginalValue(pnpmConfigJson); + const schemaValue: string | undefined = + pnpmOptionsConfigFile.getSchemaPropertyOriginalValue(pnpmConfigJson); + // Only set $schema if it has a defined value, since JsonFile.save() will fail if any property is undefined + if (schemaValue !== undefined) { + pnpmConfigJson.$schema = schemaValue; + } return new PnpmOptionsConfiguration(pnpmConfigJson || {}, commonTempFolder, jsonFilePath); } @@ -534,7 +539,11 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration */ public updateGlobalPatchedDependencies(patchedDependencies: Record | undefined): void { this._globalPatchedDependencies = patchedDependencies; - this._json.globalPatchedDependencies = patchedDependencies; + if (patchedDependencies === undefined) { + delete this._json.globalPatchedDependencies; + } else { + this._json.globalPatchedDependencies = patchedDependencies; + } if (this.jsonFilename) { JsonFile.save(this._json, this.jsonFilename, { updateExistingFile: true }); } @@ -544,7 +553,25 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration * Updates globalOnlyBuiltDependencies field of the PNPM options in the common/config/rush/pnpm-config.json file. */ public updateGlobalOnlyBuiltDependencies(onlyBuiltDependencies: string[] | undefined): void { - this._json.globalOnlyBuiltDependencies = onlyBuiltDependencies; + if (onlyBuiltDependencies === undefined) { + delete this._json.globalOnlyBuiltDependencies; + } else { + this._json.globalOnlyBuiltDependencies = onlyBuiltDependencies; + } + if (this.jsonFilename) { + JsonFile.save(this._json, this.jsonFilename, { updateExistingFile: true }); + } + } + + /** + * Updates globalCatalogs field of the PNPM options in the common/config/rush/pnpm-config.json file. + */ + public updateGlobalCatalogs(catalogs: Record> | undefined): void { + if (catalogs === undefined) { + delete this._json.globalCatalogs; + } else { + this._json.globalCatalogs = catalogs; + } if (this.jsonFilename) { JsonFile.save(this._json, this.jsonFilename, { updateExistingFile: true }); } diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts index f2d86e24dc5..e4fc4e1200f 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts @@ -3,7 +3,7 @@ import * as path from 'node:path'; -import { Sort, Import, Path } from '@rushstack/node-core-library'; +import { FileSystem, Sort, Import, Path } from '@rushstack/node-core-library'; import { BaseWorkspaceFile } from '../base/BaseWorkspaceFile'; import { PNPM_SHRINKWRAP_YAML_FORMAT } from './PnpmYamlCommon'; @@ -29,7 +29,7 @@ const globEscape: (unescaped: string) => string = require('glob-escape'); // No interface IPnpmWorkspaceYaml { /** The list of local package directories */ packages: string[]; - /** Catalog definitions for centralized version management */ + /** Named catalog definitions for centralized version management */ catalogs?: Record>; } @@ -56,6 +56,30 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { this._catalogs = undefined; } + /** + * Loads and returns the catalogs section from an existing pnpm-workspace.yaml file. + * This method handles both the singular 'catalog' field (for the default catalog) and + * the plural 'catalogs' field (for named catalogs), merging them into a single object. + * + * @param workspaceYamlFilename - The path to the pnpm-workspace.yaml file + * @returns The catalogs object, or undefined if the file doesn't exist or has no catalogs + */ + public static loadCatalogsFromFile( + workspaceYamlFilename: string + ): Record> | undefined { + if (!FileSystem.exists(workspaceYamlFilename)) { + return undefined; + } + const content: string = FileSystem.readFile(workspaceYamlFilename); + const parsed: IPnpmWorkspaceYaml | undefined = yamlModule.load(content) as IPnpmWorkspaceYaml | undefined; + + if (!parsed || !parsed.catalogs) { + return undefined; + } + + return parsed.catalogs; + } + /** * Sets the catalog definitions for the workspace. * @param catalogs - A map of catalog name to package versions diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts index 369054709ad..5b7d4e131cb 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts @@ -2,6 +2,7 @@ // 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'; @@ -122,4 +123,219 @@ describe(PnpmOptionsConfiguration.name, () => { } }); }); + + describe('updateGlobalCatalogs', () => { + it('updates and saves globalCatalogs to pnpm-config.json', () => { + const testConfigPath: string = path.join(__dirname, 'temp', 'pnpm-config-update-test.json'); + + const tempDir: string = path.dirname(testConfigPath); + FileSystem.ensureFolder(tempDir); + + try { + const initialConfig = { + globalCatalogs: { + default: { + react: '^18.0.0' + } + } + }; + JsonFile.save(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' + } + }; + pnpmConfiguration.updateGlobalCatalogs(updatedCatalogs); + + const reloadedConfig: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( + testConfigPath, + fakeCommonTempFolder + ); + + expect(TestUtilities.stripAnnotations(reloadedConfig.globalCatalogs)).toEqual(updatedCatalogs); + } finally { + // Clean up + if (FileSystem.exists(testConfigPath)) { + FileSystem.deleteFile(testConfigPath); + } + } + }); + + it('handles undefined catalogs', () => { + const testConfigPath: string = path.join(__dirname, 'temp', 'pnpm-config-undefined-test.json'); + + const tempDir: string = path.dirname(testConfigPath); + FileSystem.ensureFolder(tempDir); + + try { + const initialConfig = { + globalCatalogs: { + default: { + react: '^18.0.0' + } + } + }; + JsonFile.save(initialConfig, testConfigPath, { ensureFolderExists: true }); + + const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( + testConfigPath, + fakeCommonTempFolder + ); + + pnpmConfiguration.updateGlobalCatalogs(undefined); + + const reloadedConfig: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( + testConfigPath, + fakeCommonTempFolder + ); + + expect(reloadedConfig.globalCatalogs).toBeUndefined(); + } finally { + if (FileSystem.exists(testConfigPath)) { + FileSystem.deleteFile(testConfigPath); + } + } + }); + }); + + describe('$schema handling', () => { + it('does not fail when $schema is undefined', () => { + const testConfigPath: string = path.join(__dirname, 'temp', 'pnpm-config-no-schema.json'); + + const tempDir: string = path.dirname(testConfigPath); + FileSystem.ensureFolder(tempDir); + + try { + const configWithoutSchema = { + globalCatalogs: { + default: { + react: '^18.0.0' + } + } + }; + JsonFile.save(configWithoutSchema, testConfigPath, { ensureFolderExists: true }); + + const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( + testConfigPath, + fakeCommonTempFolder + ); + + const updatedCatalogs = { + default: { + react: '^18.2.0' + } + }; + + expect(() => { + pnpmConfiguration.updateGlobalCatalogs(updatedCatalogs); + }).not.toThrow(); + + const reloadedConfig: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( + testConfigPath, + fakeCommonTempFolder + ); + + expect(TestUtilities.stripAnnotations(reloadedConfig.globalCatalogs)).toEqual(updatedCatalogs); + } finally { + if (FileSystem.exists(testConfigPath)) { + FileSystem.deleteFile(testConfigPath); + } + } + }); + + it('preserves $schema when it exists', () => { + const testConfigPath: string = path.join(__dirname, 'temp', 'pnpm-config-with-schema.json'); + + const tempDir: string = path.dirname(testConfigPath); + FileSystem.ensureFolder(tempDir); + + try { + // Create config with $schema + const configWithSchema = { + $schema: 'https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json', + globalCatalogs: { + default: { + react: '^18.0.0' + } + } + }; + JsonFile.save(configWithSchema, testConfigPath, { ensureFolderExists: true }); + + const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( + testConfigPath, + fakeCommonTempFolder + ); + + const updatedCatalogs = { + default: { + react: '^18.2.0' + } + }; + pnpmConfiguration.updateGlobalCatalogs(updatedCatalogs); + + const savedConfig = JsonFile.load(testConfigPath); + expect(savedConfig.$schema).toBe( + 'https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json' + ); + } finally { + if (FileSystem.exists(testConfigPath)) { + FileSystem.deleteFile(testConfigPath); + } + } + }); + + it('handles undefined in updateGlobalOnlyBuiltDependencies', () => { + const testConfigPath: string = path.join(__dirname, 'temp', 'pnpm-config-undefined-test.json'); + const tempDir: string = path.dirname(testConfigPath); + FileSystem.ensureFolder(tempDir); + + try { + const initialConfig = { + $schema: 'https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json', + globalOnlyBuiltDependencies: ['node-gyp', 'esbuild'] + }; + JsonFile.save(initialConfig, testConfigPath, { ensureFolderExists: true }); + + const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( + testConfigPath, + fakeCommonTempFolder + ); + + expect(TestUtilities.stripAnnotations(pnpmConfiguration.globalOnlyBuiltDependencies)).toEqual([ + 'node-gyp', + 'esbuild' + ]); + + expect(() => { + pnpmConfiguration.updateGlobalOnlyBuiltDependencies(undefined); + }).not.toThrow(); + + const reloadedConfig: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( + testConfigPath, + fakeCommonTempFolder + ); + expect(reloadedConfig.globalOnlyBuiltDependencies).toBeUndefined(); + + const savedConfigJson = JsonFile.load(testConfigPath); + expect(savedConfigJson.$schema).toBe( + 'https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json' + ); + expect(savedConfigJson.globalOnlyBuiltDependencies).toBeUndefined(); + } finally { + if (FileSystem.exists(testConfigPath)) { + FileSystem.deleteFile(testConfigPath); + } + } + }); + }); }); diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts index d8cdb149a88..49f1dca56ff 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts @@ -180,4 +180,47 @@ describe(PnpmWorkspaceFile.name, () => { expect(content).toMatchSnapshot(); }); }); + + describe('loadCatalogsFromFile', () => { + it('returns undefined for non-existent file', () => { + const nonExistentFile: string = path.join(tempDir, 'non-existent.yaml'); + const catalogs = PnpmWorkspaceFile.loadCatalogsFromFile(nonExistentFile); + expect(catalogs).toBeUndefined(); + }); + + it('returns undefined when file has no catalogs', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const catalogs = PnpmWorkspaceFile.loadCatalogsFromFile(workspaceFilePath); + expect(catalogs).toBeUndefined(); + }); + + it('loads catalogs from existing file', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + workspaceFile.setCatalogs({ + default: { + react: '^18.0.0', + typescript: '~5.3.0' + }, + frontend: { + vue: '^3.4.0' + } + }); + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const catalogs = PnpmWorkspaceFile.loadCatalogsFromFile(workspaceFilePath); + expect(catalogs).toEqual({ + default: { + react: '^18.0.0', + typescript: '~5.3.0' + }, + frontend: { + vue: '^3.4.0' + } + }); + }); + }); }); From 8323e4364abd68d2911be4a90d5bc4ee077d2ccc Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Tue, 24 Feb 2026 08:19:52 -0800 Subject: [PATCH 02/25] Update common/changes/@microsoft/rush/main_2026-01-31-22-28.json Co-authored-by: Ian Clanton-Thuon --- common/changes/@microsoft/rush/main_2026-01-31-22-28.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/changes/@microsoft/rush/main_2026-01-31-22-28.json b/common/changes/@microsoft/rush/main_2026-01-31-22-28.json index 6721a364f5e..8f3529a2aa7 100644 --- a/common/changes/@microsoft/rush/main_2026-01-31-22-28.json +++ b/common/changes/@microsoft/rush/main_2026-01-31-22-28.json @@ -2,7 +2,7 @@ "changes": [ { "packageName": "@microsoft/rush", - "comment": "add catalog support for 'rush-pnpm update'", + "comment": "Add catalog support to `rush-pnpm update`.", "type": "none" } ], From f715b55f0606838d250bc6f32ec496308c224cbb Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Wed, 25 Feb 2026 13:00:41 -0800 Subject: [PATCH 03/25] Apply code review feedback --- common/reviews/api/rush-lib.api.md | 2 +- .../src/cli/RushPnpmCommandLineParser.ts | 4 +-- .../test/RushPnpmCommandLineParser.test.ts | 12 +++---- .../logic/pnpm/PnpmOptionsConfiguration.ts | 6 ++-- .../src/logic/pnpm/PnpmWorkspaceFile.ts | 8 ++--- .../test/PnpmOptionsConfiguration.test.ts | 8 ++--- .../logic/pnpm/test/PnpmWorkspaceFile.test.ts | 31 ++++++++++++++----- 7 files changed, 45 insertions(+), 26 deletions(-) diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 292570b2291..ae62715d680 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -1184,7 +1184,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration readonly strictPeerDependencies: boolean; readonly unsupportedPackageJsonSettings: unknown | undefined; updateGlobalCatalogs(catalogs: Record> | undefined): void; - updateGlobalOnlyBuiltDependencies(onlyBuiltDependencies: string[] | undefined): void; + updateGlobalOnlyBuiltDependenciesAsync(onlyBuiltDependencies: string[] | undefined): Promise; updateGlobalPatchedDependencies(patchedDependencies: Record | undefined): void; readonly useWorkspaces: boolean; } diff --git a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts index fd159df9339..ad5bd538611 100644 --- a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts +++ b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts @@ -584,7 +584,7 @@ export class RushPnpmCommandLineParser { if (!Objects.areDeepEqual(currentGlobalOnlyBuiltDependencies, newGlobalOnlyBuiltDependencies)) { // Update onlyBuiltDependencies to pnpm configuration file - pnpmOptions?.updateGlobalOnlyBuiltDependencies(newGlobalOnlyBuiltDependencies); + await pnpmOptions?.updateGlobalOnlyBuiltDependenciesAsync(newGlobalOnlyBuiltDependencies); // Rerun installation to update await this._doRushUpdateAsync(); @@ -605,7 +605,7 @@ export class RushPnpmCommandLineParser { const workspaceYamlFilename: string = path.join(subspaceTempFolder, 'pnpm-workspace.yaml'); const newCatalogs: Record> | undefined = - PnpmWorkspaceFile.loadCatalogsFromFile(workspaceYamlFilename); + await PnpmWorkspaceFile.loadCatalogsFromFileAsync(workspaceYamlFilename); const currentCatalogs: Record> | undefined = pnpmOptions.globalCatalogs; diff --git a/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts b/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts index cdb8803ec1e..ef14209feba 100644 --- a/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts +++ b/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts @@ -60,7 +60,7 @@ catalogs: } }); - it('syncs updated catalogs from pnpm-workspace.yaml to pnpm-config.json', () => { + it('syncs updated catalogs from pnpm-workspace.yaml to pnpm-config.json', async () => { const updatedWorkspaceYaml = `packages: - '../../apps/*' catalogs: @@ -88,7 +88,7 @@ catalogs: }); const { PnpmWorkspaceFile } = require('../../logic/pnpm/PnpmWorkspaceFile'); - const newCatalogs = PnpmWorkspaceFile.loadCatalogsFromFile(pnpmWorkspacePath); + const newCatalogs = await PnpmWorkspaceFile.loadCatalogsFromFileAsync(pnpmWorkspacePath); pnpmOptions?.updateGlobalCatalogs(newCatalogs); @@ -110,9 +110,9 @@ catalogs: }); }); - it('does not update pnpm-config.json when catalogs are unchanged', () => { + it('does not update pnpm-config.json when catalogs are unchanged', async () => { const { PnpmWorkspaceFile } = require('../../logic/pnpm/PnpmWorkspaceFile'); - const newCatalogs = PnpmWorkspaceFile.loadCatalogsFromFile(pnpmWorkspacePath); + const newCatalogs = await PnpmWorkspaceFile.loadCatalogsFromFileAsync(pnpmWorkspacePath); const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile( path.join(testRepoPath, 'rush.json') @@ -131,14 +131,14 @@ catalogs: }); }); - it('removes catalogs when pnpm-workspace.yaml has no catalogs', () => { + it('removes catalogs when pnpm-workspace.yaml has no catalogs', async () => { const workspaceWithoutCatalogs = `packages: - '../../apps/*' `; FileSystem.writeFile(pnpmWorkspacePath, workspaceWithoutCatalogs); const { PnpmWorkspaceFile } = require('../../logic/pnpm/PnpmWorkspaceFile'); - const newCatalogs = PnpmWorkspaceFile.loadCatalogsFromFile(pnpmWorkspacePath); + const newCatalogs = await PnpmWorkspaceFile.loadCatalogsFromFileAsync(pnpmWorkspacePath); expect(newCatalogs).toBeUndefined(); diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts index 4b4970d08f4..ca529adb7ce 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts @@ -552,14 +552,16 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration /** * 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 { if (onlyBuiltDependencies === undefined) { delete this._json.globalOnlyBuiltDependencies; } else { this._json.globalOnlyBuiltDependencies = onlyBuiltDependencies; } if (this.jsonFilename) { - JsonFile.save(this._json, this.jsonFilename, { updateExistingFile: true }); + await JsonFile.saveAsync(this._json, this.jsonFilename, { updateExistingFile: true }); } } diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts index e4fc4e1200f..620aacdfd34 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts @@ -64,13 +64,13 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { * @param workspaceYamlFilename - The path to the pnpm-workspace.yaml file * @returns The catalogs object, or undefined if the file doesn't exist or has no catalogs */ - public static loadCatalogsFromFile( + public static async loadCatalogsFromFileAsync( workspaceYamlFilename: string - ): Record> | undefined { - if (!FileSystem.exists(workspaceYamlFilename)) { + ): Promise> | undefined> { + if (!(await FileSystem.existsAsync(workspaceYamlFilename))) { return undefined; } - const content: string = FileSystem.readFile(workspaceYamlFilename); + const content: string = await FileSystem.readFileAsync(workspaceYamlFilename); const parsed: IPnpmWorkspaceYaml | undefined = yamlModule.load(content) as IPnpmWorkspaceYaml | undefined; if (!parsed || !parsed.catalogs) { diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts index 5b7d4e131cb..036fd768674 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts @@ -294,7 +294,7 @@ describe(PnpmOptionsConfiguration.name, () => { } }); - it('handles undefined in updateGlobalOnlyBuiltDependencies', () => { + it('handles undefined in updateGlobalOnlyBuiltDependenciesAsync', async () => { const testConfigPath: string = path.join(__dirname, 'temp', 'pnpm-config-undefined-test.json'); const tempDir: string = path.dirname(testConfigPath); FileSystem.ensureFolder(tempDir); @@ -316,9 +316,9 @@ describe(PnpmOptionsConfiguration.name, () => { 'esbuild' ]); - expect(() => { - pnpmConfiguration.updateGlobalOnlyBuiltDependencies(undefined); - }).not.toThrow(); + await expect( + pnpmConfiguration.updateGlobalOnlyBuiltDependenciesAsync(undefined) + ).resolves.not.toThrow(); const reloadedConfig: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( testConfigPath, diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts index 49f1dca56ff..a3a43edb319 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts @@ -12,7 +12,9 @@ describe(PnpmWorkspaceFile.name, () => { let mockWriteFile: jest.SpyInstance; let mockReadFile: jest.SpyInstance; + let mockReadFileAsync: jest.SpyInstance; let mockExists: jest.SpyInstance; + let mockExistsAsync: jest.SpyInstance; let writtenContent: string | undefined; beforeEach(() => { @@ -34,16 +36,31 @@ describe(PnpmWorkspaceFile.name, () => { return writtenContent; }); + // Mock async version for loadCatalogsFromFileAsync + mockReadFileAsync = jest.spyOn(FileSystem, 'readFileAsync').mockImplementation(async () => { + if (writtenContent === undefined) { + throw new Error('File not found'); + } + return writtenContent; + }); + // Mock FileSystem.exists to return true if content was written mockExists = jest.spyOn(FileSystem, 'exists').mockImplementation(() => { return writtenContent !== undefined; }); + + // Mock async version for loadCatalogsFromFileAsync + mockExistsAsync = jest.spyOn(FileSystem, 'existsAsync').mockImplementation(async () => { + return writtenContent !== undefined; + }); }); afterEach(() => { mockWriteFile.mockRestore(); mockReadFile.mockRestore(); + mockReadFileAsync.mockRestore(); mockExists.mockRestore(); + mockExistsAsync.mockRestore(); }); describe('basic functionality', () => { @@ -181,23 +198,23 @@ describe(PnpmWorkspaceFile.name, () => { }); }); - describe('loadCatalogsFromFile', () => { - it('returns undefined for non-existent file', () => { + describe('loadCatalogsFromFileAsync', () => { + it('returns undefined for non-existent file', async () => { const nonExistentFile: string = path.join(tempDir, 'non-existent.yaml'); - const catalogs = PnpmWorkspaceFile.loadCatalogsFromFile(nonExistentFile); + const catalogs = await PnpmWorkspaceFile.loadCatalogsFromFileAsync(nonExistentFile); expect(catalogs).toBeUndefined(); }); - it('returns undefined when file has no catalogs', () => { + it('returns undefined when file has no catalogs', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); workspaceFile.addPackage(path.join(projectsDir, 'app1')); workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); - const catalogs = PnpmWorkspaceFile.loadCatalogsFromFile(workspaceFilePath); + const catalogs = await PnpmWorkspaceFile.loadCatalogsFromFileAsync(workspaceFilePath); expect(catalogs).toBeUndefined(); }); - it('loads catalogs from existing file', () => { + it('loads catalogs from existing file', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); workspaceFile.addPackage(path.join(projectsDir, 'app1')); workspaceFile.setCatalogs({ @@ -211,7 +228,7 @@ describe(PnpmWorkspaceFile.name, () => { }); workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); - const catalogs = PnpmWorkspaceFile.loadCatalogsFromFile(workspaceFilePath); + const catalogs = await PnpmWorkspaceFile.loadCatalogsFromFileAsync(workspaceFilePath); expect(catalogs).toEqual({ default: { react: '^18.0.0', From ad97f1759f9677038dad4bbfc493b6a9271bebc3 Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Wed, 25 Feb 2026 19:27:49 -0800 Subject: [PATCH 04/25] Update libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts Co-authored-by: Ian Clanton-Thuon --- libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts index 620aacdfd34..f57efb948ae 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts @@ -73,11 +73,7 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { const content: string = await FileSystem.readFileAsync(workspaceYamlFilename); const parsed: IPnpmWorkspaceYaml | undefined = yamlModule.load(content) as IPnpmWorkspaceYaml | undefined; - if (!parsed || !parsed.catalogs) { - return undefined; - } - - return parsed.catalogs; + return parsed?.catalogs; } /** From a6e3c54d38f8a73eeb13b389f881f3123d67ca30 Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Wed, 25 Feb 2026 19:29:51 -0800 Subject: [PATCH 05/25] Update libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts Co-authored-by: Ian Clanton-Thuon --- .../rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts index ca529adb7ce..456a6af1d2f 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts @@ -517,12 +517,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration terminal, jsonFilePath ); - const schemaValue: string | undefined = - pnpmOptionsConfigFile.getSchemaPropertyOriginalValue(pnpmConfigJson); - // Only set $schema if it has a defined value, since JsonFile.save() will fail if any property is undefined - if (schemaValue !== undefined) { - pnpmConfigJson.$schema = schemaValue; - } + pnpmConfigJson.$schema = pnpmOptionsConfigFile.getSchemaPropertyOriginalValue(pnpmConfigJson); return new PnpmOptionsConfiguration(pnpmConfigJson || {}, commonTempFolder, jsonFilePath); } From 46f7359de9214f942f61ef7618d4ff9544b3a5d9 Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Wed, 25 Feb 2026 19:30:20 -0800 Subject: [PATCH 06/25] Update libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts Co-authored-by: Ian Clanton-Thuon --- .../rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts index 456a6af1d2f..0a390197761 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts @@ -534,11 +534,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration */ public updateGlobalPatchedDependencies(patchedDependencies: Record | undefined): void { this._globalPatchedDependencies = patchedDependencies; - if (patchedDependencies === undefined) { - delete this._json.globalPatchedDependencies; - } else { - this._json.globalPatchedDependencies = patchedDependencies; - } + this._json.globalPatchedDependencies = patchedDependencies; if (this.jsonFilename) { JsonFile.save(this._json, this.jsonFilename, { updateExistingFile: true }); } From 19f5af23f10124054fb661ddfb6434db7e93c1c9 Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Wed, 25 Feb 2026 19:30:44 -0800 Subject: [PATCH 07/25] Update libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts Co-authored-by: Ian Clanton-Thuon --- .../rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts index 0a390197761..fedc4d6b9e9 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts @@ -546,11 +546,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration public async updateGlobalOnlyBuiltDependenciesAsync( onlyBuiltDependencies: string[] | undefined ): Promise { - if (onlyBuiltDependencies === undefined) { - delete this._json.globalOnlyBuiltDependencies; - } else { - this._json.globalOnlyBuiltDependencies = onlyBuiltDependencies; - } + this._json.globalOnlyBuiltDependencies = onlyBuiltDependencies; if (this.jsonFilename) { await JsonFile.saveAsync(this._json, this.jsonFilename, { updateExistingFile: true }); } From 89041c949b2401bfc24a4102f5aaf6f9abe05bf4 Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Wed, 25 Feb 2026 19:31:04 -0800 Subject: [PATCH 08/25] Update libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts Co-authored-by: Ian Clanton-Thuon --- .../rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts b/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts index ef14209feba..a536b9959af 100644 --- a/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts +++ b/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts @@ -16,7 +16,7 @@ describe('RushPnpmCommandLineParser', () => { testRepoPath = path.join(__dirname, 'temp', 'catalog-sync-test-repo'); FileSystem.ensureFolder(testRepoPath); - const rushJsonPath: string = path.join(testRepoPath, 'rush.json'); + const rushJsonPath: string = `${testRepoPath/rush.json`; const rushJson = { $schema: 'https://developer.microsoft.com/json-schemas/rush/v5/rush.schema.json', rushVersion: '5.166.0', From 9507313263ff5bc04362cacd98c66e03bdd6ce1c Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Wed, 25 Feb 2026 19:31:45 -0800 Subject: [PATCH 09/25] Update libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts Co-authored-by: Ian Clanton-Thuon --- libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts index fedc4d6b9e9..68eb100d80e 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts @@ -548,7 +548,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration ): Promise { this._json.globalOnlyBuiltDependencies = onlyBuiltDependencies; if (this.jsonFilename) { - await JsonFile.saveAsync(this._json, this.jsonFilename, { updateExistingFile: true }); + await JsonFile.saveAsync(this._json, this.jsonFilename, { updateExistingFile: true, ignoreUndefinedValues: true }); } } From cb3c760aa72f35e4ab2d8cb761094bcd24d3cd60 Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Wed, 25 Feb 2026 19:35:18 -0800 Subject: [PATCH 10/25] Update libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts Co-authored-by: Ian Clanton-Thuon --- .../rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts index 68eb100d80e..5f71880290f 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts @@ -556,11 +556,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration * Updates globalCatalogs field of the PNPM options in the common/config/rush/pnpm-config.json file. */ public updateGlobalCatalogs(catalogs: Record> | undefined): void { - if (catalogs === undefined) { - delete this._json.globalCatalogs; - } else { - this._json.globalCatalogs = catalogs; - } + this._json.globalCatalogs = catalogs; if (this.jsonFilename) { JsonFile.save(this._json, this.jsonFilename, { updateExistingFile: true }); } From 073c2b69e2e6a1863936d060d350fb298a4cd35b Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Wed, 25 Feb 2026 19:36:07 -0800 Subject: [PATCH 11/25] Update libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts Co-authored-by: Ian Clanton-Thuon --- libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts index 5f71880290f..cb22029d1c5 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts @@ -558,7 +558,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration public updateGlobalCatalogs(catalogs: Record> | undefined): void { this._json.globalCatalogs = catalogs; if (this.jsonFilename) { - JsonFile.save(this._json, this.jsonFilename, { updateExistingFile: true }); + JsonFile.save(this._json, this.jsonFilename, { updateExistingFile: true, ignoreUndefinedValues: true }); } } } From 88b30003d7ad2bf09745ee66954fd7b4b9009038 Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Wed, 25 Feb 2026 19:36:37 -0800 Subject: [PATCH 12/25] Update libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts Co-authored-by: Ian Clanton-Thuon --- .../src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts index 036fd768674..e270733fa1a 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts @@ -201,9 +201,7 @@ describe(PnpmOptionsConfiguration.name, () => { expect(reloadedConfig.globalCatalogs).toBeUndefined(); } finally { - if (FileSystem.exists(testConfigPath)) { - FileSystem.deleteFile(testConfigPath); - } + await FileSystem.deleteFileAsync(testConfigPath); } }); }); From c59d56faaa1a82986b8783d3e6998846943e4904 Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Wed, 25 Feb 2026 19:37:23 -0800 Subject: [PATCH 13/25] Update libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts Co-authored-by: Ian Clanton-Thuon --- .../src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts index e270733fa1a..cad460577ff 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts @@ -245,9 +245,7 @@ describe(PnpmOptionsConfiguration.name, () => { expect(TestUtilities.stripAnnotations(reloadedConfig.globalCatalogs)).toEqual(updatedCatalogs); } finally { - if (FileSystem.exists(testConfigPath)) { - FileSystem.deleteFile(testConfigPath); - } + await FileSystem.deleteFile(testConfigPath); } }); From d89bc91c055572b897dea46e01f52a411be08c7f Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Wed, 25 Feb 2026 19:38:14 -0800 Subject: [PATCH 14/25] Update libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts Co-authored-by: Ian Clanton-Thuon --- .../src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts index cad460577ff..55bbe4fa816 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts @@ -284,9 +284,7 @@ describe(PnpmOptionsConfiguration.name, () => { 'https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json' ); } finally { - if (FileSystem.exists(testConfigPath)) { - FileSystem.deleteFile(testConfigPath); - } + await FileSystem.deleteFile(testConfigPath); } }); From 27445b9fffe427b2ec86dfa2f48b30abd0a4d5da Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Wed, 25 Feb 2026 19:39:25 -0800 Subject: [PATCH 15/25] Update libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts Co-authored-by: Ian Clanton-Thuon --- .../src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts index 55bbe4fa816..952bc6ccb48 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts @@ -171,11 +171,8 @@ describe(PnpmOptionsConfiguration.name, () => { } }); - it('handles undefined catalogs', () => { - const testConfigPath: string = path.join(__dirname, 'temp', 'pnpm-config-undefined-test.json'); - - const tempDir: string = path.dirname(testConfigPath); - FileSystem.ensureFolder(tempDir); + it('handles undefined catalogs', async () => { + const testConfigPath: string = `${__dirname}/temp/pnpm-config-undefined-test.json`; try { const initialConfig = { @@ -185,7 +182,7 @@ describe(PnpmOptionsConfiguration.name, () => { } } }; - JsonFile.save(initialConfig, testConfigPath, { ensureFolderExists: true }); + await JsonFile.saveAsync(initialConfig, testConfigPath, { ensureFolderExists: true }); const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( testConfigPath, From 20f3cf8c33e46acc652427434a223879f11167da Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Wed, 25 Feb 2026 19:39:58 -0800 Subject: [PATCH 16/25] Update libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts Co-authored-by: Ian Clanton-Thuon --- .../src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts index 952bc6ccb48..7b58f13c43c 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts @@ -204,11 +204,8 @@ describe(PnpmOptionsConfiguration.name, () => { }); describe('$schema handling', () => { - it('does not fail when $schema is undefined', () => { - const testConfigPath: string = path.join(__dirname, 'temp', 'pnpm-config-no-schema.json'); - - const tempDir: string = path.dirname(testConfigPath); - FileSystem.ensureFolder(tempDir); + it('does not fail when $schema is undefined', async () => { + const testConfigPath: string = `${__dirname}/temp/pnpm-config-no-schema.json`; try { const configWithoutSchema = { @@ -218,7 +215,7 @@ describe(PnpmOptionsConfiguration.name, () => { } } }; - JsonFile.save(configWithoutSchema, testConfigPath, { ensureFolderExists: true }); + await JsonFile.saveAsync(configWithoutSchema, testConfigPath, { ensureFolderExists: true }); const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( testConfigPath, From 30a60dd483a8a600394f930aefca68ad4a628ea8 Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Wed, 25 Feb 2026 19:40:48 -0800 Subject: [PATCH 17/25] Update libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts Co-authored-by: Ian Clanton-Thuon --- .../src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts index 7b58f13c43c..a222064a878 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts @@ -244,10 +244,7 @@ describe(PnpmOptionsConfiguration.name, () => { }); it('preserves $schema when it exists', () => { - const testConfigPath: string = path.join(__dirname, 'temp', 'pnpm-config-with-schema.json'); - - const tempDir: string = path.dirname(testConfigPath); - FileSystem.ensureFolder(tempDir); + const testConfigPath: string = `${__dirname}/temp/pnpm-config-with-schema.json`; try { // Create config with $schema @@ -259,7 +256,7 @@ describe(PnpmOptionsConfiguration.name, () => { } } }; - JsonFile.save(configWithSchema, testConfigPath, { ensureFolderExists: true }); + await JsonFile.saveAsync(configWithSchema, testConfigPath, { ensureFolderExists: true }); const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( testConfigPath, From fe4d7847a042642c5364acdfdb0727f72a60339f Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Wed, 25 Feb 2026 19:41:34 -0800 Subject: [PATCH 18/25] Update libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts Co-authored-by: Ian Clanton-Thuon --- .../src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts index a222064a878..b92f4b6615c 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts @@ -280,16 +280,14 @@ describe(PnpmOptionsConfiguration.name, () => { }); it('handles undefined in updateGlobalOnlyBuiltDependenciesAsync', async () => { - const testConfigPath: string = path.join(__dirname, 'temp', 'pnpm-config-undefined-test.json'); - const tempDir: string = path.dirname(testConfigPath); - FileSystem.ensureFolder(tempDir); + const testConfigPath: string = `${__dirname}/temp/pnpm-config-undefined-test.json`; try { const initialConfig = { $schema: 'https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json', globalOnlyBuiltDependencies: ['node-gyp', 'esbuild'] }; - JsonFile.save(initialConfig, testConfigPath, { ensureFolderExists: true }); + await JsonFile.saveAsync(initialConfig, testConfigPath, { ensureFolderExists: true }); const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( testConfigPath, From fa9b0392fa707dd12a303644349ec66e142d8364 Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Wed, 25 Feb 2026 19:42:01 -0800 Subject: [PATCH 19/25] Update libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts Co-authored-by: Ian Clanton-Thuon --- .../src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts index b92f4b6615c..e0dc67845f0 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts @@ -315,9 +315,7 @@ describe(PnpmOptionsConfiguration.name, () => { ); expect(savedConfigJson.globalOnlyBuiltDependencies).toBeUndefined(); } finally { - if (FileSystem.exists(testConfigPath)) { - FileSystem.deleteFile(testConfigPath); - } + await FileSystem.deleteFileAsync(testConfigPath); } }); }); From 8d442964d1a0f8c3d39efe1c84060d16801d7b4b Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Wed, 25 Feb 2026 19:42:37 -0800 Subject: [PATCH 20/25] Update libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts Co-authored-by: Ian Clanton-Thuon --- .../src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts index e0dc67845f0..a1542bc0087 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts @@ -309,7 +309,7 @@ describe(PnpmOptionsConfiguration.name, () => { ); expect(reloadedConfig.globalOnlyBuiltDependencies).toBeUndefined(); - const savedConfigJson = JsonFile.load(testConfigPath); + const savedConfigJson = await JsonFile.loadAsync(testConfigPath); expect(savedConfigJson.$schema).toBe( 'https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json' ); From 384217080c20fefff8678cdf08256bb1ac13ba0d Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Wed, 25 Feb 2026 19:43:04 -0800 Subject: [PATCH 21/25] Update libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts Co-authored-by: Ian Clanton-Thuon --- .../src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts index a1542bc0087..2ad9036fc74 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts @@ -270,7 +270,7 @@ describe(PnpmOptionsConfiguration.name, () => { }; pnpmConfiguration.updateGlobalCatalogs(updatedCatalogs); - const savedConfig = JsonFile.load(testConfigPath); + const savedConfig = await JsonFile.loadAsync(testConfigPath); expect(savedConfig.$schema).toBe( 'https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json' ); From 93d85a5371cac68167e16f25ddce15b21e627e09 Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Wed, 25 Feb 2026 19:43:31 -0800 Subject: [PATCH 22/25] Update libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts Co-authored-by: Ian Clanton-Thuon --- .../src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts index 2ad9036fc74..f3890692258 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts @@ -165,9 +165,7 @@ describe(PnpmOptionsConfiguration.name, () => { expect(TestUtilities.stripAnnotations(reloadedConfig.globalCatalogs)).toEqual(updatedCatalogs); } finally { // Clean up - if (FileSystem.exists(testConfigPath)) { - FileSystem.deleteFile(testConfigPath); - } + await FileSystem.deleteFileAsync(testConfigPath); } }); From 1ffabcd1e4e396eb0b7c28c40fa2b4f014379395 Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Wed, 25 Feb 2026 19:44:41 -0800 Subject: [PATCH 23/25] Update libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts Co-authored-by: Ian Clanton-Thuon --- .../src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts index f3890692258..673f606bbff 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts @@ -125,11 +125,8 @@ describe(PnpmOptionsConfiguration.name, () => { }); describe('updateGlobalCatalogs', () => { - it('updates and saves globalCatalogs to pnpm-config.json', () => { - const testConfigPath: string = path.join(__dirname, 'temp', 'pnpm-config-update-test.json'); - - const tempDir: string = path.dirname(testConfigPath); - FileSystem.ensureFolder(tempDir); + it('updates and saves globalCatalogs to pnpm-config.json', async () => { + const testConfigPath: string = `${__dirname}/temp/pnpm-config-update-test.json`; try { const initialConfig = { @@ -139,7 +136,7 @@ describe(PnpmOptionsConfiguration.name, () => { } } }; - JsonFile.save(initialConfig, testConfigPath, { ensureFolderExists: true }); + await JsonFile.saveAsync(initialConfig, testConfigPath, { ensureFolderExists: true }); const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( testConfigPath, From 30064fe34b3664d5ec75d74683133f3ec20a0208 Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Wed, 25 Feb 2026 19:46:20 -0800 Subject: [PATCH 24/25] Update libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts Co-authored-by: Ian Clanton-Thuon --- .../rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts index f57efb948ae..032c9ea9683 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts @@ -67,10 +67,17 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { public static async loadCatalogsFromFileAsync( workspaceYamlFilename: string ): Promise> | undefined> { - if (!(await FileSystem.existsAsync(workspaceYamlFilename))) { - return undefined; + let content: string; + try { + content = await FileSystem.readFileAsync(workspaceYamlFilename); + } catch (error) { + if (FileSystem.isNotExistError(error)) { + return undefined; + } else { + throw error; + } } - const content: string = await FileSystem.readFileAsync(workspaceYamlFilename); + const parsed: IPnpmWorkspaceYaml | undefined = yamlModule.load(content) as IPnpmWorkspaceYaml | undefined; return parsed?.catalogs; From 3ee9066ff7adcacbce04808f66318b98b331e959 Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Wed, 25 Feb 2026 21:31:47 -0800 Subject: [PATCH 25/25] Code review feedback - test and code cleanup --- common/reviews/api/rush-lib.api.md | 2 +- libraries/rush-lib/config/heft.json | 2 +- .../src/cli/RushPnpmCommandLineParser.ts | 2 +- .../test/RushPnpmCommandLineParser.test.ts | 83 ++--- .../common/config/rush/pnpm-config.json | 8 + .../catalogSyncTestRepo/pnpm-workspace.yaml | 6 + .../cli/test/catalogSyncTestRepo/rush.json | 7 + .../logic/pnpm/PnpmOptionsConfiguration.ts | 22 +- .../src/logic/pnpm/PnpmWorkspaceFile.ts | 2 +- .../test/PnpmOptionsConfiguration.test.ts | 300 +++++++++--------- .../logic/pnpm/test/PnpmWorkspaceFile.test.ts | 4 +- 11 files changed, 209 insertions(+), 229 deletions(-) create mode 100644 libraries/rush-lib/src/cli/test/catalogSyncTestRepo/common/config/rush/pnpm-config.json create mode 100644 libraries/rush-lib/src/cli/test/catalogSyncTestRepo/pnpm-workspace.yaml create mode 100644 libraries/rush-lib/src/cli/test/catalogSyncTestRepo/rush.json diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index ae62715d680..b0169614eea 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -1183,7 +1183,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration readonly resolutionMode: PnpmResolutionMode | undefined; readonly strictPeerDependencies: boolean; readonly unsupportedPackageJsonSettings: unknown | undefined; - updateGlobalCatalogs(catalogs: Record> | undefined): void; + updateGlobalCatalogsAsync(catalogs: Record> | undefined): Promise; updateGlobalOnlyBuiltDependenciesAsync(onlyBuiltDependencies: string[] | undefined): Promise; updateGlobalPatchedDependencies(patchedDependencies: Record | undefined): void; readonly useWorkspaces: boolean; diff --git a/libraries/rush-lib/config/heft.json b/libraries/rush-lib/config/heft.json index d361c6eb55b..4f76fe45dcf 100644 --- a/libraries/rush-lib/config/heft.json +++ b/libraries/rush-lib/config/heft.json @@ -30,7 +30,7 @@ { "sourcePath": "src/cli/test", "destinationFolders": ["lib-intermediate-commonjs/cli/test"], - "fileExtensions": [".js"] + "fileExtensions": [".js", ".yaml"] }, { "sourcePath": "src/logic/pnpm/test", diff --git a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts index ad5bd538611..7b8c3d04606 100644 --- a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts +++ b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts @@ -610,7 +610,7 @@ export class RushPnpmCommandLineParser { pnpmOptions.globalCatalogs; if (!Objects.areDeepEqual(currentCatalogs, newCatalogs)) { - pnpmOptions.updateGlobalCatalogs(newCatalogs); + await pnpmOptions.updateGlobalCatalogsAsync(newCatalogs); this._terminal.writeWarningLine( `Rush refreshed the ${RushConstants.pnpmConfigFilename} with updated catalog definitions.\n` + diff --git a/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts b/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts index a536b9959af..f1e16883abb 100644 --- a/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts +++ b/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts @@ -6,58 +6,29 @@ import { FileSystem, JsonFile } from '@rushstack/node-core-library'; import { TestUtilities } from '@rushstack/heft-config-file'; import { RushConfiguration } from '../../api/RushConfiguration'; +const MONOREPO_ROOT: string = path.dirname( + RushConfiguration.tryFindRushJsonLocation({ startingFolder: __dirname })! +); +const CATALOG_SYNC_REPO_PATH: string = `${__dirname}/catalogSyncTestRepo`; + describe('RushPnpmCommandLineParser', () => { describe('catalog syncing', () => { - let testRepoPath: string; - let pnpmConfigPath: string; - let pnpmWorkspacePath: string; - - beforeEach(() => { - testRepoPath = path.join(__dirname, 'temp', 'catalog-sync-test-repo'); - FileSystem.ensureFolder(testRepoPath); - - const rushJsonPath: string = `${testRepoPath/rush.json`; - const rushJson = { - $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: [] - }; - JsonFile.save(rushJson, rushJsonPath, { ensureFolderExists: true }); - - const configDir: string = path.join(testRepoPath, 'common', 'config', 'rush'); - FileSystem.ensureFolder(configDir); - - pnpmConfigPath = path.join(configDir, 'pnpm-config.json'); - const pnpmConfig = { - globalCatalogs: { - default: { - react: '^18.0.0', - 'react-dom': '^18.0.0' - } - } - }; - JsonFile.save(pnpmConfig, pnpmConfigPath); + const testRepoPath: string = `${MONOREPO_ROOT}/temp/catalog-sync-test-repo`; + const pnpmConfigPath: string = `${testRepoPath}/common/config/rush/pnpm-config.json`; + const pnpmWorkspacePath: string = `${testRepoPath}/common/temp/pnpm-workspace.yaml`; - const tempDir: string = path.join(testRepoPath, 'common', 'temp'); - FileSystem.ensureFolder(tempDir); + beforeEach(async () => { + await FileSystem.copyFilesAsync({ sourcePath: CATALOG_SYNC_REPO_PATH, destinationPath: testRepoPath }); - pnpmWorkspacePath = path.join(tempDir, 'pnpm-workspace.yaml'); - const workspaceYaml = `packages: - - '../../apps/*' -catalogs: - default: - react: ^18.0.0 - react-dom: ^18.0.0 -`; - FileSystem.writeFile(pnpmWorkspacePath, workspaceYaml); + // common/temp is gitignored so it is not part of the static repo; copy the initial workspace file here + await FileSystem.copyFilesAsync({ + sourcePath: `${CATALOG_SYNC_REPO_PATH}/pnpm-workspace.yaml`, + destinationPath: pnpmWorkspacePath + }); }); - afterEach(() => { - if (FileSystem.exists(testRepoPath)) { - FileSystem.deleteFolder(testRepoPath); - } + afterEach(async () => { + await FileSystem.deleteFolderAsync(testRepoPath); }); it('syncs updated catalogs from pnpm-workspace.yaml to pnpm-config.json', async () => { @@ -71,10 +42,10 @@ catalogs: frontend: vue: ^3.4.0 `; - FileSystem.writeFile(pnpmWorkspacePath, updatedWorkspaceYaml); + await FileSystem.writeFileAsync(pnpmWorkspacePath, updatedWorkspaceYaml); const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile( - path.join(testRepoPath, 'rush.json') + `${testRepoPath}/rush.json` ); const subspace = rushConfiguration.getSubspace('default'); @@ -90,10 +61,10 @@ catalogs: const { PnpmWorkspaceFile } = require('../../logic/pnpm/PnpmWorkspaceFile'); const newCatalogs = await PnpmWorkspaceFile.loadCatalogsFromFileAsync(pnpmWorkspacePath); - pnpmOptions?.updateGlobalCatalogs(newCatalogs); + await pnpmOptions?.updateGlobalCatalogsAsync(newCatalogs); const updatedRushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile( - path.join(testRepoPath, 'rush.json') + `${testRepoPath}/rush.json` ); const updatedSubspace = updatedRushConfiguration.getSubspace('default'); const updatedPnpmOptions = updatedSubspace.getPnpmOptions(); @@ -115,12 +86,12 @@ catalogs: const newCatalogs = await PnpmWorkspaceFile.loadCatalogsFromFileAsync(pnpmWorkspacePath); const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile( - path.join(testRepoPath, 'rush.json') + `${testRepoPath}/rush.json` ); const subspace = rushConfiguration.getSubspace('default'); const pnpmOptions = subspace.getPnpmOptions(); - pnpmOptions?.updateGlobalCatalogs(newCatalogs); + await pnpmOptions?.updateGlobalCatalogsAsync(newCatalogs); const savedConfig = JsonFile.load(pnpmConfigPath); expect(savedConfig.globalCatalogs).toEqual({ @@ -135,7 +106,7 @@ catalogs: const workspaceWithoutCatalogs = `packages: - '../../apps/*' `; - FileSystem.writeFile(pnpmWorkspacePath, workspaceWithoutCatalogs); + await FileSystem.writeFileAsync(pnpmWorkspacePath, workspaceWithoutCatalogs); const { PnpmWorkspaceFile } = require('../../logic/pnpm/PnpmWorkspaceFile'); const newCatalogs = await PnpmWorkspaceFile.loadCatalogsFromFileAsync(pnpmWorkspacePath); @@ -143,15 +114,15 @@ catalogs: expect(newCatalogs).toBeUndefined(); const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile( - path.join(testRepoPath, 'rush.json') + `${testRepoPath}/rush.json` ); const subspace = rushConfiguration.getSubspace('default'); const pnpmOptions = subspace.getPnpmOptions(); - pnpmOptions?.updateGlobalCatalogs(newCatalogs); + await pnpmOptions?.updateGlobalCatalogsAsync(newCatalogs); const updatedRushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile( - path.join(testRepoPath, 'rush.json') + `${testRepoPath}/rush.json` ); const updatedSubspace = updatedRushConfiguration.getSubspace('default'); const updatedPnpmOptions = updatedSubspace.getPnpmOptions(); diff --git a/libraries/rush-lib/src/cli/test/catalogSyncTestRepo/common/config/rush/pnpm-config.json b/libraries/rush-lib/src/cli/test/catalogSyncTestRepo/common/config/rush/pnpm-config.json new file mode 100644 index 00000000000..afa5c0bf4c8 --- /dev/null +++ b/libraries/rush-lib/src/cli/test/catalogSyncTestRepo/common/config/rush/pnpm-config.json @@ -0,0 +1,8 @@ +{ + "globalCatalogs": { + "default": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + } +} diff --git a/libraries/rush-lib/src/cli/test/catalogSyncTestRepo/pnpm-workspace.yaml b/libraries/rush-lib/src/cli/test/catalogSyncTestRepo/pnpm-workspace.yaml new file mode 100644 index 00000000000..c807b81d31e --- /dev/null +++ b/libraries/rush-lib/src/cli/test/catalogSyncTestRepo/pnpm-workspace.yaml @@ -0,0 +1,6 @@ +packages: + - '../../apps/*' +catalogs: + default: + react: ^18.0.0 + react-dom: ^18.0.0 diff --git a/libraries/rush-lib/src/cli/test/catalogSyncTestRepo/rush.json b/libraries/rush-lib/src/cli/test/catalogSyncTestRepo/rush.json new file mode 100644 index 00000000000..90cd8a844c4 --- /dev/null +++ b/libraries/rush-lib/src/cli/test/catalogSyncTestRepo/rush.json @@ -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": [] +} diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts index cb22029d1c5..efcfccc50a0 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts @@ -535,9 +535,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration public updateGlobalPatchedDependencies(patchedDependencies: Record | 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 }); } /** @@ -547,18 +545,22 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration onlyBuiltDependencies: string[] | undefined ): Promise { this._json.globalOnlyBuiltDependencies = onlyBuiltDependencies; - if (this.jsonFilename) { - await JsonFile.saveAsync(this._json, this.jsonFilename, { updateExistingFile: true, ignoreUndefinedValues: true }); - } + 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 updateGlobalCatalogs(catalogs: Record> | undefined): void { + public async updateGlobalCatalogsAsync( + catalogs: Record> | undefined + ): Promise { this._json.globalCatalogs = catalogs; - if (this.jsonFilename) { - JsonFile.save(this._json, this.jsonFilename, { updateExistingFile: true, ignoreUndefinedValues: true }); - } + await JsonFile.saveAsync(this._json, this.jsonFilename as string, { + updateExistingFile: true, + ignoreUndefinedValues: true + }); } } diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts index 032c9ea9683..fd745ce5c97 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts @@ -77,7 +77,7 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { throw error; } } - + const parsed: IPnpmWorkspaceYaml | undefined = yamlModule.load(content) as IPnpmWorkspaceYaml | undefined; return parsed?.catalogs; diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts index 673f606bbff..56ddb32e90b 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts @@ -5,10 +5,20 @@ 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'; +import { RushConfiguration } from '../../../api/RushConfiguration'; -const fakeCommonTempFolder: string = path.join(__dirname, 'common', 'temp'); +const MONOREPO_ROOT: string = path.dirname( + RushConfiguration.tryFindRushJsonLocation({ startingFolder: __dirname })! +); +const TEST_TEMP_FOLDER: string = `${MONOREPO_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( @@ -126,192 +136,168 @@ describe(PnpmOptionsConfiguration.name, () => { describe('updateGlobalCatalogs', () => { it('updates and saves globalCatalogs to pnpm-config.json', async () => { - const testConfigPath: string = `${__dirname}/temp/pnpm-config-update-test.json`; - - try { - const initialConfig = { - globalCatalogs: { - default: { - react: '^18.0.0' - } - } - }; - await JsonFile.saveAsync(initialConfig, testConfigPath, { ensureFolderExists: true }); + const testConfigPath: string = `${TEST_TEMP_FOLDER}/pnpm-config-update-test.json`; - const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( - testConfigPath, - fakeCommonTempFolder - ); - - const updatedCatalogs = { + const initialConfig = { + globalCatalogs: { default: { - react: '^18.2.0', - 'react-dom': '^18.2.0' - }, - frontend: { - vue: '^3.4.0' + react: '^18.0.0' } - }; - pnpmConfiguration.updateGlobalCatalogs(updatedCatalogs); - - const reloadedConfig: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( - testConfigPath, - fakeCommonTempFolder - ); - - expect(TestUtilities.stripAnnotations(reloadedConfig.globalCatalogs)).toEqual(updatedCatalogs); - } finally { - // Clean up - await FileSystem.deleteFileAsync(testConfigPath); - } + } + }; + 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 = `${__dirname}/temp/pnpm-config-undefined-test.json`; - - try { - const initialConfig = { - globalCatalogs: { - default: { - react: '^18.0.0' - } + 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 }); + } + }; + await JsonFile.saveAsync(initialConfig, testConfigPath, { ensureFolderExists: true }); - const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( - testConfigPath, - fakeCommonTempFolder - ); + const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( + testConfigPath, + fakeCommonTempFolder + ); - pnpmConfiguration.updateGlobalCatalogs(undefined); + await pnpmConfiguration.updateGlobalCatalogsAsync(undefined); - const reloadedConfig: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( - testConfigPath, - fakeCommonTempFolder - ); + const reloadedConfig: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( + testConfigPath, + fakeCommonTempFolder + ); - expect(reloadedConfig.globalCatalogs).toBeUndefined(); - } finally { - await FileSystem.deleteFileAsync(testConfigPath); - } + expect(reloadedConfig.globalCatalogs).toBeUndefined(); }); }); describe('$schema handling', () => { it('does not fail when $schema is undefined', async () => { - const testConfigPath: string = `${__dirname}/temp/pnpm-config-no-schema.json`; - - try { - const configWithoutSchema = { - globalCatalogs: { - default: { - react: '^18.0.0' - } - } - }; - await JsonFile.saveAsync(configWithoutSchema, testConfigPath, { ensureFolderExists: true }); - - const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( - testConfigPath, - fakeCommonTempFolder - ); + const testConfigPath: string = `${TEST_TEMP_FOLDER}/pnpm-config-no-schema.json`; - const updatedCatalogs = { + const configWithoutSchema = { + globalCatalogs: { default: { - react: '^18.2.0' + react: '^18.0.0' } - }; + } + }; + await JsonFile.saveAsync(configWithoutSchema, testConfigPath, { ensureFolderExists: true }); + + const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( + testConfigPath, + fakeCommonTempFolder + ); - expect(() => { - pnpmConfiguration.updateGlobalCatalogs(updatedCatalogs); - }).not.toThrow(); + const updatedCatalogs = { + default: { + react: '^18.2.0' + } + }; - const reloadedConfig: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( - testConfigPath, - fakeCommonTempFolder - ); + await expect(pnpmConfiguration.updateGlobalCatalogsAsync(updatedCatalogs)).resolves.not.toThrow(); - expect(TestUtilities.stripAnnotations(reloadedConfig.globalCatalogs)).toEqual(updatedCatalogs); - } finally { - await FileSystem.deleteFile(testConfigPath); - } - }); + const reloadedConfig: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( + testConfigPath, + fakeCommonTempFolder + ); - it('preserves $schema when it exists', () => { - const testConfigPath: string = `${__dirname}/temp/pnpm-config-with-schema.json`; - - try { - // Create config with $schema - 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 }); + expect(TestUtilities.stripAnnotations(reloadedConfig.globalCatalogs)).toEqual(updatedCatalogs); + }); - const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( - testConfigPath, - fakeCommonTempFolder - ); + it('preserves $schema when it exists', async () => { + const testConfigPath: string = `${TEST_TEMP_FOLDER}/pnpm-config-with-schema.json`; - const updatedCatalogs = { + const configWithSchema = { + $schema: 'https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json', + globalCatalogs: { default: { - react: '^18.2.0' + react: '^18.0.0' } - }; - pnpmConfiguration.updateGlobalCatalogs(updatedCatalogs); - - const savedConfig = await JsonFile.loadAsync(testConfigPath); - expect(savedConfig.$schema).toBe( - 'https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json' - ); - } finally { - await FileSystem.deleteFile(testConfigPath); - } + } + }; + 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 = `${__dirname}/temp/pnpm-config-undefined-test.json`; - - try { - 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(); - } finally { - await FileSystem.deleteFileAsync(testConfigPath); - } + 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(); }); }); }); diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts index a3a43edb319..44bfa8a687d 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts @@ -31,7 +31,7 @@ describe(PnpmWorkspaceFile.name, () => { // Mock FileSystem.readFile to return the written content mockReadFile = jest.spyOn(FileSystem, 'readFile').mockImplementation(() => { if (writtenContent === undefined) { - throw new Error('File not found'); + throw Object.assign(new Error('ENOENT: no such file or directory'), { code: 'ENOENT', errno: -2, syscall: 'open' }); } return writtenContent; }); @@ -39,7 +39,7 @@ describe(PnpmWorkspaceFile.name, () => { // Mock async version for loadCatalogsFromFileAsync mockReadFileAsync = jest.spyOn(FileSystem, 'readFileAsync').mockImplementation(async () => { if (writtenContent === undefined) { - throw new Error('File not found'); + throw Object.assign(new Error('ENOENT: no such file or directory'), { code: 'ENOENT', errno: -2, syscall: 'open' }); } return writtenContent; });