feat(approve-builds): positional args, !pkg deny syntax, and auto-populate allowBuilds (#11030)

### `pnpm approve-builds` positional arguments
- `pnpm approve-builds foo` — approves `foo`, leaves everything else untouched
- `pnpm approve-builds !bar` — denies `bar`, leaves everything else untouched
- `pnpm approve-builds foo !bar` — approves `foo`, denies `bar`
- Only mentioned packages are modified; unmentioned packages remain pending
- `--all` cannot be combined with positional arguments
- Contradictory arguments (`pkg !pkg`) are rejected

### Auto-populate `allowBuilds` during install
- When `pnpm install` encounters packages with build scripts that aren't yet in `allowBuilds`, they are automatically written to `pnpm-workspace.yaml` with a `'set this to true or false'` placeholder
- Users can then edit the config directly instead of running `approve-builds`
- The placeholder behaves like a missing entry: builds are skipped and `strictDepBuilds` still fails
- Existing `allowBuilds` entries are preserved (only new packages get placeholders)
This commit is contained in:
Zoltan Kochan
2026-03-20 14:58:56 +01:00
committed by GitHub
parent f7960244ea
commit 996284f8cc
11 changed files with 418 additions and 27 deletions

View File

@@ -0,0 +1,11 @@
---
"@pnpm/building.after-install": patch
"@pnpm/building.commands": minor
"@pnpm/installing.deps-installer": patch
"@pnpm/installing.commands": minor
"pnpm": minor
---
Allow `pnpm approve-builds` to receive positional arguments for approving or denying packages without the interactive prompt. Prefix a package name with `!` to deny it. Only mentioned packages are affected; the rest are left untouched.
During install, packages with ignored builds that are not yet listed in `allowBuilds` are automatically added with a placeholder value. This makes them visible in `pnpm-workspace.yaml` so users can manually change them to `true` or `false` without running `pnpm approve-builds`.

View File

@@ -146,7 +146,7 @@ export async function buildSelectedPkgs (
hoistedDependencies: ctx.hoistedDependencies,
hoistPattern: ctx.hoistPattern,
included: ctx.include,
ignoredBuilds: ignoredPkgs,
ignoredBuilds: mergeIgnoredBuilds(ctx.modulesFile?.ignoredBuilds, ignoredPkgs, pkgs as DepPath[]),
layoutVersion: LAYOUT_VERSION,
packageManager: `${opts.packageManager.name}@${opts.packageManager.version}`,
pendingBuilds: ctx.pendingBuilds,
@@ -480,3 +480,28 @@ function binDirsInAllParentDirs (pkgRoot: string, lockfileDir: string): string[]
binDirs.push(path.join(lockfileDir, 'node_modules/.bin'))
return binDirs
}
/**
* Merge new ignoredBuilds from a selective rebuild with existing ones.
* Keeps existing entries for packages that weren't part of this rebuild.
*/
function mergeIgnoredBuilds (
existing: IgnoredBuilds | undefined,
newIgnored: IgnoredBuilds,
rebuiltPkgs: DepPath[]
): IgnoredBuilds | undefined {
if (!existing?.size && !newIgnored.size) return undefined
const rebuiltSet = new Set<DepPath>(rebuiltPkgs)
const merged = new Set<DepPath>()
if (existing) {
for (const depPath of existing) {
if (!rebuiltSet.has(depPath)) {
merged.add(depPath)
}
}
}
for (const depPath of newIgnored) {
merged.add(depPath)
}
return merged.size ? merged : undefined
}

View File

@@ -1,6 +1,7 @@
import { rebuild, type RebuildCommandOpts } from '@pnpm/building.commands'
import type { Config } from '@pnpm/config.reader'
import { writeSettings } from '@pnpm/config.writer'
import { parse } from '@pnpm/deps.path'
import { PnpmError } from '@pnpm/error'
import { type StrictModules, writeModulesManifest } from '@pnpm/installing.modules-yaml'
import { globalInfo } from '@pnpm/logger'
@@ -18,7 +19,10 @@ export const commandNames = ['approve-builds']
export function help (): string {
return renderHelp({
description: 'Approve dependencies for running scripts during installation',
usages: [],
usages: [
'pnpm approve-builds',
'pnpm approve-builds [<pkg> ...] [!<pkg> ...]',
],
descriptionLists: [
{
title: 'Options',
@@ -45,7 +49,7 @@ export function rcOptionsTypes (): Record<string, unknown> {
return {}
}
export async function handler (opts: ApproveBuildsCommandOpts & RebuildCommandOpts): Promise<void> {
export async function handler (opts: ApproveBuildsCommandOpts & RebuildCommandOpts, params: string[] = []): Promise<void> {
if (opts.global) {
throw new PnpmError(
'APPROVE_BUILDS_NOT_SUPPORTED_WITH_GLOBAL',
@@ -56,6 +60,12 @@ export async function handler (opts: ApproveBuildsCommandOpts & RebuildCommandOp
}
)
}
if (opts.all && params.length) {
throw new PnpmError(
'APPROVE_BUILDS_ALL_WITH_ARGS',
'Cannot use --all with positional arguments'
)
}
const {
automaticallyIgnoredBuilds,
modulesDir,
@@ -65,8 +75,36 @@ export async function handler (opts: ApproveBuildsCommandOpts & RebuildCommandOp
globalInfo('There are no packages awaiting approval')
return
}
const denied: string[] = []
const approved: string[] = []
const unknown: string[] = []
for (const p of params) {
const name = p.startsWith('!') ? p.slice(1) : p
if (!automaticallyIgnoredBuilds.includes(name)) {
unknown.push(name)
} else if (p.startsWith('!')) {
denied.push(name)
} else {
approved.push(name)
}
}
if (unknown.length) {
throw new PnpmError(
'APPROVE_BUILDS_UNKNOWN_PACKAGES',
`The following packages are not awaiting approval: ${unknown.join(', ')}`
)
}
const contradictions = approved.filter((p) => denied.includes(p))
if (contradictions.length) {
throw new PnpmError(
'APPROVE_BUILDS_CONTRADICTING_ARGS',
`The following packages are both approved and denied: ${contradictions.join(', ')}`
)
}
let buildPackages: string[] = []
if (opts.all) {
if (params.length) {
buildPackages = sortUniqueStrings([...approved])
} else if (opts.all) {
buildPackages = sortUniqueStrings([...automaticallyIgnoredBuilds])
} else {
const { result } = await enquirer.prompt({
@@ -107,19 +145,24 @@ export async function handler (opts: ApproveBuildsCommandOpts & RebuildCommandOp
} as any) as any // eslint-disable-line @typescript-eslint/no-explicit-any
buildPackages = result.map(({ value }: { value: string }) => value)
}
const ignoredPackages = automaticallyIgnoredBuilds.filter((automaticallyIgnoredBuild) => !buildPackages.includes(automaticallyIgnoredBuild))
const allowBuilds: Record<string, boolean | string> = { ...opts.allowBuilds }
if (ignoredPackages.length) {
if (params.length) {
for (const pkg of approved) {
allowBuilds[pkg] = true
}
for (const pkg of denied) {
allowBuilds[pkg] = false
}
} else {
const ignoredPackages = automaticallyIgnoredBuilds.filter((automaticallyIgnoredBuild) => !buildPackages.includes(automaticallyIgnoredBuild))
for (const pkg of ignoredPackages) {
allowBuilds[pkg] = false
}
}
if (buildPackages.length) {
for (const pkg of buildPackages) {
allowBuilds[pkg] = true
}
}
if (!opts.all) {
if (!opts.all && !params.length) {
if (buildPackages.length) {
const confirmed = await enquirer.prompt<{ build: boolean }>({
type: 'confirm',
@@ -140,14 +183,28 @@ Do you approve?`,
workspaceDir: opts.workspaceDir ?? opts.rootProjectManifestDir,
updatedSettings: { allowBuilds },
})
if (modulesManifest?.ignoredBuilds) {
if (params.length) {
const decided = new Set([...approved, ...denied])
for (const depPath of Array.from(modulesManifest.ignoredBuilds)) {
const name = parse(depPath).name ?? depPath
if (decided.has(name)) {
modulesManifest.ignoredBuilds.delete(depPath)
}
}
if (!modulesManifest.ignoredBuilds.size) {
delete modulesManifest.ignoredBuilds
}
} else {
delete modulesManifest.ignoredBuilds
}
await writeModulesManifest(modulesDir, modulesManifest as StrictModules)
}
if (buildPackages.length) {
return rebuild.handler({
...opts,
allowBuilds,
}, buildPackages)
} else if (modulesManifest) {
delete modulesManifest.ignoredBuilds
await writeModulesManifest(modulesDir, modulesManifest as StrictModules)
}
}

View File

@@ -218,6 +218,198 @@ test('approve all builds with --all flag', async () => {
expect(fs.existsSync('node_modules/@pnpm.e2e/install-script-example/generated-by-install.js')).toBeTruthy()
})
test('approve builds via positional arguments', async () => {
prepare({
dependencies: {
'@pnpm.e2e/pre-and-postinstall-scripts-example': '1.0.0',
'@pnpm.e2e/install-script-example': '*',
},
})
await execPnpmInstall()
const config = await getApproveBuildsConfig()
prompt.mockClear()
await approveBuilds.handler(config, ['@pnpm.e2e/pre-and-postinstall-scripts-example'])
expect(prompt).not.toHaveBeenCalled()
const workspaceManifest = readYamlFileSync<any>(path.resolve('pnpm-workspace.yaml')) // eslint-disable-line
expect(workspaceManifest.allowBuilds).toStrictEqual({
'@pnpm.e2e/pre-and-postinstall-scripts-example': true,
})
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-preinstall.js')).toBeTruthy()
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-postinstall.js')).toBeTruthy()
expect(fs.existsSync('node_modules/@pnpm.e2e/install-script-example/generated-by-install.js')).toBeFalsy()
// Unmentioned package should still be in ignoredBuilds after rebuild
const modulesManifestAfter = await readModulesManifest(path.resolve('node_modules'))
expect(modulesManifestAfter?.ignoredBuilds).toBeDefined()
})
test('deny builds via !pkg positional arguments', async () => {
prepare({
dependencies: {
'@pnpm.e2e/pre-and-postinstall-scripts-example': '1.0.0',
'@pnpm.e2e/install-script-example': '*',
},
})
await execPnpmInstall()
const config = await getApproveBuildsConfig()
prompt.mockClear()
await approveBuilds.handler(config, [
'@pnpm.e2e/pre-and-postinstall-scripts-example',
'!@pnpm.e2e/install-script-example',
])
expect(prompt).not.toHaveBeenCalled()
const workspaceManifest = readYamlFileSync<any>(path.resolve('pnpm-workspace.yaml')) // eslint-disable-line
expect(workspaceManifest.allowBuilds).toStrictEqual({
'@pnpm.e2e/install-script-example': false,
'@pnpm.e2e/pre-and-postinstall-scripts-example': true,
})
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-preinstall.js')).toBeTruthy()
expect(fs.existsSync('node_modules/@pnpm.e2e/install-script-example/generated-by-install.js')).toBeFalsy()
})
test('deny-only via !pkg keeps other builds pending', async () => {
prepare({
dependencies: {
'@pnpm.e2e/pre-and-postinstall-scripts-example': '1.0.0',
'@pnpm.e2e/install-script-example': '*',
},
})
await execPnpmInstall()
const config = await getApproveBuildsConfig()
prompt.mockClear()
await approveBuilds.handler(config, [
'!@pnpm.e2e/install-script-example',
])
expect(prompt).not.toHaveBeenCalled()
const workspaceManifest = readYamlFileSync<any>(path.resolve('pnpm-workspace.yaml')) // eslint-disable-line
expect(workspaceManifest.allowBuilds).toStrictEqual({
'@pnpm.e2e/install-script-example': false,
})
const modulesManifestAfter = await readModulesManifest(path.resolve('node_modules'))
const ignoredNames = Array.from(modulesManifestAfter?.ignoredBuilds ?? []).map(String)
// The denied package should be removed from ignoredBuilds
expect(ignoredNames.some((dp) => dp.includes('install-script-example'))).toBe(false)
// The other package should still be pending
expect(ignoredNames.some((dp) => dp.includes('pre-and-postinstall-scripts-example'))).toBe(true)
})
test('positional arguments with unknown package throws error', async () => {
prepare({
dependencies: {
'@pnpm.e2e/pre-and-postinstall-scripts-example': '1.0.0',
},
})
await execPnpmInstall()
const config = await getApproveBuildsConfig()
await expect(
approveBuilds.handler(config, ['@pnpm.e2e/nonexistent-package'])
).rejects.toThrow('not awaiting approval')
})
test('!pkg with unknown package throws error', async () => {
prepare({
dependencies: {
'@pnpm.e2e/pre-and-postinstall-scripts-example': '1.0.0',
},
})
await execPnpmInstall()
const config = await getApproveBuildsConfig()
await expect(
approveBuilds.handler(config, ['!@pnpm.e2e/nonexistent-package'])
).rejects.toThrow('not awaiting approval')
})
test('contradictory arguments throw error', async () => {
prepare({
dependencies: {
'@pnpm.e2e/pre-and-postinstall-scripts-example': '1.0.0',
},
})
await execPnpmInstall()
const config = await getApproveBuildsConfig()
await expect(
approveBuilds.handler(config, [
'@pnpm.e2e/pre-and-postinstall-scripts-example',
'!@pnpm.e2e/pre-and-postinstall-scripts-example',
])
).rejects.toThrow('both approved and denied')
})
test('--all with positional arguments throws error', async () => {
prepare({
dependencies: {
'@pnpm.e2e/pre-and-postinstall-scripts-example': '1.0.0',
},
})
await execPnpmInstall()
const config = await getApproveBuildsConfig()
await expect(
approveBuilds.handler({ ...config, all: true }, ['@pnpm.e2e/pre-and-postinstall-scripts-example'])
).rejects.toThrow('Cannot use --all with positional arguments')
})
test('positional args preserve existing allowBuilds entries', async () => {
const temp = tempDir()
prepare({
dependencies: {
'@pnpm.e2e/pre-and-postinstall-scripts-example': '1.0.0',
'@pnpm.e2e/install-script-example': '*',
},
}, {
tempDir: temp,
})
const workspaceManifestFile = path.join(temp, 'pnpm-workspace.yaml')
writeYamlFileSync(workspaceManifestFile, {
packages: ['packages/*'],
allowBuilds: {
'@pnpm.e2e/existing-package': true,
},
})
await execPnpmInstall()
const config = await getApproveBuildsConfig()
await approveBuilds.handler({
...config,
workspaceDir: temp,
rootProjectManifestDir: temp,
allowBuilds: {
'@pnpm.e2e/existing-package': true,
},
}, ['@pnpm.e2e/pre-and-postinstall-scripts-example'])
const manifest = readYamlFileSync<any>(workspaceManifestFile) // eslint-disable-line
expect(manifest.allowBuilds['@pnpm.e2e/existing-package']).toBe(true)
expect(manifest.allowBuilds['@pnpm.e2e/pre-and-postinstall-scripts-example']).toBe(true)
// install-script-example should NOT be touched
expect(manifest.allowBuilds['@pnpm.e2e/install-script-example']).toBeUndefined()
})
test('should retain existing allowBuilds entries when approving builds', async () => {
const temp = tempDir()

View File

@@ -45,6 +45,7 @@
"@pnpm/config.writer": "workspace:*",
"@pnpm/constants": "workspace:*",
"@pnpm/deps.inspection.outdated": "workspace:*",
"@pnpm/deps.path": "workspace:*",
"@pnpm/deps.status": "workspace:*",
"@pnpm/error": "workspace:*",
"@pnpm/fs.graceful-fs": "workspace:*",
@@ -64,6 +65,7 @@
"@pnpm/store.connection-manager": "workspace:*",
"@pnpm/store.controller": "workspace:*",
"@pnpm/types": "workspace:*",
"@pnpm/util.lex-comparator": "catalog:",
"@pnpm/workspace.project-manifest-reader": "workspace:*",
"@pnpm/workspace.project-manifest-writer": "workspace:*",
"@pnpm/workspace.projects-filter": "workspace:*",

View File

@@ -0,0 +1,51 @@
import { writeSettings } from '@pnpm/config.writer'
import { parse } from '@pnpm/deps.path'
import {
IgnoredBuildsError,
} from '@pnpm/installing.deps-installer'
import type { IgnoredBuilds } from '@pnpm/types'
import { lexCompare } from '@pnpm/util.lex-comparator'
export interface HandleIgnoredBuildsOpts {
allowBuilds?: Record<string, boolean | string>
rootProjectManifestDir?: string
workspaceDir?: string
strictDepBuilds?: boolean
}
export async function handleIgnoredBuilds (
opts: HandleIgnoredBuildsOpts,
ignoredBuilds: IgnoredBuilds | undefined
): Promise<void> {
if (!ignoredBuilds?.size) return
await writeIgnoredBuildsToAllowBuilds(opts, ignoredBuilds)
if (opts.strictDepBuilds) {
throw new IgnoredBuildsError(ignoredBuilds)
}
}
async function writeIgnoredBuildsToAllowBuilds (
opts: Pick<HandleIgnoredBuildsOpts, 'allowBuilds' | 'rootProjectManifestDir' | 'workspaceDir'>,
ignoredBuilds: IgnoredBuilds
): Promise<void> {
const packageNames = packageNamesFromIgnoredBuilds(ignoredBuilds)
const newEntries: Record<string, string> = {}
for (const name of packageNames) {
if (opts.allowBuilds?.[name] == null) {
newEntries[name] = 'set this to true or false'
}
}
if (Object.keys(newEntries).length && opts.rootProjectManifestDir) {
await writeSettings({
rootProjectManifestDir: opts.rootProjectManifestDir,
workspaceDir: opts.workspaceDir ?? opts.rootProjectManifestDir,
updatedSettings: {
allowBuilds: { ...opts.allowBuilds, ...newEntries },
},
})
}
}
function packageNamesFromIgnoredBuilds (ignoredBuilds: IgnoredBuilds): string[] {
return Array.from(new Set(Array.from(ignoredBuilds).map((dp) => parse(dp).name ?? dp))).sort(lexCompare)
}

View File

@@ -10,7 +10,6 @@ import { checkDepsStatus } from '@pnpm/deps.status'
import { PnpmError } from '@pnpm/error'
import { arrayOfWorkspacePackagesToMap } from '@pnpm/installing.context'
import {
IgnoredBuildsError,
install,
mutateModulesInSingleProject,
type MutateModulesOptions,
@@ -39,6 +38,7 @@ import { updateWorkspaceManifest } from '@pnpm/workspace.workspace-manifest-writ
import { getPinnedVersion } from './getPinnedVersion.js'
import { getSaveType } from './getSaveType.js'
import { handleIgnoredBuilds } from './handleIgnoredBuilds.js'
import {
type CommandFullName,
createMatcher,
@@ -355,9 +355,7 @@ when running add/update with the --workspace option')
configDependencies: opts.configDependencies,
})
}
if (opts.strictDepBuilds && ignoredBuilds?.size) {
throw new IgnoredBuildsError(ignoredBuilds)
}
await handleIgnoredBuilds(opts, ignoredBuilds)
return
}
@@ -376,9 +374,7 @@ when running add/update with the --workspace option')
}),
])
}
if (opts.strictDepBuilds && ignoredBuilds?.size) {
throw new IgnoredBuildsError(ignoredBuilds)
}
await handleIgnoredBuilds(opts, ignoredBuilds)
if (opts.linkWorkspacePackages && opts.workspaceDir) {
const { selectedProjectsGraph } = await filterProjectsBySelectorObjects(allProjects, [

View File

@@ -20,7 +20,6 @@ import { requireHooks } from '@pnpm/hooks.pnpmfile'
import { arrayOfWorkspacePackagesToMap } from '@pnpm/installing.context'
import {
addDependenciesToPackage,
IgnoredBuildsError,
install,
type InstallOptions,
type MutatedProject,
@@ -35,6 +34,7 @@ import type { PreferredVersions } from '@pnpm/resolving.resolver-base'
import { createStoreController, type CreateStoreControllerOptions } from '@pnpm/store.connection-manager'
import type { StoreController } from '@pnpm/store.controller'
import type {
DepPath,
IgnoredBuilds,
IncludedDependencies,
PackageManifest,
@@ -52,6 +52,7 @@ import pLimit from 'p-limit'
import { getPinnedVersion } from './getPinnedVersion.js'
import { getSaveType } from './getSaveType.js'
import { handleIgnoredBuilds } from './handleIgnoredBuilds.js'
import { createWorkspaceSpecs, updateToWorkspacePackagesFromManifest } from './updateWorkspaceDependencies.js'
export type RecursiveOptions = CreateStoreControllerOptions & Pick<Config,
@@ -306,9 +307,7 @@ export async function recursive (
}))
await Promise.all(promises)
}
if (opts.strictDepBuilds && ignoredBuilds?.size) {
throw new IgnoredBuildsError(ignoredBuilds)
}
await handleIgnoredBuilds(opts, ignoredBuilds)
return true
}
@@ -316,6 +315,7 @@ export async function recursive (
let updatedCatalogs: Catalogs | undefined
const allIgnoredBuilds = new Set<DepPath>()
const limitInstallation = pLimit(getWorkspaceConcurrency(opts.workspaceConcurrency))
await Promise.all(pkgPaths.map(async (rootDir) =>
limitInstallation(async () => {
@@ -425,8 +425,10 @@ export async function recursive (
Object.assign(updatedCatalogs, newCatalogsAddition)
}
}
if (opts.strictDepBuilds && ignoredBuilds?.size) {
throw new IgnoredBuildsError(ignoredBuilds)
if (ignoredBuilds?.size) {
for (const depPath of ignoredBuilds) {
allIgnoredBuilds.add(depPath)
}
}
result[rootDir].status = 'passed'
} catch (err: any) { // eslint-disable-line
@@ -447,7 +449,7 @@ export async function recursive (
}
})
))
await handleIgnoredBuilds(opts, allIgnoredBuilds.size ? allIgnoredBuilds : undefined)
await updateWorkspaceManifest(opts.workspaceDir, {
updatedCatalogs,
cleanupUnusedCatalogs: opts.cleanupUnusedCatalogs,

View File

@@ -66,6 +66,9 @@
{
"path": "../../deps/inspection/outdated"
},
{
"path": "../../deps/path"
},
{
"path": "../../deps/status"
},

6
pnpm-lock.yaml generated
View File

@@ -4778,6 +4778,9 @@ importers:
'@pnpm/deps.inspection.outdated':
specifier: workspace:*
version: link:../../deps/inspection/outdated
'@pnpm/deps.path':
specifier: workspace:*
version: link:../../deps/path
'@pnpm/deps.status':
specifier: workspace:*
version: link:../../deps/status
@@ -4835,6 +4838,9 @@ importers:
'@pnpm/types':
specifier: workspace:*
version: link:../../core/types
'@pnpm/util.lex-comparator':
specifier: 'catalog:'
version: 3.0.2
'@pnpm/workspace.project-manifest-reader':
specifier: workspace:*
version: link:../../workspace/project-manifest-reader

View File

@@ -154,7 +154,10 @@ test('selectively allow scripts in some dependencies by --allow-build flag', asy
expect(fs.existsSync('node_modules/@pnpm.e2e/install-script-example/generated-by-install.js')).toBeTruthy()
const modulesManifest = await readWorkspaceManifest(project.dir())
expect(modulesManifest?.allowBuilds).toStrictEqual({ '@pnpm.e2e/install-script-example': true })
expect(modulesManifest?.allowBuilds).toStrictEqual({
'@pnpm.e2e/install-script-example': true,
'@pnpm.e2e/pre-and-postinstall-scripts-example': 'set this to true or false',
})
})
test('--allow-build flag should specify the package', async () => {
@@ -253,6 +256,49 @@ test('the list of ignored builds is preserved after a repeat install', async ()
])
})
test('ignored builds are auto-populated as placeholders in allowBuilds', async () => {
prepare({})
execPnpmSync(['add', '@pnpm.e2e/pre-and-postinstall-scripts-example@1.0.0'])
const manifest = await readWorkspaceManifest(process.cwd())
expect(manifest?.allowBuilds?.['@pnpm.e2e/pre-and-postinstall-scripts-example']).toBe('set this to true or false')
})
test('auto-populated placeholders are merged with existing allowBuilds', async () => {
prepare({})
writeYamlFileSync('pnpm-workspace.yaml', {
allowBuilds: {
'@pnpm.e2e/install-script-example': true,
},
})
execPnpmSync(['add', '@pnpm.e2e/pre-and-postinstall-scripts-example@1.0.0'])
const manifest = await readWorkspaceManifest(process.cwd())
expect(manifest?.allowBuilds?.['@pnpm.e2e/install-script-example']).toBe(true)
expect(manifest?.allowBuilds?.['@pnpm.e2e/pre-and-postinstall-scripts-example']).toBe('set this to true or false')
})
test('selective rebuild preserves ignoredBuilds for packages not being rebuilt', async () => {
const project = prepare({})
writeYamlFileSync('pnpm-workspace.yaml', {
allowBuilds: {
'@pnpm.e2e/pre-and-postinstall-scripts-example': true,
},
})
execPnpmSync(['add', '@pnpm.e2e/pre-and-postinstall-scripts-example@1.0.0', '@pnpm.e2e/install-script-example'])
// install-script-example should be in ignoredBuilds
const beforeRebuild = project.readModulesManifest()
expect(beforeRebuild!.ignoredBuilds).toBeDefined()
// Selectively rebuild only the approved package
execPnpmSync(['rebuild', '@pnpm.e2e/pre-and-postinstall-scripts-example'])
// install-script-example should still be in ignoredBuilds after selective rebuild
const afterRebuild = project.readModulesManifest()
expect(afterRebuild!.ignoredBuilds).toBeDefined()
})
test('git dependencies with preparation scripts should be installed when dangerouslyAllowAllBuilds is true', async () => {
prepare({})
writeYamlFileSync('pnpm-workspace.yaml', { dangerouslyAllowAllBuilds: true })