"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
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;
        });
    }
    add(plugin, projectData) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.ensure(projectData);
            const possiblePackageName = path.resolve(plugin);
            if (possiblePackageName.indexOf(".tgz") !== -1 &&
                this.$fs.exists(possiblePackageName)) {
                plugin = possiblePackageName;
            }
            const name = (yield 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);
                // Validate
                const action = (pluginDestinationPath, platform, platformData) => __awaiter(this, void 0, void 0, function* () {
                    this.isPluginDataValidForPlatform(pluginData, platform, projectData);
                });
                yield this.executeForAllInstalledPlatforms(action, projectData);
                this.$logger.info(`Successfully installed plugin ${realNpmPackageJson.name}.`);
            }
            else {
                yield 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.`);
            }
        });
    }
    remove(pluginName, projectData) {
        return __awaiter(this, void 0, void 0, function* () {
            const removePluginNativeCodeAction = (modulesDestinationPath, platform, platformData) => __awaiter(this, void 0, void 0, function* () {
                const pluginData = this.convertToPluginData(this.getNodeModuleData(pluginName, projectData.projectDir), projectData.projectDir);
                yield platformData.platformProjectService.removePluginNativeCode(pluginData, projectData);
            });
            yield this.executeForAllInstalledPlatforms(removePluginNativeCodeAction, projectData);
            yield this.executeNpmCommand(PluginsService.UNINSTALL_COMMAND_NAME, pluginName, projectData);
            let showMessage = true;
            const action = (modulesDestinationPath, platform, platformData) => __awaiter(this, void 0, void 0, function* () {
                shelljs.rm("-rf", path.join(modulesDestinationPath, pluginName));
                this.$logger.info(`Successfully removed plugin ${pluginName} for ${platform}.`);
                showMessage = false;
            });
            yield 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);
        }
    }
    preparePluginNativeCode({ pluginData, platform, projectData, }) {
        return __awaiter(this, void 0, void 0, function* () {
            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 = yield this.getPluginNativeHashes(pluginPlatformsFolderPath);
                if (!oldPluginNativeHashes ||
                    this.$filesHashService.hasChangesInShasums(oldPluginNativeHashes, currentPluginNativeHashes)) {
                    yield platformData.platformProjectService.preparePluginNativeCode(pluginData, projectData);
                    const updatedPluginNativeHashes = yield this.getPluginNativeHashes(pluginPlatformsFolderPath);
                    this.setPluginNativeHashes({
                        pathToPluginsBuildFile,
                        pluginData,
                        currentPluginNativeHashes: updatedPluginNativeHashes,
                        allPluginsNativeHashes,
                    });
                }
            }
        });
    }
    ensureAllDependenciesAreInstalled(projectData) {
        return __awaiter(this, void 0, void 0, function* () {
            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 if the dependency is installed - we'll filter out boolean values
                    // and end up with an array of dep names that are not installed if we end up
                    // inside the catch block.
                    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);
                yield this.$packageManager.install(projectData.projectDir, projectData.projectDir, {
                    disableNpmInstall: this.$options.disableNpmInstall,
                    frameworkPath: this.$options.frameworkPath,
                    ignoreScripts: this.$options.ignoreScripts,
                    path: this.$options.path,
                });
            }
        });
    }
    getAllInstalledPlugins(projectData) {
        return __awaiter(this, void 0, void 0, function* () {
            const nodeModules = (yield 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) => {
            // Filter out duplicates to speed up build times by not building the same dependency
            // multiple times. One possible downside is that if there are different versions
            // of the same native dependency only the first one in the array will be built
            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) {
                // the dependency exists multiple times in node_modules
                const dependencyOccurrencesGroupedByVersion = _.groupBy(dependencyOccurrences, (g) => g.version);
                const versions = _.keys(dependencyOccurrencesGroupedByVersion);
                if (versions.length === 1) {
                    // all dependencies with this name have the same version
                    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(Object.assign(Object.assign({}, 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) {
                    // A framework exists multiple times in node_modules
                    const groupedByName = _.groupBy(dependencyOccurrences, (d) => d.name);
                    const pluginsNames = _.keys(groupedByName);
                    if (pluginsNames.length > 1) {
                        // fail - the same framework is installed by different dependencies.
                        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) {
                        // all dependencies with this name have the same version
                        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" /* constants.PlatformTypes.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) {
        // module can be  modulePath or moduleName
        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,
        };
    }
    ensure(projectData) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.ensureAllDependenciesAreInstalled(projectData);
            this.$fs.ensureDirectoryExists(this.getNodeModulesPath(projectData.projectDir));
        });
    }
    getAllInstalledModules(projectData) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.ensure(projectData);
            const nodeModules = this.getDependencies(projectData.projectDir);
            return _.map(nodeModules, (nodeModuleName) => this.getNodeModuleData(nodeModuleName, projectData.projectDir));
        });
    }
    executeNpmCommand(npmCommandName, npmCommandArguments, projectData) {
        return __awaiter(this, void 0, void 0, function* () {
            if (npmCommandName === PluginsService.INSTALL_COMMAND_NAME) {
                yield this.$packageManager.install(npmCommandArguments, projectData.projectDir, this.npmInstallOptions);
            }
            else if (npmCommandName === PluginsService.UNINSTALL_COMMAND_NAME) {
                yield this.$packageManager.uninstall(npmCommandArguments, PluginsService.NPM_CONFIG, projectData.projectDir);
            }
            return this.parseNpmCommandResult(npmCommandArguments);
        });
    }
    parseNpmCommandResult(npmCommandResult) {
        return npmCommandResult.split("@")[0]; // returns plugin name
    }
    executeForAllInstalledPlatforms(action, projectData) {
        return __awaiter(this, void 0, void 0, function* () {
            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, constants.APP_FOLDER_NAME, "tns_modules");
                    yield action(pluginDestinationPath, platform.toLowerCase(), platformData);
                }
            }
        });
    }
    getInstalledFrameworkVersion(platform, projectData) {
        const runtimePackage = this.$projectDataService.getRuntimePackage(projectData.projectDir, platform);
        // const platformData = this.$platformsDataService.getPlatformData(platform, projectData);
        // const frameworkData = this.$projectDataService.getNSValue(projectData.projectDir, platformData.frameworkPackageName);
        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;
    }
    getPluginNativeHashes(pluginPlatformsDir) {
        return __awaiter(this, void 0, void 0, function* () {
            let data = {};
            if (this.$fs.exists(pluginPlatformsDir)) {
                const pluginNativeDataFiles = this.$fs.enumerateFilesInDirectorySync(pluginPlatformsDir);
                data = yield this.$filesHashService.generateHashes(pluginNativeDataFiles);
            }
            return data;
        });
    }
    getAllPluginsNativeHashes(pathToPluginsBuildFile) {
        let data = {};
        if (this.$fs.exists(pathToPluginsBuildFile)) {
            data = this.$fs.readJson(pathToPluginsBuildFile);
        }
        return data;
    }
    setPluginNativeHashes(opts) {
        opts.allPluginsNativeHashes[opts.pluginData.name] =
            opts.currentPluginNativeHashes;
        this.$fs.writeJson(opts.pathToPluginsBuildFile, opts.allPluginsNativeHashes);
    }
}
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",
];
exports.PluginsService = PluginsService;
yok_1.injector.register("pluginsService", PluginsService);
//# sourceMappingURL=plugins-service.js.map