mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-23 23:29:17 -05:00
feat: pnpm.ignoredOptionalDependencies (#7714)
This commit is contained in:
26
.changeset/thin-icons-scream.md
Normal file
26
.changeset/thin-icons-scream.md
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
"@pnpm/resolve-dependencies": minor
|
||||
"@pnpm/merge-lockfile-changes": minor
|
||||
"@pnpm/package-requester": minor
|
||||
"@pnpm/directory-fetcher": minor
|
||||
"@pnpm/tarball-fetcher": minor
|
||||
"@pnpm/exec.pkg-requires-build": minor
|
||||
"@pnpm/hooks.read-package-hook": minor
|
||||
"@pnpm/lockfile-types": minor
|
||||
"@pnpm/prune-lockfile": minor
|
||||
"@pnpm/create-cafs-store": minor
|
||||
"@pnpm/lockfile-file": minor
|
||||
"@pnpm/fetcher-base": minor
|
||||
"@pnpm/headless": minor
|
||||
"@pnpm/deps.graph-builder": minor
|
||||
"@pnpm/node.fetcher": minor
|
||||
"@pnpm/core": minor
|
||||
"@pnpm/cafs-types": minor
|
||||
"@pnpm/types": minor
|
||||
"@pnpm/config": minor
|
||||
"@pnpm/store.cafs": minor
|
||||
"@pnpm/worker": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Add a field named `ignoredOptionalDependencies`. This is an array of strings. If an optional dependency has its name included in this array, it will be skipped.
|
||||
@@ -17,6 +17,7 @@ export interface OptionsFromRootManifest {
|
||||
onlyBuiltDependencies?: string[]
|
||||
onlyBuiltDependenciesFile?: string
|
||||
packageExtensions?: Record<string, PackageExtension>
|
||||
ignoredOptionalDependencies?: string[]
|
||||
patchedDependencies?: Record<string, string>
|
||||
peerDependencyRules?: PeerDependencyRules
|
||||
supportedArchitectures?: SupportedArchitectures
|
||||
@@ -37,6 +38,7 @@ export function getOptionsFromRootManifest (manifestDir: string, manifest: Proje
|
||||
const onlyBuiltDependencies = manifest.pnpm?.onlyBuiltDependencies
|
||||
const onlyBuiltDependenciesFile = manifest.pnpm?.onlyBuiltDependenciesFile
|
||||
const packageExtensions = manifest.pnpm?.packageExtensions
|
||||
const ignoredOptionalDependencies = manifest.pnpm?.ignoredOptionalDependencies
|
||||
const peerDependencyRules = manifest.pnpm?.peerDependencyRules
|
||||
const allowedDeprecatedVersions = manifest.pnpm?.allowedDeprecatedVersions
|
||||
const allowNonAppliedPatches = manifest.pnpm?.allowNonAppliedPatches
|
||||
@@ -61,6 +63,7 @@ export function getOptionsFromRootManifest (manifestDir: string, manifest: Proje
|
||||
overrides,
|
||||
neverBuiltDependencies,
|
||||
packageExtensions,
|
||||
ignoredOptionalDependencies,
|
||||
peerDependencyRules,
|
||||
patchedDependencies,
|
||||
supportedArchitectures,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { BaseManifest, ReadPackageHook } from '@pnpm/types'
|
||||
import { createMatcher } from '@pnpm/matcher'
|
||||
|
||||
export function createOptionalDependenciesRemover (toBeRemoved: string[]): ReadPackageHook {
|
||||
if (!toBeRemoved.length) return <Manifest extends BaseManifest>(manifest: Manifest) => manifest
|
||||
const shouldBeRemoved = createMatcher(toBeRemoved)
|
||||
return <Manifest extends BaseManifest> (manifest: Manifest) => removeOptionalDependencies(manifest, shouldBeRemoved)
|
||||
}
|
||||
|
||||
function removeOptionalDependencies<Manifest extends BaseManifest> (
|
||||
manifest: Manifest,
|
||||
shouldBeRemoved: (input: string) => boolean
|
||||
): Manifest {
|
||||
for (const optionalDependency in manifest.optionalDependencies) {
|
||||
if (shouldBeRemoved(optionalDependency)) {
|
||||
delete manifest.optionalDependencies[optionalDependency]
|
||||
delete manifest.dependencies?.[optionalDependency]
|
||||
}
|
||||
}
|
||||
return manifest
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '@pnpm/types'
|
||||
import isEmpty from 'ramda/src/isEmpty'
|
||||
import pipeWith from 'ramda/src/pipeWith'
|
||||
import { createOptionalDependenciesRemover } from './createOptionalDependenciesRemover'
|
||||
import { createPackageExtender } from './createPackageExtender'
|
||||
import { createVersionsOverrider } from './createVersionsOverrider'
|
||||
import { createPeerDependencyPatcher } from './createPeerDependencyPatcher'
|
||||
@@ -17,6 +18,7 @@ export function createReadPackageHook (
|
||||
ignoreCompatibilityDb,
|
||||
lockfileDir,
|
||||
overrides,
|
||||
ignoredOptionalDependencies,
|
||||
packageExtensions,
|
||||
peerDependencyRules,
|
||||
readPackageHook,
|
||||
@@ -24,6 +26,7 @@ export function createReadPackageHook (
|
||||
ignoreCompatibilityDb?: boolean
|
||||
lockfileDir: string
|
||||
overrides?: Record<string, string>
|
||||
ignoredOptionalDependencies?: string[]
|
||||
packageExtensions?: Record<string, PackageExtension>
|
||||
peerDependencyRules?: PeerDependencyRules
|
||||
readPackageHook?: ReadPackageHook[] | ReadPackageHook
|
||||
@@ -44,6 +47,9 @@ export function createReadPackageHook (
|
||||
if (!isEmpty(overrides ?? {})) {
|
||||
hooks.push(createVersionsOverrider(overrides!, lockfileDir))
|
||||
}
|
||||
if (ignoredOptionalDependencies && !isEmpty(ignoredOptionalDependencies)) {
|
||||
hooks.push(createOptionalDependenciesRemover(ignoredOptionalDependencies))
|
||||
}
|
||||
if (
|
||||
peerDependencyRules != null &&
|
||||
(
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { createOptionalDependenciesRemover } from '../lib/createOptionalDependenciesRemover'
|
||||
import type { BaseManifest } from '@pnpm/types'
|
||||
|
||||
test('createOptionalDependenciesRemover() does not modify the manifest if provided array is empty', async () => {
|
||||
const removeOptionalDependencies = createOptionalDependenciesRemover([])
|
||||
const manifest: BaseManifest = Object.freeze({
|
||||
dependencies: {
|
||||
foo: '0.1.2',
|
||||
bar: '2.1.0',
|
||||
baz: '1.0.0',
|
||||
qux: '2.0.0',
|
||||
},
|
||||
optionalDependencies: {
|
||||
foo: '0.1.2',
|
||||
bar: '2.1.0',
|
||||
baz: '1.0.0',
|
||||
},
|
||||
})
|
||||
expect(await removeOptionalDependencies(manifest)).toBe(manifest)
|
||||
})
|
||||
|
||||
test('createOptionalDependenciesRemover() removes optional dependencies', async () => {
|
||||
const removeOptionalDependencies = createOptionalDependenciesRemover(['foo', 'bar'])
|
||||
expect(
|
||||
await removeOptionalDependencies({
|
||||
dependencies: {
|
||||
foo: '0.1.2',
|
||||
bar: '2.1.0',
|
||||
baz: '1.0.0',
|
||||
qux: '2.0.0',
|
||||
},
|
||||
optionalDependencies: {
|
||||
foo: '0.1.2',
|
||||
bar: '2.1.0',
|
||||
baz: '1.0.0',
|
||||
},
|
||||
})
|
||||
).toStrictEqual({
|
||||
dependencies: {
|
||||
baz: '1.0.0',
|
||||
qux: '2.0.0',
|
||||
},
|
||||
optionalDependencies: {
|
||||
baz: '1.0.0',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('createOptionalDependenciesRemover() does not remove non-optional packages', async () => {
|
||||
const removeOptionalDependencies = createOptionalDependenciesRemover(['foo', 'bar'])
|
||||
expect(
|
||||
await removeOptionalDependencies({
|
||||
dependencies: {
|
||||
foo: '0.1.2',
|
||||
bar: '2.1.0',
|
||||
baz: '1.0.0',
|
||||
qux: '2.0.0',
|
||||
},
|
||||
optionalDependencies: {
|
||||
foo: '0.1.2',
|
||||
baz: '1.0.0',
|
||||
},
|
||||
})
|
||||
).toStrictEqual({
|
||||
dependencies: {
|
||||
bar: '2.1.0',
|
||||
baz: '1.0.0',
|
||||
qux: '2.0.0',
|
||||
},
|
||||
optionalDependencies: {
|
||||
baz: '1.0.0',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('createOptionalDependenciesRemover() removes all optional dependencies if the pattern is a star', async () => {
|
||||
const removeOptionalDependencies = createOptionalDependenciesRemover(['*'])
|
||||
expect(
|
||||
await removeOptionalDependencies({
|
||||
dependencies: {
|
||||
foo: '0.1.2',
|
||||
bar: '2.1.0',
|
||||
baz: '1.0.0',
|
||||
qux: '2.0.0',
|
||||
},
|
||||
optionalDependencies: {
|
||||
foo: '0.1.2',
|
||||
bar: '2.1.0',
|
||||
baz: '1.0.0',
|
||||
},
|
||||
})
|
||||
).toStrictEqual({
|
||||
dependencies: {
|
||||
qux: '2.0.0',
|
||||
},
|
||||
optionalDependencies: {},
|
||||
})
|
||||
})
|
||||
|
||||
test('createOptionalDependenciesRemover() only removes optional dependencies that match one of the patterns', async () => {
|
||||
const removeOptionalDependencies = createOptionalDependenciesRemover(['@foo/*', '@bar/*'])
|
||||
expect(
|
||||
await removeOptionalDependencies({
|
||||
dependencies: {
|
||||
'@foo/abc': '0.0.0',
|
||||
'@foo/def': '0.0.0',
|
||||
'@foo/not-optional': '0.0.0',
|
||||
'@bar/ghi': '0.0.0',
|
||||
'@bar/required': '0.0.0',
|
||||
'@baz/jkl': '0.0.0',
|
||||
},
|
||||
optionalDependencies: {
|
||||
'@foo/abc': '0.0.0',
|
||||
'@foo/def': '0.0.0',
|
||||
'@bar/ghi': '0.0.0',
|
||||
'@baz/jkl': '0.0.0',
|
||||
},
|
||||
})
|
||||
).toStrictEqual({
|
||||
dependencies: {
|
||||
'@foo/not-optional': '0.0.0',
|
||||
'@bar/required': '0.0.0',
|
||||
'@baz/jkl': '0.0.0',
|
||||
},
|
||||
optionalDependencies: {
|
||||
'@baz/jkl': '0.0.0',
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -77,6 +77,9 @@ function normalizeLockfile (lockfile: InlineSpecifiersLockfile, opts: NormalizeL
|
||||
if (!lockfileToSave.packageExtensionsChecksum) {
|
||||
delete lockfileToSave.packageExtensionsChecksum
|
||||
}
|
||||
if (!lockfileToSave.ignoredOptionalDependencies?.length) {
|
||||
delete lockfileToSave.ignoredOptionalDependencies
|
||||
}
|
||||
if (!lockfileToSave.pnpmfileChecksum) {
|
||||
delete lockfileToSave.pnpmfileChecksum
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface Lockfile {
|
||||
packages?: PackageSnapshots
|
||||
overrides?: Record<string, string>
|
||||
packageExtensionsChecksum?: string
|
||||
ignoredOptionalDependencies?: string[]
|
||||
patchedDependencies?: Record<string, PatchFile>
|
||||
pnpmfileChecksum?: string
|
||||
settings?: LockfileSettings
|
||||
|
||||
@@ -14,6 +14,14 @@ export function mergeLockfileChanges (ours: Lockfile, theirs: Lockfile): Lockfil
|
||||
newLockfile.pnpmfileChecksum = pnpmfileChecksum
|
||||
}
|
||||
|
||||
const ignoredOptionalDependencies = [...new Set([
|
||||
...ours.ignoredOptionalDependencies ?? [],
|
||||
...theirs.ignoredOptionalDependencies ?? [],
|
||||
])]
|
||||
if (ignoredOptionalDependencies.length) {
|
||||
newLockfile.ignoredOptionalDependencies = ignoredOptionalDependencies
|
||||
}
|
||||
|
||||
for (const importerId of Array.from(new Set([...Object.keys(ours.importers), ...Object.keys(theirs.importers)]))) {
|
||||
newLockfile.importers[importerId] = {
|
||||
specifiers: {},
|
||||
|
||||
@@ -108,6 +108,9 @@ export function pruneLockfile (
|
||||
if (lockfile.pnpmfileChecksum) {
|
||||
prunedLockfile.pnpmfileChecksum = lockfile.pnpmfileChecksum
|
||||
}
|
||||
if (lockfile.ignoredOptionalDependencies && !isEmpty(lockfile.ignoredOptionalDependencies)) {
|
||||
prunedLockfile.ignoredOptionalDependencies = lockfile.ignoredOptionalDependencies
|
||||
}
|
||||
return pruneSharedLockfile(prunedLockfile, opts)
|
||||
}
|
||||
|
||||
|
||||
@@ -130,6 +130,7 @@ export type ProjectManifest = BaseManifest & {
|
||||
onlyBuiltDependenciesFile?: string
|
||||
overrides?: Record<string, string>
|
||||
packageExtensions?: Record<string, PackageExtension>
|
||||
ignoredOptionalDependencies?: string[]
|
||||
peerDependencyRules?: PeerDependencyRules
|
||||
allowedDeprecatedVersions?: AllowedDeprecatedVersions
|
||||
allowNonAppliedPatches?: boolean
|
||||
|
||||
@@ -14,6 +14,7 @@ export type ListMissingPeersOptions = Partial<GetContextOptions>
|
||||
| 'nodeLinker'
|
||||
| 'overrides'
|
||||
| 'packageExtensions'
|
||||
| 'ignoredOptionalDependencies'
|
||||
| 'preferWorkspacePackages'
|
||||
| 'saveWorkspaceProtocol'
|
||||
| 'storeController'
|
||||
@@ -69,6 +70,7 @@ export async function getPeerDependencyIssues (
|
||||
overrides: opts.overrides,
|
||||
packageExtensions: opts.packageExtensions,
|
||||
readPackageHook: opts.hooks?.readPackage,
|
||||
ignoredOptionalDependencies: opts.ignoredOptionalDependencies,
|
||||
}),
|
||||
},
|
||||
linkWorkspacePackagesDepth: opts.linkWorkspacePackagesDepth ?? (opts.saveWorkspaceProtocol ? 0 : -1),
|
||||
|
||||
@@ -70,6 +70,7 @@ export interface StrictInstallOptions {
|
||||
nodeLinker: 'isolated' | 'hoisted' | 'pnp'
|
||||
nodeVersion: string
|
||||
packageExtensions: Record<string, PackageExtension>
|
||||
ignoredOptionalDependencies: string[]
|
||||
pnpmfile: string
|
||||
ignorePnpmfile: boolean
|
||||
packageManager: {
|
||||
@@ -197,6 +198,7 @@ const defaults = (opts: InstallOptions) => {
|
||||
ignoreCompatibilityDb: false,
|
||||
ignorePackageManifest: false,
|
||||
packageExtensions: {},
|
||||
ignoredOptionalDependencies: [] as string[],
|
||||
packageManager,
|
||||
preferFrozenLockfile: true,
|
||||
preferWorkspacePackages: false,
|
||||
@@ -272,6 +274,7 @@ export function extendOptions (
|
||||
lockfileDir: extendedOpts.lockfileDir,
|
||||
packageExtensions: extendedOpts.packageExtensions,
|
||||
peerDependencyRules: extendedOpts.peerDependencyRules,
|
||||
ignoredOptionalDependencies: extendedOpts.ignoredOptionalDependencies,
|
||||
})
|
||||
if (extendedOpts.lockfileOnly) {
|
||||
extendedOpts.ignoreScripts = true
|
||||
|
||||
@@ -339,6 +339,7 @@ export async function mutateModules (
|
||||
autoInstallPeers: opts.autoInstallPeers,
|
||||
excludeLinksFromLockfile: opts.excludeLinksFromLockfile,
|
||||
overrides: opts.overrides,
|
||||
ignoredOptionalDependencies: opts.ignoredOptionalDependencies?.sort(),
|
||||
packageExtensionsChecksum,
|
||||
patchedDependencies,
|
||||
pnpmfileChecksum,
|
||||
@@ -359,6 +360,7 @@ export async function mutateModules (
|
||||
}
|
||||
ctx.wantedLockfile.overrides = opts.overrides
|
||||
ctx.wantedLockfile.packageExtensionsChecksum = packageExtensionsChecksum
|
||||
ctx.wantedLockfile.ignoredOptionalDependencies = opts.ignoredOptionalDependencies
|
||||
ctx.wantedLockfile.pnpmfileChecksum = pnpmfileChecksum
|
||||
ctx.wantedLockfile.patchedDependencies = patchedDependencies
|
||||
} else if (!frozenLockfile) {
|
||||
@@ -710,6 +712,7 @@ function getOutdatedLockfileSetting (
|
||||
onlyBuiltDependencies,
|
||||
overrides,
|
||||
packageExtensionsChecksum,
|
||||
ignoredOptionalDependencies,
|
||||
patchedDependencies,
|
||||
autoInstallPeers,
|
||||
excludeLinksFromLockfile,
|
||||
@@ -719,6 +722,7 @@ function getOutdatedLockfileSetting (
|
||||
overrides?: Record<string, string>
|
||||
packageExtensionsChecksum?: string
|
||||
patchedDependencies?: Record<string, PatchFile>
|
||||
ignoredOptionalDependencies?: string[]
|
||||
autoInstallPeers?: boolean
|
||||
excludeLinksFromLockfile?: boolean
|
||||
pnpmfileChecksum?: string
|
||||
@@ -730,6 +734,9 @@ function getOutdatedLockfileSetting (
|
||||
if (lockfile.packageExtensionsChecksum !== packageExtensionsChecksum) {
|
||||
return 'packageExtensionsChecksum'
|
||||
}
|
||||
if (!equals(lockfile.ignoredOptionalDependencies?.sort() ?? [], ignoredOptionalDependencies?.sort() ?? [])) {
|
||||
return 'ignoredOptionalDependencies'
|
||||
}
|
||||
if (!equals(lockfile.patchedDependencies ?? {}, patchedDependencies ?? {})) {
|
||||
return 'patchedDependencies'
|
||||
}
|
||||
|
||||
76
pkg-manager/core/test/install/ignoredOptionalDependencies.ts
Normal file
76
pkg-manager/core/test/install/ignoredOptionalDependencies.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { ProjectManifest } from '@pnpm/types'
|
||||
import { prepareEmpty } from '@pnpm/prepare'
|
||||
import { addDependenciesToPackage, install } from '@pnpm/core'
|
||||
import {
|
||||
testDefaults,
|
||||
} from '../utils'
|
||||
|
||||
test('ignoredOptionalDependencies causes listed optional dependencies to be skipped', async () => {
|
||||
const project = prepareEmpty()
|
||||
|
||||
await addDependenciesToPackage(
|
||||
{},
|
||||
['@pnpm.e2e/pkg-with-good-optional@1.0.0'],
|
||||
testDefaults({ ignoredOptionalDependencies: ['is-positive'] })
|
||||
)
|
||||
|
||||
const lockfile = project.readLockfile()
|
||||
expect(lockfile.ignoredOptionalDependencies).toStrictEqual(['is-positive'])
|
||||
expect(lockfile.packages).not.toHaveProperty(['/is-positive@1.0.0'])
|
||||
expect(lockfile.packages).toHaveProperty(['/@pnpm.e2e/pkg-with-good-optional@1.0.0'])
|
||||
})
|
||||
|
||||
test('empty ignoredOptionalDependencies is not recorded in lockfile', async () => {
|
||||
const project = prepareEmpty()
|
||||
|
||||
await addDependenciesToPackage(
|
||||
{},
|
||||
['@pnpm.e2e/pkg-with-good-optional@1.0.0'],
|
||||
testDefaults({ ignoredOptionalDependencies: [] })
|
||||
)
|
||||
|
||||
const lockfile = project.readLockfile()
|
||||
expect(lockfile).not.toHaveProperty(['ignoredOptionalDependencies'])
|
||||
expect(lockfile.packages).toHaveProperty(['/is-positive@1.0.0'])
|
||||
expect(lockfile.packages).toHaveProperty(['/@pnpm.e2e/pkg-with-good-optional@1.0.0'])
|
||||
})
|
||||
|
||||
test('names in ignoredOptionalDependencies are sorted alphabetically in the lockfile', async () => {
|
||||
const project = prepareEmpty()
|
||||
|
||||
await addDependenciesToPackage(
|
||||
{},
|
||||
['@pnpm.e2e/pkg-with-good-optional@1.0.0'],
|
||||
testDefaults({ ignoredOptionalDependencies: ['foo', 'bar', 'baz', 'qux'] })
|
||||
)
|
||||
|
||||
const lockfile = project.readLockfile()
|
||||
expect(lockfile.ignoredOptionalDependencies).toStrictEqual(['bar', 'baz', 'foo', 'qux'])
|
||||
})
|
||||
|
||||
test('adding or changing manifest.pnpm.ignoredOptionalDependencies should change lockfile.ignoredOptionalDependencies and module structure', async () => {
|
||||
const manifest: ProjectManifest = Object.freeze({
|
||||
dependencies: {
|
||||
'@pnpm.e2e/pkg-with-good-optional': '1.0.0',
|
||||
},
|
||||
})
|
||||
const project = prepareEmpty()
|
||||
|
||||
await install(manifest, testDefaults())
|
||||
{
|
||||
const lockfile = project.readLockfile()
|
||||
expect(lockfile).not.toHaveProperty(['ignoredOptionalDependencies'])
|
||||
expect(lockfile.packages).toHaveProperty(['/@pnpm.e2e/pkg-with-good-optional@1.0.0'])
|
||||
expect(lockfile.packages).toHaveProperty(['/is-positive@1.0.0'])
|
||||
}
|
||||
|
||||
await install(manifest, testDefaults({
|
||||
ignoredOptionalDependencies: ['is-positive'],
|
||||
}))
|
||||
{
|
||||
const lockfile = project.readLockfile()
|
||||
expect(lockfile.ignoredOptionalDependencies).toStrictEqual(['is-positive'])
|
||||
expect(lockfile.packages).toHaveProperty(['/@pnpm.e2e/pkg-with-good-optional@1.0.0'])
|
||||
expect(lockfile.packages).not.toHaveProperty(['/is-positive@1.0.0'])
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user