feat: support allowing the build of specific versions of dependencies (#10104)

close #10076
This commit is contained in:
Zoltan Kochan
2025-10-21 12:38:16 +02:00
committed by GitHub
parent 7c1382f7b7
commit dee39ecb8a
47 changed files with 608 additions and 185 deletions

View File

@@ -0,0 +1,5 @@
---
"@pnpm/builder.policy": major
---
Sync version with pnpm CLI.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/config.version-policy": major
---
Initial release.

View File

@@ -0,0 +1,19 @@
---
"@pnpm/plugin-commands-rebuild": minor
"@pnpm/headless": minor
"@pnpm/deps.graph-builder": minor
"@pnpm/build-modules": minor
"@pnpm/core": minor
"@pnpm/builder.policy": minor
"@pnpm/types": minor
---
You can now allow specific versions of dependencies to run postinstall scripts. `onlyBuiltDependencies` now accepts package names with lists of trusted versions. For example:
```yaml
onlyBuiltDependencies:
- nx@21.6.4 || 21.6.5
- esbuild@0.25.1
```
Related PR: [#10104](https://github.com/pnpm/pnpm/pull/10104).

View File

@@ -2,4 +2,4 @@
"@pnpm/matcher": minor
---
Implemented `createPackageVersionPolicy` function.
Export Matcher and MatcherWithIndex.

13
builder/policy/README.md Normal file
View File

@@ -0,0 +1,13 @@
# @pnpm/builder.policy
> Create a function for filtering out dependencies that are not allowed to be built
## Install
```
pnpm add @pnpm/builder.policy
```
## License
[MIT](LICENSE)

View File

@@ -0,0 +1,46 @@
{
"name": "@pnpm/builder.policy",
"version": "1000.0.0-0",
"description": "Create a function for filtering out dependencies that are not allowed to be built",
"keywords": [
"pnpm",
"pnpm10"
],
"license": "MIT",
"funding": "https://opencollective.com/pnpm",
"repository": "https://github.com/pnpm/pnpm/blob/main/builder/policy",
"homepage": "https://github.com/pnpm/pnpm/blob/main/builder/policy#readme",
"bugs": {
"url": "https://github.com/pnpm/pnpm/issues"
},
"type": "commonjs",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"exports": {
".": "./lib/index.js"
},
"files": [
"lib",
"!*.map"
],
"scripts": {
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
"_test": "jest",
"test": "pnpm run compile && pnpm run _test",
"prepublishOnly": "pnpm run compile",
"compile": "tsc --build && pnpm run lint --fix"
},
"dependencies": {
"@pnpm/config.version-policy": "workspace:*",
"@pnpm/types": "workspace:*"
},
"devDependencies": {
"@pnpm/builder.policy": "workspace:*"
},
"engines": {
"node": ">=18.12"
},
"jest": {
"preset": "@pnpm/jest-config"
}
}

View File

@@ -0,0 +1,25 @@
import { type AllowBuild } from '@pnpm/types'
import { expandPackageVersionSpecs } from '@pnpm/config.version-policy'
import fs from 'fs'
export function createAllowBuildFunction (
opts: {
neverBuiltDependencies?: string[]
onlyBuiltDependencies?: string[]
onlyBuiltDependenciesFile?: string
}
): undefined | AllowBuild {
if (opts.onlyBuiltDependenciesFile != null || opts.onlyBuiltDependencies != null) {
const onlyBuiltDeps = opts.onlyBuiltDependencies ?? []
if (opts.onlyBuiltDependenciesFile) {
onlyBuiltDeps.push(...JSON.parse(fs.readFileSync(opts.onlyBuiltDependenciesFile, 'utf8')))
}
const onlyBuiltDependencies = expandPackageVersionSpecs(onlyBuiltDeps)
return (pkgName, version) => onlyBuiltDependencies.has(pkgName) || onlyBuiltDependencies.has(`${pkgName}@${version}`)
}
if (opts.neverBuiltDependencies != null && opts.neverBuiltDependencies.length > 0) {
const neverBuiltDependencies = new Set(opts.neverBuiltDependencies)
return (pkgName) => !neverBuiltDependencies.has(pkgName)
}
return undefined
}

View File

@@ -0,0 +1,57 @@
import path from 'path'
import { createAllowBuildFunction } from '@pnpm/builder.policy'
it('should neverBuiltDependencies', () => {
const allowBuild = createAllowBuildFunction({
neverBuiltDependencies: ['foo'],
})
expect(typeof allowBuild).toBe('function')
expect(allowBuild!('foo', '1.0.0')).toBeFalsy()
expect(allowBuild!('bar', '1.0.0')).toBeTruthy()
})
it('should onlyBuiltDependencies', () => {
const allowBuild = createAllowBuildFunction({
onlyBuiltDependencies: ['foo', 'qar@1.0.0 || 2.0.0'],
})
expect(typeof allowBuild).toBe('function')
expect(allowBuild!('foo', '1.0.0')).toBeTruthy()
expect(allowBuild!('bar', '1.0.0')).toBeFalsy()
expect(allowBuild!('qar', '1.1.0')).toBeFalsy()
expect(allowBuild!('qar', '1.0.0')).toBeTruthy()
expect(allowBuild!('qar', '2.0.0')).toBeTruthy()
})
it('should not allow patterns in onlyBuiltDependencies', () => {
const allowBuild = createAllowBuildFunction({
onlyBuiltDependencies: ['is-*'],
})
expect(typeof allowBuild).toBe('function')
expect(allowBuild!('is-odd', '1.0.0')).toBeFalsy()
})
it('should onlyBuiltDependencies set via a file', () => {
const allowBuild = createAllowBuildFunction({
onlyBuiltDependenciesFile: path.join(__dirname, 'onlyBuild.json'),
})
expect(typeof allowBuild).toBe('function')
expect(allowBuild!('zoo', '1.0.0')).toBeTruthy()
expect(allowBuild!('qar', '1.0.0')).toBeTruthy()
expect(allowBuild!('bar', '1.0.0')).toBeFalsy()
})
it('should onlyBuiltDependencies set via a file and config', () => {
const allowBuild = createAllowBuildFunction({
onlyBuiltDependencies: ['bar'],
onlyBuiltDependenciesFile: path.join(__dirname, 'onlyBuild.json'),
})
expect(typeof allowBuild).toBe('function')
expect(allowBuild!('zoo', '1.0.0')).toBeTruthy()
expect(allowBuild!('qar', '1.0.0')).toBeTruthy()
expect(allowBuild!('bar', '1.0.0')).toBeTruthy()
expect(allowBuild!('esbuild', '1.0.0')).toBeFalsy()
})
it('should return undefined if no policy is set', () => {
expect(createAllowBuildFunction({})).toBeUndefined()
})

View File

@@ -0,0 +1,5 @@
[
"zoo",
"qar"
]

View File

@@ -0,0 +1,17 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": false,
"outDir": "../test.lib",
"rootDir": "."
},
"include": [
"**/*.ts",
"../../../__typings__/**/*.d.ts"
],
"references": [
{
"path": ".."
}
]
}

