Skip to content
Open
Show file tree
Hide file tree
Changes from all 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": "Fix minimumReleaseAge and minimumReleaseAgeExclude in pnpm-config.json being silently ignored because they were written to package.json instead of pnpm-workspace.yaml",
"type": "none",
"packageName": "@microsoft/rush"
}
],
"packageName": "@microsoft/rush",
"email": "aaronmaxlevy@users.noreply.github.com"
}
24 changes: 0 additions & 24 deletions libraries/rush-lib/src/logic/installManager/InstallHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ interface ICommonPackageJson extends IPackageJson {
ignoredOptionalDependencies?: typeof PnpmOptionsConfiguration.prototype.globalIgnoredOptionalDependencies;
allowedDeprecatedVersions?: typeof PnpmOptionsConfiguration.prototype.globalAllowedDeprecatedVersions;
patchedDependencies?: typeof PnpmOptionsConfiguration.prototype.globalPatchedDependencies;
minimumReleaseAge?: typeof PnpmOptionsConfiguration.prototype.minimumReleaseAgeMinutes;
minimumReleaseAgeExclude?: typeof PnpmOptionsConfiguration.prototype.minimumReleaseAgeExclude;
trustPolicy?: typeof PnpmOptionsConfiguration.prototype.trustPolicy;
trustPolicyExclude?: typeof PnpmOptionsConfiguration.prototype.trustPolicyExclude;
trustPolicyIgnoreAfter?: typeof PnpmOptionsConfiguration.prototype.trustPolicyIgnoreAfterMinutes;
Expand Down Expand Up @@ -142,28 +140,6 @@ export class InstallHelpers {
commonPackageJson.pnpm.patchedDependencies = pnpmOptions.globalPatchedDependencies;
}

if (pnpmOptions.minimumReleaseAgeMinutes !== undefined || pnpmOptions.minimumReleaseAgeExclude) {
if (semver.lt(pnpmVersion, '10.16.0')) {
terminal.writeWarningLine(
Colorize.yellow(
`Your version of PNPM (${pnpmVersion}) ` +
`doesn't support the "minimumReleaseAgeMinutes" or "minimumReleaseAgeExclude" fields in ` +
`${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` +
'Remove these fields or upgrade to PNPM 10.16.0 or newer.'
)
);
}

if (pnpmOptions.minimumReleaseAgeMinutes !== undefined) {
// NOTE: the pnpm setting is `minimumReleaseAge`, but the Rush setting is `minimumReleaseAgeMinutes`
commonPackageJson.pnpm.minimumReleaseAge = pnpmOptions.minimumReleaseAgeMinutes;
}

if (pnpmOptions.minimumReleaseAgeExclude) {
commonPackageJson.pnpm.minimumReleaseAgeExclude = pnpmOptions.minimumReleaseAgeExclude;
}
}

if (pnpmOptions.trustPolicy !== undefined) {
if (semver.lt(pnpmVersion, '10.21.0')) {
terminal.writeWarningLine(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -476,10 +476,7 @@ export class WorkspaceInstallManager extends BaseInstallManager {
) {
if (pnpmOptions.globalAllowBuilds) {
workspaceFile.setAllowBuilds(pnpmOptions.globalAllowBuilds);
} else if (
pnpmOptions.globalOnlyBuiltDependencies ||
pnpmOptions.globalNeverBuiltDependencies
) {
} else if (pnpmOptions.globalOnlyBuiltDependencies || pnpmOptions.globalNeverBuiltDependencies) {
// Backward compatibility: convert globalOnlyBuiltDependencies/globalNeverBuiltDependencies
// to allowBuilds format for pnpm 11+
const allowBuilds: Record<string, boolean> = {};
Expand Down Expand Up @@ -509,6 +506,33 @@ export class WorkspaceInstallManager extends BaseInstallManager {
);
}

// Set minimumReleaseAge/minimumReleaseAgeExclude in the workspace file.
// pnpm does not read these fields from package.json, only from pnpm-workspace.yaml or .npmrc.
if (pnpmOptions.minimumReleaseAgeMinutes !== undefined || pnpmOptions.minimumReleaseAgeExclude) {
if (
this.rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined &&
semver.lt(this.rushConfiguration.rushConfigurationJson.pnpmVersion, '10.16.0')
) {
this._terminal.writeWarningLine(
Colorize.yellow(
`Your version of pnpm (${this.rushConfiguration.rushConfigurationJson.pnpmVersion}) ` +
`doesn't support the "minimumReleaseAgeMinutes" or "minimumReleaseAgeExclude" fields in ` +
`${this.rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` +
'Remove these fields or upgrade to pnpm 10.16.0 or newer.'
)
);
}

if (pnpmOptions.minimumReleaseAgeMinutes !== undefined) {
// NOTE: the pnpm setting is `minimumReleaseAge`, but the Rush setting is `minimumReleaseAgeMinutes`
workspaceFile.setMinimumReleaseAge(pnpmOptions.minimumReleaseAgeMinutes);
}

if (pnpmOptions.minimumReleaseAgeExclude) {
workspaceFile.setMinimumReleaseAgeExclude(pnpmOptions.minimumReleaseAgeExclude);
}
}

// 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 });
Expand Down
40 changes: 40 additions & 0 deletions libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ interface IPnpmWorkspaceYaml {
* (SUPPORTED ONLY IN PNPM 11.0.0 AND NEWER)
*/
allowBuilds?: Record<string, boolean>;
/**
* The minimum number of minutes that must pass after a version is published before pnpm will install it.
* (SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER)
*/
minimumReleaseAge?: number;
/**
* List of package names or patterns that are excluded from the minimumReleaseAge check.
* (SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER)
*/
minimumReleaseAgeExclude?: string[];
}

export class PnpmWorkspaceFile extends BaseWorkspaceFile {
Expand All @@ -53,6 +63,8 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile {
private _workspacePackages: Set<string>;
private _catalogs: Record<string, Record<string, string>> | undefined;
private _allowBuilds: Record<string, boolean> | undefined;
private _minimumReleaseAge: number | undefined;
private _minimumReleaseAgeExclude: string[] | undefined;

/**
* The PNPM workspace file is used to specify the location of workspaces relative to the root
Expand All @@ -67,6 +79,8 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile {
this._workspacePackages = new Set<string>();
this._catalogs = undefined;
this._allowBuilds = undefined;
this._minimumReleaseAge = undefined;
this._minimumReleaseAgeExclude = undefined;
}

/**
Expand All @@ -86,6 +100,24 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile {
this._allowBuilds = allowBuilds;
}

/**
* Sets the minimumReleaseAge setting for the workspace.
* The minimum number of minutes that must pass after a version is published before pnpm will install it.
* (SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER)
*/
public setMinimumReleaseAge(minimumReleaseAge: number | undefined): void {
this._minimumReleaseAge = minimumReleaseAge;
}

/**
* Sets the minimumReleaseAgeExclude setting for the workspace.
* List of package names or patterns that are excluded from the minimumReleaseAge check.
* (SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER)
*/
public setMinimumReleaseAgeExclude(minimumReleaseAgeExclude: string[] | undefined): void {
this._minimumReleaseAgeExclude = minimumReleaseAgeExclude;
}

/** @override */
public addPackage(packagePath: string): void {
// Ensure the path is relative to the pnpm-workspace.yaml file
Expand Down Expand Up @@ -115,6 +147,14 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile {
workspaceYaml.allowBuilds = this._allowBuilds;
}

if (this._minimumReleaseAge !== undefined) {
workspaceYaml.minimumReleaseAge = this._minimumReleaseAge;
}

if (this._minimumReleaseAgeExclude && this._minimumReleaseAgeExclude.length > 0) {
workspaceYaml.minimumReleaseAgeExclude = this._minimumReleaseAgeExclude;
}

return yamlModule.dump(workspaceYaml, PNPM_SHRINKWRAP_YAML_FORMAT);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -242,4 +242,80 @@ describe(PnpmWorkspaceFile.name, () => {
expect(content).not.toContain('allowBuilds');
});
});

describe('minimumReleaseAge functionality', () => {
it('generates workspace file with minimumReleaseAge', () => {
const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath);
workspaceFile.addPackage(path.join(projectsDir, 'app1'));

workspaceFile.setMinimumReleaseAge(20160);

workspaceFile.save(workspaceFilePath, { onlyIfChanged: true });

const content: string = FileSystem.readFile(workspaceFilePath);
expect(content).toMatchSnapshot();
});

it('generates workspace file with minimumReleaseAge and minimumReleaseAgeExclude', () => {
const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath);
workspaceFile.addPackage(path.join(projectsDir, 'app1'));

workspaceFile.setMinimumReleaseAge(1440);
workspaceFile.setMinimumReleaseAgeExclude(['webpack', '@myorg/*']);

workspaceFile.save(workspaceFilePath, { onlyIfChanged: true });

const content: string = FileSystem.readFile(workspaceFilePath);
expect(content).toMatchSnapshot();
});

it('generates workspace file with minimumReleaseAgeExclude only', () => {
const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath);
workspaceFile.addPackage(path.join(projectsDir, 'app1'));

workspaceFile.setMinimumReleaseAgeExclude(['webpack']);

workspaceFile.save(workspaceFilePath, { onlyIfChanged: true });

const content: string = FileSystem.readFile(workspaceFilePath);
expect(content).toMatchSnapshot();
});

it('handles zero value for minimumReleaseAge', () => {
const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath);
workspaceFile.addPackage(path.join(projectsDir, 'app1'));

workspaceFile.setMinimumReleaseAge(0);

workspaceFile.save(workspaceFilePath, { onlyIfChanged: true });

const content: string = FileSystem.readFile(workspaceFilePath);
expect(content).toContain('minimumReleaseAge: 0');
});

it('handles undefined minimumReleaseAge', () => {
const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath);
workspaceFile.addPackage(path.join(projectsDir, 'app1'));

workspaceFile.setMinimumReleaseAge(undefined);
workspaceFile.setMinimumReleaseAgeExclude(undefined);

workspaceFile.save(workspaceFilePath, { onlyIfChanged: true });

const content: string = FileSystem.readFile(workspaceFilePath);
expect(content).not.toContain('minimumReleaseAge');
});

it('handles empty minimumReleaseAgeExclude', () => {
const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath);
workspaceFile.addPackage(path.join(projectsDir, 'app1'));

workspaceFile.setMinimumReleaseAgeExclude([]);

workspaceFile.save(workspaceFilePath, { onlyIfChanged: true });

const content: string = FileSystem.readFile(workspaceFilePath);
expect(content).not.toContain('minimumReleaseAgeExclude');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,28 @@ exports[`PnpmWorkspaceFile catalog functionality handles undefined catalog 1`] =
- projects/app1
"
`;

exports[`PnpmWorkspaceFile minimumReleaseAge functionality generates workspace file with minimumReleaseAge 1`] = `
"minimumReleaseAge: 20160
packages:
- projects/app1
"
`;

exports[`PnpmWorkspaceFile minimumReleaseAge functionality generates workspace file with minimumReleaseAge and minimumReleaseAgeExclude 1`] = `
"minimumReleaseAge: 1440
minimumReleaseAgeExclude:
- webpack
- '@myorg/*'
packages:
- projects/app1
"
`;

exports[`PnpmWorkspaceFile minimumReleaseAge functionality generates workspace file with minimumReleaseAgeExclude only 1`] = `
"minimumReleaseAgeExclude:
- webpack
packages:
- projects/app1
"
`;