"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MigrateController = void 0;
const path = require("path");
const semver = require("semver");
const constants = require("../constants");
const glob_1 = require("glob");
const _ = require("lodash");
const simple_git_1 = require("simple-git");
const update_controller_base_1 = require("./update-controller-base");
const helpers_1 = require("../common/helpers");
const yok_1 = require("../common/yok");
const temp = require("temp");
const color_1 = require("../color");
class MigrateController extends update_controller_base_1.UpdateControllerBase {
    constructor($fs, $platformCommandHelper, $platformsDataService, $packageInstallationManager, $packageManager, $pacoteService, $logger, $errors, $pluginsService, $projectDataService, $projectConfigService, $options, $resources, $injector, $settingsService, $staticConfig, $terminalSpinnerService, $projectCleanupService, $projectBackupService, $childProcess) {
        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.$logger = $logger;
        this.$errors = $errors;
        this.$pluginsService = $pluginsService;
        this.$projectDataService = $projectDataService;
        this.$projectConfigService = $projectConfigService;
        this.$options = $options;
        this.$resources = $resources;
        this.$injector = $injector;
        this.$settingsService = $settingsService;
        this.$staticConfig = $staticConfig;
        this.$terminalSpinnerService = $terminalSpinnerService;
        this.$projectCleanupService = $projectCleanupService;
        this.$projectBackupService = $projectBackupService;
        this.$childProcess = $childProcess;
        this.migrationDependencies = [
            {
                packageName: "@nativescript/core",
                minVersion: "6.5.0",
                desiredVersion: "~8.9.0",
                shouldAddIfMissing: true,
            },
            {
                packageName: "tns-core-modules",
                shouldRemove: true,
            },
            {
                packageName: "@nativescript/types",
                minVersion: "7.0.0",
                desiredVersion: "~8.9.0",
                isDev: true,
            },
            {
                packageName: "tns-platform-declarations",
                replaceWith: "@nativescript/types",
                minVersion: "6.5.0",
                isDev: true,
            },
            {
                packageName: "tns-core-modules-widgets",
                shouldRemove: true,
            },
            {
                packageName: "nativescript-dev-webpack",
                replaceWith: "@nativescript/webpack",
                shouldRemove: true,
                isDev: true,
                async shouldMigrateAction() {
                    return true;
                },
                migrateAction: this.migrateWebpack.bind(this),
            },
            {
                packageName: "@nativescript/webpack",
                minVersion: "3.0.0",
                desiredVersion: "~5.0.0",
                shouldAddIfMissing: true,
                isDev: true,
            },
            {
                packageName: "nativescript-vue",
                minVersion: "2.7.0",
                desiredVersion: "~2.9.3",
                async shouldMigrateAction(dependency, projectData, loose) {
                    if (!this.hasDependency(dependency, projectData)) {
                        return false;
                    }
                    return await this.shouldMigrateDependencyVersion(dependency, projectData, loose);
                },
                migrateAction: this.migrateNativeScriptVue.bind(this),
            },
            {
                packageName: "nativescript-angular",
                replaceWith: "@nativescript/angular",
                minVersion: "10.0.0",
            },
            {
                packageName: "@nativescript/angular",
                minVersion: "10.0.0",
                desiredVersion: "^19.0.0",
                async shouldMigrateAction(dependency, projectData, loose) {
                    if (!this.hasDependency(dependency, projectData)) {
                        return false;
                    }
                    return await this.shouldMigrateDependencyVersion(dependency, projectData, loose);
                },
                migrateAction: this.migrateNativeScriptAngular.bind(this),
            },
            {
                packageName: "svelte-native",
                minVersion: "0.9.0",
                desiredVersion: "~0.9.4",
                async shouldMigrateAction(dependency, projectData, loose) {
                    if (!this.hasDependency(dependency, projectData)) {
                        return false;
                    }
                    return await this.shouldMigrateDependencyVersion(dependency, projectData, loose);
                },
                migrateAction: this.migrateNativeScriptSvelte.bind(this),
            },
            {
                packageName: "nativescript-unit-test-runner",
                replaceWith: "@nativescript/unit-test-runner",
                shouldRemove: true,
                isDev: true,
                async shouldMigrateAction() {
                    return true;
                },
                migrateAction: this.migrateUnitTestRunner.bind(this),
            },
            {
                packageName: "@nativescript/unit-test-runner",
                minVersion: "1.0.0",
                desiredVersion: "~3.0.0",
                async shouldMigrateAction(dependency, projectData, loose) {
                    if (!this.hasDependency(dependency, projectData)) {
                        return false;
                    }
                    return await this.shouldMigrateDependencyVersion(dependency, projectData, loose);
                },
                migrateAction: this.migrateUnitTestRunner.bind(this),
            },
            {
                packageName: "typescript",
                isDev: true,
                minVersion: "3.7.0",
                desiredVersion: "~5.7.0",
            },
            {
                packageName: "node-sass",
                replaceWith: "sass",
                minVersion: "0.0.0",
                isDev: true,
            },
            {
                packageName: "sass",
                minVersion: "0.0.0",
                desiredVersion: "^1.49.9",
                isDev: true,
            },
            {
                packageName: "tns-ios",
                minVersion: "6.5.3",
                replaceWith: "@nativescript/ios",
                isDev: true,
            },
            {
                packageName: "tns-android",
                minVersion: "6.5.4",
                replaceWith: "@nativescript/android",
                isDev: true,
            },
            {
                packageName: "@nativescript/ios",
                minVersion: "6.5.3",
                desiredVersion: "~8.9.0",
                isDev: true,
            },
            {
                packageName: "@nativescript/android",
                minVersion: "7.0.0",
                desiredVersion: "~8.9.0",
                isDev: true,
            },
        ];
    }
    get $jsonFileSettingsService() {
        const cliVersion = semver.coerce(this.$staticConfig.version);
        const shouldMigrateCacheFilePath = path.join(this.$settingsService.getProfileDir(), `should-migrate-cache-${cliVersion}.json`);
        this.$logger.trace(`Migration cache path is: ${shouldMigrateCacheFilePath}`);
        return this.$injector.resolve("jsonFileSettingsService", {
            jsonFileSettingsPath: shouldMigrateCacheFilePath,
        });
    }
    async shouldMigrate({ projectDir, platforms, loose = false, }) {
        const remainingPlatforms = [];
        let shouldMigrate = false;
        for (const platform of platforms) {
            if (!loose) {
                remainingPlatforms.push(platform);
                continue;
            }
            const cachedResult = await this.getCachedShouldMigrate(projectDir, platform);
            this.$logger.trace(`Got cached result for shouldMigrate for platform: ${platform}: ${cachedResult}`);
            if (cachedResult !== false) {
                remainingPlatforms.push(platform);
            }
        }
        if (remainingPlatforms.length > 0) {
            shouldMigrate = await this._shouldMigrate({
                projectDir,
                platforms: remainingPlatforms,
                loose,
            });
            this.$logger.trace(`Executed shouldMigrate for platforms: ${remainingPlatforms}. Result is: ${shouldMigrate}`);
            if (!shouldMigrate && loose) {
                for (const remainingPlatform of remainingPlatforms) {
                    await this.setCachedShouldMigrate(projectDir, remainingPlatform);
                }
            }
        }
        return shouldMigrate;
    }
    async validate({ projectDir, platforms, loose = true, }) {
        const shouldMigrate = await this.shouldMigrate({
            projectDir,
            platforms,
            loose,
        });
        if (shouldMigrate) {
            this.$errors.fail(`The current application is not compatible with NativeScript CLI ${this.$staticConfig.version}.\n\nRun 'ns migrate' to migrate your project to the latest NativeScript version.\n\nAlternatively you may try running it with '--force' to skip this check.`);
        }
    }
    async migrate({ projectDir, platforms, loose = false, }) {
        this.spinner = this.$terminalSpinnerService.createSpinner();
        const projectData = this.$projectDataService.getProjectData(projectDir);
        this.$logger.trace("MigrationController.migrate called with", {
            projectDir,
            platforms,
            loose: loose,
        });
        const canMigrate = await this.ensureGitCleanOrForce(projectDir);
        if (!canMigrate) {
            this.spinner.fail("Pre-Migration verification failed");
            return;
        }
        this.spinner.succeed("Pre-Migration verification complete");
        this.spinner.info("Backing up project files before migration");
        const backup = await this.backupProject(projectDir);
        this.spinner.succeed("Project files have been backed up");
        this.spinner.info("Cleaning up project files before migration");
        await this.cleanUpProject(projectData);
        this.spinner.succeed("Project files have been cleaned up");
        this.spinner.info("Cleaning up old artifacts");
        await this.handleAutoGeneratedFiles(backup, projectData);
        this.spinner.succeed("Cleaned old artifacts");
        const newConfigPath = path.resolve(projectDir, "nativescript.config.ts");
        if (!this.$fs.exists(newConfigPath)) {
            this.spinner.info(`Migrating project to use ${color_1.color.green("nativescript.config.ts")}`);
            await this.migrateConfigs(projectDir);
            this.spinner.succeed(`Project has been migrated to use ${color_1.color.green("nativescript.config.ts")}`);
        }
        this.spinner.info("Updating project dependencies");
        await this.migrateDependencies(projectData, platforms, loose);
        this.spinner.succeed("Project dependencies have been updated");
        const isAngular = this.hasDependency({
            packageName: "@nativescript/angular",
        }, projectData);
        let polyfillsPath;
        if (isAngular) {
            polyfillsPath = await this.checkOrCreatePolyfillsTS(projectData);
        }
        const tsConfigPath = path.resolve(projectDir, "tsconfig.json");
        if (this.$fs.exists(tsConfigPath)) {
            this.spinner.info(`Updating ${color_1.color.yellow("tsconfig.json")}`);
            await this.migrateTSConfig({
                tsConfigPath,
                isAngular,
                polyfillsPath,
            });
            this.spinner.succeed(`Updated ${color_1.color.yellow("tsconfig.json")}`);
        }
        await this.migrateWebpack5(projectDir, projectData);
        await this.runESLint(projectDir);
        this.spinner.succeed("Migration complete.");
        this.$logger.info("");
        this.$logger.printMarkdown("Project has been successfully migrated. The next step is to run `ns run <platform>` to ensure everything is working properly." +
            "\n\nPlease note that you may need additional changes to complete the migration.");
    }
    async _shouldMigrate({ projectDir, platforms, loose, }) {
        const isMigrate = _.get(this.$options, "argv._[0]") === "migrate";
        const projectData = this.$projectDataService.getProjectData(projectDir);
        const projectInfo = this.$projectConfigService.detectProjectConfigs(projectData.projectDir);
        if (!isMigrate && projectInfo.hasNSConfig) {
            return;
        }
        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) {
                if (dependency.shouldAddIfMissing) {
                    this.$logger.trace(`${shouldMigrateCommonMessage}'${dependency.packageName}' is missing.`);
                    if (loose) {
                        continue;
                    }
                    return true;
                }
                continue;
            }
            if (dependency.shouldMigrateAction) {
                const shouldMigrate = await dependency.shouldMigrateAction.bind(this)(dependency, projectData, loose);
                if (shouldMigrate) {
                    this.$logger.trace(`${shouldMigrateCommonMessage}'${dependency.packageName}' requires an update.`);
                    return true;
                }
            }
            if (dependency.replaceWith || dependency.shouldRemove) {
                this.$logger.trace(`${shouldMigrateCommonMessage}'${dependency.packageName}' is deprecated.`);
                if (loose) {
                    continue;
                }
                return true;
            }
            const shouldUpdate = await this.shouldMigrateDependencyVersion(dependency, projectData, loose);
            if (shouldUpdate) {
                this.$logger.trace(`${shouldMigrateCommonMessage}'${dependency.packageName}' should be updated.`);
                return true;
            }
        }
        return false;
    }
    async shouldMigrateDependencyVersion(dependency, projectData, loose) {
        var _a, _b;
        const installedVersion = await this.$packageInstallationManager.getInstalledDependencyVersion(dependency.packageName, projectData.projectDir);
        const desiredVersion = (_a = dependency.desiredVersion) !== null && _a !== void 0 ? _a : dependency.minVersion;
        const minVersion = (_b = dependency.minVersion) !== null && _b !== void 0 ? _b : dependency.desiredVersion;
        if (dependency.shouldUseExactVersion &&
            installedVersion !== desiredVersion) {
            return true;
        }
        return this.isOutdatedVersion(installedVersion, { minVersion, desiredVersion }, loose);
    }
    async getCachedShouldMigrate(projectDir, platform) {
        let cachedShouldMigrateValue = null;
        const cachedHash = await this.$jsonFileSettingsService.getSettingValue((0, helpers_1.getHash)(`${projectDir}${platform.toLowerCase()}`));
        const packageJsonHash = await this.getPackageJsonHash(projectDir);
        if (cachedHash === packageJsonHash) {
            cachedShouldMigrateValue = false;
        }
        return cachedShouldMigrateValue;
    }
    async setCachedShouldMigrate(projectDir, platform) {
        this.$logger.trace(`Caching shouldMigrate result for platform ${platform}.`);
        const packageJsonHash = await this.getPackageJsonHash(projectDir);
        await this.$jsonFileSettingsService.saveSetting((0, helpers_1.getHash)(`${projectDir}${platform.toLowerCase()}`), packageJsonHash);
    }
    async getPackageJsonHash(projectDir) {
        const projectPackageJsonFilePath = path.join(projectDir, constants.PACKAGE_JSON_FILE_NAME);
        return await this.$fs.getFileShasum(projectPackageJsonFilePath);
    }
    async ensureGitCleanOrForce(projectDir) {
        const git = (0, simple_git_1.default)(projectDir);
        const isGit = await git.checkIsRepo();
        const isForce = this.$options.force;
        if (!isGit) {
            if (!isForce) {
                this.$logger.printMarkdown(`Running \`ns migrate\` in a non-git project is not recommended. If you want to skip this check run \`ns migrate --force\`.`);
                this.$errors.fail("Not in Git repo.");
                return false;
            }
            this.spinner.warn(`Not in Git repo, but using ${color_1.color.red("--force")}`);
            return true;
        }
        const isClean = (await git.status()).isClean();
        if (!isClean) {
            if (!isForce) {
                this.$logger.printMarkdown(`Current git branch has uncommitted changes. Please commit the changes and try again. Alternatively run \`ns migrate --force\` to skip this check.`);
                this.$errors.fail("Git branch not clean.");
                return false;
            }
            this.spinner.warn(`Git branch not clean, but using ${color_1.color.red("--force")}`);
            return true;
        }
        return true;
    }
    async backupProject(projectDir) {
        const projectData = this.$projectDataService.getProjectData(projectDir);
        const backup = this.$projectBackupService.getBackup("migration");
        backup.addPaths([
            ...MigrateController.pathsToBackup,
            path.join(projectData.getAppDirectoryRelativePath(), "package.json"),
        ]);
        try {
            return backup.create();
        }
        catch (error) {
            this.spinner.fail(`Project backup failed.`);
            backup.remove();
            this.$errors.fail(`Project backup failed. Error is: ${error.message}`);
        }
    }
    async cleanUpProject(projectData) {
        await this.$projectCleanupService.clean([
            constants.HOOKS_DIR_NAME,
            constants.PLATFORMS_DIR_NAME,
            constants.NODE_MODULES_FOLDER_NAME,
            constants.PACKAGE_LOCK_JSON_FILE_NAME,
        ]);
        const { dependencies, devDependencies } = await this.$pluginsService.getDependenciesFromPackageJson(projectData.projectDir);
        const hasSchematics = [...dependencies, ...devDependencies].find((p) => p.name === "@nativescript/schematics");
        if (!hasSchematics) {
            await this.$projectCleanupService.clean([
                constants.TSCCONFIG_TNS_JSON_NAME,
            ]);
        }
    }
    async handleAutoGeneratedFiles(backup, projectData) {
        const globOptions = {
            nocase: true,
            matchBase: true,
            nodir: true,
            absolute: false,
            cwd: projectData.appDirectoryPath,
            withFileTypes: false,
        };
        const jsFiles = (0, glob_1.globSync)("*.@(js|ts|js.map)", globOptions);
        const autoGeneratedJsFiles = this.getGeneratedFiles(jsFiles, [".js"], [".ts"]);
        const autoGeneratedJsMapFiles = this.getGeneratedFiles(jsFiles, [".map"], [""]);
        const cssFiles = (0, glob_1.globSync)("*.@(less|sass|scss|css)", globOptions);
        const autoGeneratedCssFiles = this.getGeneratedFiles(cssFiles, [".css"], [".scss", ".sass", ".less"]);
        const allGeneratedFiles = autoGeneratedJsFiles
            .concat(autoGeneratedJsMapFiles)
            .concat(autoGeneratedCssFiles);
        const pathsToBackup = allGeneratedFiles.map((generatedFile) => path.join(projectData.appDirectoryPath, generatedFile));
        backup.addPaths(pathsToBackup);
        backup.create();
        if (backup.isUpToDate()) {
            await this.$projectCleanupService.clean(pathsToBackup);
        }
    }
    getGeneratedFiles(allFiles, generatedFileExts, sourceFileExts) {
        return 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;
        });
    }
    isOutdatedVersion(current, target, loose) {
        if (!current && loose) {
            return false;
        }
        const installed = semver.coerce(current);
        const min = semver.coerce(target.minVersion);
        const desired = semver.coerce(target.desiredVersion);
        if (loose) {
            if (!installed || !min) {
                return false;
            }
            return semver.lt(installed, min);
        }
        if (!installed || !desired) {
            return true;
        }
        return semver.lt(installed, desired);
    }
    detectAppPath(projectDir, configData) {
        if (configData.appPath) {
            return configData.appPath;
        }
        const possibleAppPaths = [
            path.resolve(projectDir, constants.SRC_DIR),
            path.resolve(projectDir, constants.APP_FOLDER_NAME),
        ];
        const appPath = possibleAppPaths.find((possiblePath) => this.$fs.exists(possiblePath));
        if (appPath) {
            const relativeAppPath = path
                .relative(projectDir, appPath)
                .replace(path.sep, "/");
            this.$logger.trace(`Found app source at '${appPath}'.`);
            return relativeAppPath.toString();
        }
    }
    detectAppResourcesPath(projectDir, configData) {
        if (configData.appResourcesPath) {
            return configData.appResourcesPath;
        }
        const possibleAppResourcesPaths = [
            path.resolve(projectDir, configData.appPath, constants.APP_RESOURCES_FOLDER_NAME),
            path.resolve(projectDir, constants.APP_RESOURCES_FOLDER_NAME),
        ];
        const appResourcesPath = possibleAppResourcesPaths.find((possiblePath) => this.$fs.exists(possiblePath));
        if (appResourcesPath) {
            const relativeAppResourcesPath = path
                .relative(projectDir, appResourcesPath)
                .replace(path.sep, "/");
            this.$logger.trace(`Found App_Resources at '${appResourcesPath}'.`);
            return relativeAppResourcesPath.toString();
        }
    }
    async runMigrateActionIfAny(dependency, projectData, loose, force = false) {
        if (dependency.migrateAction) {
            const shouldMigrate = force ||
                (await dependency.shouldMigrateAction.bind(this)(dependency, projectData, loose));
            if (shouldMigrate) {
                const newDependencies = await dependency.migrateAction(projectData, path.join(projectData.projectDir, MigrateController.backupFolderName));
                for (const newDependency of newDependencies) {
                    await this.migrateDependency(newDependency, projectData, loose);
                }
            }
        }
    }
    async migrateDependencies(projectData, platforms, loose) {
        for (let i = 0; i < this.migrationDependencies.length; i++) {
            const dependency = this.migrationDependencies[i];
            const hasDependency = this.hasDependency(dependency, projectData);
            if (!hasDependency && !dependency.shouldAddIfMissing) {
                continue;
            }
            await this.runMigrateActionIfAny(dependency, projectData, loose);
            await this.migrateDependency(dependency, projectData, loose);
        }
    }
    async migrateDependency(dependency, projectData, loose) {
        var _a, _b, _c, _d, _e;
        const hasDependency = this.hasDependency(dependency, projectData);
        if (hasDependency && dependency.warning) {
            this.$logger.warn(dependency.warning);
        }
        if (!hasDependency) {
            if (!dependency.shouldAddIfMissing) {
                return;
            }
            const version = (_a = dependency.desiredVersion) !== null && _a !== void 0 ? _a : dependency.minVersion;
            this.$pluginsService.addToPackageJson(dependency.packageName, version, dependency.isDev, projectData.projectDir);
            this.spinner.clear();
            this.$logger.info(`  - ${color_1.color.yellow(dependency.packageName)} ${color_1.color.green(version)} has been added`);
            this.spinner.render();
            return;
        }
        if (dependency.replaceWith || dependency.shouldRemove) {
            this.$pluginsService.removeFromPackageJson(dependency.packageName, projectData.projectDir);
            if (!dependency.replaceWith) {
                return;
            }
            const replacementDep = _.find(this.migrationDependencies, (migrationPackage) => migrationPackage.packageName === dependency.replaceWith);
            if (!replacementDep) {
                this.$errors.fail("Failed to find replacement dependency.");
            }
            const version = (_d = (_c = (_b = replacementDep.desiredVersion) !== null && _b !== void 0 ? _b : replacementDep.minVersion) !== null && _c !== void 0 ? _c : dependency.desiredVersion) !== null && _d !== void 0 ? _d : dependency.minVersion;
            this.$pluginsService.addToPackageJson(replacementDep.packageName, version, replacementDep.isDev, projectData.projectDir);
            this.spinner.clear();
            this.$logger.info(`  - ${color_1.color.yellow(dependency.packageName)} has been replaced with ${color_1.color.cyan(replacementDep.packageName)} ${color_1.color.green(version)}`);
            this.spinner.render();
            await this.runMigrateActionIfAny(replacementDep, projectData, loose, true);
            return;
        }
        const shouldMigrateVersion = await this.shouldMigrateDependencyVersion(dependency, projectData, loose);
        if (!shouldMigrateVersion) {
            return;
        }
        const version = (_e = dependency.desiredVersion) !== null && _e !== void 0 ? _e : dependency.minVersion;
        this.$pluginsService.addToPackageJson(dependency.packageName, version, dependency.isDev, projectData.projectDir);
        this.spinner.clear();
        this.$logger.info(`  - ${color_1.color.yellow(dependency.packageName)} has been updated to ${color_1.color.green(version)}`);
        this.spinner.render();
    }
    async migrateConfigs(projectDir) {
        const projectData = this.$projectDataService.getProjectData(projectDir);
        const rootPackageJsonPath = path.resolve(projectDir, constants.PACKAGE_JSON_FILE_NAME);
        const embeddedPackageJsonPath = path.resolve(projectData.projectDir, projectData.getAppDirectoryRelativePath(), constants.PACKAGE_JSON_FILE_NAME);
        const legacyNsConfigPath = path.resolve(projectData.projectDir, constants.CONFIG_NS_FILE_NAME);
        let rootPackageJsonData = {};
        if (this.$fs.exists(rootPackageJsonPath)) {
            rootPackageJsonData = this.$fs.readJson(rootPackageJsonPath);
        }
        const newConfigPath = this.$projectConfigService.writeDefaultConfig(projectData.projectDir);
        this.$projectConfigService.setForceUsingLegacyConfig(true);
        const configData = this.$projectConfigService.readConfig(projectData.projectDir);
        this.$projectConfigService.setForceUsingLegacyConfig(false);
        if (configData.main) {
            rootPackageJsonData.main = configData.main;
            delete configData.main;
        }
        configData.appPath = this.detectAppPath(projectDir, configData);
        configData.appResourcesPath = this.detectAppResourcesPath(projectDir, configData);
        if (rootPackageJsonData.nativescript) {
            delete rootPackageJsonData.nativescript;
        }
        this.$projectConfigService.setForceUsingNewConfig(true);
        const hasUpdatedConfigSuccessfully = await this.$projectConfigService.setValue("", configData);
        if (!hasUpdatedConfigSuccessfully) {
            if (typeof newConfigPath === "string") {
                await this.$projectCleanupService.cleanPath(newConfigPath);
            }
            this.$errors.fail(`Failed to migrate project to use ${constants.CONFIG_FILE_NAME_TS}. One or more values could not be updated.`);
        }
        this.$fs.writeJson(rootPackageJsonPath, rootPackageJsonData);
        await this.$projectCleanupService.cleanPath(embeddedPackageJsonPath);
        await this.$projectCleanupService.cleanPath(legacyNsConfigPath);
        return true;
    }
    async migrateUnitTestRunner(projectData, migrationBackupDirPath) {
        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 = `'${(0, helpers_1.fromWindowsRelativePathToUnix)(relativeTestsDir)}/**/*.*'`;
            const karmaConfTemplate = this.$resources.readText("test/karma.conf.js");
            const karmaConf = _.template(karmaConfTemplate)({
                frameworks,
                testFiles,
                basePath: projectData.getAppDirectoryRelativePath(),
            });
            this.$fs.writeFile(path.join(projectData.projectDir, constants.KARMA_CONFIG_NAME), karmaConf);
        }
        const dependencies = [
            {
                packageName: "karma-webpack",
                shouldRemove: true,
            },
            {
                packageName: "karma-jasmine",
                minVersion: "2.0.1",
                desiredVersion: "~4.0.1",
                isDev: true,
            },
            {
                packageName: "karma-mocha",
                minVersion: "1.3.0",
                desiredVersion: "~2.0.1",
                isDev: true,
            },
            {
                packageName: "karma-chai",
                minVersion: "0.1.0",
                desiredVersion: "~0.1.0",
                isDev: true,
            },
            {
                packageName: "karma-qunit",
                minVersion: "3.1.2",
                desiredVersion: "~4.1.2",
                isDev: true,
            },
            {
                packageName: "karma",
                minVersion: "4.1.0",
                desiredVersion: "~6.3.4",
                isDev: true,
            },
        ];
        return dependencies;
    }
    async migrateTSConfig({ tsConfigPath, isAngular, polyfillsPath, }) {
        try {
            const configContents = this.$fs.readJson(tsConfigPath);
            configContents.compilerOptions = configContents.compilerOptions || {};
            configContents.compilerOptions.target = "es2020";
            configContents.compilerOptions.module = "esnext";
            configContents.compilerOptions.moduleResolution = "node";
            configContents.compilerOptions.experimentalDecorators = true;
            configContents.compilerOptions.removeComments = false;
            configContents.compilerOptions.lib = [
                ...new Set([...(configContents.compilerOptions.lib || []), "ESNext"]),
            ];
            if (isAngular) {
                if (configContents.files) {
                    configContents.files = [
                        ...new Set([
                            ...(configContents.files || []),
                            polyfillsPath !== null && polyfillsPath !== void 0 ? polyfillsPath : "./src/polyfills.ts",
                        ]),
                    ];
                }
            }
            this.$fs.writeJson(tsConfigPath, configContents);
            return true;
        }
        catch (error) {
            this.$logger.trace("Failed to migrate tsconfig.json. Error is: ", error);
            return false;
        }
    }
    async checkOrCreatePolyfillsTS(projectData) {
        const { projectDir, appDirectoryPath } = projectData;
        const possiblePaths = [
            `${appDirectoryPath}/polyfills.ts`,
            `./src/polyfills.ts`,
            `./app/polyfills.ts`,
        ].map((possiblePath) => path.resolve(projectDir, possiblePath));
        let polyfillsPath = possiblePaths.find((possiblePath) => {
            return this.$fs.exists(possiblePath);
        });
        if (polyfillsPath) {
            return "./" + path.relative(projectDir, polyfillsPath);
        }
        const tempDir = temp.mkdirSync({
            prefix: "migrate-angular-polyfills",
        });
        await this.$pacoteService.extractPackage(constants.RESERVED_TEMPLATE_NAMES["angular"], tempDir);
        this.$fs.copyFile(path.resolve(tempDir, "src/polyfills.ts"), possiblePaths[0]);
        this.$fs.deleteDirectory(tempDir);
        this.spinner.succeed(`Created fresh ${color_1.color.cyan("polyfills.ts")}`);
        return "./" + path.relative(projectDir, possiblePaths[0]);
    }
    async migrateNativeScriptAngular() {
        const minVersion = "10.0.0";
        const desiredVersion = "~19.1.0";
        const dependencies = [
            {
                packageName: "@angular/animations",
                minVersion,
                desiredVersion,
                shouldAddIfMissing: true,
            },
            {
                packageName: "@angular/common",
                minVersion,
                desiredVersion,
                shouldAddIfMissing: true,
            },
            {
                packageName: "@angular/compiler",
                minVersion,
                desiredVersion,
                shouldAddIfMissing: true,
            },
            {
                packageName: "@angular/core",
                minVersion,
                desiredVersion,
                shouldAddIfMissing: true,
            },
            {
                packageName: "@angular/forms",
                minVersion,
                desiredVersion,
                shouldAddIfMissing: true,
            },
            {
                packageName: "@angular/platform-browser",
                minVersion,
                desiredVersion,
                shouldAddIfMissing: true,
            },
            {
                packageName: "@angular/platform-browser-dynamic",
                minVersion,
                desiredVersion,
                shouldAddIfMissing: true,
            },
            {
                packageName: "@angular/router",
                minVersion,
                desiredVersion,
                shouldAddIfMissing: true,
            },
            {
                packageName: "rxjs",
                minVersion: "6.6.0",
                desiredVersion: "~7.8.0",
                shouldAddIfMissing: true,
            },
            {
                packageName: "zone.js",
                minVersion: "0.11.1",
                desiredVersion: "~0.15.0",
                shouldAddIfMissing: true,
            },
            {
                packageName: "@angular/cli",
                minVersion,
                desiredVersion,
                isDev: true,
            },
            {
                packageName: "@angular/compiler-cli",
                minVersion,
                desiredVersion,
                isDev: true,
            },
            {
                packageName: "@ngtools/webpack",
                minVersion,
                desiredVersion,
                isDev: true,
            },
            {
                packageName: "@angular-devkit/build-angular",
                minVersion,
                desiredVersion,
                isDev: true,
            },
        ];
        return dependencies;
    }
    async migrateNativeScriptVue() {
        const dependencies = [
            {
                packageName: "nativescript-vue-template-compiler",
                minVersion: "2.7.0",
                desiredVersion: "~2.9.3",
                isDev: true,
                shouldAddIfMissing: true,
            },
            {
                packageName: "nativescript-vue-devtools",
                minVersion: "1.4.0",
                desiredVersion: "~1.5.1",
                isDev: true,
            },
            {
                packageName: "vue-loader",
                shouldRemove: true,
            },
            {
                packageName: "babel-loader",
                shouldRemove: true,
            },
            {
                packageName: "babel-traverse",
                shouldRemove: true,
            },
            {
                packageName: "babel-types",
                shouldRemove: true,
            },
            {
                packageName: "babylon",
                shouldRemove: true,
            },
            {
                packageName: "@babel/core",
                shouldRemove: true,
            },
            {
                packageName: "@babel/preset-env",
                shouldRemove: true,
            },
            {
                packageName: "vue",
                shouldRemove: true,
            },
            {
                packageName: "vue",
                desiredVersion: "2.6.12",
                isDev: true,
            },
        ];
        return dependencies;
    }
    async migrateNativeScriptSvelte() {
        const dependencies = [
            {
                packageName: "svelte-native-nativescript-ui",
                minVersion: "0.9.0",
                desiredVersion: "~0.9.0",
                isDev: true,
                shouldAddIfMissing: true,
            },
            {
                packageName: "svelte-native-preprocessor",
                minVersion: "0.2.0",
                desiredVersion: "~0.2.0",
                isDev: true,
                shouldAddIfMissing: true,
            },
            {
                packageName: "svelte-loader",
                shouldRemove: true,
            },
            {
                packageName: "svelte-loader-hot",
                shouldRemove: true,
            },
            {
                packageName: "svelte",
                shouldRemove: true,
            },
            {
                packageName: "svelte",
                minVersion: "3.24.1",
                desiredVersion: "3.24.1",
                shouldUseExactVersion: true,
                isDev: true,
            },
        ];
        return dependencies;
    }
    async migrateWebpack() {
        const webpackDependencies = [
            "@angular-devkit/core",
            "clean-webpack-plugin",
            "copy-webpack-plugin",
            "css",
            "css-loader",
            "escape-string-regexp",
            "fork-ts-checker-webpack-plugin",
            "global-modules-path",
            "loader-utils",
            "minimatch",
            "@nativescript/hook",
            "nativescript-worker-loader",
            "properties-reader",
            "proxy-lib",
            "raw-loader",
            "resolve-url-loader",
            "sass-loader",
            "sax",
            "schema-utils",
            "semver",
            "shelljs",
            "tapable",
            "terser",
            "terser-webpack-plugin",
            "ts-loader",
            "webpack",
            "webpack-bundle-analyzer",
            "webpack-cli",
            "webpack-sources",
        ];
        return webpackDependencies.map((dep) => {
            return {
                packageName: dep,
                shouldRemove: true,
            };
        });
    }
    async migrateWebpack5(projectDir, projectData) {
        var _a;
        const webpackConfigPath = path.resolve(projectDir, "webpack.config.js");
        if (this.$fs.exists(webpackConfigPath)) {
            const webpackConfigContent = this.$fs.readText(webpackConfigPath);
            if (webpackConfigContent.includes("webpack.init(")) {
                this.spinner.succeed(`Project already using new ${color_1.color.yellow("webpack.config.js")}`);
                return;
            }
        }
        await this.$projectCleanupService.clean(["webpack.config.js"]);
        this.spinner.info(`Initializing new ${color_1.color.yellow("webpack.config.js")}`);
        const { desiredVersion: webpackVersion } = this.migrationDependencies.find((dep) => dep.packageName === constants.WEBPACK_PLUGIN_NAME);
        try {
            const scopedWebpackPackage = `@nativescript/webpack`;
            const resolvedVersion = await this.$packageInstallationManager.getMaxSatisfyingVersion(scopedWebpackPackage, webpackVersion);
            await this.runNPX([
                "--package",
                `${scopedWebpackPackage}@${resolvedVersion}`,
                "nativescript-webpack",
                "init",
            ]);
            this.spinner.succeed(`Initialized new ${color_1.color.yellow("webpack.config.js")}`);
        }
        catch (err) {
            this.spinner.fail(`Failed to initialize ${color_1.color.yellow("webpack.config.js")}`);
            this.$logger.trace("Failed to initialize webpack.config.js. Error is: ", err);
            this.$logger.printMarkdown(`You can try again by running \`npm install\` (or yarn, pnpm) and then \`npx @nativescript/webpack init\`.`);
        }
        const packageJSON = this.$fs.readJson(projectData.projectFilePath);
        const currentMain = (_a = packageJSON.main) !== null && _a !== void 0 ? _a : "app.js";
        const currentMainTS = currentMain.replace(/.js$/, ".ts");
        const appPath = projectData.appDirectoryPath;
        const possibleMains = [
            `./${appPath}/${currentMain}`,
            `./${appPath}/${currentMainTS}`,
            `./${appPath}/main.js`,
            `./${appPath}/main.ts`,
            `./app/${currentMain}`,
            `./app/${currentMainTS}`,
            `./src/${currentMain}`,
            `./src/${currentMainTS}`,
            `./app/main.js`,
            `./app/main.ts`,
            `./src/main.js`,
            `./src/main.ts`,
        ].map((possibleMain) => path.resolve(projectDir, possibleMain));
        let replacedMain = possibleMains.find((possibleMain) => {
            return this.$fs.exists(possibleMain);
        });
        if (replacedMain) {
            replacedMain = `./${path.relative(projectDir, replacedMain)}`.replace(/\\/g, "/");
            packageJSON.main = replacedMain;
            this.$fs.writeJson(projectData.projectFilePath, packageJSON);
            this.spinner.info(`Updated ${color_1.color.yellow("package.json")} main field to ${color_1.color.green(replacedMain)}`);
        }
        else {
            this.$logger.warn();
            this.$logger.warn("Note:\n-----");
            this.$logger.printMarkdown(`Could not determine the correct \`main\` field for \`package.json\`. Make sure to update it manually, pointing to the actual entry file relative to the \`package.json\`.\n`);
        }
    }
    async runESLint(projectDir) {
        this.spinner.start(`Running ESLint fixes`);
        try {
            await this.runNPX(["@nativescript/eslint-plugin", projectDir]);
            this.spinner.succeed(`Applied ESLint fixes`);
        }
        catch (err) {
            this.spinner.fail(`Failed to apply ESLint fixes`);
            this.$logger.trace("Failed to apply ESLint fixes. Error is:", err);
        }
    }
    async runNPX(args = []) {
        const npxVersion = await this.$childProcess.exec("npx -v");
        const npxFlags = [];
        if (semver.gt(semver.coerce(npxVersion), "7.0.0")) {
            npxFlags.push("-y");
        }
        const args_ = ["npx", ...npxFlags, ...args];
        await this.$childProcess.exec(args_.join(" "));
    }
}
exports.MigrateController = MigrateController;
MigrateController.backupFolderName = ".migration_backup";
MigrateController.pathsToBackup = [
    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,
    constants.CONFIG_NS_FILE_NAME,
];
yok_1.injector.register("migrateController", MigrateController);