View File

@@ -0,0 +1,19 @@
{
"extends": "@pnpm/tsconfig",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
},
"include": [
"src/**/*.ts",
"../../__typings__/**/*.d.ts"
],
"references": [
{
"path": "../../config/version-policy"
},
{
"path": "../../packages/types"
}
]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": [
"src/**/*.ts",
"test/**/*.ts",
"../../__typings__/**/*.d.ts"
]
}

View File

@@ -34,14 +34,10 @@
"compile": "tsc --build && pnpm run lint --fix"
},
"dependencies": {
"@pnpm/error": "workspace:*",
"@pnpm/types": "workspace:*",
"escape-string-regexp": "catalog:",
"semver": "catalog:"
"escape-string-regexp": "catalog:"
},
"devDependencies": {
"@pnpm/matcher": "workspace:*",
"@types/semver": "catalog:"
"@pnpm/matcher": "workspace:*"
},
"engines": {
"node": ">=18.12"

View File

@@ -1,10 +1,7 @@
import { PnpmError } from '@pnpm/error'
import { type PackageVersionPolicy } from '@pnpm/types'
import escapeStringRegexp from 'escape-string-regexp'
import semver from 'semver'
type Matcher = (input: string) => boolean
type MatcherWithIndex = (input: string) => number
export type Matcher = (input: string) => boolean
export type MatcherWithIndex = (input: string) => number
export function createMatcher (patterns: string[] | string): Matcher {
const m = createMatcherWithIndex(Array.isArray(patterns) ? patterns : [patterns])
@@ -99,65 +96,3 @@ function matcherWhenOnlyOnePattern (pattern: string): Matcher {
const m = matcherFromPattern(ignorePattern)
return (input) => !m(input)
}
export function createPackageVersionPolicy (patterns: string[]): PackageVersionPolicy {
const rules = patterns.map(parseVersionPolicyRule)
return evaluateVersionPolicy.bind(null, rules)
}
function evaluateVersionPolicy (rules: VersionPolicyRule[], pkgName: string): boolean | string[] {
for (const { nameMatcher, exactVersions } of rules) {
if (!nameMatcher(pkgName)) {
continue
}
if (exactVersions.length === 0) {
return true
}
return exactVersions
}
return false
}
interface VersionPolicyRule {
nameMatcher: Matcher
exactVersions: string[]
}
function parseVersionPolicyRule (pattern: string): VersionPolicyRule {
const isScoped = pattern.startsWith('@')
const atIndex = isScoped ? pattern.indexOf('@', 1) : pattern.indexOf('@')
if (atIndex === -1) {
return { nameMatcher: createMatcher(pattern), exactVersions: [] }
}
const packageName = pattern.slice(0, atIndex)
const versionsPart = pattern.slice(atIndex + 1)
// Parse versions separated by ||
const exactVersions: string[] | null = parseExactVersionsUnion(versionsPart)
if (exactVersions == null) {
throw new PnpmError('INVALID_VERSION_UNION',
`Invalid versions union. Found: "${pattern}". Use exact versions only.`)
}
if (packageName.includes('*')) {
throw new PnpmError('NAME_PATTERN_IN_VERSION_UNION', `Name patterns are not allowed with version unions. Found: "${pattern}"`)
}
return {
nameMatcher: (pkgName: string) => pkgName === packageName,
exactVersions,
}
}
function parseExactVersionsUnion (versionsStr: string): string[] | null {
const versions: string[] = []
for (const versionRaw of versionsStr.split('||')) {
const version = semver.valid(versionRaw)
if (version == null) {
return null
}
versions.push(version)
}
return versions
}

View File

@@ -1,4 +1,4 @@
import { createMatcher, createMatcherWithIndex, createPackageVersionPolicy } from '@pnpm/matcher'
import { createMatcher, createMatcherWithIndex } from '@pnpm/matcher'
test('matcher()', () => {
{
@@ -111,52 +111,3 @@ test('createMatcherWithIndex()', () => {
expect(match('baz')).toBe(-1)
}
})
test('createPackageVersionPolicy()', () => {
{
const match = createPackageVersionPolicy(['axios@1.12.2'])
expect(match('axios')).toStrictEqual(['1.12.2'])
}
{
const match = createPackageVersionPolicy(['is-*'])
expect(match('is-odd')).toBe(true)
expect(match('is-even')).toBe(true)
expect(match('lodash')).toBe(false)
}
{
const match = createPackageVersionPolicy(['@babel/core@7.20.0'])
expect(match('@babel/core')).toStrictEqual(['7.20.0'])
}
{
const match = createPackageVersionPolicy(['@babel/core'])
expect(match('@babel/core')).toBe(true)
}
{
const match = createPackageVersionPolicy(['axios@1.12.2'])
expect(match('is-odd')).toBe(false)
}
{
const match = createPackageVersionPolicy(['axios@1.12.2', 'lodash@4.17.21', 'is-*'])
expect(match('axios')).toStrictEqual(['1.12.2'])
expect(match('lodash')).toStrictEqual(['4.17.21'])
expect(match('is-odd')).toBe(true)
}
{
expect(() => createPackageVersionPolicy(['lodash@^4.17.0'])).toThrow(/Invalid versions union/)
expect(() => createPackageVersionPolicy(['lodash@~4.17.0'])).toThrow(/Invalid versions union/)
expect(() => createPackageVersionPolicy(['react@>=18.0.0'])).toThrow(/Invalid versions union/)
expect(() => createPackageVersionPolicy(['is-*@1.0.0'])).toThrow(/Name patterns are not allowed/)
}
{
const match = createPackageVersionPolicy(['axios@1.12.0 || 1.12.1'])
expect(match('axios')).toStrictEqual(['1.12.0', '1.12.1'])
}
{
const match = createPackageVersionPolicy(['@scope/pkg@1.0.0 || 1.0.1'])
expect(match('@scope/pkg')).toStrictEqual(['1.0.0', '1.0.1'])
}
{
const match = createPackageVersionPolicy(['pkg@1.0.0||1.0.1 || 1.0.2'])
expect(match('pkg')).toStrictEqual(['1.0.0', '1.0.1', '1.0.2'])
}
})

View File

@@ -8,12 +8,5 @@
"src/**/*.ts",
"../../__typings__/**/*.d.ts"
],
"references": [
{
"path": "../../packages/error"
},
{
"path": "../../packages/types"
}
]
"references": []
}

View File

@@ -0,0 +1,13 @@
# @pnpm/config.version-policy
> Parses and evaluates package version policy specs and produces package-version matchers
## Install
```
pnpm add @pnpm/config.version-policy
```
## License
[MIT](LICENSE)

View File

@@ -0,0 +1,49 @@
{
"name": "@pnpm/config.version-policy",
"version": "1000.0.0-0",
"description": "Parses and evaluates package version policy specs and produces package-version matchers",
"keywords": [
"pnpm",
"pnpm10"
],
"license": "MIT",
"funding": "https://opencollective.com/pnpm",
"repository": "https://github.com/pnpm/pnpm/blob/main/config/version-policy",
"homepage": "https://github.com/pnpm/pnpm/blob/main/config/version-policy#readme",
"bugs": {
"url": "https://github.com/pnpm/pnpm/issues"
},
"type": "commonjs",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"exports": {
".": "./lib/index.js"
},
"files": [
"lib",
"!*.map"
],
"scripts": {
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
"_test": "jest",
"test": "pnpm run compile && pnpm run _test",
"prepublishOnly": "pnpm run compile",
"compile": "tsc --build && pnpm run lint --fix"
},
"dependencies": {
"@pnpm/error": "workspace:*",
"@pnpm/matcher": "workspace:*",
"@pnpm/types": "workspace:*",
"semver": "catalog:"
},
"devDependencies": {
"@pnpm/config.version-policy": "workspace:*",
"@types/semver": "catalog:"
},
"engines": {
"node": ">=18.12"
},
"jest": {
"preset": "@pnpm/jest-config"
}
}

View File

@@ -0,0 +1,90 @@
import { PnpmError } from '@pnpm/error'
import { createMatcher, type Matcher } from '@pnpm/matcher'
import { type PackageVersionPolicy } from '@pnpm/types'
import semver from 'semver'
export function createPackageVersionPolicy (patterns: string[]): PackageVersionPolicy {
const rules: VersionPolicyRule[] = []
for (const pattern of patterns) {
const parsed = parseVersionPolicyRule(pattern)
rules.push({ nameMatcher: createMatcher(parsed.packageName), exactVersions: parsed.exactVersions })
}
return evaluateVersionPolicy.bind(null, rules)
}
export function expandPackageVersionSpecs (specs: string[]): Set<string> {
const expandedSpecs = new Set<string>()
for (const spec of specs) {
const parsed = parseVersionPolicyRule(spec)
if (parsed.exactVersions.length === 0) {
expandedSpecs.add(parsed.packageName)
} else {
for (const version of parsed.exactVersions) {
expandedSpecs.add(`${parsed.packageName}@${version}`)
}
}
}
return expandedSpecs
}
function evaluateVersionPolicy (rules: VersionPolicyRule[], pkgName: string): boolean | string[] {
for (const { nameMatcher, exactVersions } of rules) {
if (!nameMatcher(pkgName)) {
continue
}
if (exactVersions.length === 0) {
return true
}
return exactVersions
}
return false
}
interface VersionPolicyRule {
nameMatcher: Matcher
exactVersions: string[]
}
interface ParsedVersionPolicyRule {
packageName: string
exactVersions: string[]
}
function parseVersionPolicyRule (pattern: string): ParsedVersionPolicyRule {
const isScoped = pattern.startsWith('@')
const atIndex = isScoped ? pattern.indexOf('@', 1) : pattern.indexOf('@')
if (atIndex === -1) {
return { packageName: pattern, exactVersions: [] }
}
const packageName = pattern.slice(0, atIndex)
const versionsPart = pattern.slice(atIndex + 1)
// Parse versions separated by ||
const exactVersions: string[] | null = parseExactVersionsUnion(versionsPart)
if (exactVersions == null) {
throw new PnpmError('INVALID_VERSION_UNION',
`Invalid versions union. Found: "${pattern}". Use exact versions only.`)
}
if (packageName.includes('*')) {
throw new PnpmError('NAME_PATTERN_IN_VERSION_UNION', `Name patterns are not allowed with version unions. Found: "${pattern}"`)
}
return {
packageName,
exactVersions,
}
}
function parseExactVersionsUnion (versionsStr: string): string[] | null {
const versions: string[] = []
for (const versionRaw of versionsStr.split('||')) {
const version = semver.valid(versionRaw)
if (version == null) {
return null
}
versions.push(version)
}
return versions
}

View File

@@ -0,0 +1,50 @@
import { createPackageVersionPolicy } from '@pnpm/config.version-policy'
test('createPackageVersionPolicy()', () => {
{
const match = createPackageVersionPolicy(['axios@1.12.2'])
expect(match('axios')).toStrictEqual(['1.12.2'])
}
{
const match = createPackageVersionPolicy(['is-*'])
expect(match('is-odd')).toBe(true)
expect(match('is-even')).toBe(true)
expect(match('lodash')).toBe(false)
}
{
const match = createPackageVersionPolicy(['@babel/core@7.20.0'])
expect(match('@babel/core')).toStrictEqual(['7.20.0'])
}
{
const match = createPackageVersionPolicy(['@babel/core'])
expect(match('@babel/core')).toBe(true)
}
{
const match = createPackageVersionPolicy(['axios@1.12.2'])
expect(match('is-odd')).toBe(false)
}
{
const match = createPackageVersionPolicy(['axios@1.12.2', 'lodash@4.17.21', 'is-*'])
expect(match('axios')).toStrictEqual(['1.12.2'])
expect(match('lodash')).toStrictEqual(['4.17.21'])
expect(match('is-odd')).toBe(true)
}
{
expect(() => createPackageVersionPolicy(['lodash@^4.17.0'])).toThrow(/Invalid versions union/)
expect(() => createPackageVersionPolicy(['lodash@~4.17.0'])).toThrow(/Invalid versions union/)
expect(() => createPackageVersionPolicy(['react@>=18.0.0'])).toThrow(/Invalid versions union/)
expect(() => createPackageVersionPolicy(['is-*@1.0.0'])).toThrow(/Name patterns are not allowed/)
}
{
const match = createPackageVersionPolicy(['axios@1.12.0 || 1.12.1'])
expect(match('axios')).toStrictEqual(['1.12.0', '1.12.1'])
}
{
const match = createPackageVersionPolicy(['@scope/pkg@1.0.0 || 1.0.1'])
expect(match('@scope/pkg')).toStrictEqual(['1.0.0', '1.0.1'])
}
{
const match = createPackageVersionPolicy(['pkg@1.0.0||1.0.1 || 1.0.2'])
expect(match('pkg')).toStrictEqual(['1.0.0', '1.0.1', '1.0.2'])
}
})

View File

@@ -0,0 +1,17 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": false,
"outDir": "../test.lib",
"rootDir": "."
},
"include": [
"**/*.ts",
"../../../__typings__/**/*.d.ts"
],
"references": [
{
"path": ".."
}
]
}

