Skip to content
Merged
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": "Add support for pnpm 11's `allowBuilds` field in `pnpm-workspace.yaml`. Rush now correctly handles the pnpm 11 security model where build scripts must be explicitly approved. The new `globalAllowBuilds` field in `pnpm-config.json` replaces the deprecated `globalOnlyBuiltDependencies` and `globalNeverBuiltDependencies` fields for pnpm 11+. The `rush-pnpm approve-builds` command is also updated to work correctly with pnpm 11.",
"type": "minor",
"packageName": "@microsoft/rush"
}
],
"packageName": "@microsoft/rush",
"email": "198982749+Copilot@users.noreply.github.com"
}
3 changes: 3 additions & 0 deletions common/reviews/api/rush-lib.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,7 @@ export interface _IPnpmOptionsJson extends IPackageManagerOptionsJsonBase {
alwaysFullInstall?: boolean;
alwaysInjectDependenciesFromOtherSubspaces?: boolean;
autoInstallPeers?: boolean;
globalAllowBuilds?: Record<string, boolean>;
globalAllowedDeprecatedVersions?: Record<string, string>;
globalCatalogs?: Record<string, Record<string, string>>;
globalIgnoredOptionalDependencies?: string[];
Expand Down Expand Up @@ -1177,6 +1178,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration
readonly alwaysFullInstall: boolean | undefined;
readonly alwaysInjectDependenciesFromOtherSubspaces: boolean | undefined;
readonly autoInstallPeers: boolean | undefined;
readonly globalAllowBuilds: Record<string, boolean> | undefined;
readonly globalAllowedDeprecatedVersions: Record<string, string> | undefined;
readonly globalCatalogs: Record<string, Record<string, string>> | undefined;
readonly globalIgnoredOptionalDependencies: string[] | undefined;
Expand Down Expand Up @@ -1206,6 +1208,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration
readonly trustPolicyExclude: string[] | undefined;
readonly trustPolicyIgnoreAfterMinutes: number | undefined;
readonly unsupportedPackageJsonSettings: unknown | undefined;
updateGlobalAllowBuilds(allowBuilds: Record<string, boolean> | undefined): void;
updateGlobalOnlyBuiltDependencies(onlyBuiltDependencies: string[] | undefined): void;
updateGlobalPatchedDependencies(patchedDependencies: Record<string, string> | undefined): void;
readonly useWorkspaces: boolean;
Expand Down
73 changes: 52 additions & 21 deletions libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,9 @@ export class RushPnpmCommandLineParser {
case 'approve-builds': {
const semver: typeof import('semver') = await import('semver');
/**
* The "approve-builds" command was introduced in pnpm version 10.1.0
* to approve packages for running build scripts when onlyBuiltDependencies is used
* The "approve-builds" command was introduced in PNPM version 10.1.0
* to approve packages for running build scripts when onlyBuiltDependencies is used.
* In PNPM 11.0.0, it was updated to use allowBuilds in pnpm-workspace.yaml.
*/
if (semver.lt(this._rushConfiguration.packageManagerToolVersion, '10.1.0')) {
this._terminal.writeErrorLine(
Expand Down Expand Up @@ -572,26 +573,56 @@ export class RushPnpmCommandLineParser {
break;
}

// Example: "C:\MyRepo\common\temp\package.json"
const commonPackageJsonFilename: string = `${subspaceTempFolder}/${FileConstants.PackageJson}`;
const commonPackageJson: JsonObject = await JsonFile.loadAsync(commonPackageJsonFilename);
const newGlobalOnlyBuiltDependencies: string[] | undefined =
commonPackageJson?.pnpm?.onlyBuiltDependencies;
const pnpmOptions: PnpmOptionsConfiguration | undefined = this._subspace.getPnpmOptions();
const currentGlobalOnlyBuiltDependencies: string[] | undefined =
pnpmOptions?.globalOnlyBuiltDependencies;

if (!Objects.areDeepEqual(currentGlobalOnlyBuiltDependencies, newGlobalOnlyBuiltDependencies)) {
// Update onlyBuiltDependencies to pnpm configuration file
pnpmOptions?.updateGlobalOnlyBuiltDependencies(newGlobalOnlyBuiltDependencies);

// Rerun installation to update
await this._doRushUpdateAsync();

this._terminal.writeWarningLine(
`Rush refreshed the ${RushConstants.pnpmConfigFilename} and shrinkwrap file.\n` +
' Please commit this change to Git.'
);
const pnpmVersion: string = this._rushConfiguration.packageManagerToolVersion;
const semver: typeof import('semver') = await import('semver');

if (semver.gte(pnpmVersion, '11.0.0')) {
// PNPM 11+ uses allowBuilds in pnpm-workspace.yaml instead of onlyBuiltDependencies in package.json
const workspaceYamlFilename: string = `${subspaceTempFolder}/pnpm-workspace.yaml`;
const yamlModule: typeof import('js-yaml') = await import('js-yaml');
const workspaceYamlContent: string = await FileSystem.readFileAsync(workspaceYamlFilename);
const workspaceYaml: { allowBuilds?: Record<string, boolean> } = (yamlModule.load(
workspaceYamlContent
) ?? {}) as { allowBuilds?: Record<string, boolean> };
const newGlobalAllowBuilds: Record<string, boolean> | undefined = workspaceYaml?.allowBuilds;
const currentGlobalAllowBuilds: Record<string, boolean> | undefined =
pnpmOptions?.globalAllowBuilds;

if (!Objects.areDeepEqual(currentGlobalAllowBuilds, newGlobalAllowBuilds)) {
// Update allowBuilds to pnpm configuration file
pnpmOptions?.updateGlobalAllowBuilds(newGlobalAllowBuilds);

// Rerun installation to update
await this._doRushUpdateAsync();

this._terminal.writeWarningLine(
`Rush refreshed the ${RushConstants.pnpmConfigFilename} and shrinkwrap file.\n` +
' Please commit this change to Git.'
);
}
} else {
// PNPM 10.x uses onlyBuiltDependencies in package.json
// Example: "C:\MyRepo\common\temp\package.json"
const commonPackageJsonFilename: string = `${subspaceTempFolder}/${FileConstants.PackageJson}`;
const commonPackageJson: JsonObject = await JsonFile.loadAsync(commonPackageJsonFilename);
const newGlobalOnlyBuiltDependencies: string[] | undefined =
commonPackageJson?.pnpm?.onlyBuiltDependencies;
const currentGlobalOnlyBuiltDependencies: string[] | undefined =
pnpmOptions?.globalOnlyBuiltDependencies;

if (!Objects.areDeepEqual(currentGlobalOnlyBuiltDependencies, newGlobalOnlyBuiltDependencies)) {
// Update onlyBuiltDependencies to pnpm configuration file
pnpmOptions?.updateGlobalOnlyBuiltDependencies(newGlobalOnlyBuiltDependencies);

// Rerun installation to update
await this._doRushUpdateAsync();

this._terminal.writeWarningLine(
`Rush refreshed the ${RushConstants.pnpmConfigFilename} and shrinkwrap file.\n` +
' Please commit this change to Git.'
);
}
}
break;
}
Expand Down
86 changes: 46 additions & 40 deletions libraries/rush-lib/src/logic/installManager/InstallHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,39 +76,57 @@ export class InstallHelpers {
commonPackageJson.pnpm.peerDependencyRules = pnpmOptions.globalPeerDependencyRules;
}

const pnpmVersion: string = rushConfiguration.packageManagerToolVersion;

if (pnpmOptions.globalNeverBuiltDependencies) {
commonPackageJson.pnpm.neverBuiltDependencies = pnpmOptions.globalNeverBuiltDependencies;
if (semver.gte(pnpmVersion, '11.0.0')) {
terminal.writeWarningLine(
Colorize.yellow(
`Your version of PNPM (${pnpmVersion}) ` +
`no longer supports the "globalNeverBuiltDependencies" field in ` +
`${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` +
'Use "globalAllowBuilds" instead (with a value of false to deny build scripts).'
)
);
} else {
commonPackageJson.pnpm.neverBuiltDependencies = pnpmOptions.globalNeverBuiltDependencies;
}
}

if (pnpmOptions.globalOnlyBuiltDependencies) {
if (
rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined &&
semver.lt(rushConfiguration.rushConfigurationJson.pnpmVersion, '10.1.0')
) {
if (semver.gte(pnpmVersion, '11.0.0')) {
terminal.writeWarningLine(
Colorize.yellow(
`Your version of pnpm (${rushConfiguration.rushConfigurationJson.pnpmVersion}) ` +
`doesn't support the "globalOnlyBuiltDependencies" field in ` +
`Your version of PNPM (${pnpmVersion}) ` +
`no longer supports the "globalOnlyBuiltDependencies" field in ` +
`${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` +
'Remove this field or upgrade to pnpm 10.1.0 or newer.'
'Use "globalAllowBuilds" instead (with a value of true to allow build scripts).'
)
);
}
} else {
if (semver.lt(pnpmVersion, '10.1.0')) {
terminal.writeWarningLine(
Colorize.yellow(
`Your version of PNPM (${pnpmVersion}) ` +
`doesn't support the "globalOnlyBuiltDependencies" field in ` +
`${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` +
'Remove this field or upgrade to PNPM 10.1.0 or newer.'
)
);
}

commonPackageJson.pnpm.onlyBuiltDependencies = pnpmOptions.globalOnlyBuiltDependencies;
commonPackageJson.pnpm.onlyBuiltDependencies = pnpmOptions.globalOnlyBuiltDependencies;
}
}

if (pnpmOptions.globalIgnoredOptionalDependencies) {
if (
rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined &&
semver.lt(rushConfiguration.rushConfigurationJson.pnpmVersion, '9.0.0')
) {
if (semver.lt(pnpmVersion, '9.0.0')) {
terminal.writeWarningLine(
Colorize.yellow(
`Your version of pnpm (${rushConfiguration.rushConfigurationJson.pnpmVersion}) ` +
`Your version of PNPM (${pnpmVersion}) ` +
`doesn't support the "globalIgnoredOptionalDependencies" field in ` +
`${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` +
'Remove this field or upgrade to pnpm 9.'
'Remove this field or upgrade to PNPM 9.'
)
);
}
Expand All @@ -125,16 +143,13 @@ export class InstallHelpers {
}

if (pnpmOptions.minimumReleaseAgeMinutes !== undefined || pnpmOptions.minimumReleaseAgeExclude) {
if (
rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined &&
semver.lt(rushConfiguration.rushConfigurationJson.pnpmVersion, '10.16.0')
) {
if (semver.lt(pnpmVersion, '10.16.0')) {
terminal.writeWarningLine(
Colorize.yellow(
`Your version of pnpm (${rushConfiguration.rushConfigurationJson.pnpmVersion}) ` +
`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.'
'Remove these fields or upgrade to PNPM 10.16.0 or newer.'
)
);
}
Expand All @@ -150,16 +165,13 @@ export class InstallHelpers {
}

if (pnpmOptions.trustPolicy !== undefined) {
if (
rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined &&
semver.lt(rushConfiguration.rushConfigurationJson.pnpmVersion, '10.21.0')
) {
if (semver.lt(pnpmVersion, '10.21.0')) {
terminal.writeWarningLine(
Colorize.yellow(
`Your version of pnpm (${rushConfiguration.rushConfigurationJson.pnpmVersion}) ` +
`Your version of PNPM (${pnpmVersion}) ` +
`doesn't support the "trustPolicy" field in ` +
`${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` +
'Remove this field or upgrade to pnpm 10.21.0 or newer.'
'Remove this field or upgrade to PNPM 10.21.0 or newer.'
)
);
}
Expand All @@ -168,16 +180,13 @@ export class InstallHelpers {
}

if (pnpmOptions.trustPolicyExclude) {
if (
rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined &&
semver.lt(rushConfiguration.rushConfigurationJson.pnpmVersion, '10.22.0')
) {
if (semver.lt(pnpmVersion, '10.22.0')) {
terminal.writeWarningLine(
Colorize.yellow(
`Your version of pnpm (${rushConfiguration.rushConfigurationJson.pnpmVersion}) ` +
`Your version of PNPM (${pnpmVersion}) ` +
`doesn't support the "trustPolicyExclude" field in ` +
`${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` +
'Remove this field or upgrade to pnpm 10.22.0 or newer.'
'Remove this field or upgrade to PNPM 10.22.0 or newer.'
)
);
}
Expand All @@ -186,16 +195,13 @@ export class InstallHelpers {
}

if (pnpmOptions.trustPolicyIgnoreAfterMinutes !== undefined) {
if (
rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined &&
semver.lt(rushConfiguration.rushConfigurationJson.pnpmVersion, '10.27.0')
) {
if (semver.lt(pnpmVersion, '10.27.0')) {
terminal.writeWarningLine(
Colorize.yellow(
`Your version of pnpm (${rushConfiguration.rushConfigurationJson.pnpmVersion}) ` +
`Your version of PNPM (${pnpmVersion}) ` +
`doesn't support the "trustPolicyIgnoreAfterMinutes" field in ` +
`${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` +
'Remove this field or upgrade to pnpm 10.27.0 or newer.'
'Remove this field or upgrade to PNPM 10.27.0 or newer.'
)
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,46 @@ export class WorkspaceInstallManager extends BaseInstallManager {
workspaceFile.setCatalogs(catalogs);
}

// Set allowBuilds in the workspace file for pnpm 11+ (replaces onlyBuiltDependencies/neverBuiltDependencies)
if (
this.rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined &&
semver.gte(this.rushConfiguration.rushConfigurationJson.pnpmVersion, '11.0.0')
) {
if (pnpmOptions.globalAllowBuilds) {
workspaceFile.setAllowBuilds(pnpmOptions.globalAllowBuilds);
} else if (
pnpmOptions.globalOnlyBuiltDependencies ||
pnpmOptions.globalNeverBuiltDependencies
) {
// Backward compatibility: convert globalOnlyBuiltDependencies/globalNeverBuiltDependencies
// to allowBuilds format for pnpm 11+
const allowBuilds: Record<string, boolean> = {};
if (pnpmOptions.globalOnlyBuiltDependencies) {
for (const pkg of pnpmOptions.globalOnlyBuiltDependencies) {
allowBuilds[pkg] = true;
}
}
if (pnpmOptions.globalNeverBuiltDependencies) {
for (const pkg of pnpmOptions.globalNeverBuiltDependencies) {
allowBuilds[pkg] = false;
}
}
workspaceFile.setAllowBuilds(allowBuilds);
}
} else if (
pnpmOptions.globalAllowBuilds &&
this.rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined
) {
this._terminal.writeWarningLine(
Colorize.yellow(
`Your version of pnpm (${this.rushConfiguration.rushConfigurationJson.pnpmVersion}) ` +
`doesn't support the "globalAllowBuilds" field in ` +
`${this.rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` +
'Remove this field or upgrade to pnpm 11.0.0 or newer.'
)
);
}

// 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
37 changes: 36 additions & 1 deletion libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ export interface IPnpmOptionsJson extends IPackageManagerOptionsJsonBase {
* {@inheritDoc PnpmOptionsConfiguration.globalOnlyBuiltDependencies}
*/
globalOnlyBuiltDependencies?: string[];
/**
* {@inheritDoc PnpmOptionsConfiguration.globalAllowBuilds}
*/
globalAllowBuilds?: Record<string, boolean>;
/**
* {@inheritDoc PnpmOptionsConfiguration.globalIgnoredOptionalDependencies}
*/
Expand Down Expand Up @@ -446,12 +450,26 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration
* The settings are copied into the `pnpm.onlyBuiltDependencies` field of the `common/temp/package.json`
* file that is generated by Rush during installation.
*
* (SUPPORTED ONLY IN PNPM 10.1.0 AND NEWER)
* (SUPPORTED ONLY IN PNPM 10.1.0 AND NEWER; replaced by `globalAllowBuilds` in PNPM 11.0.0)
*
* PNPM documentation: https://pnpm.io/package_json#pnpmonlybuiltdependencies
*/
public readonly globalOnlyBuiltDependencies: string[] | undefined;

/**
* The `globalAllowBuilds` setting controls which packages are allowed to run build scripts
* (`preinstall`, `install`, and `postinstall` lifecycle events). A value of `true` means the
* package is allowed to run build scripts; `false` means it is explicitly denied.
* Packages with build scripts not listed here will cause pnpm to fail with ERR_PNPM_IGNORED_BUILDS.
* The settings are written to the `allowBuilds` field of the `pnpm-workspace.yaml` file
* that is generated by Rush during installation.
*
* (SUPPORTED ONLY IN PNPM 11.0.0 AND NEWER)
*
* PNPM documentation: https://pnpm.io/settings#allowbuilds
*/
public readonly globalAllowBuilds: Record<string, boolean> | undefined;

/**
* The ignoredOptionalDependencies setting allows you to exclude certain optional dependencies from being installed
* during the Rush installation process. This can be useful when optional dependencies are not required or are
Expand Down Expand Up @@ -555,7 +573,14 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration
this.globalPeerDependencyRules = json.globalPeerDependencyRules;
this.globalPackageExtensions = json.globalPackageExtensions;
this.globalNeverBuiltDependencies = json.globalNeverBuiltDependencies;
if (json.globalOnlyBuiltDependencies !== undefined && json.globalAllowBuilds !== undefined) {
throw new Error(
'The "globalOnlyBuiltDependencies" and "globalAllowBuilds" settings cannot both be specified' +
' in pnpm-config.json. Use "globalAllowBuilds" for PNPM 11.0.0 and newer.'
);
}
this.globalOnlyBuiltDependencies = json.globalOnlyBuiltDependencies;
this.globalAllowBuilds = json.globalAllowBuilds;
this.globalIgnoredOptionalDependencies = json.globalIgnoredOptionalDependencies;
this.globalAllowedDeprecatedVersions = json.globalAllowedDeprecatedVersions;
this.unsupportedPackageJsonSettings = json.unsupportedPackageJsonSettings;
Expand Down Expand Up @@ -629,4 +654,14 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration
JsonFile.save(this._json, this.jsonFilename, { updateExistingFile: true });
}
}

/**
* Updates globalAllowBuilds field of the PNPM options in the common/config/rush/pnpm-config.json file.
*/
public updateGlobalAllowBuilds(allowBuilds: Record<string, boolean> | undefined): void {
this._json.globalAllowBuilds = allowBuilds;
if (this.jsonFilename) {
JsonFile.save(this._json, this.jsonFilename, { updateExistingFile: true });
}
}
}
Loading
Loading