feat: add pnpm dedupe --check (#6409)

This commit is contained in:
Brandon Cheng
2023-04-16 16:00:04 -04:00
committed by GitHub
parent 32f8e08c67
commit 6850bb135e
36 changed files with 885 additions and 6 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/dedupe.issues-renderer": major
"@pnpm/dedupe.check": major
---
Initial release.

View File

@@ -0,0 +1,6 @@
---
"@pnpm/plugin-commands-installation": minor
pnpm: minor
---
Add `--check` flag to `pnpm dedupe`. No changes will be made to `node_modules` or the lockfile. Exits with a non-zero status code if changes are possible.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/dedupe.types": major
---
Initial release.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/default-reporter": minor
---
Report errors from pnpm dedupe --check

View File

@@ -35,6 +35,8 @@
"dependencies": {
"@pnpm/config": "workspace:*",
"@pnpm/core-loggers": "workspace:*",
"@pnpm/dedupe.issues-renderer": "workspace:*",
"@pnpm/dedupe.types": "workspace:*",
"@pnpm/error": "workspace:*",
"@pnpm/render-peer-issues": "workspace:*",
"@pnpm/types": "workspace:*",

View File

@@ -1,5 +1,7 @@
import { type Config } from '@pnpm/config'
import { type Log } from '@pnpm/core-loggers'
import { renderDedupeCheckIssues } from '@pnpm/dedupe.issues-renderer'
import { type DedupeCheckIssues } from '@pnpm/dedupe.types'
import { type PnpmError } from '@pnpm/error'
import { renderPeerIssues } from '@pnpm/render-peer-issues'
import { type PeerDependencyIssuesByProjects } from '@pnpm/types'
@@ -66,6 +68,8 @@ function getErrorInfo (logObj: Log, config?: Config): {
return reportEngineError(logObj as any) // eslint-disable-line @typescript-eslint/no-explicit-any
case 'ERR_PNPM_PEER_DEP_ISSUES':
return reportPeerDependencyIssuesError(err, logObj as any) // eslint-disable-line @typescript-eslint/no-explicit-any
case 'ERR_PNPM_DEDUPE_CHECK_ISSUES':
return reportDedupeCheckIssuesError(err, logObj as any) // eslint-disable-line @typescript-eslint/no-explicit-any
case 'ERR_PNPM_FETCH_401':
case 'ERR_PNPM_FETCH_403':
return reportAuthError(err, logObj as any, config) // eslint-disable-line @typescript-eslint/no-explicit-any
@@ -414,3 +418,13 @@ function getHasMissingPeers (issuesByProjects: PeerDependencyIssuesByProjects) {
return Object.values(issuesByProjects)
.some((issues) => Object.values(issues.missing).flat().some(({ optional }) => !optional))
}
function reportDedupeCheckIssuesError (err: Error, msg: { dedupeCheckIssues: DedupeCheckIssues }) {
return {
title: err.message,
body: `\
${renderDedupeCheckIssues(msg.dedupeCheckIssues)}
Run ${chalk.yellow('pnpm dedupe')} to apply the changes above.
`,
}
}

View File

@@ -14,6 +14,12 @@
{
"path": "../../config/config"
},
{
"path": "../../dedupe/issues-renderer"
},
{
"path": "../../dedupe/types"
},
{
"path": "../../packages/core-loggers"
},

13
dedupe/check/README.md Normal file
View File

@@ -0,0 +1,13 @@
# @pnpm/dedupe.issues-renderer
> Logic for "pnpm dedupe --check"
## Installation
```
pnpm add @pnpm/dedupe.check
```
## License
[MIT](LICENSE)

View File

@@ -0,0 +1 @@
module.exports = require('../../jest.config')

43
dedupe/check/package.json Normal file
View File

@@ -0,0 +1,43 @@
{
"name": "@pnpm/dedupe.check",
"version": "0.0.0",
"description": "Visualize pnpm dedupe --check issues.",
"bugs": {
"url": "https://github.com/pnpm/pnpm/issues"
},
"main": "lib/index.js",
"types": "lib/index.d.ts",
"files": [
"lib",
"!*.map"
],
"keywords": [
"pnpm8"
],
"license": "MIT",
"engines": {
"node": ">=16.14"
},
"repository": "https://github.com/pnpm/pnpm/blob/main/dedupe/check",
"scripts": {
"_test": "jest",
"test": "pnpm run compile && pnpm run _test",
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
"prepublishOnly": "pnpm run compile",
"compile": "tsc --build && pnpm run lint --fix"
},
"homepage": "https://github.com/pnpm/pnpm/blob/main/dedupe/check#readme",
"funding": "https://opencollective.com/pnpm",
"dependencies": {
"@pnpm/dedupe.types": "workspace:*",
"@pnpm/error": "workspace:*",
"@pnpm/lockfile-types": "workspace:*",
"@pnpm/types": "workspace:*"
},
"devDependencies": {
"@pnpm/dedupe.check": "workspace:*"
},
"exports": {
".": "./lib/index.js"
}
}

View File

@@ -0,0 +1,8 @@
import { type DedupeCheckIssues } from '@pnpm/dedupe.types'
import { PnpmError } from '@pnpm/error'
export class DedupeCheckIssuesError extends PnpmError {
constructor (public dedupeCheckIssues: DedupeCheckIssues) {
super('DEDUPE_CHECK_ISSUES', 'Dedupe --check found changes to the lockfile')
}
}

View File

@@ -0,0 +1,101 @@
import { type ResolvedDependencies, type Lockfile } from '@pnpm/lockfile-types'
import {
type ResolutionChangesByAlias,
type DedupeCheckIssues,
type SnapshotsChanges,
} from '@pnpm/dedupe.types'
import { DEPENDENCIES_FIELDS } from '@pnpm/types'
import { DedupeCheckIssuesError } from './DedupeCheckIssuesError'
const PACKAGE_SNAPSHOT_DEP_FIELDS = ['dependencies', 'optionalDependencies'] as const
export function dedupeDiffCheck (prev: Lockfile, next: Lockfile): void {
const issues: DedupeCheckIssues = {
importerIssuesByImporterId: diffSnapshots(prev.importers, next.importers, DEPENDENCIES_FIELDS),
packageIssuesByDepPath: diffSnapshots(prev.packages ?? {}, next.packages ?? {}, PACKAGE_SNAPSHOT_DEP_FIELDS),
}
const changesCount =
countChangedSnapshots(issues.importerIssuesByImporterId) +
countChangedSnapshots(issues.packageIssuesByDepPath)
if (changesCount > 0) {
throw new DedupeCheckIssuesError(issues)
}
}
/**
* Get all the keys of an object T where the value extends some type U.
*/
type KeysOfValue<T, U> = KeyValueMatch<T, keyof T, U>
type KeyValueMatch<T, K, U> = K extends keyof T
? T[K] extends U ? K : never
: never
/**
* Given a PackageSnapshot or ProjectSnapshot, returns the keys where values
* match ResolvedDependencies.
*
* Unfortunately the ResolvedDependencies interface is just
* Record<string,string> so this also matches the "engines" and "specifiers"
* block.
*/
type PossiblyResolvedDependenciesKeys<TSnapshot> = KeysOfValue<TSnapshot, ResolvedDependencies | undefined>
function diffSnapshots<TSnapshot> (
prev: Record<string, TSnapshot>,
next: Record<string, TSnapshot>,
fields: ReadonlyArray<PossiblyResolvedDependenciesKeys<TSnapshot>>
): SnapshotsChanges {
const removed: string[] = []
const updated: Record<string, ResolutionChangesByAlias> = {}
for (const [id, prevSnapshot] of Object.entries(prev)) {
const nextSnapshot = next[id]
if (nextSnapshot == null) {
removed.push(id)
continue
}
const updates = fields.reduce((acc: ResolutionChangesByAlias, dependencyField) => ({
...acc,
...getResolutionUpdates(prevSnapshot[dependencyField] ?? {}, nextSnapshot[dependencyField] ?? {}),
}), {})
if (Object.keys(updates).length > 0) {
updated[id] = updates
}
}
const added = Object.keys(next).filter(id => prev[id] == null)
return { added, removed, updated }
}
function getResolutionUpdates (prev: ResolvedDependencies, next: ResolvedDependencies): ResolutionChangesByAlias {
const updates: ResolutionChangesByAlias = {}
for (const [alias, prevResolution] of Object.entries(prev)) {
const nextResolution = next[alias]
if (prevResolution === nextResolution) {
continue
}
updates[alias] = nextResolution == null
? { type: 'removed', prev: prevResolution }
: { type: 'updated', prev: prevResolution, next: nextResolution }
}
const newAliases = Object.entries(next).filter(([alias]) => prev[alias] == null)
for (const [alias, nextResolution] of newAliases) {
updates[alias] = { type: 'added', next: nextResolution }
}
return updates
}
export function countChangedSnapshots (snapshotChanges: SnapshotsChanges): number {
return snapshotChanges.added.length + snapshotChanges.removed.length + Object.keys(snapshotChanges.updated).length
}

View File

@@ -0,0 +1,2 @@
export { dedupeDiffCheck, countChangedSnapshots } from './dedupeDiffCheck'
export { DedupeCheckIssuesError } from './DedupeCheckIssuesError'

View File

@@ -0,0 +1,112 @@
import { DedupeCheckIssuesError, dedupeDiffCheck } from '@pnpm/dedupe.check'
import { type Lockfile } from '@pnpm/lockfile-types'
describe('dedupeDiffCheck', () => {
it('should have no changes for same lockfile', () => {
const lockfile: Lockfile = {
importers: {
'.': {
specifiers: {},
},
},
lockfileVersion: 'testLockfileVersion',
}
expect(() => {
dedupeDiffCheck(lockfile, lockfile)
}).not.toThrow()
})
it('throws DedupeCheckIssuesError on changes', () => {
const before: Lockfile = {
importers: {
'packages/a': {
specifiers: {
'is-positive': '^3.0.0',
},
dependencies: {
'is-positive': '3.0.0',
},
},
'packages/b': {
specifiers: {
'is-positive': '^3.1.0',
},
dependencies: {
'is-positive': '3.1.0',
},
},
},
packages: {
'/is-positive@3.0.0': {
resolution: {
integrity: 'sha512-JDkaKp5jWv24ZaFuYDKTcBrC/wBOHdjhzLDkgrrkJD/j7KqqXsGcAkex336qHoOFEajMy7bYqUgm0KH9/MzQvw==',
},
engines: {
node: '>=0.10.0',
},
},
'/is-positive@3.1.0': {
resolution: {
integrity: 'sha1-hX21hKG6XRyymAUn/DtsQ103sP0=',
},
engines: {
node: '>=0.10.0',
},
},
},
lockfileVersion: 'testLockfileVersion',
}
const after: Lockfile = {
importers: {
'packages/a': {
specifiers: {
'is-positive': '^3.0.0',
},
dependencies: {
'is-positive': '3.1.0',
},
},
'packages/b': {
specifiers: {
'is-positive': '^3.1.0',
},
dependencies: {
'is-positive': '3.1.0',
},
},
},
packages: {
'/is-positive@3.1.0': {
resolution: {
integrity: 'sha1-hX21hKG6XRyymAUn/DtsQ103sP0=',
},
engines: {
node: '>=0.10.0',
},
},
},
lockfileVersion: 'testLockfileVersion',
}
expect(() => {
dedupeDiffCheck(before, after)
}).toThrow(new DedupeCheckIssuesError({
importerIssuesByImporterId: {
added: [],
removed: [],
updated: {
'packages/a': {
'is-positive': { type: 'updated', prev: '3.0.0', next: '3.1.0' },
},
},
},
packageIssuesByDepPath: {
added: [],
removed: ['/is-positive@3.0.0'],
updated: {},
},
}))
})
})

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
# @pnpm/dedupe.issues-renderer
> Visualizes "pnpm dedupe --check" issues
## Installation
```
pnpm add @pnpm/dedupe.issues-renderer
```
## License
[MIT](LICENSE)

View File

@@ -0,0 +1 @@
module.exports = require('../../jest.config')

View File

@@ -0,0 +1,44 @@
{
"name": "@pnpm/dedupe.issues-renderer",
"version": "0.0.0",
"description": "Visualize pnpm dedupe --check issues.",
"bugs": {
"url": "https://github.com/pnpm/pnpm/issues"
},
"main": "lib/index.js",
"types": "lib/index.d.ts",
"files": [
"lib",
"!*.map"
],
"keywords": [
"pnpm8"
],
"license": "MIT",
"engines": {
"node": ">=16.14"
},
"repository": "https://github.com/pnpm/pnpm/blob/main/dedupe/issues-renderer",
"scripts": {
"_test": "jest",
"test": "pnpm run compile && pnpm run _test",
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
"prepublishOnly": "pnpm run compile",
"compile": "tsc --build && pnpm run lint --fix"
},
"homepage": "https://github.com/pnpm/pnpm/blob/main/dedupe/issues-renderer#readme",
"funding": "https://opencollective.com/pnpm",
"dependencies": {
"@pnpm/dedupe.types": "workspace:*",
"archy": "^1.0.0",
"chalk": "^4.1.2"
},
"devDependencies": {
"@pnpm/dedupe.issues-renderer": "workspace:*",
"@types/archy": "0.0.32",
"strip-ansi": "^6.0.1"
},
"exports": {
".": "./lib/index.js"
}
}

View File

@@ -0,0 +1,56 @@
import {
type ResolutionChange,
type DedupeCheckIssues,
type ResolutionChangesByAlias,
type SnapshotsChanges,
} from '@pnpm/dedupe.types'
import archy from 'archy'
import chalk from 'chalk'
export function renderDedupeCheckIssues (dedupeCheckIssues: DedupeCheckIssues) {
const importersReport = report(dedupeCheckIssues.importerIssuesByImporterId)
const packagesReport = report(dedupeCheckIssues.packageIssuesByDepPath)
const lines = []
if (importersReport !== '') {
lines.push(chalk.blueBright.underline('Importers'))
lines.push(importersReport)
lines.push('')
}
if (packagesReport !== '') {
lines.push(chalk.blueBright.underline('Packages'))
lines.push(packagesReport)
lines.push('')
}
return lines.join('\n')
}
/**
* Render snapshot changes. Expected to return an empty string for no changes.
*/
function report (snapshotChanges: SnapshotsChanges): string {
return [
...Object.entries(snapshotChanges.updated).map(([alias, updates]) => archy(toArchy(alias, updates))),
...snapshotChanges.added.map((id) => `${chalk.green('+')} ${id}`),
...snapshotChanges.removed.map((id) => `${chalk.red('-')} ${id}`),
].join('\n')
}
function toArchy (name: string, issue: ResolutionChangesByAlias): archy.Data {
return {
label: name,
nodes: Object.entries(issue).map(([alias, change]) => toArchyResolution(alias, change)),
}
}
function toArchyResolution (alias: string, change: ResolutionChange): archy.Data {
switch (change.type) {
case 'added':
return { label: `${chalk.green('+')} ${alias} ${chalk.gray(change.next)}` }
case 'removed':
return { label: `${chalk.red('-')} ${alias} ${chalk.gray(change.prev)}` }
case 'updated':
return { label: `${alias} ${chalk.red(change.prev)} ${chalk.gray('→')} ${chalk.green(change.next)}` }
}
}

View File

@@ -0,0 +1,56 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renderDedupeCheckIssues prints new importers and packages 1`] = `
"Importers
.
└── + packages/a 0.0.0
+ packages/a
Packages
@types/tar-stream/2.2.2
└── @types/node 14.18.42 → 18.15.11
@types/tar/6.1.4
└── @types/node 14.18.42 → 18.15.11
+ /@types/node/18.15.11
- /@types/node/14.18.42
"
`;
exports[`renderDedupeCheckIssues prints removed importer 1`] = `
"Importers
.
└── @types/node 18.15.11 → 14.18.42
- packages/a
Packages
@types/tar-stream/2.2.2
└── @types/node 18.15.11 → 14.18.42
@types/tar/6.1.4
└── @types/node 18.15.11 → 14.18.42
+ /@types/node/14.18.42
- /@types/node/18.15.11
"
`;
exports[`renderDedupeCheckIssues prints removed packages and updated resolutions 1`] = `
"Importers
.
└── @types/node 14.18.42 → 18.15.11
Packages
@types/tar-stream/2.2.2
└── @types/node 14.18.42 → 18.15.11
@types/tar/6.1.4
└── @types/node 14.18.42 → 18.15.11
- /@types/node/14.18.42
"
`;

View File

@@ -0,0 +1,85 @@
import { renderDedupeCheckIssues } from '@pnpm/dedupe.issues-renderer'
import stripAnsi from 'strip-ansi'
describe('renderDedupeCheckIssues', () => {
test('prints removed packages and updated resolutions', () => {
expect(stripAnsi(renderDedupeCheckIssues({
importerIssuesByImporterId: {
added: [],
removed: [],
updated: {
'.': {
'@types/node': { type: 'updated', prev: '14.18.42', next: '18.15.11' },
},
},
},
packageIssuesByDepPath: {
added: [],
removed: ['/@types/node/14.18.42'],
updated: {
'@types/tar-stream/2.2.2': {
'@types/node': { type: 'updated', prev: '14.18.42', next: '18.15.11' },
},
'@types/tar/6.1.4': {
'@types/node': { type: 'updated', prev: '14.18.42', next: '18.15.11' },
},
},
},
}))).toMatchSnapshot()
})
test('prints new importers and packages', () => {
expect(stripAnsi(renderDedupeCheckIssues({
importerIssuesByImporterId: {
added: ['packages/a'],
removed: [],
updated: {
'.': {
'packages/a': { type: 'added', next: '0.0.0' },
},
},
},
packageIssuesByDepPath: {
added: [
// Suppose packages/a added a new @types/node dependency on 18.x.
'/@types/node/18.15.11',
],
removed: ['/@types/node/14.18.42'],
updated: {
'@types/tar-stream/2.2.2': {
'@types/node': { type: 'updated', prev: '14.18.42', next: '18.15.11' },
},
'@types/tar/6.1.4': {
'@types/node': { type: 'updated', prev: '14.18.42', next: '18.15.11' },
},
},
},
}))).toMatchSnapshot()
})
test('prints removed importer', () => {
expect(stripAnsi(renderDedupeCheckIssues({
importerIssuesByImporterId: {
added: [],
removed: ['packages/a'],
updated: {
'.': {
'@types/node': { type: 'updated', prev: '18.15.11', next: '14.18.42' },
},
},
},
packageIssuesByDepPath: {
added: ['/@types/node/14.18.42'],
removed: ['/@types/node/18.15.11'],
updated: {
'@types/tar-stream/2.2.2': {
'@types/node': { type: 'updated', prev: '18.15.11', next: '14.18.42' },
},
'@types/tar/6.1.4': {
'@types/node': { type: 'updated', prev: '18.15.11', next: '14.18.42' },
},
},
},
}))).toMatchSnapshot()
})
})

View File

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

View File

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

13
dedupe/types/README.md Normal file
View File

@@ -0,0 +1,13 @@
# @pnpm/dedupe.types
> Types for the pnpm dedupe command
## Installation
```
pnpm add @pnpm/dedupe.types
```
## License
[MIT](LICENSE)

36
dedupe/types/package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "@pnpm/dedupe.types",
"version": "0.0.0",
"description": "Types for the pnpm dedupe command",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"exports": {
".": "./lib/index.js"
},
"files": [
"lib",
"!*.map"
],
"engines": {
"node": ">=16.14"
},
"scripts": {
"test": "pnpm run compile",
"prepublishOnly": "pnpm run compile",
"compile": "tsc --build && pnpm run lint --fix",
"lint": "eslint \"src/**/*.ts\""
},
"repository": "https://github.com/pnpm/pnpm/blob/main/dedupe/types",
"keywords": [
"pnpm8"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/pnpm/pnpm/issues"
},
"homepage": "https://github.com/pnpm/pnpm/blob/main/dedupe/types#readme",
"funding": "https://opencollective.com/pnpm",
"devDependencies": {
"@pnpm/dedupe.types": "workspace:*"
}
}

View File

@@ -0,0 +1,30 @@
export interface DedupeCheckIssues {
readonly importerIssuesByImporterId: SnapshotsChanges
readonly packageIssuesByDepPath: SnapshotsChanges
}
export interface SnapshotsChanges {
readonly added: readonly string[]
readonly removed: readonly string[]
readonly updated: Record<string, ResolutionChangesByAlias>
}
export type ResolutionChangesByAlias = Record<string, ResolutionChange>
export type ResolutionChange = ResolutionAdded | ResolutionDeleted | ResolutionUpdated
export interface ResolutionAdded {
readonly type: 'added'
readonly next: string
}
export interface ResolutionDeleted {
readonly type: 'removed'
readonly prev: string
}
export interface ResolutionUpdated {
readonly type: 'updated'
readonly prev: string
readonly next: string
}

View File

@@ -0,0 +1 @@
export * from './DedupeCheckIssues'

View File

@@ -0,0 +1,13 @@
{
"extends": "@pnpm/tsconfig",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
},
"include": [
"src/**/*.ts",
"../../__typings__/**/*.d.ts"
],
"references": [],
"composite": true
}

View File

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

View File

@@ -65,6 +65,7 @@
"@pnpm/config": "workspace:*",
"@pnpm/constants": "workspace:*",
"@pnpm/core": "workspace:*",
"@pnpm/dedupe.check": "workspace:*",
"@pnpm/error": "workspace:*",
"@pnpm/filter-workspace-packages": "workspace:*",
"@pnpm/find-workspace-dir": "workspace:*",

View File

@@ -1,13 +1,19 @@
import { docsUrl } from '@pnpm/cli-utils'
import { UNIVERSAL_OPTIONS } from '@pnpm/common-cli-options-help'
import { dedupeDiffCheck } from '@pnpm/dedupe.check'
import renderHelp from 'render-help'
import { type InstallCommandOptions } from './install'
import { installDeps } from './installDeps'
export const rcOptionsTypes = cliOptionsTypes
export function rcOptionsTypes () {
return {}
}
export function cliOptionsTypes () {
return {}
return {
...rcOptionsTypes(),
check: Boolean,
}
}
export const commandNames = ['dedupe']
@@ -20,6 +26,10 @@ export function help () {
title: 'Options',
list: [
...UNIVERSAL_OPTIONS,
{
description: 'Check if running dedupe would result in changes without installing packages or editing the lockfile. Exits with a non-zero status code if changes are possible.',
name: '--check',
},
],
},
],
@@ -28,7 +38,11 @@ export function help () {
})
}
export async function handler (opts: InstallCommandOptions) {
export interface DedupeCommandOptions extends InstallCommandOptions {
readonly check?: boolean
}
export async function handler (opts: DedupeCommandOptions) {
const include = {
dependencies: opts.production !== false,
devDependencies: opts.dev !== false,
@@ -39,5 +53,6 @@ export async function handler (opts: InstallCommandOptions) {
dedupe: true,
include,
includeDirect: include,
lockfileCheck: opts.check ? dedupeDiffCheck : undefined,
}, [])
}

View File

@@ -1,4 +1,5 @@
import path from 'path'
import { DedupeCheckIssuesError } from '@pnpm/dedupe.check'
import { readProjects } from '@pnpm/filter-workspace-packages'
import { type Lockfile } from '@pnpm/lockfile-types'
import { dedupe, install } from '@pnpm/plugin-commands-installation'
@@ -12,16 +13,64 @@ const f = fixtures(__dirname)
describe('pnpm dedupe', () => {
test('updates old resolutions from importers block and removes old packages', async () => {
const { originalLockfile, dedupedLockfile } = await testFixture('workspace-with-lockfile-dupes')
const { originalLockfile, dedupedLockfile, dedupeCheckError } = await testFixture('workspace-with-lockfile-dupes')
// Many old packages should be deleted as result of deduping. See snapshot file for details.
expect(diff(originalLockfile, dedupedLockfile, diffOptsForLockfile)).toMatchSnapshot()
expect(dedupeCheckError.dedupeCheckIssues).toEqual({
importerIssuesByImporterId: {
added: [],
removed: [],
updated: {
'packages/bar': {
ajv: {
next: '6.12.6',
prev: '6.10.2',
type: 'updated',
},
},
},
},
packageIssuesByDepPath: {
added: [],
removed: [
'/ajv/6.10.2',
'/fast-deep-equal/2.0.1',
'/fast-json-stable-stringify/2.0.0',
'/punycode/2.1.1',
'/uri-js/4.2.2',
],
updated: {},
},
})
})
test('updates old resolutions from package block', async () => {
const { originalLockfile, dedupedLockfile } = await testFixture('workspace-with-lockfile-subdep-dupes')
const { originalLockfile, dedupedLockfile, dedupeCheckError } = await testFixture('workspace-with-lockfile-subdep-dupes')
// This is a smaller scale test that should just update uri-js@4.2.2 to
// punycode@2.3.0 and remove punycode@2.1.1. See snapshot file for details.
expect(diff(originalLockfile, dedupedLockfile, diffOptsForLockfile)).toMatchSnapshot()
expect(dedupeCheckError.dedupeCheckIssues).toEqual({
importerIssuesByImporterId: {
added: [],
removed: [],
updated: {},
},
packageIssuesByDepPath: {
added: [],
removed: [
'/punycode/2.1.1',
],
updated: {
'/uri-js/4.2.2': {
punycode: {
next: '2.3.0',
prev: '2.1.1',
type: 'updated',
},
},
},
},
})
})
})
@@ -67,6 +116,21 @@ async function testFixture (fixtureName: string) {
await install.handler(opts)
expect(await readProjectLockfile()).toEqual(originalLockfile)
let dedupeCheckError: DedupeCheckIssuesError | undefined
try {
await dedupe.handler({ ...opts, check: true })
} catch (err: unknown) {
expect(err).toBeInstanceOf(DedupeCheckIssuesError)
dedupeCheckError = err as DedupeCheckIssuesError
} finally {
// The dedupe check option should never change the lockfile.
expect(await readProjectLockfile()).toEqual(originalLockfile)
}
if (dedupeCheckError == null) {
throw new Error('Expected change report from pnpm dedupe --check')
}
// The lockfile fixture has several packages that could be removed after
// re-resolving versions.
await dedupe.handler(opts)
@@ -88,5 +152,5 @@ async function testFixture (fixtureName: string) {
await install.handler(opts)
expect(await readProjectLockfile()).toEqual(dedupedLockfile)
return { originalLockfile, dedupedLockfile }
return { originalLockfile, dedupedLockfile, dedupeCheckError }
}

View File

@@ -33,6 +33,9 @@
{
"path": "../../config/matcher"
},
{
"path": "../../dedupe/check"
},
{
"path": "../../exec/plugin-commands-rebuild"
},

56
pnpm-lock.yaml generated
View File

@@ -433,6 +433,12 @@ importers:
'@pnpm/core-loggers':
specifier: workspace:*
version: link:../../packages/core-loggers
'@pnpm/dedupe.issues-renderer':
specifier: workspace:*
version: link:../../dedupe/issues-renderer
'@pnpm/dedupe.types':
specifier: workspace:*
version: link:../../dedupe/types
'@pnpm/error':
specifier: workspace:*
version: link:../../packages/error
@@ -746,6 +752,53 @@ importers:
specifier: 1.3.31
version: 1.3.31
dedupe/check:
dependencies:
'@pnpm/dedupe.types':
specifier: workspace:*
version: link:../types
'@pnpm/error':
specifier: workspace:*
version: link:../../packages/error
'@pnpm/lockfile-types':
specifier: workspace:*
version: link:../../lockfile/lockfile-types
'@pnpm/types':
specifier: workspace:*
version: link:../../packages/types
devDependencies:
'@pnpm/dedupe.check':
specifier: workspace:*
version: 'link:'
dedupe/issues-renderer:
dependencies:
'@pnpm/dedupe.types':
specifier: workspace:*
version: link:../types
archy:
specifier: ^1.0.0
version: 1.0.0
chalk:
specifier: ^4.1.2
version: 4.1.2
devDependencies:
'@pnpm/dedupe.issues-renderer':
specifier: workspace:*
version: 'link:'
'@types/archy':
specifier: 0.0.32
version: 0.0.32
strip-ansi:
specifier: ^6.0.1
version: 6.0.1
dedupe/types:
devDependencies:
'@pnpm/dedupe.types':
specifier: workspace:*
version: 'link:'
env/node.fetcher:
dependencies:
'@pnpm/create-cafs-store':
@@ -3526,6 +3579,9 @@ importers:
'@pnpm/core':
specifier: workspace:*
version: link:../core
'@pnpm/dedupe.check':
specifier: workspace:*
version: link:../../dedupe/check
'@pnpm/error':
specifier: workspace:*
version: link:../../packages/error

View File

@@ -4,6 +4,7 @@ packages:
- __utils__/*
- cli/*
- config/*
- dedupe/*
- env/*
- exec/*
- fetching/*