"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PluginsService = void 0;
const path = require("path");
const shelljs = require("shelljs");
const semver = require("semver");
const constants = require("../constants");
const _ = require("lodash");
const yok_1 = require("../common/yok");
const package_path_helper_1 = require("../helpers/package-path-helper");
const color_1 = require("../color");
class PluginsService {
    get $platformsDataService() {
        return this.$injector.resolve("platformsDataService");
    }
    get $projectDataService() {
        return this.$injector.resolve("projectDataService");
    }
    get npmInstallOptions() {
        return _.merge({
            disableNpmInstall: this.$options.disableNpmInstall,
            frameworkPath: this.$options.frameworkPath,
            ignoreScripts: this.$options.ignoreScripts,
            path: this.$options.path,
        }, PluginsService.NPM_CONFIG);
    }
    constructor($packageManager, $fs, $options, $logger, $errors, $filesHashService, $injector, $mobileHelper, $nodeModulesDependenciesBuilder) {
        this.$packageManager = $packageManager;
        this.$fs = $fs;
        this.$options = $options;
        this.$logger = $logger;
        this.$errors = $errors;
        this.$filesHashService = $filesHashService;
        this.$injector = $injector;
        this.$mobileHelper = $mobileHelper;
        this.$nodeModulesDependenciesBuilder = $nodeModulesDependenciesBuilder;
        this.ensureValidProductionPlugins = _.memoize(this._ensureValidProductionPlugins, (productionDependencies, projectDir, platform) => {
            let key = _.sortBy(productionDependencies, (p) => p.directory)
                .map((d) => JSON.stringify(d, null, 2))
                .join("\n");
            key += projectDir + platform;
            return key;
        });
    }
    async add(plugin, projectData) {
        await this.ensure(projectData);
        const possiblePackageName = path.resolve(plugin);
        if (possiblePackageName.indexOf(".tgz") !== -1 &&
            this.$fs.exists(possiblePackageName)) {
            plugin = possiblePackageName;
        }
        const name = (await this.$packageManager.install(plugin, projectData.projectDir, this.npmInstallOptions)).name;
        const pathToRealNpmPackageJson = this.getPackageJsonFilePathForModule(name, projectData.projectDir);
        const realNpmPackageJson = this.$fs.readJson(pathToRealNpmPackageJson);
        if (realNpmPackageJson.nativescript) {
            const pluginData = this.convertToPluginData(realNpmPackageJson, projectData.projectDir);
            const action = async (pluginDestinationPath, platform, platformData) => {
                this.isPluginDataValidForPlatform(pluginData, platform, projectData);
            };
            await this.executeForAllInstalledPlatforms(action, projectData);
            this.$logger.info(`Successfully installed plugin ${realNpmPackageJson.name}.`);
        }
        else {
            await this.$packageManager.uninstall(realNpmPackageJson.name, { save: true }, projectData.projectDir);
            this.$errors.fail(`${plugin} is not a valid NativeScript plugin. Verify that the plugin package.json file contains a nativescript key and try again.`);
        }
    }
    async remove(pluginName, projectData) {
        const removePluginNativeCodeAction = async (modulesDestinationPath, platform, platformData) => {
            const pluginData = this.convertToPluginData(this.getNodeModuleData(pluginName, projectData.projectDir), projectData.projectDir);
            await platformData.platformProjectService.removePluginNativeCode(pluginData, projectData);
        };
        await this.executeForAllInstalledPlatforms(removePluginNativeCodeAction, projectData);
        await this.executeNpmCommand(PluginsService.UNINSTALL_COMMAND_NAME, pluginName, projectData);
        let showMessage = true;
        const action = async (modulesDestinationPath, platform, platformData) => {
            shelljs.rm("-rf", path.join(modulesDestinationPath, pluginName));
            this.$logger.info(`Successfully removed plugin ${pluginName} for ${platform}.`);
            showMessage = false;
        };
        await this.executeForAllInstalledPlatforms(action, projectData);
        if (showMessage) {
            this.$logger.info(`Successfully removed plugin ${pluginName}`);
        }
    }
    addToPackageJson(plugin, version, isDev, projectDir) {
        const packageJsonPath = this.getPackageJsonFilePath(projectDir);
        let packageJsonContent = this.$fs.readJson(packageJsonPath);
        const collectionKey = isDev ? "devDependencies" : "dependencies";
        const oppositeCollectionKey = isDev ? "dependencies" : "devDependencies";
        if (packageJsonContent[oppositeCollectionKey] &&
            packageJsonContent[oppositeCollectionKey][plugin]) {
            const result = this.removeDependencyFromPackageJsonContent(plugin, packageJsonContent);
            packageJsonContent = result.packageJsonContent;
        }
        packageJsonContent[collectionKey] = packageJsonContent[collectionKey] || {};
        packageJsonContent[collectionKey][plugin] = version;
        this.$fs.writeJson(packageJsonPath, packageJsonContent);
    }
    removeFromPackageJson(plugin, projectDir) {
        const packageJsonPath = this.getPackageJsonFilePath(projectDir);
        const packageJsonContent = this.$fs.readJson(packageJsonPath);
        const result = this.removeDependencyFromPackageJsonContent(plugin, packageJsonContent);
        if (result.hasModifiedPackageJson) {
            this.$fs.writeJson(packageJsonPath, result.packageJsonContent);
        }
    }
    async preparePluginNativeCode({ pluginData, platform, projectData, }) {
        const platformData = this.$platformsDataService.getPlatformData(platform, projectData);
        const pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(platform);
        if (this.$fs.exists(pluginPlatformsFolderPath)) {
            const pathToPluginsBuildFile = path.join(platformData.projectRoot, constants.PLUGINS_BUILD_DATA_FILENAME);
            const allPluginsNativeHashes = this.getAllPluginsNativeHashes(pathToPluginsBuildFile);
            const oldPluginNativeHashes = allPluginsNativeHashes[pluginData.name];
            const currentPluginNativeHashes = await this.getPluginNativeHashes(pluginPlatformsFolderPath);
            if (!oldPluginNativeHashes ||
                this.$filesHashService.hasChangesInShasums(oldPluginNativeHashes, currentPluginNativeHashes)) {
                await platformData.platformProjectService.preparePluginNativeCode(pluginData, projectData);
                const updatedPluginNativeHashes = await this.getPluginNativeHashes(pluginPlatformsFolderPath);
                this.setPluginNativeHashes({
                    pathToPluginsBuildFile,
                    pluginData,
                    currentPluginNativeHashes: updatedPluginNativeHashes,
                    allPluginsNativeHashes,
                });
            }
        }
    }
    async ensureAllDependenciesAreInstalled(projectData) {
        const packageJsonContent = this.$fs.readJson(this.getPackageJsonFilePath(projectData.projectDir));
        const allDependencies = _.keys(packageJsonContent.dependencies).concat(_.keys(packageJsonContent.devDependencies));
        const notInstalledDependencies = allDependencies
            .map((dep) => {
            this.$logger.trace(`Checking if ${dep} is installed...`);
            const pathToPackage = (0, package_path_helper_1.resolvePackagePath)(dep, {
                paths: [projectData.projectDir],
            });
            if (pathToPackage) {
                return false;
            }
            this.$logger.trace(`${dep} is not installed, or couldn't be found`);
            return dep;
        })
            .filter(Boolean);
        if (this.$options.force || notInstalledDependencies.length) {
            this.$logger.trace("Npm install will be called from CLI. Force option is: ", this.$options.force, " Not installed dependencies are: ", notInstalledDependencies);
            await this.$packageManager.install(projectData.projectDir, projectData.projectDir, {
                disableNpmInstall: this.$options.disableNpmInstall,
                frameworkPath: this.$options.frameworkPath,
                ignoreScripts: this.$options.ignoreScripts,
                path: this.$options.path,
            });
        }
    }
    async getAllInstalledPlugins(projectData) {
        const nodeModules = (await this.getAllInstalledModules(projectData)).map((nodeModuleData) => this.convertToPluginData(nodeModuleData, projectData.projectDir));
        return _.filter(nodeModules, (nodeModuleData) => nodeModuleData && nodeModuleData.isPlugin);
    }
    getAllProductionPlugins(projectData, platform, dependencies) {
        dependencies =
            dependencies ||
                this.$nodeModulesDependenciesBuilder.getProductionDependencies(projectData.projectDir, projectData.ignoredDependencies);
        if (_.isEmpty(dependencies)) {
            return [];
        }
        let productionPlugins = dependencies.filter((d) => !!d.nativescript);
        productionPlugins = this.ensureValidProductionPlugins(productionPlugins, projectData.projectDir, platform);
        return productionPlugins
            .map((plugin) => this.convertToPluginData(plugin, projectData.projectDir))
            .filter((item, idx, self) => {
            return self.findIndex((p) => p.name === item.name) === idx;
        });
    }
    getDependenciesFromPackageJson(projectDir) {
        const packageJson = this.$fs.readJson(this.getPackageJsonFilePath(projectDir));
        const dependencies = this.getBasicPluginInformation(packageJson.dependencies);
        const devDependencies = this.getBasicPluginInformation(packageJson.devDependencies);
        return {
            dependencies,
            devDependencies,
        };
    }
    isNativeScriptPlugin(pluginPackageJsonPath) {
        const pluginPackageJsonContent = this.$fs.readJson(pluginPackageJsonPath);
        return pluginPackageJsonContent && pluginPackageJsonContent.nativescript;
    }
    _ensureValidProductionPlugins(productionDependencies, projectDir, platform) {
        let clonedProductionDependencies = _.cloneDeep(productionDependencies);
        platform = platform.toLowerCase();
        if (this.$mobileHelper.isAndroidPlatform(platform)) {
            this.ensureValidProductionPluginsForAndroid(clonedProductionDependencies);
        }
        else if (this.$mobileHelper.isiOSPlatform(platform)) {
            clonedProductionDependencies = this.ensureValidProductionPluginsForIOS(clonedProductionDependencies, projectDir, platform);
        }
        return clonedProductionDependencies;
    }
    ensureValidProductionPluginsForAndroid(productionDependencies) {
        const dependenciesGroupedByName = _.groupBy(productionDependencies, (p) => p.name);
        _.each(dependenciesGroupedByName, (dependencyOccurrences, dependencyName) => {
            if (dependencyOccurrences.length > 1) {
                const dependencyOccurrencesGroupedByVersion = _.groupBy(dependencyOccurrences, (g) => g.version);
                const versions = _.keys(dependencyOccurrencesGroupedByVersion);
                if (versions.length === 1) {
                    this.$logger.trace(`Detected same versions (${_.first(versions)}) of the ${dependencyName} installed at locations: ${_.map(dependencyOccurrences, (d) => d.directory).join(", ")}`);
                }
                else {
                    this.$logger.trace(`Detected different versions of the ${dependencyName} installed at locations: ${_.map(dependencyOccurrences, (d) => d.directory).join(", ")}\nThis can cause build failures.`);
                }
            }
        });
    }
    ensureValidProductionPluginsForIOS(productionDependencies, projectDir, platform) {
        const dependenciesWithFrameworks = [];
        _.each(productionDependencies, (d) => {
            const pathToPlatforms = path.join(d.directory, "platforms", platform);
            if (this.$fs.exists(pathToPlatforms)) {
                const contents = this.$fs.readDirectory(pathToPlatforms);
                _.each(contents, (file) => {
                    if (path.extname(file) === ".framework") {
                        dependenciesWithFrameworks.push({
                            ...d,
                            frameworkName: path.basename(file),
                            frameworkLocation: path.join(pathToPlatforms, file),
                        });
                    }
                });
            }
        });
        if (dependenciesWithFrameworks.length > 0) {
            const dependenciesGroupedByFrameworkName = _.groupBy(dependenciesWithFrameworks, (d) => d.frameworkName);
            _.each(dependenciesGroupedByFrameworkName, (dependencyOccurrences, frameworkName) => {
                if (dependencyOccurrences.length > 1) {
                    const groupedByName = _.groupBy(dependencyOccurrences, (d) => d.name);
                    const pluginsNames = _.keys(groupedByName);
                    if (pluginsNames.length > 1) {
                        const locations = dependencyOccurrences.map((d) => d.frameworkLocation);
                        let msg = `Detected the framework ${frameworkName} is installed from multiple plugins at locations:\n${locations.join("\n")}\n`;
                        msg += this.getHelpMessage(projectDir);
                        this.$errors.fail(msg);
                    }
                    const dependencyName = _.first(pluginsNames);
                    const dependencyOccurrencesGroupedByVersion = _.groupBy(dependencyOccurrences, (g) => g.version);
                    const versions = _.keys(dependencyOccurrencesGroupedByVersion);
                    if (versions.length === 1) {
                        this.$logger.warn(`Detected the framework ${frameworkName} is installed multiple times from the same versions of plugin (${_.first(versions)}) at locations: ${_.map(dependencyOccurrences, (d) => d.directory).join(", ")}`);
                        const selectedPackage = _.minBy(dependencyOccurrences, (d) => d.depth);
                        this.$logger.info(color_1.color.green(`CLI will use only the native code from '${selectedPackage.directory}'.`));
                        _.each(dependencyOccurrences, (dependency) => {
                            if (dependency !== selectedPackage) {
                                productionDependencies.splice(productionDependencies.indexOf(dependency), 1);
                            }
                        });
                    }
                    else {
                        const message = this.getFailureMessageForDifferentDependencyVersions(dependencyName, frameworkName, dependencyOccurrencesGroupedByVersion, projectDir);
                        this.$errors.fail(message);
                    }
                }
            });
        }
        return productionDependencies;
    }
    getFailureMessageForDifferentDependencyVersions(dependencyName, frameworkName, dependencyOccurrencesGroupedByVersion, projectDir) {
        let message = `Cannot use the same framework ${frameworkName} multiple times in your application.
This framework comes from ${dependencyName} plugin, which is installed multiple times in node_modules:\n`;
        _.each(dependencyOccurrencesGroupedByVersion, (dependencies, version) => {
            message += dependencies.map((d) => `* Path: ${d.directory}, version: ${d.version}\n`);
        });
        message += this.getHelpMessage(projectDir);
        return message;
    }
    getHelpMessage(projectDir) {
        const existingLockFiles = [];
        PluginsService.LOCK_FILES.forEach((lockFile) => {
            if (this.$fs.exists(path.join(projectDir, lockFile))) {
                existingLockFiles.push(lockFile);
            }
        });
        let msgForLockFiles = "";
        if (existingLockFiles.length) {
            msgForLockFiles += ` and ${existingLockFiles.join(", ")}`;
        }
        return `\nProbably you need to update your dependencies, remove node_modules${msgForLockFiles} and try again.`;
    }
    convertToPluginData(cacheData, projectDir) {
        try {
            const pluginData = {};
            pluginData.name = cacheData.name;
            pluginData.version = cacheData.version;
            pluginData.fullPath =
                cacheData.directory ||
                    path.dirname(this.getPackageJsonFilePathForModule(cacheData.name, projectDir));
            pluginData.isPlugin = !!cacheData.nativescript;
            pluginData.pluginPlatformsFolderPath = (platform) => {
                if (this.$mobileHelper.isvisionOSPlatform(platform)) {
                    platform = "ios";
                }
                return path.join(pluginData.fullPath, "platforms", platform.toLowerCase());
            };
            const data = cacheData.nativescript;
            if (pluginData.isPlugin) {
                pluginData.platformsData = data.platforms;
                pluginData.pluginVariables = data.variables;
            }
            return pluginData;
        }
        catch (err) {
            this.$logger.trace("NOTE: There appears to be a problem with this dependency:", cacheData.name);
            this.$logger.trace(err);
            return null;
        }
    }
    removeDependencyFromPackageJsonContent(dependency, packageJsonContent) {
        let hasModifiedPackageJson = false;
        if (packageJsonContent.devDependencies &&
            packageJsonContent.devDependencies[dependency]) {
            delete packageJsonContent.devDependencies[dependency];
            hasModifiedPackageJson = true;
        }
        if (packageJsonContent.dependencies &&
            packageJsonContent.dependencies[dependency]) {
            delete packageJsonContent.dependencies[dependency];
            hasModifiedPackageJson = true;
        }
        return {
            hasModifiedPackageJson,
            packageJsonContent,
        };
    }
    getBasicPluginInformation(dependencies) {
        return _.map(dependencies, (version, key) => ({
            name: key,
            version: version,
        }));
    }
    getNodeModulesPath(projectDir) {
        return path.join(projectDir, "node_modules");
    }
    getPackageJsonFilePath(projectDir) {
        return path.join(projectDir, "package.json");
    }
    getPackageJsonFilePathForModule(moduleName, projectDir) {
        const pathToJsonFile = (0, package_path_helper_1.resolvePackageJSONPath)(moduleName, {
            paths: [projectDir],
        });
        return pathToJsonFile;
    }
    getDependencies(projectDir) {
        const packageJsonFilePath = this.getPackageJsonFilePath(projectDir);
        return _.keys(require(packageJsonFilePath).dependencies);
    }
    getNodeModuleData(module, projectDir) {
        if (!this.$fs.exists(module) || path.basename(module) !== "package.json") {
            module = this.getPackageJsonFilePathForModule(module, projectDir);
        }
        const data = this.$fs.readJson(module);
        return {
            name: data.name,
            version: data.version,
            fullPath: path.dirname(module),
            isPlugin: data.nativescript !== undefined,
            nativescript: data.nativescript,
        };
    }
    async ensure(projectData) {
        await this.ensureAllDependenciesAreInstalled(projectData);
        this.$fs.ensureDirectoryExists(this.getNodeModulesPath(projectData.projectDir));
    }
    async getAllInstalledModules(projectData) {
        await this.ensure(projectData);
        const nodeModules = this.getDependencies(projectData.projectDir);
        return _.map(nodeModules, (nodeModuleName) => this.getNodeModuleData(nodeModuleName, projectData.projectDir));
    }
    async executeNpmCommand(npmCommandName, npmCommandArguments, projectData) {
        if (npmCommandName === PluginsService.INSTALL_COMMAND_NAME) {
            await this.$packageManager.install(npmCommandArguments, projectData.projectDir, this.npmInstallOptions);
        }
        else if (npmCommandName === PluginsService.UNINSTALL_COMMAND_NAME) {
            await this.$packageManager.uninstall(npmCommandArguments, PluginsService.NPM_CONFIG, projectData.projectDir);
        }
        return this.parseNpmCommandResult(npmCommandArguments);
    }
    parseNpmCommandResult(npmCommandResult) {
        return npmCommandResult.split("@")[0];
    }
    async executeForAllInstalledPlatforms(action, projectData) {
        const availablePlatforms = this.$mobileHelper.platformNames.map((p) => p.toLowerCase());
        for (const platform of availablePlatforms) {
            const isPlatformInstalled = this.$fs.exists(path.join(projectData.platformsDir, platform.toLowerCase()));
            if (isPlatformInstalled) {
                const platformData = this.$platformsDataService.getPlatformData(platform.toLowerCase(), projectData);
                const pluginDestinationPath = path.join(platformData.appDestinationDirectoryPath, this.$options.hostProjectModuleName, "tns_modules");
                await action(pluginDestinationPath, platform.toLowerCase(), platformData);
            }
        }
    }
    getInstalledFrameworkVersion(platform, projectData) {
        const runtimePackage = this.$projectDataService.getRuntimePackage(projectData.projectDir, platform);
        return runtimePackage.version;
    }
    isPluginDataValidForPlatform(pluginData, platform, projectData) {
        let isValid = true;
        const installedFrameworkVersion = this.getInstalledFrameworkVersion(platform, projectData);
        const pluginPlatformsData = pluginData.platformsData;
        if (pluginPlatformsData) {
            const versionRequiredByPlugin = pluginPlatformsData[platform];
            if (!versionRequiredByPlugin) {
                this.$logger.warn(`${pluginData.name} is not supported for ${platform}.`);
                isValid = false;
            }
            else if (semver.gt(versionRequiredByPlugin, installedFrameworkVersion)) {
                this.$logger.warn(`${pluginData.name} requires at least version ${versionRequiredByPlugin} of platform ${platform}. Currently installed version is ${installedFrameworkVersion}.`);
                isValid = false;
            }
        }
        return isValid;
    }
    async getPluginNativeHashes(pluginPlatformsDir) {
        let data = {};
        if (this.$fs.exists(pluginPlatformsDir)) {
            const pluginNativeDataFiles = this.$fs.enumerateFilesInDirectorySync(pluginPlatformsDir);
            data = await this.$filesHashService.generateHashes(pluginNativeDataFiles);
        }
        return data;
    }
    getAllPluginsNativeHashes(pathToPluginsBuildFile) {
        if (this.$options.hostProjectPath) {
            return {};
        }
        let data = {};
        if (this.$fs.exists(pathToPluginsBuildFile)) {
            data = this.$fs.readJson(pathToPluginsBuildFile);
        }
        return data;
    }
    setPluginNativeHashes(opts) {
        if (this.$options.hostProjectPath) {
            return;
        }
        opts.allPluginsNativeHashes[opts.pluginData.name] =
            opts.currentPluginNativeHashes;
        this.$fs.writeJson(opts.pathToPluginsBuildFile, opts.allPluginsNativeHashes);
    }
}
exports.PluginsService = PluginsService;
PluginsService.INSTALL_COMMAND_NAME = "install";
PluginsService.UNINSTALL_COMMAND_NAME = "uninstall";
PluginsService.NPM_CONFIG = {
    save: true,
};
PluginsService.LOCK_FILES = [
    "package-lock.json",
    "npm-shrinkwrap.json",
    "yarn.lock",
    "pnpm-lock.yaml",
];
yok_1.injector.register("pluginsService", PluginsService);
