"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NX_VERSION = void 0;
exports.getDependencyVersionFromPackageJson = getDependencyVersionFromPackageJson;
exports.addDependenciesToPackageJson = addDependenciesToPackageJson;
exports.removeDependenciesFromPackageJson = removeDependenciesFromPackageJson;
exports.ensurePackage = ensurePackage;
const fs_1 = require("fs");
const module_1 = require("module");
const devkit_exports_1 = require("nx/src/devkit-exports");
const devkit_internals_1 = require("nx/src/devkit-internals");
const path_1 = require("path");
const semver_1 = require("semver");
const install_packages_task_1 = require("../tasks/install-packages-task");
const catalog_1 = require("./catalog");
const UNIDENTIFIED_VERSION = 'UNIDENTIFIED_VERSION';
const NON_SEMVER_TAGS = {
    '*': 2,
    [UNIDENTIFIED_VERSION]: 2,
    next: 1,
    latest: 0,
    previous: -1,
    legacy: -2,
};
function getDependencyVersionFromPackageJson(treeOrPackageName, packageNameOrRoot, packageJsonPathOrObjectOrRoot, dependencyLookup) {
    if (typeof treeOrPackageName !== 'string') {
        return getDependencyVersionFromPackageJsonFromTree(treeOrPackageName, packageNameOrRoot, packageJsonPathOrObjectOrRoot, dependencyLookup);
    }
    else {
        return getDependencyVersionFromPackageJsonFromFileSystem(treeOrPackageName, packageNameOrRoot, packageJsonPathOrObjectOrRoot, dependencyLookup);
    }
}
/**
 * Tree-based implementation for getDependencyVersionFromPackageJson
 */
function getDependencyVersionFromPackageJsonFromTree(tree, packageName, packageJsonPathOrObject = 'package.json', dependencyLookup = [
    'dependencies',
    'devDependencies',
]) {
    let packageJson;
    if (typeof packageJsonPathOrObject === 'object') {
        packageJson = packageJsonPathOrObject;
    }
    else if (tree.exists(packageJsonPathOrObject)) {
        packageJson = (0, devkit_exports_1.readJson)(tree, packageJsonPathOrObject);
    }
    else {
        return null;
    }
    let version = null;
    for (const section of dependencyLookup) {
        const foundVersion = packageJson[section]?.[packageName];
        if (foundVersion) {
            version = foundVersion;
            break;
        }
    }
    // Resolve catalog reference if needed
    const manager = (0, catalog_1.getCatalogManager)(tree.root);
    if (version && manager?.isCatalogReference(version)) {
        version = manager.resolveCatalogReference(tree, packageName, version);
    }
    return version;
}
/**
 * Filesystem-based implementation for getDependencyVersionFromPackageJson
 */
