"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 interfaces_1 = require("./interfaces");
const fs = require("fs");
const path = require("path");
const NPMResolver_1 = require("../model/NPMResolver");
const utils_1 = require("../utils");
const webpack = require("webpack");
const spawn = require("cross-spawn");
const crypto = require("crypto");
const CompressCssCompiler_1 = require("./CompressCssCompiler");
const CplaceTypescriptCompiler_1 = require("./CplaceTypescriptCompiler");
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
class VendorCompiler {
    constructor(pluginName, dependencyPaths, assetsPath, mainRepoDir) {
        this.pluginName = pluginName;
        this.dependencyPaths = dependencyPaths;
        this.assetsPath = assetsPath;
        this.mainRepoDir = mainRepoDir;
        this.compressCssCompiler = new CompressCssCompiler_1.CompressCssCompiler(this.pluginName, this.dependencyPaths, this.assetsPath, this.mainRepoDir, true);
    }
    /**
     * Compiles plugin vendors
     */
    compile() {
        return __awaiter(this, void 0, void 0, function* () {
            console.log(`⟲ [${this.pluginName}] starting vendors compilation...`);
            const startTime = new Date().getTime();
            const dependenciesWereUpdated = NPMResolver_1.NPMResolver.installPluginDependenciesAndCreateHash(this.pluginName, this.assetsPath);
            const pluginIndexExists = this.tscPluginIndex();
            if (!pluginIndexExists) {
                console.log(utils_1.cgreen `⇢`, `[${this.pluginName}] index.ts not found. Bundling skipped!`);
                utils_1.debug(`(VendorCompiler) [${this.pluginName}] To bundle plugin vendors, please add the index.ts file!`);
                this.removeVendors();
                this.prepareVendorJSForCompression();
                yield this.prepareVendorCSSForCompression();
                return Promise.resolve(interfaces_1.CompilationResult.UNCHANGED);
            }
            const oldIndexHash = this.readIndexHash();
            const newIndexHash = this.createIndexHashFile();
            utils_1.debug(`(VendorCompiler) [${this.pluginName}] comparing hash files...`);
            if (oldIndexHash === newIndexHash && !dependenciesWereUpdated) {
                console.log(utils_1.cgreen `✓`, `[${this.pluginName}] vendors are up to date`);
                return Promise.resolve(interfaces_1.CompilationResult.UNCHANGED);
            }
            utils_1.debug(`(VendorCompiler) [${this.pluginName}] hash files changed`);
            yield this.bundlePluginVendors();
            this.prepareVendorJSForCompression();
            yield this.prepareVendorCSSForCompression();
            const endTime = new Date().getTime();
            console.log(utils_1.cgreen `✓`, `[${this.pluginName}] vendors compiled (${utils_1.formatDuration(endTime - startTime)})`);
            return Promise.resolve(interfaces_1.CompilationResult.CHANGED);
        });
    }
    /**
     * Compiles plugin index.ts
     * @private
     */
    tscPluginIndex() {
        utils_1.debug(`(VendorCompiler) [${this.pluginName}] compiling index.ts...`);
        const tsc = path.resolve(__dirname, '../../', 'node_modules/.bin/tsc');
        const index = path.join(this.assetsPath, 'index.ts');
        if (!fs.existsSync(index)) {
            return false;
        }
        const res = spawn.sync(tsc, [path.join(this.assetsPath, 'index.ts'), `--outDir`, path.resolve(this.assetsPath, CplaceTypescriptCompiler_1.CplaceTypescriptCompiler.DEST_DIR)]);
        utils_1.debug(`(VendorCompiler) [${this.pluginName}] index.ts tsc return code: ${res.status}`);
        if (res.status !== 0) {
            utils_1.debug(`(VendorCompiler) [${this.pluginName}] index.ts compilation failed with error ${res.stdout}`);
            throw Error(`[${this.pluginName}] index.ts compilation failed!`);
        }
        utils_1.debug(`(VendorCompiler) [${this.pluginName}] index.ts compiled`);
        return true;
    }
    /**
     * Reads index.js.hash
     * @private
     */
    readIndexHash() {
        const hashPath = this.getIndexHashFilePath();
        if (fs.existsSync(hashPath)) {
            return fs.readFileSync(hashPath, { encoding: 'utf8' });
        }
        else {
            return null;
        }
    }
    /**
     * Creates index.js.hash file
     * @private
     */
    createIndexHashFile() {
        const hash = this.getHash4Index();
        fs.writeFileSync(this.getIndexHashFilePath(), hash, { encoding: 'utf8' });
        return hash;
    }
    /**
     * Gets index.js hash
     * @private
     */
    getHash4Index() {
        const hash = crypto.createHash('sha256');
        const data = fs.readFileSync(path.join(this.assetsPath, CplaceTypescriptCompiler_1.CplaceTypescriptCompiler.DEST_DIR, VendorCompiler.VENDOR_ENTRY));
        hash.update(data);
        return hash.digest('hex');
    }
    /**
     * Gets index.js.hash file path
     * @private
     */
    getIndexHashFilePath() {
        return path.join(this.assetsPath, CplaceTypescriptCompiler_1.CplaceTypescriptCompiler.DEST_DIR, VendorCompiler.VENDOR_ENTRY_HASH);
    }
    /**
     * Gets plugin webpack config
     * @private
     */
    getPluginWebpackConfig() {
        return {
            mode: 'production',
            entry: path.resolve(this.assetsPath, CplaceTypescriptCompiler_1.CplaceTypescriptCompiler.DEST_DIR, VendorCompiler.VENDOR_ENTRY),
            output: {
                path: path.resolve(this.assetsPath, CplaceTypescriptCompiler_1.CplaceTypescriptCompiler.DEST_DIR),
                filename: VendorCompiler.VENDOR_JS_FILE
            },
            resolveLoader: {
                modules: [path.resolve(__dirname, '../../', 'node_modules')]
            },
            resolve: {
                modules: [path.resolve(this.assetsPath, 'node_modules')]
            },
            optimization: {
                minimize: true,
                minimizer: [
                    new CssMinimizerPlugin({
                        parallel: true,
                        minimizerOptions: {
                            preset: [
                                'default',
                                {
                                    discardComments: { removeAll: true },
                                },
                            ],
                        }
                    }),
                    new UglifyJsPlugin({
                        extractComments: true,
                        parallel: true,
                        uglifyOptions: {
                            output: {
                                comments: false,
                            },
                        }
                    })
                ]
            },
            devtool: false,
            plugins: [
                new MiniCssExtractPlugin({
                    filename: `../${VendorCompiler.DEST_CSS_DIR}/${VendorCompiler.VENDOR_CSS_FILE}`
                })
            ],
            module: {
                rules: [
                    {
                        test: /\.css$/,
                        use: [
                            MiniCssExtractPlugin.loader,
                            'css-loader'
                        ]
                    },
                    {
                        test: /\.(sass|scss)$/,
                        use: [
                            MiniCssExtractPlugin.loader,
                            'css-loader',
                            'sass-loader'
                        ]
                    },
                    {
                        test: /\.less$/,
                        use: [
                            MiniCssExtractPlugin.loader,
                            'css-loader',
                            'less-loader'
                        ]
                    },
                    {
                        test: /\.(woff(2)?|ttf|eot|svg|png|jpe?g|gif)$/i,
                        use: [
                            'file-loader'
                        ]
                    },
                ]
            }
        };
    }
    /**
     * Bundles plugin vendors
     * @private
     */
    bundlePluginVendors() {
        const startTime = new Date().getTime();
        console.log(`⟲ [${this.pluginName}] bundling vendors...`);
        return new Promise((resolve, reject) => {
            const config = this.getPluginWebpackConfig();
            this.removeVendors();
            const entryFile = path.resolve(this.assetsPath, CplaceTypescriptCompiler_1.CplaceTypescriptCompiler.DEST_DIR, VendorCompiler.VENDOR_ENTRY);
            const buffer = fs.readFileSync(entryFile);
            if (!buffer.length) {
                // if entry file is empty, there is no need to bundle
                resolve();
                return;
            }
            webpack(config, (err, stats) => {
                if (err) {
                    reject(`${err.message}`);
                }
                else if (stats.hasErrors()) {
                    reject(`${stats.toString()}`);
                }
                else {
                    const endTime = new Date().getTime();
                    console.log(utils_1.cgreen `✓`, `[${this.pluginName}] vendors bundled (${utils_1.formatDuration(endTime - startTime)})`);
                    resolve();
                }
            });
        });
    }
    /**
     * Removes previously generated webpack bundle if exists
     * @private
     */
    removeVendors() {
        const vendorJsFile = path.resolve(this.assetsPath, CplaceTypescriptCompiler_1.CplaceTypescriptCompiler.DEST_DIR, VendorCompiler.VENDOR_JS_FILE);
        const vendorCssFile = path.resolve(this.assetsPath, VendorCompiler.DEST_CSS_DIR, VendorCompiler.VENDOR_CSS_FILE);
        this.removeFileIfExists(vendorJsFile);
        this.removeFileIfExists(vendorCssFile);
    }
    /**
     * Writes vendor.js import to javaScriptIncludesToBeCompressed.txt
     * @private
     */
    prepareVendorJSForCompression() {
        const vendorJsPath = path.resolve(this.assetsPath, CplaceTypescriptCompiler_1.CplaceTypescriptCompiler.DEST_DIR, VendorCompiler.VENDOR_JS_FILE);
        const javaScriptToBeCompressedPath = path.join(this.assetsPath, VendorCompiler.JAVASCRIPT_TO_BE_COMPRESSED);
        const pathToInclude = `/${CplaceTypescriptCompiler_1.CplaceTypescriptCompiler.DEST_DIR}/${VendorCompiler.VENDOR_JS_FILE}`;
        utils_1.debug(`(VendorCompiler) [${this.pluginName}] cleaning JS vendor imports...`);
        const noJsVendor = this.cleanVendor(vendorJsPath, javaScriptToBeCompressedPath, pathToInclude);
        utils_1.debug(`(VendorCompiler) [${this.pluginName}] JS vendor imports cleaned`);
        if (noJsVendor) {
            return;
        }
        this.writeVendorImport(javaScriptToBeCompressedPath, pathToInclude);
    }
    /**
     * Writes vendor.css import to imports.css file
     * @private
     */
    prepareVendorCSSForCompression() {
        const vendorCssPath = path.join(this.assetsPath, VendorCompiler.DEST_CSS_DIR, VendorCompiler.VENDOR_CSS_FILE);
        const cssFolder = path.join(this.assetsPath, 'css');
        const cssImportsPath = path.join(cssFolder, VendorCompiler.CSS_IMPORTS);
        const pathToInclude = `@import url("../${VendorCompiler.DEST_CSS_DIR}/${VendorCompiler.VENDOR_CSS_FILE}");`;
        if (!fs.existsSync(cssFolder)) {
            fs.mkdirSync(cssFolder);
        }
        utils_1.debug(`(VendorCompiler) [${this.pluginName}] cleaning CSS vendor imports...`);
        const noCssVendor = this.cleanVendor(vendorCssPath, cssImportsPath, pathToInclude);
        utils_1.debug(`(VendorCompiler) [${this.pluginName}] CSS vendor imports cleaned`);
        if (noCssVendor) {
            /*
                If there is no css vendor, css compiler has to be started to clean the css outputs.
             */
            return this.compressCssCompiler.compile();
        }
        const cssEntryBeforeWriteExists = fs.existsSync(cssImportsPath);
        this.writeVendorImport(cssImportsPath, pathToInclude);
        const cssEntryAfterWriteExists = fs.existsSync(cssImportsPath);
        if (!cssEntryBeforeWriteExists && cssEntryAfterWriteExists) {
            /*
                If css entry file doesn't exists at the time cplace-asc started, css watch will not be automatically fired.
                Instead css compiler has to be triggered manually.
             */
            return this.compressCssCompiler.compile();
        }
        return Promise.resolve(true);
    }
    /**
     * Writes vendor.(css|js) import to correct file
     * @param fileToWrite File the import needs to be written
     * @param pathToInclude Import path to include
     * @private
     */
    writeVendorImport(fileToWrite, pathToInclude) {
        let buffer = '';
        utils_1.debug(`(VendorCompiler) [${this.pluginName}] Writing vendor imports...`);
        if (fs.existsSync(fileToWrite)) {
            buffer = fs.readFileSync(fileToWrite, 'utf8');
            if (buffer.includes(pathToInclude)) {
                let includedPaths = buffer.split('\n');
                includedPaths = includedPaths
                    .map((line, index) => {
                    if (index === includedPaths.length - 1) {
                        return line.trim();
                    }
                    return line;
                });
                const index = includedPaths.indexOf(pathToInclude);
                // removes included path if already exists
                includedPaths.splice(index, 1);
                buffer = includedPaths.join('\n');
            }
        }
        const content = buffer + `\n${pathToInclude}`;
        fs.writeFileSync(fileToWrite, content);
        utils_1.debug(`(VendorCompiler) [${this.pluginName}] Vendor imports written`);
    }
    /**
     * Cleans vendor imports
     * @param vendorFilePath Generated vendor file path
     * @param fileToWrite File to write vendor import
     * @param pathToInclude Import path to include
     * @private
     */
    cleanVendor(vendorFilePath, fileToWrite, pathToInclude) {
        if (!fs.existsSync(vendorFilePath)) {
            // if there is no file to write import to, skip further process
            if (!fs.existsSync(fileToWrite)) {
                return true;
            }
            let buffer = fs.readFileSync(fileToWrite, 'utf8');
            // if file to write import to, doesn't have any content, remove the file
            if (!buffer.length) {
                this.removeFileIfExists(fileToWrite);
                return true;
            }
            if (buffer.includes(pathToInclude)) {
                // removes included path if already exists
                const includedPaths = buffer.split('\n');
                const index = includedPaths.indexOf(pathToInclude);
                // if file to write import to includes vendor import and there is no vendor, remove the import line
                includedPaths.splice(index, 1);
                buffer = includedPaths.join('\n');
                if (!buffer.length) {
                    this.removeFileIfExists(fileToWrite);
                    return true;
                }
                fs.writeFileSync(fileToWrite, buffer);
            }
            return true;
        }
        return false;
    }
    /**
     * Removes the file, if the file exists
     * @param filePath File to remove
     * @private
     */
    removeFileIfExists(filePath) {
        if (fs.existsSync(filePath)) {
            fs.unlinkSync(filePath);
        }
    }
}
VendorCompiler.DEST_CSS_DIR = 'generated_css';
VendorCompiler.VENDOR_ENTRY = 'index.js';
VendorCompiler.VENDOR_ENTRY_HASH = 'index.js.hash';
VendorCompiler.VENDOR_JS_FILE = 'vendor.js';
VendorCompiler.VENDOR_CSS_FILE = 'vendor.css';
VendorCompiler.JAVASCRIPT_TO_BE_COMPRESSED = 'javaScriptIncludesToBeCompressed.txt';
VendorCompiler.CSS_IMPORTS = 'imports.css';
exports.VendorCompiler = VendorCompiler;
//# sourceMappingURL=VendorCompiler.js.map