fix: dependencies that were added to onlyBuiltDependencies should be built on install (#10256)

This commit is contained in:
Zoltan Kochan
2025-12-02 15:31:52 +01:00
committed by GitHub
parent 5f73b0f2b6
commit 4362c06005
12 changed files with 145 additions and 39 deletions

View File

@@ -0,0 +1,10 @@
---
"@pnpm/plugin-commands-rebuild": patch
"@pnpm/default-reporter": patch
"@pnpm/headless": patch
"@pnpm/build-modules": patch
"@pnpm/core": patch
"pnpm": patch
---
`pnpm install` should build any dependencies that were added to `onlyBuiltDependencies` and were not built yet [#10256](https://github.com/pnpm/pnpm/pull/10256).

View File

@@ -8,6 +8,7 @@ import { reportContext } from './reportContext.js'
import { reportExecutionTime } from './reportExecutionTime.js'
import { reportDeprecations } from './reportDeprecations.js'
import { reportHooks } from './reportHooks.js'
import { reportIgnoredBuilds } from './reportIgnoredBuilds.js'
import { reportInstallChecks } from './reportInstallChecks.js'
import { reportInstallingConfigDeps } from './reportInstallingConfigDeps.js'
import { reportLifecycleScripts } from './reportLifecycleScripts.js'
@@ -156,9 +157,14 @@ export function reporterForClient (
env: opts.env,
filterPkgsDiff: opts.filterPkgsDiff,
pnpmConfig: opts.pnpmConfig,
approveBuildsInstructionText: opts.approveBuildsInstructionText,
}))
}
outputs.push(
reportIgnoredBuilds(log$, {
pnpmConfig: opts.pnpmConfig,
approveBuildsInstructionText: opts.approveBuildsInstructionText,
})
)
}
return outputs

View File

@@ -0,0 +1,34 @@
import { type Config } from '@pnpm/config'
import { type IgnoredScriptsLog } from '@pnpm/core-loggers'
import { lexCompare } from '@pnpm/util.lex-comparator'
import * as Rx from 'rxjs'
import { map } from 'rxjs/operators'
import boxen from 'boxen'
export function reportIgnoredBuilds (
log$: {
ignoredScripts: Rx.Observable<IgnoredScriptsLog>
},
opts: {
pnpmConfig?: Config
// This is used by Bit CLI
approveBuildsInstructionText?: string
}
): Rx.Observable<Rx.Observable<{ msg: string }>> {
return log$.ignoredScripts.pipe(
map((ignoredScripts) => {
if (ignoredScripts.packageNames && ignoredScripts.packageNames.length > 0 && !opts.pnpmConfig?.strictDepBuilds) {
const msg = boxen(`Ignored build scripts: ${Array.from(ignoredScripts.packageNames).sort(lexCompare).join(', ')}.
${opts.approveBuildsInstructionText ?? `Run "pnpm approve-builds${opts.pnpmConfig?.cliOptions?.global ? ' -g' : ''}" to pick which dependencies should be allowed to run scripts.`}`, {
title: 'Warning',
padding: 1,
margin: 0,
borderStyle: 'round',
borderColor: 'yellow',
})
return Rx.of({ msg })
}
return Rx.NEVER
})
)
}

View File

