"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;