import yaml from 'js-yaml'
import type { Dependency } from './types'
import * as lockfile from '@yarnpkg/lockfile'
import { z } from 'zod'
// Schemas
const PackageJsonSchema = z.object({
dependencies: z.record(z.string()).optional(),
devDependencies: z.record(z.string()).optional(),
peerDependencies: z.record(z.string()).optional(),
})
const PackageLockV2Schema = z.object({
packages: z.record(z.object({
version: z.string().optional(),
dependencies: z.record(z.string()).optional()
})).optional(),
dependencies: z.record(z.object({
version: z.string(),
dependencies: z.record(z.any()).optional() // Recursive roughly
})).optional()
})
export function parsePackageJson(content: string): Dependency[] {
try {
const json = JSON.parse(content)
const parsed = PackageJsonSchema.parse(json)
const deps = { ...parsed.dependencies, ...parsed.devDependencies, ...parsed.peerDependencies }
return Object.entries(deps).map(([name, version]) => {
const cleanVersion = (version as string).replace(/^[\^~]/, '')
return { name, version: cleanVersion }
})
} catch (e) {
throw new Error(`Invalid package.json format: ${e instanceof Error ? e.message : String(e)}`)
}
}
export function parsePackageLock(content: string): Dependency[] {
try {
const json = JSON.parse(content)
const parsed = PackageLockV2Schema.parse(json)
const deps = new Map<string, Dependency>()
if (parsed.packages) {
for (const [key, val] of Object.entries(parsed.packages)) {
if (key === '' || !val.version) continue
const parts = key.split('node_modules/')
const name = parts[parts.length - 1]
deps.set(`${name}@${val.version}`, { name, version: val.version })
}
} else if (parsed.dependencies) {
const traverse = (d: Record<string, any>) => {
for (const [name, val] of Object.entries(d)) {
if (!val.version) continue;
deps.set(`${name}@${val.version}`, { name, version: val.version })
if (val.dependencies) traverse(val.dependencies)
}
}
traverse(parsed.dependencies)
}
return Array.from(deps.values())
} catch (e) {
throw new Error(`Invalid package-lock.json format: ${e instanceof Error ? e.message : String(e)}`)
}
}
export function parseYarnLock(content: string): Dependency[] {
const parsed = lockfile.parse(content)
if (parsed.type !== 'success') throw new Error('Failed to parse yarn.lock')
const deps = new Map<string, Dependency>()
for (const [key, val] of Object.entries(parsed.object)) {
const v = val as { version: string }
const name = key.substring(0, key.lastIndexOf('@'))
deps.set(`${name}@${v.version}`, { name, version: v.version })
}
return Array.from(deps.values())
}
export function parsePnpmLock(content: string): Dependency[] {
try {
const parsed = yaml.load(content) as any
// Basic validation that it's an object
if (!parsed || typeof parsed !== 'object') throw new Error("Invalid YAML")
const deps = new Map<string, Dependency>()
if (parsed.packages) {
for (const key of Object.keys(parsed.packages)) {
// Pnpm keys are weird, this is heuristic
const cleanKey = key.startsWith('/') ? key.slice(1) : key
const match = cleanKey.match(/^((?:@[^/]+\/)?[^/]+)\/(.+)$/)
if (match) {
const name = match[1]
const version = match[2].split('_')[0]
deps.set(`${name}@${version}`, { name, version })
}
}
}
return Array.from(deps.values())
} catch (e) {
throw new Error(`Failed to parse pnpm-lock.yaml: ${e instanceof Error ? e.message : String(e)}`)
}
}