Skip to content
Draft
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
Expand Up @@ -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
Expand All @@ -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
Expand Down
28 changes: 27 additions & 1 deletion libraries/rush-lib/src/logic/installManager/InstallHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
73 changes: 73 additions & 0 deletions libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,19 @@ export interface IPnpmOptionsJson extends IPackageManagerOptionsJsonBase {
*/
globalPackageExtensions?: Record<string, IPnpmPackageExtension>;
/**
* {@inheritDoc PnpmOptionsConfiguration.globalStrictDepBuilds}
*/
globalStrictDepBuilds?: boolean;
/**
* {@inheritDoc PnpmOptionsConfiguration.globalAllowBuilds}
*/
globalAllowBuilds?: Record<string, boolean>;
/**
* {@inheritDoc PnpmOptionsConfiguration.globalDangerouslyAllowAllBuilds}
*/
globalDangerouslyAllowAllBuilds?: boolean;
/**
* @deprecated Use {@link IPnpmOptionsJson.globalAllowBuilds} instead.
* {@inheritDoc PnpmOptionsConfiguration.globalNeverBuiltDependencies}
*/
globalNeverBuiltDependencies?: string[];
Expand Down Expand Up @@ -425,6 +438,54 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration
*/
public readonly globalPackageExtensions: Record<string, IPnpmPackageExtension> | 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<string, boolean> | 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
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
70 changes: 70 additions & 0 deletions libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ interface IPnpmWorkspaceYaml {
packages: string[];
/** Catalog definitions for centralized version management */
catalogs?: Record<string, Record<string, string>>;
/** Per-package build permission map. True permits build scripts, false blocks them. (pnpm 10.26.0+) */
allowBuilds?: Record<string, boolean>;
/**
* 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 {
Expand All @@ -41,6 +53,9 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile {

private _workspacePackages: Set<string>;
private _catalogs: Record<string, Record<string, string>> | undefined;
private _allowBuilds: Record<string, boolean> | 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
Expand All @@ -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<string>();
this._catalogs = undefined;
this._allowBuilds = undefined;
this._strictDepBuilds = undefined;
this._dangerouslyAllowAllBuilds = undefined;
}

/**
Expand All @@ -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<string, boolean> | 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
Expand All @@ -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);
}
}
Loading
Loading