"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    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) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const path = require("path");
const semver = require("semver");
const constants = require("../constants");
const glob = require("glob");
const update_controller_base_1 = require("./update-controller-base");
const helpers_1 = require("../common/helpers");
class MigrateController extends update_controller_base_1.UpdateControllerBase {
    constructor($fs, $platformCommandHelper, $platformsDataService, $packageInstallationManager, $packageManager, $pacoteService, $androidResourcesMigrationService, $devicePlatformsConstants, $logger, $errors, $addPlatformService, $pluginsService, $projectDataService, $platformValidationService, $resources) {
        super($fs, $platformCommandHelper, $platformsDataService, $packageInstallationManager, $packageManager, $pacoteService);
        this.$fs = $fs;
        this.$platformCommandHelper = $platformCommandHelper;
        this.$platformsDataService = $platformsDataService;
        this.$packageInstallationManager = $packageInstallationManager;
        this.$packageManager = $packageManager;
        this.$pacoteService = $pacoteService;
        this.$androidResourcesMigrationService = $androidResourcesMigrationService;
        this.$devicePlatformsConstants = $devicePlatformsConstants;
        this.$logger = $logger;
        this.$errors = $errors;
        this.$addPlatformService = $addPlatformService;
        this.$pluginsService = $pluginsService;
        this.$projectDataService = $projectDataService;
        this.$platformValidationService = $platformValidationService;
        this.$resources = $resources;
        this.migrationDependencies = [
            { packageName: constants.TNS_CORE_MODULES_NAME, verifiedVersion: "6.0.1" },
            { packageName: constants.TNS_CORE_MODULES_WIDGETS_NAME, verifiedVersion: "6.0.1" },
            { packageName: "tns-platform-declarations", isDev: true, verifiedVersion: "6.0.1" },
            { packageName: "node-sass", isDev: true, verifiedVersion: "4.12.0" },
            { packageName: "nativescript-dev-sass", isDev: true, replaceWith: "node-sass" },
            { packageName: "nativescript-dev-typescript", isDev: true, replaceWith: MigrateController.typescriptPackageName },
            { packageName: "nativescript-dev-less", isDev: true, shouldRemove: true, warning: "LESS CSS is not supported out of the box. In order to enable it, follow the steps in this feature request: https://github.com/NativeScript/nativescript-dev-webpack/issues/967" },
            { packageName: constants.WEBPACK_PLUGIN_NAME, isDev: true, shouldAddIfMissing: true, verifiedVersion: "1.0.1" },
            { packageName: "nativescript-camera", verifiedVersion: "4.5.0" },
            { packageName: "nativescript-geolocation", verifiedVersion: "5.1.0" },
            { packageName: "nativescript-imagepicker", verifiedVersion: "6.2.0" },
            { packageName: "nativescript-social-share", verifiedVersion: "1.5.2" },
            { packageName: "nativescript-ui-chart", verifiedVersion: "5.0.0" },
            { packageName: "nativescript-ui-dataform", verifiedVersion: "5.0.0" },
            { packageName: "nativescript-ui-gauge", verifiedVersion: "5.0.0" },
            { packageName: "nativescript-ui-listview", verifiedVersion: "7.0.0" },
            { packageName: "nativescript-ui-sidedrawer", verifiedVersion: "7.0.0" },
            { packageName: "nativescript-ui-calendar", verifiedVersion: "5.0.0" },
            { packageName: "nativescript-ui-autocomplete", verifiedVersion: "5.0.0" },
            { packageName: "nativescript-datetimepicker", verifiedVersion: "1.1.0" },
            { packageName: "kinvey-nativescript-sdk", verifiedVersion: "4.2.1" },
            { packageName: "nativescript-plugin-firebase", verifiedVersion: "9.0.2" },
            {
                packageName: "nativescript-vue", verifiedVersion: "2.3.0",
                shouldMigrateAction: (projectData, allowInvalidVersions) => __awaiter(this, void 0, void 0, function* () {
                    const dependency = { packageName: "nativescript-vue", verifiedVersion: "2.3.0", isDev: false };
                    const result = this.hasDependency(dependency, projectData) && (yield this.shouldMigrateDependencyVersion(dependency, projectData, allowInvalidVersions));
                    return result;
                }),
                migrateAction: this.migrateNativeScriptVue.bind(this)
            },
            {
                packageName: "nativescript-angular", verifiedVersion: "8.0.2",
                shouldMigrateAction: (projectData, allowInvalidVersions) => __awaiter(this, void 0, void 0, function* () {
                    const dependency = { packageName: "nativescript-angular", verifiedVersion: "8.0.2", isDev: false };
                    const result = this.hasDependency(dependency, projectData) && (yield this.shouldMigrateDependencyVersion(dependency, projectData, allowInvalidVersions));
                    return result;
                }),
                migrateAction: this.migrateNativeScriptAngular.bind(this)
            },
            { packageName: "nativescript-permissions", verifiedVersion: "1.3.0" },
            { packageName: "nativescript-cardview", verifiedVersion: "3.2.0" },
            {
                packageName: "nativescript-unit-test-runner", verifiedVersion: "0.7.0",
                shouldMigrateAction: (projectData, allowInvalidVersions) => __awaiter(this, void 0, void 0, function* () {
                    const dependency = { packageName: "nativescript-unit-test-runner", verifiedVersion: "0.7.0", isDev: false };
                    const result = this.hasDependency(dependency, projectData) && (yield this.shouldMigrateDependencyVersion(dependency, projectData, allowInvalidVersions));
                    return result;
                }),
                migrateAction: this.migrateUnitTestRunner.bind(this)
            },
            { packageName: MigrateController.typescriptPackageName, isDev: true, verifiedVersion: "3.4.5" },
            { packageName: "nativescript-localize", verifiedVersion: "4.2.0" },
            { packageName: "nativescript-dev-babel", verifiedVersion: "0.2.1" },
            { packageName: "nativescript-nfc", verifiedVersion: "4.0.1" }
        ];
    }
    get verifiedPlatformVersions() {
        return {
            [this.$devicePlatformsConstants.Android.toLowerCase()]: "6.0.0",
            [this.$devicePlatformsConstants.iOS.toLowerCase()]: "6.0.1"
        };
    }
    migrate({ projectDir, platforms, allowInvalidVersions = false }) {
        return __awaiter(this, void 0, void 0, function* () {
            const projectData = this.$projectDataService.getProjectData(projectDir);
            const backupDir = path.join(projectDir, MigrateController.backupFolder);
            try {
                this.$logger.info("Backup project configuration.");
                this.backup(MigrateController.folders, backupDir, projectData.projectDir);
                this.$logger.info("Backup project configuration complete.");
            }
            catch (error) {
                this.$logger.error(MigrateController.backupFailMessage);
                this.$fs.deleteDirectory(backupDir);
                return;
            }
            try {
                this.$logger.info("Clean auto-generated files.");
                this.handleAutoGeneratedFiles(backupDir, projectData);
                this.$logger.info("Clean auto-generated files complete.");
            }
            catch (error) {
                this.$logger.trace(`Error during auto-generated files handling. ${(error && error.message) || error}`);
            }
            yield this.migrateOldAndroidAppResources(projectData, backupDir);
            try {
                yield this.cleanUpProject(projectData);
                yield this.migrateDependencies(projectData, platforms, allowInvalidVersions);
            }
            catch (error) {
                this.restoreBackup(MigrateController.folders, backupDir, projectData.projectDir);
                this.$errors.failWithoutHelp(`${MigrateController.migrateFailMessage} The error is: ${error}`);
            }
            this.$logger.info(MigrateController.MIGRATE_FINISH_MESSAGE);
        });
    }
    shouldMigrate({ projectDir, platforms, allowInvalidVersions = false }) {
        return __awaiter(this, void 0, void 0, function* () {
            const projectData = this.$projectDataService.getProjectData(projectDir);
            const shouldMigrateCommonMessage = "The app is not compatible with this CLI version and it should be migrated. Reason: ";
            for (let i = 0; i < this.migrationDependencies.length; i++) {
                const dependency = this.migrationDependencies[i];
                const hasDependency = this.hasDependency(dependency, projectData);
                if (hasDependency && dependency.shouldMigrateAction && (yield dependency.shouldMigrateAction(projectData, allowInvalidVersions))) {
                    this.$logger.trace(`${shouldMigrateCommonMessage}'${dependency.packageName}' requires an update.`);
                    return true;
                }
                if (hasDependency && (dependency.replaceWith || dependency.shouldRemove)) {
                    this.$logger.trace(`${shouldMigrateCommonMessage}'${dependency.packageName}' is deprecated.`);
                    return true;
                }
                if (hasDependency && (yield this.shouldMigrateDependencyVersion(dependency, projectData, allowInvalidVersions))) {
                    this.$logger.trace(`${shouldMigrateCommonMessage}'${dependency.packageName}' should be updated.`);
                    return true;
                }
                if (!hasDependency && dependency.shouldAddIfMissing) {
                    this.$logger.trace(`${shouldMigrateCommonMessage}'${dependency.packageName}' is missing.`);
                    return true;
                }
            }
            for (let platform of platforms) {
                platform = platform && platform.toLowerCase();
                if (!this.$platformValidationService.isValidPlatform(platform, projectData)) {
                    continue;
                }
                const hasRuntimeDependency = this.hasRuntimeDependency({ platform, projectData });
                if (hasRuntimeDependency && (yield this.shouldUpdateRuntimeVersion(this.verifiedPlatformVersions[platform.toLowerCase()], platform, projectData, allowInvalidVersions))) {
                    this.$logger.trace(`${shouldMigrateCommonMessage}Platform '${platform}' should be updated.`);
                    return true;
                }
            }
        });
    }
    validate({ projectDir, platforms, allowInvalidVersions = true }) {
        return __awaiter(this, void 0, void 0, function* () {
            const shouldMigrate = yield this.shouldMigrate({ projectDir, platforms, allowInvalidVersions });
            if (shouldMigrate) {
                this.$errors.failWithoutHelp(MigrateController.UNABLE_TO_MIGRATE_APP_ERROR);
            }
        });
    }
    migrateOldAndroidAppResources(projectData, backupDir) {
        return __awaiter(this, void 0, void 0, function* () {
            const appResourcesPath = projectData.getAppResourcesDirectoryPath();
            if (!this.$androidResourcesMigrationService.hasMigrated(appResourcesPath)) {
                this.$logger.info("Migrate old Android App_Resources structure.");
                try {
                    yield this.$androidResourcesMigrationService.migrate(appResourcesPath, backupDir);
                }
                catch (error) {
                    this.$logger.warn("Migrate old Android App_Resources structure failed: ", error.message);
                }
            }
        });
    }
    cleanUpProject(projectData) {
        return __awaiter(this, void 0, void 0, function* () {
            this.$logger.info("Clean old project artefacts.");
            this.$projectDataService.removeNSConfigProperty(projectData.projectDir, "useLegacyWorkflow");
            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));
            this.$fs.deleteFile(path.join(projectData.projectDir, constants.WEBPACK_CONFIG_NAME));
            this.$fs.deleteFile(path.join(projectData.projectDir, constants.PACKAGE_LOCK_JSON_FILE_NAME));
            if (!projectData.isShared) {
                this.$fs.deleteFile(path.join(projectData.projectDir, constants.TSCCONFIG_TNS_JSON_NAME));
            }
            this.$logger.info("Clean old project artefacts complete.");
        });
    }
    handleAutoGeneratedFiles(backupDir, projectData) {
        const globOptions = {
            silent: true,
            nocase: true,
            matchBase: true,
            nodir: true,
            absolute: false,
            cwd: projectData.appDirectoryPath
        };
        const jsFiles = glob.sync("*.@(js|ts|js.map)", globOptions);
        const autoGeneratedJsFiles = this.getGeneratedFiles(jsFiles, [".js"], [".ts"]);
        const autoGeneratedJsMapFiles = this.getGeneratedFiles(jsFiles, [".map"], [""]);
        const cssFiles = glob.sync("*.@(le|sa|sc|c)ss", globOptions);
        const autoGeneratedCssFiles = this.getGeneratedFiles(cssFiles, [".css"], [".scss", ".sass", ".less"]);
        const allGeneratedFiles = autoGeneratedJsFiles.concat(autoGeneratedJsMapFiles).concat(autoGeneratedCssFiles);
        for (const generatedFile of allGeneratedFiles) {
            const sourceFile = path.join(projectData.appDirectoryPath, generatedFile);
            const destinationFile = path.join(backupDir, generatedFile);
            const destinationFileDir = path.dirname(destinationFile);
            this.$fs.ensureDirectoryExists(destinationFileDir);
            this.$fs.rename(sourceFile, destinationFile);
        }
    }
    getGeneratedFiles(allFiles, generatedFileExts, sourceFileExts) {
        const autoGeneratedFiles = allFiles.filter(file => {
            let isGenerated = false;
            const { dir, name, ext } = path.parse(file);
            if (generatedFileExts.indexOf(ext) > -1) {
                for (const sourceExt of sourceFileExts) {
                    const possibleSourceFile = path.format({ dir, name, ext: sourceExt });
                    isGenerated = allFiles.indexOf(possibleSourceFile) > -1;
                    if (isGenerated) {
                        break;
                    }
                }
            }
            return isGenerated;
        });
        return autoGeneratedFiles;
    }
    migrateDependencies(projectData, platforms, allowInvalidVersions) {
        return __awaiter(this, void 0, void 0, function* () {
            this.$logger.info("Start dependencies migration.");
            for (let i = 0; i < this.migrationDependencies.length; i++) {
                const dependency = this.migrationDependencies[i];
                const hasDependency = this.hasDependency(dependency, projectData);
                if (hasDependency && dependency.migrateAction && (yield dependency.shouldMigrateAction(projectData, allowInvalidVersions))) {
                    const newDependencies = yield dependency.migrateAction(projectData, path.join(projectData.projectDir, MigrateController.backupFolder));
                    for (const newDependency of newDependencies) {
                        yield this.migrateDependency(newDependency, projectData, allowInvalidVersions);
                    }
                }
                yield this.migrateDependency(dependency, projectData, allowInvalidVersions);
            }
            for (const platform of platforms) {
                const lowercasePlatform = platform.toLowerCase();
                const hasRuntimeDependency = this.hasRuntimeDependency({ platform, projectData });
                if (hasRuntimeDependency && (yield this.shouldUpdateRuntimeVersion(this.verifiedPlatformVersions[lowercasePlatform], platform, projectData, allowInvalidVersions))) {
                    const verifiedPlatformVersion = this.verifiedPlatformVersions[lowercasePlatform];
                    const platformData = this.$platformsDataService.getPlatformData(lowercasePlatform, projectData);
                    this.$logger.info(`Updating ${platform} platform to version '${verifiedPlatformVersion}'.`);
                    yield this.$addPlatformService.setPlatformVersion(platformData, projectData, verifiedPlatformVersion);
                }
            }
            this.$logger.info("Install packages.");
            yield this.$packageManager.install(projectData.projectDir, projectData.projectDir, {
                disableNpmInstall: false,
                frameworkPath: null,
                ignoreScripts: false,
                path: projectData.projectDir
            });
            this.$logger.info("Migration complete.");
        });
    }
    migrateDependency(dependency, projectData, allowInvalidVersions) {
        return __awaiter(this, void 0, void 0, function* () {
            const hasDependency = this.hasDependency(dependency, projectData);
            if (hasDependency && dependency.warning) {
                this.$logger.warn(dependency.warning);
            }
            if (hasDependency && (dependency.replaceWith || dependency.shouldRemove)) {
                this.$pluginsService.removeFromPackageJson(dependency.packageName, projectData.projectDir);
                if (dependency.replaceWith) {
                    const replacementDep = _.find(this.migrationDependencies, migrationPackage => migrationPackage.packageName === dependency.replaceWith);
                    if (!replacementDep) {
                        this.$errors.failWithoutHelp("Failed to find replacement dependency.");
                    }
                    this.$logger.info(`Replacing '${dependency.packageName}' with '${replacementDep.packageName}'.`);
                    this.$pluginsService.addToPackageJson(replacementDep.packageName, replacementDep.verifiedVersion, replacementDep.isDev, projectData.projectDir);
                }
                return;
            }
            if (hasDependency && (yield this.shouldMigrateDependencyVersion(dependency, projectData, allowInvalidVersions))) {
                this.$logger.info(`Updating '${dependency.packageName}' to compatible version '${dependency.verifiedVersion}'`);
                this.$pluginsService.addToPackageJson(dependency.packageName, dependency.verifiedVersion, dependency.isDev, projectData.projectDir);
                return;
            }
            if (!hasDependency && dependency.shouldAddIfMissing) {
                this.$logger.info(`Adding '${dependency.packageName}' with version '${dependency.verifiedVersion}'`);
                this.$pluginsService.addToPackageJson(dependency.packageName, dependency.verifiedVersion, dependency.isDev, projectData.projectDir);
            }
        });
    }
    shouldMigrateDependencyVersion(dependency, projectData, allowInvalidVersions) {
        return __awaiter(this, void 0, void 0, function* () {
            const devDependencies = projectData.devDependencies || {};
            const dependencies = projectData.dependencies || {};
            const packageName = dependency.packageName;
            const referencedVersion = dependencies[packageName] || devDependencies[packageName];
            const installedVersion = yield this.getMaxDependencyVersion(dependency.packageName, referencedVersion);
            const requiredVersion = dependency.verifiedVersion;
            return this.isOutdatedVersion(installedVersion, requiredVersion, allowInvalidVersions);
        });
    }
    shouldUpdateRuntimeVersion(targetVersion, platform, projectData, allowInvalidVersions) {
        return __awaiter(this, void 0, void 0, function* () {
            const installedVersion = yield this.getMaxRuntimeVersion({ platform, projectData });
            return this.isOutdatedVersion(installedVersion, targetVersion, allowInvalidVersions);
        });
    }
    isOutdatedVersion(version, targetVersion, allowInvalidVersions) {
        return !!version ? semver.lt(version, targetVersion) : !allowInvalidVersions;
    }
    migrateUnitTestRunner(projectData, migrationBackupDirPath) {
        return __awaiter(this, void 0, void 0, function* () {
            const pathToKarmaConfig = path.join(migrationBackupDirPath, constants.KARMA_CONFIG_NAME);
            if (this.$fs.exists(pathToKarmaConfig)) {
                const oldKarmaContent = this.$fs.readText(pathToKarmaConfig);
                const regExp = /frameworks:\s+\[([\S\s]*?)\]/g;
                const matches = regExp.exec(oldKarmaContent);
                const frameworks = (matches && matches[1] && matches[1].trim()) || '["jasmine"]';
                const testsDir = path.join(projectData.appDirectoryPath, 'tests');
                const relativeTestsDir = path.relative(projectData.projectDir, testsDir);
                const testFiles = `'${helpers_1.fromWindowsRelativePathToUnix(relativeTestsDir)}/**/*.*'`;
                const karmaConfTemplate = this.$resources.readText('test/karma.conf.js');
                const karmaConf = _.template(karmaConfTemplate)({ frameworks, testFiles });
                this.$fs.writeFile(path.join(projectData.projectDir, constants.KARMA_CONFIG_NAME), karmaConf);
            }
            const dependencies = [
                { packageName: "karma-webpack", verifiedVersion: "3.0.5", isDev: true, shouldAddIfMissing: true },
                { packageName: "karma-jasmine", verifiedVersion: "2.0.1", isDev: true },
                { packageName: "karma-mocha", verifiedVersion: "1.3.0", isDev: true },
                { packageName: "karma-chai", verifiedVersion: "0.1.0", isDev: true },
                { packageName: "karma-qunit", verifiedVersion: "3.1.2", isDev: true },
                { packageName: "karma", verifiedVersion: "4.1.0", isDev: true },
            ];
            return dependencies;
        });
    }
    migrateNativeScriptAngular() {
        return __awaiter(this, void 0, void 0, function* () {
            const dependencies = [
                { packageName: "@angular/platform-browser-dynamic", verifiedVersion: "8.0.0", shouldAddIfMissing: true },
                { packageName: "@angular/common", verifiedVersion: "8.0.0", shouldAddIfMissing: true },
                { packageName: "@angular/compiler", verifiedVersion: "8.0.0", shouldAddIfMissing: true },
                { packageName: "@angular/core", verifiedVersion: "8.0.0", shouldAddIfMissing: true },
                { packageName: "@angular/forms", verifiedVersion: "8.0.0", shouldAddIfMissing: true },
                { packageName: "@angular/http", verifiedVersion: "8.0.0-beta.10", shouldAddIfMissing: true },
                { packageName: "@angular/platform-browser", verifiedVersion: "8.0.0", shouldAddIfMissing: true },
                { packageName: "@angular/router", verifiedVersion: "8.0.0", shouldAddIfMissing: true },
                { packageName: "rxjs", verifiedVersion: "6.3.3", shouldAddIfMissing: true },
                { packageName: "zone.js", verifiedVersion: "0.9.1", shouldAddIfMissing: true },
                { packageName: "@angular/animations", verifiedVersion: "8.0.0" },
                { packageName: "@angular/compiler-cli", verifiedVersion: "8.0.0", isDev: true },
                { packageName: "@ngtools/webpack", verifiedVersion: "8.0.0", isDev: true }
            ];
            return dependencies;
        });
    }
    migrateNativeScriptVue() {
        return __awaiter(this, void 0, void 0, function* () {
            const dependencies = [
                { packageName: "nativescript-vue-template-compiler", verifiedVersion: "2.3.0", isDev: true }
            ];
            return dependencies;
        });
    }
}
MigrateController.COMMON_MIGRATE_MESSAGE = "not affect the codebase of the application and you might need to do additional changes manually – for more information, refer to the instructions in the following blog post: https://www.nativescript.org/blog/nativescript-6.0-application-migration";
MigrateController.UNABLE_TO_MIGRATE_APP_ERROR = `The current application is not compatible with NativeScript CLI 6.0.
Use the \`tns migrate\` command to migrate the app dependencies to a form compatible with NativeScript 6.0.
Running this command will ${MigrateController.COMMON_MIGRATE_MESSAGE}`;
MigrateController.MIGRATE_FINISH_MESSAGE = `The \`tns migrate\` command does ${MigrateController.COMMON_MIGRATE_MESSAGE}`;
MigrateController.typescriptPackageName = "typescript";
MigrateController.backupFolder = ".migration_backup";
MigrateController.migrateFailMessage = "Could not migrate the project!";
MigrateController.backupFailMessage = "Could not backup project folders!";
MigrateController.folders = [
    constants.LIB_DIR_NAME,
    constants.HOOKS_DIR_NAME,
    constants.WEBPACK_CONFIG_NAME,
    constants.PACKAGE_JSON_FILE_NAME,
    constants.PACKAGE_LOCK_JSON_FILE_NAME,
    constants.TSCCONFIG_TNS_JSON_NAME,
    constants.KARMA_CONFIG_NAME
];
exports.MigrateController = MigrateController;
$injector.register("migrateController", MigrateController);