function getDependencyVersionFromPackageJsonFromFileSystem(packageName, root = devkit_exports_1.workspaceRoot, packageJsonPathOrObject = 'package.json', dependencyLookup = [
    'dependencies',
    'devDependencies',
]) {
    let packageJson;
    if (typeof packageJsonPathOrObject === 'object') {
        packageJson = packageJsonPathOrObject;
    }
    else {
        const packageJsonPath = (0, path_1.resolve)(root, packageJsonPathOrObject);
        if ((0, fs_1.existsSync)(packageJsonPath)) {
            packageJson = (0, devkit_exports_1.readJsonFile)(packageJsonPath);
        }
        else {
            return null;
        }
    }
    let version = null;
    for (const section of dependencyLookup) {
        const foundVersion = packageJson[section]?.[packageName];
        if (foundVersion) {
            version = foundVersion;
            break;
        }
    }
    // Resolve catalog reference if needed
    const manager = (0, catalog_1.getCatalogManager)(root);
    if (version && manager?.isCatalogReference(version)) {
        version = manager.resolveCatalogReference(root, packageName, version);
    }
    return version;
}
function filterExistingDependencies(dependencies, existingAltDependencies) {
    if (!existingAltDependencies) {
        return dependencies;
    }
    return Object.keys(dependencies ?? {})
        .filter((d) => !existingAltDependencies[d])
        .reduce((acc, d) => ({ ...acc, [d]: dependencies[d] }), {});
}
function cleanSemver(tree, version, packageName) {
    const manager = (0, catalog_1.getCatalogManager)(tree.root);
    if (manager?.isCatalogReference(version)) {
        const resolvedVersion = manager.resolveCatalogReference(tree, packageName, version);
        if (!resolvedVersion) {
            throw new Error(`Failed to resolve catalog reference '${version}' for package '${packageName}'`);
        }
        return (0, semver_1.clean)(resolvedVersion) ?? (0, semver_1.coerce)(resolvedVersion);
    }
    return (0, semver_1.clean)(version) ?? (0, semver_1.coerce)(version);
}
function isIncomingVersionGreater(tree, incomingVersion, existingVersion, packageName) {
    // the existing version might be a catalog reference, so we need to resolve
    // it if that's the case
    let resolvedExistingVersion = existingVersion;
    const manager = (0, catalog_1.getCatalogManager)(tree.root);
    if (manager?.isCatalogReference(existingVersion)) {
        const resolved = manager.resolveCatalogReference(tree, packageName, existingVersion);
        if (!resolved) {
            // catalog is supported, but failed to resolve, we throw an error
            throw new Error(`Failed to resolve catalog reference '${existingVersion}' for package '${packageName}'`);
        }
        resolvedExistingVersion = resolved;
    }
    // if version is in the format of "latest", "next" or similar - keep it, otherwise try to parse it
    const incomingVersionCompareBy = incomingVersion in NON_SEMVER_TAGS
        ? incomingVersion
        : (cleanSemver(tree, incomingVersion, packageName)?.toString() ??
            UNIDENTIFIED_VERSION);
    const existingVersionCompareBy = resolvedExistingVersion in NON_SEMVER_TAGS
        ? resolvedExistingVersion
        : (cleanSemver(tree, resolvedExistingVersion, packageName)?.toString() ??
            UNIDENTIFIED_VERSION);
    if (incomingVersionCompareBy in NON_SEMVER_TAGS &&
        existingVersionCompareBy in NON_SEMVER_TAGS) {
        return (NON_SEMVER_TAGS[incomingVersionCompareBy] >
            NON_SEMVER_TAGS[existingVersionCompareBy]);
    }
    if (incomingVersionCompareBy in NON_SEMVER_TAGS ||
        existingVersionCompareBy in NON_SEMVER_TAGS) {
        return true;
    }
    return (0, semver_1.gt)(cleanSemver(tree, incomingVersion, packageName), cleanSemver(tree, resolvedExistingVersion, packageName));
}
function updateExistingAltDependenciesVersion(tree, dependencies, existingAltDependencies, workspaceRootPath) {
    return Object.keys(existingAltDependencies || {})
        .filter((d) => {
        if (!dependencies[d]) {
            return false;
        }
        const incomingVersion = dependencies[d];
        const existingVersion = existingAltDependencies[d];
        return isIncomingVersionGreater(tree, incomingVersion, existingVersion, d);
    })
        .reduce((acc, d) => ({ ...acc, [d]: dependencies[d] }), {});
}
function updateExistingDependenciesVersion(tree, dependencies, existingDependencies = {}, workspaceRootPath) {
    return Object.keys(dependencies)
        .filter((d) => {
        if (!existingDependencies[d]) {
            return true;
        }
        const incomingVersion = dependencies[d];
        const existingVersion = existingDependencies[d];
        return isIncomingVersionGreater(tree, incomingVersion, existingVersion, d);
    })
        .reduce((acc, d) => ({ ...acc, [d]: dependencies[d] }), {});
}
/**
 * Add Dependencies and Dev Dependencies to package.json
 *
 * For example:
 * ```typescript
 * addDependenciesToPackageJson(tree, { react: 'latest' }, { jest: 'latest' })
 * ```
 * This will **add** `react` and `jest` to the dependencies and devDependencies sections of package.json respectively.
 *
 * @param tree Tree representing file system to modify
 * @param dependencies Dependencies to be added to the dependencies section of package.json
 * @param devDependencies Dependencies to be added to the devDependencies section of package.json
 * @param packageJsonPath Path to package.json
 * @param keepExistingVersions If true, prevents existing dependencies from being bumped to newer versions
 * @returns Callback to install dependencies only if necessary, no-op otherwise
 */
