From 4c73ed1ecdb01e10481c21419557665c08bd4b21 Mon Sep 17 00:00:00 2001 From: Bharat Middha <5100938+bmiddha@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:12:25 -0700 Subject: [PATCH] fix(rush-lib): use common pnpm-config.json catalogs for subspaces without their own Install falls back to common/config/rush/pnpm-config.json, but the repo-state.json catalog hash, catalog change detection, and the "rush-pnpm update" catalog sync only read the subspace's own file. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...alog-config-fallback_2026-10-13-00-00.json | 11 ++ common/reviews/api/rush-lib.api.md | 2 +- libraries/rush-lib/src/api/Subspace.ts | 23 ++-- .../src/cli/RushPnpmCommandLineParser.ts | 63 ++++++++-- .../test/RushPnpmCommandLineParser.test.ts | 113 +++++++++++++++++ .../src/logic/ProjectChangeAnalyzer.ts | 63 +++++++--- .../src/logic/base/BaseInstallManager.ts | 5 +- .../logic/installManager/InstallHelpers.ts | 3 +- .../src/logic/pnpm/EffectivePnpmOptions.ts | 118 ++++++++++++++++++ .../logic/pnpm/PnpmOptionsConfiguration.ts | 9 +- .../src/logic/pnpm/PnpmShrinkwrapFile.ts | 6 +- .../pnpm/SubspacePnpmfileConfiguration.ts | 9 +- .../src/logic/test/InstallHelpers.test.ts | 30 +++++ .../logic/test/ProjectChangeAnalyzer.test.ts | 63 ++++++++++ .../a/package.json | 8 ++ .../b/package.json | 8 ++ .../c/package.json | 8 ++ .../common/config/rush/pnpm-config.json | 13 ++ .../common/config/rush/subspaces.json | 5 + .../subspaces/default/common-versions.json | 8 ++ .../other-subspace/common-versions.json | 8 ++ .../own-config-subspace/common-versions.json | 8 ++ .../own-config-subspace/pnpm-config.json | 8 ++ .../e/package.json | 8 ++ .../repoWithSubspacesCommonCatalogs/rush.json | 25 ++++ 25 files changed, 578 insertions(+), 47 deletions(-) create mode 100644 common/changes/@microsoft/rush/bmiddha-catalog-config-fallback_2026-10-13-00-00.json create mode 100644 libraries/rush-lib/src/logic/pnpm/EffectivePnpmOptions.ts create mode 100644 libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/a/package.json create mode 100644 libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/b/package.json create mode 100644 libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/c/package.json create mode 100644 libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/common/config/rush/pnpm-config.json create mode 100644 libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/common/config/rush/subspaces.json create mode 100644 libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/common/config/subspaces/default/common-versions.json create mode 100644 libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/common/config/subspaces/other-subspace/common-versions.json create mode 100644 libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/common/config/subspaces/own-config-subspace/common-versions.json create mode 100644 libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/common/config/subspaces/own-config-subspace/pnpm-config.json create mode 100644 libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/e/package.json create mode 100644 libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/rush.json diff --git a/common/changes/@microsoft/rush/bmiddha-catalog-config-fallback_2026-10-13-00-00.json b/common/changes/@microsoft/rush/bmiddha-catalog-config-fallback_2026-10-13-00-00.json new file mode 100644 index 00000000000..1e71405de9d --- /dev/null +++ b/common/changes/@microsoft/rush/bmiddha-catalog-config-fallback_2026-10-13-00-00.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Fix repo-state.json, change detection, and \"rush-pnpm update\" ignoring the PNPM catalogs of subspaces that use common/config/rush/pnpm-config.json.", + "type": "patch" + } + ], + "packageName": "@microsoft/rush", + "email": "223556219+Copilot@users.noreply.github.com" +} diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index a85fdb820e4..914ae2604f6 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -1693,7 +1693,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration readonly autoInstallPeers: boolean | undefined; readonly globalAllowBuilds: Record | undefined; readonly globalAllowedDeprecatedVersions: Record | undefined; - readonly globalCatalogs: Record> | undefined; + get globalCatalogs(): Record> | undefined; readonly globalIgnoredOptionalDependencies: string[] | undefined; readonly globalNeverBuiltDependencies: string[] | undefined; readonly globalOnlyBuiltDependencies: string[] | undefined; diff --git a/libraries/rush-lib/src/api/Subspace.ts b/libraries/rush-lib/src/api/Subspace.ts index 0f9583f9f7f..611f96859ae 100644 --- a/libraries/rush-lib/src/api/Subspace.ts +++ b/libraries/rush-lib/src/api/Subspace.ts @@ -15,6 +15,7 @@ import { CommonVersionsConfiguration } from './CommonVersionsConfiguration'; import { RepoStateFile } from '../logic/RepoStateFile'; import type { PnpmPackageManager } from './packageManager/PnpmPackageManager'; import { PnpmOptionsConfiguration } from '../logic/pnpm/PnpmOptionsConfiguration'; +import { getEffectivePnpmCatalogs } from '../logic/pnpm/EffectivePnpmOptions'; import { SubspacePnpmfileConfiguration } from '../logic/pnpm/SubspacePnpmfileConfiguration'; import type { ISubspacePnpmfileShimSettings } from '../logic/pnpm/IPnpmfile'; @@ -411,27 +412,21 @@ export class Subspace { } /** - * Computes a hash of the PNPM catalog definitions for this subspace. + * Computes a hash of the PNPM catalogs that install uses for this subspace, which come from + * common/config/rush/pnpm-config.json if the subspace has no pnpm-config.json of its own. * Returns undefined if no catalogs are defined. */ public getPnpmCatalogsHash(): string | undefined { - const pnpmOptions: PnpmOptionsConfiguration | undefined = this.getPnpmOptions(); - if (!pnpmOptions) { - return undefined; - } - - const catalogData: Record = {}; - if (pnpmOptions.globalCatalogs && Object.keys(pnpmOptions.globalCatalogs).length !== 0) { - Object.assign(catalogData, pnpmOptions.globalCatalogs); - } - - // If no catalogs are defined, return undefined - if (Object.keys(catalogData).length === 0) { + const catalogs: Record> | undefined = getEffectivePnpmCatalogs( + this, + this.#rushConfiguration + ); + if (!catalogs) { return undefined; } const hash: crypto.Hash = crypto.createHash('sha1'); - hash.update(JSON.stringify(catalogData)); + hash.update(JSON.stringify(catalogs)); return hash.digest('hex'); } diff --git a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts index 82502d67419..b98c2976d4c 100644 --- a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts +++ b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts @@ -34,6 +34,12 @@ import { getNpmrcEnvironmentVariables } from '../utilities/npmrcUtilities'; import { InstallHelpers } from '../logic/installManager/InstallHelpers'; import type { Subspace } from '../api/Subspace'; import type { PnpmOptionsConfiguration } from '../logic/pnpm/PnpmOptionsConfiguration'; +import { + getEffectivePnpmOptions, + getOtherSubspacesSharingPnpmConfig, + mergePnpmCatalogs, + type PnpmCatalogs +} from '../logic/pnpm/EffectivePnpmOptions'; import { PnpmWorkspaceFile } from '../logic/pnpm/PnpmWorkspaceFile'; import { EnvironmentVariableNames } from '../api/EnvironmentConfiguration'; import { initializeDotEnv } from '../logic/dotenv'; @@ -752,20 +758,32 @@ export class RushPnpmCommandLineParser { // generated "catalogs" section of common/temp//pnpm-workspace.yaml. That file is // regenerated on every install, so the updated versions must be synced back to the // "globalCatalogs" field of pnpm-config.json for the change to be persisted. - const pnpmOptions: PnpmOptionsConfiguration | undefined = this.#subspace.getPnpmOptions(); - if (pnpmOptions === undefined) { + const { pnpmOptions, pnpmConfigFilePath } = getEffectivePnpmOptions( + this.#subspace, + this.#rushConfiguration + ); + if (pnpmOptions.jsonFilename === undefined) { + // The legacy "pnpmOptions" field in rush.json does not support catalogs break; } const workspaceYamlFilename: string = `${subspaceTempFolder}/pnpm-workspace.yaml`; const yamlModule: typeof import('js-yaml') = await import('js-yaml'); const workspaceYamlContent: string = await FileSystem.readFileAsync(workspaceYamlFilename); - const workspaceYaml: { catalogs?: Record> } = (yamlModule.load( - workspaceYamlContent - ) ?? {}) as { catalogs?: Record> }; - const newGlobalCatalogs: Record> | undefined = workspaceYaml?.catalogs; - const currentGlobalCatalogs: Record> | undefined = - pnpmOptions.globalCatalogs; + const workspaceYaml: IPnpmWorkspaceYamlCatalogs = (yamlModule.load(workspaceYamlContent) ?? + {}) as IPnpmWorkspaceYamlCatalogs; + const currentGlobalCatalogs: PnpmCatalogs | undefined = pnpmOptions.globalCatalogs; + let newGlobalCatalogs: PnpmCatalogs | undefined = getCatalogsFromWorkspaceYaml(workspaceYaml); + + // This subspace's workspace can't show which entries other subspaces sharing the file use, + // so never remove any + const subspacesSharingPnpmConfig: Subspace[] = getOtherSubspacesSharingPnpmConfig( + this.#subspace, + this.#rushConfiguration + ); + if (subspacesSharingPnpmConfig.length > 0) { + newGlobalCatalogs = mergePnpmCatalogs(currentGlobalCatalogs, newGlobalCatalogs); + } if (!Objects.areDeepEqual(currentGlobalCatalogs, newGlobalCatalogs)) { await pnpmOptions.updateGlobalCatalogsAsync(newGlobalCatalogs); @@ -775,6 +793,18 @@ export class RushPnpmCommandLineParser { `Rush refreshed the ${RushConstants.pnpmConfigFilename} and shrinkwrap file.\n` + ' Please commit this change to Git.' ); + + if (subspacesSharingPnpmConfig.length > 0) { + // Their lockfiles still have the old catalogs, so their next "rush install" would fail + const updateCommands: string[] = subspacesSharingPnpmConfig.map( + (subspace: Subspace) => ` rush update --subspace ${subspace.subspaceName}` + ); + this.#terminal.writeWarningLine( + `The catalogs were updated in ${pnpmConfigFilePath}, which is also used by other subspaces.\n` + + ' Run these commands to update their shrinkwrap files, then commit those changes to Git:\n' + + updateCommands.join('\n') + ); + } } break; } @@ -830,3 +860,20 @@ export class RushPnpmCommandLineParser { } } } + +interface IPnpmWorkspaceYamlCatalogs { + catalog?: Record; + catalogs?: PnpmCatalogs; +} + +/** + * Rush writes "catalogs", but PNPM may add default catalog entries to the top-level "catalog" field. + */ +function getCatalogsFromWorkspaceYaml(workspaceYaml: IPnpmWorkspaceYamlCatalogs): PnpmCatalogs | undefined { + const { catalog, catalogs } = workspaceYaml; + if (!catalog) { + return catalogs; + } + + return { ...catalogs, default: { ...catalogs?.default, ...catalog } }; +} diff --git a/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts b/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts index 3d9073e2825..ed9b7a66e5e 100644 --- a/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts +++ b/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts @@ -236,6 +236,119 @@ describe(`${RushPnpmCommandLineParser.name} catalog sync`, () => { }); }); +describe(`${RushPnpmCommandLineParser.name} catalog sync with subspaces`, () => { + const PACKAGE_ROOT: string = path.resolve(__dirname, '../../..'); + const TEST_TEMP_FOLDER: string = `${PACKAGE_ROOT}/temp/rush-pnpm-catalog-sync-subspaces-test`; + // Only "own-config-subspace" has its own pnpm-config.json + const FIXTURE_FOLDER: string = path.resolve(__dirname, '../../logic/test/repoWithSubspacesCommonCatalogs'); + const COMMON_PNPM_CONFIG_FILENAME: string = `${TEST_TEMP_FOLDER}/common/config/rush/pnpm-config.json`; + const OWN_PNPM_CONFIG_FILENAME: string = `${TEST_TEMP_FOLDER}/common/config/subspaces/own-config-subspace/pnpm-config.json`; + + async function runUpCatalogSyncAsync( + subspaceName: string, + workspaceYamlLines: string[] + ): Promise<{ rushConfiguration: RushConfiguration; warningOutput: string }> { + const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile( + `${TEST_TEMP_FOLDER}/rush.json` + ); + const subspace: Subspace = rushConfiguration.getSubspace(subspaceName); + await FileSystem.writeFileAsync( + `${subspace.getSubspaceTempFolderPath()}/pnpm-workspace.yaml`, + [...workspaceYamlLines, ''].join('\n'), + { ensureFolderExists: true } + ); + + const terminalProvider: StringBufferTerminalProvider = new StringBufferTerminalProvider(); + await RushPnpmCommandLineParser._postExecuteForTestingAsync({ + commandName: 'up', + doRushUpdateAsync: async () => {}, + rushConfiguration, + subspace, + terminal: new Terminal(terminalProvider) + }); + + return { rushConfiguration, warningOutput: terminalProvider.getWarningOutput() }; + } + + async function loadGlobalCatalogsAsync( + pnpmConfigFilename: string + ): Promise> | undefined> { + const pnpmConfig: { globalCatalogs?: Record> } = + await JsonFile.loadAsync(pnpmConfigFilename); + return pnpmConfig.globalCatalogs; + } + + beforeEach(async () => { + await FileSystem.deleteFolderAsync(TEST_TEMP_FOLDER); + await FileSystem.copyFilesAsync({ + sourcePath: FIXTURE_FOLDER, + destinationPath: TEST_TEMP_FOLDER + }); + }); + + afterEach(async () => { + await FileSystem.deleteFolderAsync(TEST_TEMP_FOLDER); + }); + + it('writes to the common pnpm-config.json and warns about the other subspaces that use it', async () => { + const ownPnpmConfigContent: string = await FileSystem.readFileAsync(OWN_PNPM_CONFIG_FILENAME); + + const { rushConfiguration, warningOutput } = await runUpCatalogSyncAsync('default', [ + 'packages:', + ' - ../../../a', + ' - ../../../b', + 'catalogs:', + ' default:', + ' react: ^18.3.0', + ' foo: ~2.0.0', + ' tools:', + ' typescript: ~5.3.0' + ]); + + const expectedCatalogs: Record> = { + default: { react: '^18.3.0', foo: '~2.0.0' }, + tools: { typescript: '~5.3.0' } + }; + expect(await loadGlobalCatalogsAsync(COMMON_PNPM_CONFIG_FILENAME)).toEqual(expectedCatalogs); + expect(rushConfiguration.pnpmOptions.globalCatalogs).toEqual(expectedCatalogs); + expect(await FileSystem.readFileAsync(OWN_PNPM_CONFIG_FILENAME)).toEqual(ownPnpmConfigContent); + expect(warningOutput).toContain('rush update --subspace other-subspace'); + expect(warningOutput).not.toContain('own-config-subspace'); + }); + + it('never removes catalog entries from a pnpm-config.json that is shared by several subspaces', async () => { + await runUpCatalogSyncAsync('other-subspace', [ + 'packages:', + ' - ../../../c', + 'catalogs:', + ' default:', + ' foo: ~2.1.0' + ]); + + expect(await loadGlobalCatalogsAsync(COMMON_PNPM_CONFIG_FILENAME)).toEqual({ + default: { react: '^18.0.0', foo: '~2.1.0' }, + tools: { typescript: '~5.3.0' } + }); + }); + + it("writes to the subspace's own pnpm-config.json and reads PNPM's top-level default catalog", async () => { + const commonPnpmConfigContent: string = await FileSystem.readFileAsync(COMMON_PNPM_CONFIG_FILENAME); + + const { warningOutput } = await runUpCatalogSyncAsync('own-config-subspace', [ + 'packages:', + ' - ../../../e', + 'catalog:', + ' react: ^17.0.2' + ]); + + expect(await loadGlobalCatalogsAsync(OWN_PNPM_CONFIG_FILENAME)).toEqual({ + default: { react: '^17.0.2' } + }); + expect(await FileSystem.readFileAsync(COMMON_PNPM_CONFIG_FILENAME)).toEqual(commonPnpmConfigContent); + expect(warningOutput).not.toContain('rush update --subspace'); + }); +}); + describe(`${RushPnpmCommandLineParser.name} patch-commit patchedDependencies sync`, () => { it('reads patchedDependencies from pnpm-workspace.yaml for pnpm >= 11', async () => { const updateGlobalPatchedDependencies: jest.Mock = jest.fn(); diff --git a/libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts b/libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts index 2720d5504c5..24e4252923c 100644 --- a/libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts +++ b/libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts @@ -35,7 +35,12 @@ import { BaseProjectShrinkwrapFile } from './base/BaseProjectShrinkwrapFile'; import { PnpmShrinkwrapFile } from './pnpm/PnpmShrinkwrapFile'; import { Git } from './Git'; import { DependencySpecifier, DependencySpecifierType } from './DependencySpecifier'; -import type { IPnpmOptionsJson, PnpmOptionsConfiguration } from './pnpm/PnpmOptionsConfiguration'; +import type { IPnpmOptionsJson } from './pnpm/PnpmOptionsConfiguration'; +import { + getCommonPnpmConfigFilePath, + getEffectivePnpmOptions, + type IEffectivePnpmOptions +} from './pnpm/EffectivePnpmOptions'; import { type IInputsSnapshotProjectMetadata, type IInputsSnapshot, @@ -620,7 +625,7 @@ export class ProjectChangeAnalyzer { } /** - * Detects changes to pnpm catalog entries in a subspace's pnpm-config.json and marks + * Detects changes to the pnpm catalog entries that install uses for a subspace and marks * affected projects as changed. */ async #detectCatalogChangesAsync( @@ -632,29 +637,55 @@ export class ProjectChangeAnalyzer { terminal: ITerminal, changedProjects: Set ): Promise { - const pnpmOptions: PnpmOptionsConfiguration | undefined = subspace.getPnpmOptions(); + const effectivePnpmOptions: IEffectivePnpmOptions = getEffectivePnpmOptions(subspace, rushConfiguration); // Default to an empty object if no global catalogs are configured, handle case of globalCatalogs being deleted - const currentCatalogs: Record> = pnpmOptions?.globalCatalogs ?? {}; - - const pnpmConfigRelativePath: string = Path.convertToSlashes( - path.relative(repoRoot, subspace.getPnpmConfigFilePath()) + const currentCatalogs: Record> = effectivePnpmOptions.pnpmOptions + .globalCatalogs ?? {}; + + const toRepoRelativePath = (filePath: string): string => + Path.convertToSlashes(path.relative(repoRoot, filePath)); + const subspacePnpmConfigRelativePath: string = toRepoRelativePath(subspace.getPnpmConfigFilePath()); + const commonPnpmConfigRelativePath: string = toRepoRelativePath( + getCommonPnpmConfigFilePath(rushConfiguration) ); - if (!changedFiles.has(pnpmConfigRelativePath)) { + // Check the subspace's own file even if deleted, since deleting it switches the subspace to the common file + if ( + !changedFiles.has(subspacePnpmConfigRelativePath) && + !(effectivePnpmOptions.isFallback && changedFiles.has(commonPnpmConfigRelativePath)) + ) { return; } // Determine which specific packages changed within each catalog namespace // Maps catalogNamespace (e.g. "default", "react17") → Set of changed package names let oldCatalogs: Record> | undefined; - try { - const oldPnpmConfigText: string = await this.#git.getBlobContentAsync({ - blobSpec: `${mergeCommit}:${pnpmConfigRelativePath}`, - repositoryRoot: repoRoot - }); - const oldPnpmConfig: IPnpmOptionsJson = JsonFile.parseString(oldPnpmConfigText); - oldCatalogs = oldPnpmConfig.globalCatalogs ?? {}; - } catch { + // Use whichever file was in effect at the merge commit + const oldPnpmConfigCandidatePaths: Set = new Set([ + subspacePnpmConfigRelativePath, + commonPnpmConfigRelativePath + ]); + for (const oldPnpmConfigRelativePath of oldPnpmConfigCandidatePaths) { + let oldPnpmConfigText: string; + try { + oldPnpmConfigText = await this.#git.getBlobContentAsync({ + blobSpec: `${mergeCommit}:${oldPnpmConfigRelativePath}`, + repositoryRoot: repoRoot + }); + } catch { + continue; + } + + try { + const oldPnpmConfig: IPnpmOptionsJson = JsonFile.parseString(oldPnpmConfigText); + oldCatalogs = oldPnpmConfig.globalCatalogs ?? {}; + } catch { + // Unparseable: treat all packages as changed + } + break; + } + + if (oldCatalogs === undefined) { // Old file didn't exist or was unparseable — treat all packages in all current catalogs as changed if (rushConfiguration.subspacesFeatureEnabled) { terminal.writeWarningLine( diff --git a/libraries/rush-lib/src/logic/base/BaseInstallManager.ts b/libraries/rush-lib/src/logic/base/BaseInstallManager.ts index a636b4b8234..bb49222ba9b 100644 --- a/libraries/rush-lib/src/logic/base/BaseInstallManager.ts +++ b/libraries/rush-lib/src/logic/base/BaseInstallManager.ts @@ -55,6 +55,7 @@ import type { IInstallManagerOptions } from './BaseInstallManagerTypes'; import { isVariableSetInNpmrcFile } from '../../utilities/npmrcUtilities'; import type { PnpmResolutionMode } from '../pnpm/PnpmOptionsConfiguration'; import { SubspacePnpmfileConfiguration } from '../pnpm/SubspacePnpmfileConfiguration'; +import { getEffectivePnpmOptions } from '../pnpm/EffectivePnpmOptions'; import type { Subspace } from '../../api/Subspace'; import { ProjectImpactGraphGenerator } from '../ProjectImpactGraphGenerator'; import { FlagFile } from '../../api/FlagFile'; @@ -427,7 +428,9 @@ export abstract class BaseInstallManager { potentiallyChangedFiles.push(subspace.getCommonVersionsFilePath(variant)); // Add pnpm-config.json file to the potentially changed files list. - potentiallyChangedFiles.push(subspace.getPnpmConfigFilePath()); + potentiallyChangedFiles.push( + getEffectivePnpmOptions(subspace, this.rushConfiguration).pnpmConfigFilePath + ); if (this.rushConfiguration.isPnpm) { // If the repo is using pnpmfile.js, consider that also diff --git a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts index 1d1eb22c5a6..6fcbb1bf649 100644 --- a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts +++ b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts @@ -20,6 +20,7 @@ import { Utilities } from '../../utilities/Utilities'; import type { IConfigurationEnvironment } from '../base/BasePackageManagerOptionsConfiguration'; import type { PnpmOptionsConfiguration } from '../pnpm/PnpmOptionsConfiguration'; import { PnpmWorkspaceFile } from '../pnpm/PnpmWorkspaceFile'; +import { getEffectivePnpmOptions } from '../pnpm/EffectivePnpmOptions'; import { merge } from '../../utilities/objectUtilities'; import { getNpmrcEnvironmentVariables } from '../../utilities/npmrcUtilities'; import type { Subspace } from '../../api/Subspace'; @@ -156,7 +157,7 @@ export class InstallHelpers { // NOTE: the pnpm setting is `trustPolicyIgnoreAfter`, but the rush pnpm setting is `trustPolicyIgnoreAfterMinutes` trustPolicyIgnoreAfterMinutes: trustPolicyIgnoreAfter, unsupportedPackageJsonSettings - } = subspace.getPnpmOptions() || rushConfiguration.pnpmOptions; + } = getEffectivePnpmOptions(subspace, rushConfiguration).pnpmOptions; const pnpmVersion: string = rushConfiguration.packageManagerToolVersion; const isPnpm11: boolean = semver.gte(pnpmVersion, '11.0.0'); diff --git a/libraries/rush-lib/src/logic/pnpm/EffectivePnpmOptions.ts b/libraries/rush-lib/src/logic/pnpm/EffectivePnpmOptions.ts new file mode 100644 index 00000000000..81507d6b41e --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/EffectivePnpmOptions.ts @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { RushConfiguration } from '../../api/RushConfiguration'; +import type { Subspace } from '../../api/Subspace'; +import { RushConstants } from '../RushConstants'; +import type { PnpmOptionsConfiguration } from './PnpmOptionsConfiguration'; + +/** + * Catalog name to package name to version. The default catalog uses the key `default`. + */ +export type PnpmCatalogs = Record>; + +export interface IEffectivePnpmOptions { + readonly pnpmOptions: PnpmOptionsConfiguration; + /** + * The file `pnpmOptions` came from. It might not exist if rush.json uses the legacy `pnpmOptions` field. + */ + readonly pnpmConfigFilePath: string; + /** + * True if the subspace has no pnpm-config.json of its own and uses the common one. + */ + readonly isFallback: boolean; +} + +/** + * Returns the subspace's own pnpm-config.json settings if it has one, otherwise the common ones. + * This is the rule install uses, so every other reader of a subspace's PNPM settings must use it too. + */ +export function getEffectivePnpmOptions( + subspace: Subspace, + rushConfiguration: RushConfiguration +): IEffectivePnpmOptions { + const subspacePnpmOptions: PnpmOptionsConfiguration | undefined = subspace.getPnpmOptions(); + if (subspacePnpmOptions) { + return { + pnpmOptions: subspacePnpmOptions, + pnpmConfigFilePath: subspace.getPnpmConfigFilePath(), + isFallback: false + }; + } + + return { + pnpmOptions: rushConfiguration.pnpmOptions, + pnpmConfigFilePath: getCommonPnpmConfigFilePath(rushConfiguration), + isFallback: true + }; +} + +/** + * The catalogs that install writes to the subspace's pnpm-workspace.yaml, or `undefined` if there are none. + */ +export function getEffectivePnpmCatalogs( + subspace: Subspace, + rushConfiguration: RushConfiguration +): PnpmCatalogs | undefined { + if (!rushConfiguration.isPnpm) { + return undefined; + } + + const { globalCatalogs } = getEffectivePnpmOptions(subspace, rushConfiguration).pnpmOptions; + if (!globalCatalogs || Object.keys(globalCatalogs).length === 0) { + return undefined; + } + + return globalCatalogs; +} + +/** + * Returns the other subspaces that read their settings from the same pnpm-config.json as `subspace`. + */ +export function getOtherSubspacesSharingPnpmConfig( + subspace: Subspace, + rushConfiguration: RushConfiguration +): Subspace[] { + const { pnpmConfigFilePath } = getEffectivePnpmOptions(subspace, rushConfiguration); + const result: Subspace[] = []; + if (!rushConfiguration.subspacesFeatureEnabled) { + return result; + } + + for (const otherSubspace of rushConfiguration.subspaces) { + if ( + otherSubspace !== subspace && + getEffectivePnpmOptions(otherSubspace, rushConfiguration).pnpmConfigFilePath === pnpmConfigFilePath + ) { + result.push(otherSubspace); + } + } + + return result; +} + +export function getCommonPnpmConfigFilePath(rushConfiguration: RushConfiguration): string { + return `${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}`; +} + +/** + * Adds and updates entries from `catalogsToAdd` without removing anything from `existingCatalogs`. + * Neither input is modified. + */ +export function mergePnpmCatalogs( + existingCatalogs: PnpmCatalogs | undefined, + catalogsToAdd: PnpmCatalogs | undefined +): PnpmCatalogs | undefined { + const result: PnpmCatalogs = {}; + for (const catalogs of [existingCatalogs, catalogsToAdd]) { + for (const [catalogName, packages] of Object.entries(catalogs ?? {})) { + const mergedPackages: Record = (result[catalogName] ??= {}); + // Not object spread, which would copy heft-config-file's annotation symbols + for (const [packageName, version] of Object.entries(packages)) { + mergedPackages[packageName] = version; + } + } + } + + return Object.keys(result).length > 0 ? result : existingCatalogs; +} diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts index 898699e671c..02353380916 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts @@ -215,6 +215,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration readonly #json: JsonObject; readonly #commonTempFolder: string; #globalPatchedDependencies: Record | undefined; + #globalCatalogs: Record> | undefined; /** * The method used to resolve the store used by PNPM. @@ -542,7 +543,9 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration * * PNPM documentation: https://pnpm.io/catalogs */ - public readonly globalCatalogs: Record> | undefined; + public get globalCatalogs(): Record> | undefined { + return this.#globalCatalogs; + } /** * (GENERATED BY RUSH-PNPM PATCH-COMMIT) When modifying this property, make sure you know what you are doing. @@ -608,7 +611,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration this.alwaysInjectDependenciesFromOtherSubspaces = json.alwaysInjectDependenciesFromOtherSubspaces; this.alwaysFullInstall = json.alwaysFullInstall; this.pnpmLockfilePolicies = json.pnpmLockfilePolicies; - this.globalCatalogs = json.globalCatalogs; + this.#globalCatalogs = json.globalCatalogs; } /** @internal */ @@ -710,6 +713,8 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration public async updateGlobalCatalogsAsync( catalogs: Record> | undefined ): Promise { + // The "rush update" that follows "rush-pnpm up" runs in this process and must see the new catalogs + this.#globalCatalogs = catalogs; this.#json.globalCatalogs = catalogs; await JsonFile.saveAsync(this.#json, this.#getJsonFilenameOrThrow(), { updateExistingFile: true, diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts index 6945bde6d69..d501cbc3b47 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts @@ -38,6 +38,7 @@ import { PnpmfileConfiguration } from './PnpmfileConfiguration'; import { PnpmProjectShrinkwrapFile } from './PnpmProjectShrinkwrapFile'; import type { PackageManagerOptionsConfigurationBase } from '../base/BasePackageManagerOptionsConfiguration'; import { PnpmOptionsConfiguration } from './PnpmOptionsConfiguration'; +import { getEffectivePnpmOptions } from './EffectivePnpmOptions'; import type { IPnpmfile, IPnpmfileContext } from './IPnpmfile'; import type { Subspace } from '../../api/Subspace'; import { CustomTipId, type CustomTipsConfiguration } from '../../api/CustomTipsConfiguration'; @@ -514,7 +515,10 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { subspace: Subspace, terminal: ITerminal ): void { - const pnpmOptions: PnpmOptionsConfiguration = subspace.getPnpmOptions() || rushConfiguration.pnpmOptions; + const pnpmOptions: PnpmOptionsConfiguration = getEffectivePnpmOptions( + subspace, + rushConfiguration + ).pnpmOptions; const { pnpmLockfilePolicies } = pnpmOptions; let invalidPoliciesCount: number = 0; diff --git a/libraries/rush-lib/src/logic/pnpm/SubspacePnpmfileConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/SubspacePnpmfileConfiguration.ts index b190671a0d0..17bf18a5a57 100644 --- a/libraries/rush-lib/src/logic/pnpm/SubspacePnpmfileConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/SubspacePnpmfileConfiguration.ts @@ -13,6 +13,7 @@ import type { PnpmPackageManager } from '../../api/packageManager/PnpmPackageMan import { RushConstants } from '../RushConstants'; import type { Subspace } from '../../api/Subspace'; import type { PnpmOptionsConfiguration } from './PnpmOptionsConfiguration'; +import { getEffectivePnpmOptions } from './EffectivePnpmOptions'; /** * Loads PNPM's pnpmfile.js configuration, and invokes it to preprocess package.json files, @@ -142,9 +143,11 @@ function _getProjectNameToInjectedDependenciesMap( // and the dependency is not injected yet // and the dependency is in another subspace // then, make this dependency as injected dependency - const pnpmOptions: PnpmOptionsConfiguration | undefined = - subspace.getPnpmOptions() || rushConfiguration.pnpmOptions; - if (pnpmOptions && pnpmOptions.alwaysInjectDependenciesFromOtherSubspaces) { + const pnpmOptions: PnpmOptionsConfiguration = getEffectivePnpmOptions( + subspace, + rushConfiguration + ).pnpmOptions; + if (pnpmOptions.alwaysInjectDependenciesFromOtherSubspaces) { const dependencyProjects: ReadonlySet = subspaceProject.dependencyProjects; for (const dependencyProject of dependencyProjects) { const dependencyName: string = dependencyProject.packageName; diff --git a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts index 9f6eda0d499..2bf4590e1e9 100644 --- a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts +++ b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts @@ -2,6 +2,7 @@ // See LICENSE in the project root for license information. import { execFileSync } from 'node:child_process'; +import * as crypto from 'node:crypto'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; @@ -15,6 +16,7 @@ import { RushConfiguration } from '../../api/RushConfiguration'; import type { RushGlobalFolder } from '../../api/RushGlobalFolder'; import { Utilities } from '../../utilities/Utilities'; import type { PnpmWorkspaceFile } from '../pnpm/PnpmWorkspaceFile'; +import type { Subspace } from '../../api/Subspace'; describe(InstallHelpers.name, () => { describe(InstallHelpers.shouldProvideNpmrcCredentialsViaEnvironment.name, () => { @@ -390,4 +392,32 @@ describe(InstallHelpers.name, () => { expect(workspaceFile?.globalPnpmfile).toBeUndefined(); }); }); + + describe(InstallHelpers.resolvePnpmSettings.name, () => { + it('writes the same catalogs that the repo-state.json catalog hash is computed from', () => { + const RUSH_JSON_FILENAME: string = `${__dirname}/repoWithSubspacesCommonCatalogs/rush.json`; + const rushConfiguration: RushConfiguration = + RushConfiguration.loadFromConfigurationFile(RUSH_JSON_FILENAME); + const terminal: Terminal = new Terminal(new StringBufferTerminalProvider()); + const commonCatalogs: unknown = TestUtilities.stripAnnotations( + rushConfiguration.pnpmOptions.globalCatalogs + ); + + for (const [subspaceName, expectedCatalogs] of [ + ['default', commonCatalogs], + ['other-subspace', commonCatalogs], + ['own-config-subspace', { default: { react: '^17.0.0' } }] + ]) { + const subspace: Subspace = rushConfiguration.getSubspace(subspaceName as string); + const pnpmSettings = InstallHelpers.resolvePnpmSettings(rushConfiguration, subspace, terminal); + const catalogs: Record> | undefined = + TestUtilities.stripAnnotations(pnpmSettings)?.workspaceFile.catalogs; + + expect(catalogs).toEqual(expectedCatalogs); + expect(subspace.getPnpmCatalogsHash()).toEqual( + crypto.createHash('sha1').update(JSON.stringify(catalogs)).digest('hex') + ); + } + }); + }); }); diff --git a/libraries/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts b/libraries/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts index 618bd4c6948..e111588a792 100644 --- a/libraries/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts +++ b/libraries/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts @@ -1501,6 +1501,69 @@ describe(ProjectChangeAnalyzer.name, () => { expect(changedProjects.has(rushConfiguration.getProjectByName('f')!)).toBe(false); }); }); + + describe('catalog change detection for subspaces that use common/config/rush/pnpm-config.json', () => { + const COMMON_PNPM_CONFIG_PATH: string = 'common/config/rush/pnpm-config.json'; + + async function getChangedProjectNamesAsync( + changedFilePath: string, + changedFileStatus: IFileDiffStatus, + terminalProvider: StringBufferTerminalProvider + ): Promise { + mockGetRepoChanges.mockReturnValue(new Map([[changedFilePath, changedFileStatus]])); + // Only the common pnpm-config.json exists at the merge commit + mockGetBlobContentAsync.mockImplementation((opts: { blobSpec: string }) => + opts.blobSpec.endsWith(`:${COMMON_PNPM_CONFIG_PATH}`) + ? Promise.resolve( + JSON.stringify({ + globalCatalogs: { + default: { react: '^17.0.0', foo: '~1.0.0' }, + tools: { typescript: '~5.3.0' } + } + }) + ) + : Promise.reject(new Error('Not found')) + ); + + const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile( + resolve(__dirname, 'repoWithSubspacesCommonCatalogs', 'rush.json') + ); + const changedProjects: Set = await new ProjectChangeAnalyzer( + rushConfiguration + ).getChangedProjectsAsync({ + enableFiltering: false, + includeExternalDependencies: false, + targetBranchName: 'main', + terminal: new Terminal(terminalProvider) + }); + return Array.from(changedProjects, (project: RushConfigurationProject) => project.packageName).sort(); + } + + it('marks projects in every subspace that uses the common catalogs', async () => { + // "a" uses react from the default catalog and "c" (other-subspace) uses foo. "b" uses the unchanged + // tools catalog, and "e" uses own-config-subspace's own pnpm-config.json. + expect( + await getChangedProjectNamesAsync( + COMMON_PNPM_CONFIG_PATH, + { mode: 'modified', newhash: 'newhash', oldhash: 'oldhash', status: 'M' }, + new StringBufferTerminalProvider(true) + ) + ).toEqual(['a', 'c']); + }); + + it("compares against the common catalogs when a subspace's own pnpm-config.json is added", async () => { + // "e" uses react, which was already ^17.0.0 in the common catalogs + const terminalProvider: StringBufferTerminalProvider = new StringBufferTerminalProvider(true); + expect( + await getChangedProjectNamesAsync( + 'common/config/subspaces/own-config-subspace/pnpm-config.json', + { mode: 'added', newhash: 'newhash', oldhash: '', status: 'A' }, + terminalProvider + ) + ).toEqual([]); + expect(terminalProvider.getWarningOutput()).not.toContain('was created or unparseable'); + }); + }); }); describe('isPackageJsonVersionOnlyChange', () => { diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/a/package.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/a/package.json new file mode 100644 index 00000000000..5c102c007ba --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/a/package.json @@ -0,0 +1,8 @@ +{ + "name": "a", + "version": "1.0.0", + "description": "Test package a", + "dependencies": { + "react": "catalog:" + } +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/b/package.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/b/package.json new file mode 100644 index 00000000000..f662bd13865 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/b/package.json @@ -0,0 +1,8 @@ +{ + "name": "b", + "version": "1.0.0", + "description": "Test package b", + "devDependencies": { + "typescript": "catalog:tools" + } +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/c/package.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/c/package.json new file mode 100644 index 00000000000..03d6d9935e7 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/c/package.json @@ -0,0 +1,8 @@ +{ + "name": "c", + "version": "1.0.0", + "description": "Test package c", + "dependencies": { + "foo": "catalog:" + } +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/common/config/rush/pnpm-config.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/common/config/rush/pnpm-config.json new file mode 100644 index 00000000000..cede4f94767 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/common/config/rush/pnpm-config.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json", + "useWorkspaces": true, + "globalCatalogs": { + "default": { + "react": "^18.0.0", + "foo": "~2.0.0" + }, + "tools": { + "typescript": "~5.3.0" + } + } +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/common/config/rush/subspaces.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/common/config/rush/subspaces.json new file mode 100644 index 00000000000..9b41e2bfe21 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/common/config/rush/subspaces.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/subspaces.schema.json", + "subspacesEnabled": true, + "subspaceNames": ["other-subspace", "own-config-subspace"] +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/common/config/subspaces/default/common-versions.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/common/config/subspaces/default/common-versions.json new file mode 100644 index 00000000000..9280fe7b96d --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/common/config/subspaces/default/common-versions.json @@ -0,0 +1,8 @@ +/** + * This configuration file specifies NPM dependency version selections that affect all projects + * in a Rush repo. More documentation is available on the Rush website: https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/common-versions.schema.json", + "ensureConsistentVersions": true +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/common/config/subspaces/other-subspace/common-versions.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/common/config/subspaces/other-subspace/common-versions.json new file mode 100644 index 00000000000..9280fe7b96d --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/common/config/subspaces/other-subspace/common-versions.json @@ -0,0 +1,8 @@ +/** + * This configuration file specifies NPM dependency version selections that affect all projects + * in a Rush repo. More documentation is available on the Rush website: https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/common-versions.schema.json", + "ensureConsistentVersions": true +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/common/config/subspaces/own-config-subspace/common-versions.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/common/config/subspaces/own-config-subspace/common-versions.json new file mode 100644 index 00000000000..9280fe7b96d --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/common/config/subspaces/own-config-subspace/common-versions.json @@ -0,0 +1,8 @@ +/** + * This configuration file specifies NPM dependency version selections that affect all projects + * in a Rush repo. More documentation is available on the Rush website: https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/common-versions.schema.json", + "ensureConsistentVersions": true +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/common/config/subspaces/own-config-subspace/pnpm-config.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/common/config/subspaces/own-config-subspace/pnpm-config.json new file mode 100644 index 00000000000..949c6c7eab3 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/common/config/subspaces/own-config-subspace/pnpm-config.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json", + "globalCatalogs": { + "default": { + "react": "^17.0.0" + } + } +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/e/package.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/e/package.json new file mode 100644 index 00000000000..55df4b0048a --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/e/package.json @@ -0,0 +1,8 @@ +{ + "name": "e", + "version": "1.0.0", + "description": "Test package e", + "dependencies": { + "react": "catalog:" + } +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/rush.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/rush.json new file mode 100644 index 00000000000..48e4bf8c4a9 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCommonCatalogs/rush.json @@ -0,0 +1,25 @@ +{ + "rushVersion": "1.0.5", + "pnpmVersion": "9.15.0", + + "projects": [ + { + "packageName": "a", + "projectFolder": "a" + }, + { + "packageName": "b", + "projectFolder": "b" + }, + { + "packageName": "c", + "projectFolder": "c", + "subspaceName": "other-subspace" + }, + { + "packageName": "e", + "projectFolder": "e", + "subspaceName": "own-config-subspace" + } + ] +}