Skip to content

Commit 544f205

Browse files
authored
feat: add eslint typescript support (#305)
* test: create test fixture for eslint + activate vscode plugin * feat: add eslint option infrastructure * feat: add basic implementation (untested) - might work! * chore: disable eslint in Travis for now * test: added first test of eslint message being published * test: add error test for eslint * feat: add eslint support to IncrementalChecker * test: remove duplicate test and up test timeout * docs: add eslint details to readme * feat: validate eslint and allow options to be passed * fix: correctly output version * feat: persist eslinter instance between runs * feat: incrementalchecker no longer recreates eslint each time * feat: share eslint between checkers * feat: share eslint logic between checkers * fix: respond to @piotr-oles PR feedback * fix: delete stale lints first * fix: addressed comments suggested by @phryneas * docs: more eslint info in the readme * fix: respond to review comments from piotr * fix: move cancellation token check
1 parent cae170b commit 544f205

35 files changed

+931
-333
lines changed

.eslintrc.json

Lines changed: 0 additions & 14 deletions
This file was deleted.

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,3 @@ package-lock.json
2525

2626
# Editor directories and files
2727
.idea
28-
.vscode

.travis.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ install:
1414
- yarn add $WEBPACK $TSLOADER $VUELOADER -D
1515

1616
script:
17-
- yarn lint
1817
- yarn test
1918

2019
env:

.vscode/settings.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"eslint.validate": [
3+
"javascript",
4+
"javascriptreact",
5+
"typescript",
6+
"typescriptreact"
7+
]
8+
}

README.md

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,35 @@
11
# Fork TS Checker Webpack Plugin
22