function addDependenciesToPackageJson(tree, dependencies, devDependencies, packageJsonPath = 'package.json', keepExistingVersions) {
    const currentPackageJson = (0, devkit_exports_1.readJson)(tree, packageJsonPath);
    /** Dependencies to install that are not met in dev dependencies */
    let filteredDependencies = filterExistingDependencies(dependencies, currentPackageJson.devDependencies);
    /** Dev dependencies to install that are not met in dependencies */
    let filteredDevDependencies = filterExistingDependencies(devDependencies, currentPackageJson.dependencies);
    // filtered dependencies should consist of:
    // - dependencies of the same type that are not present
    // by default, filtered dependencies also include these (unless keepExistingVersions is true):
    // - dependencies of the same type that have greater version
    // - specified dependencies of the other type that have greater version and are already installed as current type
    filteredDependencies = {
        ...updateExistingDependenciesVersion(tree, filteredDependencies, currentPackageJson.dependencies, tree.root),
        ...updateExistingAltDependenciesVersion(tree, devDependencies, currentPackageJson.dependencies, tree.root),
    };
    filteredDevDependencies = {
        ...updateExistingDependenciesVersion(tree, filteredDevDependencies, currentPackageJson.devDependencies, tree.root),
        ...updateExistingAltDependenciesVersion(tree, dependencies, currentPackageJson.devDependencies, tree.root),
    };
    if (keepExistingVersions) {
        filteredDependencies = removeExistingDependencies(filteredDependencies, currentPackageJson.dependencies);
        filteredDevDependencies = removeExistingDependencies(filteredDevDependencies, currentPackageJson.devDependencies);
    }
    else {
        filteredDependencies = removeLowerVersions(tree, filteredDependencies, currentPackageJson.dependencies, tree.root);
        filteredDevDependencies = removeLowerVersions(tree, filteredDevDependencies, currentPackageJson.devDependencies, tree.root);
    }
    if (requiresAddingOfPackages(tree, currentPackageJson, filteredDependencies, filteredDevDependencies, tree.root)) {
        const { catalogUpdates, directDependencies, directDevDependencies } = splitDependenciesByCatalogType(tree, filteredDependencies, filteredDevDependencies, packageJsonPath);
        writeCatalogDependencies(tree, catalogUpdates);
        writeDirectDependencies(tree, packageJsonPath, directDependencies, directDevDependencies);
        return () => {
            (0, install_packages_task_1.installPackagesTask)(tree);
        };
    }
    return () => { };
}
function splitDependenciesByCatalogType(tree, filteredDependencies, filteredDevDependencies, packageJsonPath) {
    const allFilteredUpdates = {
        ...filteredDependencies,
        ...filteredDevDependencies,
    };
    const catalogUpdates = [];
    let directDependencies = { ...filteredDependencies };
    let directDevDependencies = { ...filteredDevDependencies };
    const manager = (0, catalog_1.getCatalogManager)(tree.root);
    if (!manager) {
        return {
            catalogUpdates: [],
            directDependencies: filteredDependencies,
            directDevDependencies: filteredDevDependencies,
        };
    }
    const existingCatalogDeps = (0, catalog_1.getCatalogDependenciesFromPackageJson)(tree, packageJsonPath, manager);
    if (!existingCatalogDeps.size) {
        return {
            catalogUpdates: [],
            directDependencies: filteredDependencies,
            directDevDependencies: filteredDevDependencies,
        };
    }
    // Check filtered results for catalog references or existing catalog dependencies
    for (const [packageName, version] of Object.entries(allFilteredUpdates)) {
        if (!existingCatalogDeps.has(packageName)) {
            continue;
        }
        let catalogName = existingCatalogDeps.get(packageName);
        const catalogRef = catalogName ? `catalog:${catalogName}` : 'catalog:';
        try {
            manager.validateCatalogReference(tree, packageName, catalogRef);
            catalogUpdates.push({ packageName, version, catalogName });
            // Remove from direct updates since this will be handled via catalog
            delete directDependencies[packageName];
            delete directDevDependencies[packageName];
        }
        catch (error) {
            devkit_exports_1.output.error({
                title: 'Invalid catalog reference',
                bodyLines: [
                    `Invalid catalog reference "${catalogRef}" for package "${packageName}".`,
                    error.message,
                ],
            });
            throw new Error(`Could not update "${packageName}" to version "${version}". See above for more details.`);
        }
    }
    return { catalogUpdates, directDependencies, directDevDependencies };
}
function writeCatalogDependencies(tree, catalogUpdates) {
    if (!catalogUpdates.length) {
        return;
    }
    const manager = (0, catalog_1.getCatalogManager)(tree.root);
    manager.updateCatalogVersions(tree, catalogUpdates);
}
function writeDirectDependencies(tree, packageJsonPath, dependencies, devDependencies) {
    (0, devkit_exports_1.updateJson)(tree, packageJsonPath, (json) => {
        json.dependencies = {
            ...(json.dependencies || {}),
            ...dependencies,
        };
        json.devDependencies = {
            ...(json.devDependencies || {}),
            ...devDependencies,
        };
        json.dependencies = sortObjectByKeys(json.dependencies);
        json.devDependencies = sortObjectByKeys(json.devDependencies);
        return json;
    });
}
/**
 * @returns The the incoming dependencies that are higher than the existing verions
 **/
