feat: pnpm.ignoredOptionalDependencies (#7714)

This commit is contained in:
Khải
2024-02-28 20:46:29 +07:00
committed by GitHub
parent 0e6b757cf5
commit 730929e330
14 changed files with 289 additions and 0 deletions

View 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.

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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 &&
(

View File

@@ -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',
},
})
})

View File

@@ -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
}

View File

@@ -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

View File

@@ -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: {},

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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),

View File

@@ -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

View File

@@ -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'
}

View 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'])
}
})