| "use strict"; |
| var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { |
| if (k2 === undefined) k2 = k; |
| Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); |
| }) : (function(o, m, k, k2) { |
| if (k2 === undefined) k2 = k; |
| o[k2] = m[k]; |
| })); |
| var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { |
| Object.defineProperty(o, "default", { enumerable: true, value: v }); |
| }) : function(o, v) { |
| o["default"] = v; |
| }); |
| var __importStar = (this && this.__importStar) || function (mod) { |
| if (mod && mod.__esModule) return mod; |
| var result = {}; |
| if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); |
| __setModuleDefault(result, mod); |
| return result; |
| }; |
| var __importDefault = (this && this.__importDefault) || function (mod) { |
| return (mod && mod.__esModule) ? mod : { "default": mod }; |
| }; |
| Object.defineProperty(exports, "__esModule", { value: true }); |
| exports.getProgramsForProjects = exports.createWatchProgram = exports.clearWatchCaches = void 0; |
| const debug_1 = __importDefault(require("debug")); |
| const fs_1 = __importDefault(require("fs")); |
| const semver_1 = __importDefault(require("semver")); |
| const ts = __importStar(require("typescript")); |
| const shared_1 = require("./shared"); |
| const log = (0, debug_1.default)('typescript-eslint:typescript-estree:createWatchProgram'); |
| /** |
| * Maps tsconfig paths to their corresponding file contents and resulting watches |
| */ |
| const knownWatchProgramMap = new Map(); |
| /** |
| * Maps file/folder paths to their set of corresponding watch callbacks |
| * There may be more than one per file/folder if a file/folder is shared between projects |
| */ |
| const fileWatchCallbackTrackingMap = new Map(); |
| const folderWatchCallbackTrackingMap = new Map(); |
| /** |
| * Stores the list of known files for each program |
| */ |
| const programFileListCache = new Map(); |
| /** |
| * Caches the last modified time of the tsconfig files |
| */ |
| const tsconfigLastModifiedTimestampCache = new Map(); |
| const parsedFilesSeenHash = new Map(); |
| /** |
| * Clear all of the parser caches. |
| * This should only be used in testing to ensure the parser is clean between tests. |
| */ |
| function clearWatchCaches() { |
| knownWatchProgramMap.clear(); |
| fileWatchCallbackTrackingMap.clear(); |
| folderWatchCallbackTrackingMap.clear(); |
| parsedFilesSeenHash.clear(); |
| programFileListCache.clear(); |
| tsconfigLastModifiedTimestampCache.clear(); |
| } |
| exports.clearWatchCaches = clearWatchCaches; |
| function saveWatchCallback(trackingMap) { |
| return (fileName, callback) => { |
| const normalizedFileName = (0, shared_1.getCanonicalFileName)(fileName); |
| const watchers = (() => { |
| let watchers = trackingMap.get(normalizedFileName); |
| if (!watchers) { |
| watchers = new Set(); |
| trackingMap.set(normalizedFileName, watchers); |
| } |
| return watchers; |
| })(); |
| watchers.add(callback); |
| return { |
| close: () => { |
| watchers.delete(callback); |
| }, |
| }; |
| }; |
| } |
| /** |
| * 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) { |
| throw new Error(ts.flattenDiagnosticMessageText(diagnostic.messageText, ts.sys.newLine)); |
| } |
| /** |
| * Hash content for compare content. |
| * @param content hashed contend |
| * @returns hashed result |
| */ |
| function createHash(content) { |
| var _a; |
| // No ts.sys in browser environments. |
| if ((_a = ts.sys) === null || _a === void 0 ? void 0 : _a.createHash) { |
| return ts.sys.createHash(content); |
| } |
| return content; |
| } |
| function updateCachedFileList(tsconfigPath, program, extra) { |
| const fileList = extra.EXPERIMENTAL_useSourceOfProjectReferenceRedirect |
| ? new Set(program.getSourceFiles().map(sf => (0, shared_1.getCanonicalFileName)(sf.fileName))) |
| : new Set(program.getRootFileNames().map(f => (0, shared_1.getCanonicalFileName)(f))); |
| programFileListCache.set(tsconfigPath, fileList); |
| return fileList; |
| } |
| /** |
| * Calculate project environments using options provided by consumer and paths from config |
| * @param code The code being linted |
| * @param filePathIn The path of the file being parsed |
| * @param extra.tsconfigRootDir The root directory for relative tsconfig paths |
| * @param extra.projects Provided tsconfig paths |
| * @returns The programs corresponding to the supplied tsconfig paths |
| */ |
| function getProgramsForProjects(code, filePathIn, extra) { |
| const filePath = (0, shared_1.getCanonicalFileName)(filePathIn); |
| const results = []; |
| // preserve reference to code and file being linted |
| currentLintOperationState.code = code; |
| currentLintOperationState.filePath = filePath; |
| // Update file version if necessary |
| const fileWatchCallbacks = fileWatchCallbackTrackingMap.get(filePath); |
| const codeHash = createHash(code); |
| if (parsedFilesSeenHash.get(filePath) !== codeHash && |
| fileWatchCallbacks && |
| fileWatchCallbacks.size > 0) { |
| fileWatchCallbacks.forEach(cb => cb(filePath, ts.FileWatcherEventKind.Changed)); |
| } |
| /* |
| * before we go into the process of attempting to find and update every program |
| * see if we know of a program that contains this file |
| */ |
| for (const [tsconfigPath, existingWatch] of knownWatchProgramMap.entries()) { |
| let fileList = programFileListCache.get(tsconfigPath); |
| let updatedProgram = null; |
| if (!fileList) { |
| updatedProgram = existingWatch.getProgram().getProgram(); |
| fileList = updateCachedFileList(tsconfigPath, updatedProgram, extra); |
| } |
| if (fileList.has(filePath)) { |
| log('Found existing program for file. %s', filePath); |
| updatedProgram = |
| updatedProgram !== null && updatedProgram !== void 0 ? updatedProgram : existingWatch.getProgram().getProgram(); |
| // sets parent pointers in source files |
| updatedProgram.getTypeChecker(); |
| return [updatedProgram]; |
| } |
| } |
| log('File did not belong to any existing programs, moving to create/update. %s', filePath); |
| /* |
| * We don't know of a program that contains the file, this means that either: |
| * - the required program hasn't been created yet, or |
| * - the file is new/renamed, and the program hasn't been updated. |
| */ |
| for (const tsconfigPath of extra.projects) { |
| const existingWatch = knownWatchProgramMap.get(tsconfigPath); |
| if (existingWatch) { |
| const updatedProgram = maybeInvalidateProgram(existingWatch, filePath, tsconfigPath); |
| if (!updatedProgram) { |
| continue; |
| } |
| // sets parent pointers in source files |
| updatedProgram.getTypeChecker(); |
| // cache and check the file list |
| const fileList = updateCachedFileList(tsconfigPath, updatedProgram, extra); |
| if (fileList.has(filePath)) { |
| log('Found updated program for file. %s', filePath); |
| // we can return early because we know this program contains the file |
| return [updatedProgram]; |
| } |
| results.push(updatedProgram); |
| continue; |
| } |
| const programWatch = createWatchProgram(tsconfigPath, extra); |
| knownWatchProgramMap.set(tsconfigPath, programWatch); |
| const program = programWatch.getProgram().getProgram(); |
| // sets parent pointers in source files |
| program.getTypeChecker(); |
| // cache and check the file list |
| const fileList = updateCachedFileList(tsconfigPath, program, extra); |
| if (fileList.has(filePath)) { |
| log('Found program for file. %s', filePath); |
| // we can return early because we know this program contains the file |
| return [program]; |
| } |
| results.push(program); |
| } |
| return results; |
| } |
| exports.getProgramsForProjects = getProgramsForProjects; |
| const isRunningNoTimeoutFix = semver_1.default.satisfies(ts.version, '>=3.9.0-beta', { |
| includePrerelease: true, |
| }); |
| function createWatchProgram(tsconfigPath, extra) { |
| log('Creating watch program for %s.', tsconfigPath); |
| // create compiler host |
| const watchCompilerHost = ts.createWatchCompilerHost(tsconfigPath, (0, shared_1.createDefaultCompilerOptionsFromExtra)(extra), ts.sys, ts.createAbstractBuilder, diagnosticReporter, |
| /*reportWatchStatus*/ () => { }); |
| if (extra.moduleResolver) { |
| watchCompilerHost.resolveModuleNames = (0, shared_1.getModuleResolver)(extra.moduleResolver).resolveModuleNames; |
| } |
| // ensure readFile reads the code being linted instead of the copy on disk |
| const oldReadFile = watchCompilerHost.readFile; |
| watchCompilerHost.readFile = (filePathIn, encoding) => { |
| const filePath = (0, shared_1.getCanonicalFileName)(filePathIn); |
| const fileContent = filePath === currentLintOperationState.filePath |
| ? currentLintOperationState.code |
| : oldReadFile(filePath, encoding); |
| if (fileContent !== undefined) { |
| parsedFilesSeenHash.set(filePath, createHash(fileContent)); |
| } |
| return fileContent; |
| }; |
| // 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]); |
| } |
| }; |
| /* |
| * From the CLI, the file watchers won't matter, as the files will be parsed once and then forgotten. |
| * When running from an IDE, these watchers will let us tell typescript about changes. |
| * |
| * ESLint IDE plugins will send us unfinished file content as the user types (before it's saved to disk). |
| * We use the file watchers to tell typescript about this latest file content. |
| * |
| * When files are created (or renamed), we won't know about them because we have no filesystem watchers attached. |
| * We use the folder watchers to tell typescript it needs to go and find new files in the project folders. |
| */ |
| watchCompilerHost.watchFile = saveWatchCallback(fileWatchCallbackTrackingMap); |
| watchCompilerHost.watchDirectory = saveWatchCallback(folderWatchCallbackTrackingMap); |
| // allow files with custom extensions to be included in program (uses internal ts api) |
| const oldOnDirectoryStructureHostCreate = watchCompilerHost.onCachedDirectoryStructureHostCreate; |
| watchCompilerHost.onCachedDirectoryStructureHostCreate = (host) => { |
| const oldReadDirectory = host.readDirectory; |
| host.readDirectory = (path, extensions, exclude, include, depth) => oldReadDirectory(path, !extensions ? undefined : extensions.concat(extra.extraFileExtensions), exclude, include, depth); |
| oldOnDirectoryStructureHostCreate(host); |
| }; |
| // This works only on 3.9 |
| watchCompilerHost.extraFileExtensions = extra.extraFileExtensions.map(extension => ({ |
| extension, |
| isMixedContent: true, |
| scriptKind: ts.ScriptKind.Deferred, |
| })); |
| watchCompilerHost.trace = log; |
| /** |
| * TODO: this needs refinement and development, but we're allowing users to opt-in to this for now for testing and feedback. |
| * See https://github.com/typescript-eslint/typescript-eslint/issues/2094 |
| */ |
| watchCompilerHost.useSourceOfProjectReferenceRedirect = () => extra.EXPERIMENTAL_useSourceOfProjectReferenceRedirect; |
| // Since we don't want to asynchronously update program we want to disable timeout methods |
| // So any changes in the program will be delayed and updated when getProgram is called on watch |
| let callback; |
| if (isRunningNoTimeoutFix) { |
| watchCompilerHost.setTimeout = undefined; |
| watchCompilerHost.clearTimeout = undefined; |
| } |
| else { |
| log('Running without timeout fix'); |
| // But because of https://github.com/microsoft/TypeScript/pull/37308 we cannot just set it to undefined |
| // instead save it and call before getProgram is called |
| watchCompilerHost.setTimeout = (cb, _ms, ...args) => { |
| callback = cb.bind(/*this*/ undefined, ...args); |
| return callback; |
| }; |
| watchCompilerHost.clearTimeout = () => { |
| callback = undefined; |
| }; |
| } |
| const watch = ts.createWatchProgram(watchCompilerHost); |
| if (!isRunningNoTimeoutFix) { |
| const originalGetProgram = watch.getProgram; |
| watch.getProgram = () => { |
| if (callback) { |
| callback(); |
| } |
| callback = undefined; |
| return originalGetProgram.call(watch); |
| }; |
| } |
| return watch; |
| } |
| exports.createWatchProgram = createWatchProgram; |
| function hasTSConfigChanged(tsconfigPath) { |
| const stat = fs_1.default.statSync(tsconfigPath); |
| const lastModifiedAt = stat.mtimeMs; |
| const cachedLastModifiedAt = tsconfigLastModifiedTimestampCache.get(tsconfigPath); |
| tsconfigLastModifiedTimestampCache.set(tsconfigPath, lastModifiedAt); |
| if (cachedLastModifiedAt === undefined) { |
| return false; |
| } |
| return Math.abs(cachedLastModifiedAt - lastModifiedAt) > Number.EPSILON; |
| } |
| function maybeInvalidateProgram(existingWatch, filePath, tsconfigPath) { |
| /* |
| * By calling watchProgram.getProgram(), it will trigger a resync of the program based on |
| * whatever new file content we've given it from our input. |
| */ |
| let updatedProgram = existingWatch.getProgram().getProgram(); |
| // In case this change causes problems in larger real world codebases |
| // Provide an escape hatch so people don't _have_ to revert to an older version |
| if (process.env.TSESTREE_NO_INVALIDATION === 'true') { |
| return updatedProgram; |
| } |
| if (hasTSConfigChanged(tsconfigPath)) { |
| /* |
| * If the stat of the tsconfig has changed, that could mean the include/exclude/files lists has changed |
| * We need to make sure typescript knows this so it can update appropriately |
| */ |
| log('tsconfig has changed - triggering program update. %s', tsconfigPath); |
| fileWatchCallbackTrackingMap |
| .get(tsconfigPath) |
| .forEach(cb => cb(tsconfigPath, ts.FileWatcherEventKind.Changed)); |
| // tsconfig change means that the file list more than likely changed, so clear the cache |
| programFileListCache.delete(tsconfigPath); |
| } |
| let sourceFile = updatedProgram.getSourceFile(filePath); |
| if (sourceFile) { |
| return updatedProgram; |
| } |
| /* |
| * Missing source file means our program's folder structure might be out of date. |
| * So we need to tell typescript it needs to update the correct folder. |
| */ |
| log('File was not found in program - triggering folder update. %s', filePath); |
| // Find the correct directory callback by climbing the folder tree |
| const currentDir = (0, shared_1.canonicalDirname)(filePath); |
| let current = null; |
| let next = currentDir; |
| let hasCallback = false; |
| while (current !== next) { |
| current = next; |
| const folderWatchCallbacks = folderWatchCallbackTrackingMap.get(current); |
| if (folderWatchCallbacks) { |
| folderWatchCallbacks.forEach(cb => { |
| if (currentDir !== current) { |
| cb(currentDir, ts.FileWatcherEventKind.Changed); |
| } |
| cb(current, ts.FileWatcherEventKind.Changed); |
| }); |
| hasCallback = true; |
| } |
| next = (0, shared_1.canonicalDirname)(current); |
| } |
| if (!hasCallback) { |
| /* |
| * No callback means the paths don't matchup - so no point returning any program |
| * this will signal to the caller to skip this program |
| */ |
| log('No callback found for file, not part of this program. %s', filePath); |
| return null; |
| } |
| // directory update means that the file list more than likely changed, so clear the cache |
| programFileListCache.delete(tsconfigPath); |
| // force the immediate resync |
| updatedProgram = existingWatch.getProgram().getProgram(); |
| sourceFile = updatedProgram.getSourceFile(filePath); |
| if (sourceFile) { |
| return updatedProgram; |
| } |
| /* |
| * At this point we're in one of two states: |
| * - The file isn't supposed to be in this program due to exclusions |
| * - The file is new, and was renamed from an old, included filename |
| * |
| * For the latter case, we need to tell typescript that the old filename is now deleted |
| */ |
| log('File was still not found in program after directory update - checking file deletions. %s', filePath); |
| const rootFilenames = updatedProgram.getRootFileNames(); |
| // use find because we only need to "delete" one file to cause typescript to do a full resync |
| const deletedFile = rootFilenames.find(file => !fs_1.default.existsSync(file)); |
| if (!deletedFile) { |
| // There are no deleted files, so it must be the former case of the file not belonging to this program |
| return null; |
| } |
| const fileWatchCallbacks = fileWatchCallbackTrackingMap.get((0, shared_1.getCanonicalFileName)(deletedFile)); |
| if (!fileWatchCallbacks) { |
| // shouldn't happen, but just in case |
| log('Could not find watch callbacks for root file. %s', deletedFile); |
| return updatedProgram; |
| } |
| log('Marking file as deleted. %s', deletedFile); |
| fileWatchCallbacks.forEach(cb => cb(deletedFile, ts.FileWatcherEventKind.Deleted)); |
| // deleted files means that the file list _has_ changed, so clear the cache |
| programFileListCache.delete(tsconfigPath); |
| updatedProgram = existingWatch.getProgram().getProgram(); |
| sourceFile = updatedProgram.getSourceFile(filePath); |
| if (sourceFile) { |
| return updatedProgram; |
| } |
| log('File was still not found in program after deletion check, assuming it is not part of this program. %s', filePath); |
| return null; |
| } |
| //# sourceMappingURL=createWatchProgram.js.map |