mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-27 18:46:18 -04:00
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:
11
.changeset/approve-builds-positional-args.md
Normal file
11
.changeset/approve-builds-positional-args.md
Normal 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`.
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
51
installing/commands/src/handleIgnoredBuilds.ts
Normal file
51
installing/commands/src/handleIgnoredBuilds.ts
Normal 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)
|
||||
}
|
||||
@@ -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, [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -66,6 +66,9 @@
|
||||
{
|
||||
"path": "../../deps/inspection/outdated"
|
||||
},
|
||||
{
|
||||
"path": "../../deps/path"
|
||||
},
|
||||
{
|
||||
"path": "../../deps/status"
|
||||
},
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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 })
|
||||
|
||||
Reference in New Issue
Block a user