function removeLowerVersions(tree, incomingDeps, existingDeps, workspaceRootPath) {
    return Object.keys(incomingDeps).reduce((acc, d) => {
        if (!existingDeps?.[d] ||
            isIncomingVersionGreater(tree, incomingDeps[d], existingDeps[d], d)) {
            acc[d] = incomingDeps[d];
        }
        return acc;
    }, {});
}
function removeExistingDependencies(incomingDeps, existingDeps) {
    return Object.keys(incomingDeps).reduce((acc, d) => {
        if (!existingDeps?.[d]) {
            acc[d] = incomingDeps[d];
        }
        return acc;
    }, {});
}
/**
 * Remove Dependencies and Dev Dependencies from package.json
 *
 * For example:
 * ```typescript
 * removeDependenciesFromPackageJson(tree, ['react'], ['jest'])
 * ```
 * This will **remove** `react` and `jest` from the dependencies and devDependencies sections of package.json respectively.
 *
 * @param dependencies Dependencies to be removed from the dependencies section of package.json
 * @param devDependencies Dependencies to be removed from the devDependencies section of package.json
 * @returns Callback to uninstall dependencies only if necessary. undefined is returned if changes are not necessary.
 */
function removeDependenciesFromPackageJson(tree, dependencies, devDependencies, packageJsonPath = 'package.json') {
    const currentPackageJson = (0, devkit_exports_1.readJson)(tree, packageJsonPath);
    if (requiresRemovingOfPackages(currentPackageJson, dependencies, devDependencies)) {
        (0, devkit_exports_1.updateJson)(tree, packageJsonPath, (json) => {
            if (json.dependencies) {
                for (const dep of dependencies) {
                    delete json.dependencies[dep];
                }
                json.dependencies = sortObjectByKeys(json.dependencies);
            }
            if (json.devDependencies) {
                for (const devDep of devDependencies) {
                    delete json.devDependencies[devDep];
                }
                json.devDependencies = sortObjectByKeys(json.devDependencies);
            }
            return json;
        });
    }
    return () => {
        (0, install_packages_task_1.installPackagesTask)(tree);
    };
}
function sortObjectByKeys(obj) {
    if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
        return obj;
    }
    return Object.keys(obj)
        .sort()
        .reduce((result, key) => {
        return {
            ...result,
            [key]: obj[key],
        };
    }, {});
}
/**
 * Verifies whether the given packageJson dependencies require an update
 * given the deps & devDeps passed in
 */
function requiresAddingOfPackages(tree, packageJsonFile, deps, devDeps, workspaceRootPath) {
    let needsDepsUpdate = false;
    let needsDevDepsUpdate = false;
    packageJsonFile.dependencies = packageJsonFile.dependencies || {};
    packageJsonFile.devDependencies = packageJsonFile.devDependencies || {};
    if (Object.keys(deps).length > 0) {
        needsDepsUpdate = Object.keys(deps).some((entry) => {
            const incomingVersion = deps[entry];
            if (packageJsonFile.dependencies[entry]) {
                const existingVersion = packageJsonFile.dependencies[entry];
                return isIncomingVersionGreater(tree, incomingVersion, existingVersion, entry);
            }
            if (packageJsonFile.devDependencies[entry]) {
                const existingVersion = packageJsonFile.devDependencies[entry];
                return isIncomingVersionGreater(tree, incomingVersion, existingVersion, entry);
            }
            return true;
        });
    }
    if (Object.keys(devDeps).length > 0) {
        needsDevDepsUpdate = Object.keys(devDeps).some((entry) => {
            const incomingVersion = devDeps[entry];
            if (packageJsonFile.devDependencies[entry]) {
                const existingVersion = packageJsonFile.devDependencies[entry];
                return isIncomingVersionGreater(tree, incomingVersion, existingVersion, entry);
            }
            if (packageJsonFile.dependencies[entry]) {
                const existingVersion = packageJsonFile.dependencies[entry];
                return isIncomingVersionGreater(tree, incomingVersion, existingVersion, entry);
            }
            return true;
        });
    }
    return needsDepsUpdate || needsDevDepsUpdate;
}
/**
 * Verifies whether the given packageJson dependencies require an update
 * given the deps & devDeps passed in
 */
