mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-24 07:38:12 -05:00
feat: add pnpm dedupe --check (#6409)
This commit is contained in:
6
.changeset/moody-jokes-destroy.md
Normal file
6
.changeset/moody-jokes-destroy.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/dedupe.issues-renderer": major
|
||||
"@pnpm/dedupe.check": major
|
||||
---
|
||||
|
||||
Initial release.
|
||||
6
.changeset/neat-cheetahs-turn.md
Normal file
6
.changeset/neat-cheetahs-turn.md
Normal 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.
|
||||
5
.changeset/poor-cobras-rule.md
Normal file
5
.changeset/poor-cobras-rule.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/dedupe.types": major
|
||||
---
|
||||
|
||||
Initial release.
|
||||
5
.changeset/weak-maps-teach.md
Normal file
5
.changeset/weak-maps-teach.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/default-reporter": minor
|
||||
---
|
||||
|
||||
Report errors from pnpm dedupe --check
|
||||
@@ -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:*",
|
||||
|
||||
@@ -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.
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
13
dedupe/check/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# @pnpm/dedupe.issues-renderer
|
||||
|
||||
> Logic for "pnpm dedupe --check"
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
pnpm add @pnpm/dedupe.check
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
1
dedupe/check/jest.config.js
Normal file
1
dedupe/check/jest.config.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('../../jest.config')
|
||||
43
dedupe/check/package.json
Normal file
43
dedupe/check/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
8
dedupe/check/src/DedupeCheckIssuesError.ts
Normal file
8
dedupe/check/src/DedupeCheckIssuesError.ts
Normal 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')
|
||||
}
|
||||
}
|
||||
101
dedupe/check/src/dedupeDiffCheck.ts
Normal file
101
dedupe/check/src/dedupeDiffCheck.ts
Normal 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
|
||||
}
|
||||
2
dedupe/check/src/index.ts
Normal file
2
dedupe/check/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { dedupeDiffCheck, countChangedSnapshots } from './dedupeDiffCheck'
|
||||
export { DedupeCheckIssuesError } from './DedupeCheckIssuesError'
|
||||
112
dedupe/check/test/dedupeDiffCheck.ts
Normal file
112
dedupe/check/test/dedupeDiffCheck.ts
Normal 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: {},
|
||||
},
|
||||
}))
|
||||
})
|
||||
})
|
||||
26
dedupe/check/tsconfig.json
Normal file
26
dedupe/check/tsconfig.json
Normal 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
|
||||
}
|
||||
8
dedupe/check/tsconfig.lint.json
Normal file
8
dedupe/check/tsconfig.lint.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts",
|
||||
"../../__typings__/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
13
dedupe/issues-renderer/README.md
Normal file
13
dedupe/issues-renderer/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# @pnpm/dedupe.issues-renderer
|
||||
|
||||
> Visualizes "pnpm dedupe --check" issues
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
pnpm add @pnpm/dedupe.issues-renderer
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
1
dedupe/issues-renderer/jest.config.js
Normal file
1
dedupe/issues-renderer/jest.config.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('../../jest.config')
|
||||
44
dedupe/issues-renderer/package.json
Normal file
44
dedupe/issues-renderer/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
56
dedupe/issues-renderer/src/index.ts
Normal file
56
dedupe/issues-renderer/src/index.ts
Normal 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)}` }
|
||||
}
|
||||
}
|
||||
56
dedupe/issues-renderer/test/__snapshots__/index.ts.snap
Normal file
56
dedupe/issues-renderer/test/__snapshots__/index.ts.snap
Normal 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
|
||||
"
|
||||
`;
|
||||
85
dedupe/issues-renderer/test/index.ts
Normal file
85
dedupe/issues-renderer/test/index.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
17
dedupe/issues-renderer/tsconfig.json
Normal file
17
dedupe/issues-renderer/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "@pnpm/tsconfig",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"../../__typings__/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../types"
|
||||
}
|
||||
],
|
||||
"composite": true
|
||||
}
|
||||
8
dedupe/issues-renderer/tsconfig.lint.json
Normal file
8
dedupe/issues-renderer/tsconfig.lint.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts",
|
||||
"../../__typings__/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
13
dedupe/types/README.md
Normal file
13
dedupe/types/README.md
Normal 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
36
dedupe/types/package.json
Normal 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:*"
|
||||
}
|
||||
}
|
||||
30
dedupe/types/src/DedupeCheckIssues.ts
Normal file
30
dedupe/types/src/DedupeCheckIssues.ts
Normal 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
|
||||
}
|
||||
1
dedupe/types/src/index.ts
Normal file
1
dedupe/types/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './DedupeCheckIssues'
|
||||
13
dedupe/types/tsconfig.json
Normal file
13
dedupe/types/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "@pnpm/tsconfig",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"../../__typings__/**/*.d.ts"
|
||||
],
|
||||
"references": [],
|
||||
"composite": true
|
||||
}
|
||||
8
dedupe/types/tsconfig.lint.json
Normal file
8
dedupe/types/tsconfig.lint.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts",
|
||||
"../../__typings__/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -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:*",
|
||||
|
||||
@@ -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,
|
||||
}, [])
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -33,6 +33,9 @@
|
||||
{
|
||||
"path": "../../config/matcher"
|
||||
},
|
||||
{
|
||||
"path": "../../dedupe/check"
|
||||
},
|
||||
{
|
||||
"path": "../../exec/plugin-commands-rebuild"
|
||||
},
|
||||
|
||||
56
pnpm-lock.yaml
generated
56
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -4,6 +4,7 @@ packages:
|
||||
- __utils__/*
|
||||
- cli/*
|
||||
- config/*
|
||||
- dedupe/*
|
||||
- env/*
|
||||
- exec/*
|
||||
- fetching/*
|
||||
|
||||
Reference in New Issue
Block a user