Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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": "aramissennyeydd@users.noreply.github.com"
}
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