// dep-auditor/lockfile-parser.ts — Transitive dependency walker (SPEC-335)
// Reads pnpm-lock.yaml (v9) or package-lock.json (v2/v3) to build a full
// flat map of all installed packages including transitive dependencies.
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
/**
 * Parse transitive deps from the project lockfile.
 * Returns Map<packageName, installedVersion> for ALL deps (direct + transitive).
 * Falls back to direct deps from package.json if no lockfile found.
 */
export async function parseTransitiveDeps(projectPath) {
    // Try pnpm-lock.yaml first
    const pnpmResult = await tryParsePnpmLock(projectPath);
    if (pnpmResult !== null) {
        return pnpmResult;
    }
    // Try package-lock.json (npm v2/v3)
    const npmResult = await tryParsePackageLock(projectPath);
    if (npmResult !== null) {
        return npmResult;
    }
    // Fallback: read direct deps from package.json
    return readDirectDepsFromPackageJson(projectPath);
}
async function tryParsePnpmLock(projectPath) {
    const lockPath = join(projectPath, 'pnpm-lock.yaml');
    let raw;
    try {
        raw = await readFile(lockPath, 'utf-8');
    }
    catch {
        return null;
    }
    return parsePnpmLockYaml(raw);
}
/**
 * Parse pnpm-lock.yaml v9 format.
 * v9 format has a `snapshots:` section with entries like:
 *   pkg@version:
 *     ...
 * and a `packages:` section with entries like:
 *   /pkg/version:
 *     ...
 * We parse both sections to extract all package names and versions.
 */
export function parsePnpmLockYaml(raw) {
    const result = new Map();
    // pnpm-lock.yaml v9: snapshots section
    // Pattern: "  pkg@version:" or "  '@scope/pkg@version':" at start of line
    // Also handle: "  pkg@version(peer@ver):"
    // Scoped packages start with @scope/name, unscoped are plain names.
    const snapshotPattern = /^ {2}'?(@[^/@\s']+\/[^/@\s'(]+|[^/@\s'(]+)@([^:()\s']+)/gm;
    let match;
    while ((match = snapshotPattern.exec(raw)) !== null) {
        const name = match[1];
        const version = match[2];
        if (name && version && !result.has(name)) {
            result.set(name, version);
        }
    }
    // pnpm-lock.yaml v6/v8: packages section with /pkg/version: format
    // Pattern: "  /pkg/version:" — older pnpm format
    const packagePattern = /^ {2}\/(@?[^/\s]+(?:\/[^/\s]+)?)\/([^:()\s]+):/gm;
    while ((match = packagePattern.exec(raw)) !== null) {
        const name = match[1];
        const version = match[2];
        if (name && version && !result.has(name)) {
            result.set(name, version);
        }
    }
    return result;
}
async function tryParsePackageLock(projectPath) {
    const lockPath = join(projectPath, 'package-lock.json');
    let raw;
    try {
        raw = await readFile(lockPath, 'utf-8');
    }
    catch {
        return null;
    }
    try {
        return parsePackageLockJson(raw);
    }
    catch {
        return null;
    }
}
/**
 * Parse package-lock.json v2/v3 format.
 * v3: top-level `packages` object with keys like "node_modules/pkg" or
 *     "node_modules/pkg/node_modules/nested-pkg".
 * v2: both `packages` and `dependencies` present.
 * v1: only `dependencies` present (older format).
 */
export function parsePackageLockJson(raw) {
    const result = new Map();
    const parsed = JSON.parse(raw);
    // v2/v3: use packages section
    if (parsed.packages && typeof parsed.packages === 'object') {
        const pkgs = parsed.packages;
        for (const [key, entry] of Object.entries(pkgs)) {
            if (!key || key === '') {
                continue; // skip root package entry
            }
            const version = entry.version;
            if (!version) {
                continue;
            }
            // Extract package name from key: "node_modules/foo" -> "foo"
            // Scoped: "node_modules/@scope/foo" -> "@scope/foo"
            // Nested: "node_modules/foo/node_modules/bar" -> "bar" (use the deepest)
            const name = extractNameFromPackagesKey(key);
            if (name && !result.has(name)) {
                result.set(name, version);
            }
        }
        return result;
    }
    // v1 fallback: use dependencies section (flat list)
    if (parsed.dependencies && typeof parsed.dependencies === 'object') {
        flattenDependencies(parsed.dependencies, result);
    }
    return result;
}
function extractNameFromPackagesKey(key) {
    // Remove leading "node_modules/" segments, keep the last package name
    const parts = key.split('node_modules/');
    const lastPart = parts[parts.length - 1];
    if (!lastPart) {
        return null;
    }
    // Handle scoped packages like "@scope/name"
    return lastPart || null;
}
function flattenDependencies(deps, result) {
    for (const [name, entry] of Object.entries(deps)) {
        if (entry.version && !result.has(name)) {
            result.set(name, entry.version);
        }
        if (entry.dependencies && typeof entry.dependencies === 'object') {
            flattenDependencies(entry.dependencies, result);
        }
    }
}
async function readDirectDepsFromPackageJson(projectPath) {
    const result = new Map();
    try {
        const raw = await readFile(join(projectPath, 'package.json'), 'utf-8');
        const parsed = JSON.parse(raw);
        const allDeps = {
            ...(parsed.dependencies ?? {}),
            ...(parsed.devDependencies ?? {}),
        };
        for (const [name, version] of Object.entries(allDeps)) {
            result.set(name, version.replace(/^[\^~>=<]/, ''));
        }
    }
    catch {
        // no package.json or parse error — return empty map
    }
    return result;
}
//# sourceMappingURL=lockfile-parser.js.map