"use strict";
var path = require("path");
var process = require("process");
var childProcess = require("child_process");
var chalk_1 = require("chalk");
var fs = require("fs");
var micromatch = require("micromatch");
var os = require("os");
var isString = require("lodash/isString");
var isFunction = require("lodash/isFunction");
var CancellationToken_1 = require("./CancellationToken");
var NormalizedMessage_1 = require("./NormalizedMessage");
var defaultFormatter_1 = require("./formatter/defaultFormatter");
var codeframeFormatter_1 = require("./formatter/codeframeFormatter");
var tapable_1 = require("tapable");
var checkerPluginName = 'fork-ts-checker-webpack-plugin';
var customHooks = {
    forkTsCheckerServiceBeforeStart: 'fork-ts-checker-service-before-start',
    forkTsCheckerCancel: 'fork-ts-checker-cancel',
    forkTsCheckerServiceStartError: 'fork-ts-checker-service-start-error',
    forkTsCheckerWaiting: 'fork-ts-checker-waiting',
    forkTsCheckerServiceStart: 'fork-ts-checker-service-start',
    forkTsCheckerReceive: 'fork-ts-checker-receive',
    forkTsCheckerServiceOutOfMemory: 'fork-ts-checker-service-out-of-memory',
    forkTsCheckerEmit: 'fork-ts-checker-emit',
    forkTsCheckerDone: 'fork-ts-checker-done'
};
/**
 * ForkTsCheckerWebpackPlugin
 * Runs typescript type checker and linter (tslint) on separate process.
 * This speed-ups build a lot.
 *
 * Options description in README.md
 */
