Skip to content

Commit 944e0c9

Browse files
authored
feat: improve overall typescript performance (#435)
Aside from general performance improvements, I added a `profile` option to be able to measure timings and compare them with other tools, like tsc. BREAKING CHANGE: 🧨 Changed default TypeScript compiler options to incremental: true and the default mode to write-tsbuildinfo
1 parent a430979 commit 944e0c9

13 files changed

+297
-34
lines changed

README.md

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -153,17 +153,18 @@ Options passed to the plugin constructor will overwrite options from the cosmico
153153

154154
Options for the TypeScript checker (`typescript` option object).
155155

156-
| Name | Type | Default value | Description |
157-
| -------------------- | --------- | ------------------------------------------------------------------------- | ----------- |
158-
| `enabled` | `boolean` | `true` | If `true`, it enables TypeScript checker. |
159-
| `memoryLimit` | `number` | `2048` | Memory limit for the checker process in MB. If the process exits with the allocation failed error, try to increase this number. |
160-
| `tsconfig` | `string` | `'tsconfig.json'` | Path to the `tsconfig.json` file (path relative to the `compiler.options.context` or absolute path) |
161-
| `context` | `string` | `dirname(configuration.tsconfig)` | The base path for finding files specified in the `tsconfig.json`. Same as the `context` option from the [ts-loader](https://github.com/TypeStrong/ts-loader#context). Useful if you want to keep your `tsconfig.json` in an external package. Keep in mind that **not** having a `tsconfig.json` in your project root can cause different behaviour between `fork-ts-checker-webpack-plugin` and `tsc`. When using editors like `VS Code` it is advised to add a `tsconfig.json` file to the root of the project and extend the config file referenced in option `tsconfig`. |
162-
| `build` | `boolean` | `false` | The equivalent of the `--build` flag for the `tsc` command. To enable `incremental` mode, set it in the `tsconfig.json` file. |
163-
| `mode` | `'readonly'` or `'write-tsbuildinfo'` or `'write-references'` | `'readonly'` | If you use the `babel-loader`, it's recommended to use `write-references` mode to improve initial compilation time. If you use `ts-loader`, it's recommended to use `readonly` mode to not overwrite filed emitted by `ts-loader`. |
164-
| `compilerOptions` | `object` | `{ skipLibCheck: true, skipDefaultLibCheck: true }` | These options will overwrite compiler options from the `tsconfig.json` file. |
165-
| `diagnosticsOptions` | `object` | `{ syntactic: false, semantic: true, declaration: false, global: false }` | Settings to select which diagnostics do we want to perform. |
166-
| `extensions` | `object` | `{}` | See [TypeScript extensions options](#typescript-extensions-options). |
156+
| Name | Type | Default value | Description |
157+
| -------------------- | --------- | -------------------------------------------------------------------------------------- | ----------- |
158+
| `enabled` | `boolean` | `true` | If `true`, it enables TypeScript checker. |
159+
| `memoryLimit` | `number` | `2048` | Memory limit for the checker process in MB. If the process exits with the allocation failed error, try to increase this number. |
160+
| `tsconfig` | `string` | `'tsconfig.json'` | Path to the `tsconfig.json` file (path relative to the `compiler.options.context` or absolute path) |
161+
| `context` | `string` | `dirname(configuration.tsconfig)` | The base path for finding files specified in the `tsconfig.json`. Same as the `context` option from the [ts-loader](https://github.com/TypeStrong/ts-loader#context). Useful if you want to keep your `tsconfig.json` in an external package. Keep in mind that **not** having a `tsconfig.json` in your project root can cause different behaviour between `fork-ts-checker-webpack-plugin` and `tsc`. When using editors like `VS Code` it is advised to add a `tsconfig.json` file to the root of the project and extend the config file referenced in option `tsconfig`. |
162+
| `build` | `boolean` | `false` | The equivalent of the `--build` flag for the `tsc` command. To enable `incremental` mode, set it in the `tsconfig.json` file. |
163+
| `mode` | `'readonly'` or `'write-tsbuildinfo'` or `'write-references'` | `'write-tsbuildinfo'` | If you use the `babel-loader`, it's recommended to use `write-references` mode to improve initial compilation time. If you use `ts-loader`, it's recommended to use `write-tsbuildinfo` mode to not overwrite filed emitted by the `ts-loader`. |
164+
| `compilerOptions` | `object` | `{ skipLibCheck: true, sourceMap: false, inlineSourceMap: false, incremental: true }` | These options will overwrite compiler options from the `tsconfig.json` file. |
165+
| `diagnosticsOptions` | `object` | `{ syntactic: false, semantic: true, declaration: false, global: false }` | Settings to select which diagnostics do we want to perform. |
166+
| `extensions` | `object` | `{}` | See [TypeScript extensions options](#typescript-extensions-options). |
167+
| `profile` | `boolean` | `false` | Measures and prints timings related to the TypeScript performance. |
167168

168169
#### TypeScript extensions options
169170

src/ForkTsCheckerWebpackPluginOptions.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@
160160
"$ref": "#/definitions/TypeScriptVueExtensionOptions"
161161
}
162162
}
163+
},
164+
"profile": {
165+
"type": "boolean",
166+
"description": "Measures and prints timings related to the TypeScript performance."
163167
}
164168
}
165169
}