function requiresRemovingOfPackages(packageJsonFile, deps, devDeps) {
    let needsDepsUpdate = false;
    let needsDevDepsUpdate = false;
    packageJsonFile.dependencies = packageJsonFile.dependencies || {};
    packageJsonFile.devDependencies = packageJsonFile.devDependencies || {};
    if (deps.length > 0) {
        needsDepsUpdate = deps.some((entry) => packageJsonFile.dependencies[entry]);
    }
    if (devDeps.length > 0) {
        needsDevDepsUpdate = devDeps.some((entry) => packageJsonFile.devDependencies[entry]);
    }
    return needsDepsUpdate || needsDevDepsUpdate;
}
const packageMapCache = new Map();
function ensurePackage(pkgOrTree, requiredVersionOrPackage, maybeRequiredVersion, _) {
    let pkg;
    let requiredVersion;
    if (typeof pkgOrTree === 'string') {
        pkg = pkgOrTree;
        requiredVersion = requiredVersionOrPackage;
    }
    else {
        // Old Signature
        pkg = requiredVersionOrPackage;
        requiredVersion = maybeRequiredVersion;
    }
    if (packageMapCache.has(pkg)) {
        return packageMapCache.get(pkg);
    }
    try {
        return require(pkg);
    }
    catch (e) {
        if (e.code === 'ERR_REQUIRE_ESM') {
            // The package is installed, but is an ESM package.
            // The consumer of this function can import it as needed.
            return null;
        }
        else if (e.code !== 'MODULE_NOT_FOUND') {
            throw e;
        }
    }
    if (process.env.NX_DRY_RUN && process.env.NX_DRY_RUN !== 'false') {
        throw new Error('NOTE: This generator does not support --dry-run. If you are running this in Nx Console, it should execute fine once you hit the "Generate" button.\n');
    }
    const { tempDir } = (0, devkit_internals_1.installPackageToTmp)(pkg, requiredVersion);
    addToNodePath((0, path_1.join)(devkit_exports_1.workspaceRoot, 'node_modules'));
    addToNodePath((0, path_1.join)(tempDir, 'node_modules'));
    // Re-initialize the added paths into require
    module_1.Module._initPaths();
    try {
        const result = require(require.resolve(pkg, {
            paths: [tempDir],
        }));
        packageMapCache.set(pkg, result);
        return result;
    }
    catch (e) {
        if (e.code === 'ERR_REQUIRE_ESM') {
            // The package is installed, but is an ESM package.
            // The consumer of this function can import it as needed.
            packageMapCache.set(pkg, null);
            return null;
        }
        throw e;
    }
}
function addToNodePath(dir) {
    // NODE_PATH is a delimited list of paths.
    // The delimiter is different for windows.
    const delimiter = require('os').platform() === 'win32' ? ';' : ':';
    const paths = process.env.NODE_PATH
        ? process.env.NODE_PATH.split(delimiter)
        : [];
    // The path is already in the node path
    if (paths.includes(dir)) {
        return;
    }
    // Add the tmp path
    paths.push(dir);
    // Update the env variable.
    process.env.NODE_PATH = paths.join(delimiter);
}
function getInstalledPackageModuleVersion(pkg) {
    return require((0, path_1.join)(pkg, 'package.json')).version;
}
/**
 * @description The version of Nx used by the workspace. Returns null if no version is found.
 */
exports.NX_VERSION = getInstalledPackageModuleVersion('nx');
