Skip to content

Commit 2d4b726

Browse files
Copiloticlanton
andcommitted
Add globalOnlyBuiltDependencies support to PNPM configuration
Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
1 parent 4c89090 commit 2d4b726

File tree

8 files changed

+167
-1
lines changed

8 files changed

+167
-1
lines changed

libraries/rush-lib/assets/rush-init/common/config/rush/pnpm-config.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,31 @@
306306
/*[LINE "HYPOTHETICAL"]*/ "fsevents"
307307
],
308308

309+
/**
310+
* The `globalOnlyBuiltDependencies` setting specifies which dependencies are permitted to run
311+
* build scripts (`preinstall`, `install`, and `postinstall` lifecycle events). This is the inverse
312+
* of `globalNeverBuiltDependencies`. In PNPM 10.x, build scripts are disabled by default for
313+
* security, so this setting is required to explicitly permit specific packages to run their
314+
* build scripts. The settings are written to the `onlyBuiltDependencies` field of the
315+
* `pnpm-workspace.yaml` file that is generated by Rush during installation.
316+
*
317+
* (SUPPORTED ONLY IN PNPM 10.1.0 AND NEWER)
318+
*
319+
* PNPM documentation: https://pnpm.io/settings#onlybuiltdependencies
320+
*
321+
* Example:
322+
* "globalOnlyBuiltDependencies": [
323+
* "esbuild",
324+
* "playwright",
325+
* "@swc/core"
326+
* ]
327+
*/
328+
/*[BEGIN "HYPOTHETICAL"]*/
329+
"globalOnlyBuiltDependencies": [
330+
"esbuild"
331+
],
332+
/*[END "HYPOTHETICAL"]*/
333+
309334
/**
310335
* The `globalIgnoredOptionalDependencies` setting suppresses the installation of optional NPM
311336
* dependencies specified in the list. This is useful when certain optional dependencies are

libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,25 @@ export class WorkspaceInstallManager extends BaseInstallManager {
468468
workspaceFile.setCatalogs(catalogs);
469469
}
470470

471+
// Set onlyBuiltDependencies in the workspace file if specified
472+
if (pnpmOptions.globalOnlyBuiltDependencies) {
473+
if (
474+
this.rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined &&
475+
semver.lt(this.rushConfiguration.rushConfigurationJson.pnpmVersion, '10.1.0')
476+
) {
477+
this._terminal.writeWarningLine(
478+
Colorize.yellow(
479+
`Your version of pnpm (${this.rushConfiguration.rushConfigurationJson.pnpmVersion}) ` +
480+
`doesn't support the "globalOnlyBuiltDependencies" field in ` +
481+
`${this.rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` +
482+
'Remove this field or upgrade to pnpm 10.1.0 or newer.'
483+
)
484+
);
485+
}
486+
487+
workspaceFile.setOnlyBuiltDependencies(pnpmOptions.globalOnlyBuiltDependencies);
488+
}
489+
471490
// Save the generated workspace file. Don't update the file timestamp unless the content has changed,
472491
// since "rush install" will consider this timestamp
473492
workspaceFile.save(workspaceFile.workspaceFilename, { onlyIfChanged: true });

libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ export interface IPnpmOptionsJson extends IPackageManagerOptionsJsonBase {
114114
* {@inheritDoc PnpmOptionsConfiguration.globalNeverBuiltDependencies}
115115
*/
116116
globalNeverBuiltDependencies?: string[];
117+
/**
118+
* {@inheritDoc PnpmOptionsConfiguration.globalOnlyBuiltDependencies}
119+
*/
120+
globalOnlyBuiltDependencies?: string[];
117121
/**
118122
* {@inheritDoc PnpmOptionsConfiguration.globalIgnoredOptionalDependencies}
119123
*/
@@ -365,6 +369,20 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration
365369
*/
366370
public readonly globalNeverBuiltDependencies: string[] | undefined;
367371

