"use strict";
/**********************************************************************
 * Copyright (c) 2018-2020 Red Hat, Inc.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 ***********************************************************************/
Object.defineProperty(exports, "__esModule", { value: true });
exports.InitSources = void 0;
const axios = require("axios");
const cp = require("child_process");
const fs = require("fs-extra");
const jsYaml = require("js-yaml");
const os = require("os");
const path = require("path");
const readPkg = require("read-pkg");
const tmp = require("tmp");
const resolve_tsconfigs_1 = require("./resolve-tsconfigs");
const cli_error_1 = require("./cli-error");
const logger_1 = require("./logger");
const repository_1 = require("./repository");
/**
 * Init all sources of extensions and plugins by cloning them, creating symlinks, update package.json, etc.
 * @author Florent Benoit
 */
class InitSources {
    /**
     * Constructor
     */
    constructor(rootFolder, packagesFolder, pluginsFolder, cheTheiaFolder, assemblyFolder, theiaVersion) {
        this.rootFolder = rootFolder;
        this.packagesFolder = packagesFolder;
        this.pluginsFolder = pluginsFolder;
        this.cheTheiaFolder = cheTheiaFolder;
        this.assemblyFolder = assemblyFolder;
        this.theiaVersion = theiaVersion;
        /**
         * Source clone locations could be replaced from the command line --alias option.
         */
        this.sourceLocationAliases = new Map();
        /**
         * Set of global dependencies
         */
        this.globalDevDependencies = new Map();
        /**
         * Will clone sources keeping or omiting the history
         */
        this.keepHistory = true;
        this.extensions = [];
    }
    /**
     * Keep or omit git history when cloning sources
     */
    set keepGitHistory(value) {
        this.keepHistory = value;
    }
    /**
     * Install all extensions
     */
    async generate(extensionsPath, isDevMode = false) {
        const extensionsYamlContent = await fs.readFile(extensionsPath);
        const extensionsYaml = jsYaml.load(extensionsYamlContent.toString());
        await this.initGlobalDependencies();
        await fs.ensureDir(this.cheTheiaFolder);
        await Promise.all(extensionsYaml.sources.map(async (extension) => {
            if (isDevMode) {
                extension.checkoutTo = 'main';
            }
            await this.addExtension(extension);
            this.extensions.push(extension);
        }));
        const yarnWorkspaces = this.getYarnWorkspaces();
        await Promise.all(this.extensions.map((extension) => this.updateTypescriptReferences(extension, yarnWorkspaces)));
        await resolve_tsconfigs_1.updateRootTsConfigFile(this.rootFolder);
        await resolve_tsconfigs_1.updateAssemblyTsConfigFile(this.rootFolder, this.assemblyFolder);
    }
    /**
     * Scan package.json file and grab all dev dependencies and store them in globalDevDependencies variable
     */
    async initGlobalDependencies() {
        const extensionPackage = await readPkg(path.join(this.rootFolder, 'package.json'), { normalize: false });
        const keys = Object.keys(extensionPackage.devDependencies);
        await Promise.all(keys.map(key => {
            this.globalDevDependencies.set(key, extensionPackage.devDependencies[key]);
        }));
    }
    /**
     * Adds an extension to the current theia
     * @param extension the extension to add
     */
    async addExtension(extension) {
        // dealing with aliases that may be passed to the command line
        const sourceAlias = this.sourceLocationAliases.get(extension.source);
        if (sourceAlias) {
            logger_1.Logger.info(`Source alias detected for ${extension.source}, replacing with provided source: ${sourceAlias}`);
            extension.source = sourceAlias;
        }
        // first, clone
        await this.clone(extension);
        // perform symlink
        await this.symlink(extension);
        await this.updateDependencies(extension);
        // insert extensions
        await this.insertExtensionIntoAssembly(extension);
        // perform plugins
        await this.pluginsSymlink(extension);
    }
    /**
     * perform update of devDependencies or dependencies in package.json file of the cloned extension
     */
    async updateDependencies(extension, rewrite = true) {
        await Promise.all(extension.extSymbolicLinks.map(async (symbolicLink) => {
            // grab package.json
            const extensionJsonPath = path.join(symbolicLink, 'package.json');
            const extensionPackage = await readPkg(extensionJsonPath, { normalize: false });
            const rawExtensionPackage = require(extensionJsonPath);
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            const dependencies = extensionPackage.dependencies;
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            const devDependencies = extensionPackage.devDependencies;
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            const updatedDependencies = {};
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            const updatedDevDependencies = {};
            const keysDependencies = dependencies ? Object.keys(dependencies) : [];
            await Promise.all(keysDependencies.map(async (key) => {
                updatedDependencies[key] = this.updateDependency(key, dependencies[key]);
            }));
            rawExtensionPackage['dependencies'] = updatedDependencies;
            const keysDevDependencies = devDependencies ? Object.keys(devDependencies) : [];
            await Promise.all(keysDevDependencies.map(async (key) => {
                updatedDevDependencies[key] = this.updateDependency(key, devDependencies[key]);
            }));
            rawExtensionPackage['devDependencies'] = updatedDevDependencies;
            // write again the file
            if (rewrite) {
                const json = JSON.stringify(rawExtensionPackage, undefined, 2);
                await fs.writeFile(extensionJsonPath, json + os.EOL, { encoding: 'utf-8' });
            }
        }));
    }
    async updateTypescriptReferences(extension, yarnWorkspaces) {
        await Promise.all(extension.extSymbolicLinks.map(async (symbolicLink) => {
            const extensionJsonPath = path.join(symbolicLink, 'package.json');
            const extensionPackage = await readPkg(extensionJsonPath, { normalize: false });
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            const dependencies = extensionPackage.dependencies;
            const keysDependencies = dependencies ? Object.keys(dependencies) : [];
            await resolve_tsconfigs_1.updateTypescriptReferencesFor(symbolicLink, keysDependencies, yarnWorkspaces);
        }));
    }
    /**
     * Update the given dependency by comparing with global dependencies or checking if it's a theia dependency.
     * @param dependencyKey the key of dependency
     * @param dependencyValue its original value
     */
    updateDependency(dependencyKey, dependencyValue) {
        // is it already defined as a Theia dev dependency ? if yes then return this value
        const rest = this.globalDevDependencies.get(dependencyKey);
        if (rest) {
            return rest;
        }
        // is it a theia dependency
        if (dependencyKey.startsWith('@theia/')) {
            // add carret and the current version
            return `^${this.theiaVersion}`;
        }
        // return default value
        return dependencyValue;
    }
    /**
     * Insert the given extension into the package.json of the assembly.
     * @param extension the given extension
     */
    async insertExtensionIntoAssembly(extension) {
        // first, read the assembly json file
        const assemblyPackageJsonPath = path.join(this.assemblyFolder, 'package.json');
        const assemblyJsonRawContent = require(assemblyPackageJsonPath);
        const dependencies = assemblyJsonRawContent.dependencies;
        extension.extSymbolicLinks.forEach(extensionSymLink => {
            // first resolve path
            const resolvedPath = path.resolve(extensionSymLink, 'package.json');
            // read extension name within symlink
            const extensionName = require(resolvedPath).name;
            const extensionVersion = require(resolvedPath).version;
            dependencies[extensionName] = extensionVersion;
        });
        const json = JSON.stringify(assemblyJsonRawContent, undefined, 2);
        await fs.writeFile(assemblyPackageJsonPath, json);
    }
    async symlink(source) {
        const symbolicLinks = [];
        // now, perform symlink for specific folder or current folder
        if (source.extensions) {
            // ok here we have several folders, need to iterate
            await Promise.all(source.extensions.map(async (folder) => {
                // source folder
                const sourceFolder = path.resolve(source.clonedDir, folder);
                const dest = path.resolve(this.packagesFolder, `${InitSources.PREFIX_PACKAGES_EXTENSIONS}${path.basename(sourceFolder)}`);
                logger_1.Logger.info(`Creating symlink from ${sourceFolder} to ${dest}`);
                await fs.ensureSymlink(sourceFolder, dest);
                symbolicLinks.push(dest);
            }));
        }
        else {
            const dest = path.resolve(this.packagesFolder, `${InitSources.PREFIX_PACKAGES_EXTENSIONS}${path.basename(source.clonedDir)}`);
            logger_1.Logger.info(`Creating symlink from ${source.clonedDir} to ${dest}`);
            await fs.ensureSymlink(source.clonedDir, dest);
            symbolicLinks.push(dest);
        }
        source.extSymbolicLinks = symbolicLinks;
    }
    async pluginsSymlink(source) {
        const symbolicLinks = [];
        // now, perform symlink for specific folder or current folder
        if (source.plugins) {
            // ok here we have several folders, need to iterate
            await Promise.all(source.plugins.map(async (folder) => {
                // source folder
                const sourceFolder = path.resolve(source.clonedDir, folder);
                const dest = path.resolve(this.pluginsFolder, `${path.basename(sourceFolder)}`);
                logger_1.Logger.info(`Creating symlink from ${sourceFolder} to ${dest}`);
                await fs.ensureSymlink(sourceFolder, dest);
                symbolicLinks.push(dest);
            }));
        }
        else {
            const dest = path.resolve(this.pluginsFolder, `${path.basename(source.clonedDir)}`);
            logger_1.Logger.info(`Creating symlink from ${source.clonedDir} to ${dest}`);
            await fs.ensureSymlink(source.clonedDir, dest);
            symbolicLinks.push(dest);
        }
        source.pluginSymbolicLinks = symbolicLinks;
    }
    /**
     * Clone the given extension with the correct branch/tag
     * @param extension the extension to clone
     */
    async clone(extension) {
        if (fs.existsSync(extension.source)) {
            logger_1.Logger.info(`Skipping cloning sources for ${extension.source} already provided...`);
            extension.clonedDir = extension.source;
        }
        else {
            logger_1.Logger.info(`Cloning ${extension.source}...`);
            const repository = new repository_1.Repository(extension.source);
            extension.clonedDir = await repository.clone(this.cheTheiaFolder, repository.getRepositoryName(), extension.checkoutTo, this.keepHistory);
        }
    }
    async initSourceLocationAliases(alias) {
        if (alias) {
            alias.forEach(element => {
                if (element.indexOf('=')) {
                    const index = element.substring(0, element.indexOf('='));
                    const value = element.substring(element.indexOf('=') + 1, element.length);
                    this.sourceLocationAliases.set(index, value);
                }
            });
        }
    }
    async readConfigurationAndGenerate(configPath, dev) {
        let extensionsYamlPath;
        if (configPath) {
            extensionsYamlPath = path.resolve(configPath);
            if (!fs.existsSync(extensionsYamlPath)) {
                throw new cli_error_1.CliError('Config file does not exists');
            }
        }
        else {
            logger_1.Logger.debug("Config wasn't provided, downloading default...");
            const tmpFile = tmp.fileSync();
            const response = await axios.default.get(InitSources.DEFAULT_EXTENSIONS_URI);
            const data = response.data;
            fs.writeFileSync(tmpFile.name, data);
            extensionsYamlPath = tmpFile.name;
        }
        await this.generate(extensionsYamlPath, dev);
    }
    getYarnWorkspaces() {
        const yarnWorkspaces = new Map();
        const yarnWorkspacesRawData = cp.execSync('yarn --json --silent workspaces info', { cwd: this.rootFolder });
        const yarnWorkspacesParsedContent = JSON.parse(yarnWorkspacesRawData.toString());
        if (yarnWorkspacesParsedContent && yarnWorkspacesParsedContent.data) {
            const yarnWorkspacesParsedData = JSON.parse(yarnWorkspacesParsedContent.data.toString());
            for (const [packageName, workspace] of Object.entries(yarnWorkspacesParsedData)) {
                const yarnWorkspace = workspace;
                yarnWorkspace.location = path.join(this.rootFolder, yarnWorkspace.location);
                yarnWorkspaces.set(packageName, yarnWorkspace);
            }
        }
        if (yarnWorkspaces.size === 0) {
            throw new Error('Yarn Workspaces are not found');
        }
        return yarnWorkspaces;
    }
}
exports.InitSources = InitSources;
/**
 * Prefix for extensions.
 */
InitSources.PREFIX_PACKAGES_EXTENSIONS = '@che-';
InitSources.DEFAULT_EXTENSIONS_URI = 'https://raw.githubusercontent.com/eclipse-che/che-theia/main/che-theia-init-sources.yml';
InitSources.argBuilder = (theYargs) => theYargs
    .option('config', {
    description: 'Path to custom config file',
    alias: 'c',
})
    .option('dev', {
    description: 'Initialize current Theia with Che/Theia extensions from "main" branch instead of provided branches',
    alias: 'd',
    type: 'boolean',
    default: false,
})
    .option('alias', {
    description: "Replace clone source location. If a local path is provided, it won't clone anything but use the folder as a source folder.",
    type: 'array',
});
//# sourceMappingURL=init-sources.js.map