From f030ce09ffe07fc73fad37b5ecd7b65cc24ba1c7 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Wed, 15 Jul 2026 18:52:49 -0400 Subject: [PATCH 1/5] Clean up InstallHelpers tests. --- .../src/logic/test/InstallHelpers.test.ts | 21 +++++------- .../__snapshots__/InstallHelpers.test.ts.snap | 34 +++++++++++++++++++ 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts index c6441d74840..0e111e9799c 100644 --- a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts +++ b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts @@ -3,20 +3,18 @@ import { type IPackageJson, JsonFile } from '@rushstack/node-core-library'; import { StringBufferTerminalProvider, Terminal } from '@rushstack/terminal'; -import { TestUtilities } from '@rushstack/heft-config-file'; import { InstallHelpers } from '../installManager/InstallHelpers'; import { RushConfiguration } from '../../api/RushConfiguration'; -describe('InstallHelpers', () => { - describe('generateCommonPackageJson', () => { - const originalJsonFileSave = JsonFile.save; - const mockJsonFileSave: jest.Mock = jest.fn(); +describe(InstallHelpers.name, () => { + describe(InstallHelpers.generateCommonPackageJson.name, () => { + let mockJsonFileSave: jest.SpyInstance; let terminal: Terminal; let terminalProvider: StringBufferTerminalProvider; beforeAll(() => { - JsonFile.save = mockJsonFileSave; + mockJsonFileSave = jest.spyOn(JsonFile, 'save').mockImplementation(() => true); }); beforeEach(() => { @@ -34,10 +32,6 @@ describe('InstallHelpers', () => { mockJsonFileSave.mockClear(); }); - afterAll(() => { - JsonFile.save = originalJsonFileSave; - }); - it('generates correct package json with pnpm configurations', () => { const RUSH_JSON_FILENAME: string = `${__dirname}/pnpmConfig/rush.json`; const rushConfiguration: RushConfiguration = @@ -48,8 +42,10 @@ describe('InstallHelpers', () => { undefined, terminal ); - const packageJson: IPackageJson = mockJsonFileSave.mock.calls[0][0]; - expect(TestUtilities.stripAnnotations(packageJson)).toEqual( + const packageJson: IPackageJson = JSON.parse( + JsonFile.stringify(mockJsonFileSave.mock.calls[0][0], { ignoreUndefinedValues: true }) + ); + expect(packageJson).toEqual( expect.objectContaining({ pnpm: { overrides: { @@ -71,6 +67,7 @@ describe('InstallHelpers', () => { } }) ); + expect(packageJson).toMatchSnapshot(); }); }); }); diff --git a/libraries/rush-lib/src/logic/test/__snapshots__/InstallHelpers.test.ts.snap b/libraries/rush-lib/src/logic/test/__snapshots__/InstallHelpers.test.ts.snap index 080d68c7baf..fd822686df2 100644 --- a/libraries/rush-lib/src/logic/test/__snapshots__/InstallHelpers.test.ts.snap +++ b/libraries/rush-lib/src/logic/test/__snapshots__/InstallHelpers.test.ts.snap @@ -1,3 +1,37 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +exports[`InstallHelpers generateCommonPackageJson generates correct package json with pnpm configurations 1`] = ` +Object { + "dependencies": Object {}, + "description": "Temporary file generated by the Rush tool", + "name": "rush-common", + "pnpm": Object { + "neverBuiltDependencies": Array [ + "fsevents", + "level", + ], + "onlyBuiltDependencies": Array [ + "esbuild", + "playwright", + ], + "overrides": Object { + "bar@^2.1.0": "3.0.0", + "foo": "^2.0.0", + "qar@1>zoo": "2", + "quux": "npm:@myorg/quux@^1.0.0", + }, + "packageExtensions": Object { + "react-redux": Object { + "peerDependencies": Object { + "react-dom": "*", + }, + }, + }, + "pnpmFutureFeature": true, + }, + "private": true, + "version": "0.0.0", +} +`; + exports[`InstallHelpers generateCommonPackageJson generates correct package json with pnpm configurations: Terminal Output 1`] = `Array []`; From cf2125473e0102bd37d32355509c960e560238b0 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Wed, 15 Jul 2026 19:01:51 -0400 Subject: [PATCH 2/5] Clean up workspace file generation. --- .../src/logic/base/BaseWorkspaceFile.ts | 25 +- .../installManager/WorkspaceInstallManager.ts | 2 +- .../src/logic/pnpm/PnpmWorkspaceFile.ts | 11 +- .../logic/pnpm/test/PnpmWorkspaceFile.test.ts | 213 +++++++----------- 4 files changed, 103 insertions(+), 148 deletions(-) diff --git a/libraries/rush-lib/src/logic/base/BaseWorkspaceFile.ts b/libraries/rush-lib/src/logic/base/BaseWorkspaceFile.ts index e3546d58ed5..15900596233 100644 --- a/libraries/rush-lib/src/logic/base/BaseWorkspaceFile.ts +++ b/libraries/rush-lib/src/logic/base/BaseWorkspaceFile.ts @@ -26,32 +26,33 @@ export abstract class BaseWorkspaceFile { /** * Serializes and saves the workspace file to specified location */ - public save(filePath: string, options: IWorkspaceFileSaveOptions): void { + public async saveAsync(filePath: string, options: IWorkspaceFileSaveOptions): Promise { + const { onlyIfChanged, ensureFolderExists } = options; + // Do we need to read the previous file contents? - let oldBuffer: Buffer | undefined = undefined; - if (options.onlyIfChanged && FileSystem.exists(filePath)) { + let oldBuffer: Buffer | undefined; + if (onlyIfChanged) { try { - oldBuffer = FileSystem.readFileToBuffer(filePath); + oldBuffer = await FileSystem.readFileToBufferAsync(filePath); } catch (error) { // Ignore this error, and try writing a new file. If that fails, then we should report that // error instead. } } - const newYaml: string = this.serialize(); - - const newBuffer: Buffer = Buffer.from(newYaml); // utf8 encoding happens here + const newContent: string = await this.serializeAsync(); + const newBuffer: Buffer = Buffer.from(newContent); // utf8 encoding happens here - if (options.onlyIfChanged) { + if (oldBuffer) { // Has the file changed? - if (oldBuffer && Buffer.compare(newBuffer, oldBuffer) === 0) { + if (Buffer.compare(newBuffer, oldBuffer) === 0) { // Nothing has changed, so don't touch the file return; } } - FileSystem.writeFile(filePath, newBuffer.toString(), { - ensureFolderExists: options.ensureFolderExists + await FileSystem.writeFileAsync(filePath, newBuffer.toString(), { + ensureFolderExists }); } @@ -63,5 +64,5 @@ export abstract class BaseWorkspaceFile { public abstract addPackage(packagePath: string): void; /** @virtual */ - protected abstract serialize(): string; + protected abstract serializeAsync(): Promise; } diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index 24029bc41cc..f6f3ff5d449 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -535,7 +535,7 @@ export class WorkspaceInstallManager extends BaseInstallManager { // Save the generated workspace file. Don't update the file timestamp unless the content has changed, // since "rush install" will consider this timestamp - workspaceFile.save(workspaceFile.workspaceFilename, { onlyIfChanged: true }); + await workspaceFile.saveAsync(workspaceFile.workspaceFilename, { onlyIfChanged: true }); return { shrinkwrapIsUpToDate, shrinkwrapWarnings }; } diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts index 5289b01eac7..839b97b92a2 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts @@ -5,13 +5,11 @@ import * as path from 'node:path'; import { escapePath as globEscape } from 'fast-glob'; -import { Sort, Import, Path } from '@rushstack/node-core-library'; +import { Sort, Path } from '@rushstack/node-core-library'; import { BaseWorkspaceFile } from '../base/BaseWorkspaceFile'; import { PNPM_SHRINKWRAP_YAML_FORMAT } from './PnpmYamlCommon'; -const yamlModule: typeof import('js-yaml') = Import.lazy('js-yaml', require); - /** * This interface represents the raw pnpm-workspace.YAML file * Example: @@ -118,8 +116,7 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { this._minimumReleaseAgeExclude = minimumReleaseAgeExclude; } - /** @override */ - public addPackage(packagePath: string): void { + public override addPackage(packagePath: string): void { // Ensure the path is relative to the pnpm-workspace.yaml file if (path.isAbsolute(packagePath)) { packagePath = path.relative(path.dirname(this.workspaceFilename), packagePath); @@ -130,8 +127,7 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { this._workspacePackages.add(globEscape(globPath)); } - /** @override */ - protected serialize(): string { + protected override async serializeAsync(): Promise { // Ensure stable sort order when serializing Sort.sortSet(this._workspacePackages); @@ -151,6 +147,7 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { workspaceYaml.minimumReleaseAge = this._minimumReleaseAge; workspaceYaml.minimumReleaseAgeExclude = this._minimumReleaseAgeExclude; + const yamlModule: typeof import('js-yaml') = await import('js-yaml'); return yamlModule.dump(workspaceYaml, PNPM_SHRINKWRAP_YAML_FORMAT); } } 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 54d4bc8bb0e..81f51b75414 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts @@ -1,78 +1,52 @@ // 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 } from '@rushstack/node-core-library'; import { PnpmWorkspaceFile } from '../PnpmWorkspaceFile'; describe(PnpmWorkspaceFile.name, () => { - const tempDir: string = path.join(__dirname, 'temp'); - const workspaceFilePath: string = path.join(tempDir, 'pnpm-workspace.yaml'); - const projectsDir: string = path.join(tempDir, 'projects'); + const tempDir: string = `${__dirname}/temp`; + const workspaceFilePath: string = `${tempDir}/pnpm-workspace.yaml`; + const projectsDir: string = `${tempDir}/projects`; - let mockWriteFile: jest.SpyInstance; - let mockReadFile: jest.SpyInstance; - let mockExists: jest.SpyInstance; let writtenContent: string | undefined; beforeEach(() => { writtenContent = undefined; // Mock FileSystem.writeFile to capture content instead of writing to disk - mockWriteFile = jest - .spyOn(FileSystem, 'writeFile') - .mockImplementation((filePath: string, contents: string | Buffer) => { - void filePath; // Unused parameter - writtenContent = typeof contents === 'string' ? contents : contents.toString(); + jest + .spyOn(FileSystem, 'writeFileAsync') + .mockImplementation(async (filePath: string, contents: string | Buffer) => { + writtenContent = String(contents); }); - - // Mock FileSystem.readFile to return the written content - mockReadFile = jest.spyOn(FileSystem, 'readFile').mockImplementation(() => { - 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; - }); - }); - - afterEach(() => { - mockWriteFile.mockRestore(); - mockReadFile.mockRestore(); - mockExists.mockRestore(); }); describe('basic functionality', () => { - it('generates workspace file with packages only', () => { + it('generates workspace file with packages only', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage(path.join(projectsDir, 'app1')); - workspaceFile.addPackage(path.join(projectsDir, 'app2')); + workspaceFile.addPackage(`${projectsDir}/app2`); + workspaceFile.addPackage(`${projectsDir}/app1`); - workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); - const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).toMatchSnapshot(); + expect(writtenContent).toMatchSnapshot(); }); - it('escapes special characters in package paths', () => { + it('escapes special characters in package paths', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage(path.join(projectsDir, '[app-with-brackets]')); + workspaceFile.addPackage(`${projectsDir}/[app-with-brackets]`); - workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); - const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).toContain('\\[app-with-brackets\\]'); + expect(writtenContent).toContain('\\[app-with-brackets\\]'); }); }); describe('catalog functionality', () => { - it('generates workspace file with default catalog only', () => { + it('generates workspace file with default catalog only', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage(path.join(projectsDir, 'app1')); + workspaceFile.addPackage(`${projectsDir}/app1`); workspaceFile.setCatalogs({ default: { @@ -82,15 +56,14 @@ describe(PnpmWorkspaceFile.name, () => { } }); - workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); - const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).toMatchSnapshot(); + expect(writtenContent).toMatchSnapshot(); }); - it('generates workspace file with named catalogs', () => { + it('generates workspace file with named catalogs', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage(path.join(projectsDir, 'app1')); + workspaceFile.addPackage(`${projectsDir}/app1`); workspaceFile.setCatalogs({ default: { @@ -106,39 +79,36 @@ describe(PnpmWorkspaceFile.name, () => { } }); - workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); - const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).toMatchSnapshot(); + expect(writtenContent).toMatchSnapshot(); }); - it('handles empty catalog object', () => { + it('handles empty catalog object', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage(path.join(projectsDir, 'app1')); + workspaceFile.addPackage(`${projectsDir}/app1`); workspaceFile.setCatalogs({}); - workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); - const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).toMatchSnapshot(); + expect(writtenContent).toMatchSnapshot(); }); - it('handles undefined catalog', () => { + it('handles undefined catalog', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage(path.join(projectsDir, 'app1')); + workspaceFile.addPackage(`${projectsDir}/app1`); workspaceFile.setCatalogs(undefined); - workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); - const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).toMatchSnapshot(); + expect(writtenContent).toMatchSnapshot(); }); - it('handles scoped packages in catalogs', () => { + it('handles scoped packages in catalogs', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage(path.join(projectsDir, 'app1')); + workspaceFile.addPackage(`${projectsDir}/app1`); workspaceFile.setCatalogs({ default: { @@ -148,15 +118,14 @@ describe(PnpmWorkspaceFile.name, () => { } }); - workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); - const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).toMatchSnapshot(); + expect(writtenContent).toMatchSnapshot(); }); - it('can update catalogs after initial creation', () => { + it('can update catalogs after initial creation', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage(path.join(projectsDir, 'app1')); + workspaceFile.addPackage(`${projectsDir}/app1`); workspaceFile.setCatalogs({ default: { @@ -164,7 +133,7 @@ describe(PnpmWorkspaceFile.name, () => { } }); - workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); // Update catalogs workspaceFile.setCatalogs({ @@ -174,17 +143,16 @@ describe(PnpmWorkspaceFile.name, () => { } }); - workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); - const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).toMatchSnapshot(); + expect(writtenContent).toMatchSnapshot(); }); }); describe('allowBuilds functionality', () => { - it('generates workspace file with allowBuilds', () => { + it('generates workspace file with allowBuilds', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage(path.join(projectsDir, 'app1')); + workspaceFile.addPackage(`${projectsDir}/app1`); workspaceFile.setAllowBuilds({ esbuild: true, @@ -192,15 +160,14 @@ describe(PnpmWorkspaceFile.name, () => { fsevents: false }); - workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); - const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).toMatchSnapshot(); + expect(writtenContent).toMatchSnapshot(); }); - it('generates workspace file with allowBuilds and catalogs', () => { + it('generates workspace file with allowBuilds and catalogs', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage(path.join(projectsDir, 'app1')); + workspaceFile.addPackage(`${projectsDir}/app1`); workspaceFile.setCatalogs({ default: { @@ -212,122 +179,112 @@ describe(PnpmWorkspaceFile.name, () => { esbuild: true }); - workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); - const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).toMatchSnapshot(); + expect(writtenContent).toMatchSnapshot(); }); - it('handles empty allowBuilds object', () => { + it('handles empty allowBuilds object', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage(path.join(projectsDir, 'app1')); + workspaceFile.addPackage(`${projectsDir}/app1`); workspaceFile.setAllowBuilds({}); - workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); - const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).not.toContain('allowBuilds'); + expect(writtenContent).not.toContain('allowBuilds'); }); - it('handles undefined allowBuilds', () => { + it('handles undefined allowBuilds', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage(path.join(projectsDir, 'app1')); + workspaceFile.addPackage(`${projectsDir}/app1`); workspaceFile.setAllowBuilds(undefined); - workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); - const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).not.toContain('allowBuilds'); + expect(writtenContent).not.toContain('allowBuilds'); }); }); describe('minimumReleaseAge functionality', () => { - it('generates workspace file with minimumReleaseAge', () => { + it('generates workspace file with minimumReleaseAge', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage(path.join(projectsDir, 'app1')); + workspaceFile.addPackage(`${projectsDir}/app1`); workspaceFile.setMinimumReleaseAge(20160); - workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); - const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).toMatchSnapshot(); + expect(writtenContent).toMatchSnapshot(); }); - it('generates workspace file with minimumReleaseAge and minimumReleaseAgeExclude', () => { + it('generates workspace file with minimumReleaseAge and minimumReleaseAgeExclude', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage(path.join(projectsDir, 'app1')); + workspaceFile.addPackage(`${projectsDir}/app1`); workspaceFile.setMinimumReleaseAge(1440); workspaceFile.setMinimumReleaseAgeExclude(['webpack', '@myorg/*']); - workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); - const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).toMatchSnapshot(); + expect(writtenContent).toMatchSnapshot(); }); - it('generates workspace file with minimumReleaseAgeExclude only', () => { + it('generates workspace file with minimumReleaseAgeExclude only', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage(path.join(projectsDir, 'app1')); + workspaceFile.addPackage(`${projectsDir}/app1`); workspaceFile.setMinimumReleaseAgeExclude(['webpack']); - workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); - const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).toMatchSnapshot(); + expect(writtenContent).toMatchSnapshot(); }); - it('handles zero value for minimumReleaseAge', () => { + it('handles zero value for minimumReleaseAge', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage(path.join(projectsDir, 'app1')); + workspaceFile.addPackage(`${projectsDir}/app1`); workspaceFile.setMinimumReleaseAge(0); - workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); - const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).toContain('minimumReleaseAge: 0'); + expect(writtenContent).toContain('minimumReleaseAge: 0'); }); - it('handles undefined minimumReleaseAge', () => { + it('handles undefined minimumReleaseAge', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage(path.join(projectsDir, 'app1')); + workspaceFile.addPackage(`${projectsDir}/app1`); workspaceFile.setMinimumReleaseAge(undefined); workspaceFile.setMinimumReleaseAgeExclude(undefined); - workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); - const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).not.toContain('minimumReleaseAge'); + expect(writtenContent).not.toContain('minimumReleaseAge'); }); - it('passes through an explicitly-set empty minimumReleaseAgeExclude', () => { + it('passes through an explicitly-set empty minimumReleaseAgeExclude', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage(path.join(projectsDir, 'app1')); + workspaceFile.addPackage(`${projectsDir}/app1`); workspaceFile.setMinimumReleaseAgeExclude([]); - workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); - const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).toContain('minimumReleaseAgeExclude: []'); + expect(writtenContent).toContain('minimumReleaseAgeExclude: []'); }); - it('omits an undefined minimumReleaseAgeExclude', () => { + it('omits an undefined minimumReleaseAgeExclude', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage(path.join(projectsDir, 'app1')); + workspaceFile.addPackage(`${projectsDir}/app1`); workspaceFile.setMinimumReleaseAgeExclude(undefined); - workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); - const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).not.toContain('minimumReleaseAgeExclude'); + expect(writtenContent).not.toContain('minimumReleaseAgeExclude'); }); }); }); From 437b4e7984e23ba18268a2478db1e62600dc1b42 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Wed, 15 Jul 2026 19:02:38 -0400 Subject: [PATCH 3/5] Clean up InstallHelpers. --- .../logic/installManager/InstallHelpers.ts | 257 +++++++++--------- .../installManager/RushInstallManager.ts | 2 +- .../installManager/WorkspaceInstallManager.ts | 7 +- .../src/logic/test/InstallHelpers.test.ts | 14 +- .../__snapshots__/InstallHelpers.test.ts.snap | 4 +- 5 files changed, 138 insertions(+), 146 deletions(-) diff --git a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts index 9dfa031b72a..a46610a7921 100644 --- a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts +++ b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'node:path'; - import * as semver from 'semver'; import { @@ -25,59 +23,56 @@ import { merge } from '../../utilities/objectUtilities'; import type { Subspace } from '../../api/Subspace'; import { RushConstants } from '../RushConstants'; +interface ICommonPackageJsonPnpmSection { + overrides?: typeof PnpmOptionsConfiguration.prototype.globalOverrides; + packageExtensions?: typeof PnpmOptionsConfiguration.prototype.globalPackageExtensions; + peerDependencyRules?: typeof PnpmOptionsConfiguration.prototype.globalPeerDependencyRules; + neverBuiltDependencies?: typeof PnpmOptionsConfiguration.prototype.globalNeverBuiltDependencies; + onlyBuiltDependencies?: typeof PnpmOptionsConfiguration.prototype.globalOnlyBuiltDependencies; + ignoredOptionalDependencies?: typeof PnpmOptionsConfiguration.prototype.globalIgnoredOptionalDependencies; + allowedDeprecatedVersions?: typeof PnpmOptionsConfiguration.prototype.globalAllowedDeprecatedVersions; + patchedDependencies?: typeof PnpmOptionsConfiguration.prototype.globalPatchedDependencies; + trustPolicy?: typeof PnpmOptionsConfiguration.prototype.trustPolicy; + trustPolicyExclude?: typeof PnpmOptionsConfiguration.prototype.trustPolicyExclude; + trustPolicyIgnoreAfter?: typeof PnpmOptionsConfiguration.prototype.trustPolicyIgnoreAfterMinutes; +} + interface ICommonPackageJson extends IPackageJson { - pnpm?: { - overrides?: typeof PnpmOptionsConfiguration.prototype.globalOverrides; - packageExtensions?: typeof PnpmOptionsConfiguration.prototype.globalPackageExtensions; - peerDependencyRules?: typeof PnpmOptionsConfiguration.prototype.globalPeerDependencyRules; - neverBuiltDependencies?: typeof PnpmOptionsConfiguration.prototype.globalNeverBuiltDependencies; - onlyBuiltDependencies?: typeof PnpmOptionsConfiguration.prototype.globalOnlyBuiltDependencies; - ignoredOptionalDependencies?: typeof PnpmOptionsConfiguration.prototype.globalIgnoredOptionalDependencies; - allowedDeprecatedVersions?: typeof PnpmOptionsConfiguration.prototype.globalAllowedDeprecatedVersions; - patchedDependencies?: typeof PnpmOptionsConfiguration.prototype.globalPatchedDependencies; - trustPolicy?: typeof PnpmOptionsConfiguration.prototype.trustPolicy; - trustPolicyExclude?: typeof PnpmOptionsConfiguration.prototype.trustPolicyExclude; - trustPolicyIgnoreAfter?: typeof PnpmOptionsConfiguration.prototype.trustPolicyIgnoreAfterMinutes; - }; + pnpm?: ICommonPackageJsonPnpmSection; } export class InstallHelpers { - public static generateCommonPackageJson( + public static async generateCommonPackageJsonAsync( rushConfiguration: RushConfiguration, subspace: Subspace, - dependencies: Map = new Map(), + dependenciesMap: Map = new Map(), terminal: ITerminal - ): void { - const commonPackageJson: ICommonPackageJson = { - dependencies: {}, - description: 'Temporary file generated by the Rush tool', - name: 'rush-common', - private: true, - version: '0.0.0' - }; - + ): Promise { + let pnpmSection: ICommonPackageJsonPnpmSection | undefined; + let additionalCommonPackageJsonPropertiesToMerge: unknown | undefined; if (rushConfiguration.isPnpm) { - const pnpmOptions: PnpmOptionsConfiguration = - subspace.getPnpmOptions() || rushConfiguration.pnpmOptions; - if (!commonPackageJson.pnpm) { - commonPackageJson.pnpm = {}; - } - - if (pnpmOptions.globalOverrides) { - commonPackageJson.pnpm.overrides = pnpmOptions.globalOverrides; - } - - if (pnpmOptions.globalPackageExtensions) { - commonPackageJson.pnpm.packageExtensions = pnpmOptions.globalPackageExtensions; - } - if (pnpmOptions.globalPeerDependencyRules) { - commonPackageJson.pnpm.peerDependencyRules = pnpmOptions.globalPeerDependencyRules; - } + const { + globalOverrides: overrides, + globalPackageExtensions: packageExtensions, + globalPeerDependencyRules: peerDependencyRules, + globalNeverBuiltDependencies, + globalOnlyBuiltDependencies, + globalIgnoredOptionalDependencies: ignoredOptionalDependencies, + globalAllowedDeprecatedVersions: allowedDeprecatedVersions, + globalPatchedDependencies: patchedDependencies, + trustPolicy, + trustPolicyExclude, + // NOTE: the pnpm setting is `trustPolicyIgnoreAfter`, but the rush pnpm setting is `trustPolicyIgnoreAfterMinutes` + trustPolicyIgnoreAfterMinutes: trustPolicyIgnoreAfter, + unsupportedPackageJsonSettings + } = subspace.getPnpmOptions() || rushConfiguration.pnpmOptions; const pnpmVersion: string = rushConfiguration.packageManagerToolVersion; + const isPnpm11: boolean = semver.gte(pnpmVersion, '11.0.0'); - if (pnpmOptions.globalNeverBuiltDependencies) { - if (semver.gte(pnpmVersion, '11.0.0')) { + let neverBuiltDependencies: ICommonPackageJsonPnpmSection['neverBuiltDependencies']; + if (globalNeverBuiltDependencies) { + if (isPnpm11) { terminal.writeWarningLine( Colorize.yellow( `Your version of PNPM (${pnpmVersion}) ` + @@ -87,12 +82,13 @@ export class InstallHelpers { ) ); } else { - commonPackageJson.pnpm.neverBuiltDependencies = pnpmOptions.globalNeverBuiltDependencies; + neverBuiltDependencies = globalNeverBuiltDependencies; } } - if (pnpmOptions.globalOnlyBuiltDependencies) { - if (semver.gte(pnpmVersion, '11.0.0')) { + let onlyBuiltDependencies: ICommonPackageJsonPnpmSection['onlyBuiltDependencies']; + if (globalOnlyBuiltDependencies) { + if (isPnpm11) { terminal.writeWarningLine( Colorize.yellow( `Your version of PNPM (${pnpmVersion}) ` + @@ -113,99 +109,99 @@ export class InstallHelpers { ); } - commonPackageJson.pnpm.onlyBuiltDependencies = pnpmOptions.globalOnlyBuiltDependencies; + onlyBuiltDependencies = globalOnlyBuiltDependencies; } } - if (pnpmOptions.globalIgnoredOptionalDependencies) { - if (semver.lt(pnpmVersion, '9.0.0')) { - terminal.writeWarningLine( - Colorize.yellow( - `Your version of PNPM (${pnpmVersion}) ` + - `doesn't support the "globalIgnoredOptionalDependencies" field in ` + - `${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` + - 'Remove this field or upgrade to PNPM 9.' - ) - ); - } - - commonPackageJson.pnpm.ignoredOptionalDependencies = pnpmOptions.globalIgnoredOptionalDependencies; + if (ignoredOptionalDependencies && semver.lt(pnpmVersion, '9.0.0')) { + terminal.writeWarningLine( + Colorize.yellow( + `Your version of PNPM (${pnpmVersion}) ` + + `doesn't support the "globalIgnoredOptionalDependencies" field in ` + + `${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` + + 'Remove this field or upgrade to PNPM 9.' + ) + ); } - if (pnpmOptions.globalAllowedDeprecatedVersions) { - commonPackageJson.pnpm.allowedDeprecatedVersions = pnpmOptions.globalAllowedDeprecatedVersions; + if (trustPolicy !== undefined && semver.lt(pnpmVersion, '10.21.0')) { + terminal.writeWarningLine( + Colorize.yellow( + `Your version of PNPM (${pnpmVersion}) ` + + `doesn't support the "trustPolicy" field in ` + + `${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` + + 'Remove this field or upgrade to PNPM 10.21.0 or newer.' + ) + ); } - if (pnpmOptions.globalPatchedDependencies) { - commonPackageJson.pnpm.patchedDependencies = pnpmOptions.globalPatchedDependencies; + if (trustPolicyExclude && semver.lt(pnpmVersion, '10.22.0')) { + terminal.writeWarningLine( + Colorize.yellow( + `Your version of PNPM (${pnpmVersion}) ` + + `doesn't support the "trustPolicyExclude" field in ` + + `${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` + + 'Remove this field or upgrade to PNPM 10.22.0 or newer.' + ) + ); } - if (pnpmOptions.trustPolicy !== undefined) { - if (semver.lt(pnpmVersion, '10.21.0')) { - terminal.writeWarningLine( - Colorize.yellow( - `Your version of PNPM (${pnpmVersion}) ` + - `doesn't support the "trustPolicy" field in ` + - `${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` + - 'Remove this field or upgrade to PNPM 10.21.0 or newer.' - ) - ); - } - - commonPackageJson.pnpm.trustPolicy = pnpmOptions.trustPolicy; + if (trustPolicyIgnoreAfter !== undefined && semver.lt(pnpmVersion, '10.27.0')) { + terminal.writeWarningLine( + Colorize.yellow( + `Your version of PNPM (${pnpmVersion}) ` + + `doesn't support the "trustPolicyIgnoreAfterMinutes" field in ` + + `${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` + + 'Remove this field or upgrade to PNPM 10.27.0 or newer.' + ) + ); } - if (pnpmOptions.trustPolicyExclude) { - if (semver.lt(pnpmVersion, '10.22.0')) { - terminal.writeWarningLine( - Colorize.yellow( - `Your version of PNPM (${pnpmVersion}) ` + - `doesn't support the "trustPolicyExclude" field in ` + - `${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` + - 'Remove this field or upgrade to PNPM 10.22.0 or newer.' - ) - ); - } - - commonPackageJson.pnpm.trustPolicyExclude = pnpmOptions.trustPolicyExclude; - } - - if (pnpmOptions.trustPolicyIgnoreAfterMinutes !== undefined) { - if (semver.lt(pnpmVersion, '10.27.0')) { - terminal.writeWarningLine( - Colorize.yellow( - `Your version of PNPM (${pnpmVersion}) ` + - `doesn't support the "trustPolicyIgnoreAfterMinutes" field in ` + - `${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` + - 'Remove this field or upgrade to PNPM 10.27.0 or newer.' - ) - ); - } - - // NOTE: the pnpm setting is `trustPolicyIgnoreAfter`, but the rush pnpm setting is `trustPolicyIgnoreAfterMinutes` - commonPackageJson.pnpm.trustPolicyIgnoreAfter = pnpmOptions.trustPolicyIgnoreAfterMinutes; - } - - if (pnpmOptions.unsupportedPackageJsonSettings) { - merge(commonPackageJson, pnpmOptions.unsupportedPackageJsonSettings); - } + additionalCommonPackageJsonPropertiesToMerge = unsupportedPackageJsonSettings; + + pnpmSection = { + overrides, + packageExtensions, + peerDependencyRules, + neverBuiltDependencies, + onlyBuiltDependencies, + ignoredOptionalDependencies, + allowedDeprecatedVersions, + patchedDependencies, + trustPolicy, + trustPolicyExclude, + trustPolicyIgnoreAfter + }; } // Add any preferred versions to the top of the commonPackageJson // do this in alphabetical order for simpler debugging - for (const dependency of Array.from(dependencies.keys()).sort()) { - commonPackageJson.dependencies![dependency] = dependencies.get(dependency)!; + const sortedDependencyEntries: [string, string][] = Array.from(dependenciesMap.entries()).sort( + ([a], [b]) => (a < b ? -1 : a > b ? 1 : 0) + ); + const dependencies: Record = Object.fromEntries(sortedDependencyEntries); + const commonPackageJson: ICommonPackageJson = { + dependencies, + description: 'Temporary file generated by the Rush tool', + name: 'rush-common', + private: true, + version: '0.0.0', + pnpm: pnpmSection + }; + + if (additionalCommonPackageJsonPropertiesToMerge) { + merge(commonPackageJson, additionalCommonPackageJsonPropertiesToMerge); } // Example: "C:\MyRepo\common\temp\package.json" - const commonPackageJsonFilename: string = path.join( - subspace.getSubspaceTempFolderPath(), - FileConstants.PackageJson - ); + const commonPackageJsonFilename: string = `${subspace.getSubspaceTempFolderPath()}/${FileConstants.PackageJson}`; // Don't update the file timestamp unless the content has changed, since "rush install" // will consider this timestamp - JsonFile.save(commonPackageJson, commonPackageJsonFilename, { onlyIfChanged: true }); + await JsonFile.saveAsync(commonPackageJson, commonPackageJsonFilename, { + onlyIfChanged: true, + ignoreUndefinedValues: true + }); } public static getPackageManagerEnvironment( @@ -217,17 +213,11 @@ export class InstallHelpers { let configurationEnvironment: IConfigurationEnvironment | undefined = undefined; if (rushConfiguration.packageManager === 'npm') { - if (rushConfiguration.npmOptions && rushConfiguration.npmOptions.environmentVariables) { - configurationEnvironment = rushConfiguration.npmOptions.environmentVariables; - } + configurationEnvironment = rushConfiguration.npmOptions?.environmentVariables; } else if (rushConfiguration.isPnpm) { - if (rushConfiguration.pnpmOptions && rushConfiguration.pnpmOptions.environmentVariables) { - configurationEnvironment = rushConfiguration.pnpmOptions.environmentVariables; - } + configurationEnvironment = rushConfiguration.pnpmOptions?.environmentVariables; } else if (rushConfiguration.packageManager === 'yarn') { - if (rushConfiguration.yarnOptions && rushConfiguration.yarnOptions.environmentVariables) { - configurationEnvironment = rushConfiguration.yarnOptions.environmentVariables; - } + configurationEnvironment = rushConfiguration.yarnOptions?.environmentVariables; } return InstallHelpers._mergeEnvironmentVariables(process.env, configurationEnvironment, options); @@ -268,7 +258,7 @@ export class InstallHelpers { const packageManagerAndVersion: string = `${packageManager}-${packageManagerVersion}`; // Example: "C:\Users\YourName\.rush\pnpm-1.2.3" - const packageManagerToolFolder: string = path.join(rushUserFolder, packageManagerAndVersion); + const packageManagerToolFolder: string = `${rushUserFolder}/${packageManagerAndVersion}`; const packageManagerMarker: LastInstallFlag = new LastInstallFlag(packageManagerToolFolder, { node: process.versions.node @@ -319,10 +309,7 @@ export class InstallHelpers { FileSystem.ensureFolder(rushConfiguration.commonTempFolder); // Example: "C:\MyRepo\common\temp\pnpm-local" - const localPackageManagerToolFolder: string = path.join( - rushConfiguration.commonTempFolder, - `${packageManager}-local` - ); + const localPackageManagerToolFolder: string = `${rushConfiguration.commonTempFolder}/${packageManager}-local`; logIfConsoleOutputIsNotRestricted(`\nSymlinking "${localPackageManagerToolFolder}"`); logIfConsoleOutputIsNotRestricted(` --> "${packageManagerToolFolder}"`); @@ -330,14 +317,14 @@ export class InstallHelpers { // We cannot use FileSystem.exists() to test the existence of a symlink, because it will // return false for broken symlinks. There is no way to test without catching an exception. try { - FileSystem.deleteFolder(localPackageManagerToolFolder); + await FileSystem.deleteFolderAsync(localPackageManagerToolFolder); } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { throw error; } } - FileSystem.createSymbolicLinkJunction({ + await FileSystem.createSymbolicLinkJunctionAsync({ linkTargetPath: packageManagerToolFolder, newLinkPath: localPackageManagerToolFolder }); diff --git a/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts b/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts index b88e6147971..0aa8d858357 100644 --- a/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts @@ -383,7 +383,7 @@ export class RushInstallManager extends BaseInstallManager { } // Write the common package.json - InstallHelpers.generateCommonPackageJson( + await InstallHelpers.generateCommonPackageJsonAsync( this.rushConfiguration, this.rushConfiguration.defaultSubspace, commonDependencies, diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index f6f3ff5d449..510e63644db 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -442,7 +442,12 @@ export class WorkspaceInstallManager extends BaseInstallManager { } // Write the common package.json - InstallHelpers.generateCommonPackageJson(this.rushConfiguration, subspace, undefined, this._terminal); + await InstallHelpers.generateCommonPackageJsonAsync( + this.rushConfiguration, + subspace, + undefined, + this._terminal + ); // Set catalog definitions in the workspace file if specified if (pnpmOptions.globalCatalogs) { diff --git a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts index 0e111e9799c..636522166e6 100644 --- a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts +++ b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts @@ -8,13 +8,13 @@ import { InstallHelpers } from '../installManager/InstallHelpers'; import { RushConfiguration } from '../../api/RushConfiguration'; describe(InstallHelpers.name, () => { - describe(InstallHelpers.generateCommonPackageJson.name, () => { - let mockJsonFileSave: jest.SpyInstance; + describe(InstallHelpers.generateCommonPackageJsonAsync.name, () => { + let mockJsonFileSaveAsync: jest.SpyInstance; let terminal: Terminal; let terminalProvider: StringBufferTerminalProvider; beforeAll(() => { - mockJsonFileSave = jest.spyOn(JsonFile, 'save').mockImplementation(() => true); + mockJsonFileSaveAsync = jest.spyOn(JsonFile, 'saveAsync').mockImplementation(async () => true); }); beforeEach(() => { @@ -29,21 +29,21 @@ describe(InstallHelpers.name, () => { asLines: true }) ).toMatchSnapshot('Terminal Output'); - mockJsonFileSave.mockClear(); + mockJsonFileSaveAsync.mockClear(); }); - it('generates correct package json with pnpm configurations', () => { + it('generates correct package json with pnpm configurations', async () => { const RUSH_JSON_FILENAME: string = `${__dirname}/pnpmConfig/rush.json`; const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile(RUSH_JSON_FILENAME); - InstallHelpers.generateCommonPackageJson( + await InstallHelpers.generateCommonPackageJsonAsync( rushConfiguration, rushConfiguration.defaultSubspace, undefined, terminal ); const packageJson: IPackageJson = JSON.parse( - JsonFile.stringify(mockJsonFileSave.mock.calls[0][0], { ignoreUndefinedValues: true }) + JsonFile.stringify(mockJsonFileSaveAsync.mock.calls[0][0], { ignoreUndefinedValues: true }) ); expect(packageJson).toEqual( expect.objectContaining({ diff --git a/libraries/rush-lib/src/logic/test/__snapshots__/InstallHelpers.test.ts.snap b/libraries/rush-lib/src/logic/test/__snapshots__/InstallHelpers.test.ts.snap index fd822686df2..3138b8ae2ed 100644 --- a/libraries/rush-lib/src/logic/test/__snapshots__/InstallHelpers.test.ts.snap +++ b/libraries/rush-lib/src/logic/test/__snapshots__/InstallHelpers.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`InstallHelpers generateCommonPackageJson generates correct package json with pnpm configurations 1`] = ` +exports[`InstallHelpers generateCommonPackageJsonAsync generates correct package json with pnpm configurations 1`] = ` Object { "dependencies": Object {}, "description": "Temporary file generated by the Rush tool", @@ -34,4 +34,4 @@ Object { } `; -exports[`InstallHelpers generateCommonPackageJson generates correct package json with pnpm configurations: Terminal Output 1`] = `Array []`; +exports[`InstallHelpers generateCommonPackageJsonAsync generates correct package json with pnpm configurations: Terminal Output 1`] = `Array []`; From 2d9e25ba4acc58be4a8b6e25a1e0e132ea3d1c39 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Wed, 15 Jul 2026 20:17:28 -0400 Subject: [PATCH 4/5] Fix dropped catalogs assignment and pass through empty pnpm-workspace objects - Restore workspaceFile.catalogs assignment in WorkspaceInstallManager that was lost during cleanup, so globalCatalogs are written to pnpm-workspace.yaml again - Pass through explicitly-set empty catalogs/allowBuilds objects rather than omitting them; only undefined is omitted - Update tests and snapshot to reflect the passthrough behavior --- .../installManager/WorkspaceInstallManager.ts | 64 +++++++------- .../src/logic/pnpm/PnpmWorkspaceFile.ts | 84 +++++-------------- .../logic/pnpm/test/PnpmWorkspaceFile.test.ts | 60 ++++++------- .../PnpmWorkspaceFile.test.ts.snap | 3 +- 4 files changed, 84 insertions(+), 127 deletions(-) diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index 510e63644db..f708068f3d9 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -5,7 +5,6 @@ import * as path from 'node:path'; import { createHash } from 'node:crypto'; import * as semver from 'semver'; -import yaml from 'js-yaml'; import { FileSystem, @@ -386,11 +385,19 @@ export class WorkspaceInstallManager extends BaseInstallManager { } // Check if overrides and globalOverrides are the same - const pnpmOptions: PnpmOptionsConfiguration = - subspace.getPnpmOptions() || this.rushConfiguration.pnpmOptions; + const { + globalOverrides = {}, + globalPackageExtensions, + globalCatalogs: catalogs, + globalAllowBuilds, + globalOnlyBuiltDependencies, + globalNeverBuiltDependencies, + minimumReleaseAgeMinutes, + minimumReleaseAgeExclude + }: PnpmOptionsConfiguration = subspace.getPnpmOptions() || this.rushConfiguration.pnpmOptions; const overridesAreEqual: boolean = Objects.areDeepEqual>( - pnpmOptions.globalOverrides ?? {}, + globalOverrides, shrinkwrapFile?.overrides ? Object.fromEntries(shrinkwrapFile?.overrides) : {} ); @@ -420,8 +427,6 @@ export class WorkspaceInstallManager extends BaseInstallManager { } } - const globalPackageExtensions: Record | undefined = - pnpmOptions.globalPackageExtensions; // https://github.com/pnpm/pnpm/blob/ba9409ffcef0c36dc1b167d770a023c87444822d/pkg-manager/core/src/install/index.ts#L331 if (globalPackageExtensions && Object.keys(globalPackageExtensions).length !== 0) { if (packageExtensionsChecksumAlgorithm) { @@ -450,7 +455,7 @@ export class WorkspaceInstallManager extends BaseInstallManager { ); // Set catalog definitions in the workspace file if specified - if (pnpmOptions.globalCatalogs) { + if (catalogs) { if ( this.rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined && semver.lt(this.rushConfiguration.rushConfigurationJson.pnpmVersion, '9.5.0') @@ -465,13 +470,7 @@ export class WorkspaceInstallManager extends BaseInstallManager { ); } - const catalogs: Record> = {}; - - if (pnpmOptions.globalCatalogs) { - Object.assign(catalogs, pnpmOptions.globalCatalogs); - } - - workspaceFile.setCatalogs(catalogs); + workspaceFile.catalogs = catalogs; } // Set allowBuilds in the workspace file for pnpm 11+ (replaces onlyBuiltDependencies/neverBuiltDependencies) @@ -479,28 +478,27 @@ export class WorkspaceInstallManager extends BaseInstallManager { this.rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined && semver.gte(this.rushConfiguration.rushConfigurationJson.pnpmVersion, '11.0.0') ) { - if (pnpmOptions.globalAllowBuilds) { - workspaceFile.setAllowBuilds(pnpmOptions.globalAllowBuilds); - } else if (pnpmOptions.globalOnlyBuiltDependencies || pnpmOptions.globalNeverBuiltDependencies) { + if (globalAllowBuilds) { + workspaceFile.allowBuilds = globalAllowBuilds; + } else if (globalOnlyBuiltDependencies || globalNeverBuiltDependencies) { // Backward compatibility: convert globalOnlyBuiltDependencies/globalNeverBuiltDependencies // to allowBuilds format for pnpm 11+ const allowBuilds: Record = {}; - if (pnpmOptions.globalOnlyBuiltDependencies) { - for (const pkg of pnpmOptions.globalOnlyBuiltDependencies) { + if (globalOnlyBuiltDependencies) { + for (const pkg of globalOnlyBuiltDependencies) { allowBuilds[pkg] = true; } } - if (pnpmOptions.globalNeverBuiltDependencies) { - for (const pkg of pnpmOptions.globalNeverBuiltDependencies) { + + if (globalNeverBuiltDependencies) { + for (const pkg of globalNeverBuiltDependencies) { allowBuilds[pkg] = false; } } - workspaceFile.setAllowBuilds(allowBuilds); + + workspaceFile.allowBuilds = allowBuilds; } - } else if ( - pnpmOptions.globalAllowBuilds && - this.rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined - ) { + } else if (globalAllowBuilds && this.rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined) { this._terminal.writeWarningLine( Colorize.yellow( `Your version of pnpm (${this.rushConfiguration.rushConfigurationJson.pnpmVersion}) ` + @@ -513,7 +511,7 @@ export class WorkspaceInstallManager extends BaseInstallManager { // Set minimumReleaseAge/minimumReleaseAgeExclude in the workspace file. // pnpm does not read these fields from package.json, only from pnpm-workspace.yaml or .npmrc. - if (pnpmOptions.minimumReleaseAgeMinutes !== undefined || pnpmOptions.minimumReleaseAgeExclude) { + if (minimumReleaseAgeMinutes !== undefined || minimumReleaseAgeExclude) { if ( this.rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined && semver.lt(this.rushConfiguration.rushConfigurationJson.pnpmVersion, '10.16.0') @@ -528,14 +526,9 @@ export class WorkspaceInstallManager extends BaseInstallManager { ); } - if (pnpmOptions.minimumReleaseAgeMinutes !== undefined) { - // NOTE: the pnpm setting is `minimumReleaseAge`, but the Rush setting is `minimumReleaseAgeMinutes` - workspaceFile.setMinimumReleaseAge(pnpmOptions.minimumReleaseAgeMinutes); - } - - if (pnpmOptions.minimumReleaseAgeExclude) { - workspaceFile.setMinimumReleaseAgeExclude(pnpmOptions.minimumReleaseAgeExclude); - } + // NOTE: the pnpm setting is `minimumReleaseAge`, but the Rush setting is `minimumReleaseAgeMinutes` + workspaceFile.minimumReleaseAge = minimumReleaseAgeMinutes; + workspaceFile.minimumReleaseAgeExclude = minimumReleaseAgeExclude; } // Save the generated workspace file. Don't update the file timestamp unless the content has changed, @@ -824,6 +817,7 @@ export class WorkspaceInstallManager extends BaseInstallManager { ) { // Find the .modules.yaml file in the subspace temp/node_modules folder const modulesContent: string = await FileSystem.readFileAsync(modulesFilePath); + const yaml: typeof import('js-yaml') = await import('js-yaml'); const yamlContent: IPnpmModules = yaml.load(modulesContent, { filename: modulesFilePath }) as IPnpmModules; diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts index 839b97b92a2..0be4e009deb 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts @@ -32,24 +32,24 @@ interface IPnpmWorkspaceYaml { /** The list of local package directories */ packages: string[]; /** Catalog definitions for centralized version management */ - catalogs?: Record>; + catalogs: Record> | undefined; /** * Controls which packages are allowed to run build scripts. A value of `true` means the * package is allowed to run build scripts; `false` means it is explicitly denied. * Packages with build scripts not listed here will cause pnpm to fail with ERR_PNPM_IGNORED_BUILDS. * (SUPPORTED ONLY IN PNPM 11.0.0 AND NEWER) */ - allowBuilds?: Record; + allowBuilds: Record | undefined; /** * The minimum number of minutes that must pass after a version is published before pnpm will install it. * (SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER) */ - minimumReleaseAge?: number; + minimumReleaseAge: number | undefined; /** * List of package names or patterns that are excluded from the minimumReleaseAge check. * (SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER) */ - minimumReleaseAgeExclude?: string[]; + minimumReleaseAgeExclude: string[] | undefined; } export class PnpmWorkspaceFile extends BaseWorkspaceFile { @@ -58,11 +58,11 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { */ public readonly workspaceFilename: string; - private _workspacePackages: Set; - private _catalogs: Record> | undefined; - private _allowBuilds: Record | undefined; - private _minimumReleaseAge: number | undefined; - private _minimumReleaseAgeExclude: string[] | undefined; + private readonly _workspacePackages: Set; + public catalogs: IPnpmWorkspaceYaml['catalogs']; + public allowBuilds: IPnpmWorkspaceYaml['allowBuilds']; + public minimumReleaseAge: IPnpmWorkspaceYaml['minimumReleaseAge']; + public minimumReleaseAgeExclude: IPnpmWorkspaceYaml['minimumReleaseAgeExclude']; /** * The PNPM workspace file is used to specify the location of workspaces relative to the root @@ -75,45 +75,6 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { // Ignore any existing file since this file is generated and we need to handle deleting packages // If we need to support manual customization, that should be an additional parameter for "base file" this._workspacePackages = new Set(); - this._catalogs = undefined; - this._allowBuilds = undefined; - this._minimumReleaseAge = undefined; - this._minimumReleaseAgeExclude = undefined; - } - - /** - * Sets the catalog definitions for the workspace. - * @param catalogs - A map of catalog name to package versions - */ - public setCatalogs(catalogs: Record> | undefined): void { - this._catalogs = catalogs; - } - - /** - * Sets the allowBuilds definitions for the workspace. - * This controls which packages are allowed to run build scripts in pnpm 11+. - * @param allowBuilds - A map of package name to boolean (true = allowed, false = denied) - */ - public setAllowBuilds(allowBuilds: Record | undefined): void { - this._allowBuilds = allowBuilds; - } - - /** - * Sets the minimumReleaseAge setting for the workspace. - * The minimum number of minutes that must pass after a version is published before pnpm will install it. - * (SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER) - */ - public setMinimumReleaseAge(minimumReleaseAge: number | undefined): void { - this._minimumReleaseAge = minimumReleaseAge; - } - - /** - * Sets the minimumReleaseAgeExclude setting for the workspace. - * List of package names or patterns that are excluded from the minimumReleaseAge check. - * (SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER) - */ - public setMinimumReleaseAgeExclude(minimumReleaseAgeExclude: string[] | undefined): void { - this._minimumReleaseAgeExclude = minimumReleaseAgeExclude; } public override addPackage(packagePath: string): void { @@ -131,22 +92,23 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { // Ensure stable sort order when serializing Sort.sortSet(this._workspacePackages); + const { + _workspacePackages: workspacePackages, + catalogs, + allowBuilds, + minimumReleaseAge, + minimumReleaseAgeExclude + } = this; const workspaceYaml: IPnpmWorkspaceYaml = { - packages: Array.from(this._workspacePackages) + packages: Array.from(workspacePackages), + // js-yaml omits mapping entries whose value is `undefined`, so no guard is needed here. + // An explicitly-set empty object is passed through as-is. + catalogs, + allowBuilds, + minimumReleaseAge, + minimumReleaseAgeExclude }; - if (this._catalogs && Object.keys(this._catalogs).length > 0) { - workspaceYaml.catalogs = this._catalogs; - } - - if (this._allowBuilds && Object.keys(this._allowBuilds).length > 0) { - workspaceYaml.allowBuilds = this._allowBuilds; - } - - // js-yaml omits mapping entries whose value is `undefined`, so no guard is needed here. - workspaceYaml.minimumReleaseAge = this._minimumReleaseAge; - workspaceYaml.minimumReleaseAgeExclude = this._minimumReleaseAgeExclude; - const yamlModule: typeof import('js-yaml') = await import('js-yaml'); return yamlModule.dump(workspaceYaml, PNPM_SHRINKWRAP_YAML_FORMAT); } 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 81f51b75414..b4f772679d3 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts @@ -48,13 +48,13 @@ describe(PnpmWorkspaceFile.name, () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); workspaceFile.addPackage(`${projectsDir}/app1`); - workspaceFile.setCatalogs({ + workspaceFile.catalogs = { default: { react: '^18.0.0', 'react-dom': '^18.0.0', typescript: '~5.3.0' } - }); + }; await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); @@ -65,7 +65,7 @@ describe(PnpmWorkspaceFile.name, () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); workspaceFile.addPackage(`${projectsDir}/app1`); - workspaceFile.setCatalogs({ + workspaceFile.catalogs = { default: { typescript: '~5.3.0' }, @@ -77,7 +77,7 @@ describe(PnpmWorkspaceFile.name, () => { express: '^4.18.0', fastify: '^4.26.0' } - }); + }; await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); @@ -88,7 +88,7 @@ describe(PnpmWorkspaceFile.name, () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); workspaceFile.addPackage(`${projectsDir}/app1`); - workspaceFile.setCatalogs({}); + workspaceFile.catalogs = {}; await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); @@ -99,7 +99,7 @@ describe(PnpmWorkspaceFile.name, () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); workspaceFile.addPackage(`${projectsDir}/app1`); - workspaceFile.setCatalogs(undefined); + workspaceFile.catalogs = undefined; await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); @@ -110,13 +110,13 @@ describe(PnpmWorkspaceFile.name, () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); workspaceFile.addPackage(`${projectsDir}/app1`); - workspaceFile.setCatalogs({ + workspaceFile.catalogs = { default: { '@types/node': '~22.9.4', '@types/cookies': '^0.7.7', '@rushstack/node-core-library': '~5.0.0' } - }); + }; await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); @@ -127,21 +127,21 @@ describe(PnpmWorkspaceFile.name, () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); workspaceFile.addPackage(`${projectsDir}/app1`); - workspaceFile.setCatalogs({ + workspaceFile.catalogs = { default: { react: '^18.0.0' } - }); + }; await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); // Update catalogs - workspaceFile.setCatalogs({ + workspaceFile.catalogs = { default: { react: '^18.2.0', 'react-dom': '^18.2.0' } - }); + }; await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); @@ -154,11 +154,11 @@ describe(PnpmWorkspaceFile.name, () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); workspaceFile.addPackage(`${projectsDir}/app1`); - workspaceFile.setAllowBuilds({ + workspaceFile.allowBuilds = { esbuild: true, '@parcel/watcher': true, fsevents: false - }); + }; await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); @@ -169,15 +169,15 @@ describe(PnpmWorkspaceFile.name, () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); workspaceFile.addPackage(`${projectsDir}/app1`); - workspaceFile.setCatalogs({ + workspaceFile.catalogs = { default: { react: '^18.0.0' } - }); + }; - workspaceFile.setAllowBuilds({ + workspaceFile.allowBuilds = { esbuild: true - }); + }; await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); @@ -188,18 +188,18 @@ describe(PnpmWorkspaceFile.name, () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); workspaceFile.addPackage(`${projectsDir}/app1`); - workspaceFile.setAllowBuilds({}); + workspaceFile.allowBuilds = {}; await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); - expect(writtenContent).not.toContain('allowBuilds'); + expect(writtenContent).toContain('allowBuilds: {}'); }); it('handles undefined allowBuilds', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); workspaceFile.addPackage(`${projectsDir}/app1`); - workspaceFile.setAllowBuilds(undefined); + workspaceFile.allowBuilds = undefined; await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); @@ -212,7 +212,7 @@ describe(PnpmWorkspaceFile.name, () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); workspaceFile.addPackage(`${projectsDir}/app1`); - workspaceFile.setMinimumReleaseAge(20160); + workspaceFile.minimumReleaseAge = 20160; await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); @@ -223,8 +223,8 @@ describe(PnpmWorkspaceFile.name, () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); workspaceFile.addPackage(`${projectsDir}/app1`); - workspaceFile.setMinimumReleaseAge(1440); - workspaceFile.setMinimumReleaseAgeExclude(['webpack', '@myorg/*']); + workspaceFile.minimumReleaseAge = 1440; + workspaceFile.minimumReleaseAgeExclude = ['webpack', '@myorg/*']; await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); @@ -235,7 +235,7 @@ describe(PnpmWorkspaceFile.name, () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); workspaceFile.addPackage(`${projectsDir}/app1`); - workspaceFile.setMinimumReleaseAgeExclude(['webpack']); + workspaceFile.minimumReleaseAgeExclude = ['webpack']; await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); @@ -246,7 +246,7 @@ describe(PnpmWorkspaceFile.name, () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); workspaceFile.addPackage(`${projectsDir}/app1`); - workspaceFile.setMinimumReleaseAge(0); + workspaceFile.minimumReleaseAge = 0; await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); @@ -257,8 +257,8 @@ describe(PnpmWorkspaceFile.name, () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); workspaceFile.addPackage(`${projectsDir}/app1`); - workspaceFile.setMinimumReleaseAge(undefined); - workspaceFile.setMinimumReleaseAgeExclude(undefined); + workspaceFile.minimumReleaseAge = undefined; + workspaceFile.minimumReleaseAgeExclude = undefined; await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); @@ -269,7 +269,7 @@ describe(PnpmWorkspaceFile.name, () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); workspaceFile.addPackage(`${projectsDir}/app1`); - workspaceFile.setMinimumReleaseAgeExclude([]); + workspaceFile.minimumReleaseAgeExclude = []; await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); @@ -280,7 +280,7 @@ describe(PnpmWorkspaceFile.name, () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); workspaceFile.addPackage(`${projectsDir}/app1`); - workspaceFile.setMinimumReleaseAgeExclude(undefined); + workspaceFile.minimumReleaseAgeExclude = undefined; await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); diff --git a/libraries/rush-lib/src/logic/pnpm/test/__snapshots__/PnpmWorkspaceFile.test.ts.snap b/libraries/rush-lib/src/logic/pnpm/test/__snapshots__/PnpmWorkspaceFile.test.ts.snap index 7cf5594bd98..39ea9278efd 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/__snapshots__/PnpmWorkspaceFile.test.ts.snap +++ b/libraries/rush-lib/src/logic/pnpm/test/__snapshots__/PnpmWorkspaceFile.test.ts.snap @@ -65,7 +65,8 @@ packages: `; exports[`PnpmWorkspaceFile catalog functionality handles empty catalog object 1`] = ` -"packages: +"catalogs: {} +packages: - projects/app1 " `; From 8141ee5659c680a750ca980e9db67264f9309a44 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Wed, 15 Jul 2026 20:57:14 -0400 Subject: [PATCH 5/5] Rush change. --- ...an-up-pnpm-options-saving_2026-07-16-00-57-14.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/@microsoft/rush/clean-up-pnpm-options-saving_2026-07-16-00-57-14.json diff --git a/common/changes/@microsoft/rush/clean-up-pnpm-options-saving_2026-07-16-00-57-14.json b/common/changes/@microsoft/rush/clean-up-pnpm-options-saving_2026-07-16-00-57-14.json new file mode 100644 index 00000000000..efcd84c45fb --- /dev/null +++ b/common/changes/@microsoft/rush/clean-up-pnpm-options-saving_2026-07-16-00-57-14.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "", + "type": "none", + "packageName": "@microsoft/rush" + } + ], + "packageName": "@microsoft/rush", + "email": "iclanton@users.noreply.github.com" +} \ No newline at end of file