From 76718b32ad333bdda2602cd8728da0b5bfa195c0 Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Sat, 13 Dec 2025 22:14:27 +0100 Subject: [PATCH] feat: create a new field for allowing/disallowing builds (#10311) ref #10235 --- .changeset/cold-weeks-work.md | 27 +++++++++++++++++++ config/config/src/Config.ts | 1 + config/config/src/dependencyBuildOptions.ts | 1 + .../config/src/getOptionsFromRootManifest.ts | 20 +++++++++++++- .../test/getOptionsFromRootManifest.test.ts | 16 +++++++++++ packages/types/src/package.ts | 7 ++--- workspace/manifest-writer/src/index.ts | 27 ++++++++++++++++++- .../test/updateWorkspaceManifest.test.ts | 17 ++++++++++++ 8 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 .changeset/cold-weeks-work.md diff --git a/.changeset/cold-weeks-work.md b/.changeset/cold-weeks-work.md new file mode 100644 index 0000000000..7e9228b473 --- /dev/null +++ b/.changeset/cold-weeks-work.md @@ -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) diff --git a/config/config/src/Config.ts b/config/config/src/Config.ts index 76ce99cb59..bec78364c2 100644 --- a/config/config/src/Config.ts +++ b/config/config/src/Config.ts @@ -185,6 +185,7 @@ export interface Config extends OptionsFromRootManifest { gitShallowHosts?: string[] legacyDirFiltering?: boolean onlyBuiltDependencies?: string[] + allowBuilds?: Record dedupePeerDependents?: boolean patchesDir?: string ignoreWorkspaceCycles?: boolean diff --git a/config/config/src/dependencyBuildOptions.ts b/config/config/src/dependencyBuildOptions.ts index 7e39a305f6..19e3c207c2 100644 --- a/config/config/src/dependencyBuildOptions.ts +++ b/config/config/src/dependencyBuildOptions.ts @@ -5,6 +5,7 @@ export const DEPS_BUILD_CONFIG_KEYS = [ 'onlyBuiltDependencies', 'onlyBuiltDependenciesFile', 'neverBuiltDependencies', + 'allowBuilds', ] as const satisfies Array export type DepsBuildConfigKey = typeof DEPS_BUILD_CONFIG_KEYS[number] diff --git a/config/config/src/getOptionsFromRootManifest.ts b/config/config/src/getOptionsFromRootManifest.ts index a6b629432e..19dc597a86 100644 --- a/config/config/src/getOptionsFromRootManifest.ts +++ b/config/config/src/getOptionsFromRootManifest.ts @@ -26,12 +26,14 @@ export type OptionsFromRootManifest = { patchedDependencies?: Record peerDependencyRules?: PeerDependencyRules supportedArchitectures?: SupportedArchitectures + allowBuilds?: Record } & Pick export function getOptionsFromRootManifest (manifestDir: string, manifest: ProjectManifest): OptionsFromRootManifest { const settings: OptionsFromRootManifest = getOptionsFromPnpmSettings(manifestDir, { ...pick([ 'allowNonAppliedPatches', + 'allowBuilds', 'allowUnusedPatches', 'allowedDeprecatedVersions', 'auditConfig', @@ -65,7 +67,7 @@ export function getOptionsFromRootManifest (manifestDir: string, manifest: Proje } export function getOptionsFromPnpmSettings (manifestDir: string | undefined, pnpmSettings: PnpmSettings, manifest?: ProjectManifest): OptionsFromRootManifest { - const renamedKeys = ['allowNonAppliedPatches'] as const satisfies Array + const renamedKeys = ['allowNonAppliedPatches', 'allowBuilds'] as const satisfies Array const settings: OptionsFromRootManifest = omit(renamedKeys, replaceEnvInSettings(pnpmSettings)) if (settings.overrides) { if (Object.keys(settings.overrides).length === 0) { @@ -91,6 +93,22 @@ export function getOptionsFromPnpmSettings (manifestDir: string | undefined, pnp 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 } diff --git a/config/config/test/getOptionsFromRootManifest.test.ts b/config/config/test/getOptionsFromRootManifest.test.ts index 4d7ab1a45e..10e082206a 100644 --- a/config/config/test/getOptionsFromRootManifest.test.ts +++ b/config/config/test/getOptionsFromRootManifest.test.ts @@ -174,3 +174,19 @@ test('getOptionsFromPnpmSettings() replaces env variables in settings', () => { } as any) as any // eslint-disable-line expect(options.foo).toBe('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'], + }) +}) diff --git a/packages/types/src/package.ts b/packages/types/src/package.ts index f8f5508241..3980dd5468 100644 --- a/packages/types/src/package.ts +++ b/packages/types/src/package.ts @@ -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 ignoredBuiltDependencies?: string[] overrides?: Record packageExtensions?: Record diff --git a/workspace/manifest-writer/src/index.ts b/workspace/manifest-writer/src/index.ts index 44e8e63f8d..df449c99c7 100644 --- a/workspace/manifest-writer/src/index.ts +++ b/workspace/manifest-writer/src/index.ts @@ -45,7 +45,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 = { ...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) { diff --git a/workspace/manifest-writer/test/updateWorkspaceManifest.test.ts b/workspace/manifest-writer/test/updateWorkspaceManifest.test.ts index 6cc396c244..31a39995cf 100644 --- a/workspace/manifest-writer/test/updateWorkspaceManifest.test.ts +++ b/workspace/manifest-writer/test/updateWorkspaceManifest.test.ts @@ -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', + }, + }) +})