fix: reporting ignored dependency builds (#10276)

This commit is contained in:
Zoltan Kochan
2025-12-06 16:32:19 +01:00
parent 7c15c93c26
commit 9b05bdd7e1
25 changed files with 187 additions and 78 deletions

View File

@@ -0,0 +1,10 @@
---
"@pnpm/plugin-commands-rebuild": major
"@pnpm/modules-yaml": major
"@pnpm/headless": major
"@pnpm/build-modules": major
"@pnpm/core": major
"@pnpm/exec.build-commands": major
---
`ignoreBuilds` is now a set of DepPath.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/types": minor
---
Add type for IgnoredBuilds.

View File

@@ -0,0 +1,5 @@
---
"pnpm": patch
---
Improved reporting of ignored dependency scripts [#10276](https://github.com/pnpm/pnpm/pull/10276).

View File

@@ -34,6 +34,7 @@
"dependencies": {
"@pnpm/config": "workspace:*",
"@pnpm/config.config-writer": "workspace:*",
"@pnpm/dependency-path": "workspace:*",
"@pnpm/modules-yaml": "workspace:*",
"@pnpm/plugin-commands-rebuild": "workspace:*",
"@pnpm/prepare-temp-dir": "workspace:*",

View File

@@ -1,4 +1,5 @@
import path from 'path'
import { parse } from '@pnpm/dependency-path'
import { type Modules, readModulesManifest } from '@pnpm/modules-yaml'
import { type IgnoredBuildsCommandOpts } from './ignoredBuilds.js'
@@ -11,8 +12,18 @@ export interface GetAutomaticallyIgnoredBuildsResult {
export async function getAutomaticallyIgnoredBuilds (opts: IgnoredBuildsCommandOpts): Promise<GetAutomaticallyIgnoredBuildsResult> {
const modulesDir = getModulesDir(opts)
const modulesManifest = await readModulesManifest(modulesDir)
let automaticallyIgnoredBuilds: null | string[]
if (modulesManifest?.ignoredBuilds) {
const ignoredPkgNames = new Set<string>()
for (const depPath of modulesManifest?.ignoredBuilds) {
ignoredPkgNames.add(parse(depPath).name ?? depPath)
}
automaticallyIgnoredBuilds = Array.from(ignoredPkgNames)
} else {
automaticallyIgnoredBuilds = null
}
return {
automaticallyIgnoredBuilds: modulesManifest && (modulesManifest.ignoredBuilds ?? []),
automaticallyIgnoredBuilds,
modulesDir,
modulesManifest,
}

View File

@@ -7,7 +7,7 @@ import { type RebuildCommandOpts } from '@pnpm/plugin-commands-rebuild'
import { prepare } from '@pnpm/prepare'
import { type ProjectManifest } from '@pnpm/types'
import { getConfig } from '@pnpm/config'
import { type Modules, readModulesManifest } from '@pnpm/modules-yaml'
import { readModulesManifest } from '@pnpm/modules-yaml'
import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
import { jest } from '@jest/globals'
import { sync as loadJsonFile } from 'load-json-file'
@@ -123,7 +123,7 @@ test('approve no builds', async () => {
expect(fs.readdirSync('node_modules/@pnpm.e2e/install-script-example')).not.toContain('generated-by-install.js')
// Covers https://github.com/pnpm/pnpm/issues/9296
expect(await readModulesManifest('node_modules')).not.toHaveProperty(['ignoredBuilds' satisfies keyof Modules])
expect((await readModulesManifest('node_modules'))!.ignoredBuilds).toBeUndefined()
})
test("works when root project manifest doesn't exist in a workspace", async () => {

View File

@@ -3,6 +3,7 @@ import fs from 'fs'
import { ignoredBuilds } from '@pnpm/exec.build-commands'
import { tempDir } from '@pnpm/prepare-temp-dir'
import { writeModulesManifest } from '@pnpm/modules-yaml'
import { type DepPath } from '@pnpm/types'
const DEFAULT_MODULES_MANIFEST = {
hoistedDependencies: {},
@@ -30,7 +31,7 @@ test('ignoredBuilds lists automatically ignored dependencies', async () => {
fs.mkdirSync(modulesDir, { recursive: true })
await writeModulesManifest(modulesDir, {
...DEFAULT_MODULES_MANIFEST,
ignoredBuilds: ['foo'],
ignoredBuilds: new Set(['foo@1.0.0' as DepPath]),
})
const output = await ignoredBuilds.handler({
dir,
@@ -46,7 +47,7 @@ test('ignoredBuilds lists explicitly ignored dependencies', async () => {
fs.mkdirSync(modulesDir, { recursive: true })
await writeModulesManifest(modulesDir, {
...DEFAULT_MODULES_MANIFEST,
ignoredBuilds: [],
ignoredBuilds: new Set(),
})
const output = await ignoredBuilds.handler({
dir,
@@ -66,7 +67,7 @@ test('ignoredBuilds lists both automatically and explicitly ignored dependencies
fs.mkdirSync(modulesDir, { recursive: true })
await writeModulesManifest(modulesDir, {
...DEFAULT_MODULES_MANIFEST,
ignoredBuilds: ['foo', 'bar'],
ignoredBuilds: new Set(['foo@1.0.0', 'bar@1.0.0'] as DepPath[]),
})
const output = await ignoredBuilds.handler({
dir,

View File

@@ -21,6 +21,9 @@
{
"path": "../../config/config-writer"
},
{
"path": "../../packages/dependency-path"
},
{
"path": "../../packages/types"
},

View File

@@ -36,6 +36,7 @@
"@pnpm/calc-dep-state": "workspace:*",
"@pnpm/config": "workspace:*",
"@pnpm/core-loggers": "workspace:*",
"@pnpm/dependency-path": "workspace:*",
"@pnpm/deps.graph-sequencer": "workspace:*",
"@pnpm/fs.hard-link-dir": "workspace:*",
"@pnpm/lifecycle": "workspace:*",

View File

@@ -4,6 +4,7 @@ import util from 'util'
import { calcDepState, type DepsStateCache } from '@pnpm/calc-dep-state'
import { getWorkspaceConcurrency } from '@pnpm/config'
import { skippedOptionalDependencyLogger } from '@pnpm/core-loggers'
import * as dp from '@pnpm/dependency-path'
import { runPostinstallHooks } from '@pnpm/lifecycle'
import { linkBins, linkBinsOfPackages } from '@pnpm/link-bins'
import { logger } from '@pnpm/logger'
@@ -11,7 +12,12 @@ import { hardLinkDir } from '@pnpm/worker'
import { readPackageJsonFromDir, safeReadPackageJsonFromDir } from '@pnpm/read-package-json'
import { type StoreController } from '@pnpm/store-controller-types'
import { applyPatchToDir } from '@pnpm/patching.apply-patch'
import { type AllowBuild, type DependencyManifest } from '@pnpm/types'
import {
type AllowBuild,
type DependencyManifest,
type DepPath,
type IgnoredBuilds,
} from '@pnpm/types'
import pDefer, { type DeferredPromise } from 'p-defer'
import pickBy from 'ramda/src/pickBy'
import runGroups from 'run-groups'
@@ -47,7 +53,7 @@ export async function buildModules<T extends string> (
rootModulesDir: string
hoistedLocations?: Record<string, string[]>
}
): Promise<{ ignoredBuilds?: string[] }> {
): Promise<{ ignoredBuilds?: IgnoredBuilds }> {
if (!rootDepPaths.length) return {}
const warn = (message: string) => {
logger.warn({ message, prefix: opts.lockfileDir })
@@ -61,7 +67,7 @@ export async function buildModules<T extends string> (
}
const chunks = buildSequence<T>(depGraph, rootDepPaths)
if (!chunks.length) return {}
const ignoredPkgs = new Set<string>()
let ignoredBuilds = new Set<DepPath>()
const allowBuild = opts.allowBuild ?? (() => true)
const groups = chunks.map((chunk) => {
chunk = chunk.filter((depPath) => {
@@ -77,7 +83,7 @@ export async function buildModules<T extends string> (
let ignoreScripts = Boolean(buildDepOpts.ignoreScripts)
if (!ignoreScripts) {
if (depGraph[depPath].requiresBuild && !allowBuild(depGraph[depPath].name, depGraph[depPath].version)) {
ignoredPkgs.add(depGraph[depPath].name)
ignoredBuilds.add(depGraph[depPath].depPath)
ignoreScripts = true
}
}
@@ -90,14 +96,15 @@ export async function buildModules<T extends string> (
})
await runGroups(getWorkspaceConcurrency(opts.childConcurrency), groups)
if (opts.ignoredBuiltDependencies?.length) {
for (const ignoredBuild of opts.ignoredBuiltDependencies) {
// We already ignore the build of this dependency.
// No need to report it.
ignoredPkgs.delete(ignoredBuild)
}
// We already ignore the build of these dependencies.
// No need to report them.
ignoredBuilds = new Set(Array.from(ignoredBuilds).filter((ignoredPkgDepPath) =>
!opts.ignoredBuiltDependencies!.some((ignoredInSettings) =>
(ignoredInSettings === ignoredPkgDepPath) || (dp.parse(ignoredPkgDepPath).name === ignoredInSettings)
)
))
}
const packageNames = Array.from(ignoredPkgs)
return { ignoredBuilds: packageNames }
return { ignoredBuilds }
}
async function buildDependency<T extends string> (

View File

@@ -24,6 +24,9 @@
{
"path": "../../packages/core-loggers"
},
{
"path": "../../packages/dependency-path"
},
{
"path": "../../packages/logger"
},

View File

@@ -26,7 +26,13 @@ import { lockfileWalker, type LockfileWalkerStep } from '@pnpm/lockfile.walker'
import { logger, streamParser } from '@pnpm/logger'
import { writeModulesManifest } from '@pnpm/modules-yaml'
import { createOrConnectStoreController } from '@pnpm/store-connection-manager'
import { type DepPath, type ProjectManifest, type ProjectId, type ProjectRootDir } from '@pnpm/types'
import {
type DepPath,
type IgnoredBuilds,
type ProjectManifest,
type ProjectId,
type ProjectRootDir,
} from '@pnpm/types'
import { createAllowBuildFunction } from '@pnpm/builder.policy'
import { pkgRequiresBuild } from '@pnpm/exec.pkg-requires-build'
import * as dp from '@pnpm/dependency-path'
@@ -92,7 +98,7 @@ export async function rebuildSelectedPkgs (
projects: Array<{ buildIndex: number, manifest: ProjectManifest, rootDir: ProjectRootDir }>,
pkgSpecs: string[],
maybeOpts: RebuildOptions
): Promise<{ ignoredBuilds?: string[] }> {
): Promise<{ ignoredBuilds?: IgnoredBuilds }> {
const reporter = maybeOpts?.reporter
if ((reporter != null) && typeof reporter === 'function') {
streamParser.on('data', reporter)
@@ -269,7 +275,7 @@ async function _rebuild (
extraNodePaths: string[]
} & Pick<PnpmContext, 'modulesFile'>,
opts: StrictRebuildOptions
): Promise<{ pkgsThatWereRebuilt: Set<string>, ignoredPkgs: string[] }> {
): Promise<{ pkgsThatWereRebuilt: Set<string>, ignoredPkgs: IgnoredBuilds }> {
const depGraph = lockfileToDepGraph(ctx.currentLockfile)
const depsStateCache: DepsStateCache = {}
const pkgsThatWereRebuilt = new Set<string>()
@@ -309,12 +315,12 @@ async function _rebuild (
logger.info({ message, prefix: opts.dir })
}
const ignoredPkgs: string[] = []
const ignoredPkgs = new Set<DepPath>()
const _allowBuild = createAllowBuildFunction(opts) ?? (() => true)
const allowBuild = (pkgName: string, version: string) => {
const allowBuild = (pkgName: string, version: string, depPath: DepPath) => {
if (_allowBuild(pkgName, version)) return true
if (!opts.ignoredBuiltDependencies?.includes(pkgName)) {
ignoredPkgs.push(pkgName)
ignoredPkgs.add(depPath)
}
return false
}
@@ -370,7 +376,7 @@ async function _rebuild (
requiresBuild = pkgRequiresBuild(pgkManifest, {})
}
const hasSideEffects = requiresBuild && allowBuild(pkgInfo.name, pkgInfo.version) && await runPostinstallHooks({
const hasSideEffects = requiresBuild && allowBuild(pkgInfo.name, pkgInfo.version, depPath) && await runPostinstallHooks({
depPath,
extraBinPaths,
extraEnv: opts.extraEnv,

View File

@@ -6,6 +6,7 @@ import {
parse,
refToRelative,
tryGetPackageId,
removeSuffix,
} from '@pnpm/dependency-path'
import { type DepPath } from '@pnpm/types'
@@ -139,3 +140,7 @@ test('getPkgIdWithPatchHash', () => {
// Scoped packages with both patch hash and peer dependencies
expect(getPkgIdWithPatchHash('@foo/bar@1.0.0(patch_hash=zzzz)(@types/node@18.0.0)' as DepPath)).toBe('@foo/bar@1.0.0(patch_hash=zzzz)')
})
test('removeSuffix', () => {
expect(removeSuffix('foo@1.0.0(patch_hash=0000)(@types/babel__core@7.1.14)')).toBe('foo@1.0.0')
})

View File

@@ -42,3 +42,5 @@ export type PinnedVersion =
| 'patch'
| 'minor'
| 'major'
export type IgnoredBuilds = Set<DepPath>

View File

@@ -106,6 +106,7 @@
"@pnpm/store-controller-types": "workspace:*",
"@pnpm/symlink-dependency": "workspace:*",
"@pnpm/types": "workspace:*",
"@pnpm/util.lex-comparator": "catalog:",
"@zkochan/rimraf": "catalog:",
"enquirer": "catalog:",
"is-inner-link": "catalog:",

View File

@@ -17,6 +17,7 @@ import {
summaryLogger,
} from '@pnpm/core-loggers'
import { hashObjectNullableWithPrefix } from '@pnpm/crypto.object-hasher'
import * as dp from '@pnpm/dependency-path'
import {
calcPatchHashes,
createOverridesMapFromParsed,
@@ -67,12 +68,14 @@ import {
type DepPath,
type DependenciesField,
type DependencyManifest,
type IgnoredBuilds,
type PeerDependencyIssues,
type ProjectId,
type ProjectManifest,
type ReadPackageHook,
type ProjectRootDir,
} from '@pnpm/types'
import { lexCompare } from '@pnpm/util.lex-comparator'
import isSubdir from 'is-subdir'
import pLimit from 'p-limit'
import mapValues from 'ramda/src/map'
@@ -154,7 +157,7 @@ export interface InstallResult {
*/
updatedCatalogs: Catalogs | undefined
updatedManifest: ProjectManifest
ignoredBuilds: string[] | undefined
ignoredBuilds: IgnoredBuilds | undefined
}
export async function install (
@@ -207,7 +210,7 @@ export type MutateModulesOptions = InstallOptions & {
export interface MutateModulesInSingleProjectResult {
updatedCatalogs: Catalogs | undefined
updatedProject: UpdatedProject
ignoredBuilds: string[] | undefined
ignoredBuilds: IgnoredBuilds | undefined
}
export async function mutateModulesInSingleProject (
@@ -249,7 +252,7 @@ export interface MutateModulesResult {
updatedProjects: UpdatedProject[]
stats: InstallationResultStats
depsRequiringBuild?: DepPath[]
ignoredBuilds: string[] | undefined
ignoredBuilds: IgnoredBuilds | undefined
}
const pickCatalogSpecifier: CatalogResultMatcher<string | undefined> = {
@@ -350,10 +353,14 @@ export async function mutateModules (
}
let ignoredBuilds = result.ignoredBuilds
if (!opts.ignoreScripts && ignoredBuilds?.length) {
if (!opts.ignoreScripts && ignoredBuilds?.size) {
ignoredBuilds = await runUnignoredDependencyBuilds(opts, ignoredBuilds)
}
ignoredScriptsLogger.debug({ packageNames: ignoredBuilds })
if (!opts.neverBuiltDependencies) {
ignoredScriptsLogger.debug({
packageNames: ignoredBuilds ? dedupePackageNamesFromIgnoredBuilds(ignoredBuilds) : [],
})
}
if ((reporter != null) && typeof reporter === 'function') {
streamParser.removeListener('data', reporter)
@@ -372,7 +379,7 @@ export async function mutateModules (
readonly updatedProjects: UpdatedProject[]
readonly stats?: InstallationResultStats
readonly depsRequiringBuild?: DepPath[]
readonly ignoredBuilds: string[] | undefined
readonly ignoredBuilds: IgnoredBuilds | undefined
}
async function _install (): Promise<InnerInstallResult> {
@@ -862,17 +869,19 @@ Note that in CI environments, this setting is enabled by default.`,
}
}
async function runUnignoredDependencyBuilds (opts: StrictInstallOptions, previousIgnoredBuilds: string[]): Promise<string[]> {
async function runUnignoredDependencyBuilds (opts: StrictInstallOptions, previousIgnoredBuilds: IgnoredBuilds): Promise<Set<DepPath>> {
if (!opts.onlyBuiltDependencies?.length) {
return previousIgnoredBuilds
}
const onlyBuiltDeps = createPackageVersionPolicy(opts.onlyBuiltDependencies)
const pkgsToBuild = previousIgnoredBuilds.flatMap((ignoredPkg) => {
const matchResult = onlyBuiltDeps(ignoredPkg)
const pkgsToBuild = Array.from(previousIgnoredBuilds).flatMap((ignoredPkg) => {
const ignoredPkgName = dp.parse(ignoredPkg).name
if (!ignoredPkgName) return []
const matchResult = onlyBuiltDeps(ignoredPkgName)
if (matchResult === true) {
return [ignoredPkg]
return [ignoredPkgName]
} else if (Array.isArray(matchResult)) {
return matchResult.map(version => `${ignoredPkg}@${version}`)
return matchResult.map(version => `${ignoredPkgName}@${version}`)
}
return []
})
@@ -1052,7 +1061,7 @@ interface InstallFunctionResult {
projects: UpdatedProject[]
stats?: InstallationResultStats
depsRequiringBuild: DepPath[]
ignoredBuilds?: string[]
ignoredBuilds?: IgnoredBuilds
}
type InstallFunction = (
@@ -1271,7 +1280,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
}
let stats: InstallationResultStats | undefined
const allowBuild = createAllowBuildFunction(opts)
let ignoredBuilds: string[] | undefined
let ignoredBuilds: IgnoredBuilds | undefined
if (!opts.lockfileOnly && !isInstallationOnlyForLockfileCheck && opts.enableModulesDir) {
const result = await linkPackages(
projects,
@@ -1371,8 +1380,13 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
unsafePerm: opts.unsafePerm,
userAgent: opts.userAgent,
})).ignoredBuilds
if (ignoredBuilds == null && ctx.modulesFile?.ignoredBuilds?.length) {
ignoredBuilds = ctx.modulesFile.ignoredBuilds
if (ctx.modulesFile?.ignoredBuilds?.size) {
ignoredBuilds ??= new Set()
for (const ignoredBuild of ctx.modulesFile.ignoredBuilds.values()) {
if (result.currentLockfile.packages?.[ignoredBuild]) {
ignoredBuilds.add(ignoredBuild)
}
}
}
}
}
@@ -1703,3 +1717,16 @@ async function linkAllBins (
depNodes.map(async depNode => limitLinking(async () => linkBinsOfDependencies(depNode, depGraph, opts)))
)
}
export class IgnoredBuildsError extends PnpmError {
constructor (ignoredBuilds: IgnoredBuilds) {
const packageNames = dedupePackageNamesFromIgnoredBuilds(ignoredBuilds)
super('IGNORED_BUILDS', `Ignored build scripts: ${packageNames.join(', ')}`, {
hint: 'Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.',
})
}
}
function dedupePackageNamesFromIgnoredBuilds (ignoredBuilds: IgnoredBuilds): string[] {
return Array.from(new Set(Array.from(ignoredBuilds ?? []).map(dp.removeSuffix))).sort(lexCompare)
}

View File

@@ -485,7 +485,7 @@ test('selectively allow scripts in some dependencies by onlyBuiltDependencies',
{
const ignoredPkgsLog = reporter.getCalls().find((call) => call.firstArg.name === 'pnpm:ignored-scripts')?.firstArg
expect(ignoredPkgsLog.packageNames).toStrictEqual(['@pnpm.e2e/pre-and-postinstall-scripts-example'])
expect(ignoredPkgsLog.packageNames).toStrictEqual(['@pnpm.e2e/pre-and-postinstall-scripts-example@1.0.0'])
}
reporter.resetHistory()
@@ -526,7 +526,7 @@ test('selectively allow scripts in some dependencies by onlyBuiltDependencies us
{
const ignoredPkgsLog = reporter.getCalls().find((call) => call.firstArg.name === 'pnpm:ignored-scripts')?.firstArg
expect(ignoredPkgsLog.packageNames).toStrictEqual(['@pnpm.e2e/pre-and-postinstall-scripts-example'])
expect(ignoredPkgsLog.packageNames).toStrictEqual(['@pnpm.e2e/pre-and-postinstall-scripts-example@1.0.0'])
}
reporter.resetHistory()

View File

@@ -62,6 +62,7 @@ import {
type DepPath,
type DependencyManifest,
type HoistedDependencies,
type IgnoredBuilds,
type ProjectId,
type ProjectManifest,
type Registries,
@@ -191,7 +192,7 @@ export interface InstallationResultStats {
export interface InstallationResult {
stats: InstallationResultStats
ignoredBuilds: string[] | undefined
ignoredBuilds: IgnoredBuilds | undefined
}
export async function headlessInstall (opts: HeadlessOptions): Promise<InstallationResult> {
@@ -517,7 +518,7 @@ export async function headlessInstall (opts: HeadlessOptions): Promise<Installat
.map(({ depPath }) => depPath)
)
}
let ignoredBuilds: string[] | undefined
let ignoredBuilds: IgnoredBuilds | undefined
if ((!opts.ignoreScripts || Object.keys(opts.patchedDependencies ?? {}).length > 0) && opts.enableModulesDir !== false) {
const directNodes = new Set<string>()
for (const id of union(importerIds, ['.'])) {
@@ -562,8 +563,13 @@ export async function headlessInstall (opts: HeadlessOptions): Promise<Installat
unsafePerm: opts.unsafePerm,
userAgent: opts.userAgent,
})).ignoredBuilds
if (ignoredBuilds == null && opts.modulesFile?.ignoredBuilds?.length) {
ignoredBuilds = opts.modulesFile.ignoredBuilds
if (opts.modulesFile?.ignoredBuilds?.size) {
ignoredBuilds ??= new Set()
for (const ignoredBuild of opts.modulesFile.ignoredBuilds.values()) {
if (filteredLockfile.packages?.[ignoredBuild]) {
ignoredBuilds.add(ignoredBuild)
}
}
}
}

View File

@@ -1,5 +1,11 @@
import path from 'path'
import { type DepPath, type DependenciesField, type HoistedDependencies, type Registries } from '@pnpm/types'
import {
type DepPath,
type DependenciesField,
type HoistedDependencies,
type IgnoredBuilds,
type Registries,
} from '@pnpm/types'
import readYamlFile from 'read-yaml-file'
import mapValues from 'ramda/src/map'
import isWindows from 'is-windows'
@@ -13,7 +19,7 @@ export type IncludedDependencies = {
[dependenciesField in DependenciesField]: boolean
}
export interface Modules {
interface ModulesRaw {
hoistedAliases?: { [depPath: DepPath]: string[] } // for backward compatibility
hoistedDependencies: HoistedDependencies
hoistPattern?: string[]
@@ -22,7 +28,7 @@ export interface Modules {
nodeLinker?: 'hoisted' | 'isolated' | 'pnp'
packageManager: string
pendingBuilds: string[]
ignoredBuilds?: string[]
ignoredBuilds?: DepPath[]
prunedAt: string
registries?: Registries // nullable for backward compatibility
shamefullyHoist?: boolean // for backward compatibility
@@ -35,18 +41,26 @@ export interface Modules {
hoistedLocations?: Record<string, string[]>
}
export type Modules = Omit<ModulesRaw, 'ignoredBuilds'> & {
ignoredBuilds?: IgnoredBuilds
}
export async function readModulesManifest (modulesDir: string): Promise<Modules | null> {
const modulesYamlPath = path.join(modulesDir, MODULES_FILENAME)
let modules!: Modules
let modulesRaw!: ModulesRaw
try {
modules = await readYamlFile<Modules>(modulesYamlPath)
if (!modules) return modules
modulesRaw = await readYamlFile<ModulesRaw>(modulesYamlPath)
if (!modulesRaw) return modulesRaw
} catch (err: any) { // eslint-disable-line
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
throw err
}
return null
}
const modules = {
...modulesRaw,
ignoredBuilds: modulesRaw.ignoredBuilds ? new Set<DepPath>(modulesRaw.ignoredBuilds) : undefined,
}
if (!modules.virtualStoreDir) {
modules.virtualStoreDir = path.join(modulesDir, '.pnpm')
} else if (!path.isAbsolute(modules.virtualStoreDir)) {
@@ -107,7 +121,7 @@ export async function writeModulesManifest (
}
): Promise<void> {
const modulesYamlPath = path.join(modulesDir, MODULES_FILENAME)
const saveModules = { ...modules }
const saveModules = { ...modules, ignoredBuilds: modules.ignoredBuilds ? Array.from(modules.ignoredBuilds) : undefined }
if (saveModules.skipped) saveModules.skipped.sort()
if (saveModules.hoistPattern == null || (saveModules.hoistPattern as unknown) === '') {

View File

@@ -1,21 +1,21 @@
/// <reference path="../../../__typings__/index.d.ts"/>
import fs from 'fs'
import path from 'path'
import { readModulesManifest, writeModulesManifest } from '@pnpm/modules-yaml'
import { readModulesManifest, writeModulesManifest, type StrictModules } from '@pnpm/modules-yaml'
import { sync as readYamlFile } from 'read-yaml-file'
import isWindows from 'is-windows'
import tempy from 'tempy'
test('writeModulesManifest() and readModulesManifest()', async () => {
const modulesDir = tempy.directory()
const modulesYaml = {
const modulesYaml: StrictModules = {
hoistedDependencies: {},
included: {
dependencies: true,
devDependencies: true,
optionalDependencies: true,
},
ignoredBuilds: [],
ignoredBuilds: new Set(),
layoutVersion: 1,
packageManager: 'pnpm@2',
pendingBuilds: [],
@@ -66,14 +66,14 @@ test('backward compatible read of .modules.yaml created with shamefully-hoist=fa
test('readModulesManifest() should not create a node_modules directory if it does not exist', async () => {
const modulesDir = path.join(tempy.directory(), 'node_modules')
const modulesYaml = {
const modulesYaml: StrictModules = {
hoistedDependencies: {},
included: {
dependencies: true,
devDependencies: true,
optionalDependencies: true,
},
ignoredBuilds: [],
ignoredBuilds: new Set(),
layoutVersion: 1,
packageManager: 'pnpm@2',
pendingBuilds: [],
@@ -94,14 +94,14 @@ test('readModulesManifest() should not create a node_modules directory if it doe
test('readModulesManifest() should create a node_modules directory if makeModuleDir is set to true', async () => {
const modulesDir = path.join(tempy.directory(), 'node_modules')
const modulesYaml = {
const modulesYaml: StrictModules = {
hoistedDependencies: {},
included: {
dependencies: true,
devDependencies: true,
optionalDependencies: true,
},
ignoredBuilds: [],
ignoredBuilds: new Set(),
layoutVersion: 1,
packageManager: 'pnpm@2',
pendingBuilds: [],

View File

@@ -1,9 +0,0 @@
import { PnpmError } from '@pnpm/error'
export class IgnoredBuildsError extends PnpmError {
constructor (ignoredBuilds: string[]) {
super('IGNORED_BUILDS', `Ignored build scripts: ${ignoredBuilds.join(', ')}`, {
hint: 'Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.',
})
}
}

View File

@@ -15,6 +15,7 @@ import { rebuildProjects } from '@pnpm/plugin-commands-rebuild'
import { createOrConnectStoreController, type CreateStoreControllerOptions } from '@pnpm/store-connection-manager'
import { type IncludedDependencies, type Project, type ProjectsGraph, type ProjectRootDir, type PrepareExecutionEnv } from '@pnpm/types'
import {
IgnoredBuildsError,
install,
mutateModulesInSingleProject,
type MutateModulesOptions,
@@ -26,7 +27,6 @@ import { updateWorkspaceManifest } from '@pnpm/workspace.manifest-writer'
import { createPkgGraph } from '@pnpm/workspace.pkgs-graph'
import { updateWorkspaceState, type WorkspaceStateSettings } from '@pnpm/workspace.state'
import isSubdir from 'is-subdir'
import { IgnoredBuildsError } from './errors.js'
import { getPinnedVersion } from './getPinnedVersion.js'
import { getSaveType } from './getSaveType.js'
import { getNodeExecPath } from './nodeExecPath.js'
@@ -343,7 +343,7 @@ when running add/update with the --workspace option')
configDependencies: opts.configDependencies,
})
}
if (opts.strictDepBuilds && ignoredBuilds?.length) {
if (opts.strictDepBuilds && ignoredBuilds?.size) {
throw new IgnoredBuildsError(ignoredBuilds)
}
return
@@ -364,7 +364,7 @@ when running add/update with the --workspace option')
}),
])
}
if (opts.strictDepBuilds && ignoredBuilds?.length) {
if (opts.strictDepBuilds && ignoredBuilds?.size) {
throw new IgnoredBuildsError(ignoredBuilds)
}

View File

@@ -23,6 +23,7 @@ import { requireHooks } from '@pnpm/pnpmfile'
import { sortPackages } from '@pnpm/sort-packages'
import { createOrConnectStoreController, type CreateStoreControllerOptions } from '@pnpm/store-connection-manager'
import {
type IgnoredBuilds,
type IncludedDependencies,
type PackageManifest,
type Project,
@@ -34,6 +35,7 @@ import {
import { updateWorkspaceManifest } from '@pnpm/workspace.manifest-writer'
import {
addDependenciesToPackage,
IgnoredBuildsError,
install,
type InstallOptions,
type MutatedProject,
@@ -50,7 +52,6 @@ import { createWorkspaceSpecs, updateToWorkspacePackagesFromManifest } from './u
import { getSaveType } from './getSaveType.js'
import { getPinnedVersion } from './getPinnedVersion.js'
import { type PreferredVersions } from '@pnpm/resolver-base'
import { IgnoredBuildsError } from './errors.js'
export type RecursiveOptions = CreateStoreControllerOptions & Pick<Config,
| 'bail'
@@ -299,7 +300,7 @@ export async function recursive (
}))
await Promise.all(promises)
}
if (opts.strictDepBuilds && ignoredBuilds?.length) {
if (opts.strictDepBuilds && ignoredBuilds?.size) {
throw new IgnoredBuildsError(ignoredBuilds)
}
return true
@@ -355,7 +356,7 @@ export async function recursive (
interface ActionResult {
updatedCatalogs?: Catalogs
updatedManifest: ProjectManifest
ignoredBuilds: string[] | undefined
ignoredBuilds: IgnoredBuilds | undefined
}
type ActionFunction = (manifest: PackageManifest | ProjectManifest, opts: ActionOpts) => Promise<ActionResult>
@@ -419,7 +420,7 @@ export async function recursive (
Object.assign(updatedCatalogs, newCatalogsAddition)
}
}
if (opts.strictDepBuilds && ignoredBuilds?.length) {
if (opts.strictDepBuilds && ignoredBuilds?.size) {
throw new IgnoredBuildsError(ignoredBuilds)
}
result[rootDir].status = 'passed'

9
pnpm-lock.yaml generated
View File

@@ -2472,6 +2472,9 @@ importers:
'@pnpm/config.config-writer':
specifier: workspace:*
version: link:../../config/config-writer
'@pnpm/dependency-path':
specifier: workspace:*
version: link:../../packages/dependency-path
'@pnpm/logger':
specifier: 'catalog:'
version: 1001.0.0
@@ -2545,6 +2548,9 @@ importers:
'@pnpm/core-loggers':
specifier: workspace:*
version: link:../../packages/core-loggers
'@pnpm/dependency-path':
specifier: workspace:*
version: link:../../packages/dependency-path
'@pnpm/deps.graph-sequencer':
specifier: workspace:*
version: link:../../deps/graph-sequencer
@@ -4978,6 +4984,9 @@ importers:
'@pnpm/types':
specifier: workspace:*
version: link:../../packages/types
'@pnpm/util.lex-comparator':
specifier: 'catalog:'
version: 3.0.2
'@pnpm/worker':
specifier: workspace:^
version: link:../../worker

View File

@@ -383,8 +383,8 @@ test('the list of ignored builds is preserved after a repeat install', async ()
expect(result.stdout.toString()).toContain('Ignored build scripts:')
const modulesManifest = project.readModulesManifest()
expect(modulesManifest?.ignoredBuilds?.sort()).toStrictEqual([
'@pnpm.e2e/pre-and-postinstall-scripts-example',
'esbuild',
expect(Array.from(modulesManifest!.ignoredBuilds!).sort()).toStrictEqual([
'@pnpm.e2e/pre-and-postinstall-scripts-example@1.0.0',
'esbuild@0.25.0',
])
})