/
tsconfig-parser.ts
221 lines (190 loc) · 7.2 KB
/
tsconfig-parser.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
import path from 'path';
import ts from 'typescript';
import { Extra } from './parser-options';
//------------------------------------------------------------------------------
// Environment calculation
//------------------------------------------------------------------------------
/**
* Default compiler options for program generation from single root file
*/
const defaultCompilerOptions: ts.CompilerOptions = {
allowNonTsExtensions: true,
allowJs: true,
};
/**
* Maps tsconfig paths to their corresponding file contents and resulting watches
*/
const knownWatchProgramMap = new Map<
string,
ts.WatchOfConfigFile<ts.SemanticDiagnosticsBuilderProgram>
>();
/**
* Maps file paths to their set of corresponding watch callbacks
* There may be more than one per file if a file is shared between projects
*/
const watchCallbackTrackingMap = new Map<string, ts.FileWatcherCallback>();
const parsedFilesSeen = new Set<string>();
/**
* Holds information about the file currently being linted
*/
const currentLintOperationState = {
code: '',
filePath: '',
};
/**
* Appropriately report issues found when reading a config file
* @param diagnostic The diagnostic raised when creating a program
*/
function diagnosticReporter(diagnostic: ts.Diagnostic): void {
throw new Error(
ts.flattenDiagnosticMessageText(diagnostic.messageText, ts.sys.newLine),
);
}
const noopFileWatcher = { close: () => {} };
/**
* Calculate project environments using options provided by consumer and paths from config
* @param code The code being linted
* @param filePath The path of the file being parsed
* @param extra.tsconfigRootDir The root directory for relative tsconfig paths
* @param extra.project Provided tsconfig paths
* @returns The programs corresponding to the supplied tsconfig paths
*/
export function calculateProjectParserOptions(
code: string,
filePath: string,
extra: Extra,
): ts.Program[] {
const results = [];
const tsconfigRootDir = extra.tsconfigRootDir;
// preserve reference to code and file being linted
currentLintOperationState.code = code;
currentLintOperationState.filePath = filePath;
// Update file version if necessary
// TODO: only update when necessary, currently marks as changed on every lint
const watchCallback = watchCallbackTrackingMap.get(filePath);
if (parsedFilesSeen.has(filePath) && typeof watchCallback !== 'undefined') {
watchCallback(filePath, ts.FileWatcherEventKind.Changed);
}
for (let tsconfigPath of extra.projects) {
// if absolute paths aren't provided, make relative to tsconfigRootDir
if (!path.isAbsolute(tsconfigPath)) {
tsconfigPath = path.join(tsconfigRootDir, tsconfigPath);
}
const existingWatch = knownWatchProgramMap.get(tsconfigPath);
if (typeof existingWatch !== 'undefined') {
// get new program (updated if necessary)
results.push(existingWatch.getProgram().getProgram());
continue;
}
// create compiler host
const watchCompilerHost = ts.createWatchCompilerHost(
tsconfigPath,
/*optionsToExtend*/ { allowNonTsExtensions: true } as ts.CompilerOptions,
ts.sys,
ts.createSemanticDiagnosticsBuilderProgram,
diagnosticReporter,
/*reportWatchStatus*/ () => {},
);
// ensure readFile reads the code being linted instead of the copy on disk
const oldReadFile = watchCompilerHost.readFile;
watchCompilerHost.readFile = (filePath, encoding) =>
path.normalize(filePath) ===
path.normalize(currentLintOperationState.filePath)
? currentLintOperationState.code
: oldReadFile(filePath, encoding);
// ensure process reports error on failure instead of exiting process immediately
watchCompilerHost.onUnRecoverableConfigFileDiagnostic = diagnosticReporter;
// ensure process doesn't emit programs
watchCompilerHost.afterProgramCreate = program => {
// report error if there are any errors in the config file
const configFileDiagnostics = program
.getConfigFileParsingDiagnostics()
.filter(
diag =>
diag.category === ts.DiagnosticCategory.Error &&
diag.code !== 18003,
);
if (configFileDiagnostics.length > 0) {
diagnosticReporter(configFileDiagnostics[0]);
}
};
// register callbacks to trigger program updates without using fileWatchers
watchCompilerHost.watchFile = (fileName, callback) => {
const normalizedFileName = path.normalize(fileName);
watchCallbackTrackingMap.set(normalizedFileName, callback);
return {
close: () => {
watchCallbackTrackingMap.delete(normalizedFileName);
},
};
};
// ensure fileWatchers aren't created for directories
watchCompilerHost.watchDirectory = () => noopFileWatcher;
// allow files with custom extensions to be included in program (uses internal ts api)
const oldOnDirectoryStructureHostCreate = (watchCompilerHost as any)
.onCachedDirectoryStructureHostCreate;
(watchCompilerHost as any).onCachedDirectoryStructureHostCreate = (
host: any,
) => {
const oldReadDirectory = host.readDirectory;
host.readDirectory = (
path: string,
extensions?: ReadonlyArray<string>,
exclude?: ReadonlyArray<string>,
include?: ReadonlyArray<string>,
depth?: number,
) =>
oldReadDirectory(
path,
!extensions
? undefined
: extensions.concat(extra.extraFileExtensions),
exclude,
include,
depth,
);
oldOnDirectoryStructureHostCreate(host);
};
// create program
const programWatch = ts.createWatchProgram(watchCompilerHost);
const program = programWatch.getProgram().getProgram();
// cache watch program and return current program
knownWatchProgramMap.set(tsconfigPath, programWatch);
results.push(program);
}
parsedFilesSeen.add(filePath);
return results;
}
/**
* Create program from single root file. Requires a single tsconfig to be specified.
* @param code The code being linted
* @param filePath The file being linted
* @param extra.tsconfigRootDir The root directory for relative tsconfig paths
* @param extra.project Provided tsconfig paths
* @returns The program containing just the file being linted and associated library files
*/
export function createProgram(code: string, filePath: string, extra: Extra) {
if (!extra.projects || extra.projects.length !== 1) {
return undefined;
}
let tsconfigPath = extra.projects[0];
// if absolute paths aren't provided, make relative to tsconfigRootDir
if (!path.isAbsolute(tsconfigPath)) {
tsconfigPath = path.join(extra.tsconfigRootDir, tsconfigPath);
}
const commandLine = ts.getParsedCommandLineOfConfigFile(
tsconfigPath,
defaultCompilerOptions,
{ ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} },
);
if (!commandLine) {
return undefined;
}
const compilerHost = ts.createCompilerHost(commandLine.options, true);
const oldReadFile = compilerHost.readFile;
compilerHost.readFile = (fileName: string) =>
path.normalize(fileName) === path.normalize(filePath)
? code
: oldReadFile(fileName);
return ts.createProgram([filePath], commandLine.options, compilerHost);
}