var ForkTsCheckerWebpackPlugin = /** @class */ (function () {
    function ForkTsCheckerWebpackPlugin(options) {
        options = options || {};
        this.options = Object.assign({}, options);
        this.tsconfig = options.tsconfig || './tsconfig.json';
        this.compilerOptions =
            typeof options.compilerOptions === 'object'
                ? options.compilerOptions
                : {};
        this.tslint = options.tslint
            ? options.tslint === true
                ? './tslint.json'
                : options.tslint
            : undefined;
        this.tslintAutoFix = options.tslintAutoFix || false;
        this.watch = isString(options.watch)
            ? [options.watch]
            : options.watch || [];
        this.ignoreDiagnostics = options.ignoreDiagnostics || [];
        this.ignoreLints = options.ignoreLints || [];
        this.reportFiles = options.reportFiles || [];
        this.logger = options.logger || console;
        this.silent = options.silent === true; // default false
        this.async = options.async !== false; // default true
        this.checkSyntacticErrors = options.checkSyntacticErrors === true; // default false
        this.workersNumber = options.workers || ForkTsCheckerWebpackPlugin.ONE_CPU;
        this.memoryLimit =
            options.memoryLimit || ForkTsCheckerWebpackPlugin.DEFAULT_MEMORY_LIMIT;
        this.useColors = options.colors !== false; // default true
        this.colors = new chalk_1.default.constructor({ enabled: this.useColors });
        this.formatter =
            options.formatter && isFunction(options.formatter)
                ? options.formatter
                : ForkTsCheckerWebpackPlugin.createFormatter(options.formatter || 'default', options.formatterOptions || {});
        this.tsconfigPath = undefined;
        this.tslintPath = undefined;
        this.watchPaths = [];
        this.compiler = undefined;
        this.started = undefined;
        this.elapsed = undefined;
        this.cancellationToken = undefined;
        this.isWatching = false;
        this.checkDone = false;
        this.compilationDone = false;
        this.diagnostics = [];
        this.lints = [];
        this.emitCallback = this.createNoopEmitCallback();
        this.doneCallback = this.createDoneCallback();
        this.typescriptVersion = require('typescript').version;
        this.tslintVersion = this.tslint
            ? require('tslint').Linter.VERSION
            : undefined;
        this.vue = options.vue === true; // default false
    }
    ForkTsCheckerWebpackPlugin.createFormatter = function (type, options) {
        switch (type) {
            case 'default':
                return defaultFormatter_1.createDefaultFormatter();
            case 'codeframe':
                return codeframeFormatter_1.createCodeframeFormatter(options);
            default:
                throw new Error('Unknown "' + type + '" formatter. Available are: default, codeframe.');
        }
    };
    ForkTsCheckerWebpackPlugin.prototype.apply = function (compiler) {
        this.compiler = compiler;
        this.tsconfigPath = this.computeContextPath(this.tsconfig);
        this.tslintPath = this.tslint
            ? this.computeContextPath(this.tslint)
            : null;
        this.watchPaths = this.watch.map(this.computeContextPath.bind(this));
        // validate config
        var tsconfigOk = fs.existsSync(this.tsconfigPath);
        var tslintOk = !this.tslintPath || fs.existsSync(this.tslintPath);
        // validate logger
        if (this.logger) {
            if (!this.logger.error || !this.logger.warn || !this.logger.info) {
                throw new Error("Invalid logger object - doesn't provide `error`, `warn` or `info` method.");
            }
        }
        if (tsconfigOk && tslintOk) {
            if ('hooks' in compiler) {
                this.registerCustomHooks();
            }
            this.pluginStart();
            this.pluginStop();
            this.pluginCompile();
            this.pluginEmit();
            this.pluginDone();
        }
        else {
            if (!tsconfigOk) {
                throw new Error('Cannot find "' +
                    this.tsconfigPath +
                    '" file. Please check webpack and ForkTsCheckerWebpackPlugin configuration. \n' +
                    'Possible errors: \n' +
                    '  - wrong `context` directory in webpack configuration' +
                    ' (if `tsconfig` is not set or is a relative path in fork plugin configuration)\n' +
                    '  - wrong `tsconfig` path in fork plugin configuration' +
                    ' (should be a relative or absolute path)');
            }
            if (!tslintOk) {
                throw new Error('Cannot find "' +
                    this.tslintPath +
                    '" file. Please check webpack and ForkTsCheckerWebpackPlugin configuration. \n' +
                    'Possible errors: \n' +
                    '  - wrong `context` directory in webpack configuration' +
                    ' (if `tslint` is not set or is a relative path in fork plugin configuration)\n' +
                    '  - wrong `tslint` path in fork plugin configuration' +
                    ' (should be a relative or absolute path)\n' +
                    '  - `tslint` path is not set to false in fork plugin configuration' +
                    ' (if you want to disable tslint support)');
            }
        }
    };
    ForkTsCheckerWebpackPlugin.prototype.computeContextPath = function (filePath) {
        return path.isAbsolute(filePath)
            ? filePath
            : path.resolve(this.compiler.options.context, filePath);
    };
    ForkTsCheckerWebpackPlugin.prototype.pluginStart = function () {
        var _this = this;
        var run = function (_compiler, callback) {
            _this.isWatching = false;
            callback();
        };
        var watchRun = function (_compiler, callback) {
            _this.isWatching = true;
            callback();
        };
        if ('hooks' in this.compiler) {
            // webpack 4
            this.compiler.hooks.run.tapAsync(checkerPluginName, run);
            this.compiler.hooks.watchRun.tapAsync(checkerPluginName, watchRun);
        }
        else {
            // webpack 2 / 3
            this.compiler.plugin('run', run);
            this.compiler.plugin('watch-run', watchRun);
        }
    };
    ForkTsCheckerWebpackPlugin.prototype.pluginStop = function () {
        var _this = this;
        var watchClose = function () {
            _this.killService();
        };
        var done = function (_stats) {
            if (!_this.isWatching) {
                _this.killService();
            }
        };
        if ('hooks' in this.compiler) {
            // webpack 4
            this.compiler.hooks.watchClose.tap(checkerPluginName, watchClose);
            this.compiler.hooks.done.tap(checkerPluginName, done);
        }
        else {
            // webpack 2 / 3
            this.compiler.plugin('watch-close', watchClose);
            this.compiler.plugin('done', done);
        }
        process.on('exit', function () {
            _this.killService();
        });
    };
    ForkTsCheckerWebpackPlugin.prototype.registerCustomHooks = function () {
        if (this.compiler.hooks.forkTsCheckerServiceBeforeStart ||
            this.compiler.hooks.forkTsCheckerCancel ||
            this.compiler.hooks.forkTsCheckerServiceStartError ||
            this.compiler.hooks.forkTsCheckerWaiting ||
            this.compiler.hooks.forkTsCheckerServiceStart ||
            this.compiler.hooks.forkTsCheckerReceive ||
            this.compiler.hooks.forkTsCheckerServiceOutOfMemory ||
            this.compiler.hooks.forkTsCheckerDone ||
            this.compiler.hooks.forkTsCheckerEmit) {
            throw new Error('fork-ts-checker-webpack-plugin hooks are already in use');
        }
        this.compiler.hooks.forkTsCheckerServiceBeforeStart = new tapable_1.AsyncSeriesHook([]);
        this.compiler.hooks.forkTsCheckerCancel = new tapable_1.SyncHook([
            'cancellationToken'
        ]);
        this.compiler.hooks.forkTsCheckerServiceStartError = new tapable_1.SyncHook([
            'error'
        ]);
        this.compiler.hooks.forkTsCheckerWaiting = new tapable_1.SyncHook(['hasTsLint']);
        this.compiler.hooks.forkTsCheckerServiceStart = new tapable_1.SyncHook([
            'tsconfigPath',
            'tslintPath',
            'watchPaths',
            'workersNumber',
            'memoryLimit'
        ]);
        this.compiler.hooks.forkTsCheckerReceive = new tapable_1.SyncHook([
            'diagnostics',
            'lints'
        ]);
        this.compiler.hooks.forkTsCheckerServiceOutOfMemory = new tapable_1.SyncHook([]);
        this.compiler.hooks.forkTsCheckerEmit = new tapable_1.SyncHook([
            'diagnostics',
            'lints',
            'elapsed'
        ]);
        this.compiler.hooks.forkTsCheckerDone = new tapable_1.SyncHook([
            'diagnostics',
            'lints',
            'elapsed'
        ]);
        // for backwards compatibility
        this.compiler._pluginCompat.tap(checkerPluginName, function (options) {
            switch (options.name) {
                case customHooks.forkTsCheckerServiceBeforeStart:
                    options.async = true;
                    break;
                case customHooks.forkTsCheckerCancel:
                case customHooks.forkTsCheckerServiceStartError:
                case customHooks.forkTsCheckerWaiting:
                case customHooks.forkTsCheckerServiceStart:
                case customHooks.forkTsCheckerReceive:
                case customHooks.forkTsCheckerServiceOutOfMemory:
                case customHooks.forkTsCheckerEmit:
                case customHooks.forkTsCheckerDone:
                    return true;
            }
            return undefined;
        });
    };
    ForkTsCheckerWebpackPlugin.prototype.pluginCompile = function () {
        var _this = this;
        if ('hooks' in this.compiler) {
            // webpack 4
            this.compiler.hooks.compile.tap(checkerPluginName, function () {
                _this.compilationDone = false;
                _this.compiler.hooks.forkTsCheckerServiceBeforeStart.callAsync(function () {
                    if (_this.cancellationToken) {
                        // request cancellation if there is not finished job
                        _this.cancellationToken.requestCancellation();
                        _this.compiler.hooks.forkTsCheckerCancel.call(_this.cancellationToken);
                    }
                    _this.checkDone = false;
                    _this.started = process.hrtime();
                    // create new token for current job
                    _this.cancellationToken = new CancellationToken_1.CancellationToken(undefined, undefined);
                    if (!_this.service || !_this.service.connected) {
                        _this.spawnService();
                    }
                    try {
                        _this.service.send(_this.cancellationToken);
                    }
                    catch (error) {
                        if (!_this.silent && _this.logger) {
                            _this.logger.error(_this.colors.red('Cannot start checker service: ' +
                                (error ? error.toString() : 'Unknown error')));
                        }
                        _this.compiler.hooks.forkTsCheckerServiceStartError.call(error);
                    }
                });
            });
        }
        else {
            // webpack 2 / 3
            this.compiler.plugin('compile', function () {
                _this.compilationDone = false;
                _this.compiler.applyPluginsAsync('fork-ts-checker-service-before-start', function () {
                    if (_this.cancellationToken) {
                        // request cancellation if there is not finished job
                        _this.cancellationToken.requestCancellation();
                        _this.compiler.applyPlugins('fork-ts-checker-cancel', _this.cancellationToken);
                    }
                    _this.checkDone = false;
                    _this.started = process.hrtime();
                    // create new token for current job
                    _this.cancellationToken = new CancellationToken_1.CancellationToken(undefined, undefined);
                    if (!_this.service || !_this.service.connected) {
                        _this.spawnService();
                    }
                    try {
                        _this.service.send(_this.cancellationToken);
                    }
                    catch (error) {
                        if (!_this.silent && _this.logger) {
                            _this.logger.error(_this.colors.red('Cannot start checker service: ' +
                                (error ? error.toString() : 'Unknown error')));
                        }
                        _this.compiler.applyPlugins('fork-ts-checker-service-start-error', error);
                    }
                });
            });
        }
    };
    ForkTsCheckerWebpackPlugin.prototype.pluginEmit = function () {
        var _this = this;
        var emit = function (compilation, callback) {
            if (_this.isWatching && _this.async) {
                callback();
                return;
            }
            _this.emitCallback = _this.createEmitCallback(compilation, callback);
            if (_this.checkDone) {
                _this.emitCallback();
            }
            _this.compilationDone = true;
        };
        if ('hooks' in this.compiler) {
            // webpack 4
            this.compiler.hooks.emit.tapAsync(checkerPluginName, emit);
        }
        else {
            // webpack 2 / 3
            this.compiler.plugin('emit', emit);
        }
    };
    ForkTsCheckerWebpackPlugin.prototype.pluginDone = function () {
        var _this = this;
        if ('hooks' in this.compiler) {
            // webpack 4
            this.compiler.hooks.done.tap(checkerPluginName, function (_stats) {
                if (!_this.isWatching || !_this.async) {
                    return;
                }
                if (_this.checkDone) {
                    _this.doneCallback();
                }
                else {
                    if (_this.compiler) {
                        _this.compiler.hooks.forkTsCheckerWaiting.call(_this.tslint !== false);
                    }
                    if (!_this.silent && _this.logger) {
                        _this.logger.info(_this.tslint
                            ? 'Type checking and linting in progress...'
                            : 'Type checking in progress...');
                    }
                }
                _this.compilationDone = true;
            });
        }
        else {
            // webpack 2 / 3
            this.compiler.plugin('done', function () {
                if (!_this.isWatching || !_this.async) {
                    return;
                }
                if (_this.checkDone) {
                    _this.doneCallback();
                }
                else {
                    if (_this.compiler) {
                        _this.compiler.applyPlugins('fork-ts-checker-waiting', _this.tslint !== false);
                    }
                    if (!_this.silent && _this.logger) {
                        _this.logger.info(_this.tslint
                            ? 'Type checking and linting in progress...'
                            : 'Type checking in progress...');
                    }
                }
                _this.compilationDone = true;
            });
        }
    };
    ForkTsCheckerWebpackPlugin.prototype.spawnService = function () {
        var _this = this;
        this.service = childProcess.fork(path.resolve(__dirname, this.workersNumber > 1 ? './cluster.js' : './service.js'), [], {
            execArgv: this.workersNumber > 1
                ? []
                : ['--max-old-space-size=' + this.memoryLimit],
            env: Object.assign({}, process.env, {
                TSCONFIG: this.tsconfigPath,
                COMPILER_OPTIONS: JSON.stringify(this.compilerOptions),
                TSLINT: this.tslintPath || '',
                TSLINTAUTOFIX: this.tslintAutoFix,
                WATCH: this.isWatching ? this.watchPaths.join('|') : '',
                WORK_DIVISION: Math.max(1, this.workersNumber),
                MEMORY_LIMIT: this.memoryLimit,
                CHECK_SYNTACTIC_ERRORS: this.checkSyntacticErrors,
                VUE: this.vue
            }),
            stdio: ['inherit', 'inherit', 'inherit', 'ipc']
        });
        if ('hooks' in this.compiler) {
            // webpack 4
            this.compiler.hooks.forkTsCheckerServiceStart.call(this.tsconfigPath, this.tslintPath, this.watchPaths, this.workersNumber, this.memoryLimit);
        }
        else {
            // webpack 2 / 3
            this.compiler.applyPlugins('fork-ts-checker-service-start', this.tsconfigPath, this.tslintPath, this.watchPaths, this.workersNumber, this.memoryLimit);
        }
        if (!this.silent && this.logger) {
            this.logger.info('Starting type checking' +
                (this.tslint ? ' and linting' : '') +
                ' service...');
            this.logger.info('Using ' +
                this.colors.bold(this.workersNumber === 1
                    ? '1 worker'
                    : this.workersNumber + ' workers') +
                ' with ' +
                this.colors.bold(this.memoryLimit + 'MB') +
                ' memory limit');
            if (this.watchPaths.length && this.isWatching) {
                this.logger.info('Watching:' +
                    (this.watchPaths.length > 1 ? '\n' : ' ') +
                    this.watchPaths.map(function (wpath) { return _this.colors.grey(wpath); }).join('\n'));
            }
        }
        this.service.on('message', function (message) {
            return _this.handleServiceMessage(message);
        });
        this.service.on('exit', function (code, signal) {
            return _this.handleServiceExit(code, signal);
        });
    };
    ForkTsCheckerWebpackPlugin.prototype.killService = function () {
        if (this.service) {
            try {
                if (this.cancellationToken) {
                    this.cancellationToken.cleanupCancellation();
                }
                this.service.kill();
                this.service = undefined;
            }
            catch (e) {
                if (this.logger && !this.silent) {
                    this.logger.error(e);
                }
            }
        }
    };
    ForkTsCheckerWebpackPlugin.prototype.handleServiceMessage = function (message) {
        var _this = this;
        if (this.cancellationToken) {
            this.cancellationToken.cleanupCancellation();
            // job is done - nothing to cancel
            this.cancellationToken = undefined;
        }
        this.checkDone = true;
        this.elapsed = process.hrtime(this.started);
        this.diagnostics = message.diagnostics.map(NormalizedMessage_1.NormalizedMessage.createFromJSON);
        this.lints = message.lints.map(NormalizedMessage_1.NormalizedMessage.createFromJSON);
        if (this.ignoreDiagnostics.length) {
            this.diagnostics = this.diagnostics.filter(function (diagnostic) {
                return _this.ignoreDiagnostics.indexOf(parseInt(diagnostic.getCode(), 10)) === -1;
            });
        }
        if (this.ignoreLints.length) {
            this.lints = this.lints.filter(function (lint) { return _this.ignoreLints.indexOf(lint.getCode()) === -1; });
        }
        if (this.reportFiles.length) {
            var reportFilesPredicate = function (diagnostic) {
                if (diagnostic.file) {
                    var relativeFileName = path.relative(_this.compiler.options.context, diagnostic.file);
                    var matchResult = micromatch([relativeFileName], _this.reportFiles);
                    if (matchResult.length === 0) {
                        return false;
                    }
                }
                return true;
            };
            this.diagnostics = this.diagnostics.filter(reportFilesPredicate);
            this.lints = this.lints.filter(reportFilesPredicate);
        }
        if ('hooks' in this.compiler) {
            // webpack 4
            this.compiler.hooks.forkTsCheckerReceive.call(this.diagnostics, this.lints);
        }
        else {
            // webpack 2 / 3
            this.compiler.applyPlugins('fork-ts-checker-receive', this.diagnostics, this.lints);
        }
        if (this.compilationDone) {
            this.isWatching && this.async ? this.doneCallback() : this.emitCallback();
        }
    };
    ForkTsCheckerWebpackPlugin.prototype.handleServiceExit = function (_code, signal) {
        if (signal === 'SIGABRT') {
            // probably out of memory :/
            if (this.compiler) {
                if ('hooks' in this.compiler) {
                    // webpack 4
                    this.compiler.hooks.forkTsCheckerServiceOutOfMemory.call();
                }
                else {
                    // webpack 2 / 3
                    this.compiler.applyPlugins('fork-ts-checker-service-out-of-memory');
                }
            }
            if (!this.silent && this.logger) {
                this.logger.error(this.colors.red('Type checking and linting aborted - probably out of memory. ' +
                    'Check `memoryLimit` option in ForkTsCheckerWebpackPlugin configuration.'));
            }
        }
    };
    ForkTsCheckerWebpackPlugin.prototype.createEmitCallback = function (compilation, callback) {
        return function emitCallback() {
            var _this = this;
            var elapsed = Math.round(this.elapsed[0] * 1e9 + this.elapsed[1]);
            if ('hooks' in this.compiler) {
                // webpack 4
                this.compiler.hooks.forkTsCheckerEmit.call(this.diagnostics, this.lints, elapsed);
            }
            else {
                // webpack 2 / 3
                this.compiler.applyPlugins('fork-ts-checker-emit', this.diagnostics, this.lints, elapsed);
            }
            this.diagnostics.concat(this.lints).forEach(function (message) {
                // webpack message format
                var formatted = {
                    rawMessage: message.getSeverity().toUpperCase() +
                        ' ' +
                        message.getFormattedCode() +
                        ': ' +
                        message.getContent(),
                    message: _this.formatter(message, _this.useColors),
                    location: {
                        line: message.getLine(),
                        character: message.getCharacter()
                    },
                    file: message.getFile()
                };
                if (message.isWarningSeverity()) {
                    compilation.warnings.push(formatted);
                }
                else {
                    compilation.errors.push(formatted);
                }
            });
            callback();
        };
    };
    ForkTsCheckerWebpackPlugin.prototype.createNoopEmitCallback = function () {
        // tslint:disable-next-line:no-empty
        return function noopEmitCallback() { };
    };
    ForkTsCheckerWebpackPlugin.prototype.createDoneCallback = function () {
        return function doneCallback() {
            var _this = this;
            var elapsed = Math.round(this.elapsed[0] * 1e9 + this.elapsed[1]);
            if (this.compiler) {
                if ('hooks' in this.compiler) {
                    // webpack 4
                    this.compiler.hooks.forkTsCheckerDone.call(this.diagnostics, this.lints, elapsed);
                }
                else {
                    // webpack 2 / 3
                    this.compiler.applyPlugins('fork-ts-checker-done', this.diagnostics, this.lints, elapsed);
                }
            }
            if (!this.silent && this.logger) {
                if (this.diagnostics.length || this.lints.length) {
                    (this.lints || []).concat(this.diagnostics).forEach(function (message) {
                        var formattedMessage = _this.formatter(message, _this.useColors);
                        message.isWarningSeverity()
                            ? _this.logger.warn(formattedMessage)
                            : _this.logger.error(formattedMessage);
                    });
                }
                if (!this.diagnostics.length) {
                    this.logger.info(this.colors.green('No type errors found'));
                }
                if (this.tslint && !this.lints.length) {
                    this.logger.info(this.colors.green('No lint errors found'));
                }
                this.logger.info('Version: typescript ' +
                    this.colors.bold(this.typescriptVersion) +
                    (this.tslint
                        ? ', tslint ' + this.colors.bold(this.tslintVersion)
                        : ''));
                this.logger.info('Time: ' +
                    this.colors.bold(Math.round(elapsed / 1e6).toString()) +
                    'ms');
            }
        };
    };
    ForkTsCheckerWebpackPlugin.DEFAULT_MEMORY_LIMIT = 2048;
    ForkTsCheckerWebpackPlugin.ONE_CPU = 1;
    ForkTsCheckerWebpackPlugin.ALL_CPUS = os.cpus && os.cpus() ? os.cpus().length : 1;
    ForkTsCheckerWebpackPlugin.ONE_CPU_FREE = Math.max(1, ForkTsCheckerWebpackPlugin.ALL_CPUS - 1);
    ForkTsCheckerWebpackPlugin.TWO_CPUS_FREE = Math.max(1, ForkTsCheckerWebpackPlugin.ALL_CPUS - 2);
    return ForkTsCheckerWebpackPlugin;
}());
module.exports = ForkTsCheckerWebpackPlugin;