fix: upgrade @pnpm/semver-diff and @pnpm/colorize-semver-diff to v2 (#11555)

- Upgrade `@pnpm/semver-diff` and `@pnpm/colorize-semver-diff` to v2, which expose the helpers as named exports.
- Update the call sites in `@pnpm/deps.inspection.commands` and `@pnpm/installing.commands` from `semverDiff.default(...)` / `colorizeSemverDiff.default(...)` to plain `semverDiff(...)` / `colorizeSemverDiff(...)`.
- Refactor `buildPkgChoice` in `getUpdateChoices.ts` to build the row as a `string[]`. Previously the row was an object whose values relied on `nextVersion` being inferred as `any` (a side effect of the broken `.default` access poisoning the type) — that masked `outdatedPkg.current` and `outdatedPkg.workspace` being `string | undefined`. With the v2 named imports the types tighten up, and `Object.values(lineParts)` would no longer assign cleanly to `string[]`.

The previous v1 packages exported their helpers as `module.exports.default = fn`, so `.default(...)` only worked through the legacy CJS interop — and it broke under Node.js ESM (which is what the Jest runner uses with `--experimental-vm-modules`). Most of the `deps/inspection/commands` outdated tests had been silently failing on `main` with `TypeError: semverDiff.default is not a function`; this change brings them back.
This commit is contained in:
Zoltan Kochan
2026-05-09 01:30:12 +02:00
parent a57f7bd9a4
commit 15e9e3556e
10 changed files with 92 additions and 72 deletions

View File

@@ -0,0 +1,8 @@
---
"@pnpm/deps.inspection.commands": patch
"@pnpm/installing.commands": patch
"@pnpm/lockfile.make-dedicated-lockfile": patch
"@pnpm/resolving.npm-resolver": patch
---
Upgrade `@pnpm/semver-diff`, `@pnpm/colorize-semver-diff`, `@pnpm/exec`, and `parse-npm-tarball-url` to versions that expose their helpers as named exports instead of CommonJS default exports. This eliminates the `.default` property accesses that broke under Node.js ESM interop in tests and could fail at runtime in some module loaders.

View File

@@ -8,7 +8,7 @@ import {
readProjectManifestOnly,
TABLE_OPTIONS,
} from '@pnpm/cli.utils'
import colorizeSemverDiff from '@pnpm/colorize-semver-diff'
import { colorizeSemverDiff } from '@pnpm/colorize-semver-diff'
import { type Config, type ConfigContext, types as allTypes } from '@pnpm/config.reader'
import {
outdatedDepsOfProjects,
@@ -16,7 +16,7 @@ import {
} from '@pnpm/deps.inspection.outdated'
import { PnpmError } from '@pnpm/error'
import { scanGlobalPackages } from '@pnpm/global.packages'
import semverDiff from '@pnpm/semver-diff'
import { semverDiff } from '@pnpm/semver-diff'
import type { DependenciesField, PackageManifest, ProjectManifest, ProjectRootDir } from '@pnpm/types'
import { table } from '@zkochan/table'
import chalk from 'chalk'
@@ -366,7 +366,7 @@ export function toOutdatedWithVersionDiff<Pkg extends OutdatedPackage> (outdated
if (outdated.latestManifest != null) {
return {
...outdated,
...semverDiff.default(outdated.wanted, outdated.latestManifest.version),
...semverDiff(outdated.wanted, outdated.latestManifest.version),
}
}
return {
@@ -398,7 +398,7 @@ export function renderLatest (outdatedPkg: OutdatedWithVersionDiff): string {
: latestManifest.version
}
const versionText = colorizeSemverDiff.default({ change, diff })
const versionText = colorizeSemverDiff({ change, diff })
if (latestManifest.deprecated) {
return `${versionText} ${chalk.redBright('(deprecated)')}`
}

View File

@@ -2,13 +2,13 @@ import { stripVTControlCharacters as stripAnsi } from 'node:util'
import { expect, test } from '@jest/globals'
import { outdated } from '@pnpm/deps.inspection.commands'
import semverDiff from '@pnpm/semver-diff'
import { semverDiff } from '@pnpm/semver-diff'
import type { PackageManifest } from '@pnpm/types'
import type { OutdatedWithVersionDiff } from '../../src/outdated/utils.js'
test('renderLatest: outdated and deprecated', () => {
const diffResult = semverDiff.default('0.0.1', '1.0.0')
const diffResult = semverDiff('0.0.1', '1.0.0')
const outdatedPkg: OutdatedWithVersionDiff = {
...diffResult,
alias: 'foo',
@@ -31,7 +31,7 @@ test('renderLatest: outdated and deprecated', () => {
})
test('renderLatest: outdated and not deprecated', () => {
const diffResult = semverDiff.default('0.0.1', '1.0.0')
const diffResult = semverDiff('0.0.1', '1.0.0')
const outdatedPkg: OutdatedWithVersionDiff = {
...diffResult,
alias: 'foo',

View File

@@ -17,8 +17,7 @@ import { readProjectManifestOnly } from '@pnpm/workspace.project-manifest-reader
import { findWorkspaceProjects } from '@pnpm/workspace.projects-reader'
import { sequenceGraph } from '@pnpm/workspace.projects-sorter'
import * as structUtils from '@yarnpkg/core/structUtils'
import type { LockFileObject } from '@yarnpkg/lockfile'
import yarnLockfileLib from '@yarnpkg/lockfile'
import { type LockFileObject, parse as parseYarnLockfile } from '@yarnpkg/lockfile'
import { parseSyml } from '@yarnpkg/parsers'
import { rimraf } from '@zkochan/rimraf'
import { loadJsonFile } from 'load-json-file'
@@ -196,7 +195,7 @@ async function readYarnLockFile (dir: string): Promise<LockFileObject> {
const yarnLockFile = await gfs.readFile(path.join(dir, 'yarn.lock'), 'utf8')
const yarnLockFileType = getYarnLockfileType(yarnLockFile)
if (yarnLockFileType === YarnLockType.yarn) {
const lockJsonFile = yarnLockfileLib.parse(yarnLockFile)
const lockJsonFile = parseYarnLockfile(yarnLockFile)
if (lockJsonFile.type === 'success') {
return lockJsonFile.object
} else {

View File

@@ -1,8 +1,8 @@
import { stripVTControlCharacters } from 'node:util'
import colorizeSemverDiff from '@pnpm/colorize-semver-diff'
import { colorizeSemverDiff } from '@pnpm/colorize-semver-diff'
import type { OutdatedPackage } from '@pnpm/deps.inspection.outdated'
import semverDiff from '@pnpm/semver-diff'
import { semverDiff } from '@pnpm/semver-diff'
import { getBorderCharacters, table } from '@zkochan/table'
import { and, groupBy, isEmpty, pickBy, pipe, pluck, uniqBy } from 'ramda'
@@ -51,9 +51,10 @@ export function getUpdateChoices (outdatedPkgsOfProjects: OutdatedPackage[], wor
if (choiceRows.length === 0) continue
const rawChoices: RawChoice[] = []
for (const choice of choiceRows) {
// The list of outdated dependencies also contains deprecated packages.
// But we only want to show those dependencies that have newer versions.
if (choice.latestManifest?.version !== choice.current) {
// The list of outdated dependencies also contains deprecated packages
// and entries from registries we cannot resolve against (no manifest).
// We only want to show those dependencies that have a known newer version.
if (choice.latestManifest != null && choice.latestManifest.version !== choice.current) {
rawChoices.push(buildPkgChoice(choice, workspacesEnabled))
}
}
@@ -97,27 +98,25 @@ interface RawChoice {
}
function buildPkgChoice (outdatedPkg: OutdatedPackage, workspacesEnabled: boolean): RawChoice {
const sdiff = semverDiff.default(outdatedPkg.wanted, outdatedPkg.latestManifest!.version)
const sdiff = semverDiff(outdatedPkg.wanted, outdatedPkg.latestManifest!.version)
const nextVersion = sdiff.change === null
? outdatedPkg.latestManifest!.version
: colorizeSemverDiff.default(sdiff as any) // eslint-disable-line @typescript-eslint/no-explicit-any
: colorizeSemverDiff(sdiff as any) // eslint-disable-line @typescript-eslint/no-explicit-any
const label = outdatedPkg.packageName
const lineParts = {
const raw: string[] = [
label,
current: outdatedPkg.current,
arrow: '',
outdatedPkg.current ?? '',
'',
nextVersion,
workspace: outdatedPkg.workspace,
url: getPkgUrl(outdatedPkg),
}
if (!workspacesEnabled) {
delete lineParts.workspace
]
if (workspacesEnabled) {
raw.push(outdatedPkg.workspace ?? '')
}
raw.push(getPkgUrl(outdatedPkg))
return {
raw: Object.values(lineParts),
raw,
name: outdatedPkg.packageName,
}
}

View File

@@ -1,4 +1,5 @@
import fs from 'node:fs'
import { createRequire } from 'node:module'
import path from 'node:path'
import { describe, expect, test } from '@jest/globals'
@@ -9,11 +10,16 @@ import { prepare } from '@pnpm/prepare'
import { fixtures } from '@pnpm/test-fixtures'
import { createTestIpcServer } from '@pnpm/test-ipc-server'
import { filterProjectsBySelectorObjectsFromDir } from '@pnpm/workspace.projects-filter'
import { diff } from 'jest-diff'
import type { diff as DiffFn } from 'jest-diff'
import { readYamlFileSync } from 'read-yaml-file'
import { DEFAULT_OPTS } from './utils/index.js'
// jest-diff's ESM entry re-exports off `import cjsModule from './index.js'`,
// which resolves to undefined under Jest's experimental VM modules. Load it
// through CJS to bypass the broken bridge.
const { diff } = createRequire(import.meta.url)('jest-diff') as { diff: typeof DiffFn }
const f = fixtures(import.meta.dirname)
const noColor = (str: string) => str

View File

@@ -1,6 +1,6 @@
import path from 'node:path'
import pnpmExec from '@pnpm/exec'
import { pnpmExec } from '@pnpm/exec'
import {
getLockfileImporterId,
type ProjectSnapshot,
@@ -58,7 +58,7 @@ export async function makeDedicatedLockfile (lockfileDir: string, projectDir: st
}
try {
await pnpmExec.default([
await pnpmExec([
'install',
'--frozen-lockfile',
'--lockfile-dir=.',

78
pnpm-lock.yaml generated
View File

@@ -239,8 +239,8 @@ catalogs:
specifier: ^1.0.0
version: 1.0.0
'@pnpm/colorize-semver-diff':
specifier: ^1.0.1
version: 1.0.1
specifier: ^2.0.0
version: 2.0.0
'@pnpm/config.env-replace':
specifier: ^3.0.2
version: 3.0.2
@@ -248,8 +248,8 @@ catalogs:
specifier: ^1.0.1
version: 1.0.1
'@pnpm/exec':
specifier: ^2.0.0
version: 2.0.0
specifier: ^4.0.0
version: 4.0.0
'@pnpm/log.group':
specifier: 3.0.2
version: 3.0.2
@@ -278,8 +278,8 @@ catalogs:
specifier: 6.0.0
version: 6.0.0
'@pnpm/semver-diff':
specifier: ^1.1.0
version: 1.1.0
specifier: ^2.0.0
version: 2.0.0
'@pnpm/tabtab':
specifier: ^0.5.4
version: 0.5.4
@@ -737,8 +737,8 @@ catalogs:
specifier: ^8.3.0
version: 8.3.0
parse-npm-tarball-url:
specifier: ^4.0.0
version: 4.0.0
specifier: ^5.0.0
version: 5.0.0
path-absolute:
specifier: ^2.0.0
version: 2.0.0
@@ -3344,7 +3344,7 @@ importers:
version: link:../../../cli/utils
'@pnpm/colorize-semver-diff':
specifier: 'catalog:'
version: 1.0.1
version: 2.0.0
'@pnpm/config.matcher':
specifier: workspace:*
version: link:../../../config/matcher
@@ -3404,7 +3404,7 @@ importers:
version: link:../../../resolving/registry/types
'@pnpm/semver-diff':
specifier: 'catalog:'
version: 1.1.0
version: 2.0.0
'@pnpm/store.path':
specifier: workspace:*
version: link:../../../store/path
@@ -5161,7 +5161,7 @@ importers:
version: link:../../cli/utils
'@pnpm/colorize-semver-diff':
specifier: 'catalog:'
version: 1.0.1
version: 2.0.0
'@pnpm/config.matcher':
specifier: workspace:*
version: link:../../config/matcher
@@ -5230,7 +5230,7 @@ importers:
version: link:../../resolving/resolver-base
'@pnpm/semver-diff':
specifier: 'catalog:'
version: 1.1.0
version: 2.0.0
'@pnpm/store.connection-manager':
specifier: workspace:*
version: link:../../store/connection-manager
@@ -6718,7 +6718,7 @@ importers:
version: link:../../core/error
'@pnpm/exec':
specifier: 'catalog:'
version: 2.0.0
version: 4.0.0
'@pnpm/lockfile.fs':
specifier: workspace:*
version: link:../fs
@@ -8540,7 +8540,7 @@ importers:
version: 8.0.0
parse-npm-tarball-url:
specifier: 'catalog:'
version: 4.0.0
version: 5.0.0
path-temp:
specifier: 'catalog:'
version: 3.0.0
@@ -10977,9 +10977,9 @@ packages:
resolution: {integrity: sha512-2UNrokmnXaD2dxQ4R0VzWJ4NjfTgJnIW2BhE7QLB5rgo/hyO5EUiZkDdCgJoxa38S0QxHJDKRRHHCkO/Pa+Etg==}
engines: {node: '>=18.12'}
'@pnpm/colorize-semver-diff@1.0.1':
resolution: {integrity: sha512-qP4E7mzmCBhB4so6szszeIdVDrcKGTTCxBazCKoiPUG34xLha6r57zJuFBkTmD65i3TB7++lf3BwpQruUwf/BQ==}
engines: {node: '>=10'}
'@pnpm/colorize-semver-diff@2.0.0':
resolution: {integrity: sha512-rdzxBK0DRDWKXNrkNL+jvzKm5BGuNqQOGYe/1QpJQhaBd6tmXwkQmbcKm18CJI9qQJFrPH2KZ/99cNhpwYPqvg==}
engines: {node: '>=22.13'}
'@pnpm/config.config-writer@1000.1.3':
resolution: {integrity: sha512-9W82H0iHWDDX6Jx6gk+4dghOzwcN6NLKh53lxN0n23dMnJyXreHWPAEQ2i0j2w3oSKJcg5GfdoyvuNn2YQvC0w==}
@@ -11083,9 +11083,9 @@ packages:
resolution: {integrity: sha512-IF1hAWdq61gCIbaCpy01SmoL5GLfJRC5/+qd0ZwBFdBeWEcEETCcuBYFFss71Q9OdjdTxNrNsz9apE3DPx1Tsg==}
engines: {node: '>=18.12'}
'@pnpm/exec@2.0.0':
resolution: {integrity: sha512-b5ALfWEOFQprWKntN7MF8XWCyslBk2c8u20GEDcDDQOs6c0HyHlWxX5lig8riQKdS000U6YyS4L4b32NOleXAQ==}
engines: {node: '>=10'}
'@pnpm/exec@4.0.0':
resolution: {integrity: sha512-iy8nX/dE+QqDtRRni1Qwt83uMuiWqbLfh7oECm4KbwnZrjOMj0RDEawoNOPdGE5snB4ChrhcEBI1F+61u3vT3w==}
engines: {node: '>=22.13'}
'@pnpm/fetch@1001.0.0':
resolution: {integrity: sha512-b5S29O4WaqxlFDpmyy1NWkvB3X+pz5kM2Se1ENSY7Rx+uKEIcRE1CzpRxpILDFf3uWHAOt90bBNywCeh6LQkKQ==}
@@ -11409,14 +11409,9 @@ packages:
resolution: {integrity: sha512-oxf+Y5lumvfNx9Igss/dvDb0t4iWtE0wjrmdDPFkVrXTlQbdWLujnyYSp3Obim/65K+r5RspoizJgjWJRe69nw==}
engines: {node: '>=18.12'}
'@pnpm/self-installer@2.2.1':
resolution: {integrity: sha512-aefLe96wAWghkx6q1PwbVS1Iz1iGE+HKwkTmtzWLFXeGhbknaIdG2voMwaBGIYGCSxm8sDKR1uLO4aRRAYuc+Q==}
engines: {node: '>=4'}
hasBin: true
'@pnpm/semver-diff@1.1.0':
resolution: {integrity: sha512-qQDQqPlveM7+Q0PMc21t1gfrCXMBe054HCdqJn3Hbo2WTDAlQjAShvTNDXaqVe/wx2JOnhA9GkQR4lk/zDrT4Q==}
engines: {node: '>=8.15'}
'@pnpm/semver-diff@2.0.0':
resolution: {integrity: sha512-BeJTTAegvEHqgldGtJuZMCC0IfC/mVyxR1parznqCTtPwQSYrjft4tDvKd+oBMJQjcl7sp70G8rPR89ecpeicg==}
engines: {node: '>=22.13'}
'@pnpm/semver.peer-range@1000.0.0':
resolution: {integrity: sha512-r6VzkrdH7ZKjPmAogTNvxuV/UyS/xwHNme+ZuEFiG0UthZgqudDftYtKmG20fcfrjG1lgJbbWICA8KvZy7mmbw==}
@@ -15653,6 +15648,10 @@ packages:
resolution: {integrity: sha512-XueE/Vkz0fzKhMu2L+pBrfpCn5lSnvdbfsVg5+hj0KWQ1VtcHhWRSDyiz9vEdj3I9DKHXoIUAVqkh3I1lwuP/g==}
engines: {node: '>=18.12'}
parse-npm-tarball-url@5.0.0:
resolution: {integrity: sha512-RIC1ICbP8vgo4Mt8v/FJNUo3yy/3gWs1Wf5vues+CMPrZPtGdwnAWTJxVgg3Kvk8CK4as8fpihOr/WDTMdifwg==}
engines: {node: '>=22.13'}
parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
@@ -15783,6 +15782,11 @@ packages:
resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==}
engines: {node: '>=10'}
pnpm@11.0.8:
resolution: {integrity: sha512-TECX4d0tQjcsTn+lp5H/KPx1pITHrBkuZLHfD97xdZS6mC+bT+2a37PHV4RvVlt5mydj+zcz0d4by4LPRmhJEg==}
engines: {node: '>=22.13'}
hasBin: true
possible-typed-array-names@1.1.0:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
@@ -18737,9 +18741,9 @@ snapshots:
- supports-color
- typanion
'@pnpm/colorize-semver-diff@1.0.1':
'@pnpm/colorize-semver-diff@2.0.0':
dependencies:
chalk: 4.1.2
chalk: 5.6.2
'@pnpm/config.config-writer@1000.1.3(@pnpm/logger@1001.0.1)':
dependencies:
@@ -18938,11 +18942,11 @@ snapshots:
dependencies:
'@pnpm/types': 1001.3.0
'@pnpm/exec@2.0.0':
'@pnpm/exec@4.0.0':
dependencies:
'@pnpm/self-installer': 2.2.1
command-exists: 1.2.9
cross-spawn: 7.0.6
pnpm: 11.0.8
'@pnpm/fetch@1001.0.0(@pnpm/logger@1001.0.1)':
dependencies:
@@ -19601,9 +19605,7 @@ snapshots:
dependencies:
'@pnpm/error': 1000.1.0
'@pnpm/self-installer@2.2.1': {}
'@pnpm/semver-diff@1.1.0': {}
'@pnpm/semver-diff@2.0.0': {}
'@pnpm/semver.peer-range@1000.0.0':
dependencies:
@@ -24777,6 +24779,10 @@ snapshots:
dependencies:
semver: 7.7.4
parse-npm-tarball-url@5.0.0:
dependencies:
semver: 7.7.4
parseurl@1.3.3: {}
path-absolute@1.0.1: {}
@@ -24885,6 +24891,8 @@ snapshots:
dependencies:
find-up: 5.0.0
pnpm@11.0.8: {}
possible-typed-array-names@1.1.0: {}
postman-request@2.88.1-postman.40:

View File

@@ -80,10 +80,10 @@ catalog:
'@jest/globals': 30.3.0
'@npm/types': ^2.1.0
'@pnpm/byline': ^1.0.0
'@pnpm/colorize-semver-diff': ^1.0.1
'@pnpm/colorize-semver-diff': ^2.0.0
'@pnpm/config.env-replace': ^3.0.2
'@pnpm/config.nerf-dart': ^1.0.1
'@pnpm/exec': ^2.0.0
'@pnpm/exec': ^4.0.0
'@pnpm/log.group': 3.0.2
'@pnpm/logger': '^1001.0.1'
'@pnpm/meta-updater': 2.0.6
@@ -93,7 +93,7 @@ catalog:
'@pnpm/os.env.path-extender': ^3.0.0
'@pnpm/patch-package': 0.0.1
'@pnpm/registry-mock': 6.0.0
'@pnpm/semver-diff': ^1.1.0
'@pnpm/semver-diff': ^2.0.0
'@pnpm/tabtab': ^0.5.4
'@pnpm/tgz-fixtures': 0.0.0
'@pnpm/util.lex-comparator': ^3.0.2
@@ -248,7 +248,7 @@ catalog:
p-memoize: 8.0.0
p-queue: ^9.2.0
parse-json: ^8.3.0
parse-npm-tarball-url: ^4.0.0
parse-npm-tarball-url: ^5.0.0
path-absolute: ^2.0.0
path-exists: ^5.0.0
path-name: ^1.0.0
@@ -362,7 +362,7 @@ minimumReleaseAgeExclude:
- make-empty-dir
- normalize-registry-url
- p-map-values
- parse-npm-tarball-url@4.0.0
- parse-npm-tarball-url@5.0.0
- path-absolute
- path-temp
- pnpm

View File

@@ -1,5 +1,5 @@
import { parseJsrSpecifier } from '@pnpm/resolving.jsr-specifier-parser'
import parseNpmTarballUrl from 'parse-npm-tarball-url'
import { parseNpmTarballUrl } from 'parse-npm-tarball-url'
import getVersionSelectorType from 'version-selector-type'
export interface RegistryPackageSpec {
@@ -38,7 +38,7 @@ export function parseBareSpecifier (
}
}
if (bareSpecifier.startsWith(registry)) {
const pkg = parseNpmTarballUrl.default(bareSpecifier)
const pkg = parseNpmTarballUrl(bareSpecifier)
if (pkg != null) {
return {
fetchSpec: pkg.version,