"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.YarnNodeModulesCollector = void 0;
const builder_util_1 = require("builder-util");
const lazy_val_1 = require("lazy-val");
const path = require("path");
const nodeModulesCollector_1 = require("./nodeModulesCollector");
const packageManager_1 = require("./packageManager");
class YarnNodeModulesCollector extends nodeModulesCollector_1.NodeModulesCollector {
    constructor() {
        super(...arguments);
        this.installOptions = {
            manager: packageManager_1.PM.YARN,
            lockfile: "yarn.lock",
        };
        this.isHoisted = new lazy_val_1.Lazy(async () => {
            return Promise.resolve(true); // Yarn Classic always hoists
        });
    }
    getArgs() {
        return ["list", "--production", "--json", "--depth=Infinity", "--no-progress"];
    }
    async extractProductionDependencyGraph(tree, dependencyId) {
        if (this.productionGraph[dependencyId]) {
            return;
        }
        const productionDeps = Object.entries(tree.dependencies || {}).map(async ([, dependency]) => {
            const dep = {
                ...dependency,
                path: await this.resolvePath(dependency.path),
            };
            const childDependencyId = this.packageVersionString(dep);
            await this.extractProductionDependencyGraph(dep, childDependencyId);
            return childDependencyId;
        });
        this.productionGraph[dependencyId] = { dependencies: await Promise.all(productionDeps) };
    }
    getDependencyType(pkgName, parentPkgJson) {
        var _a, _b, _c;
        if ((_a = parentPkgJson.optionalDependencies) === null || _a === void 0 ? void 0 : _a[pkgName]) {
            return "optional";
        }
        if ((_b = parentPkgJson.devDependencies) === null || _b === void 0 ? void 0 : _b[pkgName]) {
            return "dev";
        }
        if ((_c = parentPkgJson.dependencies) === null || _c === void 0 ? void 0 : _c[pkgName]) {
            return "prod";
        }
        return "prod";
    }
    async parseDependenciesTree(jsonBlob) {
        const lines = jsonBlob
            .split("\n")
            .map(l => l.trim())
            .filter(Boolean)
            .map(l => {
            try {
                return JSON.parse(l);
            }
            catch {
                return undefined;
            }
        })
            .filter(Boolean);
        const parsedTree = lines
            .filter(l => l.type === "tree")
            .map(l => l.data.trees)
            .shift();
        if (!parsedTree) {
            throw new Error('Failed to extract Yarn tree: no "type":"tree" line found in console output');
        }
        const normalizedTree = await this.normalizeTree(parsedTree, new Set(), undefined);
        const dependencies = {};
        for (const [name, dep] of Object.entries(normalizedTree)) {
            dependencies[name] = dep;
        }
        const rootPkgJson = await this.appPkgJson.value;
        return Promise.resolve({
            name: rootPkgJson.name,
            version: rootPkgJson.version,
            path: this.rootDir,
            dependencies,
            workspaces: rootPkgJson === null || rootPkgJson === void 0 ? void 0 : rootPkgJson.workspaces,
        });
    }
    async normalizeTree(tree, seen = new Set(), appName, parentPath = this.rootDir) {
        const normalized = {};
        for (const node of tree) {
            const match = node.name.match(/^(.*)@([^@]+)$/);
            if (!match) {
                builder_util_1.log.debug({ name: node.name }, "invalid node name format");
                continue;
            }
            const [, pkgName, version] = match;
            const id = `${pkgName}@${version}`;
            const isShadow = node.shadow && node.color === "dim";
            if (isShadow) {
                builder_util_1.log.debug({ pkgName, version }, "registering shadow node (hoisted elsewhere), will resolve");
            }
            if (seen.has(id)) {
                continue;
            }
            // For non-shadow nodes, try to resolve the actual path
            let pkgPath = null;
            // First try hoisted location (most common for Yarn Classic)
            const hoistedPath = path.join(this.rootDir, "node_modules", pkgName);
            if (await this.existsMemoized(hoistedPath)) {
                pkgPath = hoistedPath;
            }
            else {
                // If not hoisted, try nested under parent
                const nestedPath = path.join(parentPath, "node_modules", pkgName);
                if (await this.existsMemoized(nestedPath)) {
                    pkgPath = nestedPath;
                }
                else {
                    // Walk up the directory tree
                    let current = parentPath;
                    while (current !== this.rootDir && current !== path.dirname(current)) {
                        const candidatePath = path.join(current, "node_modules", pkgName);
                        if (await this.existsMemoized(candidatePath)) {
                            pkgPath = candidatePath;
                            break;
                        }
                        current = path.dirname(current);
                    }
                }
            }
            if (!pkgPath) {
                builder_util_1.log.warn({ pkgName, version, parentPath }, "could not find package in node_modules hierarchy");
                continue;
            }
            seen.add(id);
            const normalizedDep = {
                name: pkgName,
                version,
                path: pkgPath,
                dependencies: {},
                optionalDependencies: {},
            };
            builder_util_1.log.debug({ name: pkgName, version, path: pkgPath }, "+ normalize");
            // Process children (which may include shadow nodes that we'll skip)
            if (node.children && node.children.length > 0) {
                const childDeps = await this.normalizeTree(node.children, seen, appName, pkgPath);
                for (const [childDepName, childDep] of Object.entries(childDeps)) {
                    normalizedDep.dependencies[childDepName] = childDep;
                }
            }
            normalized[pkgName] = normalizedDep;
        }
        return normalized;
    }
    async collectAllDependencies(tree, packageToExclude) {
        const rootPkgJson = await this.appPkgJson.value;
        const failedPackages = new Set();
        builder_util_1.log.debug({ packageToExclude, hasWorkspaces: !!tree.workspaces }, "collectAllDependencies starting");
        const collect = async (deps = {}, isOptionalDependency, parentIsOptional = false) => {
            var _a, _b, _c, _d;
            for (const [, value] of Object.entries(deps)) {
                // Skip the app package if provided
                if (packageToExclude && value.name === packageToExclude) {
                    builder_util_1.log.debug({ name: value.name }, "skipping app package in collectAllDependencies");
                    continue;
                }
                const isRootOptional = !!((_a = rootPkgJson.optionalDependencies) === null || _a === void 0 ? void 0 : _a[value.name]);
                const isDirectRootDep = ((_b = rootPkgJson.dependencies) === null || _b === void 0 ? void 0 : _b[value.name]) || ((_c = rootPkgJson.optionalDependencies) === null || _c === void 0 ? void 0 : _c[value.name]) || ((_d = rootPkgJson.devDependencies) === null || _d === void 0 ? void 0 : _d[value.name]);
                const treatAsOptional = isOptionalDependency || parentIsOptional || isRootOptional;
                let p;
                try {
                    p = await this.resolvePath(value.path);
                }
                catch (e) {
                    if (treatAsOptional) {
                        builder_util_1.log.debug({ pkg: this.cacheKey(value), name: value.name }, "failed to resolve optional dependency, skipping");
                        failedPackages.add(value.name);
                        continue;
                    }
                    if (!isDirectRootDep) {
                        builder_util_1.log.debug({ pkg: this.cacheKey(value), name: value.name }, "failed to resolve transitive dependency, treating as optional");
                        failedPackages.add(value.name);
                        continue;
                    }
                    builder_util_1.log.error({ pkg: this.cacheKey(value) }, "failed to resolve module directory");
                    throw e;
                }
                if (!p) {
                    builder_util_1.log.debug({ pkg: this.cacheKey(value), name: value.name }, "optional dependency not found, skipping");
                    failedPackages.add(value.name);
                    continue;
                }
                let resolvedVersion = value.version;
                const versionMatch = p.match(/[/\\]node_modules[/\\][^@]+@([^/\\]+)[/\\]/);
                if (versionMatch) {
                    resolvedVersion = versionMatch[1];
                    if (resolvedVersion !== value.version) {
                        builder_util_1.log.debug({ name: value.name, declared: value.version, resolved: resolvedVersion }, "resolved actual version from path");
                    }
                }
                const m = {
                    ...value,
                    version: resolvedVersion,
                    path: p,
                };
                const moduleKey = this.packageVersionString(m);
                if (this.allDependencies.has(moduleKey)) {
                    continue;
                }
                this.allDependencies.set(moduleKey, m);
                const childIsOptional = treatAsOptional;
                await collect(m.dependencies, false, childIsOptional);
                await collect(m.optionalDependencies, true, true);
            }
        };
        await collect(tree.dependencies, false, false);
        await collect(tree.optionalDependencies, true, true);
        // Final cleanup: remove the app package from allDependencies
        if (packageToExclude) {
            for (const [key, dep] of this.allDependencies.entries()) {
                if (dep.name === packageToExclude) {
                    builder_util_1.log.debug({ key, name: dep.name }, "removing app package from allDependencies");
                    this.allDependencies.delete(key);
                }
            }
        }
        if (failedPackages.size > 0) {
            const cleanDependencies = (deps = {}) => {
                for (const [key, dep] of Object.entries(deps)) {
                    if (failedPackages.has(dep.name)) {
                        builder_util_1.log.debug({ name: dep.name }, "removing failed package from tree");
                        delete deps[key];
                    }
                    else {
                        if (dep.dependencies) {
                            cleanDependencies(dep.dependencies);
                        }
                        if (dep.optionalDependencies) {
                            cleanDependencies(dep.optionalDependencies);
                        }
                    }
                }
            };
            cleanDependencies(tree.dependencies);
            cleanDependencies(tree.optionalDependencies);
        }
    }
}
exports.YarnNodeModulesCollector = YarnNodeModulesCollector;
//# sourceMappingURL=yarnNodeModulesCollector.js.map