diff --git a/libraries/rush-lib/assets/rush-init/common/config/rush/pnpm-config.json b/libraries/rush-lib/assets/rush-init/common/config/rush/pnpm-config.json index 9d5f764bc67..ec3ac1a55a5 100644 --- a/libraries/rush-lib/assets/rush-init/common/config/rush/pnpm-config.json +++ b/libraries/rush-lib/assets/rush-init/common/config/rush/pnpm-config.json @@ -344,6 +344,8 @@ }, /** + * @deprecated Use `globalAllowBuilds` instead. + * * The `globalNeverBuiltDependencies` setting suppresses the `preinstall`, `install`, and `postinstall` * lifecycle events for the specified NPM dependencies. This is useful for scripts with poor practices * such as downloading large binaries without retries or attempting to invoke OS tools such as @@ -352,12 +354,71 @@ * The settings are copied into the `pnpm.neverBuiltDependencies` field of the `common/temp/package.json` * file that is generated by Rush during installation. * + * NOTE: This setting is not supported in pnpm 11.0.0+. Use `globalAllowBuilds` instead. + * * PNPM documentation: https://pnpm.io/package_json#pnpmneverbuiltdependencies */ "globalNeverBuiltDependencies": [ /*[LINE "HYPOTHETICAL"]*/ "fsevents" ], + /** + * The `globalAllowBuilds` setting is a map of package names to booleans that controls which + * dependencies are permitted to run build scripts (`preinstall`, `install`, `postinstall` + * lifecycle events). A value of `true` explicitly permits a package to run build scripts; + * a value of `false` explicitly blocks it. Packages not listed inherit the default behavior. + * + * This is the replacement for `globalNeverBuiltDependencies` and `globalOnlyBuiltDependencies`. + * 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 10.26.0 AND NEWER) + * + * PNPM documentation: https://pnpm.io/settings#allowbuilds + * + * Example: + * "globalAllowBuilds": { + * "esbuild": true, + * "playwright": true, + * "core-js": false + * } + */ + /*[BEGIN "HYPOTHETICAL"]*/ + "globalAllowBuilds": { + "esbuild": true + }, + /*[END "HYPOTHETICAL"]*/ + + /** + * When `globalStrictDepBuilds` is enabled, the installation will exit with a non-zero exit code + * if any dependencies have unreviewed build scripts (i.e., scripts not explicitly listed in + * `globalAllowBuilds`). This helps enforce that all package build permissions are intentionally + * reviewed and approved. The setting maps to the `strictDepBuilds` field of the + * `pnpm-workspace.yaml` file generated by Rush during installation. + * + * (SUPPORTED ONLY IN PNPM 10.3.0 AND NEWER) + * + * PNPM documentation: https://pnpm.io/settings#strictdepbuilds + */ + /*[LINE "HYPOTHETICAL"]*/ "globalStrictDepBuilds": false, + + /** + * If set to `true`, all build scripts (`preinstall`, `install`, `postinstall`) from all + * dependencies will run automatically without requiring explicit approval via `globalAllowBuilds`. + * The setting maps to the `dangerouslyAllowAllBuilds` field of the `pnpm-workspace.yaml` file + * generated by Rush during installation. + * + * WARNING: This allows all dependencies—including transitive ones—to run install scripts, both + * now and in the future. Future updates may introduce new, untrusted dependencies, or existing + * packages may add malicious scripts. For maximum safety, use `globalAllowBuilds` to explicitly + * review and allow builds. + * + * (SUPPORTED ONLY IN PNPM 10.9.0 AND NEWER) + * + * PNPM documentation: https://pnpm.io/settings#dangerouslyallowallbuilds + */ + /*[LINE "HYPOTHETICAL"]*/ "globalDangerouslyAllowAllBuilds": false, + /** * The `globalOnlyBuiltDependencies` setting specifies which dependencies are permitted to run * build scripts (`preinstall`, `install`, and `postinstall` lifecycle events). This is the inverse diff --git a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts index fd0ba621639..81f65019a08 100644 --- a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts +++ b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts @@ -77,7 +77,33 @@ export class InstallHelpers { } if (pnpmOptions.globalNeverBuiltDependencies) { - commonPackageJson.pnpm.neverBuiltDependencies = pnpmOptions.globalNeverBuiltDependencies; + if ( + rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined && + semver.gte(rushConfiguration.rushConfigurationJson.pnpmVersion, '11.0.0') + ) { + terminal.writeWarningLine( + Colorize.yellow( + `The "globalNeverBuiltDependencies" field in ` + + `${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename} ` + + `is deprecated and will be ignored by pnpm ${rushConfiguration.rushConfigurationJson.pnpmVersion} ` + + `(pnpm 11+ no longer reads build settings from package.json). ` + + 'Migrate to "globalAllowBuilds" instead. ' + + `For example, replace "globalNeverBuiltDependencies": ["pkg"] ` + + `with "globalAllowBuilds": { "pkg": false }.` + ) + ); + } else { + terminal.writeWarningLine( + Colorize.yellow( + `The "globalNeverBuiltDependencies" field in ` + + `${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename} ` + + 'is deprecated. Migrate to "globalAllowBuilds" instead. ' + + `For example, replace "globalNeverBuiltDependencies": ["pkg"] ` + + `with "globalAllowBuilds": { "pkg": false }.` + ) + ); + commonPackageJson.pnpm!.neverBuiltDependencies = pnpmOptions.globalNeverBuiltDependencies; + } } if (pnpmOptions.globalOnlyBuiltDependencies) { diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index 9526f0e8d59..bd59666a122 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -469,6 +469,63 @@ export class WorkspaceInstallManager extends BaseInstallManager { workspaceFile.setCatalogs(catalogs); } + // Set strictDepBuilds in the workspace file if specified (requires pnpm 10.3.0+) + if (pnpmOptions.globalStrictDepBuilds !== undefined) { + if ( + this.rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined && + semver.lt(this.rushConfiguration.rushConfigurationJson.pnpmVersion, '10.3.0') + ) { + this._terminal.writeWarningLine( + Colorize.yellow( + `Your version of pnpm (${this.rushConfiguration.rushConfigurationJson.pnpmVersion}) ` + + `doesn't support the "globalStrictDepBuilds" field in ` + + `${this.rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` + + 'Remove this field or upgrade to pnpm 10.3.0 or newer.' + ) + ); + } + + workspaceFile.setStrictDepBuilds(pnpmOptions.globalStrictDepBuilds); + } + + // Set allowBuilds in the workspace file if specified (requires pnpm 10.26.0+) + if (pnpmOptions.globalAllowBuilds) { + if ( + this.rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined && + semver.lt(this.rushConfiguration.rushConfigurationJson.pnpmVersion, '10.26.0') + ) { + 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 10.26.0 or newer.' + ) + ); + } + + workspaceFile.setAllowBuilds(pnpmOptions.globalAllowBuilds); + } + + // Set dangerouslyAllowAllBuilds in the workspace file if specified (requires pnpm 10.9.0+) + if (pnpmOptions.globalDangerouslyAllowAllBuilds !== undefined) { + if ( + this.rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined && + semver.lt(this.rushConfiguration.rushConfigurationJson.pnpmVersion, '10.9.0') + ) { + this._terminal.writeWarningLine( + Colorize.yellow( + `Your version of pnpm (${this.rushConfiguration.rushConfigurationJson.pnpmVersion}) ` + + `doesn't support the "globalDangerouslyAllowAllBuilds" field in ` + + `${this.rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` + + 'Remove this field or upgrade to pnpm 10.9.0 or newer.' + ) + ); + } + + workspaceFile.setDangerouslyAllowAllBuilds(pnpmOptions.globalDangerouslyAllowAllBuilds); + } + // 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..266ca35cfc3 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts @@ -121,6 +121,19 @@ export interface IPnpmOptionsJson extends IPackageManagerOptionsJsonBase { */ globalPackageExtensions?: Record; /** + * {@inheritDoc PnpmOptionsConfiguration.globalStrictDepBuilds} + */ + globalStrictDepBuilds?: boolean; + /** + * {@inheritDoc PnpmOptionsConfiguration.globalAllowBuilds} + */ + globalAllowBuilds?: Record; + /** + * {@inheritDoc PnpmOptionsConfiguration.globalDangerouslyAllowAllBuilds} + */ + globalDangerouslyAllowAllBuilds?: boolean; + /** + * @deprecated Use {@link IPnpmOptionsJson.globalAllowBuilds} instead. * {@inheritDoc PnpmOptionsConfiguration.globalNeverBuiltDependencies} */ globalNeverBuiltDependencies?: string[]; @@ -425,6 +438,54 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration */ public readonly globalPackageExtensions: Record | undefined; + /** + * The `globalStrictDepBuilds` setting causes the installation to fail with a non-zero exit code if + * any dependencies have unreviewed build scripts (i.e., scripts not explicitly listed in + * `globalAllowBuilds`). This helps enforce that all package build permissions are intentionally + * reviewed and approved. The setting maps to the `strictDepBuilds` field of the + * `pnpm-workspace.yaml` file generated by Rush during installation. + * + * (SUPPORTED ONLY IN PNPM 10.3.0 AND NEWER) + * + * PNPM documentation: https://pnpm.io/settings#strictdepbuilds + */ + public readonly globalStrictDepBuilds: boolean | undefined; + + /** + * The `globalAllowBuilds` setting is a map of package names to booleans that controls which + * dependencies are permitted to run build scripts (`preinstall`, `install`, `postinstall` + * lifecycle events). A value of `true` explicitly permits a package to run build scripts; + * a value of `false` explicitly blocks it. Packages not listed inherit the default behavior. + * + * This is the replacement for `globalNeverBuiltDependencies` and `globalOnlyBuiltDependencies`. + * 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 10.26.0 AND NEWER) + * + * PNPM documentation: https://pnpm.io/settings#allowbuilds + */ + public readonly globalAllowBuilds: Record | undefined; + + /** + * The `globalDangerouslyAllowAllBuilds` setting, when `true`, allows all build scripts + * (`preinstall`, `install`, `postinstall`) from all dependencies to run automatically without + * requiring explicit approval in `globalAllowBuilds`. The setting maps to the + * `dangerouslyAllowAllBuilds` field of the `pnpm-workspace.yaml` file generated by Rush during + * installation. + * + * @remarks + * **Use with caution.** This permits every dependency—including transitive ones—to execute + * install scripts, both now and in the future. Future dependency updates or package compromises + * could introduce malicious code. For maximum safety, explicitly review and allow builds using + * `globalAllowBuilds` instead. + * + * (SUPPORTED ONLY IN PNPM 10.9.0 AND NEWER) + * + * PNPM documentation: https://pnpm.io/settings#dangerouslyallowallbuilds + */ + public readonly globalDangerouslyAllowAllBuilds: boolean | undefined; + /** * The `globalNeverBuiltDependencies` setting suppresses the `preinstall`, `install`, and `postinstall` * lifecycle events for the specified NPM dependencies. This is useful for scripts with poor practices @@ -434,6 +495,8 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration * The settings are copied into the `pnpm.neverBuiltDependencies` field of the `common/temp/package.json` * file that is generated by Rush during installation. * + * @deprecated Use {@link PnpmOptionsConfiguration.globalAllowBuilds} instead. + * * PNPM documentation: https://pnpm.io/package_json#pnpmneverbuiltdependencies */ public readonly globalNeverBuiltDependencies: string[] | undefined; @@ -554,6 +617,16 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration this.globalOverrides = json.globalOverrides; this.globalPeerDependencyRules = json.globalPeerDependencyRules; this.globalPackageExtensions = json.globalPackageExtensions; + + if (json.globalNeverBuiltDependencies !== undefined && json.globalAllowBuilds !== undefined) { + throw new Error( + 'The "globalNeverBuiltDependencies" setting is deprecated. Use "globalAllowBuilds" instead.' + + ' Both settings cannot be specified together in pnpm-config.json.' + ); + } + this.globalStrictDepBuilds = json.globalStrictDepBuilds; + this.globalAllowBuilds = json.globalAllowBuilds; + this.globalDangerouslyAllowAllBuilds = json.globalDangerouslyAllowAllBuilds; this.globalNeverBuiltDependencies = json.globalNeverBuiltDependencies; this.globalOnlyBuiltDependencies = json.globalOnlyBuiltDependencies; this.globalIgnoredOptionalDependencies = json.globalIgnoredOptionalDependencies; diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts index 99f007c1003..ed0ed6f2920 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts @@ -31,6 +31,18 @@ interface IPnpmWorkspaceYaml { packages: string[]; /** Catalog definitions for centralized version management */ catalogs?: Record>; + /** Per-package build permission map. True permits build scripts, false blocks them. (pnpm 10.26.0+) */ + allowBuilds?: Record; + /** + * When true, installation exits with non-zero if any dependencies have unreviewed build scripts. + * (pnpm 10.3.0+) + */ + strictDepBuilds?: boolean; + /** + * When true, all build scripts from dependencies run automatically without requiring approval. + * (pnpm 10.9.0+) + */ + dangerouslyAllowAllBuilds?: boolean; } export class PnpmWorkspaceFile extends BaseWorkspaceFile { @@ -41,6 +53,9 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { private _workspacePackages: Set; private _catalogs: Record> | undefined; + private _allowBuilds: Record | undefined; + private _strictDepBuilds: boolean | undefined; + private _dangerouslyAllowAllBuilds: boolean | undefined; /** * The PNPM workspace file is used to specify the location of workspaces relative to the root @@ -54,6 +69,9 @@ 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; + this._strictDepBuilds = undefined; + this._dangerouslyAllowAllBuilds = undefined; } /** @@ -64,6 +82,46 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { this._catalogs = catalogs; } + /** + * Sets the `allowBuilds` map for the workspace. Each key is a package name and each value + * is `true` (permit build scripts) or `false` (block build scripts). + * + * @remarks + * This writes to the `allowBuilds` field in `pnpm-workspace.yaml`, which requires pnpm 10.26.0+. + * + * @param allowBuilds - A map of package name to boolean permission flag + */ + public setAllowBuilds(allowBuilds: Record | undefined): void { + this._allowBuilds = allowBuilds; + } + + /** + * Sets the `strictDepBuilds` flag for the workspace. When `true`, installation exits with a + * non-zero exit code if any dependencies have unreviewed build scripts. + * + * @remarks + * This writes to the `strictDepBuilds` field in `pnpm-workspace.yaml`, which requires pnpm 10.3.0+. + * + * @param strictDepBuilds - Whether to enforce strict build script review + */ + public setStrictDepBuilds(strictDepBuilds: boolean | undefined): void { + this._strictDepBuilds = strictDepBuilds; + } + + /** + * Sets the `dangerouslyAllowAllBuilds` flag for the workspace. When `true`, all build scripts + * from all dependencies run automatically without requiring approval. + * + * @remarks + * This writes to the `dangerouslyAllowAllBuilds` field in `pnpm-workspace.yaml`, which requires + * pnpm 10.9.0+. + * + * @param dangerouslyAllowAllBuilds - Whether to allow all build scripts unconditionally + */ + public setDangerouslyAllowAllBuilds(dangerouslyAllowAllBuilds: boolean | undefined): void { + this._dangerouslyAllowAllBuilds = dangerouslyAllowAllBuilds; + } + /** @override */ public addPackage(packagePath: string): void { // Ensure the path is relative to the pnpm-workspace.yaml file @@ -89,6 +147,18 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { workspaceYaml.catalogs = this._catalogs; } + if (this._allowBuilds && Object.keys(this._allowBuilds).length > 0) { + workspaceYaml.allowBuilds = this._allowBuilds; + } + + if (this._strictDepBuilds !== undefined) { + workspaceYaml.strictDepBuilds = this._strictDepBuilds; + } + + if (this._dangerouslyAllowAllBuilds !== undefined) { + workspaceYaml.dangerouslyAllowAllBuilds = this._dangerouslyAllowAllBuilds; + } + 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..c34683bde66 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,47 @@ describe(PnpmOptionsConfiguration.name, () => { ]); }); + it('loads strictDepBuilds', () => { + const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( + `${__dirname}/jsonFiles/pnpm-config-strictDepBuilds.json`, + fakeCommonTempFolder + ); + + expect(pnpmConfiguration.globalStrictDepBuilds).toBe(true); + }); + + it('loads allowBuilds', () => { + const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( + `${__dirname}/jsonFiles/pnpm-config-allowBuilds.json`, + fakeCommonTempFolder + ); + + expect(TestUtilities.stripAnnotations(pnpmConfiguration.globalAllowBuilds)).toEqual({ + esbuild: true, + playwright: true, + 'core-js': false, + '@swc/core': true + }); + }); + + it('throws if both globalNeverBuiltDependencies and globalAllowBuilds are specified', () => { + expect(() => + PnpmOptionsConfiguration.loadFromJsonFileOrThrow( + `${__dirname}/jsonFiles/pnpm-config-allowBuilds-conflict.json`, + fakeCommonTempFolder + ) + ).toThrow(/Both settings cannot be specified together/); + }); + + it('loads dangerouslyAllowAllBuilds', () => { + const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( + `${__dirname}/jsonFiles/pnpm-config-dangerouslyAllowAllBuilds.json`, + fakeCommonTempFolder + ); + + expect(pnpmConfiguration.globalDangerouslyAllowAllBuilds).toBe(true); + }); + 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..dd226520513 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,132 @@ 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, + playwright: true, + 'core-js': false + }); + + 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).toMatchSnapshot(); + }); + + 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).toMatchSnapshot(); + }); + + it('generates workspace file with both catalogs and allowBuilds', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setCatalogs({ + default: { + react: '^18.0.0' + } + }); + + workspaceFile.setAllowBuilds({ + esbuild: true, + 'core-js': false + }); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toMatchSnapshot(); + }); + }); + + describe('strictDepBuilds functionality', () => { + it('generates workspace file with strictDepBuilds enabled', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setStrictDepBuilds(true); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toMatchSnapshot(); + }); + + it('generates workspace file with strictDepBuilds disabled', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setStrictDepBuilds(false); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toMatchSnapshot(); + }); + + it('does not include strictDepBuilds when undefined', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setStrictDepBuilds(undefined); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toMatchSnapshot(); + }); + }); + + describe('dangerouslyAllowAllBuilds functionality', () => { + it('generates workspace file with dangerouslyAllowAllBuilds enabled', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setDangerouslyAllowAllBuilds(true); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toMatchSnapshot(); + }); + + it('generates workspace file with all build-related settings combined', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setStrictDepBuilds(true); + workspaceFile.setAllowBuilds({ esbuild: true, 'core-js': false }); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toMatchSnapshot(); + }); + }); }); 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..87c09674778 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 @@ -65,3 +65,39 @@ exports[`PnpmWorkspaceFile catalog functionality handles undefined catalog 1`] = - projects/app1 " `; + +exports[`PnpmWorkspaceFile allowBuilds functionality generates workspace file with allowBuilds 1`] = ` +"allowBuilds: + core-js: false + esbuild: true + playwright: true +packages: + - projects/app1 +" +`; + +exports[`PnpmWorkspaceFile allowBuilds functionality generates workspace file with both catalogs and allowBuilds 1`] = ` +"allowBuilds: + core-js: false + esbuild: true +catalogs: + default: + react: ^18.0.0 +packages: + - projects/app1 +" +`; + +exports[`PnpmWorkspaceFile allowBuilds functionality handles empty allowBuilds object 1`] = ` +"packages: + - projects/app1 +" +`; + +exports[`PnpmWorkspaceFile allowBuilds functionality handles undefined allowBuilds 1`] = ` +"packages: + - projects/app1 +" +`; + + diff --git a/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-allowBuilds-conflict.json b/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-allowBuilds-conflict.json new file mode 100644 index 00000000000..76c8b404d47 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-allowBuilds-conflict.json @@ -0,0 +1,6 @@ +{ + "globalNeverBuiltDependencies": ["fsevents"], + "globalAllowBuilds": { + "esbuild": true + } +} 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..2d2074f67ac --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-allowBuilds.json @@ -0,0 +1,8 @@ +{ + "globalAllowBuilds": { + "esbuild": true, + "playwright": true, + "core-js": false, + "@swc/core": true + } +} diff --git a/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-dangerouslyAllowAllBuilds.json b/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-dangerouslyAllowAllBuilds.json new file mode 100644 index 00000000000..e025552fd21 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-dangerouslyAllowAllBuilds.json @@ -0,0 +1,3 @@ +{ + "globalDangerouslyAllowAllBuilds": true +} diff --git a/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-strictDepBuilds.json b/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-strictDepBuilds.json new file mode 100644 index 00000000000..93c814f79c2 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-strictDepBuilds.json @@ -0,0 +1,3 @@ +{ + "globalStrictDepBuilds": true +} diff --git a/libraries/rush-lib/src/logic/test/__snapshots__/InstallHelpers.test.ts.snap b/libraries/rush-lib/src/logic/test/__snapshots__/InstallHelpers.test.ts.snap deleted file mode 100644 index 080d68c7baf..00000000000 --- a/libraries/rush-lib/src/logic/test/__snapshots__/InstallHelpers.test.ts.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[`InstallHelpers generateCommonPackageJson generates correct package json with pnpm configurations: Terminal Output 1`] = `Array []`; diff --git a/libraries/rush-lib/src/schemas/pnpm-config.schema.json b/libraries/rush-lib/src/schemas/pnpm-config.schema.json index 6bf53443cec..88b313515ea 100644 --- a/libraries/rush-lib/src/schemas/pnpm-config.schema.json +++ b/libraries/rush-lib/src/schemas/pnpm-config.schema.json @@ -141,8 +141,25 @@ } }, + "globalStrictDepBuilds": { + "description": "When `globalStrictDepBuilds` is enabled, the installation will exit with a non-zero exit code if any dependencies have unreviewed build scripts (i.e., scripts not explicitly listed in `globalAllowBuilds`). This helps enforce that all package build permissions are intentionally reviewed and approved. The setting maps to the `strictDepBuilds` field of the `pnpm-workspace.yaml` file generated by Rush during installation.\n\n(SUPPORTED ONLY IN PNPM 10.3.0 AND NEWER)\n\nPNPM documentation: https://pnpm.io/settings#strictdepbuilds", + "type": "boolean" + }, + + "globalAllowBuilds": { + "description": "The `globalAllowBuilds` setting is a map of package names to booleans that controls which dependencies are permitted to run build scripts (`preinstall`, `install`, `postinstall` lifecycle events). A value of `true` explicitly permits a package to run build scripts; a value of `false` explicitly blocks it. Packages not listed inherit the default behavior.\n\nThis is the replacement for `globalNeverBuiltDependencies` and `globalOnlyBuiltDependencies`. 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 10.26.0 AND NEWER)\n\nPNPM documentation: https://pnpm.io/settings#allowbuilds", + "type": "object", + "additionalProperties": { + "type": "boolean" + } + }, + + "globalDangerouslyAllowAllBuilds": { + "description": "If set to `true`, all build scripts (`preinstall`, `install`, `postinstall`) from all dependencies will run automatically without requiring explicit approval via `globalAllowBuilds`. The setting maps to the `dangerouslyAllowAllBuilds` field of the `pnpm-workspace.yaml` file generated by Rush during installation.\n\n**WARNING:** This allows all dependencies—including transitive ones—to run install scripts, both now and in the future. Future updates may introduce new, untrusted dependencies, or existing packages may add malicious scripts. For maximum safety, use `globalAllowBuilds` to explicitly review and allow builds.\n\n(SUPPORTED ONLY IN PNPM 10.9.0 AND NEWER)\n\nPNPM documentation: https://pnpm.io/settings#dangerouslyallowallbuilds", + "type": "boolean" + }, "globalNeverBuiltDependencies": { - "description": "This field allows to ignore the builds of specific dependencies. The \"preinstall\", \"install\", and \"postinstall\" scripts of the listed packages will not be executed during installation.", + "description": "@deprecated - Use `globalAllowBuilds` instead.\n\nThis field allows to ignore the builds of specific dependencies. The \"preinstall\", \"install\", and \"postinstall\" scripts of the listed packages will not be executed during installation.", "type": "array", "items": { "description": "Specify package name of the dependency",