372+
/**
373+
* The `globalOnlyBuiltDependencies` setting specifies an allowlist of dependencies that are permitted
374+
* to run build scripts (`preinstall`, `install`, and `postinstall` lifecycle events). This is the inverse
375+
* of `globalNeverBuiltDependencies`. In PNPM 10.x, build scripts are disabled by default for security,
376+
* so this setting is required to explicitly permit specific packages to run their build scripts.
377+
* The settings are written to the `onlyBuiltDependencies` field of the `pnpm-workspace.yaml` file
378+
* that is generated by Rush during installation.
379+
*
380+
* (SUPPORTED ONLY IN PNPM 10.1.0 AND NEWER)
381+
*
382+
* PNPM documentation: https://pnpm.io/settings#onlybuiltdependencies
383+
*/
384+
public readonly globalOnlyBuiltDependencies: string[] | undefined;
385+
368386
/**
369387
* The ignoredOptionalDependencies setting allows you to exclude certain optional dependencies from being installed
370388
* during the Rush installation process. This can be useful when optional dependencies are not required or are
@@ -468,6 +486,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration
468486
this.globalPeerDependencyRules = json.globalPeerDependencyRules;
469487
this.globalPackageExtensions = json.globalPackageExtensions;
470488
this.globalNeverBuiltDependencies = json.globalNeverBuiltDependencies;
489+
this.globalOnlyBuiltDependencies = json.globalOnlyBuiltDependencies;
471490
this.globalIgnoredOptionalDependencies = json.globalIgnoredOptionalDependencies;
472491
this.globalAllowedDeprecatedVersions = json.globalAllowedDeprecatedVersions;
473492
this.unsupportedPackageJsonSettings = json.unsupportedPackageJsonSettings;

libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,20 @@ const globEscape: (unescaped: string) => string = require('glob-escape'); // No
2323
* "default": {
2424
* "react": "^18.0.0"
2525
* }
26-
* }
26+
* },
27+
* "onlyBuiltDependencies": [
28+
* "esbuild",
29+
* "playwright"
30+
* ]
2731
* }
2832
*/
2933
interface IPnpmWorkspaceYaml {
3034
/** The list of local package directories */
3135
packages: string[];
3236
/** Catalog definitions for centralized version management */
3337
catalogs?: Record<string, Record<string, string>>;
38+
/** Allowlist of dependencies permitted to run build scripts (PNPM 10.1.0+) */
39+
onlyBuiltDependencies?: string[];
3440
}
3541

3642
export class PnpmWorkspaceFile extends BaseWorkspaceFile {
@@ -41,6 +47,7 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile {
4147

4248
private _workspacePackages: Set<string>;
4349
private _catalogs: Record<string, Record<string, string>> | undefined;
50+
private _onlyBuiltDependencies: string[] | undefined;
4451

4552
/**
4653
* The PNPM workspace file is used to specify the location of workspaces relative to the root
@@ -54,6 +61,7 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile {
5461
// If we need to support manual customization, that should be an additional parameter for "base file"
5562
this._workspacePackages = new Set<string>();
5663
this._catalogs = undefined;
64+
this._onlyBuiltDependencies = undefined;
5765
}
5866

5967
/**
@@ -64,6 +72,15 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile {
6472
this._catalogs = catalogs;
6573
}
6674

75+
/**
76+
* Sets the onlyBuiltDependencies list for the workspace.
77+
* This specifies which dependencies are allowed to run build scripts in PNPM 10.1.0+.
78+
* @param deps - An array of package names allowed to run build scripts
79+
*/
80+
public setOnlyBuiltDependencies(deps: string[] | undefined): void {
81+
this._onlyBuiltDependencies = deps;
82+
}
83+
6784
/** @override */
6885
public addPackage(packagePath: string): void {
6986
// Ensure the path is relative to the pnpm-workspace.yaml file
@@ -89,6 +106,10 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile {
89106
workspaceYaml.catalogs = this._catalogs;
90107
}
91108

109+
if (this._onlyBuiltDependencies && this._onlyBuiltDependencies.length > 0) {
110+
workspaceYaml.onlyBuiltDependencies = this._onlyBuiltDependencies;
111+
}
112+
92113
return yamlModule.dump(workspaceYaml, PNPM_SHRINKWRAP_YAML_FORMAT);
93114
}
94115
}

libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,19 @@ describe(PnpmOptionsConfiguration.name, () => {
7474
]);
7575
});
7676