View File

@@ -0,0 +1,22 @@
{
"extends": "@pnpm/tsconfig",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
},
"include": [
"src/**/*.ts",
"../../__typings__/**/*.d.ts"
],
"references": [
{
"path": "../../packages/error"
},
{
"path": "../../packages/types"
},
{
"path": "../matcher"
}
]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": [
"src/**/*.ts",
"test/**/*.ts",
"../../__typings__/**/*.d.ts"
]
}

View File

@@ -32,6 +32,7 @@ export interface DependenciesGraphNode {
hasBundledDependencies: boolean
modules: string
name: string
version: string
fetching?: () => Promise<PkgRequestFetchResult>
forceImportPackage?: boolean // Used to force re-imports from the store of local tarballs that have changed.
dir: string
@@ -255,6 +256,7 @@ async function buildGraphFromPackages (
hasBundledDependencies: pkgSnapshot.bundledDependencies != null,
modules,
name: pkgName,
version: pkgVersion,
optional: !!pkgSnapshot.optional,
optionalDependencies: new Set(Object.keys(pkgSnapshot.optionalDependencies ?? {})),
patch: _getPatchInfo(pkgName, pkgVersion),

View File

@@ -8,6 +8,7 @@ export interface DependenciesGraphNode<T extends string> {
depPath: DepPath
pkgIdWithPatchHash: PkgIdWithPatchHash
name: string
version: string
dir: string
fetchingBundledManifest?: () => Promise<PackageManifest | undefined>
filesIndexFile?: string

View File

@@ -11,7 +11,7 @@ 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 DependencyManifest } from '@pnpm/types'
import { type AllowBuild, type DependencyManifest } from '@pnpm/types'
import pDefer, { type DeferredPromise } from 'p-defer'
import pickBy from 'ramda/src/pickBy'
import runGroups from 'run-groups'
@@ -23,7 +23,7 @@ export async function buildModules<T extends string> (
depGraph: DependenciesGraph<T>,
rootDepPaths: T[],
opts: {
allowBuild?: (pkgName: string) => boolean
allowBuild?: AllowBuild
ignorePatchFailures?: boolean
ignoredBuiltDependencies?: string[]
childConcurrency?: number
@@ -76,7 +76,7 @@ export async function buildModules<T extends string> (
() => {
let ignoreScripts = Boolean(buildDepOpts.ignoreScripts)
if (!ignoreScripts) {
if (depGraph[depPath].requiresBuild && !allowBuild(depGraph[depPath].name)) {
if (depGraph[depPath].requiresBuild && !allowBuild(depGraph[depPath].name, depGraph[depPath].version)) {
ignoredPkgs.add(depGraph[depPath].name)
ignoreScripts = true
}

View File

@@ -32,7 +32,7 @@
"compile": "tsc --build && pnpm run lint --fix"
},
"dependencies": {
"@pnpm/builder.policy": "catalog:",
"@pnpm/builder.policy": "workspace:*",
"@pnpm/calc-dep-state": "workspace:*",
"@pnpm/cli-utils": "workspace:*",
"@pnpm/common-cli-options-help": "workspace:*",

View File

@@ -308,8 +308,8 @@ async function _rebuild (
const ignoredPkgs: string[] = []
const _allowBuild = createAllowBuildFunction(opts) ?? (() => true)
const allowBuild = (pkgName: string) => {
if (_allowBuild(pkgName)) return true
const allowBuild = (pkgName: string, version: string) => {
if (_allowBuild(pkgName, version)) return true
if (!opts.ignoredBuiltDependencies?.includes(pkgName)) {
ignoredPkgs.push(pkgName)
}
@@ -367,7 +367,7 @@ async function _rebuild (
requiresBuild = pkgRequiresBuild(pgkManifest, {})
}
const hasSideEffects = requiresBuild && allowBuild(pkgInfo.name) && await runPostinstallHooks({
const hasSideEffects = requiresBuild && allowBuild(pkgInfo.name, pkgInfo.version) && await runPostinstallHooks({
depPath,
extraBinPaths,
extraEnv: opts.extraEnv,

View File

@@ -21,6 +21,9 @@
{
"path": "../../__utils__/test-ipc-server"
},
{
"path": "../../builder/policy"
},
{
"path": "../../cli/cli-utils"
},

View File

@@ -1 +1,3 @@
export type PackageVersionPolicy = (pkgName: string) => boolean | string[]
export type AllowBuild = (pkgName: string, pkgVersion: string) => boolean

View File

@@ -57,7 +57,7 @@
},
"dependencies": {
"@pnpm/build-modules": "workspace:*",
"@pnpm/builder.policy": "catalog:",
"@pnpm/builder.policy": "workspace:*",
"@pnpm/calc-dep-state": "workspace:*",
"@pnpm/catalogs.protocol-parser": "workspace:*",
"@pnpm/catalogs.resolver": "workspace:*",

View File

@@ -24,6 +24,7 @@ import {
import { type StoreController, type TarballResolution } from '@pnpm/store-controller-types'
import { symlinkDependency } from '@pnpm/symlink-dependency'
import {
type AllowBuild,
type DepPath,
type HoistedDependencies,
type Registries,
@@ -43,7 +44,7 @@ import { type ImporterToUpdate } from './index.js'
const brokenModulesLogger = logger('_broken_node_modules')
export interface LinkPackagesOptions {
allowBuild?: (pkgName: string) => boolean
allowBuild?: AllowBuild
currentLockfile: LockfileObject
dedupeDirectDeps: boolean
dependenciesByProjectId: Record<string, Map<string, DepPath>>
@@ -318,7 +319,7 @@ function resolvePath (where: string, spec: string): string {
}
interface LinkNewPackagesOptions {
allowBuild?: (pkgName: string) => boolean
allowBuild?: AllowBuild
depsStateCache: DepsStateCache
disableRelinkLocalDirDeps?: boolean
force: boolean
@@ -451,7 +452,7 @@ async function linkAllPkgs (
storeController: StoreController,
depNodes: DependenciesGraphNode[],
opts: {
allowBuild?: (pkgName: string) => boolean
allowBuild?: AllowBuild
depGraph: DependenciesGraph
depsStateCache: DepsStateCache
disableRelinkLocalDirDeps?: boolean
@@ -468,7 +469,7 @@ async function linkAllPkgs (
depNode.requiresBuild = files.requiresBuild
let sideEffectsCacheKey: string | undefined
if (opts.sideEffectsCacheRead && files.sideEffects && !isEmpty(files.sideEffects)) {
if (opts?.allowBuild?.(depNode.name) !== false) {
if (opts?.allowBuild?.(depNode.name, depNode.version) !== false) {
sideEffectsCacheKey = calcDepState(opts.depGraph, opts.depsStateCache, depNode.depPath, {
includeDepGraphHash: !opts.ignoreScripts && depNode.requiresBuild, // true when is built
patchFileHash: depNode.patch?.file.hash,

View File

@@ -510,6 +510,47 @@ test('selectively allow scripts in some dependencies by onlyBuiltDependencies',
}
})
test('selectively allow scripts in some dependencies by onlyBuiltDependencies using exact versions', async () => {
prepareEmpty()
const reporter = sinon.spy()
const onlyBuiltDependencies = ['@pnpm.e2e/install-script-example@1.0.0']
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, neverBuiltDependencies, reporter })
)
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()
{
const ignoredPkgsLog = reporter.getCalls().find((call) => call.firstArg.name === 'pnpm:ignored-scripts')?.firstArg
expect(ignoredPkgsLog.packageNames).toStrictEqual(['@pnpm.e2e/pre-and-postinstall-scripts-example'])
}
reporter.resetHistory()
rimraf('node_modules')
await install(manifest, testDefaults({
fastUnpack: false,
frozenLockfile: true,
ignoredBuiltDependencies: ['@pnpm.e2e/pre-and-postinstall-scripts-example'],
neverBuiltDependencies,
onlyBuiltDependencies,
reporter,
}))
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()
{
const ignoredPkgsLog = reporter.getCalls().find((call) => call.firstArg.name === 'pnpm:ignored-scripts')?.firstArg
expect(ignoredPkgsLog.packageNames).toStrictEqual([])
}
})
test('lifecycle scripts have access to package\'s own binary by binary name', async () => {
const project = prepareEmpty()
await addDependenciesToPackage({},

View File

@@ -24,6 +24,9 @@
{
"path": "../../__utils__/test-ipc-server"
},
{
"path": "../../builder/policy"
},
{
"path": "../../catalogs/protocol-parser"
},

View File

@@ -40,7 +40,7 @@
},
"dependencies": {
"@pnpm/build-modules": "workspace:*",
"@pnpm/builder.policy": "catalog:",
"@pnpm/builder.policy": "workspace:*",
"@pnpm/calc-dep-state": "workspace:*",
"@pnpm/constants": "workspace:*",
"@pnpm/core-loggers": "workspace:*",

View File

@@ -59,6 +59,7 @@ import {
} from '@pnpm/store-controller-types'
import { symlinkDependency } from '@pnpm/symlink-dependency'
import {
type AllowBuild,
type DepPath,
type DependencyManifest,
type HoistedDependencies,
@@ -853,7 +854,7 @@ async function linkAllPkgs (
storeController: StoreController,
depNodes: DependenciesGraphNode[],
opts: {
allowBuild?: (pkgName: string) => boolean
allowBuild?: AllowBuild
depGraph: DependenciesGraph
depsStateCache: DepsStateCache
disableRelinkLocalDirDeps?: boolean
@@ -877,7 +878,7 @@ async function linkAllPkgs (
depNode.requiresBuild = filesResponse.requiresBuild
let sideEffectsCacheKey: string | undefined
if (opts.sideEffectsCacheRead && filesResponse.sideEffects && !isEmpty(filesResponse.sideEffects)) {
if (opts?.allowBuild?.(depNode.name) !== false) {
if (opts?.allowBuild?.(depNode.name, depNode.version) !== false) {
sideEffectsCacheKey = calcDepState(opts.depGraph, opts.depsStateCache, depNode.dir, {
includeDepGraphHash: !opts.ignoreScripts && depNode.requiresBuild, // true when is built
patchFileHash: depNode.patch?.file.hash,

View File

@@ -19,6 +19,7 @@ import pLimit from 'p-limit'
import difference from 'ramda/src/difference'
import isEmpty from 'ramda/src/isEmpty'
import rimraf from '@zkochan/rimraf'
import { type AllowBuild } from '@pnpm/types'
const limitLinking = pLimit(16)
@@ -28,7 +29,7 @@ export async function linkHoistedModules (
prevGraph: DependenciesGraph,
hierarchy: DepHierarchy,
opts: {
allowBuild?: (pkgName: string) => boolean
allowBuild?: AllowBuild
depsStateCache: DepsStateCache
disableRelinkLocalDirDeps?: boolean
force: boolean
@@ -88,7 +89,7 @@ async function linkAllPkgsInOrder (
hierarchy: DepHierarchy,
parentDir: string,
opts: {
allowBuild?: (pkgName: string) => boolean
allowBuild?: AllowBuild
depsStateCache: DepsStateCache
disableRelinkLocalDirDeps?: boolean
force: boolean
@@ -115,7 +116,7 @@ async function linkAllPkgsInOrder (
depNode.requiresBuild = filesResponse.requiresBuild
let sideEffectsCacheKey: string | undefined
if (opts.sideEffectsCacheRead && filesResponse.sideEffects && !isEmpty(filesResponse.sideEffects)) {
if (opts?.allowBuild?.(depNode.name) !== false) {
if (opts?.allowBuild?.(depNode.name, depNode.version) !== false) {
sideEffectsCacheKey = _calcDepState(dir, {
includeDepGraphHash: !opts.ignoreScripts && depNode.requiresBuild, // true when is built
patchFileHash: depNode.patch?.file.hash,

View File

@@ -249,6 +249,7 @@ async function fetchDeps (
hasBundledDependencies: pkgSnapshot.bundledDependencies != null,
modules,
name: pkgName,
version: pkgVersion,
optional: !!pkgSnapshot.optional,
optionalDependencies: new Set(Object.keys(pkgSnapshot.optionalDependencies ?? {})),
patch: getPatchInfo(opts.patchedDependencies, pkgName, pkgVersion),

View File

@@ -21,6 +21,9 @@
{
"path": "../../__utils__/test-ipc-server"
},
{
"path": "../../builder/policy"
},
{
"path": "../../config/package-is-installable"
},

View File

@@ -35,6 +35,7 @@
"@pnpm/calc-dep-state": "workspace:*",
"@pnpm/catalogs.resolver": "workspace:*",
"@pnpm/catalogs.types": "workspace:*",
"@pnpm/config.version-policy": "workspace:*",
"@pnpm/constants": "workspace:*",
"@pnpm/core-loggers": "workspace:*",
"@pnpm/dependency-path": "workspace:*",
@@ -44,7 +45,6 @@
"@pnpm/lockfile.types": "workspace:*",
"@pnpm/lockfile.utils": "workspace:*",
"@pnpm/manifest-utils": "workspace:*",
"@pnpm/matcher": "workspace:*",
"@pnpm/npm-resolver": "workspace:*",
"@pnpm/patching.config": "workspace:*",
"@pnpm/patching.types": "workspace:*",

View File

@@ -3,7 +3,7 @@ import { resolveFromCatalog } from '@pnpm/catalogs.resolver'
import { type Catalogs } from '@pnpm/catalogs.types'
import { type LockfileObject } from '@pnpm/lockfile.types'
import { globalWarn } from '@pnpm/logger'
import { createPackageVersionPolicy } from '@pnpm/matcher'
import { createPackageVersionPolicy } from '@pnpm/config.version-policy'
import { type PatchGroupRecord } from '@pnpm/patching.config'
import { type PreferredVersions, type Resolution, type WorkspacePackages } from '@pnpm/resolver-base'
import { type StoreController } from '@pnpm/store-controller-types'

View File

@@ -16,7 +16,7 @@
"path": "../../catalogs/types"
},
{
"path": "../../config/matcher"
"path": "../../config/version-policy"
},
{
"path": "../../fetching/pick-fetcher"

77
pnpm-lock.yaml generated
View File

@@ -39,9 +39,6 @@ catalogs:
'@jest/globals':
specifier: 29.7.0
version: 29.7.0
'@pnpm/builder.policy':
specifier: 3.0.1
version: 3.0.1
'@pnpm/byline':
specifier: ^1.0.0
version: 1.0.0
@@ -1230,6 +1227,19 @@ importers:
specifier: workspace:*
version: 'link:'
builder/policy:
dependencies:
'@pnpm/config.version-policy':
specifier: workspace:*
version: link:../../config/version-policy
'@pnpm/types':
specifier: workspace:*
version: link:../../packages/types
devDependencies:
'@pnpm/builder.policy':
specifier: workspace:*
version: 'link:'
cache/api:
dependencies:
'@pnpm/config':
@@ -1803,25 +1813,13 @@ importers:
config/matcher:
dependencies:
'@pnpm/error':
specifier: workspace:*
version: link:../../packages/error
'@pnpm/types':
specifier: workspace:*
version: link:../../packages/types
escape-string-regexp:
specifier: 'catalog:'
version: 4.0.0
semver:
specifier: 'catalog:'
version: 7.7.1
devDependencies:
'@pnpm/matcher':
specifier: workspace:*
version: 'link:'
'@types/semver':
specifier: 'catalog:'
version: 7.5.3
config/normalize-registries:
dependencies:
@@ -1978,6 +1976,28 @@ importers:
specifier: 'catalog:'
version: 2.1.0
config/version-policy:
dependencies:
'@pnpm/error':
specifier: workspace:*
version: link:../../packages/error
'@pnpm/matcher':
specifier: workspace:*
version: link:../matcher
'@pnpm/types':
specifier: workspace:*
version: link:../../packages/types
semver:
specifier: 'catalog:'
version: 7.7.1
devDependencies:
'@pnpm/config.version-policy':
specifier: workspace:*
version: 'link:'
'@types/semver':
specifier: 'catalog:'
version: 7.5.3
crypto/hash:
dependencies:
'@pnpm/crypto.polyfill':
@@ -2654,8 +2674,8 @@ importers:
exec/plugin-commands-rebuild:
dependencies:
'@pnpm/builder.policy':
specifier: 'catalog:'
version: 3.0.1
specifier: workspace:*
version: link:../../builder/policy
'@pnpm/calc-dep-state':
specifier: workspace:*
version: link:../../packages/calc-dep-state
@@ -4799,8 +4819,8 @@ importers:
specifier: workspace:*
version: link:../../exec/build-modules
'@pnpm/builder.policy':
specifier: 'catalog:'
version: 3.0.1
specifier: workspace:*
version: link:../../builder/policy
'@pnpm/calc-dep-state':
specifier: workspace:*
version: link:../../packages/calc-dep-state
@@ -5162,8 +5182,8 @@ importers:
specifier: workspace:*
version: link:../../exec/build-modules
'@pnpm/builder.policy':
specifier: 'catalog:'
version: 3.0.1
specifier: workspace:*
version: link:../../builder/policy
'@pnpm/calc-dep-state':
specifier: workspace:*
version: link:../../packages/calc-dep-state
@@ -6036,6 +6056,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
@@ -6063,9 +6086,6 @@ importers:
'@pnpm/manifest-utils':
specifier: workspace:*
version: link:../../pkg-manifest/manifest-utils
'@pnpm/matcher':
specifier: workspace:*
version: link:../../config/matcher
'@pnpm/npm-resolver':
specifier: workspace:*
version: link:../../resolving/npm-resolver
@@ -7568,6 +7588,9 @@ importers:
'@pnpm/client':
specifier: workspace:*
version: link:../../pkg-manager/client
'@pnpm/config.version-policy':
specifier: workspace:*
version: link:../../config/version-policy
'@pnpm/constants':
specifier: workspace:*
version: link:../../packages/constants
@@ -9842,10 +9865,6 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
'@pnpm/builder.policy@3.0.1':
resolution: {integrity: sha512-CFmLQQs7qLM0KVe0f/ZyLqQwm2COdogJiLV5kXckf/7raN8ifsWOexxXgi6HDCm+8UUQvsjYDyjnWSNSQPCHUQ==}
engines: {node: '>=18.12'}
'@pnpm/byline@1.0.0':
resolution: {integrity: sha512-61tmh+k7hnKK6b2XbF4GvxmiaF3l2a+xQlZyeoOGBs7mXU3Ie8iCAeAnM0+r70KiqTrgWvBCjMeM+W3JarJqaQ==}
engines: {node: '>=12.17'}
@@ -17249,8 +17268,6 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
'@pnpm/builder.policy@3.0.1': {}
'@pnpm/byline@1.0.0': {}
'@pnpm/cafs-types@1000.0.0': {}

View File

@@ -4,6 +4,7 @@ packages:
- __typings__
- __utils__/*
- '!__utils__/build-artifacts'
- builder/*
- cache/*
- catalogs/*
- cli/*
@@ -62,7 +63,6 @@ catalog:
'@eslint/js': 9.9.1
'@gwhitney/detect-indent': 7.0.1
'@jest/globals': 29.7.0
'@pnpm/builder.policy': 3.0.1
'@pnpm/byline': ^1.0.0
'@pnpm/colorize-semver-diff': ^1.0.1
'@pnpm/config.env-replace': ^3.0.2

View File

@@ -35,6 +35,7 @@
"@pnpm/catalogs.resolver": "workspace:*",
"@pnpm/catalogs.types": "workspace:*",
"@pnpm/client": "workspace:*",
"@pnpm/config.version-policy": "workspace:*",
"@pnpm/constants": "workspace:*",
"@pnpm/dependency-path": "workspace:*",
"@pnpm/error": "workspace:*",

View File

@@ -3,7 +3,7 @@ import {
createResolver,
type ResolveFunction,
} from '@pnpm/client'
import { createPackageVersionPolicy } from '@pnpm/matcher'
import { createPackageVersionPolicy } from '@pnpm/config.version-policy'
import { type PackageVersionPolicy, type DependencyManifest } from '@pnpm/types'
interface GetManifestOpts {

View File

@@ -24,6 +24,9 @@
{
"path": "../../config/pick-registry-for-package"
},
{
"path": "../../config/version-policy"
},
{
"path": "../../hooks/read-package-hook"
},