"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
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.WebpackCompilerService = void 0;
const path = require("path");
const semver = require("semver");
const _ = require("lodash");
const events_1 = require("events");
const decorators_1 = require("../../common/decorators");
const constants_1 = require("../../constants");
const yok_1 = require("../../common/yok");
class WebpackCompilerService extends events_1.EventEmitter {
    constructor($errors, $childProcess, $fs, $hooksService, $hostInfo, $logger, $mobileHelper, $cleanupService, $packageManager, $packageInstallationManager) {
        super();
        this.$errors = $errors;
        this.$childProcess = $childProcess;
        this.$fs = $fs;
        this.$hooksService = $hooksService;
        this.$hostInfo = $hostInfo;
        this.$logger = $logger;
        this.$mobileHelper = $mobileHelper;
        this.$cleanupService = $cleanupService;
        this.$packageManager = $packageManager;
        this.$packageInstallationManager = $packageInstallationManager;
        this.webpackProcesses = {};
        this.expectedHashes = {};
    }
    compileWithWatch(platformData, projectData, prepareData) {
        return __awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
                if (this.webpackProcesses[platformData.platformNameLowerCase]) {
                    resolve();
                    return;
                }
                let isFirstWebpackWatchCompilation = true;
                prepareData.watch = true;
                try {
                    const childProcess = yield this.startWebpackProcess(platformData, projectData, prepareData);
                    childProcess.on("message", (message) => {
                        this.$logger.trace("Message from webpack", message);
                        if (message === "Webpack compilation complete.") {
                            this.$logger.info("Webpack build done!");
                            resolve(childProcess);
                        }
                        message = message;
                        if (message.emittedFiles) {
                            if (isFirstWebpackWatchCompilation) {
                                isFirstWebpackWatchCompilation = false;
                                this.expectedHashes[platformData.platformNameLowerCase] = prepareData.hmr ? message.hash : "";
                                return;
                            }
                            const previousHash = this.expectedHashes[platformData.platformNameLowerCase];
                            let result;
                            if (prepareData.hmr) {
                                result = this.getUpdatedEmittedFiles(message.emittedFiles, message.chunkFiles, message.hash, platformData.platformNameLowerCase);
                            }
                            else {
                                result = {
                                    emittedFiles: message.emittedFiles,
                                    fallbackFiles: [],
                                    hash: "",
                                };
                            }
                            const files = result.emittedFiles.map((file) => path.join(platformData.appDestinationDirectoryPath, "app", file));
                            const fallbackFiles = result.fallbackFiles.map((file) => path.join(platformData.appDestinationDirectoryPath, "app", file));
                            const data = {
                                files,
                                hasOnlyHotUpdateFiles: files.every((f) => f.indexOf("hot-update") > -1),
                                hmrData: {
                                    hash: result.hash,
                                    fallbackFiles,
                                },
                                platform: platformData.platformNameLowerCase,
                            };
                            this.$logger.trace("Generated data from webpack message:", data);
                            if (data.hasOnlyHotUpdateFiles && previousHash === message.hash) {
                                return;
                            }
                            if (data.files.length) {
                                this.emit(constants_1.WEBPACK_COMPILATION_COMPLETE, data);
                            }
                        }
                    });
                    childProcess.on("error", (err) => {
                        this.$logger.trace(`Unable to start webpack process in watch mode. Error is: ${err}`);
                        delete this.webpackProcesses[platformData.platformNameLowerCase];
                        reject(err);
                    });
                    childProcess.on("close", (arg) => __awaiter(this, void 0, void 0, function* () {
                        yield this.$cleanupService.removeKillProcess(childProcess.pid.toString());
                        const exitCode = typeof arg === "number" ? arg : arg && arg.code;
                        this.$logger.trace(`Webpack process exited with code ${exitCode} when we expected it to be long living with watch.`);
                        const error = new Error(`Executing webpack failed with exit code ${exitCode}.`);
                        error.code = exitCode;
                        delete this.webpackProcesses[platformData.platformNameLowerCase];
                        reject(error);
                    }));
                }
                catch (err) {
                    reject(err);
                }
            }));
        });
    }
    compileWithoutWatch(platformData, projectData, prepareData) {
        return __awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
                if (this.webpackProcesses[platformData.platformNameLowerCase]) {
                    resolve();
                    return;
                }
                try {
                    const childProcess = yield this.startWebpackProcess(platformData, projectData, prepareData);
                    childProcess.on("error", (err) => {
                        this.$logger.trace(`Unable to start webpack process in non-watch mode. Error is: ${err}`);
                        delete this.webpackProcesses[platformData.platformNameLowerCase];
                        reject(err);
                    });
                    childProcess.on("close", (arg) => __awaiter(this, void 0, void 0, function* () {
                        yield this.$cleanupService.removeKillProcess(childProcess.pid.toString());
                        delete this.webpackProcesses[platformData.platformNameLowerCase];
                        const exitCode = typeof arg === "number" ? arg : arg && arg.code;
                        if (exitCode === 0) {
                            resolve();
                        }
                        else {
                            const error = new Error(`Executing webpack failed with exit code ${exitCode}.`);
                            error.code = exitCode;
                            reject(error);
                        }
                    }));
                }
                catch (err) {
                    reject(err);
                }
            }));
        });
    }
    stopWebpackCompiler(platform) {
        return __awaiter(this, void 0, void 0, function* () {
            if (platform) {
                yield this.stopWebpackForPlatform(platform);
            }
            else {
                const webpackedPlatforms = Object.keys(this.webpackProcesses);
                for (let i = 0; i < webpackedPlatforms.length; i++) {
                    yield this.stopWebpackForPlatform(webpackedPlatforms[i]);
                }
            }
        });
    }
    shouldUsePreserveSymlinksOption() {
        return __awaiter(this, void 0, void 0, function* () {
            const currentPackageManager = yield this.$packageManager.getPackageManagerName();
            const res = currentPackageManager !== constants_1.PackageManagers.pnpm;
            return res;
        });
    }
    startWebpackProcess(platformData, projectData, prepareData) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.$fs.exists(projectData.webpackConfigPath)) {
                this.$errors.fail(`The webpack configuration file ${projectData.webpackConfigPath} does not exist. Ensure you have such file or set correct path in ${constants_1.CONFIG_FILE_NAME_DISPLAY}`);
            }
            const envData = this.buildEnvData(platformData.platformNameLowerCase, projectData, prepareData);
            const envParams = yield this.buildEnvCommandLineParams(envData, platformData, projectData, prepareData);
            const additionalNodeArgs = semver.major(process.version) <= 8 ? ["--harmony"] : [];
            if (yield this.shouldUsePreserveSymlinksOption()) {
                additionalNodeArgs.push("--preserve-symlinks");
            }
            if (process.arch === "x64") {
                additionalNodeArgs.unshift("--max_old_space_size=4096");
            }
            const args = [
                ...additionalNodeArgs,
                path.join(projectData.projectDir, "node_modules", "webpack", "bin", "webpack.js"),
                `--config=${projectData.webpackConfigPath}`,
                ...envParams,
            ];
            if (prepareData.watch) {
                args.push("--watch");
            }
            const stdio = prepareData.watch
                ? ["inherit", "inherit", "inherit", "ipc"]
                : "inherit";
            const childProcess = this.$childProcess.spawn(process.execPath, args, {
                cwd: projectData.projectDir,
                stdio,
            });
            this.webpackProcesses[platformData.platformNameLowerCase] = childProcess;
            yield this.$cleanupService.addKillProcess(childProcess.pid.toString());
            return childProcess;
        });
    }
    buildEnvData(platform, projectData, prepareData) {
        const { env } = prepareData;
        const envData = Object.assign({}, env, { [platform.toLowerCase()]: true });
        const appPath = projectData.getAppDirectoryRelativePath();
        const appResourcesPath = projectData.getAppResourcesRelativeDirectoryPath();
        Object.assign(envData, appPath && { appPath }, appResourcesPath && { appResourcesPath }, {
            nativescriptLibPath: path.resolve(__dirname, "..", "..", "nativescript-cli-lib.js"),
        });
        envData.verbose = envData.verbose || this.$logger.isVerbose();
        envData.production = envData.production || prepareData.release;
        envData.skipSnapshotTools =
            prepareData.nativePrepare && prepareData.nativePrepare.skipNativePrepare;
        if (prepareData.env &&
            (prepareData.env.sourceMap === false ||
                prepareData.env.sourceMap === "false")) {
            delete envData.sourceMap;
        }
        else if (!prepareData.release) {
            envData.sourceMap = true;
        }
        return envData;
    }
    buildEnvCommandLineParams(envData, platformData, projectData, prepareData) {
        return __awaiter(this, void 0, void 0, function* () {
            const envFlagNames = Object.keys(envData);
            const canSnapshot = prepareData.release &&
                this.$mobileHelper.isAndroidPlatform(platformData.normalizedPlatformName);
            if (envData && envData.snapshot) {
                if (!canSnapshot) {
                    this.$logger.warn("Stripping the snapshot flag. " +
                        "Bear in mind that snapshot is only available in Android release builds.");
                    envFlagNames.splice(envFlagNames.indexOf("snapshot"), 1);
                }
                else if (this.$hostInfo.isWindows) {
                    const minWebpackPluginWithWinSnapshotsVersion = "1.3.0";
                    const installedWebpackPluginVersion = yield this.$packageInstallationManager.getInstalledDependencyVersion(constants_1.WEBPACK_PLUGIN_NAME, projectData.projectDir);
                    const hasWebpackPluginWithWinSnapshotsSupport = !!installedWebpackPluginVersion
                        ? semver.gte(semver.coerce(installedWebpackPluginVersion), minWebpackPluginWithWinSnapshotsVersion)
                        : true;
                    if (!hasWebpackPluginWithWinSnapshotsSupport) {
                        this.$errors.fail(`In order to generate Snapshots on Windows, please upgrade your Webpack plugin version (npm i ${constants_1.WEBPACK_PLUGIN_NAME}@latest).`);
                    }
                }
            }
            const args = [];
            envFlagNames.map((item) => {
                let envValue = envData[item];
                if (typeof envValue === "undefined") {
                    return;
                }
                if (typeof envValue === "boolean") {
                    if (envValue) {
                        args.push(`--env.${item}`);
                    }
                }
                else {
                    if (!Array.isArray(envValue)) {
                        envValue = [envValue];
                    }
                    envValue.map((value) => args.push(`--env.${item}=${value}`));
                }
            });
            return args;
        });
    }
    getUpdatedEmittedFiles(allEmittedFiles, chunkFiles, nextHash, platform) {
        const currentHash = this.getCurrentHotUpdateHash(allEmittedFiles);
        const isHashValid = nextHash
            ? this.expectedHashes[platform] === currentHash
            : true;
        this.expectedHashes[platform] = nextHash;
        const emittedHotUpdatesAndAssets = isHashValid
            ? _.difference(allEmittedFiles, chunkFiles)
            : allEmittedFiles;
        const fallbackFiles = chunkFiles.concat(emittedHotUpdatesAndAssets.filter((f) => f.indexOf("hot-update") === -1));
        return {
            emittedFiles: emittedHotUpdatesAndAssets,
            fallbackFiles,
            hash: currentHash,
        };
    }
    getCurrentHotUpdateHash(emittedFiles) {
        let hotHash;
        const hotUpdateScripts = emittedFiles.filter((x) => x.endsWith(".hot-update.js"));
        if (hotUpdateScripts && hotUpdateScripts.length) {
            const hotUpdateName = hotUpdateScripts[0];
            const matcher = /^(.+)\.(.+)\.hot-update/gm;
            const matches = matcher.exec(hotUpdateName);
            hotHash = matches[2];
        }
        return hotHash || "";
    }
    stopWebpackForPlatform(platform) {
        return __awaiter(this, void 0, void 0, function* () {
            this.$logger.trace(`Stopping webpack watch for platform ${platform}.`);
            const webpackProcess = this.webpackProcesses[platform];
            yield this.$cleanupService.removeKillProcess(webpackProcess.pid.toString());
            if (webpackProcess) {
                webpackProcess.kill("SIGINT");
                delete this.webpackProcesses[platform];
            }
        });
    }
}
__decorate([
    decorators_1.performanceLog()
], WebpackCompilerService.prototype, "startWebpackProcess", null);
exports.WebpackCompilerService = WebpackCompilerService;
yok_1.injector.register("webpackCompilerService", WebpackCompilerService);
