diff --git a/common/changes/@microsoft/rush/copilot-pnpm11-allowbuilds-support_2026-06-04-01-00.json b/common/changes/@microsoft/rush/copilot-pnpm11-allowbuilds-support_2026-06-04-01-00.json new file mode 100644 index 00000000000..138c08f9e24 --- /dev/null +++ b/common/changes/@microsoft/rush/copilot-pnpm11-allowbuilds-support_2026-06-04-01-00.json @@ -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" +} diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 706fca89ba0..24c5a6606ac 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -752,6 +752,7 @@ export interface _IPnpmOptionsJson extends IPackageManagerOptionsJsonBase { alwaysFullInstall?: boolean; alwaysInjectDependenciesFromOtherSubspaces?: boolean; autoInstallPeers?: boolean; + globalAllowBuilds?: Record; globalAllowedDeprecatedVersions?: Record; globalCatalogs?: Record>; globalIgnoredOptionalDependencies?: string[]; @@ -1177,6 +1178,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration readonly alwaysFullInstall: boolean | undefined; readonly alwaysInjectDependenciesFromOtherSubspaces: boolean | undefined; readonly autoInstallPeers: boolean | undefined; + readonly globalAllowBuilds: Record | undefined; readonly globalAllowedDeprecatedVersions: Record | undefined; readonly globalCatalogs: Record> | undefined; readonly globalIgnoredOptionalDependencies: string[] | undefined; @@ -1206,6 +1208,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration readonly trustPolicyExclude: string[] | undefined; readonly trustPolicyIgnoreAfterMinutes: number | undefined; readonly unsupportedPackageJsonSettings: unknown | undefined; + updateGlobalAllowBuilds(allowBuilds: Record | undefined): void; updateGlobalOnlyBuiltDependencies(onlyBuiltDependencies: string[] | undefined): void; updateGlobalPatchedDependencies(patchedDependencies: Record | undefined): void; readonly useWorkspaces: boolean; diff --git a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts index 1980c57f085..6d1b270cce0 100644 --- a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts +++ b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts @@ -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( @@ -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 } = (yamlModule.load( + workspaceYamlContent + ) ?? {}) as { allowBuilds?: Record }; + const newGlobalAllowBuilds: Record | undefined = workspaceYaml?.allowBuilds; + const currentGlobalAllowBuilds: Record | 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; } diff --git a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts index fd0ba621639..1296125a985 100644 --- a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts +++ b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts @@ -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.' ) ); } @@ -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.' ) ); } @@ -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.' ) ); } @@ -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.' ) ); } @@ -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.' ) ); } diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index 9526f0e8d59..222dd451419 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -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 = {}; + 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 }); diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts index 22d4bfaa45f..dbaf9027c02 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts @@ -128,6 +128,10 @@ export interface IPnpmOptionsJson extends IPackageManagerOptionsJsonBase { * {@inheritDoc PnpmOptionsConfiguration.globalOnlyBuiltDependencies} */ globalOnlyBuiltDependencies?: string[]; + /** + * {@inheritDoc PnpmOptionsConfiguration.globalAllowBuilds} + */ + globalAllowBuilds?: Record; /** * {@inheritDoc PnpmOptionsConfiguration.globalIgnoredOptionalDependencies} */ @@ -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 | 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 @@ -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; @@ -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 | undefined): void { + this._json.globalAllowBuilds = allowBuilds; + if (this.jsonFilename) { + JsonFile.save(this._json, this.jsonFilename, { updateExistingFile: true }); + } + } } diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts index 99f007c1003..850aaf1687b 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts @@ -23,6 +23,10 @@ const yamlModule: typeof import('js-yaml') = Import.lazy('js-yaml', require); * "default": { * "react": "^18.0.0" * } + * }, + * "allowBuilds": { + * "esbuild": true, + * "fsevents": false * } * } */ @@ -31,6 +35,13 @@ interface IPnpmWorkspaceYaml { packages: string[]; /** Catalog definitions for centralized version management */ catalogs?: Record>; + /** + * Controls which packages are allowed to run build scripts. 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. + * (SUPPORTED ONLY IN PNPM 11.0.0 AND NEWER) + */ + allowBuilds?: Record; } export class PnpmWorkspaceFile extends BaseWorkspaceFile { @@ -41,6 +52,7 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { private _workspacePackages: Set; private _catalogs: Record> | undefined; + private _allowBuilds: Record | undefined; /** * The PNPM workspace file is used to specify the location of workspaces relative to the root @@ -54,6 +66,7 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { // If we need to support manual customization, that should be an additional parameter for "base file" this._workspacePackages = new Set(); this._catalogs = undefined; + this._allowBuilds = undefined; } /** @@ -64,6 +77,15 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { this._catalogs = catalogs; } + /** + * Sets the allowBuilds definitions for the workspace. + * This controls which packages are allowed to run build scripts in pnpm 11+. + * @param allowBuilds - A map of package name to boolean (true = allowed, false = denied) + */ + public setAllowBuilds(allowBuilds: Record | undefined): void { + this._allowBuilds = allowBuilds; + } + /** @override */ public addPackage(packagePath: string): void { // Ensure the path is relative to the pnpm-workspace.yaml file @@ -89,6 +111,10 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { workspaceYaml.catalogs = this._catalogs; } + if (this._allowBuilds && Object.keys(this._allowBuilds).length > 0) { + workspaceYaml.allowBuilds = this._allowBuilds; + } + return yamlModule.dump(workspaceYaml, PNPM_SHRINKWRAP_YAML_FORMAT); } } 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 531c4e78c77..eb546fdc357 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts @@ -87,6 +87,19 @@ describe(PnpmOptionsConfiguration.name, () => { ]); }); + it('loads allowBuilds', () => { + const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( + `${__dirname}/jsonFiles/pnpm-config-allowBuilds.json`, + fakeCommonTempFolder + ); + + expect(TestUtilities.stripAnnotations(pnpmConfiguration.globalAllowBuilds)).toEqual({ + esbuild: true, + '@parcel/watcher': true, + fsevents: false + }); + }); + it('loads minimumReleaseAgeMinutes', () => { const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( `${__dirname}/jsonFiles/pnpm-config-minimumReleaseAge.json`, 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 d8cdb149a88..a113bcc0b8e 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts @@ -180,4 +180,66 @@ describe(PnpmWorkspaceFile.name, () => { expect(content).toMatchSnapshot(); }); }); + + describe('allowBuilds functionality', () => { + it('generates workspace file with allowBuilds', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setAllowBuilds({ + esbuild: true, + '@parcel/watcher': true, + fsevents: false + }); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toMatchSnapshot(); + }); + + it('generates workspace file with allowBuilds and catalogs', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setCatalogs({ + default: { + react: '^18.0.0' + } + }); + + workspaceFile.setAllowBuilds({ + esbuild: true + }); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toMatchSnapshot(); + }); + + it('handles empty allowBuilds object', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setAllowBuilds({}); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).not.toContain('allowBuilds'); + }); + + it('handles undefined allowBuilds', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setAllowBuilds(undefined); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).not.toContain('allowBuilds'); + }); + }); }); 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 index 97c62b0db79..18f15e74569 100644 --- 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 @@ -1,5 +1,26 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +exports[`PnpmWorkspaceFile allowBuilds functionality generates workspace file with allowBuilds 1`] = ` +"allowBuilds: + '@parcel/watcher': true + esbuild: true + fsevents: false +packages: + - projects/app1 +" +`; + +exports[`PnpmWorkspaceFile allowBuilds functionality generates workspace file with allowBuilds and catalogs 1`] = ` +"allowBuilds: + esbuild: true +catalogs: + default: + react: ^18.0.0 +packages: + - projects/app1 +" +`; + exports[`PnpmWorkspaceFile basic functionality generates workspace file with packages only 1`] = ` "packages: - projects/app1 diff --git a/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-allowBuilds.json b/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-allowBuilds.json new file mode 100644 index 00000000000..8a99dc3b3ee --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-allowBuilds.json @@ -0,0 +1,7 @@ +{ + "globalAllowBuilds": { + "esbuild": true, + "@parcel/watcher": true, + "fsevents": false + } +} diff --git a/libraries/rush-lib/src/schemas/pnpm-config.schema.json b/libraries/rush-lib/src/schemas/pnpm-config.schema.json index 6bf53443cec..93be4e62642 100644 --- a/libraries/rush-lib/src/schemas/pnpm-config.schema.json +++ b/libraries/rush-lib/src/schemas/pnpm-config.schema.json @@ -151,7 +151,7 @@ }, "globalOnlyBuiltDependencies": { - "description": "This field allows specifying which dependencies are permitted to run build scripts (preinstall, install, postinstall). In PNPM 10.x, build scripts are disabled by default for security. Use this allowlist to explicitly permit specific packages to run their build scripts.\n\n(SUPPORTED ONLY IN PNPM 10.1.0 AND NEWER)\n\nPNPM documentation: https://pnpm.io/settings#onlybuiltdependencies", + "description": "This field allows specifying which dependencies are permitted to run build scripts (preinstall, install, postinstall). In PNPM 10.x, build scripts are disabled by default for security. Use this allowlist to explicitly permit specific packages to run their build scripts.\n\n(SUPPORTED ONLY IN PNPM 10.1.0 - 10.x; replaced by `globalAllowBuilds` in PNPM 11.0.0)\n\nPNPM documentation: https://pnpm.io/settings#onlybuiltdependencies", "type": "array", "items": { "description": "Specify package name of the dependency allowed to run build scripts", @@ -159,6 +159,14 @@ } }, + "globalAllowBuilds": { + "description": "This field controls which dependencies are allowed to run build scripts (preinstall, install, postinstall). 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.\n\n(SUPPORTED ONLY IN PNPM 11.0.0 AND NEWER)\n\nPNPM documentation: https://pnpm.io/settings#allowbuilds", + "type": "object", + "additionalProperties": { + "type": "boolean" + } + }, + "globalIgnoredOptionalDependencies": { "description": "This field allows you to skip the installation of specific optional dependencies. The listed packages will be treated as if they are not present in the dependency tree during installation, meaning they will not be installed even if required by other packages.\n\n(SUPPORTED ONLY IN PNPM 9.0.0 AND NEWER)\n\nPNPM documentation: https://pnpm.io/package_json#pnpmalloweddeprecatedversions", "type": "array",