src/profile/Performance.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { performance } from 'perf_hooks';
2+
3+
interface Performance {
4+
enable(): void;
5+
disable(): void;
6+
mark(name: string): void;
7+
markStart(name: string): void;
8+
markEnd(name: string): void;
9+
measure(name: string, startMark?: string, endMark?: string): void;
10+
print(): void;
11+
}
12+
13+
function createPerformance(): Performance {
14+
let enabled = false;
15+
let timeOrigin: number;
16+
let marks: Map<string, number>;
17+
let measurements: Map<string, number>;
18+
19+
function enable() {
20+
enabled = true;
21+
marks = new Map();
22+
measurements = new Map();
23+
timeOrigin = performance.now();
24+
}
25+
26+
function disable() {
27+
enabled = false;
28+
}
29+
30+
function mark(name: string) {
31+
if (enabled) {
32+
marks.set(name, performance.now());
33+
}
34+
}
35+
36+
function measure(name: string, startMark?: string, endMark?: string) {
37+
if (enabled) {
38+
const start = (startMark && marks.get(startMark)) || timeOrigin;
39+
const end = (endMark && marks.get(endMark)) || performance.now();
40+
41+
measurements.set(name, (measurements.get(name) || 0) + (end - start));
42+
}
43+
}
44+
45+
function markStart(name: string) {
46+
if (enabled) {
47+
mark(`${name} start`);
48+
}
49+
}
50+
51+
function markEnd(name: string) {
52+
if (enabled) {
53+
mark(`${name} end`);
54+
measure(name, `${name} start`, `${name} end`);
55+
}
56+
}
57+
58+
function formatName(name: string, width = 0) {
59+
return `${name}:`.padEnd(width);
60+
}
61+
62+
function formatDuration(duration: number, width = 0) {
63+
return `${(duration / 1000).toFixed(2)} s`.padStart(width);
64+
}
65+
66+
function print() {
67+
if (enabled) {
68+
let nameWidth = 0;
69+
let durationWidth = 0;
70+
71+
measurements.forEach((duration, name) => {
72+
nameWidth = Math.max(nameWidth, formatName(name).length);
73+
durationWidth = Math.max(durationWidth, formatDuration(duration).length);
74+
});
75+
76+
measurements.forEach((duration, name) => {
77+
console.log(`${formatName(name, nameWidth)} ${formatDuration(duration, durationWidth)}`);
78+
});
79+
}
80+
}
81+
82+
return { enable, disable, mark, markStart, markEnd, measure, print };
83+
}
84+
85+
export { Performance, createPerformance };