77+
it('loads onlyBuiltDependencies', () => {
78+
const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow(
79+
`${__dirname}/jsonFiles/pnpm-config-onlyBuiltDependencies.json`,
80+
fakeCommonTempFolder
81+
);
82+
83+
expect(TestUtilities.stripAnnotations(pnpmConfiguration.globalOnlyBuiltDependencies)).toEqual([
84+
'esbuild',
85+
'playwright',
86+
'@swc/core'
87+
]);
88+
});
89+
7790
it('loads minimumReleaseAge', () => {
7891
const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow(
7992
`${__dirname}/jsonFiles/pnpm-config-minimumReleaseAge.json`,

libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,61 @@ describe(PnpmWorkspaceFile.name, () => {
180180
expect(content).toMatchSnapshot();
181181
});
182182
});
183+
184+
describe('onlyBuiltDependencies functionality', () => {
185+
it('generates workspace file with onlyBuiltDependencies', () => {
186+
const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath);
187+
workspaceFile.addPackage(path.join(projectsDir, 'app1'));
188+
189+
workspaceFile.setOnlyBuiltDependencies(['esbuild', 'playwright', '@swc/core']);
190+
191+
workspaceFile.save(workspaceFilePath, { onlyIfChanged: true });
192+
193+
const content: string = FileSystem.readFile(workspaceFilePath);
194+
expect(content).toMatchSnapshot();
195+
});
196+
197+
it('handles empty onlyBuiltDependencies array', () => {
198+
const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath);
199+
workspaceFile.addPackage(path.join(projectsDir, 'app1'));
200+
201+
workspaceFile.setOnlyBuiltDependencies([]);
202+
203+
workspaceFile.save(workspaceFilePath, { onlyIfChanged: true });
204+
205+
const content: string = FileSystem.readFile(workspaceFilePath);
206+
expect(content).toMatchSnapshot();
207+
});
208+
209+
it('handles undefined onlyBuiltDependencies', () => {
210+
const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath);
211+
workspaceFile.addPackage(path.join(projectsDir, 'app1'));
212+
213+
workspaceFile.setOnlyBuiltDependencies(undefined);
214+
215+
workspaceFile.save(workspaceFilePath, { onlyIfChanged: true });
216+
217+
const content: string = FileSystem.readFile(workspaceFilePath);
218+
expect(content).toMatchSnapshot();
219+
});
220+
221+
it('generates workspace file with both catalogs and onlyBuiltDependencies', () => {
222+
const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath);
223+
workspaceFile.addPackage(path.join(projectsDir, 'app1'));
224+
225+
workspaceFile.setCatalogs({
226+
default: {
227+
react: '^18.0.0',
228+
'react-dom': '^18.0.0'
229+
}
230+
});
231+
232+
workspaceFile.setOnlyBuiltDependencies(['esbuild', 'playwright']);
233+
234+
workspaceFile.save(workspaceFilePath, { onlyIfChanged: true });
235+
236+
const content: string = FileSystem.readFile(workspaceFilePath);
237+
expect(content).toMatchSnapshot();
238+
});
239+
});
183240
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"globalOnlyBuiltDependencies": ["esbuild", "playwright", "@swc/core"]
3+
}

libraries/rush-lib/src/schemas/pnpm-config.schema.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,15 @@
150150
}
151151
},
152152

153+
"globalOnlyBuiltDependencies": {
154+
"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",
155+
"type": "array",
156+
"items": {
157+
"description": "Specify package name of the dependency allowed to run build scripts",
158+
"type": "string"
159+
}
160+
},
161+
153162
"globalIgnoredOptionalDependencies": {
154163
"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",
155164
"type": "array",

0 commit comments

Comments
 (0)