Skip to content

Cleanup pnpm helpers #566

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 11 additions & 18 deletions src/commands/fix/pnpm-fix.mts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { existsSync } from 'node:fs'
import path from 'node:path'

import yaml from 'js-yaml'

import { getManifestData } from '@socketsecurity/registry'
import { arrayUnique } from '@socketsecurity/registry/lib/arrays'
import { debugLog, isDebug } from '@socketsecurity/registry/lib/debug'
Expand All @@ -13,7 +10,6 @@ import {
readPackageJson,
} from '@socketsecurity/registry/lib/packages'
import { naturalCompare } from '@socketsecurity/registry/lib/sorts'
import { stripBom } from '@socketsecurity/registry/lib/strings'

import {
getBaseGitBranch,
Expand Down Expand Up @@ -48,9 +44,12 @@ import {
getAlertsMapFromPnpmLockfile,
getAlertsMapFromPurls,
} from '../../utils/alerts-map.mts'
import { readFileUtf8, removeNodeModules } from '../../utils/fs.mts'
import { removeNodeModules } from '../../utils/fs.mts'
import { globWorkspace } from '../../utils/glob.mts'
import { parsePnpmLockfileVersion } from '../../utils/pnpm.mts'
import {
parsePnpmLockfileVersion,
readPnpmLockfile,
} from '../../utils/pnpm.mts'
import { applyRange } from '../../utils/semver.mts'
import { getCveInfoFromAlertsMap } from '../../utils/socket-package-alert.mts'
import { idToPurl } from '../../utils/spec.mts'
Expand All @@ -59,28 +58,22 @@ import type { NormalizedFixOptions } from './types.mts'
import type { SafeNode } from '../../shadow/npm/arborist/lib/node.mts'
import type { StringKeyValueObject } from '../../types.mts'
import type { EnvDetails } from '../../utils/package-environment.mts'
import type { LockfileObject } from '@pnpm/lockfile.fs'
import type { PackageJson } from '@socketsecurity/registry/lib/packages'
import type { Spinner } from '@socketsecurity/registry/lib/spinner'

const { DRY_RUN_NOT_SAVING, NPM, OVERRIDES, PNPM } = constants

async function getActualTree(cwd: string = process.cwd()): Promise<SafeNode> {
// npm DOES have some support pnpm structured node_modules folders. However,
// the support is a little iffy where the unhappy path errors. So, we restrict
// our usage to --dry-run loading of the node_modules folder.
const arb = new SafeArborist({
path: cwd,
...SAFE_ARBORIST_REIFY_OPTIONS_OVERRIDES,
})
return await arb.loadActual()
}

async function readLockfile(
lockfilePath: string,
): Promise<LockfileObject | null> {
return existsSync(lockfilePath)
? (yaml.load(stripBom(await readFileUtf8(lockfilePath))) as LockfileObject)
: null
}

type InstallOptions = {
args?: string[] | undefined
cwd?: string | undefined
Expand Down Expand Up @@ -136,12 +129,12 @@ export async function pnpmFix(

let actualTree: SafeNode | undefined
const lockfilePath = path.join(rootPath, 'pnpm-lock.yaml')
let lockfile = await readLockfile(lockfilePath)
let lockfile = await readPnpmLockfile(lockfilePath)

// If pnpm-lock.yaml does NOT exist then install with pnpm to create it.
if (!lockfile) {
actualTree = await install(pkgEnvDetails, { cwd, spinner })
lockfile = await readLockfile(lockfilePath)
lockfile = await readPnpmLockfile(lockfilePath)
}
// Update pnpm-lock.yaml if its version is older than what the installed pnpm
// produces.
Expand All @@ -155,7 +148,7 @@ export async function pnpmFix(
cwd,
spinner,
})
lockfile = await readLockfile(lockfilePath)
lockfile = await readPnpmLockfile(lockfilePath)
}
// Exit early if pnpm-lock.yaml is not found.
if (!lockfile) {
Expand Down
16 changes: 9 additions & 7 deletions src/shadow/npm/arborist-helpers.mts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function findPackageNode(
throw new Error('Detected infinite loop in findPackageNode')
}
const nodeOrLink = queue.pop()!
const node = nodeOrLink.isLink ? nodeOrLink.target : nodeOrLink
const node = getTargetNode(nodeOrLink)
if (visited.has(node)) {
continue
}
Expand Down Expand Up @@ -118,7 +118,7 @@ export function findPackageNodes(
throw new Error('Detected infinite loop in findPackageNodes')
}
const nodeOrLink = queue.pop()!
const node = nodeOrLink.isLink ? nodeOrLink.target : nodeOrLink
const node = getTargetNode(nodeOrLink)
if (visited.has(node)) {
continue
}
Expand Down Expand Up @@ -299,12 +299,14 @@ export function getDetailsFromDiff(
return details
}

export function getTargetNode(nodeOrLink: SafeNode | LinkClass): SafeNode
export function getTargetNode<T>(nodeOrLink: T): SafeNode | null
export function getTargetNode(nodeOrLink: any): SafeNode | null {
return nodeOrLink?.isLink ? nodeOrLink.target : (nodeOrLink ?? null)
}

export function isTopLevel(tree: SafeNode, node: SafeNode): boolean {
const childNodeOrLink = tree.children.get(node.name)
const childNode = childNodeOrLink?.isLink
? childNodeOrLink.target
: childNodeOrLink
return childNode === node
return getTargetNode(tree.children.get(node.name)) === node
}

export type Packument = Exclude<
Expand Down
40 changes: 32 additions & 8 deletions src/utils/pnpm.mts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { existsSync } from 'node:fs'

import yaml from 'js-yaml'
import semver from 'semver'

import {
idToPurl,
isDepPath,
stripLeadingSlash,
stripPeerSuffix,
} from './spec.mts'
import { stripBom } from '@socketsecurity/registry/lib/strings'

import { readFileUtf8 } from './fs.mts'
import { idToPurl } from './spec.mts'

import type { LockfileObject, PackageSnapshot } from '@pnpm/lockfile.fs'
import type { SemVer } from 'semver'
Expand All @@ -32,16 +33,39 @@ export async function extractPurlsFromPnpmLockfile(
}
for (const depName in deps) {
const ref = deps[depName]!
const subKey = isDepPath(ref) ? ref : `/${depName}@${ref}`
const subKey = isPnpmDepPath(ref) ? ref : `/${depName}@${ref}`
visit(subKey)
}
}
for (const pkgPath of Object.keys(packages)) {
visit(pkgPath)
}
return [...seen].map(p => idToPurl(stripPeerSuffix(stripLeadingSlash(p))))
return [...seen].map(p =>
idToPurl(stripPnpmPeerSuffix(stripLeadingPnpmDepPathSlash(p))),
)
}

