import fs from 'fs/promises';
import path from 'path';
/**
* Parse dependencies from various package manager files
* @param {string} projectPath - Path to the project directory
* @param {string[]} files - List of files in the project
* @returns {Promise<{primary: string[], dev: string[], all: string[]}>}
*/
export async function parseDependencies(projectPath, files) {
const result = {
primary: [],
dev: [],
all: []
};
const fileSet = new Set(files.map(f => path.basename(f)));
// Parse package.json (Node.js/TypeScript)
if (fileSet.has('package.json')) {
try {
const pkgPath = path.join(projectPath, 'package.json');
const data = await fs.readFile(pkgPath, 'utf-8');
const pkg = JSON.parse(data);
if (pkg.dependencies) {
result.primary.push(...Object.keys(pkg.dependencies));
}
if (pkg.devDependencies) {
result.dev.push(...Object.keys(pkg.devDependencies));
}
} catch {
// Ignore parse errors
}
}
// Parse requirements.txt (Python)
if (fileSet.has('requirements.txt')) {
try {
const reqPath = path.join(projectPath, 'requirements.txt');
const data = await fs.readFile(reqPath, 'utf-8');
const deps = parseRequirementsTxt(data);
result.primary.push(...deps);
} catch {
// Ignore parse errors
}
}
// Parse pyproject.toml (Python - basic parsing)
if (fileSet.has('pyproject.toml')) {
try {
const tomlPath = path.join(projectPath, 'pyproject.toml');
const data = await fs.readFile(tomlPath, 'utf-8');
const deps = parsePyprojectToml(data);
result.primary.push(...deps);
} catch {
// Ignore parse errors
}
}
// Parse composer.json (PHP)
if (fileSet.has('composer.json')) {
try {
const composerPath = path.join(projectPath, 'composer.json');
const data = await fs.readFile(composerPath, 'utf-8');
const pkg = JSON.parse(data);
if (pkg.require) {
result.primary.push(...Object.keys(pkg.require).filter(k => k !== 'php'));
}
if (pkg['require-dev']) {
result.dev.push(...Object.keys(pkg['require-dev']));
}
} catch {
// Ignore parse errors
}
}
// Parse go.mod (Go)
if (fileSet.has('go.mod')) {
try {
const goModPath = path.join(projectPath, 'go.mod');
const data = await fs.readFile(goModPath, 'utf-8');
const deps = parseGoMod(data);
result.primary.push(...deps);
} catch {
// Ignore parse errors
}
}
// Parse Cargo.toml (Rust - basic parsing)
if (fileSet.has('Cargo.toml')) {
try {
const cargoPath = path.join(projectPath, 'Cargo.toml');
const data = await fs.readFile(cargoPath, 'utf-8');
const deps = parseCargoToml(data);
result.primary.push(...deps);
} catch {
// Ignore parse errors
}
}
// Deduplicate
result.primary = [...new Set(result.primary)];
result.dev = [...new Set(result.dev)];
result.all = [...new Set([...result.primary, ...result.dev])];
return result;
}
/**
* Parse requirements.txt format
*/
function parseRequirementsTxt(content) {
const deps = [];
const lines = content.split('\n');
for (const line of lines) {
const trimmed = line.trim();
// Skip comments and empty lines
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) {
continue;
}
// Extract package name (before ==, >=, <=, ~=, etc.)
const match = trimmed.match(/^([a-zA-Z0-9_-]+)/);
if (match) {
deps.push(match[1].toLowerCase());
}
}
return deps;
}
/**
* Parse pyproject.toml dependencies (basic regex-based)
*/
function parsePyprojectToml(content) {
const deps = [];
// Match dependencies array in [project] or [tool.poetry.dependencies]
const depsMatch = content.match(/dependencies\s*=\s*\[([\s\S]*?)\]/);
if (depsMatch) {
const depsBlock = depsMatch[1];
const packageMatches = depsBlock.matchAll(/"([a-zA-Z0-9_-]+)[\s\S]*?"/g);
for (const match of packageMatches) {
deps.push(match[1].toLowerCase());
}
}
// Also check poetry format
const poetryMatch = content.match(/\[tool\.poetry\.dependencies\]([\s\S]*?)(\[|$)/);
if (poetryMatch) {
const block = poetryMatch[1];
const lines = block.split('\n');
for (const line of lines) {
const match = line.match(/^([a-zA-Z0-9_-]+)\s*=/);
if (match && match[1] !== 'python') {
deps.push(match[1].toLowerCase());
}
}
}
return deps;
}
/**
* Parse go.mod require block
*/
function parseGoMod(content) {
const deps = [];
// Match single require statements
const singleMatches = content.matchAll(/^require\s+(\S+)/gm);
for (const match of singleMatches) {
deps.push(match[1]);
}
// Match require block
const blockMatch = content.match(/require\s*\(([\s\S]*?)\)/);
if (blockMatch) {
const lines = blockMatch[1].split('\n');
for (const line of lines) {
const match = line.trim().match(/^(\S+)/);
if (match && match[1] && !match[1].startsWith('//')) {
deps.push(match[1]);
}
}
}
return deps;
}
/**
* Parse Cargo.toml dependencies (basic regex-based)
*/
function parseCargoToml(content) {
const deps = [];
// Match [dependencies] section
const depsMatch = content.match(/\[dependencies\]([\s\S]*?)(\[|$)/);
if (depsMatch) {
const block = depsMatch[1];
const lines = block.split('\n');
for (const line of lines) {
const match = line.match(/^([a-zA-Z0-9_-]+)\s*=/);
if (match) {
deps.push(match[1]);
}
}
}
return deps;
}