@@ -1,16 +1,13 @@
import path from 'path'
import {
type IgnoredScriptsLog,
type DeprecationLog,
type PackageManifestLog,
type RootLog,
type SummaryLog,
} from '@pnpm/core-loggers'
import { type Config } from '@pnpm/config'
import { lexCompare } from '@pnpm/util.lex-comparator'
import * as Rx from 'rxjs'
import { map, take } from 'rxjs/operators'
import boxen from 'boxen'
import chalk from 'chalk'
import semver from 'semver'
import { EOL } from '../constants.js'
@@ -40,7 +37,6 @@ export function reportSummary (
summary: Rx.Observable<SummaryLog>
root: Rx.Observable<RootLog>
packageManifest: Rx.Observable<PackageManifestLog>
ignoredScripts: Rx.Observable<IgnoredScriptsLog>
},
opts: {
cmd: string
@@ -48,8 +44,6 @@ export function reportSummary (
env: NodeJS.ProcessEnv
filterPkgsDiff?: FilterPkgsDiff
pnpmConfig?: Config
// This is used by Bit CLI
approveBuildsInstructionText?: string
}
): Rx.Observable<Rx.Observable<{ msg: string }>> {
const pkgsDiff$ = getPkgsDiff(log$, { prefix: opts.cwd })
@@ -59,12 +53,11 @@ export function reportSummary (
return Rx.combineLatest(
pkgsDiff$,
log$.ignoredScripts.pipe(Rx.startWith({ packageNames: undefined })),
summaryLog$
)
.pipe(
take(1),
map(([pkgsDiff, ignoredScripts]) => {
map(([pkgsDiff]) => {
let msg = ''
for (const depType of ['prod', 'optional', 'peer', 'dev', 'nodeModulesOnly'] as const) {
let diffs: PackageDiff[] = Object.values(pkgsDiff[depType as keyof typeof pkgsDiff])
@@ -89,18 +82,6 @@ export function reportSummary (
msg += EOL
}
}
if (ignoredScripts.packageNames && ignoredScripts.packageNames.length > 0 && !opts.pnpmConfig?.strictDepBuilds) {
msg += EOL
msg += boxen(`Ignored build scripts: ${Array.from(ignoredScripts.packageNames).sort(lexCompare).join(', ')}.
${opts.approveBuildsInstructionText ?? `Run "pnpm approve-builds${opts.pnpmConfig?.cliOptions?.global ? ' -g' : ''}" to pick which dependencies should be allowed to run scripts.`}`, {
title: 'Warning',
padding: 1,
margin: 0,
borderStyle: 'round',
borderColor: 'yellow',
})
msg += EOL
}
return Rx.of({ msg })
})
)

View File

@@ -3,7 +3,7 @@ import path from 'path'
import util from 'util'
import { calcDepState, type DepsStateCache } from '@pnpm/calc-dep-state'
import { getWorkspaceConcurrency } from '@pnpm/config'
import { skippedOptionalDependencyLogger, ignoredScriptsLogger } from '@pnpm/core-loggers'
import { skippedOptionalDependencyLogger } from '@pnpm/core-loggers'
import { runPostinstallHooks } from '@pnpm/lifecycle'
import { linkBins, linkBinsOfPackages } from '@pnpm/link-bins'
import { logger } from '@pnpm/logger'
@@ -97,7 +97,6 @@ export async function buildModules<T extends string> (
}
}
const packageNames = Array.from(ignoredPkgs)
ignoredScriptsLogger.debug({ packageNames })
return { ignoredBuilds: packageNames }
}

View File

@@ -92,7 +92,7 @@ export async function rebuildSelectedPkgs (
projects: Array<{ buildIndex: number, manifest: ProjectManifest, rootDir: ProjectRootDir }>,
pkgSpecs: string[],
maybeOpts: RebuildOptions
): Promise<void> {
): Promise<{ ignoredBuilds?: string[] }> {
const reporter = maybeOpts?.reporter
if ((reporter != null) && typeof reporter === 'function') {
streamParser.on('data', reporter)
@@ -100,7 +100,7 @@ export async function rebuildSelectedPkgs (
const opts = await extendRebuildOptions(maybeOpts)
const ctx = await getContext({ ...opts, allProjects: projects })
if (ctx.currentLockfile?.packages == null) return
if (ctx.currentLockfile?.packages == null) return {}
const packages = ctx.currentLockfile.packages
const searched: PackageSelector[] = pkgSpecs.map((arg) => {
@@ -149,6 +149,9 @@ export async function rebuildSelectedPkgs (
virtualStoreDir: ctx.virtualStoreDir,
virtualStoreDirMaxLength: ctx.virtualStoreDirMaxLength,
})
return {
ignoredBuilds: ignoredPkgs,
}
}
export async function rebuildProjects (

View File

@@ -62,6 +62,7 @@
"@pnpm/catalogs.protocol-parser": "workspace:*",
"@pnpm/catalogs.resolver": "workspace:*",
"@pnpm/catalogs.types": "workspace:*",
"@pnpm/config.version-policy": "workspace:*",
"@pnpm/constants": "workspace:*",
"@pnpm/core-loggers": "workspace:*",
"@pnpm/crypto.hash": "workspace:*",
@@ -96,6 +97,7 @@
"@pnpm/parse-wanted-dependency": "workspace:*",
"@pnpm/patching.config": "workspace:*",
"@pnpm/pkg-manager.direct-dep-linker": "workspace:*",
"@pnpm/plugin-commands-rebuild": "workspace:*",
"@pnpm/read-modules-dir": "workspace:*",
"@pnpm/read-project-manifest": "workspace:*",
"@pnpm/remove-bins": "workspace:*",

View File

@@ -4,6 +4,7 @@ import { createAllowBuildFunction } from '@pnpm/builder.policy'
import { parseCatalogProtocol } from '@pnpm/catalogs.protocol-parser'
import { resolveFromCatalog, matchCatalogResolveResult, type CatalogResultMatcher } from '@pnpm/catalogs.resolver'
import { type Catalogs } from '@pnpm/catalogs.types'
import { createPackageVersionPolicy } from '@pnpm/config.version-policy'
import {
LAYOUT_VERSION,
LOCKFILE_VERSION,
@@ -48,6 +49,7 @@ import { logger, globalInfo, streamParser } from '@pnpm/logger'
import { getAllDependenciesFromManifest, getAllUniqueSpecs } from '@pnpm/manifest-utils'
import { writeModulesManifest } from '@pnpm/modules-yaml'
import { type PatchGroupRecord, groupPatchedDependencies } from '@pnpm/patching.config'
import { rebuildSelectedPkgs } from '@pnpm/plugin-commands-rebuild'
import { safeReadProjectManifestOnly } from '@pnpm/read-project-manifest'
import {
getWantedDependencies,
@@ -357,20 +359,27 @@ export async function mutateModules (
// @ts-expect-error
globalInfo(`The integrity of ${global['verifiedFileIntegrity']} files was checked. This might have caused installation to take longer.`)
}
if ((reporter != null) && typeof reporter === 'function') {
streamParser.removeListener('data', reporter)
}
if (opts.mergeGitBranchLockfiles) {
await cleanGitBranchLockfiles(ctx.lockfileDir)
}
let ignoredBuilds = result.ignoredBuilds
if (!opts.ignoreScripts && ignoredBuilds?.length) {
ignoredBuilds = await runUnignoredDependencyBuilds(opts, ignoredBuilds)
}
ignoredScriptsLogger.debug({ packageNames: ignoredBuilds })
if ((reporter != null) && typeof reporter === 'function') {
streamParser.removeListener('data', reporter)
}
return {
updatedCatalogs: result.updatedCatalogs,
updatedProjects: result.updatedProjects,
stats: result.stats ?? { added: 0, removed: 0, linkedToRoot: 0 },
depsRequiringBuild: result.depsRequiringBuild,
ignoredBuilds: result.ignoredBuilds,
ignoredBuilds,
}
interface InnerInstallResult {
@@ -869,6 +878,30 @@ Note that in CI environments, this setting is enabled by default.`,
}
}
async function runUnignoredDependencyBuilds (opts: StrictInstallOptions, previousIgnoredBuilds: string[]): Promise<string[]> {
if (!opts.onlyBuiltDependencies?.length) {
return previousIgnoredBuilds
}
const onlyBuiltDeps = createPackageVersionPolicy(opts.onlyBuiltDependencies)
const pkgsToBuild = previousIgnoredBuilds.flatMap((ignoredPkg) => {
const matchResult = onlyBuiltDeps(ignoredPkg)
if (matchResult === true) {
return [ignoredPkg]
} else if (Array.isArray(matchResult)) {
return matchResult.map(version => `${ignoredPkg}@${version}`)
}
return []
})
if (pkgsToBuild.length) {
return (await rebuildSelectedPkgs(opts.allProjects, pkgsToBuild, {
...opts,
reporter: undefined, // We don't want to attach the reporter again, it was already attached.
rootProjectManifestDir: opts.lockfileDir,
})).ignoredBuilds ?? previousIgnoredBuilds
}
return previousIgnoredBuilds
}
function cacheExpired (prunedAt: string, maxAgeInMinutes: number): boolean {
return ((Date.now() - new Date(prunedAt).valueOf()) / (1000 * 60)) > maxAgeInMinutes
}
@@ -1357,7 +1390,6 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
})).ignoredBuilds
if (ignoredBuilds == null && ctx.modulesFile?.ignoredBuilds?.length) {
ignoredBuilds = ctx.modulesFile.ignoredBuilds
ignoredScriptsLogger.debug({ packageNames: ignoredBuilds })
}
}
}

View File

@@ -764,3 +764,32 @@ test('run pre/postinstall scripts in a project that uses node-linker=hoisted. Sh
message: `An error occurred while uploading ${path.resolve('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example')}`,
}))
})
test('build dependencies that were not previously built after onlyBuiltDependencies changes', async () => {
prepareEmpty()
const neverBuiltDependencies: string[] | undefined = undefined
const { updatedManifest: manifest } = await addDependenciesToPackage({},
['@pnpm.e2e/pre-and-postinstall-scripts-example@1.0.0', '@pnpm.e2e/install-script-example'],
testDefaults({
fastUnpack: false,
onlyBuiltDependencies: ['@pnpm.e2e/install-script-example'],
neverBuiltDependencies,
})
)
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-preinstall.js')).toBeFalsy()
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-postinstall.js')).toBeFalsy()
expect(fs.existsSync('node_modules/@pnpm.e2e/install-script-example/generated-by-install.js')).toBeTruthy()
await install(manifest, testDefaults({
fastUnpack: false,
frozenLockfile: true,
ignoredBuiltDependencies: [],
neverBuiltDependencies,
onlyBuiltDependencies: ['@pnpm.e2e/install-script-example', '@pnpm.e2e/pre-and-postinstall-scripts-example@1.0.0'],
}))
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')).toBeTruthy()
})

View File

@@ -45,6 +45,9 @@
{
"path": "../../config/parse-overrides"
},
{
"path": "../../config/version-policy"
},
{
"path": "../../crypto/hash"
},
@@ -60,6 +63,9 @@
{
"path": "../../exec/lifecycle"
},
{
"path": "../../exec/plugin-commands-rebuild"
},
{
"path": "../../fs/read-modules-dir"
},

View File

@@ -8,7 +8,6 @@ import {
WANTED_LOCKFILE,
} from '@pnpm/constants'
import {
ignoredScriptsLogger,
packageManifestLogger,
progressLogger,
stageLogger,
@@ -560,7 +559,6 @@ export async function headlessInstall (opts: HeadlessOptions): Promise<Installat
})).ignoredBuilds
if (ignoredBuilds == null && opts.modulesFile?.ignoredBuilds?.length) {
ignoredBuilds = opts.modulesFile.ignoredBuilds
ignoredScriptsLogger.debug({ packageNames: ignoredBuilds })
}
}

20
pnpm-lock.yaml generated
View File

@@ -4953,6 +4953,9 @@ importers:
'@pnpm/catalogs.types':
specifier: workspace:*
version: link:../../catalogs/types
'@pnpm/config.version-policy':
specifier: workspace:*
version: link:../../config/version-policy
'@pnpm/constants':
specifier: workspace:*
version: link:../../packages/constants
@@ -5055,6 +5058,9 @@ importers:
'@pnpm/pkg-manager.direct-dep-linker':
specifier: workspace:*
version: link:../direct-dep-linker
'@pnpm/plugin-commands-rebuild':
specifier: workspace:*
version: link:../../exec/plugin-commands-rebuild
'@pnpm/read-modules-dir':
specifier: workspace:*
version: link:../../fs/read-modules-dir
@@ -18351,7 +18357,7 @@ snapshots:
'@pnpm/fs.packlist': 2.0.0
'@pnpm/logger': 1001.0.0
'@pnpm/prepare-package': 1000.0.16(@pnpm/logger@1001.0.0)(typanion@3.14.0)
'@pnpm/worker': 1000.1.7(@pnpm/logger@packages+logger)(@types/node@22.15.30)
'@pnpm/worker': 1000.1.7(@pnpm/logger@1001.0.0)(@types/node@22.15.30)
'@zkochan/rimraf': 3.0.2
execa: safe-execa@0.1.2
transitivePeerDependencies:
@@ -18486,7 +18492,7 @@ snapshots:
'@pnpm/find-workspace-dir': 1000.1.0
'@pnpm/logger': 1001.0.0
'@pnpm/types': 1000.6.0
'@pnpm/worker': 1000.1.7(@pnpm/logger@packages+logger)(@types/node@22.15.30)
'@pnpm/worker': 1000.1.7(@pnpm/logger@1001.0.0)(@types/node@22.15.30)
'@pnpm/workspace.find-packages': 1000.0.25(@pnpm/logger@1001.0.0)(@pnpm/worker@1000.1.7(@pnpm/logger@1001.0.0)(@types/node@22.15.30))(typanion@3.14.0)
'@pnpm/workspace.read-manifest': 1000.1.5
load-json-file: 7.0.1
@@ -18692,7 +18698,7 @@ snapshots:
'@pnpm/store-controller-types': 1003.0.2
'@pnpm/store.cafs': 1000.0.13
'@pnpm/types': 1000.6.0
'@pnpm/worker': 1000.1.7(@pnpm/logger@packages+logger)(@types/node@22.15.30)
'@pnpm/worker': 1000.1.7(@pnpm/logger@1001.0.0)(@types/node@22.15.30)
p-defer: 3.0.0
p-limit: 3.1.0
p-queue: 6.6.2
@@ -18711,7 +18717,7 @@ snapshots:
'@pnpm/store-controller-types': 1003.0.2
'@pnpm/store.cafs': 1000.0.13
'@pnpm/types': 1000.6.0
'@pnpm/worker': 1000.1.7(@pnpm/logger@packages+logger)(@types/node@22.15.30)
'@pnpm/worker': 1000.1.7(@pnpm/logger@1001.0.0)(@types/node@22.15.30)
'@zkochan/rimraf': 3.0.2
load-json-file: 6.2.0
ramda: '@pnpm/ramda@0.28.1'
@@ -18990,7 +18996,7 @@ snapshots:
'@pnpm/graceful-fs': 1000.0.0
'@pnpm/logger': 1001.0.0
'@pnpm/prepare-package': 1000.0.16(@pnpm/logger@1001.0.0)(typanion@3.14.0)
'@pnpm/worker': 1000.1.7(@pnpm/logger@packages+logger)(@types/node@22.15.30)
'@pnpm/worker': 1000.1.7(@pnpm/logger@1001.0.0)(@types/node@22.15.30)
'@zkochan/retry': 0.2.0
lodash.throttle: 4.1.1
p-map-values: 1.0.0
@@ -19029,7 +19035,7 @@ snapshots:
dependencies:
isexe: 2.0.0
'@pnpm/worker@1000.1.7(@pnpm/logger@packages+logger)(@types/node@22.15.30)':
'@pnpm/worker@1000.1.7(@pnpm/logger@1001.0.0)(@types/node@22.15.30)':
dependencies:
'@pnpm/cafs-types': 1000.0.0
'@pnpm/create-cafs-store': 1000.0.14(@pnpm/logger@1001.0.0)
@@ -19038,7 +19044,7 @@ snapshots:
'@pnpm/exec.pkg-requires-build': 1000.0.8
'@pnpm/fs.hard-link-dir': 1000.0.1(@pnpm/logger@1001.0.0)
'@pnpm/graceful-fs': 1000.0.0
'@pnpm/logger': link:packages/logger
'@pnpm/logger': 1001.0.0
'@pnpm/store.cafs': 1000.0.13
'@pnpm/symlink-dependency': 1000.0.9(@pnpm/logger@1001.0.0)
'@rushstack/worker-pool': 0.4.9(@types/node@22.15.30)