fork-ts-checker-webpack-plugin-alt
Version:
Runs typescript type checker and linter on separate process.
625 lines (624 loc) • 27.9 kB
JavaScript
"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.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();
// tslint:disable-next-line:no-implicit-dependencies
this.typescriptPath = options.typescript || require.resolve('typescript');
try {
this.typescriptVersion = require(this.typescriptPath).version;
}
catch (_ignored) {
throw new Error('When you use this plugin you must install `typescript`.');
}
try {
this.tslintVersion = this.tslint
? // tslint:disable-next-line:no-implicit-dependencies
require('tslint').Linter.VERSION
: undefined;
}
catch (_ignored) {
throw new Error('When you use `tslint` option, make sure to install `tslint`.');
}
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, {
TYPESCRIPT_PATH: this.typescriptPath,
TSCONFIG: this.tsconfigPath,
COMPILER_OPTIONS: JSON.stringify(this.compilerOptions),
TSLINT: this.tslintPath || '',
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;