"use strict";
/*
 * Copyright 2018, collaboration Factory AG. All rights reserved.
 */
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.NPMResolver = void 0;
const path = require("path");
const process = require("process");
const fs = require("fs");
const fs_1 = require("fs");
const crypto = require("crypto");
const spawn = require("cross-spawn");
const chokidar = require("chokidar");
const executor_1 = require("../executor");
const utils_1 = require("../utils");
const PackageVersion_1 = require("./PackageVersion");
const rimraf = require("rimraf");
const RegistryInitializer_1 = require("./RegistryInitializer");
class NPMResolver {
    constructor(mainRepo, watch) {
        this.watch = watch;
        this.mainRepo = mainRepo;
        this.hashFilePath = this.getHashFilePath();
        this.watchers = [];
    }
    /**
     * Installs plugin dependencies
     * @param pluginName Plugin name
     * @param assetsPath Assets folder path
     * @private
     */
    static installPluginDependencies(pluginName, assetsPath) {
        NPMResolver.warnNonExactPluginDependenciesVersions(pluginName, NPMResolver.getPluginPackageJsonPath(assetsPath));
        const oldCwd = process.cwd();
        process.chdir(assetsPath);
        const nodeModulesFolder = NPMResolver.getNodeModulesPath(assetsPath);
        if (fs.existsSync(nodeModulesFolder)) {
            console.log(`⟲ [${pluginName}] (NPM) removing node_modules folder...`);
            console.log(fs.rmSync(nodeModulesFolder, {
                recursive: true,
                force: true,
            }));
        }
        else {
            console.log(`⟲ [${pluginName}] (NPM) node_modules folder does not exist...`, nodeModulesFolder);
        }
        console.log(`⟲ [${pluginName}] (NPM) installing dependencies...`);
        utils_1.debug(`[${pluginName}] (NPM) running: npm install --force --package-lock false`);
        const res = spawn.sync('npm', [
            'install',
            '--force',
            '--package-lock',
            'false',
        ]);
        if (res.status !== 0) {
            utils_1.debug(`[${pluginName}] (NPM) installing dependencies failed with error ${res.stderr}`);
            throw Error(`[${pluginName}] (NPM) installing dependencies failed!`);
        }
        console.log(utils_1.cgreen `✓`, `[${pluginName}] (NPM) dependencies successfully installed`);
        NPMResolver.removePluginSymlinks(pluginName, assetsPath);
        process.chdir(oldCwd);
        return true;
    }
    /**
     * Gets plugin node_modules path
     * @param assetsPath Assets path
     * @private
     */
    static getPluginNodeModulesPath(assetsPath) {
        return path.resolve(assetsPath, NPMResolver.NODE_MODULES);
    }
    /**
     * Plugin dependencies should have exact versions. This method will log if there are any dependencies with non-exact
     * version
     * @param pluginName Name of a cplace plugin
     * @param packageJsonPath package.json path
     * @private
     */
    static warnNonExactPluginDependenciesVersions(pluginName, packageJsonPath) {
        const nonExactVersions = NPMResolver.getNonExactPluginDependenciesVersions(pluginName, packageJsonPath);
        if (nonExactVersions.length) {
            console.log(utils_1.cwarn `⇢ [${pluginName}] (NPM) The following dependencies should have strict versions:`, utils_1.cwarn `\n\n${nonExactVersions.join('\r\n')}\n\n`, utils_1.cwarn `To avoid potential malfunctions, please install dependencies the following way:\n`, utils_1.cwarn `npm install yourdependency --save-exact\n`);
        }
    }
    /**
     * Gets all non-exact versions from the plugin package.json
     * @param pluginName Name of a cplace plugin
     * @param packageJsonPath package.json path
     * @private
     */
    static getNonExactPluginDependenciesVersions(pluginName, packageJsonPath) {
        const packageJsonString = fs.readFileSync(packageJsonPath, {
            encoding: 'utf8',
        });
        let packageJson;
        try {
            packageJson = JSON.parse(packageJsonString);
        }
        catch (e) {
            utils_1.debug(`[${pluginName}] (NPM) can't parse ${packageJsonPath}`);
            return [];
        }
        const dependencies = packageJson.dependencies;
        if (!dependencies) {
            return [];
        }
        const rangeCharacters = ['^', '~', '>', '<', 'x', '*', '||'];
        return Object.keys(dependencies).reduce((acc, dependency) => {
            const version = dependencies[dependency];
            const hasRangeCharacter = rangeCharacters.some((rangeCharacter) => version.includes(rangeCharacter));
            if (hasRangeCharacter) {
                acc.push(dependency.concat(':').concat(version));
            }
            return acc;
        }, []);
    }
    /**
     * Removes symlinks inside plugin node_modules folder
     * @param pluginName Plugin name
     * @param assetsPath Assets folder path
     * @private
     */
    static removePluginSymlinks(pluginName, assetsPath) {
        const isWindows = process.platform === 'win32';
        if (isWindows) {
            const nodeModulesPath = path.resolve(assetsPath, 'node_modules');
            if (fs.existsSync(nodeModulesPath)) {
                console.log(`⟲ [${pluginName}] (NPM) removing symlinks...`);
                fs.readdirSync(nodeModulesPath).forEach((dir) => {
                    const dirPath = path.resolve(nodeModulesPath, dir);
                    if (fs.lstatSync(dirPath).isSymbolicLink()) {
                        rimraf.sync(dirPath);
                    }
                });
                console.log(utils_1.cgreen `✓`, `[${pluginName}] (NPM) symlinks removed`);
            }
        }
    }
    /**
     * Gets package-lock.json hash
     * @param packageLockPath package-lock.json path
     * @private
     */
    static getHash4PackageLock(packageLockPath) {
        const hash = crypto.createHash('sha256');
        const data = fs.readFileSync(packageLockPath);
        hash.update(data);
        return hash.digest('hex');
    }
    /**
     * Gets plugin package-lock.json path
     * @param assetsPath Assets path
     * @private
     */
    static getPluginPackageJsonPath(assetsPath) {
        return path.resolve(assetsPath, NPMResolver.PACKAGE_JSON);
    }
    /**
     * Gets plugin node_modules path
     * @param assetsPath Assets path
     * @private
     */
    static getNodeModulesPath(assetsPath) {
        return path.resolve(assetsPath, NPMResolver.NODE_MODULES);
    }
    /**
     * Checks if package-lock.json was updated
     * @param hashFilePath Hash file path
     * @param packageLockPath package-lock.json path
     * @param pluginName Provided plugin name
     * @private
     */
    static packageLockWasUpdated(hashFilePath, packageLockPath, pluginName) {
        const oldHash = fs.readFileSync(hashFilePath, { encoding: 'utf8' });
        if (oldHash === NPMResolver.getHash4PackageLock(packageLockPath)) {
            const pluginLog = pluginName ? `[${pluginName}] ` : '';
            console.log(utils_1.cgreen `✓`, `${pluginLog}(NPM) node_modules are up to date`);
            return false;
        }
        return true;
    }
    static shouldResolveNpmModules() {
        return PackageVersion_1.PackageVersion.get().major !== 1;
    }
    resolve() {
        return __awaiter(this, void 0, void 0, function* () {
            const registryInitializer = new RegistryInitializer_1.RegistryInitializer();
            registryInitializer.initRegistry();
            this.checkAndInstall();
            if (this.watch) {
                this.registerWatchers();
            }
            return Promise.resolve();
        });
    }
    stop() {
        this.watchers.forEach((watcher) => {
            watcher.close();
        });
    }
    registerWatchers() {
        // packageLockWatcher
        const packageJsonWatcher = chokidar.watch([this.getPackageLockPath()]);
        this.watchers.push(packageJsonWatcher);
        packageJsonWatcher
            .on('change', () => {
            this.checkAndInstall();
        })
            .on('error', (e) => {
            console.error(utils_1.cerr `(NPM) error while watching package-lock.json: ${e}`);
            packageJsonWatcher.close();
        });
        // nodeModulesWatcher
        const glob = executor_1.Scheduler.convertToUnixPath(`${this.getNodeModulesPath()}`);
        const nodeModulesWatcher = chokidar.watch(glob);
        this.watchers.push(nodeModulesWatcher);
        let ready = false;
        let debounce;
        const handleEvent = () => {
            if (!ready) {
                return;
            }
            debounce && clearTimeout(debounce);
            debounce = setTimeout(() => {
                if (!fs.existsSync(this.getNodeModulesPath())) {
                    console.log(utils_1.cerr `(NPM) node_modules folder has been removed - restart cplace-asc`);
                    process.exit();
                }
                else {
                    this.checkAndInstall();
                }
            }, 500);
        };
        nodeModulesWatcher
            .on('ready', () => (ready = true))
            .on('unlink', handleEvent)
            .on('unlinkDir', handleEvent)
            .on('error', (e) => {
            console.error(utils_1.cerr `(NPM) node_modules watcher failed: ${e}`);
            nodeModulesWatcher.close();
        });
    }
    checkAndInstall() {
        if (!NPMResolver.shouldResolveNpmModules()) {
            // clean up the checked-in node_modules if required
            if (fs_1.existsSync(path.join(NPMResolver.NODE_MODULES, 'webdriverio'))) {
                console.log(utils_1.cgreen `⇢`, 'Deleting the node_modules folder...');
                rimraf.sync(path.join(NPMResolver.NODE_MODULES));
                // Fun on Windows! rmdirSync can return before the folder is actually deleted completely.
                utils_1.debug('Deleted, try to recreate...');
                let delCount = 0;
                while (true) {
                    if (delCount > 20) {
                        throw 'Waited too long for the node_modules folder to be deleted. Giving up.';
                    }
                    try {
                        fs.mkdirSync(NPMResolver.NODE_MODULES);
                    }
                    catch (e) {
                        utils_1.debug(e);
                        utils_1.debug('Wait a little...');
                        utils_1.sleepBusy(1000);
                        delCount++;
                        continue;
                    }
                    utils_1.debug('Delete again...');
                    rimraf.sync(path.join(NPMResolver.NODE_MODULES));
                    break;
                }
            }
            if (!fs_1.existsSync(NPMResolver.NODE_MODULES)) {
                console.log(utils_1.cgreen `⇢`, 'Checkout the node_modules from Git');
                spawn.sync('git', ['checkout', '--', NPMResolver.NODE_MODULES], {
                    stdio: [process.stdin, process.stdout, process.stderr],
                    cwd: this.mainRepo,
                });
            }
            console.log(utils_1.cgreen `⇢`, `(NPM) package.json:v1.0.0 -> node_modules checked in`);
            return;
        }
        else {
            console.log(utils_1.cgreen `⇢`, `(NPM) package.json:>v2.0.0 -> checking for npm install`);
        }
        if (this.hasNoNodeModules()) {
            console.log(utils_1.cgreen `⇢`, `(NPM) node_modules don't exist...`);
            this.doNpmInstallAndCreateHash();
        }
        else {
            if (fs.existsSync(this.hashFilePath)) {
                if (NPMResolver.packageLockWasUpdated(this.hashFilePath, this.getPackageLockPath())) {
                    console.log(utils_1.cgreen `⇢`, `(NPM) package-lock.json was updated...`);
                    this.doNpmInstallAndCreateHash();
                }
            }
            else {
                this.doNpmInstallAndCreateHash();
            }
        }
    }
    doNpmInstallAndCreateHash() {
        console.log(`⟲ (NPM) executing npm install`);
        const result = spawn.sync('npm', ['install'], {
            stdio: [process.stdin, process.stdout, process.stderr],
            cwd: this.mainRepo,
        });
        if (result.status !== 0) {
            console.log(utils_1.cred `✗`, `(NPM) npm install ran into: ${result.error} and failed`);
            throw Error(`✗ (NPM) npm install failed...`);
        }
        console.log(utils_1.cgreen `⇢`, `(NPM) npm install successful`);
        this.createHashFile();
    }
    createHashFile() {
        fs.writeFileSync(this.hashFilePath, NPMResolver.getHash4PackageLock(this.getPackageLockPath()), { encoding: 'utf8' });
    }
    hasNoNodeModules() {
        const nodeModulesPath = this.getNodeModulesPath();
        if (fs.existsSync(nodeModulesPath)) {
            const directories = fs
                .readdirSync(nodeModulesPath)
                .filter((name) => fs.statSync(path.join(nodeModulesPath, name)).isDirectory());
            if (directories.length === 0) {
                return true;
            }
        }
        else {
            fs.mkdirSync(this.getNodeModulesPath());
            return true;
        }
        return false;
    }
    getHashFilePath() {
        return path.resolve(this.mainRepo, NPMResolver.NODE_MODULES, NPMResolver.PACKAGE_LOCK_HASH);
    }
    getNodeModulesPath() {
        return path.resolve(this.mainRepo, NPMResolver.NODE_MODULES);
    }
    getPackageLockPath() {
        return path.resolve(this.mainRepo, NPMResolver.PACKAGE_LOCK_JSON);
    }
}
exports.NPMResolver = NPMResolver;
NPMResolver.PACKAGE_LOCK_HASH = 'package-lock.hash';
NPMResolver.PACKAGE_LOCK_JSON = 'package-lock.json';
NPMResolver.PACKAGE_JSON = 'package.json';
NPMResolver.NODE_MODULES = 'node_modules';
//# sourceMappingURL=NPMResolver.js.map