mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-23 23:29:17 -05:00
feat: onlyBuiltDependencies (#4014)
close #4001 Co-authored-by: Jack Works <jackworks@protonmail.com>
This commit is contained in:
13
.changeset/dry-flowers-grab.md
Normal file
13
.changeset/dry-flowers-grab.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
A new setting is supported in the `pnpm` section of the `package.json` file [#4001](https://github.com/pnpm/pnpm/issues/4001). `onlyBuiltDependencies` is an array of package names that are allowed to be executed during installation. If this field exists, only mentioned packages will be able to run install scripts.
|
||||
|
||||
```json
|
||||
{
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": ["fsevents"]
|
||||
}
|
||||
}
|
||||
```
|
||||
8
.changeset/large-games-hug.md
Normal file
8
.changeset/large-games-hug.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@pnpm/core": minor
|
||||
"@pnpm/lockfile-file": minor
|
||||
"@pnpm/lockfile-types": minor
|
||||
"@pnpm/types": minor
|
||||
---
|
||||
|
||||
New optional field supported: `onlyBuiltDependencies`.
|
||||
5
.changeset/serious-frogs-yell.md
Normal file
5
.changeset/serious-frogs-yell.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/resolve-dependencies": major
|
||||
---
|
||||
|
||||
Removed the `neverBuiltDependencies` option. In order to ignore scripts of some dependencies, use the new `allowBuild`. `allowBuild` is a function that accepts the package name and returns `true` if the package should be allowed to build.
|
||||
@@ -50,6 +50,7 @@ export interface StrictInstallOptions {
|
||||
verifyStoreIntegrity: boolean
|
||||
engineStrict: boolean
|
||||
neverBuiltDependencies: string[]
|
||||
onlyBuiltDependencies?: string[]
|
||||
nodeExecPath?: string
|
||||
nodeLinker?: 'isolated' | 'hoisted' | 'pnp'
|
||||
nodeVersion: string
|
||||
@@ -182,6 +183,9 @@ export default async (
|
||||
}
|
||||
}
|
||||
}
|
||||
if (opts.onlyBuiltDependencies && opts.neverBuiltDependencies) {
|
||||
throw new PnpmError('CONFIG_CONFLICT_BUILT_DEPENDENCIES', 'Cannot have both neverBuiltDependencies and onlyBuiltDependencies')
|
||||
}
|
||||
const defaultOpts = await defaults(opts)
|
||||
const extendedOpts = {
|
||||
...defaultOpts,
|
||||
|
||||
@@ -223,14 +223,18 @@ export async function mutateModules (
|
||||
)
|
||||
}
|
||||
const packageExtensionsChecksum = isEmpty(opts.packageExtensions ?? {}) ? undefined : createObjectChecksum(opts.packageExtensions!)
|
||||
let needsFullResolution = !maybeOpts.ignorePackageManifest && (
|
||||
!equals(ctx.wantedLockfile.overrides ?? {}, opts.overrides ?? {}) ||
|
||||
!equals((ctx.wantedLockfile.neverBuiltDependencies ?? []).sort(), (opts.neverBuiltDependencies ?? []).sort()) ||
|
||||
ctx.wantedLockfile.packageExtensionsChecksum !== packageExtensionsChecksum) ||
|
||||
let needsFullResolution = !maybeOpts.ignorePackageManifest &&
|
||||
lockfileIsUpToDate(ctx.wantedLockfile, {
|
||||
overrides: opts.overrides,
|
||||
neverBuiltDependencies: opts.neverBuiltDependencies,
|
||||
onlyBuiltDependencies: opts.onlyBuiltDependencies,
|
||||
packageExtensionsChecksum,
|
||||
}) ||
|
||||
opts.fixLockfile
|
||||
if (needsFullResolution) {
|
||||
ctx.wantedLockfile.overrides = opts.overrides
|
||||
ctx.wantedLockfile.neverBuiltDependencies = opts.neverBuiltDependencies
|
||||
ctx.wantedLockfile.onlyBuiltDependencies = opts.onlyBuiltDependencies
|
||||
ctx.wantedLockfile.packageExtensionsChecksum = packageExtensionsChecksum
|
||||
}
|
||||
const frozenLockfile = opts.frozenLockfile ||
|
||||
@@ -467,6 +471,25 @@ export async function mutateModules (
|
||||
}
|
||||
}
|
||||
|
||||
function lockfileIsUpToDate (
|
||||
lockfile: Lockfile,
|
||||
{
|
||||
neverBuiltDependencies,
|
||||
onlyBuiltDependencies,
|
||||
overrides,
|
||||
packageExtensionsChecksum,
|
||||
}: {
|
||||
neverBuiltDependencies?: string[]
|
||||
onlyBuiltDependencies?: string[]
|
||||
overrides?: Record<string, string>
|
||||
packageExtensionsChecksum?: string
|
||||
}) {
|
||||
return !equals(lockfile.overrides ?? {}, overrides ?? {}) ||
|
||||
!equals((lockfile.neverBuiltDependencies ?? []).sort(), (neverBuiltDependencies ?? []).sort()) ||
|
||||
!equals(onlyBuiltDependencies?.sort(), lockfile.onlyBuiltDependencies) ||
|
||||
lockfile.packageExtensionsChecksum !== packageExtensionsChecksum
|
||||
}
|
||||
|
||||
export function createObjectChecksum (obj: Object) {
|
||||
const s = JSON.stringify(obj)
|
||||
return crypto.createHash('md5').update(s).digest('hex')
|
||||
@@ -608,7 +631,8 @@ type InstallFunction = (
|
||||
opts: StrictInstallOptions & {
|
||||
makePartialCurrentLockfile: boolean
|
||||
needsFullResolution: boolean
|
||||
neverBuiltDependencies: string[]
|
||||
neverBuiltDependencies?: string[]
|
||||
onlyBuiltDependencies?: string[]
|
||||
overrides?: Record<string, string>
|
||||
updateLockfileMinorVersion: boolean
|
||||
preferredVersions?: PreferredVersions
|
||||
@@ -705,6 +729,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
|
||||
} = await resolveDependencies(
|
||||
projects,
|
||||
{
|
||||
allowBuild: createAllowBuildFunction(opts),
|
||||
currentLockfile: ctx.currentLockfile,
|
||||
defaultUpdateDepth: (opts.update || (opts.updateMatching != null)) ? opts.depth : -1,
|
||||
dryRun: opts.lockfileOnly,
|
||||
@@ -714,7 +739,6 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
|
||||
hooks: opts.hooks,
|
||||
linkWorkspacePackagesDepth: opts.linkWorkspacePackagesDepth ?? (opts.saveWorkspaceProtocol ? 0 : -1),
|
||||
lockfileDir: opts.lockfileDir,
|
||||
neverBuiltDependencies: new Set(opts.neverBuiltDependencies),
|
||||
nodeVersion: opts.nodeVersion,
|
||||
pnpmVersion: opts.packageManager.name === 'pnpm' ? opts.packageManager.version : '',
|
||||
preferWorkspacePackages: opts.preferWorkspacePackages,
|
||||
@@ -989,6 +1013,22 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
|
||||
}
|
||||
}
|
||||
|
||||
function createAllowBuildFunction (
|
||||
opts: {
|
||||
neverBuiltDependencies?: string[]
|
||||
onlyBuiltDependencies?: string[]
|
||||
}
|
||||
): undefined | ((pkgName: string) => boolean) {
|
||||
if (opts.neverBuiltDependencies != null && opts.neverBuiltDependencies.length > 0) {
|
||||
const neverBuiltDependencies = new Set(opts.neverBuiltDependencies)
|
||||
return (pkgName) => !neverBuiltDependencies.has(pkgName)
|
||||
} else if (opts.onlyBuiltDependencies != null) {
|
||||
const onlyBuiltDependencies = new Set(opts.onlyBuiltDependencies)
|
||||
return (pkgName) => onlyBuiltDependencies.has(pkgName)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const installInContext: InstallFunction = async (projects, ctx, opts) => {
|
||||
try {
|
||||
if (opts.nodeLinker === 'hoisted' && !opts.lockfileOnly) {
|
||||
|
||||
@@ -443,7 +443,7 @@ test('scripts have access to unlisted bins when hoisting is used', async () => {
|
||||
expect(project.requireModule('pkg-that-calls-unlisted-dep-in-hooks/output.json')).toStrictEqual(['Hello world!'])
|
||||
})
|
||||
|
||||
test('selectively ignore scripts in some dependencies', async () => {
|
||||
test('selectively ignore scripts in some dependencies by neverBuiltDependencies', async () => {
|
||||
const project = prepareEmpty()
|
||||
const neverBuiltDependencies = ['pre-and-postinstall-scripts-example']
|
||||
const manifest = await addDependenciesToPackage({},
|
||||
@@ -469,6 +469,44 @@ test('selectively ignore scripts in some dependencies', async () => {
|
||||
expect(await exists('node_modules/install-script-example/generated-by-install.js')).toBeTruthy()
|
||||
})
|
||||
|
||||
test('throw an exception when both neverBuiltDependencies and onlyBuiltDependencies are used', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
await expect(
|
||||
addDependenciesToPackage(
|
||||
{},
|
||||
['pre-and-postinstall-scripts-example'],
|
||||
await testDefaults({ onlyBuiltDependencies: ['foo'], neverBuiltDependencies: ['bar'] })
|
||||
)
|
||||
).rejects.toThrow(/Cannot have both/)
|
||||
})
|
||||
|
||||
test('selectively allow scripts in some dependencies by onlyBuiltDependencies', async () => {
|
||||
const project = prepareEmpty()
|
||||
const onlyBuiltDependencies = ['install-script-example']
|
||||
const manifest = await addDependenciesToPackage({},
|
||||
['pre-and-postinstall-scripts-example', 'install-script-example'],
|
||||
await testDefaults({ fastUnpack: false, onlyBuiltDependencies })
|
||||
)
|
||||
|
||||
expect(await exists('node_modules/pre-and-postinstall-scripts-example/generated-by-preinstall.js')).toBeFalsy()
|
||||
expect(await exists('node_modules/pre-and-postinstall-scripts-example/generated-by-postinstall.js')).toBeFalsy()
|
||||
expect(await exists('node_modules/install-script-example/generated-by-install.js')).toBeTruthy()
|
||||
|
||||
const lockfile = await project.readLockfile()
|
||||
expect(lockfile.onlyBuiltDependencies).toStrictEqual(onlyBuiltDependencies)
|
||||
expect(lockfile.packages['/pre-and-postinstall-scripts-example/1.0.0'].requiresBuild).toBe(undefined)
|
||||
expect(lockfile.packages['/install-script-example/1.0.0'].requiresBuild).toBe(true)
|
||||
|
||||
await rimraf('node_modules')
|
||||
|
||||
await install(manifest, await testDefaults({ fastUnpack: false, frozenLockfile: true, onlyBuiltDependencies }))
|
||||
|
||||
expect(await exists('node_modules/pre-and-postinstall-scripts-example/generated-by-preinstall.js')).toBeFalsy()
|
||||
expect(await exists('node_modules/pre-and-postinstall-scripts-example/generated-by-postinstall.js')).toBeFalsy()
|
||||
expect(await exists('node_modules/install-script-example/generated-by-install.js')).toBeTruthy()
|
||||
})
|
||||
|
||||
test('lockfile is updated if neverBuiltDependencies is changed', async () => {
|
||||
const project = prepareEmpty()
|
||||
const manifest = await addDependenciesToPackage({},
|
||||
@@ -501,6 +539,55 @@ test('lockfile is updated if neverBuiltDependencies is changed', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('lockfile is updated if onlyBuiltDependencies is changed', async () => {
|
||||
const project = prepareEmpty()
|
||||
const manifest = await addDependenciesToPackage({},
|
||||
['pre-and-postinstall-scripts-example', 'install-script-example'],
|
||||
await testDefaults({ fastUnpack: false })
|
||||
)
|
||||
|
||||
{
|
||||
const lockfile = await project.readLockfile()
|
||||
expect(lockfile.onlyBuiltDependencies).toBeFalsy()
|
||||
expect(lockfile.packages['/pre-and-postinstall-scripts-example/1.0.0'].requiresBuild).toBeTruthy()
|
||||
expect(lockfile.packages['/install-script-example/1.0.0'].requiresBuild).toBeTruthy()
|
||||
}
|
||||
|
||||
const onlyBuiltDependencies: string[] = []
|
||||
await mutateModules([
|
||||
{
|
||||
buildIndex: 0,
|
||||
manifest,
|
||||
mutation: 'install',
|
||||
rootDir: process.cwd(),
|
||||
},
|
||||
], await testDefaults({ onlyBuiltDependencies }))
|
||||
|
||||
{
|
||||
const lockfile = await project.readLockfile()
|
||||
expect(lockfile.onlyBuiltDependencies).toStrictEqual(onlyBuiltDependencies)
|
||||
expect(lockfile.packages['/pre-and-postinstall-scripts-example/1.0.0'].requiresBuild).toBe(undefined)
|
||||
expect(lockfile.packages['/install-script-example/1.0.0'].requiresBuild).toBe(undefined)
|
||||
}
|
||||
|
||||
onlyBuiltDependencies.push('pre-and-postinstall-scripts-example')
|
||||
await mutateModules([
|
||||
{
|
||||
buildIndex: 0,
|
||||
manifest,
|
||||
mutation: 'install',
|
||||
rootDir: process.cwd(),
|
||||
},
|
||||
], await testDefaults({ onlyBuiltDependencies }))
|
||||
|
||||
{
|
||||
const lockfile = await project.readLockfile()
|
||||
expect(lockfile.onlyBuiltDependencies).toStrictEqual(onlyBuiltDependencies)
|
||||
expect(lockfile.packages['/pre-and-postinstall-scripts-example/1.0.0'].requiresBuild).toBe(true)
|
||||
expect(lockfile.packages['/install-script-example/1.0.0'].requiresBuild).toBe(undefined)
|
||||
}
|
||||
})
|
||||
|
||||
test('lifecycle scripts have access to package\'s own binary by binary name', async () => {
|
||||
const project = prepareEmpty()
|
||||
await addDependenciesToPackage({},
|
||||
|
||||
@@ -31,7 +31,9 @@ const ORDERED_KEYS = {
|
||||
|
||||
const ROOT_KEYS_ORDER = {
|
||||
lockfileVersion: 1,
|
||||
// only and never are conflict options.
|
||||
neverBuiltDependencies: 2,
|
||||
onlyBuiltDependencies: 2,
|
||||
overrides: 3,
|
||||
packageExtensionsChecksum: 4,
|
||||
specifiers: 10,
|
||||
|
||||
@@ -126,6 +126,9 @@ export function normalizeLockfile (lockfile: Lockfile, forceSharedFormat: boolea
|
||||
lockfileToSave.neverBuiltDependencies = lockfileToSave.neverBuiltDependencies.sort()
|
||||
}
|
||||
}
|
||||
if (lockfileToSave.onlyBuiltDependencies != null) {
|
||||
lockfileToSave.onlyBuiltDependencies = lockfileToSave.onlyBuiltDependencies.sort()
|
||||
}
|
||||
if (!lockfileToSave.packageExtensionsChecksum) {
|
||||
delete lockfileToSave.packageExtensionsChecksum
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { normalizeLockfile } from '@pnpm/lockfile-file/lib/write'
|
||||
test('empty overrides and neverBuiltDependencies are removed during lockfile normalization', () => {
|
||||
expect(normalizeLockfile({
|
||||
lockfileVersion: LOCKFILE_VERSION,
|
||||
// but this should be preserved.
|
||||
onlyBuiltDependencies: [],
|
||||
overrides: {},
|
||||
neverBuiltDependencies: [],
|
||||
packages: {},
|
||||
@@ -19,6 +21,7 @@ test('empty overrides and neverBuiltDependencies are removed during lockfile nor
|
||||
},
|
||||
}, false)).toStrictEqual({
|
||||
lockfileVersion: LOCKFILE_VERSION,
|
||||
onlyBuiltDependencies: [],
|
||||
importers: {
|
||||
foo: {
|
||||
dependencies: {
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface Lockfile {
|
||||
lockfileVersion: number
|
||||
packages?: PackageSnapshots
|
||||
neverBuiltDependencies?: string[]
|
||||
onlyBuiltDependencies?: string[]
|
||||
overrides?: Record<string, string>
|
||||
packageExtensionsChecksum?: string
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
export default function getOptionsFromRootManifest (manifest: ProjectManifest): {
|
||||
overrides?: Record<string, string>
|
||||
neverBuiltDependencies?: string[]
|
||||
onlyBuiltDependencies?: string[]
|
||||
packageExtensions?: Record<string, PackageExtension>
|
||||
peerDependencyRules?: PeerDependencyRules
|
||||
} {
|
||||
@@ -15,11 +16,13 @@ export default function getOptionsFromRootManifest (manifest: ProjectManifest):
|
||||
// so we cannot call it resolutions
|
||||
const overrides = manifest.pnpm?.overrides ?? manifest.resolutions
|
||||
const neverBuiltDependencies = manifest.pnpm?.neverBuiltDependencies ?? []
|
||||
const onlyBuiltDependencies = manifest.pnpm?.onlyBuiltDependencies
|
||||
const packageExtensions = manifest.pnpm?.packageExtensions
|
||||
const peerDependencyRules = manifest.pnpm?.peerDependencyRules
|
||||
return {
|
||||
overrides,
|
||||
neverBuiltDependencies,
|
||||
onlyBuiltDependencies,
|
||||
packageExtensions,
|
||||
peerDependencyRules,
|
||||
}
|
||||
|
||||
@@ -195,7 +195,12 @@ export default async function (
|
||||
if (opts.forceFullResolution && opts.wantedLockfile != null) {
|
||||
for (const [depPath, pkg] of Object.entries(dependenciesGraph)) {
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
if (opts.neverBuiltDependencies?.has(pkg.name) || opts.wantedLockfile.packages?.[depPath] == null || pkg.requiresBuild) continue
|
||||
if (
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
(opts.allowBuild != null && !opts.allowBuild(pkg.name)) ||
|
||||
(opts.wantedLockfile.packages?.[depPath] == null) ||
|
||||
pkg.requiresBuild
|
||||
) continue
|
||||
pendingRequiresBuilds.push(depPath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,7 @@ export interface ChildrenByParentDepPath {
|
||||
}
|
||||
|
||||
export interface ResolutionContext {
|
||||
allowBuild?: (pkgName: string) => boolean
|
||||
updatedSet: Set<string>
|
||||
defaultTag: string
|
||||
dryRun: boolean
|
||||
@@ -128,7 +129,6 @@ export interface ResolutionContext {
|
||||
currentLockfile: Lockfile
|
||||
linkWorkspacePackagesDepth: number
|
||||
lockfileDir: string
|
||||
neverBuiltDependencies: Set<string>
|
||||
storeController: StoreController
|
||||
// the IDs of packages that are not installable
|
||||
skipped: Set<string>
|
||||
@@ -784,11 +784,11 @@ async function resolveDependency (
|
||||
})
|
||||
|
||||
ctx.resolvedPackagesByDepPath[depPath] = getResolvedPackage({
|
||||
allowBuild: ctx.allowBuild,
|
||||
dependencyLockfile: currentPkg.dependencyLockfile,
|
||||
depPath,
|
||||
force: ctx.force,
|
||||
hasBin,
|
||||
neverBuiltDependencies: ctx.neverBuiltDependencies,
|
||||
pkg,
|
||||
pkgResponse,
|
||||
prepare,
|
||||
@@ -843,11 +843,11 @@ function pkgIsLeaf (pkg: PackageManifest) {
|
||||
|
||||
function getResolvedPackage (
|
||||
options: {
|
||||
allowBuild?: (pkgName: string) => boolean
|
||||
dependencyLockfile?: PackageSnapshot
|
||||
depPath: string
|
||||
force: boolean
|
||||
hasBin: boolean
|
||||
neverBuiltDependencies: Set<string>
|
||||
pkg: PackageManifest
|
||||
pkgResponse: PackageResponse
|
||||
prepare: boolean
|
||||
@@ -856,6 +856,10 @@ function getResolvedPackage (
|
||||
) {
|
||||
const peerDependencies = peerDependenciesWithoutOwn(options.pkg)
|
||||
|
||||
const requiresBuild = (options.allowBuild == null || options.allowBuild(options.pkg.name))
|
||||
? ((options.dependencyLockfile != null) ? Boolean(options.dependencyLockfile.requiresBuild) : undefined)
|
||||
: false
|
||||
|
||||
return {
|
||||
additionalInfo: {
|
||||
bundledDependencies: options.pkg.bundledDependencies,
|
||||
@@ -881,9 +885,7 @@ function getResolvedPackage (
|
||||
peerDependenciesMeta: options.pkg.peerDependenciesMeta,
|
||||
prepare: options.prepare,
|
||||
prod: !options.wantedDependency.dev && !options.wantedDependency.optional,
|
||||
requiresBuild: options.neverBuiltDependencies.has(options.pkg.name)
|
||||
? false
|
||||
: ((options.dependencyLockfile != null) ? Boolean(options.dependencyLockfile.requiresBuild) : undefined),
|
||||
requiresBuild,
|
||||
resolution: options.pkgResponse.body.resolution,
|
||||
version: options.pkg.version,
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ export interface ImporterToResolveGeneric<T> extends Importer<T> {
|
||||
}
|
||||
|
||||
export interface ResolveDependenciesOptions {
|
||||
allowBuild?: (pkgName: string) => boolean
|
||||
currentLockfile: Lockfile
|
||||
dryRun: boolean
|
||||
engineStrict: boolean
|
||||
@@ -60,7 +61,6 @@ export interface ResolveDependenciesOptions {
|
||||
hooks: {
|
||||
readPackage?: ReadPackageHook
|
||||
}
|
||||
neverBuiltDependencies?: Set<string>
|
||||
nodeVersion: string
|
||||
registries: Registries
|
||||
pnpmVersion: string
|
||||
@@ -84,6 +84,7 @@ export default async function<T> (
|
||||
|
||||
const wantedToBeSkippedPackageIds = new Set<string>()
|
||||
const ctx = {
|
||||
allowBuild: opts.allowBuild,
|
||||
childrenByParentDepPath: {} as ChildrenByParentDepPath,
|
||||
currentLockfile: opts.currentLockfile,
|
||||
defaultTag: opts.tag,
|
||||
@@ -94,7 +95,6 @@ export default async function<T> (
|
||||
forceFullResolution: opts.forceFullResolution,
|
||||
linkWorkspacePackagesDepth: opts.linkWorkspacePackagesDepth ?? -1,
|
||||
lockfileDir: opts.lockfileDir,
|
||||
neverBuiltDependencies: opts.neverBuiltDependencies ?? new Set(),
|
||||
nodeVersion: opts.nodeVersion,
|
||||
outdatedDependencies: {} as {[pkgId: string]: string},
|
||||
pendingNodes: [] as PendingNode[],
|
||||
|
||||
@@ -105,6 +105,7 @@ export interface PeerDependencyRules {
|
||||
export type ProjectManifest = BaseManifest & {
|
||||
pnpm?: {
|
||||
neverBuiltDependencies?: string[]
|
||||
onlyBuiltDependencies?: string[]
|
||||
overrides?: Record<string, string>
|
||||
packageExtensions?: Record<string, PackageExtension>
|
||||
peerDependencyRules?: PeerDependencyRules
|
||||
|
||||
Reference in New Issue
Block a user