Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"comment": "Add support for defining pnpm catalog config.",
"type": "none",
"packageName": "@microsoft/rush"
}
],
"packageName": "@microsoft/rush",
"email": "[email protected]"
}
4 changes: 4 additions & 0 deletions common/reviews/api/rush-lib.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,8 @@ export interface _IPnpmOptionsJson extends IPackageManagerOptionsJsonBase {
alwaysInjectDependenciesFromOtherSubspaces?: boolean;
autoInstallPeers?: boolean;
globalAllowedDeprecatedVersions?: Record<string, string>;
globalCatalog?: Record<string, string>;
globalCatalogs?: Record<string, Record<string, string>>;
globalIgnoredOptionalDependencies?: string[];
globalNeverBuiltDependencies?: string[];
globalOverrides?: Record<string, string>;
Expand Down Expand Up @@ -1151,6 +1153,8 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration
readonly alwaysInjectDependenciesFromOtherSubspaces: boolean | undefined;
readonly autoInstallPeers: boolean | undefined;
readonly globalAllowedDeprecatedVersions: Record<string, string> | undefined;
readonly globalCatalog: Record<string, string> | undefined;
readonly globalCatalogs: Record<string, Record<string, string>> | undefined;
readonly globalIgnoredOptionalDependencies: string[] | undefined;
readonly globalNeverBuiltDependencies: string[] | undefined;
readonly globalOverrides: Record<string, string> | undefined;
Expand Down
38 changes: 38 additions & 0 deletions libraries/rush-lib/src/logic/installManager/InstallHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
}

Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> = {};
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);

Expand Down
30 changes: 30 additions & 0 deletions libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,14 @@ export interface IPnpmOptionsJson extends IPackageManagerOptionsJsonBase {
* {@inheritDoc PnpmOptionsConfiguration.pnpmLockfilePolicies}
*/
pnpmLockfilePolicies?: IPnpmLockfilePolicies;
/**
* {@inheritDoc PnpmOptionsConfiguration.globalCatalog}
*/
globalCatalog?: Record<string, string>;
/**
* {@inheritDoc PnpmOptionsConfiguration.globalCatalogs}
*/
globalCatalogs?: Record<string, Record<string, string>>;
}

/**
Expand Down Expand Up @@ -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<string, string> | 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<string, Record<string, string>> | undefined;

/**
* (GENERATED BY RUSH-PNPM PATCH-COMMIT) When modifying this property, make sure you know what you are doing.
*
Expand Down Expand Up @@ -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 */
Expand Down
4 changes: 4 additions & 0 deletions libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ export interface IPnpmShrinkwrapYaml extends Lockfile {
specifiers?: Record<string, string>;
/** URL of the registry which was used */
registry?: string;
/** The checksum for catalog definitions */
catalogsChecksum?: string;
}

export interface ILoadFromFileOptions {
Expand Down Expand Up @@ -310,6 +312,7 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile {
public readonly packages: ReadonlyMap<string, IPnpmShrinkwrapDependencyYaml>;
public readonly overrides: ReadonlyMap<string, string>;
public readonly packageExtensionsChecksum: undefined | string;
public readonly catalogsChecksum: undefined | string;
public readonly hash: string;

private readonly _shrinkwrapJson: IPnpmShrinkwrapYaml;
Expand Down Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
});
});
});
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
9 changes: 9 additions & 0 deletions libraries/rush-lib/src/logic/test/InstallHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]",
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
22 changes: 22 additions & 0 deletions libraries/rush-lib/src/schemas/pnpm-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
}
}
Loading