"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.UpdateController = void 0;
const path = require("path");
const semver = require("semver");
const constants = require("../constants");
const update_controller_base_1 = require("./update-controller-base");
const yok_1 = require("../common/yok");
const constants_1 = require("../constants");
class UpdateController extends update_controller_base_1.UpdateControllerBase {
    constructor($fs, $platformsDataService, $platformCommandHelper, $packageInstallationManager, $packageManager, $devicePlatformsConstants, $addPlatformService, $logger, $errors, $pluginsService, $pacoteService, $projectDataService) {
        super($fs, $platformCommandHelper, $platformsDataService, $packageInstallationManager, $packageManager, $pacoteService);
        this.$fs = $fs;
        this.$platformsDataService = $platformsDataService;
        this.$platformCommandHelper = $platformCommandHelper;
        this.$packageInstallationManager = $packageInstallationManager;
        this.$packageManager = $packageManager;
        this.$devicePlatformsConstants = $devicePlatformsConstants;
        this.$addPlatformService = $addPlatformService;
        this.$logger = $logger;
        this.$errors = $errors;
        this.$pluginsService = $pluginsService;
        this.$pacoteService = $pacoteService;
        this.$projectDataService = $projectDataService;
    }
    update(updateOptions) {
        return __awaiter(this, void 0, void 0, function* () {
            const projectData = this.$projectDataService.getProjectData(updateOptions.projectDir);
            const backupDir = path.join(updateOptions.projectDir, UpdateController.backupFolder);
            try {
                this.backup(UpdateController.folders, backupDir, projectData.projectDir);
            }
            catch (error) {
                this.$logger.error(UpdateController.backupFailMessage);
                this.$fs.deleteDirectory(backupDir);
                return;
            }
            try {
                yield this.cleanUpProject(projectData);
                yield this.updateProject(projectData, updateOptions.version);
            }
            catch (error) {
                this.restoreBackup(UpdateController.folders, backupDir, projectData.projectDir);
                this.$logger.error(`${UpdateController.updateFailMessage} Reason is: ${error.message}`);
            }
        });
    }
    shouldUpdate({ projectDir, version, }) {
        return __awaiter(this, void 0, void 0, function* () {
            if (version && !semver.valid(version) && !semver.validRange(version)) {
                return true;
            }
            const projectData = this.$projectDataService.getProjectData(projectDir);
            const templateManifest = yield this.getTemplateManifest(projectData, version);
            const dependencies = this.getUpdatableDependencies(templateManifest.dependencies);
            const devDependencies = this.getUpdatableDependencies(templateManifest.devDependencies);
            if ((yield this.hasDependenciesToUpdate({
                dependencies,
                areDev: false,
                projectData,
            })) ||
                (yield this.hasDependenciesToUpdate({
                    dependencies: devDependencies,
                    areDev: true,
                    projectData,
                }))) {
                return true;
            }
            for (const platform in this.$devicePlatformsConstants) {
                const lowercasePlatform = platform.toLowerCase();
                const platformData = this.$platformsDataService.getPlatformData(lowercasePlatform, projectData);
                if (yield this.shouldUpdateRuntimeVersion(constants_1.PackageVersion.LATEST, platformData.frameworkPackageName, platform, projectData)) {
                    return true;
                }
            }
        });
    }
    cleanUpProject(projectData) {
        return __awaiter(this, void 0, void 0, function* () {
            this.$logger.info("Clean old project artifacts.");
            this.$fs.deleteDirectory(path.join(projectData.projectDir, constants.HOOKS_DIR_NAME));
            this.$fs.deleteDirectory(path.join(projectData.projectDir, constants.PLATFORMS_DIR_NAME));
            this.$fs.deleteDirectory(path.join(projectData.projectDir, constants.NODE_MODULES_FOLDER_NAME));
            if (projectData.projectType === constants.ProjectTypes.ReactFlavorName ||
                projectData.projectType === constants.ProjectTypes.SvelteFlavorName) {
                this.$logger.warn(`As this project is of type ${projectData.projectType}, CLI will not update its ${constants.WEBPACK_CONFIG_NAME} file. Consider updating it manually.`);
            }
            else {
                this.$fs.deleteFile(path.join(projectData.projectDir, constants.WEBPACK_CONFIG_NAME));
            }
            this.$fs.deleteFile(path.join(projectData.projectDir, constants.PACKAGE_LOCK_JSON_FILE_NAME));
            this.$logger.info("Clean old project artifacts complete.");
        });
    }
    updateProject(projectData, version) {
        return __awaiter(this, void 0, void 0, function* () {
            let templateManifest = {};
            if (!version || semver.valid(version) || semver.validRange(version)) {
                templateManifest = yield this.getTemplateManifest(projectData, version);
            }
            else {
                templateManifest = yield this.constructTemplateManifestForTag(version);
            }
            const dependencies = this.getUpdatableDependencies(templateManifest.dependencies);
            const devDependencies = this.getUpdatableDependencies(templateManifest.devDependencies);
            this.$logger.info("Start updating dependencies.");
            yield this.updateDependencies({ dependencies, areDev: false, projectData });
            this.$logger.info("Finished updating dependencies.");
            this.$logger.info("Start updating devDependencies.");
            yield this.updateDependencies({
                dependencies: devDependencies,
                areDev: true,
                projectData,
            });
            this.$logger.info("Finished updating devDependencies.");
            this.$logger.info("Start updating runtimes.");
            yield this.updateRuntimes(projectData);
            this.$logger.info("Finished updating runtimes.");
            this.$logger.info("Install packages.");
            yield this.$packageManager.install(projectData.projectDir, projectData.projectDir, {
                disableNpmInstall: false,
                frameworkPath: null,
                ignoreScripts: false,
                path: projectData.projectDir,
            });
        });
    }
    constructTemplateManifestForTag(tag) {
        return __awaiter(this, void 0, void 0, function* () {
            this.$logger.trace(`Will construct manually template manifest for tag ${tag}`);
            const templateManifest = {};
            templateManifest.dependencies = {};
            templateManifest.devDependencies = {};
            for (const updatableDependency of UpdateController.updatableDependencies) {
                const version = yield this.getVersionFromTag(updatableDependency.name, tag);
                if (!version) {
                    this.$errors.fail(`Unable to execute update as package '${updatableDependency.name}' does not have version or tag '${tag}'`);
                }
                const dictionaryToModify = updatableDependency.isDev
                    ? templateManifest.devDependencies
                    : templateManifest.dependencies;
                dictionaryToModify[updatableDependency.name] = version;
                if (updatableDependency.alias) {
                    const aliasVersion = yield this.getVersionFromTag(updatableDependency.name, tag);
                    dictionaryToModify[updatableDependency.alias] = aliasVersion;
                }
            }
            templateManifest.nativescript = {
                [constants.TNS_ANDROID_RUNTIME_NAME]: {
                    version: yield this.getVersionFromTag(constants.TNS_ANDROID_RUNTIME_NAME, tag),
                },
                [constants.TNS_IOS_RUNTIME_NAME]: {
                    version: yield this.$packageManager.getTagVersion(constants.TNS_IOS_RUNTIME_NAME, tag),
                },
            };
            this.$logger.trace(`Manually constructed template manifest for tag ${tag}. Content is: ${JSON.stringify(templateManifest, null, 2)}`);
            return templateManifest;
        });
    }
    getVersionFromTag(packageName, tag) {
        return __awaiter(this, void 0, void 0, function* () {
            const version = yield this.$packageManager.getTagVersion(packageName, tag);
            if (!version) {
                this.$errors.fail(`Unable to execute update as package ${packageName} does not have version/tag ${tag}. Please enter valid version or npm tag.`);
            }
            return version;
        });
    }
    updateDependencies({ dependencies, areDev, projectData, }) {
        return __awaiter(this, void 0, void 0, function* () {
            for (const dependency in dependencies) {
                const templateVersion = dependencies[dependency];
                if (!this.hasDependency({ packageName: dependency, isDev: areDev }, projectData)) {
                    continue;
                }
                if (yield this.shouldUpdateDependency(dependency, templateVersion, projectData)) {
                    this.$logger.info(`Updating '${dependency}' to version '${templateVersion}'.`);
                    this.$pluginsService.addToPackageJson(dependency, templateVersion, areDev, projectData.projectDir);
                }
            }
        });
    }
    shouldUpdateDependency(dependency, targetVersion, projectData) {
        return __awaiter(this, void 0, void 0, function* () {
            const devDependencies = projectData.devDependencies || {};
            const dependencies = projectData.dependencies || {};
            const projectVersion = dependencies[dependency] || devDependencies[dependency];
            const maxSatisfyingTargetVersion = yield this.$packageInstallationManager.getMaxSatisfyingVersionSafe(dependency, targetVersion);
            const maxSatisfyingProjectVersion = yield this.$packageInstallationManager.getMaxSatisfyingVersionSafe(dependency, projectVersion);
            return (maxSatisfyingProjectVersion &&
                maxSatisfyingTargetVersion &&
                semver.gt(maxSatisfyingTargetVersion, maxSatisfyingProjectVersion));
        });
    }
    hasDependenciesToUpdate({ dependencies, areDev, projectData, }) {
        return __awaiter(this, void 0, void 0, function* () {
            for (const dependency in dependencies) {
                const templateVersion = dependencies[dependency];
                if (!this.hasDependency({ packageName: dependency, isDev: areDev }, projectData)) {
                    continue;
                }
                if (yield this.shouldUpdateDependency(dependency, templateVersion, projectData)) {
                    return true;
                }
            }
        });
    }
    updateRuntimes(projectData) {
        return __awaiter(this, void 0, void 0, function* () {
            for (const platform in this.$devicePlatformsConstants) {
                const lowercasePlatform = platform.toLowerCase();
                const platformData = this.$platformsDataService.getPlatformData(lowercasePlatform, projectData);
                if (yield this.shouldUpdateRuntimeVersion(constants_1.PackageVersion.LATEST, platformData.frameworkPackageName, platform, projectData)) {
                    const version = yield this.$packageInstallationManager.getMaxSatisfyingVersionSafe(platformData.frameworkPackageName, constants_1.PackageVersion.LATEST);
                    this.$logger.info(`Updating ${platform} platform to version '${version}'.`);
                    yield this.$addPlatformService.setPlatformVersion(platformData, projectData, version);
                }
            }
        });
    }
    shouldUpdateRuntimeVersion(templateRuntimeVersion, frameworkPackageName, platform, projectData) {
        return __awaiter(this, void 0, void 0, function* () {
            const hasRuntimeDependency = this.hasRuntimeDependency({
                platform,
                projectData,
            });
            if (!hasRuntimeDependency) {
                return false;
            }
            const maxTemplateRuntimeVersion = yield this.$packageInstallationManager.getMaxSatisfyingVersionSafe(frameworkPackageName, templateRuntimeVersion);
            const maxRuntimeVersion = yield this.getMaxRuntimeVersion({
                platform,
                projectData,
            });
            return (maxTemplateRuntimeVersion &&
                maxRuntimeVersion &&
                semver.gt(maxTemplateRuntimeVersion, maxRuntimeVersion));
        });
    }
    getUpdatableDependencies(dependencies) {
        const updatableDependencies = {};
        UpdateController.updatableDependencies.forEach((updatableDependency) => {
            if (dependencies[updatableDependency.name]) {
                updatableDependencies[updatableDependency.name] =
                    dependencies[updatableDependency.name];
            }
            else if (updatableDependency.alias &&
                dependencies[updatableDependency.alias]) {
                updatableDependencies[updatableDependency.name] =
                    dependencies[updatableDependency.alias];
            }
        });
        return updatableDependencies;
    }
    getTemplateName(projectData) {
        let template;
        switch (projectData.projectType) {
            case constants.ProjectTypes.NgFlavorName: {
                template = constants.RESERVED_TEMPLATE_NAMES.angular;
                break;
            }
            case constants.ProjectTypes.VueFlavorName: {
                template = constants.RESERVED_TEMPLATE_NAMES.vue;
                break;
            }
            case constants.ProjectTypes.TsFlavorName: {
                template = constants.RESERVED_TEMPLATE_NAMES.typescript;
                break;
            }
            case constants.ProjectTypes.JsFlavorName: {
                template = constants.RESERVED_TEMPLATE_NAMES.javascript;
                break;
            }
            default: {
                template = constants.RESERVED_TEMPLATE_NAMES.javascript;
                break;
            }
        }
        return template;
    }
    getTemplateManifest(projectData, version) {
        return __awaiter(this, void 0, void 0, function* () {
            let templateManifest;
            const templateName = this.getTemplateName(projectData);
            version =
                version ||
                    (yield this.$packageInstallationManager.getLatestCompatibleVersionSafe(templateName));
            try {
                templateManifest = yield this.getPackageManifest(templateName, version);
            }
            catch (err) {
                this.$errors.fail(UpdateController.failedToGetTemplateManifestMessage, err.message);
            }
            return templateManifest;
        });
    }
}
exports.UpdateController = UpdateController;
UpdateController.updatableDependencies = [
    {
        name: constants.SCOPED_TNS_CORE_MODULES,
        alias: constants.TNS_CORE_MODULES_NAME,
    },
    { name: constants.TNS_CORE_MODULES_NAME },
    { name: constants.TNS_CORE_MODULES_WIDGETS_NAME },
    {
        name: constants.SCOPED_IOS_RUNTIME_NAME,
        alias: constants.TNS_IOS_RUNTIME_NAME,
        isDev: true,
    },
    {
        name: constants.SCOPED_ANDROID_RUNTIME_NAME,
        alias: constants.TNS_ANDROID_RUNTIME_NAME,
        isDev: true,
    },
    { name: constants.WEBPACK_PLUGIN_NAME, isDev: true },
];
UpdateController.folders = [
    constants.LIB_DIR_NAME,
    constants.HOOKS_DIR_NAME,
    constants.WEBPACK_CONFIG_NAME,
    constants.PACKAGE_JSON_FILE_NAME,
    constants.PACKAGE_LOCK_JSON_FILE_NAME,
];
UpdateController.backupFolder = ".update_backup";
UpdateController.updateFailMessage = "Could not update the project!";
UpdateController.backupFailMessage = "Could not backup project folders!";
UpdateController.failedToGetTemplateManifestMessage = "Failed to get template information for the specified version. Original error: %s";
yok_1.injector.register("updateController", UpdateController);
