Skip to content

Commit 3a312ea

Browse files
0xorialjohnnyreilly
authored andcommitted
Add incremental compilation. (#198)
* extracted interface from IncrementalChecker. * got something working with TypeScript incremental compilation API. * added some linting. * got new checker passing old integration tests. * more reliable way of finding if compilation started. * keep errors from previous run. * added some tests to incremental typescript compiler. * added some vue support. * output compilation time. * add option to disable time measurement and use logger for output. * added some docs. * added some comments. * fix 'should find the same errors on multi-process mode' test * improve WorkSet typing - bye bye any * update changelog / version * name incremental api testpack * fixed useCaseSensitiveFileNames. * added tests comments. * vue tests for incremental API are actually testing inremental API now. * added tslintAutofix test. * reorganise tests * split out tests * run common tests with useTypescriptIncrementalApi both true and false * removed typescript-collections. * added should find semantic errors test * upgrade deps
1 parent 51981e7 commit 3a312ea

25 files changed

+1553
-1274
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ install:
88
- yarn add $WEBPACK $TSLOADER $VUELOADER -D
99
- yarn lint
1010
env:
11-
- WEBPACK=webpack@^5.0.0-alpha.0 TSLOADER=ts-loader@^5.0.0 VUELOADER=vue-loader@^15.2.4
11+
- WEBPACK=webpack@^5.0.0-alpha.5 TSLOADER=ts-loader@^5.0.0 VUELOADER=vue-loader@^15.2.4
1212
- WEBPACK=webpack@^4.0.0 TSLOADER=ts-loader@^5.0.0 VUELOADER=vue-loader@^15.2.4
1313
- WEBPACK=webpack@^3.10.0 TSLOADER=ts-loader@^3.4.0 VUELOADER=vue-loader@^13.5.0
1414
- WEBPACK=webpack@^2.7.0 TSLOADER=ts-loader@^3.4.0 VUELOADER=vue-loader@^13.5.0

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## v1.0.0-alpha.2
2+
3+
* [Add `useTypescriptIncrementalApi`](https://github.com/Realytics/fork-ts-checker-webpack-plugin/pull/198) (#196)
4+
15
## v1.0.0-alpha.1
26

37
* [Use object-spread instead of `Object.assign`](https://github.com/Realytics/fork-ts-checker-webpack-plugin/pull/194) (#194)

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ Options passed to formatters (currently only `codeframe` - see [available option
107107
If `true`, logger will not be used. Default: `false`.
108108

109109
* **checkSyntacticErrors** `boolean`:
110-
This option is useful if you're using ts-loader in `happyPackMode` with [HappyPack](https://github.com/amireh/happypack) or [thread-loader](https://github.com/webpack-contrib/thread-loader) to parallelise your builds. It will ensure that the plugin checks for both syntactic errors (eg `const array = [{} {}];`) and semantic errors (eg `const x: number = '1';`). By default the plugin only checks for semantic errors. This is because when ts-loader is used in `transpileOnly` mode, ts-loader will still report syntactic errors. When used in `happyPackMode` it does not. Default: `false`.
110+
This option is useful if you're using ts-loader in `happyPackMode` with [HappyPack](https://github.com/amireh/happypack) or [thread-loader](https://github.com/webpack-contrib/thread-loader) to parallelise your builds. If `true` it will ensure that the plugin checks for *both* syntactic errors (eg `const array = [{} {}];`) and semantic errors (eg `const x: number = '1';`). By default the plugin only checks for semantic errors. This is because when ts-loader is used in `transpileOnly` mode, ts-loader will still report syntactic errors. When used in `happyPackMode` it does not. Default: `false`.
111111

112112
* **memoryLimit** `number`:
113113
Memory limit for service process in MB. If service exits with allocation failed error, increase this number. Default: `2048`.
@@ -122,6 +122,15 @@ number **can increase checking time**. Default: `ForkTsCheckerWebpackPlugin.ONE_
122122
If `true`, the linter and compiler will process VueJs single-file-component (.vue) files. See the
123123
[Vue section](https://github.com/Realytics/fork-ts-checker-webpack-plugin#vue) further down for information on how to correctly setup your project.
124124

125+
* **useTypescriptIncrementalApi** `boolean`:
126+
If true, the plugin will use incremental compilation API introduced in typescript 2.7. In this mode you can only have 1
127+
worker, but if the changes in your code are small (like you normally have when you work in 'watch' mode), the compilation
128+
may be much faster, even compared to multi-threaded compilation.
129+
130+
* **measureCompilationTime** `boolean`:
131+
If true, the plugin will measure the time spent inside the compilation code. This may be useful to compare modes,
132+
especially if there are other loaders/plugins involved in the compilation.
133+
125134
### Pre-computed consts:
126135
* `ForkTsCheckerWebpackPlugin.ONE_CPU` - always use one CPU
127136
* `ForkTsCheckerWebpackPlugin.ALL_CPUS` - always use all CPUs (will increase build time)

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "fork-ts-checker-webpack-plugin",
3-
"version": "1.0.0-alpha.1",
3+
"version": "1.0.0-alpha.2",
44
"description": "Runs typescript type checker and linter on separate process.",
55
"main": "lib/index.js",
66
"types": "lib/index.d.ts",
@@ -15,7 +15,8 @@
1515
"test:watch": "mocha -R spec --watch ./test/unit",
1616
"test:coverage": "rimraf coverage && istanbul cover -root lib --include-all-sources mocha -- -R spec ./test/unit ./test/integration",
1717
"lint": "tslint --project src/tsconfig.json && eslint ./test",
18-
"lint:fix": "tslint --project src/tsconfig.json --fix && eslint ./test --fix"
18+
"lint:fix": "tslint --project src/tsconfig.json --fix && eslint ./test --fix",
19+
"watch": "tsc --version && tsc --project \"./src\" --watch"
1920
},
2021
"repository": {
2122
"url": "https://github.com/Realytics/fork-ts-checker-webpack-plugin.git",

src/ApiIncrementalChecker.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import * as ts from 'typescript';
2+
import * as minimatch from 'minimatch';
3+
import * as path from 'path';
4+
import { IncrementalCheckerInterface } from './IncrementalCheckerInterface';
5+
import { CancellationToken } from './CancellationToken';
6+
import { NormalizedMessage } from './NormalizedMessage';
7+
import { Configuration, Linter, LintResult } from 'tslint';
8+
import { CompilerHost } from './CompilerHost';
9+
import { FsHelper } from './FsHelper';
10+
11+
// Need some augmentation here - linterOptions.exclude is not (yet) part of the official
12+
// types for tslint.
13+
interface ConfigurationFile extends Configuration.IConfigurationFile {
14+
linterOptions?: {
15+
typeCheck?: boolean;
16+
exclude?: string[];
17+
};
18+
}
19+
20+
export class ApiIncrementalChecker implements IncrementalCheckerInterface {
21+
private linterConfig?: ConfigurationFile;
22+
23+
private readonly tsIncrementalCompiler: CompilerHost;
24+
private linterExclusions: minimatch.IMinimatch[] = [];
25+
26+
private currentLintErrors = new Map<string, LintResult>();
27+
private lastUpdatedFiles: string[] = [];
28+
private lastRemovedFiles: string[] = [];
29+
30+
constructor(
31+
programConfigFile: string,
32+
compilerOptions: ts.CompilerOptions,
33+
private linterConfigFile: string | false,
34+
private linterAutoFix: boolean,
35+
checkSyntacticErrors: boolean
36+
) {
37+
this.initLinterConfig();
38+
39+
this.tsIncrementalCompiler = new CompilerHost(
40+
programConfigFile,
41+
compilerOptions,
42+
checkSyntacticErrors
43+
);
44+
}
45+
46+
private initLinterConfig() {
47+
if (!this.linterConfig && this.linterConfigFile) {
48+
this.linterConfig = ApiIncrementalChecker.loadLinterConfig(
49+
this.linterConfigFile
50+
);
51+
52+
if (
53+
this.linterConfig.linterOptions &&
54+
this.linterConfig.linterOptions.exclude
55+
) {
56+
// Pre-build minimatch patterns to avoid additional overhead later on.
57+
// Note: Resolving the path is required to properly match against the full file paths,
58+
// and also deals with potential cross-platform problems regarding path separators.
59+
this.linterExclusions = this.linterConfig.linterOptions.exclude.map(
60+
pattern => new minimatch.Minimatch(path.resolve(pattern))
61+
);
62+
}
63+
}
64+
}
65+
66+
private static loadLinterConfig(configFile: string): ConfigurationFile {
67+
const tslint = require('tslint');
68+
69+
return tslint.Configuration.loadConfigurationFromPath(
70+
configFile
71+
) as ConfigurationFile;
72+
}
73+
74+
private createLinter(program: ts.Program): Linter {
75+
const tslint = require('tslint');
76+
77+
return new tslint.Linter({ fix: this.linterAutoFix }, program);
78+
}
79+
80+
public hasLinter(): boolean {
81+
return !!this.linterConfig;
82+
}
83+
84+
public isFileExcluded(filePath: string): boolean {
85+
return (
86+
filePath.endsWith('.d.ts') ||
87+
this.linterExclusions.some(matcher => matcher.match(filePath))
88+
);
89+
}
90+
91+
public nextIteration() {
92+
// do nothing
93+
}
94+
95+
public async getDiagnostics(_cancellationToken: CancellationToken) {
96+
const diagnostics = await this.tsIncrementalCompiler.processChanges();
97+
this.lastUpdatedFiles = diagnostics.updatedFiles;
98+
this.lastRemovedFiles = diagnostics.removedFiles;
99+
100+
return NormalizedMessage.deduplicate(
101+
diagnostics.results.map(NormalizedMessage.createFromDiagnostic)
102+
);
103+
}
104+
105+
public getLints(_cancellationToken: CancellationToken) {
106+
if (!this.linterConfig) {
107+
return [];
108+
}
109+
110+
for (const updatedFile of this.lastUpdatedFiles) {
111+
if (this.isFileExcluded(updatedFile)) {
112+
continue;
113+
}
114+
115+
try {
116+
const linter = this.createLinter(
117+
this.tsIncrementalCompiler.getProgram()
118+
);
119+
// const source = fs.readFileSync(updatedFile, 'utf-8');
120+
linter.lint(updatedFile, undefined!, this.linterConfig);
121+
const lints = linter.getResult();
122+
this.currentLintErrors.set(updatedFile, lints);
123+
} catch (e) {
124+
if (
125+
FsHelper.existsSync(updatedFile) &&
126+
// check the error type due to file system lag
127+
!(e instanceof Error) &&
128+
!(e.constructor.name === 'FatalError') &&
129+
!(e.message && e.message.trim().startsWith('Invalid source file'))
130+
) {
131+
// it's not because file doesn't exist - throw error
132+
throw e;
133+
}
134+
}
135+
136+
for (const removedFile of this.lastRemovedFiles) {
137+
this.currentLintErrors.delete(removedFile);
138+
}
139+
}
140+
141+
const allLints = [];
142+
for (const [, value] of this.currentLintErrors) {
143+
allLints.push(...value.failures);
144+
}
145+
146+
return NormalizedMessage.deduplicate(
147+
allLints.map(NormalizedMessage.createFromLint)
148+
);
149+
}
150+
}

0 commit comments

Comments
 (0)