export function isPnpmDepPath(maybeDepPath: string): boolean {
return maybeDepPath.length > 0 && maybeDepPath.charCodeAt(0) === 47 /*'/'*/
}

export function parsePnpmLockfileVersion(version: string): SemVer {
return semver.coerce(version)!
}

export async function readPnpmLockfile(
lockfilePath: string,
): Promise<LockfileObject | null> {
return existsSync(lockfilePath)
? (yaml.load(stripBom(await readFileUtf8(lockfilePath))) as LockfileObject)
: null
}

export function stripLeadingPnpmDepPathSlash(depPath: string): string {
return isPnpmDepPath(depPath) ? depPath.slice(1) : depPath
}

export function stripPnpmPeerSuffix(depPath: string): string {
const index = depPath.indexOf('(')
return index === -1 ? depPath : depPath.slice(0, index)
}
19 changes: 5 additions & 14 deletions src/utils/spec.mts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,15 @@ import semver from 'semver'

import { PackageURL } from '@socketregistry/packageurl-js'

import { stripPnpmPeerSuffix } from './pnpm.mts'

export function idToPurl(id: string): string {
return `pkg:npm/${id}`
}

export function isDepPath(maybeDepPath: string): boolean {
return maybeDepPath.length > 0 && maybeDepPath.charCodeAt(0) === 47 /*'/'*/
}

export function resolvePackageVersion(purlObj: PackageURL): string {
const { version } = purlObj
return version ? (semver.coerce(stripPeerSuffix(version))?.version ?? '') : ''
}

export function stripLeadingSlash(depPath: string): string {
return isDepPath(depPath) ? depPath.slice(1) : depPath
}

export function stripPeerSuffix(depPath: string): string {
const index = depPath.indexOf('(')
return index === -1 ? depPath : depPath.slice(0, index)
return version
? (semver.coerce(stripPnpmPeerSuffix(version))?.version ?? '')
: ''
}
Loading