33
[![npm version](https://img.shields.io/npm/v/fork-ts-checker-webpack-plugin.svg)](https://www.npmjs.com/package/fork-ts-checker-webpack-plugin)
4+
45
[![npm beta version](https://img.shields.io/npm/v/fork-ts-checker-webpack-plugin/beta.svg)](https://www.npmjs.com/package/fork-ts-checker-webpack-plugin)
6+
57
[![build status](https://travis-ci.org/TypeStrong/fork-ts-checker-webpack-plugin.svg?branch=master)](https://travis-ci.org/TypeStrong/fork-ts-checker-webpack-plugin)
8+
69
[![downloads](http://img.shields.io/npm/dm/fork-ts-checker-webpack-plugin.svg)](https://npmjs.org/package/fork-ts-checker-webpack-plugin)
10+
711
[![commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
12+
813
[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
14+
915
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
1016

1117
Webpack plugin that runs TypeScript type checker on a separate process.
1218

1319
## Installation
1420

15-
This plugin requires minimum **webpack 2.3**, **TypeScript 2.1** and optionally **tslint 4.0**
21+
This plugin requires minimum **webpack 2.3**, **TypeScript 2.1** and optionally **ESLint 6.0.0** or **TSLint 4.0**
1622

1723
```sh
18-
npm install --save-dev fork-ts-checker-webpack-plugin
24+
yarn add fork-ts-checker-webpack-plugin --dev
1925
```
2026

2127
Basic webpack config (with [ts-loader](https://github.com/TypeStrong/ts-loader))
2228

2329
```js
24-
var ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
30+
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
2531

26-
var webpackConfig = {
32+
const webpackConfig = {
2733
context: __dirname, // to automatically find tsconfig.json
2834
entry: './src/index.ts',
2935
module: {
@@ -44,12 +50,12 @@ var webpackConfig = {
4450

4551
## Motivation
4652

47-
There is already similar solution - [awesome-typescript-loader](https://github.com/s-panferov/awesome-typescript-loader). You can
53+
There was already similar solution - [awesome-typescript-loader](https://github.com/s-panferov/awesome-typescript-loader). You can
4854
add `CheckerPlugin` and delegate checker to the separate process. The problem with `awesome-typescript-loader` was that, in our case,
4955
it was a lot slower than [ts-loader](https://github.com/TypeStrong/ts-loader) on an incremental build (~20s vs ~3s).
50-
Secondly, we use [tslint](https://palantir.github.io/tslint) and we wanted to run this, along with type checker, in a separate process.
51-
This is why we've created this plugin. To provide better performance, plugin reuses Abstract Syntax Trees between compilations and shares
52-
these trees with tslint. It can be scaled with a multi-process mode to utilize maximum CPU power.
56+
Secondly, we used [tslint](https://palantir.github.io/tslint) and we wanted to run this, along with type checker, in a separate process.
57+
This is why this plugin was created. To provide better performance, the plugin reuses Abstract Syntax Trees between compilations and shares
58+
these trees with TSLint. It can be scaled with a multi-process mode to utilize maximum CPU power.
5359

5460
## Modules resolution
5561

@@ -61,12 +67,47 @@ to compile files (which traverses dependency graph during compilation) - we have
6167

6268
To debug TypeScript's modules resolution, you can use `tsc --traceResolution` command.
6369

70+
## ESLint
71+
72+
[ESLint is the future of linting in the TypeScript world.](https://eslint.org/blog/2019/01/future-typescript-eslint) If you'd like to use eslint with the plugin, supply this option: `eslint: true` and ensure you have the relevant dependencies installed:
73+
74+
`yarn add eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --dev`
75+
76+
You should have an ESLint configuration file in your root project directory. Here is a sample `.eslintrc.js` configuration for a TypeScript project:
77+
78+
```js
79+
const path = require('path');
80+
module.exports = {
81+
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
82+
extends: [
83+
'plugin:@typescript-eslint/recommended' // Uses the recommended rules from the @typescript-eslint/eslint-plugin
84+
],
85+
parserOptions: {
86+
project: path.resolve(__dirname, './tsconfig.json'),
87+
tsconfigRootDir: __dirname,
88+
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
89+
sourceType: 'module', // Allows for the use of imports
90+
},
91+
rules: {
92+
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
93+
// e.g. "@typescript-eslint/explicit-function-return-type": "off",
94+
}
95+
};
96+
```
97+
98+
There's a good explanation on setting up TypeScript ESLint support by Robert Cooper [here](https://dev.to/robertcoopercode/using-eslint-and-prettier-in-a-typescript-project-53jb).
99+
64100
## TSLint
65101

102+
*[TSLint is being replaced by ESLint](https://medium.com/palantir/tslint-in-2019-1a144c2317a9).
103+
https://eslint.org/blog/2019/01/future-typescript-eslint. As a consequence, support for TSLint in fork-ts-checker-webpack-plugin will be deprecated and removed in future versions of the plugin.*
104+
66105
If you have installed [tslint](https://palantir.github.io/tslint), you can enable it by setting `tslint: true` or
67106
`tslint: './path/to/tslint.json'`. We recommend changing `defaultSeverity` to a `"warning"` in `tslint.json` file.
68107
It helps to distinguish lints from TypeScript's diagnostics.
69108

109+
110+
70111
## Options
71112

72113
- **tsconfig** `string`:
@@ -75,6 +116,14 @@ It helps to distinguish lints from TypeScript's diagnostics.
75116
- **compilerOptions** `object`:
76117
Allows overriding TypeScript options. Should be specified in the same format as you would do for the `compilerOptions` property in tsconfig.json. Default: `{}`.
77118

119+
- **eslint** `true | undefined`:
120+
121+
- If `true`, this activates eslint support.
122+
123+
- **eslintOptions** `object`:
124+
125+
- Options that can be used to initialise ESLint. See https://eslint.org/docs/1.0.0/developer-guide/nodejs-api#cliengine
126+
78127
- **tslint** `string | true | undefined`:
79128

80129
- If `string`, path to _tslint.json_ file to check source files against.

package.json

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,6 @@
6767
"git add"
6868
],
6969
"*.ts": [
70-
"tslint --fix",
71-
"prettier --write",
72-
"git add"
73-
],
74-
"*.{yml,json,md,html}": [
7570
"prettier --write",
7671
"git add"
7772
]
@@ -113,6 +108,7 @@
113108
"@commitlint/config-conventional": "^7.5.0",
114109
"@types/babel-code-frame": "^6.20.1",
115110
"@types/chokidar": "^1.7.5",
111+
"@types/eslint": "^4.16.6",
116112
"@types/jest": "^24.0.11",
117113
"@types/lodash": "^4.14.134",
118114
"@types/micromatch": "^3.1.0",
@@ -122,10 +118,12 @@
122118
"@types/rimraf": "^2.0.2",
123119
"@types/semver": "^5.5.0",
124120
"@types/webpack": "^4.4.19",
121+
"@typescript-eslint/eslint-plugin": "^1.11.0",
122+
"@typescript-eslint/parser": "^1.11.0",
125123
"commitlint": "^7.5.2",
126124
"copy-dir": "^0.4.0",
127125
"css-loader": "0.28.11",
128-
"eslint": "^5.7.0",
126+
"eslint": "^6.0.0",
129127
"git-cz": "^3.0.1",
130128
"husky": "^1.1.4",
131129
"istanbul": "^0.4.5",

src/.eslintrc.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module.exports = {
2+
parser: '@typescript-eslint/parser',
3+
plugins: ['@typescript-eslint'],
4+
parserOptions: {
5+
project: './tsconfig.json',
6+
tsconfigRootDir: __dirname,
7+
sourceType: 'module'
8+
}
9+
};

src/ApiIncrementalChecker.ts

Lines changed: 70 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
// tslint:disable-next-line:no-implicit-dependencies
22
import * as ts from 'typescript'; // Imported for types alone
33
// tslint:disable-next-line:no-implicit-dependencies
4-
import { Linter, LintResult, RuleFailure } from 'tslint';
4+
import { Linter, LintResult, RuleFailure } from 'tslint'; // Imported for types alone
5+
// tslint:disable-next-line:no-implicit-dependencies
6+
import * as eslinttypes from 'eslint'; // Imported for types alone
57
import * as minimatch from 'minimatch';
68
import * as path from 'path';
7-
import { IncrementalCheckerInterface } from './IncrementalCheckerInterface';
9+
import {
10+
IncrementalCheckerInterface,
11+
ApiIncrementalCheckerParams
12+
} from './IncrementalCheckerInterface';
813
import { CancellationToken } from './CancellationToken';
914
import {
1015
ConfigurationFile,
@@ -13,8 +18,8 @@ import {
1318
} from './linterConfigHelpers';
1419
import { NormalizedMessage } from './NormalizedMessage';
1520
import { CompilerHost } from './CompilerHost';
16-
import { ResolveModuleName, ResolveTypeReferenceDirective } from './resolution';
17-
import { FsHelper } from './FsHelper';
21+
import { fileExistsSync } from './FsHelper';
22+
import { createEslinter } from './createEslinter';
1823

1924
export class ApiIncrementalChecker implements IncrementalCheckerInterface {
2025
private linterConfig?: ConfigurationFile;
@@ -24,28 +29,47 @@ export class ApiIncrementalChecker implements IncrementalCheckerInterface {
2429
private linterExclusions: minimatch.IMinimatch[] = [];
2530

2631
private currentLintErrors = new Map<string, LintResult>();
32+
private currentEsLintErrors = new Map<
33+
string,
34+
eslinttypes.CLIEngine.LintReport
35+
>();
2736
private lastUpdatedFiles: string[] = [];
2837
private lastRemovedFiles: string[] = [];
2938

3039
private readonly hasFixedConfig: boolean;
3140

32-
constructor(
33-
typescript: typeof ts,
34-
private createNormalizedMessageFromDiagnostic: (
35-
diagnostic: ts.Diagnostic
36-
) => NormalizedMessage,
37-
private createNormalizedMessageFromRuleFailure: (
38-
ruleFailure: RuleFailure
39-
) => NormalizedMessage,
40-
programConfigFile: string,
41-
compilerOptions: ts.CompilerOptions,
42-
private context: string,
43-
private linterConfigFile: string | boolean,
44-
private linterAutoFix: boolean,
45-
checkSyntacticErrors: boolean,
46-
resolveModuleName: ResolveModuleName | undefined,
47-
resolveTypeReferenceDirective: ResolveTypeReferenceDirective | undefined
48-
) {
41+
private readonly context: string;
42+
private readonly createNormalizedMessageFromDiagnostic: (
43+
diagnostic: ts.Diagnostic
44+
) => NormalizedMessage;
45+
private readonly linterConfigFile: string | boolean;
46+
private readonly linterAutoFix: boolean;
47+
private readonly createNormalizedMessageFromRuleFailure: (
48+
ruleFailure: RuleFailure
49+
) => NormalizedMessage;
50+
private readonly eslinter: ReturnType<typeof createEslinter> | undefined;
51+
52+
constructor({
53+
typescript,
54+
context,
55+
programConfigFile,
56+
compilerOptions,
57+
createNormalizedMessageFromDiagnostic,
58+
linterConfigFile,
59+
linterAutoFix,
60+
createNormalizedMessageFromRuleFailure,
61+
eslinter,
62+
checkSyntacticErrors = false,
63+
resolveModuleName,
64+
resolveTypeReferenceDirective
65+
}: ApiIncrementalCheckerParams) {
66+
this.context = context;
67+
this.createNormalizedMessageFromDiagnostic = createNormalizedMessageFromDiagnostic;
68+
this.linterConfigFile = linterConfigFile;
69+
this.linterAutoFix = linterAutoFix;
70+
this.createNormalizedMessageFromRuleFailure = createNormalizedMessageFromRuleFailure;
71+
this.eslinter = eslinter;
72+
4973
this.hasFixedConfig = typeof this.linterConfigFile === 'string';
5074

5175
this.initLinterConfig();
@@ -97,6 +121,10 @@ export class ApiIncrementalChecker implements IncrementalCheckerInterface {
97121
return !!this.linterConfigFile;
98122
}
99123

124+
public hasEsLinter(): boolean {
125+
return this.eslinter !== undefined;
126+
}
127+
100128
public isFileExcluded(filePath: string): boolean {
101129
return (
102130
filePath.endsWith('.d.ts') ||
@@ -140,7 +168,7 @@ export class ApiIncrementalChecker implements IncrementalCheckerInterface {
140168
this.currentLintErrors.set(updatedFile, lints);
141169
} catch (e) {
142170
if (
143-
FsHelper.existsSync(updatedFile) &&
171+
fileExistsSync(updatedFile) &&
144172
// check the error type due to file system lag
145173
!(e instanceof Error) &&
146174
!(e.constructor.name === 'FatalError') &&
@@ -165,4 +193,24 @@ export class ApiIncrementalChecker implements IncrementalCheckerInterface {
165193
allLints.map(this.createNormalizedMessageFromRuleFailure)
166194
);
167195
}
196+
197+
public getEsLints(cancellationToken: CancellationToken) {
198+
for (const removedFile of this.lastRemovedFiles) {
199+
this.currentEsLintErrors.delete(removedFile);
200+
}
201+
202+
for (const updatedFile of this.lastUpdatedFiles) {
203+
cancellationToken.throwIfCancellationRequested();
204+
if (this.isFileExcluded(updatedFile)) {
205+
continue;
206+
}
207+
208+
const lints = this.eslinter!.getLints(updatedFile);
209+
if (lints !== undefined) {
210+
this.currentEsLintErrors.set(updatedFile, lints);
211+
}
212+
}
213+
214+
return this.eslinter!.getFormattedLints(this.currentEsLintErrors.values());
215+
}
168216
}

src/CancellationToken.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as path from 'path';
55
// tslint:disable-next-line:no-implicit-dependencies
66
import * as ts from 'typescript'; // Imported for types alone
77

8-
import { FsHelper } from './FsHelper';
8+
import { fileExistsSync } from './FsHelper';
99

1010
export interface CancellationTokenData {
1111
isCancelled: boolean;
@@ -60,7 +60,7 @@ export class CancellationToken {
6060
if (duration > 10) {
6161
// check no more than once every 10ms
6262
this.lastCancellationCheckTime = time;
63-
this.isCancelled = FsHelper.existsSync(this.getCancellationFilePath());
63+
this.isCancelled = fileExistsSync(this.getCancellationFilePath());
6464
}
6565

6666
return this.isCancelled;
@@ -78,10 +78,7 @@ export class CancellationToken {
7878
}
7979

8080
public cleanupCancellation() {
81-
if (
82-
this.isCancelled &&
83-
FsHelper.existsSync(this.getCancellationFilePath())
84-
) {
81+
if (this.isCancelled && fileExistsSync(this.getCancellationFilePath())) {
8582
fs.unlinkSync(this.getCancellationFilePath());
8683
this.isCancelled = false;
8784
}

src/FilesRegister.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
import * as ts from 'typescript'; // import for types alone
33
// tslint:disable-next-line:no-implicit-dependencies
44
import { RuleFailure } from 'tslint'; // import for types alone
5+
import { CLIEngine } from 'eslint'; // import for types alone
56

67
export interface DataShape {
78
source?: ts.SourceFile;
89
linted: boolean;
9-
lints: RuleFailure[];
10+
tslints: RuleFailure[];
11+
eslints: CLIEngine.LintReport[];
1012
}
1113

1214
export class FilesRegister {

0 commit comments

Comments
 (0)