feat: create a new field for allowing/disallowing builds (#10311)

ref #10235
This commit is contained in:
Zoltan Kochan
2025-12-13 22:14:27 +01:00
parent a21fe77d33
commit 59a81aaa6d
8 changed files with 111 additions and 5 deletions

View File

@@ -0,0 +1,27 @@
---
"@pnpm/workspace.manifest-writer": minor
"@pnpm/types": minor
"@pnpm/config": minor
"pnpm": minor
---
Added support for `allowBuilds`, which is a new field that can be used instead of `onlyBuiltDependencies` and `ignoredBuiltDependencies`. The new `allowBuilds` field in your `pnpm-workspace.yaml` uses a map of package matchers to explicitly allow (`true`) or disallow (`false`) script execution. This allows for a single, easy-to-manage source of truth for your build permissions.
**Example Usage.** To explicitly allow all versions of `esbuild` to run scripts and prevent `core-js` from running them:
```yaml
allowBuilds:
esbuild: true
core-js: false
```
The example above achieves the same result as the previous configuration:
```yaml
onlyBuiltDependencies:
- esbuild
ignoredBuiltDependencies:
- core-js
```
Related PR: [#10311](https://github.com/pnpm/pnpm/pull/10311)

View File

@@ -185,6 +185,7 @@ export interface Config extends OptionsFromRootManifest {
gitShallowHosts?: string[]
legacyDirFiltering?: boolean
onlyBuiltDependencies?: string[]
allowBuilds?: Record<string, boolean | string>
dedupePeerDependents?: boolean
patchesDir?: string
ignoreWorkspaceCycles?: boolean

View File

@@ -5,6 +5,7 @@ export const DEPS_BUILD_CONFIG_KEYS = [
'onlyBuiltDependencies',
'onlyBuiltDependenciesFile',
'neverBuiltDependencies',
'allowBuilds',
] as const satisfies Array<keyof Config>
export type DepsBuildConfigKey = typeof DEPS_BUILD_CONFIG_KEYS[number]

View File

@@ -28,12 +28,14 @@ export type OptionsFromRootManifest = {
patchedDependencies?: Record<string, string>
peerDependencyRules?: PeerDependencyRules
supportedArchitectures?: SupportedArchitectures
allowBuilds?: Record<string, boolean | string>
} & Pick<PnpmSettings, 'configDependencies' | 'auditConfig' | 'executionEnv' | 'updateConfig'>
export function getOptionsFromRootManifest (manifestDir: string, manifest: ProjectManifest): OptionsFromRootManifest {
const settings: OptionsFromRootManifest = getOptionsFromPnpmSettings(manifestDir, {
...pick([
'allowNonAppliedPatches',
'allowBuilds',
'allowUnusedPatches',
'allowedDeprecatedVersions',
'auditConfig',
@@ -67,7 +69,7 @@ export function getOptionsFromRootManifest (manifestDir: string, manifest: Proje
}
export function getOptionsFromPnpmSettings (manifestDir: string, pnpmSettings: PnpmSettings, manifest?: ProjectManifest): OptionsFromRootManifest {
const renamedKeys = ['allowNonAppliedPatches'] as const satisfies Array<keyof PnpmSettings>
const renamedKeys = ['allowNonAppliedPatches', 'allowBuilds'] as const satisfies Array<keyof PnpmSettings>
const settings: OptionsFromRootManifest = omit(renamedKeys, replaceEnvInSettings(pnpmSettings))
if (settings.overrides) {
if (Object.keys(settings.overrides).length === 0) {
@@ -93,6 +95,22 @@ export function getOptionsFromPnpmSettings (manifestDir: string, pnpmSettings: P
if (pnpmSettings.ignorePatchFailures != null) {
settings.ignorePatchFailures = pnpmSettings.ignorePatchFailures
}
if (pnpmSettings.allowBuilds) {
settings.onlyBuiltDependencies ??= []
settings.ignoredBuiltDependencies ??= []
for (const [packagePattern, build] of Object.entries(pnpmSettings.allowBuilds)) {
switch (build) {
case true:
settings.onlyBuiltDependencies.push(packagePattern)
break
case false:
settings.ignoredBuiltDependencies.push(packagePattern)
break
}
}
}
return settings
}

View File

@@ -174,3 +174,19 @@ test('getOptionsFromPnpmSettings() replaces env variables in settings', () => {
} as any) as any // eslint-disable-line
expect(options.foo).toEqual('bar')
})
test('getOptionsFromRootManifest() converts allowBuilds', () => {
const options = getOptionsFromRootManifest(process.cwd(), {
pnpm: {
allowBuilds: {
foo: true,
bar: false,
qar: 'warn',
},
},
})
expect(options).toStrictEqual({
onlyBuiltDependencies: ['foo'],
ignoredBuiltDependencies: ['bar'],
})
})

View File

@@ -153,9 +153,10 @@ export interface AuditConfig {
export interface PnpmSettings {
configDependencies?: ConfigDependencies
neverBuiltDependencies?: string[]
onlyBuiltDependencies?: string[]
onlyBuiltDependenciesFile?: string
neverBuiltDependencies?: string[] // deprecated
onlyBuiltDependencies?: string[] // deprecated
onlyBuiltDependenciesFile?: string // deprecated
allowBuilds?: Record<string, boolean | string>
ignoredBuiltDependencies?: string[]
overrides?: Record<string, string>
packageExtensions?: Record<string, PackageExtension>

View File

@@ -37,7 +37,32 @@ export async function updateWorkspaceManifest (dir: string, opts: {
shouldBeUpdated = removePackagesFromWorkspaceCatalog(manifest, opts.allProjects ?? []) || shouldBeUpdated
}
for (const [key, value] of Object.entries(opts.updatedFields ?? {})) {
// If the current manifest has allowBuilds, convert old fields to allowBuilds format
const updatedFields = { ...opts.updatedFields }
if (manifest.allowBuilds != null && (updatedFields.onlyBuiltDependencies != null || updatedFields.ignoredBuiltDependencies != null)) {
const allowBuilds: Record<string, boolean | string> = { ...manifest.allowBuilds }
// Convert onlyBuiltDependencies to allowBuilds with true values
if (updatedFields.onlyBuiltDependencies != null) {
for (const pattern of updatedFields.onlyBuiltDependencies) {
allowBuilds[pattern] = true
}
}
// Convert ignoredBuiltDependencies to allowBuilds with false values
if (updatedFields.ignoredBuiltDependencies != null) {
for (const pattern of updatedFields.ignoredBuiltDependencies) {
allowBuilds[pattern] = false
}
}
// Update allowBuilds instead of the old fields
updatedFields.allowBuilds = allowBuilds
delete updatedFields.onlyBuiltDependencies
delete updatedFields.ignoredBuiltDependencies
}
for (const [key, value] of Object.entries(updatedFields)) {
if (!equals(manifest[key as keyof WorkspaceManifest], value)) {
shouldBeUpdated = true
if (value == null) {

View File

@@ -42,3 +42,20 @@ test('updateWorkspaceManifest updates an existing setting', async () => {
overrides: { bar: '3' },
})
})
test('updateWorkspaceManifest updates allowBuilds', async () => {
const dir = tempDir(false)
const filePath = path.join(dir, WORKSPACE_MANIFEST_FILENAME)
writeYamlFile(filePath, { packages: ['*'], allowBuilds: { qar: 'warn' } })
await updateWorkspaceManifest(dir, {
updatedFields: { onlyBuiltDependencies: ['foo'], ignoredBuiltDependencies: ['bar'] },
})
expect(readYamlFile(filePath)).toStrictEqual({
packages: ['*'],
allowBuilds: {
bar: false,
foo: true,
qar: 'warn',
},
})
})