2 var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3 if (k2 === undefined) k2 = k;
4 Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5 }) : (function(o, m, k, k2) {
6 if (k2 === undefined) k2 = k;
9 var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
10 Object.defineProperty(o, "default", { enumerable: true, value: v });
14 var __importStar = (this && this.__importStar) || function (mod) {
15 if (mod && mod.__esModule) return mod;
17 if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18 __setModuleDefault(result, mod);
21 var __importDefault = (this && this.__importDefault) || function (mod) {
22 return (mod && mod.__esModule) ? mod : { "default": mod };
24 Object.defineProperty(exports, "__esModule", { value: true });
25 exports.getProgramsForProjects = exports.createWatchProgram = exports.clearCaches = void 0;
26 const debug_1 = __importDefault(require("debug"));
27 const fs_1 = __importDefault(require("fs"));
28 const semver_1 = __importDefault(require("semver"));
29 const ts = __importStar(require("typescript"));
30 const shared_1 = require("./shared");
31 const log = debug_1.default('typescript-eslint:typescript-estree:createWatchProgram');
33 * Maps tsconfig paths to their corresponding file contents and resulting watches
35 const knownWatchProgramMap = new Map();
37 * Maps file/folder paths to their set of corresponding watch callbacks
38 * There may be more than one per file/folder if a file/folder is shared between projects
40 const fileWatchCallbackTrackingMap = new Map();
41 const folderWatchCallbackTrackingMap = new Map();
43 * Stores the list of known files for each program
45 const programFileListCache = new Map();
47 * Caches the last modified time of the tsconfig files
49 const tsconfigLastModifiedTimestampCache = new Map();
50 const parsedFilesSeenHash = new Map();
52 * Clear all of the parser caches.
53 * This should only be used in testing to ensure the parser is clean between tests.
55 function clearCaches() {
56 knownWatchProgramMap.clear();
57 fileWatchCallbackTrackingMap.clear();
58 folderWatchCallbackTrackingMap.clear();
59 parsedFilesSeenHash.clear();
60 programFileListCache.clear();
61 tsconfigLastModifiedTimestampCache.clear();
63 exports.clearCaches = clearCaches;
64 function saveWatchCallback(trackingMap) {
65 return (fileName, callback) => {
66 const normalizedFileName = shared_1.getCanonicalFileName(fileName);
67 const watchers = (() => {
68 let watchers = trackingMap.get(normalizedFileName);
71 trackingMap.set(normalizedFileName, watchers);
75 watchers.add(callback);
78 watchers.delete(callback);
84 * Holds information about the file currently being linted
86 const currentLintOperationState = {
91 * Appropriately report issues found when reading a config file
92 * @param diagnostic The diagnostic raised when creating a program
94 function diagnosticReporter(diagnostic) {
95 throw new Error(ts.flattenDiagnosticMessageText(diagnostic.messageText, ts.sys.newLine));
98 * Hash content for compare content.
99 * @param content hashed contend
100 * @returns hashed result
102 function createHash(content) {
104 // No ts.sys in browser environments.
105 if ((_a = ts.sys) === null || _a === void 0 ? void 0 : _a.createHash) {
106 return ts.sys.createHash(content);
111 * Calculate project environments using options provided by consumer and paths from config
112 * @param code The code being linted
113 * @param filePathIn The path of the file being parsed
114 * @param extra.tsconfigRootDir The root directory for relative tsconfig paths
115 * @param extra.projects Provided tsconfig paths
116 * @returns The programs corresponding to the supplied tsconfig paths
118 function getProgramsForProjects(code, filePathIn, extra) {
119 const filePath = shared_1.getCanonicalFileName(filePathIn);
121 // preserve reference to code and file being linted
122 currentLintOperationState.code = code;
123 currentLintOperationState.filePath = filePath;
124 // Update file version if necessary
125 const fileWatchCallbacks = fileWatchCallbackTrackingMap.get(filePath);
126 const codeHash = createHash(code);
127 if (parsedFilesSeenHash.get(filePath) !== codeHash &&
128 fileWatchCallbacks &&
129 fileWatchCallbacks.size > 0) {
130 fileWatchCallbacks.forEach(cb => cb(filePath, ts.FileWatcherEventKind.Changed));
133 * before we go into the process of attempting to find and update every program
134 * see if we know of a program that contains this file
136 for (const rawTsconfigPath of extra.projects) {
137 const tsconfigPath = shared_1.getTsconfigPath(rawTsconfigPath, extra);
138 const existingWatch = knownWatchProgramMap.get(tsconfigPath);
139 if (!existingWatch) {
142 let fileList = programFileListCache.get(tsconfigPath);
143 let updatedProgram = null;
145 updatedProgram = existingWatch.getProgram().getProgram();
146 fileList = new Set(updatedProgram.getRootFileNames().map(f => shared_1.getCanonicalFileName(f)));
147 programFileListCache.set(tsconfigPath, fileList);
149 if (fileList.has(filePath)) {
150 log('Found existing program for file. %s', filePath);
151 updatedProgram = updatedProgram !== null && updatedProgram !== void 0 ? updatedProgram : existingWatch.getProgram().getProgram();
152 // sets parent pointers in source files
153 updatedProgram.getTypeChecker();
154 return [updatedProgram];
157 log('File did not belong to any existing programs, moving to create/update. %s', filePath);
159 * We don't know of a program that contains the file, this means that either:
160 * - the required program hasn't been created yet, or
161 * - the file is new/renamed, and the program hasn't been updated.
163 for (const rawTsconfigPath of extra.projects) {
164 const tsconfigPath = shared_1.getTsconfigPath(rawTsconfigPath, extra);
165 const existingWatch = knownWatchProgramMap.get(tsconfigPath);
167 const updatedProgram = maybeInvalidateProgram(existingWatch, filePath, tsconfigPath);
168 if (!updatedProgram) {
171 // sets parent pointers in source files
172 updatedProgram.getTypeChecker();
173 results.push(updatedProgram);
176 const programWatch = createWatchProgram(tsconfigPath, extra);
177 const program = programWatch.getProgram().getProgram();
178 // cache watch program and return current program
179 knownWatchProgramMap.set(tsconfigPath, programWatch);
180 // sets parent pointers in source files
181 program.getTypeChecker();
182 results.push(program);
186 exports.getProgramsForProjects = getProgramsForProjects;
187 const isRunningNoTimeoutFix = semver_1.default.satisfies(ts.version, '>=3.9.0-beta', {
188 includePrerelease: true,
190 function createWatchProgram(tsconfigPath, extra) {
191 log('Creating watch program for %s.', tsconfigPath);
192 // create compiler host
193 const watchCompilerHost = ts.createWatchCompilerHost(tsconfigPath, shared_1.createDefaultCompilerOptionsFromExtra(extra), ts.sys, ts.createAbstractBuilder, diagnosticReporter,
194 /*reportWatchStatus*/ () => { });
195 // ensure readFile reads the code being linted instead of the copy on disk
196 const oldReadFile = watchCompilerHost.readFile;
197 watchCompilerHost.readFile = (filePathIn, encoding) => {
198 const filePath = shared_1.getCanonicalFileName(filePathIn);
199 const fileContent = filePath === currentLintOperationState.filePath
200 ? currentLintOperationState.code
201 : oldReadFile(filePath, encoding);
202 if (fileContent !== undefined) {
203 parsedFilesSeenHash.set(filePath, createHash(fileContent));
207 // ensure process reports error on failure instead of exiting process immediately
208 watchCompilerHost.onUnRecoverableConfigFileDiagnostic = diagnosticReporter;
209 // ensure process doesn't emit programs
210 watchCompilerHost.afterProgramCreate = (program) => {
211 // report error if there are any errors in the config file
212 const configFileDiagnostics = program
213 .getConfigFileParsingDiagnostics()
214 .filter(diag => diag.category === ts.DiagnosticCategory.Error && diag.code !== 18003);
215 if (configFileDiagnostics.length > 0) {
216 diagnosticReporter(configFileDiagnostics[0]);
220 * From the CLI, the file watchers won't matter, as the files will be parsed once and then forgotten.
221 * When running from an IDE, these watchers will let us tell typescript about changes.
223 * ESLint IDE plugins will send us unfinished file content as the user types (before it's saved to disk).
224 * We use the file watchers to tell typescript about this latest file content.
226 * When files are created (or renamed), we won't know about them because we have no filesystem watchers attached.
227 * We use the folder watchers to tell typescript it needs to go and find new files in the project folders.
229 watchCompilerHost.watchFile = saveWatchCallback(fileWatchCallbackTrackingMap);
230 watchCompilerHost.watchDirectory = saveWatchCallback(folderWatchCallbackTrackingMap);
231 // allow files with custom extensions to be included in program (uses internal ts api)
232 const oldOnDirectoryStructureHostCreate = watchCompilerHost.onCachedDirectoryStructureHostCreate;
233 watchCompilerHost.onCachedDirectoryStructureHostCreate = (host) => {
234 const oldReadDirectory = host.readDirectory;
235 host.readDirectory = (path, extensions, exclude, include, depth) => oldReadDirectory(path, !extensions ? undefined : extensions.concat(extra.extraFileExtensions), exclude, include, depth);
236 oldOnDirectoryStructureHostCreate(host);
238 // This works only on 3.9
239 watchCompilerHost.extraFileExtensions = extra.extraFileExtensions.map(extension => ({
241 isMixedContent: true,
242 scriptKind: ts.ScriptKind.Deferred,
244 watchCompilerHost.trace = log;
245 // Since we don't want to asynchronously update program we want to disable timeout methods
246 // So any changes in the program will be delayed and updated when getProgram is called on watch
248 if (isRunningNoTimeoutFix) {
249 watchCompilerHost.setTimeout = undefined;
250 watchCompilerHost.clearTimeout = undefined;
253 log('Running without timeout fix');
254 // But because of https://github.com/microsoft/TypeScript/pull/37308 we cannot just set it to undefined
255 // instead save it and call before getProgram is called
256 watchCompilerHost.setTimeout = (cb, _ms, ...args) => {
257 callback = cb.bind(/*this*/ undefined, ...args);
260 watchCompilerHost.clearTimeout = () => {
261 callback = undefined;
264 const watch = ts.createWatchProgram(watchCompilerHost);
265 if (!isRunningNoTimeoutFix) {
266 const originalGetProgram = watch.getProgram;
267 watch.getProgram = () => {
271 callback = undefined;
272 return originalGetProgram.call(watch);
277 exports.createWatchProgram = createWatchProgram;
278 function hasTSConfigChanged(tsconfigPath) {
279 const stat = fs_1.default.statSync(tsconfigPath);
280 const lastModifiedAt = stat.mtimeMs;
281 const cachedLastModifiedAt = tsconfigLastModifiedTimestampCache.get(tsconfigPath);
282 tsconfigLastModifiedTimestampCache.set(tsconfigPath, lastModifiedAt);
283 if (cachedLastModifiedAt === undefined) {
286 return Math.abs(cachedLastModifiedAt - lastModifiedAt) > Number.EPSILON;
288 function maybeInvalidateProgram(existingWatch, filePath, tsconfigPath) {
290 * By calling watchProgram.getProgram(), it will trigger a resync of the program based on
291 * whatever new file content we've given it from our input.
293 let updatedProgram = existingWatch.getProgram().getProgram();
294 // In case this change causes problems in larger real world codebases
295 // Provide an escape hatch so people don't _have_ to revert to an older version
296 if (process.env.TSESTREE_NO_INVALIDATION === 'true') {
297 return updatedProgram;
299 if (hasTSConfigChanged(tsconfigPath)) {
301 * If the stat of the tsconfig has changed, that could mean the include/exclude/files lists has changed
302 * We need to make sure typescript knows this so it can update appropriately
304 log('tsconfig has changed - triggering program update. %s', tsconfigPath);
305 fileWatchCallbackTrackingMap
307 .forEach(cb => cb(tsconfigPath, ts.FileWatcherEventKind.Changed));
308 // tsconfig change means that the file list more than likely changed, so clear the cache
309 programFileListCache.delete(tsconfigPath);
311 let sourceFile = updatedProgram.getSourceFile(filePath);
313 return updatedProgram;
316 * Missing source file means our program's folder structure might be out of date.
317 * So we need to tell typescript it needs to update the correct folder.
319 log('File was not found in program - triggering folder update. %s', filePath);
320 // Find the correct directory callback by climbing the folder tree
321 const currentDir = shared_1.canonicalDirname(filePath);
323 let next = currentDir;
324 let hasCallback = false;
325 while (current !== next) {
327 const folderWatchCallbacks = folderWatchCallbackTrackingMap.get(current);
328 if (folderWatchCallbacks) {
329 folderWatchCallbacks.forEach(cb => {
330 if (currentDir !== current) {
331 cb(currentDir, ts.FileWatcherEventKind.Changed);
333 cb(current, ts.FileWatcherEventKind.Changed);
337 next = shared_1.canonicalDirname(current);
341 * No callback means the paths don't matchup - so no point returning any program
342 * this will signal to the caller to skip this program
344 log('No callback found for file, not part of this program. %s', filePath);
347 // directory update means that the file list more than likely changed, so clear the cache
348 programFileListCache.delete(tsconfigPath);
349 // force the immediate resync
350 updatedProgram = existingWatch.getProgram().getProgram();
351 sourceFile = updatedProgram.getSourceFile(filePath);
353 return updatedProgram;
356 * At this point we're in one of two states:
357 * - The file isn't supposed to be in this program due to exclusions
358 * - The file is new, and was renamed from an old, included filename
360 * For the latter case, we need to tell typescript that the old filename is now deleted
362 log('File was still not found in program after directory update - checking file deletions. %s', filePath);
363 const rootFilenames = updatedProgram.getRootFileNames();
364 // use find because we only need to "delete" one file to cause typescript to do a full resync
365 const deletedFile = rootFilenames.find(file => !fs_1.default.existsSync(file));
367 // There are no deleted files, so it must be the former case of the file not belonging to this program
370 const fileWatchCallbacks = fileWatchCallbackTrackingMap.get(shared_1.getCanonicalFileName(deletedFile));
371 if (!fileWatchCallbacks) {
372 // shouldn't happen, but just in case
373 log('Could not find watch callbacks for root file. %s', deletedFile);
374 return updatedProgram;
376 log('Marking file as deleted. %s', deletedFile);
377 fileWatchCallbacks.forEach(cb => cb(deletedFile, ts.FileWatcherEventKind.Deleted));
378 // deleted files means that the file list _has_ changed, so clear the cache
379 programFileListCache.delete(tsconfigPath);
380 updatedProgram = existingWatch.getProgram().getProgram();
381 sourceFile = updatedProgram.getSourceFile(filePath);
383 return updatedProgram;
385 log('File was still not found in program after deletion check, assuming it is not part of this program. %s', filePath);
388 //# sourceMappingURL=createWatchProgram.js.map