From 2591a264194896284dceea9bb9820558dcf7e190 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Tue, 25 Nov 2025 13:37:24 -0500 Subject: [PATCH 01/12] feat: add support for pnpm catalogs Signed-off-by: Aramis Sennyey --- common/reviews/api/rush-lib.api.md | 4 ++ .../logic/installManager/InstallHelpers.ts | 38 +++++++++++++++ .../installManager/WorkspaceInstallManager.ts | 47 +++++++++++++++++++ .../logic/pnpm/PnpmOptionsConfiguration.ts | 30 ++++++++++++ .../src/logic/pnpm/PnpmShrinkwrapFile.ts | 4 ++ .../test/PnpmOptionsConfiguration.test.ts | 24 ++++++++++ .../test/jsonFiles/pnpm-config-catalog.json | 17 +++++++ .../src/logic/test/InstallHelpers.test.ts | 9 ++++ .../__snapshots__/InstallHelpers.test.ts.snap | 2 +- .../common/config/rush/pnpm-config.json | 9 ++++ .../src/schemas/pnpm-config.schema.json | 22 +++++++++ 11 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-catalog.json diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index cf0b56b9abb..b4da63b8d0a 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -740,6 +740,8 @@ export interface _IPnpmOptionsJson extends IPackageManagerOptionsJsonBase { alwaysInjectDependenciesFromOtherSubspaces?: boolean; autoInstallPeers?: boolean; globalAllowedDeprecatedVersions?: Record; + globalCatalog?: Record; + globalCatalogs?: Record>; globalIgnoredOptionalDependencies?: string[]; globalNeverBuiltDependencies?: string[]; globalOverrides?: Record; @@ -1151,6 +1153,8 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration readonly alwaysInjectDependenciesFromOtherSubspaces: boolean | undefined; readonly autoInstallPeers: boolean | undefined; readonly globalAllowedDeprecatedVersions: Record | undefined; + readonly globalCatalog: Record | undefined; + readonly globalCatalogs: Record> | undefined; readonly globalIgnoredOptionalDependencies: string[] | undefined; readonly globalNeverBuiltDependencies: string[] | undefined; readonly globalOverrides: Record | undefined; diff --git a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts index 8f850b4e73d..4238304a659 100644 --- a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts +++ b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts @@ -36,6 +36,8 @@ interface ICommonPackageJson extends IPackageJson { patchedDependencies?: typeof PnpmOptionsConfiguration.prototype.globalPatchedDependencies; minimumReleaseAge?: typeof PnpmOptionsConfiguration.prototype.minimumReleaseAge; minimumReleaseAgeExclude?: typeof PnpmOptionsConfiguration.prototype.minimumReleaseAgeExclude; + catalog?: typeof PnpmOptionsConfiguration.prototype.globalCatalog; + catalogs?: typeof PnpmOptionsConfiguration.prototype.globalCatalogs; }; } @@ -126,6 +128,42 @@ export class InstallHelpers { } } + if (pnpmOptions.globalCatalog) { + if ( + rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined && + semver.lt(rushConfiguration.rushConfigurationJson.pnpmVersion, '9.5.0') + ) { + terminal.writeWarningLine( + Colorize.yellow( + `Your version of pnpm (${rushConfiguration.rushConfigurationJson.pnpmVersion}) ` + + `doesn't support the "globalCatalog" field in ` + + `${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` + + 'Remove this field or upgrade to pnpm 9.5.0 or newer.' + ) + ); + } + + commonPackageJson.pnpm.catalog = pnpmOptions.globalCatalog; + } + + if (pnpmOptions.globalCatalogs) { + if ( + rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined && + semver.lt(rushConfiguration.rushConfigurationJson.pnpmVersion, '9.5.0') + ) { + terminal.writeWarningLine( + Colorize.yellow( + `Your version of pnpm (${rushConfiguration.rushConfigurationJson.pnpmVersion}) ` + + `doesn't support the "globalCatalogs" field in ` + + `${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` + + 'Remove this field or upgrade to pnpm 9.5.0 or newer.' + ) + ); + } + + commonPackageJson.pnpm.catalogs = pnpmOptions.globalCatalogs; + } + if (pnpmOptions.unsupportedPackageJsonSettings) { merge(commonPackageJson, pnpmOptions.unsupportedPackageJsonSettings); } diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index 9411c549e43..ffeda0700fc 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -440,6 +440,53 @@ export class WorkspaceInstallManager extends BaseInstallManager { shrinkwrapIsUpToDate = false; } + // Check if catalogsChecksum matches catalog's hash + let catalogsChecksum: string | undefined; + let existingCatalogsChecksum: string | undefined; + if (shrinkwrapFile) { + existingCatalogsChecksum = shrinkwrapFile.catalogsChecksum; + let catalogsChecksumAlgorithm: string | undefined; + if (existingCatalogsChecksum) { + const dashIndex: number = existingCatalogsChecksum.indexOf('-'); + if (dashIndex !== -1) { + catalogsChecksumAlgorithm = existingCatalogsChecksum.substring(0, dashIndex); + } + + if (catalogsChecksumAlgorithm && catalogsChecksumAlgorithm !== 'sha256') { + this._terminal.writeErrorLine( + `The existing catalogsChecksum algorithm "${catalogsChecksumAlgorithm}" is not supported. ` + + `This may indicate that the shrinkwrap was created with a newer version of PNPM than Rush supports.` + ); + throw new AlreadyReportedError(); + } + } + + // Combine both catalog and catalogs into a single object for checksum calculation + const catalogData: Record = {}; + if (pnpmOptions.globalCatalog && Object.keys(pnpmOptions.globalCatalog).length !== 0) { + catalogData.default = pnpmOptions.globalCatalog; + } + if (pnpmOptions.globalCatalogs && Object.keys(pnpmOptions.globalCatalogs).length !== 0) { + Object.assign(catalogData, pnpmOptions.globalCatalogs); + } + + if (Object.keys(catalogData).length !== 0) { + if (catalogsChecksumAlgorithm) { + // In PNPM v10, the algorithm changed to SHA256 and the digest changed from hex to base64 + catalogsChecksum = await createObjectChecksumAsync(catalogData); + } else { + catalogsChecksum = createObjectChecksumLegacy(catalogData); + } + } + } + + const catalogsChecksumAreEqual: boolean = catalogsChecksum === existingCatalogsChecksum; + + if (!catalogsChecksumAreEqual) { + shrinkwrapWarnings.push("The catalog hash doesn't match the current shrinkwrap."); + shrinkwrapIsUpToDate = false; + } + // Write the common package.json InstallHelpers.generateCommonPackageJson(this.rushConfiguration, subspace, undefined, this._terminal); diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts index 4b249e7a883..a00f778cb0a 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts @@ -158,6 +158,14 @@ export interface IPnpmOptionsJson extends IPackageManagerOptionsJsonBase { * {@inheritDoc PnpmOptionsConfiguration.pnpmLockfilePolicies} */ pnpmLockfilePolicies?: IPnpmLockfilePolicies; + /** + * {@inheritDoc PnpmOptionsConfiguration.globalCatalog} + */ + globalCatalog?: Record; + /** + * {@inheritDoc PnpmOptionsConfiguration.globalCatalogs} + */ + globalCatalogs?: Record>; } /** @@ -421,6 +429,26 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration /*[LINE "DEMO"]*/ public readonly alwaysFullInstall: boolean | undefined; + /** + * The `globalCatalog` setting provides a centralized way to define dependency versions + * that can be referenced using the `catalog:` protocol in package.json files. + * The settings are copied into the `pnpm.catalog` field of the `common/temp/package.json` + * file that is generated by Rush during installation. + * + * PNPM documentation: https://pnpm.io/catalogs + */ + public readonly globalCatalog: Record | undefined; + + /** + * The `globalCatalogs` setting provides named catalogs for organizing dependency versions. + * Each catalog can be referenced using the `catalog:catalogName:` protocol in package.json files. + * The settings are copied into the `pnpm.catalogs` field of the `common/temp/package.json` + * file that is generated by Rush during installation. + * + * PNPM documentation: https://pnpm.io/catalogs + */ + public readonly globalCatalogs: Record> | undefined; + /** * (GENERATED BY RUSH-PNPM PATCH-COMMIT) When modifying this property, make sure you know what you are doing. * @@ -465,6 +493,8 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration this.alwaysInjectDependenciesFromOtherSubspaces = json.alwaysInjectDependenciesFromOtherSubspaces; this.alwaysFullInstall = json.alwaysFullInstall; this.pnpmLockfilePolicies = json.pnpmLockfilePolicies; + this.globalCatalog = json.globalCatalog; + this.globalCatalogs = json.globalCatalogs; } /** @internal */ diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts index ab61818fee1..842047def70 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts @@ -121,6 +121,8 @@ export interface IPnpmShrinkwrapYaml extends Lockfile { specifiers?: Record; /** URL of the registry which was used */ registry?: string; + /** The checksum for catalog definitions */ + catalogsChecksum?: string; } export interface ILoadFromFileOptions { @@ -310,6 +312,7 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { public readonly packages: ReadonlyMap; public readonly overrides: ReadonlyMap; public readonly packageExtensionsChecksum: undefined | string; + public readonly catalogsChecksum: undefined | string; public readonly hash: string; private readonly _shrinkwrapJson: IPnpmShrinkwrapYaml; @@ -343,6 +346,7 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { this.packages = new Map(Object.entries(shrinkwrapJson.packages || {})); this.overrides = new Map(Object.entries(shrinkwrapJson.overrides || {})); this.packageExtensionsChecksum = shrinkwrapJson.packageExtensionsChecksum; + this.catalogsChecksum = shrinkwrapJson.catalogsChecksum; // Lockfile v9 always has "." in importers filed. this.isWorkspaceCompatible = diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts index 75b9e3e874f..372771f54fe 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts @@ -86,4 +86,28 @@ describe(PnpmOptionsConfiguration.name, () => { '@myorg/*' ]); }); + + it('loads catalog and catalogs', () => { + const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( + `${__dirname}/jsonFiles/pnpm-config-catalog.json`, + fakeCommonTempFolder + ); + + expect(TestUtilities.stripAnnotations(pnpmConfiguration.globalCatalog)).toEqual({ + react: '^18.0.0', + 'react-dom': '^18.0.0', + typescript: '~5.3.0' + }); + + expect(TestUtilities.stripAnnotations(pnpmConfiguration.globalCatalogs)).toEqual({ + frontend: { + vue: '^3.4.0', + 'vue-router': '^4.2.0' + }, + backend: { + express: '^4.18.0', + fastify: '^4.26.0' + } + }); + }); }); diff --git a/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-catalog.json b/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-catalog.json new file mode 100644 index 00000000000..eb8b11abe41 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-catalog.json @@ -0,0 +1,17 @@ +{ + "globalCatalog": { + "react": "^18.0.0", + "react-dom": "^18.0.0", + "typescript": "~5.3.0" + }, + "globalCatalogs": { + "frontend": { + "vue": "^3.4.0", + "vue-router": "^4.2.0" + }, + "backend": { + "express": "^4.18.0", + "fastify": "^4.26.0" + } + } +} diff --git a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts index d161d5da632..784d25f56f5 100644 --- a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts +++ b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts @@ -67,6 +67,15 @@ describe('InstallHelpers', () => { } }, neverBuiltDependencies: ['fsevents', 'level'], + catalog: { + react: '^18.0.0', + lodash: '^4.17.21' + }, + catalogs: { + test: { + jest: '^29.0.0' + } + }, pnpmFutureFeature: true } }) 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 7f122921b53..e260b76eb7f 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 @@ -6,6 +6,6 @@ Object { "error": "", "output": "", "verbose": "", - "warning": "", + "warning": "Your version of pnpm (6.23.1) doesn't support the \\"globalCatalog\\" field in /Users/aramis.sennyey/Projects/rushstack/libraries/rush-lib/lib-commonjs/logic/test/pnpmConfig/common/config/rush/pnpm-config.json. Remove this field or upgrade to pnpm 9.5.0 or newer.[n]Your version of pnpm (6.23.1) doesn't support the \\"globalCatalogs\\" field in /Users/aramis.sennyey/Projects/rushstack/libraries/rush-lib/lib-commonjs/logic/test/pnpmConfig/common/config/rush/pnpm-config.json. Remove this field or upgrade to pnpm 9.5.0 or newer.[n]", } `; diff --git a/libraries/rush-lib/src/logic/test/pnpmConfig/common/config/rush/pnpm-config.json b/libraries/rush-lib/src/logic/test/pnpmConfig/common/config/rush/pnpm-config.json index b5d6f9baba8..8053f6cb05c 100644 --- a/libraries/rush-lib/src/logic/test/pnpmConfig/common/config/rush/pnpm-config.json +++ b/libraries/rush-lib/src/logic/test/pnpmConfig/common/config/rush/pnpm-config.json @@ -13,6 +13,15 @@ } }, "globalNeverBuiltDependencies": ["fsevents", "level"], + "globalCatalog": { + "react": "^18.0.0", + "lodash": "^4.17.21" + }, + "globalCatalogs": { + "test": { + "jest": "^29.0.0" + } + }, "unsupportedPackageJsonSettings": { "pnpm": { "overrides": { diff --git a/libraries/rush-lib/src/schemas/pnpm-config.schema.json b/libraries/rush-lib/src/schemas/pnpm-config.schema.json index 0501173b754..d3315d2d48b 100644 --- a/libraries/rush-lib/src/schemas/pnpm-config.schema.json +++ b/libraries/rush-lib/src/schemas/pnpm-config.schema.json @@ -237,6 +237,28 @@ "required": ["enabled", "exemptPackageVersions"] } } + }, + + "globalCatalog": { + "description": "The \"globalCatalog\" setting provides a centralized way to define dependency versions that can be referenced using the `catalog:` protocol in package.json files. This allows for consistent version management across all projects in the monorepo. The settings are copied into the `pnpm.catalog` field of the `common/temp/package.json` file that is generated by Rush during installation.\n\nPNPM documentation: https://pnpm.io/catalogs", + "type": "object", + "additionalProperties": { + "description": "Specify the version for a package in the catalog", + "type": "string" + } + }, + + "globalCatalogs": { + "description": "The \"globalCatalogs\" setting provides named catalogs for organizing dependency versions. Each catalog can be referenced using the `catalog:catalogName:` protocol in package.json files. The settings are copied into the `pnpm.catalogs` field of the `common/temp/package.json` file that is generated by Rush during installation.\n\nPNPM documentation: https://pnpm.io/catalogs", + "type": "object", + "additionalProperties": { + "description": "A named catalog containing package versions", + "type": "object", + "additionalProperties": { + "description": "Specify the version for a package in this catalog", + "type": "string" + } + } } } } From 9a6e9b26600e4b10eba7283c38729efa7fdc4a47 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Tue, 25 Nov 2025 13:38:24 -0500 Subject: [PATCH 02/12] add changeset Signed-off-by: Aramis Sennyey --- .../rush/sennyeya-pnpm-catalog_2025-11-25-18-38.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/@microsoft/rush/sennyeya-pnpm-catalog_2025-11-25-18-38.json diff --git a/common/changes/@microsoft/rush/sennyeya-pnpm-catalog_2025-11-25-18-38.json b/common/changes/@microsoft/rush/sennyeya-pnpm-catalog_2025-11-25-18-38.json new file mode 100644 index 00000000000..afcb7b99bb0 --- /dev/null +++ b/common/changes/@microsoft/rush/sennyeya-pnpm-catalog_2025-11-25-18-38.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "Add support for defining pnpm catalog config.", + "type": "none", + "packageName": "@microsoft/rush" + } + ], + "packageName": "@microsoft/rush", + "email": "aramissennyeydd@users.noreply.github.com" +} \ No newline at end of file From 4e52c508810d3d558c945532c6df371b25331e3b Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Tue, 25 Nov 2025 14:51:28 -0500 Subject: [PATCH 03/12] move to pnpm-workspace file to let catalog: specifier be picked up Signed-off-by: Aramis Sennyey --- .../rush-lib/src/logic/DependencySpecifier.ts | 47 +++- .../logic/installManager/InstallHelpers.ts | 38 ---- .../installManager/WorkspaceInstallManager.ts | 30 +++ .../src/logic/pnpm/PnpmWorkspaceFile.ts | 24 +- .../logic/pnpm/test/PnpmWorkspaceFile.test.ts | 208 ++++++++++++++++++ .../logic/test/DependencySpecifier.test.ts | 26 +++ .../src/logic/test/InstallHelpers.test.ts | 9 - .../__snapshots__/InstallHelpers.test.ts.snap | 2 +- 8 files changed, 334 insertions(+), 50 deletions(-) create mode 100644 libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts diff --git a/libraries/rush-lib/src/logic/DependencySpecifier.ts b/libraries/rush-lib/src/logic/DependencySpecifier.ts index 52eefceb0dc..f43e4110f9a 100644 --- a/libraries/rush-lib/src/logic/DependencySpecifier.ts +++ b/libraries/rush-lib/src/logic/DependencySpecifier.ts @@ -13,6 +13,14 @@ import { InternalError } from '@rushstack/node-core-library'; */ const WORKSPACE_PREFIX_REGEX: RegExp = /^workspace:((?[^._/][^@]*)@)?(?.*)$/; +/** + * match catalog protocol in dependencies value declaration in `package.json` + * example: + * `"catalog:"` - references the default catalog + * `"catalog:catalogName"` - references a named catalog + */ +const CATALOG_PREFIX_REGEX: RegExp = /^catalog:(?.*)$/; + /** * resolve workspace protocol(from `@pnpm/workspace.spec-parser`). * used by pnpm. see [pkgs-graph](https://github.com/pnpm/pnpm/blob/27c33f0319f86c45c1645d064cd9c28aada80780/workspace/pkgs-graph/src/index.ts#L49) @@ -40,6 +48,29 @@ class WorkspaceSpec { } } +/** + * resolve catalog protocol. + * Used by pnpm for centralized version management via catalogs. + */ +class CatalogSpec { + public readonly catalogName: string; + + public constructor(catalogName: string) { + this.catalogName = catalogName; + } + + public static tryParse(pref: string): CatalogSpec | undefined { + const parts: RegExpExecArray | null = CATALOG_PREFIX_REGEX.exec(pref); + if (parts?.groups !== undefined) { + return new CatalogSpec(parts.groups.catalogName); + } + } + + public toString(): `catalog:${string}` { + return `catalog:${this.catalogName}`; + } +} + /** * The parsed format of a provided version specifier. */ @@ -87,7 +118,12 @@ export enum DependencySpecifierType { /** * A package specified using workspace protocol, e.g. "workspace:^1.2.3" */ - Workspace = 'Workspace' + Workspace = 'Workspace', + + /** + * A package specified using catalog protocol, e.g. "catalog:" or "catalog:react18" + */ + Catalog = 'Catalog' } const dependencySpecifierParseCache: Map = new Map(); @@ -125,6 +161,15 @@ export class DependencySpecifier { this.packageName = packageName; this.versionSpecifier = versionSpecifier; + // Catalog protocol is a feature from PNPM for centralized version management + const catalogSpecResult: CatalogSpec | undefined = CatalogSpec.tryParse(versionSpecifier); + if (catalogSpecResult) { + this.specifierType = DependencySpecifierType.Catalog; + this.versionSpecifier = catalogSpecResult.catalogName; + this.aliasTarget = undefined; + return; + } + // Workspace ranges are a feature from PNPM and Yarn. Set the version specifier // to the trimmed version range. const workspaceSpecResult: WorkspaceSpec | undefined = WorkspaceSpec.tryParse(versionSpecifier); diff --git a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts index 4238304a659..8f850b4e73d 100644 --- a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts +++ b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts @@ -36,8 +36,6 @@ interface ICommonPackageJson extends IPackageJson { patchedDependencies?: typeof PnpmOptionsConfiguration.prototype.globalPatchedDependencies; minimumReleaseAge?: typeof PnpmOptionsConfiguration.prototype.minimumReleaseAge; minimumReleaseAgeExclude?: typeof PnpmOptionsConfiguration.prototype.minimumReleaseAgeExclude; - catalog?: typeof PnpmOptionsConfiguration.prototype.globalCatalog; - catalogs?: typeof PnpmOptionsConfiguration.prototype.globalCatalogs; }; } @@ -128,42 +126,6 @@ export class InstallHelpers { } } - if (pnpmOptions.globalCatalog) { - if ( - rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined && - semver.lt(rushConfiguration.rushConfigurationJson.pnpmVersion, '9.5.0') - ) { - terminal.writeWarningLine( - Colorize.yellow( - `Your version of pnpm (${rushConfiguration.rushConfigurationJson.pnpmVersion}) ` + - `doesn't support the "globalCatalog" field in ` + - `${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` + - 'Remove this field or upgrade to pnpm 9.5.0 or newer.' - ) - ); - } - - commonPackageJson.pnpm.catalog = pnpmOptions.globalCatalog; - } - - if (pnpmOptions.globalCatalogs) { - if ( - rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined && - semver.lt(rushConfiguration.rushConfigurationJson.pnpmVersion, '9.5.0') - ) { - terminal.writeWarningLine( - Colorize.yellow( - `Your version of pnpm (${rushConfiguration.rushConfigurationJson.pnpmVersion}) ` + - `doesn't support the "globalCatalogs" field in ` + - `${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` + - 'Remove this field or upgrade to pnpm 9.5.0 or newer.' - ) - ); - } - - commonPackageJson.pnpm.catalogs = pnpmOptions.globalCatalogs; - } - if (pnpmOptions.unsupportedPackageJsonSettings) { merge(commonPackageJson, pnpmOptions.unsupportedPackageJsonSettings); } diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index ffeda0700fc..b49fbc2d135 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -490,6 +490,36 @@ export class WorkspaceInstallManager extends BaseInstallManager { // Write the common package.json InstallHelpers.generateCommonPackageJson(this.rushConfiguration, subspace, undefined, this._terminal); + // Set catalog definitions in the workspace file if specified + if (pnpmOptions.globalCatalog || pnpmOptions.globalCatalogs) { + if ( + this.rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined && + semver.lt(this.rushConfiguration.rushConfigurationJson.pnpmVersion, '9.5.0') + ) { + this._terminal.writeWarningLine( + Colorize.yellow( + `Your version of pnpm (${this.rushConfiguration.rushConfigurationJson.pnpmVersion}) ` + + `doesn't support the "globalCatalog" or "globalCatalogs" fields in ` + + `${this.rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` + + 'Remove these fields or upgrade to pnpm 9.5.0 or newer.' + ) + ); + } + + const catalogs: Record> = {}; + + if (pnpmOptions.globalCatalog) { + // https://pnpm.io/catalogs#default-catalog, basically `catalog` is an alias for `catalogs.default` in pnpm. + catalogs.default = pnpmOptions.globalCatalog; + } + + if (pnpmOptions.globalCatalogs) { + Object.assign(catalogs, pnpmOptions.globalCatalogs); + } + + workspaceFile.setCatalogs(catalogs); + } + // 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 }); diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts index 8ecccd04c0f..f2d86e24dc5 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts @@ -18,12 +18,19 @@ const globEscape: (unescaped: string) => string = require('glob-escape'); // No * { * "packages": [ * "../../apps/project1" - * ] + * ], + * "catalogs": { + * "default": { + * "react": "^18.0.0" + * } + * } * } */ interface IPnpmWorkspaceYaml { /** The list of local package directories */ packages: string[]; + /** Catalog definitions for centralized version management */ + catalogs?: Record>; } export class PnpmWorkspaceFile extends BaseWorkspaceFile { @@ -33,6 +40,7 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { public readonly workspaceFilename: string; private _workspacePackages: Set; + private _catalogs: Record> | undefined; /** * The PNPM workspace file is used to specify the location of workspaces relative to the root @@ -45,6 +53,15 @@ 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; + } + + /** + * 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; } /** @override */ @@ -67,6 +84,11 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { const workspaceYaml: IPnpmWorkspaceYaml = { packages: Array.from(this._workspacePackages) }; + + if (this._catalogs && Object.keys(this._catalogs).length > 0) { + workspaceYaml.catalogs = this._catalogs; + } + 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 new file mode 100644 index 00000000000..6f98777d71f --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts @@ -0,0 +1,208 @@ +// 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'); + + beforeEach(() => { + FileSystem.ensureFolder(tempDir); + }); + + afterEach(() => { + if (FileSystem.exists(tempDir)) { + FileSystem.deleteFolder(tempDir); + } + }); + + describe('basic functionality', () => { + it('generates workspace file with packages only', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage('/projects/app1'); + workspaceFile.addPackage('/projects/app2'); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toMatchInlineSnapshot(` +"packages: + - ../../../../../../../../../../../projects/app1 + - ../../../../../../../../../../../projects/app2 +" +`); + }); + + it('escapes special characters in package paths', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage('/projects/[app-with-brackets]'); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toContain('\\[app-with-brackets\\]'); + }); + }); + + describe('catalog functionality', () => { + it('generates workspace file with default catalog only', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage('/projects/app1'); + + workspaceFile.setCatalogs({ + default: { + react: '^18.0.0', + 'react-dom': '^18.0.0', + typescript: '~5.3.0' + } + }); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toMatchInlineSnapshot(` +"catalogs: + default: + react: ^18.0.0 + react-dom: ^18.0.0 + typescript: ~5.3.0 +packages: + - ../../../../../../../../../../../projects/app1 +" +`); + }); + + it('generates workspace file with named catalogs', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage('/projects/app1'); + + workspaceFile.setCatalogs({ + default: { + typescript: '~5.3.0' + }, + frontend: { + vue: '^3.4.0', + 'vue-router': '^4.2.0' + }, + backend: { + express: '^4.18.0', + fastify: '^4.26.0' + } + }); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toMatchInlineSnapshot(` +"catalogs: + backend: + express: ^4.18.0 + fastify: ^4.26.0 + default: + typescript: ~5.3.0 + frontend: + vue: ^3.4.0 + vue-router: ^4.2.0 +packages: + - ../../../../../../../../../../../projects/app1 +" +`); + }); + + it('handles empty catalog object', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage('/projects/app1'); + + workspaceFile.setCatalogs({}); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toMatchInlineSnapshot(` +"packages: + - ../../../../../../../../../../../projects/app1 +" +`); + }); + + it('handles undefined catalog', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage('/projects/app1'); + + workspaceFile.setCatalogs(undefined); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toMatchInlineSnapshot(` +"packages: + - ../../../../../../../../../../../projects/app1 +" +`); + }); + + it('handles scoped packages in catalogs', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage('/projects/app1'); + + workspaceFile.setCatalogs({ + default: { + '@types/node': '~22.9.4', + '@types/cookies': '^0.7.7', + '@rushstack/node-core-library': '~5.0.0' + } + }); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toMatchInlineSnapshot(` +"catalogs: + default: + '@rushstack/node-core-library': ~5.0.0 + '@types/cookies': ^0.7.7 + '@types/node': ~22.9.4 +packages: + - ../../../../../../../../../../../projects/app1 +" +`); + }); + + it('can update catalogs after initial creation', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage('/projects/app1'); + + workspaceFile.setCatalogs({ + default: { + react: '^18.0.0' + } + }); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + // Update catalogs + workspaceFile.setCatalogs({ + default: { + react: '^18.2.0', + 'react-dom': '^18.2.0' + } + }); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toMatchInlineSnapshot(` +"catalogs: + default: + react: ^18.2.0 + react-dom: ^18.2.0 +packages: + - ../../../../../../../../../../../projects/app1 +" +`); + }); + }); +}); diff --git a/libraries/rush-lib/src/logic/test/DependencySpecifier.test.ts b/libraries/rush-lib/src/logic/test/DependencySpecifier.test.ts index 1d738bf9b0d..9ec30a66024 100644 --- a/libraries/rush-lib/src/logic/test/DependencySpecifier.test.ts +++ b/libraries/rush-lib/src/logic/test/DependencySpecifier.test.ts @@ -140,6 +140,32 @@ DependencySpecifier { }); }); + describe('Catalog protocol', () => { + it('correctly parses a "catalog:" version (default catalog)', () => { + const specifier = new DependencySpecifier('dep', 'catalog:'); + expect(specifier).toMatchInlineSnapshot(` +DependencySpecifier { + "aliasTarget": undefined, + "packageName": "dep", + "specifierType": "Catalog", + "versionSpecifier": "", +} +`); + }); + + it('correctly parses a "catalog:catalogName" version (named catalog)', () => { + const specifier = new DependencySpecifier('dep', 'catalog:react18'); + expect(specifier).toMatchInlineSnapshot(` +DependencySpecifier { + "aliasTarget": undefined, + "packageName": "dep", + "specifierType": "Catalog", + "versionSpecifier": "react18", +} +`); + }); + }); + describe(DependencySpecifier.parseWithCache.name, () => { it('returns a cached instance for the same input', () => { const specifier1 = DependencySpecifier.parseWithCache('dep', '1.2.3'); diff --git a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts index 784d25f56f5..d161d5da632 100644 --- a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts +++ b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts @@ -67,15 +67,6 @@ describe('InstallHelpers', () => { } }, neverBuiltDependencies: ['fsevents', 'level'], - catalog: { - react: '^18.0.0', - lodash: '^4.17.21' - }, - catalogs: { - test: { - jest: '^29.0.0' - } - }, pnpmFutureFeature: true } }) 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 e260b76eb7f..7f122921b53 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 @@ -6,6 +6,6 @@ Object { "error": "", "output": "", "verbose": "", - "warning": "Your version of pnpm (6.23.1) doesn't support the \\"globalCatalog\\" field in /Users/aramis.sennyey/Projects/rushstack/libraries/rush-lib/lib-commonjs/logic/test/pnpmConfig/common/config/rush/pnpm-config.json. Remove this field or upgrade to pnpm 9.5.0 or newer.[n]Your version of pnpm (6.23.1) doesn't support the \\"globalCatalogs\\" field in /Users/aramis.sennyey/Projects/rushstack/libraries/rush-lib/lib-commonjs/logic/test/pnpmConfig/common/config/rush/pnpm-config.json. Remove this field or upgrade to pnpm 9.5.0 or newer.[n]", + "warning": "", } `; From 19b870fab689919c1ef8f76232f4d7d2cc2d5d7a Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Tue, 25 Nov 2025 15:01:11 -0500 Subject: [PATCH 04/12] remove catalog checksum Signed-off-by: Aramis Sennyey --- .../installManager/WorkspaceInstallManager.ts | 47 ------------------- .../src/logic/pnpm/PnpmShrinkwrapFile.ts | 4 -- 2 files changed, 51 deletions(-) diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index b49fbc2d135..94b9b3490ba 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -440,53 +440,6 @@ export class WorkspaceInstallManager extends BaseInstallManager { shrinkwrapIsUpToDate = false; } - // Check if catalogsChecksum matches catalog's hash - let catalogsChecksum: string | undefined; - let existingCatalogsChecksum: string | undefined; - if (shrinkwrapFile) { - existingCatalogsChecksum = shrinkwrapFile.catalogsChecksum; - let catalogsChecksumAlgorithm: string | undefined; - if (existingCatalogsChecksum) { - const dashIndex: number = existingCatalogsChecksum.indexOf('-'); - if (dashIndex !== -1) { - catalogsChecksumAlgorithm = existingCatalogsChecksum.substring(0, dashIndex); - } - - if (catalogsChecksumAlgorithm && catalogsChecksumAlgorithm !== 'sha256') { - this._terminal.writeErrorLine( - `The existing catalogsChecksum algorithm "${catalogsChecksumAlgorithm}" is not supported. ` + - `This may indicate that the shrinkwrap was created with a newer version of PNPM than Rush supports.` - ); - throw new AlreadyReportedError(); - } - } - - // Combine both catalog and catalogs into a single object for checksum calculation - const catalogData: Record = {}; - if (pnpmOptions.globalCatalog && Object.keys(pnpmOptions.globalCatalog).length !== 0) { - catalogData.default = pnpmOptions.globalCatalog; - } - if (pnpmOptions.globalCatalogs && Object.keys(pnpmOptions.globalCatalogs).length !== 0) { - Object.assign(catalogData, pnpmOptions.globalCatalogs); - } - - if (Object.keys(catalogData).length !== 0) { - if (catalogsChecksumAlgorithm) { - // In PNPM v10, the algorithm changed to SHA256 and the digest changed from hex to base64 - catalogsChecksum = await createObjectChecksumAsync(catalogData); - } else { - catalogsChecksum = createObjectChecksumLegacy(catalogData); - } - } - } - - const catalogsChecksumAreEqual: boolean = catalogsChecksum === existingCatalogsChecksum; - - if (!catalogsChecksumAreEqual) { - shrinkwrapWarnings.push("The catalog hash doesn't match the current shrinkwrap."); - shrinkwrapIsUpToDate = false; - } - // Write the common package.json InstallHelpers.generateCommonPackageJson(this.rushConfiguration, subspace, undefined, this._terminal); diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts index 842047def70..ab61818fee1 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts @@ -121,8 +121,6 @@ export interface IPnpmShrinkwrapYaml extends Lockfile { specifiers?: Record; /** URL of the registry which was used */ registry?: string; - /** The checksum for catalog definitions */ - catalogsChecksum?: string; } export interface ILoadFromFileOptions { @@ -312,7 +310,6 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { public readonly packages: ReadonlyMap; public readonly overrides: ReadonlyMap; public readonly packageExtensionsChecksum: undefined | string; - public readonly catalogsChecksum: undefined | string; public readonly hash: string; private readonly _shrinkwrapJson: IPnpmShrinkwrapYaml; @@ -346,7 +343,6 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { this.packages = new Map(Object.entries(shrinkwrapJson.packages || {})); this.overrides = new Map(Object.entries(shrinkwrapJson.overrides || {})); this.packageExtensionsChecksum = shrinkwrapJson.packageExtensionsChecksum; - this.catalogsChecksum = shrinkwrapJson.catalogsChecksum; // Lockfile v9 always has "." in importers filed. this.isWorkspaceCompatible = From 6140bc1b23d9c96d593f1bdb573558e6b8462f84 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Tue, 25 Nov 2025 15:18:06 -0500 Subject: [PATCH 05/12] add back checksum Signed-off-by: Aramis Sennyey --- .../installManager/WorkspaceInstallManager.ts | 47 +++++++++++++++++++ .../src/logic/pnpm/PnpmShrinkwrapFile.ts | 4 ++ .../pnpm/test/PnpmShrinkwrapFile.test.ts | 16 +++++++ .../yamlFiles/pnpm-lock-with-catalog.yaml | 36 ++++++++++++++ 4 files changed, 103 insertions(+) create mode 100644 libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-with-catalog.yaml diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index 94b9b3490ba..b49fbc2d135 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -440,6 +440,53 @@ export class WorkspaceInstallManager extends BaseInstallManager { shrinkwrapIsUpToDate = false; } + // Check if catalogsChecksum matches catalog's hash + let catalogsChecksum: string | undefined; + let existingCatalogsChecksum: string | undefined; + if (shrinkwrapFile) { + existingCatalogsChecksum = shrinkwrapFile.catalogsChecksum; + let catalogsChecksumAlgorithm: string | undefined; + if (existingCatalogsChecksum) { + const dashIndex: number = existingCatalogsChecksum.indexOf('-'); + if (dashIndex !== -1) { + catalogsChecksumAlgorithm = existingCatalogsChecksum.substring(0, dashIndex); + } + + if (catalogsChecksumAlgorithm && catalogsChecksumAlgorithm !== 'sha256') { + this._terminal.writeErrorLine( + `The existing catalogsChecksum algorithm "${catalogsChecksumAlgorithm}" is not supported. ` + + `This may indicate that the shrinkwrap was created with a newer version of PNPM than Rush supports.` + ); + throw new AlreadyReportedError(); + } + } + + // Combine both catalog and catalogs into a single object for checksum calculation + const catalogData: Record = {}; + if (pnpmOptions.globalCatalog && Object.keys(pnpmOptions.globalCatalog).length !== 0) { + catalogData.default = pnpmOptions.globalCatalog; + } + if (pnpmOptions.globalCatalogs && Object.keys(pnpmOptions.globalCatalogs).length !== 0) { + Object.assign(catalogData, pnpmOptions.globalCatalogs); + } + + if (Object.keys(catalogData).length !== 0) { + if (catalogsChecksumAlgorithm) { + // In PNPM v10, the algorithm changed to SHA256 and the digest changed from hex to base64 + catalogsChecksum = await createObjectChecksumAsync(catalogData); + } else { + catalogsChecksum = createObjectChecksumLegacy(catalogData); + } + } + } + + const catalogsChecksumAreEqual: boolean = catalogsChecksum === existingCatalogsChecksum; + + if (!catalogsChecksumAreEqual) { + shrinkwrapWarnings.push("The catalog hash doesn't match the current shrinkwrap."); + shrinkwrapIsUpToDate = false; + } + // Write the common package.json InstallHelpers.generateCommonPackageJson(this.rushConfiguration, subspace, undefined, this._terminal); diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts index ab61818fee1..842047def70 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts @@ -121,6 +121,8 @@ export interface IPnpmShrinkwrapYaml extends Lockfile { specifiers?: Record; /** URL of the registry which was used */ registry?: string; + /** The checksum for catalog definitions */ + catalogsChecksum?: string; } export interface ILoadFromFileOptions { @@ -310,6 +312,7 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { public readonly packages: ReadonlyMap; public readonly overrides: ReadonlyMap; public readonly packageExtensionsChecksum: undefined | string; + public readonly catalogsChecksum: undefined | string; public readonly hash: string; private readonly _shrinkwrapJson: IPnpmShrinkwrapYaml; @@ -343,6 +346,7 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { this.packages = new Map(Object.entries(shrinkwrapJson.packages || {})); this.overrides = new Map(Object.entries(shrinkwrapJson.overrides || {})); this.packageExtensionsChecksum = shrinkwrapJson.packageExtensionsChecksum; + this.catalogsChecksum = shrinkwrapJson.catalogsChecksum; // Lockfile v9 always has "." in importers filed. this.isWorkspaceCompatible = diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts index aa1170e999b..142d52b6cbb 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts @@ -380,6 +380,22 @@ describe(PnpmShrinkwrapFile.name, () => { }); }); }); + + describe('Catalog checksum', () => { + it('reads catalogsChecksum from pnpm-lock.yaml', () => { + const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( + `${__dirname}/yamlFiles/pnpm-lock-with-catalog.yaml` + ); + expect(pnpmShrinkwrapFile.catalogsChecksum).toBe('1a2b3c4d5e6f7890abcdef1234567890'); + }); + + it('returns undefined when catalogsChecksum is not present', () => { + const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( + `${__dirname}/yamlFiles/pnpm-lock-v9/pnpm-lock-v9.yaml` + ); + expect(pnpmShrinkwrapFile.catalogsChecksum).toBeUndefined(); + }); + }); }); function getPnpmShrinkwrapFileFromFile(filepath: string): PnpmShrinkwrapFile { diff --git a/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-with-catalog.yaml b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-with-catalog.yaml new file mode 100644 index 00000000000..27f534f073a --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-with-catalog.yaml @@ -0,0 +1,36 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +catalogsChecksum: 1a2b3c4d5e6f7890abcdef1234567890 + +catalogs: + default: + react: 18.2.0 + typescript: 5.3.0 + frontend: + vue: 3.4.0 + +importers: + .: + dependencies: + react: + specifier: 'catalog:' + version: 18.2.0 + typescript: + specifier: 'catalog:' + version: 5.3.0 + +packages: + react@18.2.0: + resolution: { integrity: sha512-abc123 } + + typescript@5.3.0: + resolution: { integrity: sha512-def456 } + +snapshots: + react@18.2.0: {} + + typescript@5.3.0: {} From b3aaaf5edb53320c43f4286c295f24fc5ab39e19 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Tue, 25 Nov 2025 15:31:45 -0500 Subject: [PATCH 06/12] make test directory agnostic Signed-off-by: Aramis Sennyey --- .../logic/pnpm/test/PnpmWorkspaceFile.test.ts | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) 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 6f98777d71f..63bc7507953 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts @@ -8,9 +8,11 @@ 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'); beforeEach(() => { FileSystem.ensureFolder(tempDir); + FileSystem.ensureFolder(projectsDir); }); afterEach(() => { @@ -22,23 +24,23 @@ describe(PnpmWorkspaceFile.name, () => { describe('basic functionality', () => { it('generates workspace file with packages only', () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage('/projects/app1'); - workspaceFile.addPackage('/projects/app2'); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + workspaceFile.addPackage(path.join(projectsDir, 'app2')); workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); const content: string = FileSystem.readFile(workspaceFilePath); expect(content).toMatchInlineSnapshot(` "packages: - - ../../../../../../../../../../../projects/app1 - - ../../../../../../../../../../../projects/app2 + - projects/app1 + - projects/app2 " `); }); it('escapes special characters in package paths', () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage('/projects/[app-with-brackets]'); + workspaceFile.addPackage(path.join(projectsDir, '[app-with-brackets]')); workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); @@ -50,7 +52,7 @@ describe(PnpmWorkspaceFile.name, () => { describe('catalog functionality', () => { it('generates workspace file with default catalog only', () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage('/projects/app1'); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); workspaceFile.setCatalogs({ default: { @@ -70,14 +72,14 @@ describe(PnpmWorkspaceFile.name, () => { react-dom: ^18.0.0 typescript: ~5.3.0 packages: - - ../../../../../../../../../../../projects/app1 + - projects/app1 " `); }); it('generates workspace file with named catalogs', () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage('/projects/app1'); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); workspaceFile.setCatalogs({ default: { @@ -107,14 +109,14 @@ packages: vue: ^3.4.0 vue-router: ^4.2.0 packages: - - ../../../../../../../../../../../projects/app1 + - projects/app1 " `); }); it('handles empty catalog object', () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage('/projects/app1'); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); workspaceFile.setCatalogs({}); @@ -123,14 +125,14 @@ packages: const content: string = FileSystem.readFile(workspaceFilePath); expect(content).toMatchInlineSnapshot(` "packages: - - ../../../../../../../../../../../projects/app1 + - projects/app1 " `); }); it('handles undefined catalog', () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage('/projects/app1'); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); workspaceFile.setCatalogs(undefined); @@ -139,14 +141,14 @@ packages: const content: string = FileSystem.readFile(workspaceFilePath); expect(content).toMatchInlineSnapshot(` "packages: - - ../../../../../../../../../../../projects/app1 + - projects/app1 " `); }); it('handles scoped packages in catalogs', () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage('/projects/app1'); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); workspaceFile.setCatalogs({ default: { @@ -166,14 +168,14 @@ packages: '@types/cookies': ^0.7.7 '@types/node': ~22.9.4 packages: - - ../../../../../../../../../../../projects/app1 + - projects/app1 " `); }); it('can update catalogs after initial creation', () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage('/projects/app1'); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); workspaceFile.setCatalogs({ default: { @@ -200,7 +202,7 @@ packages: react: ^18.2.0 react-dom: ^18.2.0 packages: - - ../../../../../../../../../../../projects/app1 + - projects/app1 " `); }); From ccb976f7e23f591354804c1704a652aa58765dab Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Tue, 25 Nov 2025 15:59:29 -0500 Subject: [PATCH 07/12] write to repo-state file with catalogsHash Signed-off-by: Aramis Sennyey --- common/reviews/api/rush-lib.api.md | 2 + libraries/rush-lib/src/api/Subspace.ts | 28 +++++++ .../rush-lib/src/api/test/Subspace.test.ts | 77 +++++++++++++++++++ .../common/config/rush/pnpm-config.json | 14 ++++ .../test/repoCatalogs/project1/package.json | 4 + .../src/api/test/repoCatalogs/rush.json | 13 ++++ libraries/rush-lib/src/logic/RepoStateFile.ts | 29 ++++++- .../installManager/WorkspaceInstallManager.ts | 47 ----------- .../src/schemas/repo-state.schema.json | 4 + 9 files changed, 170 insertions(+), 48 deletions(-) create mode 100644 libraries/rush-lib/src/api/test/Subspace.test.ts create mode 100644 libraries/rush-lib/src/api/test/repoCatalogs/common/config/rush/pnpm-config.json create mode 100644 libraries/rush-lib/src/api/test/repoCatalogs/project1/package.json create mode 100644 libraries/rush-lib/src/api/test/repoCatalogs/rush.json diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index b4da63b8d0a..6237c53a526 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -1207,6 +1207,7 @@ export class RepoStateFile { get isValid(): boolean; static loadFromFile(jsonFilename: string): RepoStateFile; get packageJsonInjectedDependenciesHash(): string | undefined; + get pnpmCatalogsHash(): string | undefined; get pnpmShrinkwrapHash(): string | undefined; get preferredVersionsHash(): string | undefined; refreshState(rushConfiguration: RushConfiguration, subspace: Subspace | undefined, variant?: string): boolean; @@ -1570,6 +1571,7 @@ export class Subspace { getCommonVersionsFilePath(variant?: string): string; // @beta getPackageJsonInjectedDependenciesHash(variant?: string): string | undefined; + getPnpmCatalogsHash(): string | undefined; // @beta getPnpmConfigFilePath(): string; // @beta diff --git a/libraries/rush-lib/src/api/Subspace.ts b/libraries/rush-lib/src/api/Subspace.ts index 741240d5f36..f272f103958 100644 --- a/libraries/rush-lib/src/api/Subspace.ts +++ b/libraries/rush-lib/src/api/Subspace.ts @@ -409,6 +409,34 @@ export class Subspace { this._projects.push(project); } + /** + * Computes a hash of the PNPM catalog definitions for this subspace. + * 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.globalCatalog && Object.keys(pnpmOptions.globalCatalog).length !== 0) { + catalogData.default = pnpmOptions.globalCatalog; + } + 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) { + return undefined; + } + + const hash: crypto.Hash = crypto.createHash('sha1'); + hash.update(JSON.stringify(catalogData)); + return hash.digest('hex'); + } + /** * Returns hash value of injected dependencies in related package.json. * @beta diff --git a/libraries/rush-lib/src/api/test/Subspace.test.ts b/libraries/rush-lib/src/api/test/Subspace.test.ts new file mode 100644 index 00000000000..b7e01689519 --- /dev/null +++ b/libraries/rush-lib/src/api/test/Subspace.test.ts @@ -0,0 +1,77 @@ +// 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 { RushConfiguration } from '../RushConfiguration'; +import { Subspace } from '../Subspace'; + +describe(Subspace.name, () => { + describe('getPnpmCatalogsHash', () => { + it('returns undefined when no catalogs are defined', () => { + const rushJsonFilename: string = path.resolve(__dirname, 'repo', 'rush-pnpm.json'); + const rushConfiguration: RushConfiguration = + RushConfiguration.loadFromConfigurationFile(rushJsonFilename); + const defaultSubspace: Subspace = rushConfiguration.defaultSubspace; + + const catalogsHash: string | undefined = defaultSubspace.getPnpmCatalogsHash(); + expect(catalogsHash).toBeUndefined(); + }); + + it('returns undefined for non-pnpm package manager', () => { + const rushJsonFilename: string = path.resolve(__dirname, 'repo', 'rush-npm.json'); + const rushConfiguration: RushConfiguration = + RushConfiguration.loadFromConfigurationFile(rushJsonFilename); + const defaultSubspace: Subspace = rushConfiguration.defaultSubspace; + + const catalogsHash: string | undefined = defaultSubspace.getPnpmCatalogsHash(); + expect(catalogsHash).toBeUndefined(); + }); + + it('computes hash when catalogs are defined', () => { + const rushJsonFilename: string = path.resolve(__dirname, 'repoCatalogs', 'rush.json'); + const rushConfiguration: RushConfiguration = + RushConfiguration.loadFromConfigurationFile(rushJsonFilename); + const defaultSubspace: Subspace = rushConfiguration.defaultSubspace; + + const catalogsHash: string | undefined = defaultSubspace.getPnpmCatalogsHash(); + expect(catalogsHash).toBeDefined(); + expect(typeof catalogsHash).toBe('string'); + expect(catalogsHash).toHaveLength(40); // SHA1 hash is 40 characters + }); + + it('computes consistent hash for same catalog data', () => { + const rushJsonFilename: string = path.resolve(__dirname, 'repoCatalogs', 'rush.json'); + const rushConfiguration: RushConfiguration = + RushConfiguration.loadFromConfigurationFile(rushJsonFilename); + const defaultSubspace: Subspace = rushConfiguration.defaultSubspace; + + const hash1: string | undefined = defaultSubspace.getPnpmCatalogsHash(); + const hash2: string | undefined = defaultSubspace.getPnpmCatalogsHash(); + + expect(hash1).toBeDefined(); + expect(hash1).toBe(hash2); + }); + + it('computes different hashes for different catalog data', () => { + // Configuration without catalogs + const rushJsonWithoutCatalogs: string = path.resolve(__dirname, 'repo', 'rush-pnpm.json'); + const rushConfigWithoutCatalogs: RushConfiguration = + RushConfiguration.loadFromConfigurationFile(rushJsonWithoutCatalogs); + const subspaceWithoutCatalogs: Subspace = rushConfigWithoutCatalogs.defaultSubspace; + + // Configuration with catalogs + const rushJsonWithCatalogs: string = path.resolve(__dirname, 'repoCatalogs', 'rush.json'); + const rushConfigWithCatalogs: RushConfiguration = + RushConfiguration.loadFromConfigurationFile(rushJsonWithCatalogs); + const subspaceWithCatalogs: Subspace = rushConfigWithCatalogs.defaultSubspace; + + const hashWithoutCatalogs: string | undefined = subspaceWithoutCatalogs.getPnpmCatalogsHash(); + const hashWithCatalogs: string | undefined = subspaceWithCatalogs.getPnpmCatalogsHash(); + + // One should be undefined (no catalogs) and one should have a hash + expect(hashWithoutCatalogs).toBeUndefined(); + expect(hashWithCatalogs).toBeDefined(); + }); + }); +}); diff --git a/libraries/rush-lib/src/api/test/repoCatalogs/common/config/rush/pnpm-config.json b/libraries/rush-lib/src/api/test/repoCatalogs/common/config/rush/pnpm-config.json new file mode 100644 index 00000000000..38133cb190f --- /dev/null +++ b/libraries/rush-lib/src/api/test/repoCatalogs/common/config/rush/pnpm-config.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json", + "globalCatalog": { + "react": "^18.0.0", + "react-dom": "^18.0.0", + "typescript": "~5.3.0" + }, + "globalCatalogs": { + "internal": { + "lodash": "^4.17.21", + "axios": "^1.6.0" + } + } +} diff --git a/libraries/rush-lib/src/api/test/repoCatalogs/project1/package.json b/libraries/rush-lib/src/api/test/repoCatalogs/project1/package.json new file mode 100644 index 00000000000..31cf82c13cf --- /dev/null +++ b/libraries/rush-lib/src/api/test/repoCatalogs/project1/package.json @@ -0,0 +1,4 @@ +{ + "name": "project1", + "version": "1.0.0" +} diff --git a/libraries/rush-lib/src/api/test/repoCatalogs/rush.json b/libraries/rush-lib/src/api/test/repoCatalogs/rush.json new file mode 100644 index 00000000000..a045354bbc2 --- /dev/null +++ b/libraries/rush-lib/src/api/test/repoCatalogs/rush.json @@ -0,0 +1,13 @@ +{ + "pnpmVersion": "9.5.0", + "rushVersion": "5.46.1", + "projectFolderMinDepth": 1, + "projectFolderMaxDepth": 99, + + "projects": [ + { + "packageName": "project1", + "projectFolder": "project1" + } + ] +} diff --git a/libraries/rush-lib/src/logic/RepoStateFile.ts b/libraries/rush-lib/src/logic/RepoStateFile.ts index 074609fa363..0bfd0f2e82e 100644 --- a/libraries/rush-lib/src/logic/RepoStateFile.ts +++ b/libraries/rush-lib/src/logic/RepoStateFile.ts @@ -15,7 +15,8 @@ import type { Subspace } from '../api/Subspace'; * { * "pnpmShrinkwrapHash": "...", * "preferredVersionsHash": "...", - * "packageJsonInjectedDependenciesHash": "..." + * "packageJsonInjectedDependenciesHash": "...", + * "pnpmCatalogsHash": "..." * } */ interface IRepoStateJson { @@ -31,6 +32,10 @@ interface IRepoStateJson { * A hash of the injected dependencies in related package.json */ packageJsonInjectedDependenciesHash?: string; + /** + * A hash of the PNPM catalog definitions + */ + pnpmCatalogsHash?: string; } /** @@ -45,6 +50,7 @@ export class RepoStateFile { private _pnpmShrinkwrapHash: string | undefined; private _preferredVersionsHash: string | undefined; private _packageJsonInjectedDependenciesHash: string | undefined; + private _pnpmCatalogsHash: string | undefined; private _isValid: boolean; private _modified: boolean = false; @@ -61,6 +67,7 @@ export class RepoStateFile { this._pnpmShrinkwrapHash = repoStateJson.pnpmShrinkwrapHash; this._preferredVersionsHash = repoStateJson.preferredVersionsHash; this._packageJsonInjectedDependenciesHash = repoStateJson.packageJsonInjectedDependenciesHash; + this._pnpmCatalogsHash = repoStateJson.pnpmCatalogsHash; } } @@ -85,6 +92,13 @@ export class RepoStateFile { return this._packageJsonInjectedDependenciesHash; } + /** + * The hash of the PNPM catalog definitions at the end of the last update. + */ + public get pnpmCatalogsHash(): string | undefined { + return this._pnpmCatalogsHash; + } + /** * If false, the repo-state.json file is not valid and its values cannot be relied upon */ @@ -219,6 +233,16 @@ export class RepoStateFile { this._packageJsonInjectedDependenciesHash = undefined; this._modified = true; } + + // Track catalog hash to detect when catalog definitions change + const pnpmCatalogsHash: string | undefined = subspace.getPnpmCatalogsHash(); + if (pnpmCatalogsHash && pnpmCatalogsHash !== this._pnpmCatalogsHash) { + this._pnpmCatalogsHash = pnpmCatalogsHash; + this._modified = true; + } else if (!pnpmCatalogsHash && this._pnpmCatalogsHash) { + this._pnpmCatalogsHash = undefined; + this._modified = true; + } } // Now that the file has been refreshed, we know its contents are valid @@ -255,6 +279,9 @@ export class RepoStateFile { if (this._packageJsonInjectedDependenciesHash) { repoStateJson.packageJsonInjectedDependenciesHash = this._packageJsonInjectedDependenciesHash; } + if (this._pnpmCatalogsHash) { + repoStateJson.pnpmCatalogsHash = this._pnpmCatalogsHash; + } return JsonFile.stringify(repoStateJson, { newlineConversion: NewlineKind.Lf }); } diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index b49fbc2d135..94b9b3490ba 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -440,53 +440,6 @@ export class WorkspaceInstallManager extends BaseInstallManager { shrinkwrapIsUpToDate = false; } - // Check if catalogsChecksum matches catalog's hash - let catalogsChecksum: string | undefined; - let existingCatalogsChecksum: string | undefined; - if (shrinkwrapFile) { - existingCatalogsChecksum = shrinkwrapFile.catalogsChecksum; - let catalogsChecksumAlgorithm: string | undefined; - if (existingCatalogsChecksum) { - const dashIndex: number = existingCatalogsChecksum.indexOf('-'); - if (dashIndex !== -1) { - catalogsChecksumAlgorithm = existingCatalogsChecksum.substring(0, dashIndex); - } - - if (catalogsChecksumAlgorithm && catalogsChecksumAlgorithm !== 'sha256') { - this._terminal.writeErrorLine( - `The existing catalogsChecksum algorithm "${catalogsChecksumAlgorithm}" is not supported. ` + - `This may indicate that the shrinkwrap was created with a newer version of PNPM than Rush supports.` - ); - throw new AlreadyReportedError(); - } - } - - // Combine both catalog and catalogs into a single object for checksum calculation - const catalogData: Record = {}; - if (pnpmOptions.globalCatalog && Object.keys(pnpmOptions.globalCatalog).length !== 0) { - catalogData.default = pnpmOptions.globalCatalog; - } - if (pnpmOptions.globalCatalogs && Object.keys(pnpmOptions.globalCatalogs).length !== 0) { - Object.assign(catalogData, pnpmOptions.globalCatalogs); - } - - if (Object.keys(catalogData).length !== 0) { - if (catalogsChecksumAlgorithm) { - // In PNPM v10, the algorithm changed to SHA256 and the digest changed from hex to base64 - catalogsChecksum = await createObjectChecksumAsync(catalogData); - } else { - catalogsChecksum = createObjectChecksumLegacy(catalogData); - } - } - } - - const catalogsChecksumAreEqual: boolean = catalogsChecksum === existingCatalogsChecksum; - - if (!catalogsChecksumAreEqual) { - shrinkwrapWarnings.push("The catalog hash doesn't match the current shrinkwrap."); - shrinkwrapIsUpToDate = false; - } - // Write the common package.json InstallHelpers.generateCommonPackageJson(this.rushConfiguration, subspace, undefined, this._terminal); diff --git a/libraries/rush-lib/src/schemas/repo-state.schema.json b/libraries/rush-lib/src/schemas/repo-state.schema.json index 563fa791b51..d14c1de3ac4 100644 --- a/libraries/rush-lib/src/schemas/repo-state.schema.json +++ b/libraries/rush-lib/src/schemas/repo-state.schema.json @@ -20,6 +20,10 @@ "packageJsonInjectedDependenciesHash": { "description": "A hash of the injected dependencies in related package.json. This hash is used to determine whether or not the shrinkwrap needs to updated prior to install.", "type": "string" + }, + "pnpmCatalogsHash": { + "description": "A hash of the PNPM catalog definitions for the repository. This hash is used to determine whether or not the catalog has been modified prior to install.", + "type": "string" } }, "additionalProperties": false From 5a280231fd03788e13fbce60593c3ac485fb0096 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Tue, 25 Nov 2025 21:59:41 -0500 Subject: [PATCH 08/12] remove catalogsChecksum from the lockfile Signed-off-by: Aramis Sennyey --- .../src/logic/pnpm/PnpmShrinkwrapFile.ts | 4 ---- .../logic/pnpm/test/PnpmShrinkwrapFile.test.ts | 16 ---------------- .../test/yamlFiles/pnpm-lock-with-catalog.yaml | 6 ++---- 3 files changed, 2 insertions(+), 24 deletions(-) diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts index 842047def70..ab61818fee1 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts @@ -121,8 +121,6 @@ export interface IPnpmShrinkwrapYaml extends Lockfile { specifiers?: Record; /** URL of the registry which was used */ registry?: string; - /** The checksum for catalog definitions */ - catalogsChecksum?: string; } export interface ILoadFromFileOptions { @@ -312,7 +310,6 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { public readonly packages: ReadonlyMap; public readonly overrides: ReadonlyMap; public readonly packageExtensionsChecksum: undefined | string; - public readonly catalogsChecksum: undefined | string; public readonly hash: string; private readonly _shrinkwrapJson: IPnpmShrinkwrapYaml; @@ -346,7 +343,6 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { this.packages = new Map(Object.entries(shrinkwrapJson.packages || {})); this.overrides = new Map(Object.entries(shrinkwrapJson.overrides || {})); this.packageExtensionsChecksum = shrinkwrapJson.packageExtensionsChecksum; - this.catalogsChecksum = shrinkwrapJson.catalogsChecksum; // Lockfile v9 always has "." in importers filed. this.isWorkspaceCompatible = diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts index 142d52b6cbb..aa1170e999b 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts @@ -380,22 +380,6 @@ describe(PnpmShrinkwrapFile.name, () => { }); }); }); - - describe('Catalog checksum', () => { - it('reads catalogsChecksum from pnpm-lock.yaml', () => { - const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( - `${__dirname}/yamlFiles/pnpm-lock-with-catalog.yaml` - ); - expect(pnpmShrinkwrapFile.catalogsChecksum).toBe('1a2b3c4d5e6f7890abcdef1234567890'); - }); - - it('returns undefined when catalogsChecksum is not present', () => { - const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( - `${__dirname}/yamlFiles/pnpm-lock-v9/pnpm-lock-v9.yaml` - ); - expect(pnpmShrinkwrapFile.catalogsChecksum).toBeUndefined(); - }); - }); }); function getPnpmShrinkwrapFileFromFile(filepath: string): PnpmShrinkwrapFile { diff --git a/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-with-catalog.yaml b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-with-catalog.yaml index 27f534f073a..143be44f5b0 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-with-catalog.yaml +++ b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-with-catalog.yaml @@ -4,8 +4,6 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -catalogsChecksum: 1a2b3c4d5e6f7890abcdef1234567890 - catalogs: default: react: 18.2.0 @@ -17,10 +15,10 @@ importers: .: dependencies: react: - specifier: 'catalog:' + specifier: 'catalog:default' version: 18.2.0 typescript: - specifier: 'catalog:' + specifier: 'catalog:default' version: 5.3.0 packages: From d45781345a9c4582b106d0f7ac81ad936fee5cdb Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Tue, 25 Nov 2025 22:00:00 -0500 Subject: [PATCH 09/12] fix schema description Signed-off-by: Aramis Sennyey --- libraries/rush-lib/src/schemas/pnpm-config.schema.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/rush-lib/src/schemas/pnpm-config.schema.json b/libraries/rush-lib/src/schemas/pnpm-config.schema.json index d3315d2d48b..84c38cd5c88 100644 --- a/libraries/rush-lib/src/schemas/pnpm-config.schema.json +++ b/libraries/rush-lib/src/schemas/pnpm-config.schema.json @@ -240,7 +240,7 @@ }, "globalCatalog": { - "description": "The \"globalCatalog\" setting provides a centralized way to define dependency versions that can be referenced using the `catalog:` protocol in package.json files. This allows for consistent version management across all projects in the monorepo. The settings are copied into the `pnpm.catalog` field of the `common/temp/package.json` file that is generated by Rush during installation.\n\nPNPM documentation: https://pnpm.io/catalogs", + "description": "The \"globalCatalog\" setting provides a centralized way to define dependency versions that can be referenced using the `catalog:` protocol in package.json files. This allows for consistent version management across all projects in the monorepo. The settings are written to the `catalogs.default` field of the `pnpm-workspace.yaml` file that is generated by Rush during installation.\n\nPNPM documentation: https://pnpm.io/catalogs", "type": "object", "additionalProperties": { "description": "Specify the version for a package in the catalog", @@ -249,7 +249,7 @@ }, "globalCatalogs": { - "description": "The \"globalCatalogs\" setting provides named catalogs for organizing dependency versions. Each catalog can be referenced using the `catalog:catalogName:` protocol in package.json files. The settings are copied into the `pnpm.catalogs` field of the `common/temp/package.json` file that is generated by Rush during installation.\n\nPNPM documentation: https://pnpm.io/catalogs", + "description": "The \"globalCatalogs\" setting provides named catalogs for organizing dependency versions. Each catalog can be referenced using the `catalog:catalogName` protocol in package.json files (e.g., `catalog:react18`). The settings are written to the `catalogs` field of the `pnpm-workspace.yaml` file that is generated by Rush during installation.\n\nPNPM documentation: https://pnpm.io/catalogs", "type": "object", "additionalProperties": { "description": "A named catalog containing package versions", From 85c9e58d2cc14eef9ee8dd4c2397b04658fd55b6 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Tue, 25 Nov 2025 22:00:27 -0500 Subject: [PATCH 10/12] mock FileSystem and move to separate file snapshots Signed-off-by: Aramis Sennyey --- .../logic/pnpm/test/PnpmWorkspaceFile.test.ts | 101 +++++++----------- .../PnpmWorkspaceFile.test.ts.snap | 67 ++++++++++++ 2 files changed, 104 insertions(+), 64 deletions(-) create mode 100644 libraries/rush-lib/src/logic/pnpm/test/__snapshots__/PnpmWorkspaceFile.test.ts.snap 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 63bc7507953..d8cdb149a88 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts @@ -10,15 +10,40 @@ describe(PnpmWorkspaceFile.name, () => { const workspaceFilePath: string = path.join(tempDir, 'pnpm-workspace.yaml'); const projectsDir: string = path.join(tempDir, 'projects'); + let mockWriteFile: jest.SpyInstance; + let mockReadFile: jest.SpyInstance; + let mockExists: jest.SpyInstance; + let writtenContent: string | undefined; + beforeEach(() => { - FileSystem.ensureFolder(tempDir); - FileSystem.ensureFolder(projectsDir); + 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(); + }); + + // 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(() => { - if (FileSystem.exists(tempDir)) { - FileSystem.deleteFolder(tempDir); - } + mockWriteFile.mockRestore(); + mockReadFile.mockRestore(); + mockExists.mockRestore(); }); describe('basic functionality', () => { @@ -30,12 +55,7 @@ describe(PnpmWorkspaceFile.name, () => { workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).toMatchInlineSnapshot(` -"packages: - - projects/app1 - - projects/app2 -" -`); + expect(content).toMatchSnapshot(); }); it('escapes special characters in package paths', () => { @@ -65,16 +85,7 @@ describe(PnpmWorkspaceFile.name, () => { workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).toMatchInlineSnapshot(` -"catalogs: - default: - react: ^18.0.0 - react-dom: ^18.0.0 - typescript: ~5.3.0 -packages: - - projects/app1 -" -`); + expect(content).toMatchSnapshot(); }); it('generates workspace file with named catalogs', () => { @@ -98,20 +109,7 @@ packages: workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).toMatchInlineSnapshot(` -"catalogs: - backend: - express: ^4.18.0 - fastify: ^4.26.0 - default: - typescript: ~5.3.0 - frontend: - vue: ^3.4.0 - vue-router: ^4.2.0 -packages: - - projects/app1 -" -`); + expect(content).toMatchSnapshot(); }); it('handles empty catalog object', () => { @@ -123,11 +121,7 @@ packages: workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).toMatchInlineSnapshot(` -"packages: - - projects/app1 -" -`); + expect(content).toMatchSnapshot(); }); it('handles undefined catalog', () => { @@ -139,11 +133,7 @@ packages: workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).toMatchInlineSnapshot(` -"packages: - - projects/app1 -" -`); + expect(content).toMatchSnapshot(); }); it('handles scoped packages in catalogs', () => { @@ -161,16 +151,7 @@ packages: workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).toMatchInlineSnapshot(` -"catalogs: - default: - '@rushstack/node-core-library': ~5.0.0 - '@types/cookies': ^0.7.7 - '@types/node': ~22.9.4 -packages: - - projects/app1 -" -`); + expect(content).toMatchSnapshot(); }); it('can update catalogs after initial creation', () => { @@ -196,15 +177,7 @@ packages: workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); const content: string = FileSystem.readFile(workspaceFilePath); - expect(content).toMatchInlineSnapshot(` -"catalogs: - default: - react: ^18.2.0 - react-dom: ^18.2.0 -packages: - - projects/app1 -" -`); + expect(content).toMatchSnapshot(); }); }); }); 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 new file mode 100644 index 00000000000..88be9e3823a --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/__snapshots__/PnpmWorkspaceFile.test.ts.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PnpmWorkspaceFile basic functionality generates workspace file with packages only 1`] = ` +"packages: + - projects/app1 + - projects/app2 +" +`; + +exports[`PnpmWorkspaceFile catalog functionality can update catalogs after initial creation 1`] = ` +"catalogs: + default: + react: ^18.2.0 + react-dom: ^18.2.0 +packages: + - projects/app1 +" +`; + +exports[`PnpmWorkspaceFile catalog functionality generates workspace file with default catalog only 1`] = ` +"catalogs: + default: + react: ^18.0.0 + react-dom: ^18.0.0 + typescript: ~5.3.0 +packages: + - projects/app1 +" +`; + +exports[`PnpmWorkspaceFile catalog functionality generates workspace file with named catalogs 1`] = ` +"catalogs: + backend: + express: ^4.18.0 + fastify: ^4.26.0 + default: + typescript: ~5.3.0 + frontend: + vue: ^3.4.0 + vue-router: ^4.2.0 +packages: + - projects/app1 +" +`; + +exports[`PnpmWorkspaceFile catalog functionality handles empty catalog object 1`] = ` +"packages: + - projects/app1 +" +`; + +exports[`PnpmWorkspaceFile catalog functionality handles scoped packages in catalogs 1`] = ` +"catalogs: + default: + '@rushstack/node-core-library': ~5.0.0 + '@types/cookies': ^0.7.7 + '@types/node': ~22.9.4 +packages: + - projects/app1 +" +`; + +exports[`PnpmWorkspaceFile catalog functionality handles undefined catalog 1`] = ` +"packages: + - projects/app1 +" +`; From 0fd6e89981f55c330ad8f8137fe37887cf98b0aa Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Tue, 25 Nov 2025 22:02:50 -0500 Subject: [PATCH 11/12] update pnpm-config schema again Signed-off-by: Aramis Sennyey --- .../src/logic/pnpm/PnpmOptionsConfiguration.ts | 10 ++++++---- libraries/rush-lib/src/schemas/pnpm-config.schema.json | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts index a00f778cb0a..33d0361c2a4 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts @@ -432,18 +432,20 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration /** * The `globalCatalog` setting provides a centralized way to define dependency versions * that can be referenced using the `catalog:` protocol in package.json files. - * The settings are copied into the `pnpm.catalog` field of the `common/temp/package.json` + * The settings are written to the `catalogs.default` field of the `pnpm-workspace.yaml` * file that is generated by Rush during installation. * + * This is syntactic sugar for `globalCatalogs.default`. + * * PNPM documentation: https://pnpm.io/catalogs */ public readonly globalCatalog: Record | undefined; /** * The `globalCatalogs` setting provides named catalogs for organizing dependency versions. - * Each catalog can be referenced using the `catalog:catalogName:` protocol in package.json files. - * The settings are copied into the `pnpm.catalogs` field of the `common/temp/package.json` - * file that is generated by Rush during installation. + * Each catalog can be referenced using the `catalog:catalogName` protocol in package.json files + * (e.g., `catalog:react18`). The settings are written to the `catalogs` field of the + * `pnpm-workspace.yaml` file that is generated by Rush during installation. * * PNPM documentation: https://pnpm.io/catalogs */ diff --git a/libraries/rush-lib/src/schemas/pnpm-config.schema.json b/libraries/rush-lib/src/schemas/pnpm-config.schema.json index 84c38cd5c88..996f84e3d90 100644 --- a/libraries/rush-lib/src/schemas/pnpm-config.schema.json +++ b/libraries/rush-lib/src/schemas/pnpm-config.schema.json @@ -240,7 +240,7 @@ }, "globalCatalog": { - "description": "The \"globalCatalog\" setting provides a centralized way to define dependency versions that can be referenced using the `catalog:` protocol in package.json files. This allows for consistent version management across all projects in the monorepo. The settings are written to the `catalogs.default` field of the `pnpm-workspace.yaml` file that is generated by Rush during installation.\n\nPNPM documentation: https://pnpm.io/catalogs", + "description": "The \"globalCatalog\" setting provides a centralized way to define dependency versions that can be referenced using the `catalog:` protocol in package.json files. This allows for consistent version management across all projects in the monorepo. The settings are written to the `catalogs.default` field of the `pnpm-workspace.yaml` file that is generated by Rush during installation.\n\nThis is syntactic sugar for \"globalCatalogs.default\".\n\nPNPM documentation: https://pnpm.io/catalogs", "type": "object", "additionalProperties": { "description": "Specify the version for a package in the catalog", From d2c4f7585e44f5adf67fe0df0072a38b6483a1f9 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Mon, 1 Dec 2025 17:14:56 -0500 Subject: [PATCH 12/12] remove globalCatalog Signed-off-by: Aramis Sennyey --- common/reviews/api/rush-lib.api.md | 2 -- libraries/rush-lib/src/api/Subspace.ts | 3 --- .../common/config/rush/pnpm-config.json | 10 +++++----- .../installManager/WorkspaceInstallManager.ts | 9 ++------- .../src/logic/pnpm/PnpmOptionsConfiguration.ts | 17 ----------------- .../pnpm/test/PnpmOptionsConfiguration.test.ts | 11 +++++------ .../test/jsonFiles/pnpm-config-catalog.json | 10 +++++----- .../common/config/rush/pnpm-config.json | 8 ++++---- .../src/schemas/pnpm-config.schema.json | 9 --------- 9 files changed, 21 insertions(+), 58 deletions(-) diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 6237c53a526..da3c87d6bd4 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -740,7 +740,6 @@ export interface _IPnpmOptionsJson extends IPackageManagerOptionsJsonBase { alwaysInjectDependenciesFromOtherSubspaces?: boolean; autoInstallPeers?: boolean; globalAllowedDeprecatedVersions?: Record; - globalCatalog?: Record; globalCatalogs?: Record>; globalIgnoredOptionalDependencies?: string[]; globalNeverBuiltDependencies?: string[]; @@ -1153,7 +1152,6 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration readonly alwaysInjectDependenciesFromOtherSubspaces: boolean | undefined; readonly autoInstallPeers: boolean | undefined; readonly globalAllowedDeprecatedVersions: Record | undefined; - readonly globalCatalog: Record | undefined; readonly globalCatalogs: Record> | undefined; readonly globalIgnoredOptionalDependencies: string[] | undefined; readonly globalNeverBuiltDependencies: string[] | undefined; diff --git a/libraries/rush-lib/src/api/Subspace.ts b/libraries/rush-lib/src/api/Subspace.ts index f272f103958..624b1a33bd6 100644 --- a/libraries/rush-lib/src/api/Subspace.ts +++ b/libraries/rush-lib/src/api/Subspace.ts @@ -420,9 +420,6 @@ export class Subspace { } const catalogData: Record = {}; - if (pnpmOptions.globalCatalog && Object.keys(pnpmOptions.globalCatalog).length !== 0) { - catalogData.default = pnpmOptions.globalCatalog; - } if (pnpmOptions.globalCatalogs && Object.keys(pnpmOptions.globalCatalogs).length !== 0) { Object.assign(catalogData, pnpmOptions.globalCatalogs); } diff --git a/libraries/rush-lib/src/api/test/repoCatalogs/common/config/rush/pnpm-config.json b/libraries/rush-lib/src/api/test/repoCatalogs/common/config/rush/pnpm-config.json index 38133cb190f..797e41e0c2c 100644 --- a/libraries/rush-lib/src/api/test/repoCatalogs/common/config/rush/pnpm-config.json +++ b/libraries/rush-lib/src/api/test/repoCatalogs/common/config/rush/pnpm-config.json @@ -1,11 +1,11 @@ { "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json", - "globalCatalog": { - "react": "^18.0.0", - "react-dom": "^18.0.0", - "typescript": "~5.3.0" - }, "globalCatalogs": { + "default": { + "react": "^18.0.0", + "react-dom": "^18.0.0", + "typescript": "~5.3.0" + }, "internal": { "lodash": "^4.17.21", "axios": "^1.6.0" diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index 94b9b3490ba..d9b3b577c90 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -444,7 +444,7 @@ export class WorkspaceInstallManager extends BaseInstallManager { InstallHelpers.generateCommonPackageJson(this.rushConfiguration, subspace, undefined, this._terminal); // Set catalog definitions in the workspace file if specified - if (pnpmOptions.globalCatalog || pnpmOptions.globalCatalogs) { + if (pnpmOptions.globalCatalogs) { if ( this.rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined && semver.lt(this.rushConfiguration.rushConfigurationJson.pnpmVersion, '9.5.0') @@ -452,7 +452,7 @@ export class WorkspaceInstallManager extends BaseInstallManager { this._terminal.writeWarningLine( Colorize.yellow( `Your version of pnpm (${this.rushConfiguration.rushConfigurationJson.pnpmVersion}) ` + - `doesn't support the "globalCatalog" or "globalCatalogs" fields in ` + + `doesn't support the "globalCatalogs" fields in ` + `${this.rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` + 'Remove these fields or upgrade to pnpm 9.5.0 or newer.' ) @@ -461,11 +461,6 @@ export class WorkspaceInstallManager extends BaseInstallManager { const catalogs: Record> = {}; - if (pnpmOptions.globalCatalog) { - // https://pnpm.io/catalogs#default-catalog, basically `catalog` is an alias for `catalogs.default` in pnpm. - catalogs.default = pnpmOptions.globalCatalog; - } - if (pnpmOptions.globalCatalogs) { Object.assign(catalogs, pnpmOptions.globalCatalogs); } diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts index 33d0361c2a4..555f02e7dd0 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts @@ -158,10 +158,6 @@ export interface IPnpmOptionsJson extends IPackageManagerOptionsJsonBase { * {@inheritDoc PnpmOptionsConfiguration.pnpmLockfilePolicies} */ pnpmLockfilePolicies?: IPnpmLockfilePolicies; - /** - * {@inheritDoc PnpmOptionsConfiguration.globalCatalog} - */ - globalCatalog?: Record; /** * {@inheritDoc PnpmOptionsConfiguration.globalCatalogs} */ @@ -429,18 +425,6 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration /*[LINE "DEMO"]*/ public readonly alwaysFullInstall: boolean | undefined; - /** - * The `globalCatalog` setting provides a centralized way to define dependency versions - * that can be referenced using the `catalog:` protocol in package.json files. - * The settings are written to the `catalogs.default` field of the `pnpm-workspace.yaml` - * file that is generated by Rush during installation. - * - * This is syntactic sugar for `globalCatalogs.default`. - * - * PNPM documentation: https://pnpm.io/catalogs - */ - public readonly globalCatalog: Record | undefined; - /** * The `globalCatalogs` setting provides named catalogs for organizing dependency versions. * Each catalog can be referenced using the `catalog:catalogName` protocol in package.json files @@ -495,7 +479,6 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration this.alwaysInjectDependenciesFromOtherSubspaces = json.alwaysInjectDependenciesFromOtherSubspaces; this.alwaysFullInstall = json.alwaysFullInstall; this.pnpmLockfilePolicies = json.pnpmLockfilePolicies; - this.globalCatalog = json.globalCatalog; this.globalCatalogs = json.globalCatalogs; } diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts index 372771f54fe..c590ca689fe 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts @@ -93,13 +93,12 @@ describe(PnpmOptionsConfiguration.name, () => { fakeCommonTempFolder ); - expect(TestUtilities.stripAnnotations(pnpmConfiguration.globalCatalog)).toEqual({ - react: '^18.0.0', - 'react-dom': '^18.0.0', - typescript: '~5.3.0' - }); - expect(TestUtilities.stripAnnotations(pnpmConfiguration.globalCatalogs)).toEqual({ + default: { + react: '^18.0.0', + 'react-dom': '^18.0.0', + typescript: '~5.3.0' + }, frontend: { vue: '^3.4.0', 'vue-router': '^4.2.0' diff --git a/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-catalog.json b/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-catalog.json index eb8b11abe41..5e0189115bd 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-catalog.json +++ b/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-catalog.json @@ -1,10 +1,10 @@ { - "globalCatalog": { - "react": "^18.0.0", - "react-dom": "^18.0.0", - "typescript": "~5.3.0" - }, "globalCatalogs": { + "default": { + "react": "^18.0.0", + "react-dom": "^18.0.0", + "typescript": "~5.3.0" + }, "frontend": { "vue": "^3.4.0", "vue-router": "^4.2.0" diff --git a/libraries/rush-lib/src/logic/test/pnpmConfig/common/config/rush/pnpm-config.json b/libraries/rush-lib/src/logic/test/pnpmConfig/common/config/rush/pnpm-config.json index 8053f6cb05c..1636e755728 100644 --- a/libraries/rush-lib/src/logic/test/pnpmConfig/common/config/rush/pnpm-config.json +++ b/libraries/rush-lib/src/logic/test/pnpmConfig/common/config/rush/pnpm-config.json @@ -13,11 +13,11 @@ } }, "globalNeverBuiltDependencies": ["fsevents", "level"], - "globalCatalog": { - "react": "^18.0.0", - "lodash": "^4.17.21" - }, "globalCatalogs": { + "default": { + "react": "^18.0.0", + "lodash": "^4.17.21" + }, "test": { "jest": "^29.0.0" } diff --git a/libraries/rush-lib/src/schemas/pnpm-config.schema.json b/libraries/rush-lib/src/schemas/pnpm-config.schema.json index 996f84e3d90..c7e628af851 100644 --- a/libraries/rush-lib/src/schemas/pnpm-config.schema.json +++ b/libraries/rush-lib/src/schemas/pnpm-config.schema.json @@ -239,15 +239,6 @@ } }, - "globalCatalog": { - "description": "The \"globalCatalog\" setting provides a centralized way to define dependency versions that can be referenced using the `catalog:` protocol in package.json files. This allows for consistent version management across all projects in the monorepo. The settings are written to the `catalogs.default` field of the `pnpm-workspace.yaml` file that is generated by Rush during installation.\n\nThis is syntactic sugar for \"globalCatalogs.default\".\n\nPNPM documentation: https://pnpm.io/catalogs", - "type": "object", - "additionalProperties": { - "description": "Specify the version for a package in the catalog", - "type": "string" - } - }, - "globalCatalogs": { "description": "The \"globalCatalogs\" setting provides named catalogs for organizing dependency versions. Each catalog can be referenced using the `catalog:catalogName` protocol in package.json files (e.g., `catalog:react18`). The settings are written to the `catalogs` field of the `pnpm-workspace.yaml` file that is generated by Rush during installation.\n\nPNPM documentation: https://pnpm.io/catalogs", "type": "object",