src/typescript-reporter/TypeScriptReporterConfiguration.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import webpack from 'webpack';
22
import path from 'path';
3+
import * as ts from 'typescript';
4+
import semver from 'semver';
35
import { TypeScriptDiagnosticsOptions } from './TypeScriptDiagnosticsOptions';
46
import { TypeScriptReporterOptions } from './TypeScriptReporterOptions';
57
import {
@@ -20,6 +22,7 @@ interface TypeScriptReporterConfiguration {
2022
extensions: {
2123
vue: TypeScriptVueExtensionConfiguration;
2224
};
25+
profile: boolean;
2326
}
2427

2528
function createTypeScriptReporterConfiguration(
@@ -39,17 +42,29 @@ function createTypeScriptReporterConfiguration(
3942
const optionsAsObject: Exclude<TypeScriptReporterOptions, boolean> =
4043
typeof options === 'object' ? options : {};
4144

45+
const defaultCompilerOptions: Record<string, unknown> = {
46+
skipLibCheck: true,
47+
sourceMap: false,
48+
inlineSourceMap: false,
49+
};
50+
if (semver.gte(ts.version, '2.9.0')) {
51+
defaultCompilerOptions.declarationMap = false;
52+
}
53+
if (semver.gte(ts.version, '3.4.0')) {
54+
defaultCompilerOptions.incremental = true;
55+
}
56+
4257
return {
4358
enabled: options !== false,
4459
memoryLimit: 2048,
4560
build: false,
46-
mode: 'readonly',
61+
mode: 'write-tsbuildinfo',
62+
profile: false,
4763
...optionsAsObject,
4864
tsconfig: tsconfig,
4965
context: optionsAsObject.context || path.dirname(tsconfig),
5066
compilerOptions: {
51-
skipDefaultLibCheck: true,
52-
skipLibCheck: true,
67+
...defaultCompilerOptions,
5368
...(optionsAsObject.compilerOptions || {}),
5469
},
5570
extensions: {

src/typescript-reporter/TypeScriptReporterOptions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type TypeScriptReporterOptions =
1515
extensions?: {
1616
vue?: TypeScriptVueExtensionOptions;
1717
};
18+
profile?: boolean;
1819
};
1920

2021
export { TypeScriptReporterOptions };
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as ts from 'typescript';
2+
import { Performance } from '../../profile/Performance';
3+
4+
interface TypeScriptPerformance {
5+
enable(): void;
6+
disable(): void;
7+
mark(name: string): void;
8+
measure(name: string, startMark?: string, endMark?: string): void;
9+
}
10+
11+
function getTypeScriptPerformance(): TypeScriptPerformance | undefined {
12+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
13+
return (ts as any).performance;
14+
}
15+
16+
function connectTypeScriptPerformance(performance: Performance): Performance {
17+
const typeScriptPerformance = getTypeScriptPerformance();
18+
19+
if (typeScriptPerformance) {
20+
const { mark, measure } = typeScriptPerformance;
21+
const { enable, disable } = performance;
22+
23+
typeScriptPerformance.mark = (name) => {
24+
mark(name);
25+
performance.mark(name);
26+
};
27+
typeScriptPerformance.measure = (name, startMark, endMark) => {
28+
measure(name, startMark, endMark);
29+
performance.measure(name, startMark, endMark);
30+
};
31+
32+
return {
33+
...performance,
34+
enable() {
35+
enable();
36+
typeScriptPerformance.enable();
37+
},
38+
disable() {
39+
disable();
40+
typeScriptPerformance.disable();
41+
},
42+
};
43+
} else {
44+
return performance;
45+
}
46+
}
47+
48+
export { TypeScriptPerformance, connectTypeScriptPerformance };

src/typescript-reporter/reporter/ControlledTypeScriptSystem.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ function createControlledTypeScriptSystem(
5151
const realFileSystem = createRealFileSystem(caseSensitive);
5252
const passiveFileSystem = createPassiveFileSystem(caseSensitive, realFileSystem);
5353

54+
// based on the ts.ignorePaths
55+
const ignoredPaths = ['/node_modules/.', '/.git', '/.#'];
56+
5457
function createWatcher<TCallback>(
5558
watchersMap: Map<string, TCallback[]>,
5659
path: string,
@@ -88,7 +91,11 @@ function createControlledTypeScriptSystem(
8891

8992
function invokeDirectoryWatchers(path: string) {
9093
const normalizedPath = realFileSystem.normalizePath(path);
91-
let directory = dirname(normalizedPath);
94+
const directory = dirname(normalizedPath);
95+
96+
if (ignoredPaths.some((ignoredPath) => forwardSlash(normalizedPath).includes(ignoredPath))) {
97+
return;
98+
}
9299

93100
const directoryWatchers = directoryWatchersMap.get(directory);
94101
if (directoryWatchers) {
@@ -97,16 +104,17 @@ function createControlledTypeScriptSystem(
97104
);
98105
}
99106

100-
while (directory !== dirname(directory)) {
101-
const recursiveDirectoryWatchers = recursiveDirectoryWatchersMap.get(directory);
102-
if (recursiveDirectoryWatchers) {
107+
recursiveDirectoryWatchersMap.forEach((recursiveDirectoryWatchers, watchedDirectory) => {
108+
if (
109+
watchedDirectory === directory ||
110+
(directory.startsWith(watchedDirectory) &&
111+
forwardSlash(directory)[watchedDirectory.length] === '/')
112+
) {
103113
recursiveDirectoryWatchers.forEach((recursiveDirectoryWatcher) =>
104114
recursiveDirectoryWatcher(forwardSlash(normalizedPath))
105115
);
106116
}
107-
108-
directory = dirname(directory);
109-
}
117+
});
110118
}
111119

112120
function getWriteFileSystem(path: string) {
@@ -207,10 +215,10 @@ function createControlledTypeScriptSystem(
207215
invokeFileChanged(path: string) {
208216
const normalizedPath = realFileSystem.normalizePath(path);
209217

210-
invokeDirectoryWatchers(normalizedPath);
211-
212-
if (deletedFiles.get(normalizedPath)) {
218+
if (deletedFiles.get(normalizedPath) || !fileWatchersMap.has(path)) {
213219
invokeFileWatchers(path, ts.FileWatcherEventKind.Created);
220+
invokeDirectoryWatchers(normalizedPath);
221+
214222
deletedFiles.set(normalizedPath, false);
215223
} else {
216224
invokeFileWatchers(path, ts.FileWatcherEventKind.Changed);
@@ -220,8 +228,8 @@ function createControlledTypeScriptSystem(
220228
const normalizedPath = realFileSystem.normalizePath(path);
221229

222230
if (!deletedFiles.get(normalizedPath)) {
223-
invokeDirectoryWatchers(path);
224231
invokeFileWatchers(path, ts.FileWatcherEventKind.Deleted);
232+
invokeDirectoryWatchers(path);
225233

226234
deletedFiles.set(normalizedPath, true);
227235
}

src/typescript-reporter/reporter/TypeScriptConfigurationParser.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ function parseTypeScriptConfiguration(
2323
parsedConfigFile.errors.push(...customCompilerOptionsConvertResults.errors);
2424
}
2525

26-
return parsedConfigFile;
26+
return {
27+
...parsedConfigFile,
28+
options: {
29+
...parsedConfigFile.options,
30+
configFilePath: configFileName,
31+
},
32+
};
2733
}
2834

2935
export { parseTypeScriptConfiguration };

0 commit comments

Comments
 (0)